Skip to content
Draft

wip #922

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ The backend to which to write logs can be specified with the `--logs.write.endpo

```txt mdox-exec="./observatorium-api -h"
Usage of ./observatorium-api:
-alerting.alertmanager.endpoint string
The endpoint against which to make requests for alerts and silences
-debug.block-profile-rate int
The percentage of goroutine blocking events that are reported in the blocking profile. (default 10)
-debug.mutex-profile-fraction int
Expand Down Expand Up @@ -106,8 +108,6 @@ Usage of ./observatorium-api:
The HTTP write timeout for proxied requests to the logs endpoint. (default 10m0s)
-logs.write.endpoint string
The endpoint against which to make write requests for logs.
-metrics.alertmanager.endpoint string
The endpoint against which to make requests for alerts and silences
-metrics.read.endpoint string
The endpoint against which to send read requests for metrics.
-metrics.rules.endpoint string
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,20 +127,20 @@ func TestWithEnforceTenancyOnSilenceID(t *testing.T) {
r.Use(authentication.WithTenantID(map[string]string{tenantName: tenantID}))
r.With(WithEnforceTenancyOnSilenceID(label, upstreamURL, srv.Client().Transport)).Method(
http.MethodGet,
"/{tenant}/am/api/v2/silence/{silenceID}",
"/{tenant}/api/v2/silence/{silenceID}",
next,
)
r.With(WithEnforceTenancyOnSilenceID(label, upstreamURL, srv.Client().Transport)).Method(
http.MethodDelete,
"/{tenant}/am/api/v2/silence/{silenceID}",
"/{tenant}/api/v2/silence/{silenceID}",
next,
)

return r
}

newRequest := func(method string) *http.Request {
req := httptest.NewRequest(method, "/"+tenantName+"/am/api/v2/silence/"+silID, nil)
req := httptest.NewRequest(method, "/"+tenantName+"/api/v2/silence/"+silID, nil)
rctx := chi.NewRouteContext()
rctx.URLParams.Add("tenant", tenantName)
rctx.URLParams.Add("silenceID", silID)
Expand Down
252 changes: 252 additions & 0 deletions api/alerting/v1/http.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
package v1

import (
"net"
"net/http"
"net/http/httputil"
"net/url"
"time"

"github.com/go-chi/chi/v5"
"github.com/go-kit/log"
"github.com/prometheus/client_golang/prometheus"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"

"github.com/observatorium/api/proxy"
"github.com/observatorium/api/server"
"github.com/observatorium/api/tls"
"github.com/observatorium/api/tracing"
)

const (
dialTimeout = 30 * time.Second // Set as in http.DefaultTransport
)

const (
AlertsRoute = "/api/v2/alerts"
SilencesRoute = "/api/v2/silences"
SilenceRoute = "/api/v2/silence/{silenceID}"
)

type handlerConfiguration struct {
logger log.Logger
registry *prometheus.Registry
instrument handlerInstrumenter
tenantLabel string
alertsReadMiddlewares []func(http.Handler) http.Handler
silenceReadMiddlewares []func(http.Handler) http.Handler
silenceWriteMiddlewares []func(http.Handler) http.Handler
silenceIDReadMiddlewares []func(http.Handler) http.Handler
silenceIDWriteMiddlewares []func(http.Handler) http.Handler
}

// HandlerOption modifies the handler's configuration.
type HandlerOption func(h *handlerConfiguration)

// WithLogger add a custom logger for the handler to use.
func WithLogger(logger log.Logger) HandlerOption {
return func(h *handlerConfiguration) {
h.logger = logger
}
}

// WithRegistry adds a custom Prometheus registry for the handler to use.
func WithRegistry(r *prometheus.Registry) HandlerOption {
return func(h *handlerConfiguration) {
h.registry = r
}
}

// WithHandlerInstrumenter adds a custom HTTP handler instrument middleware for the handler to use.
func WithHandlerInstrumenter(instrumenter handlerInstrumenter) HandlerOption {
return func(h *handlerConfiguration) {
h.instrument = instrumenter
}
}

// WithTenantLabel adds tenant label for the handler to use.
func WithTenantLabel(tenantLabel string) HandlerOption {
return func(h *handlerConfiguration) {
h.tenantLabel = tenantLabel
}
}

// WithAlertsReadMiddleware adds a middleware for the read alerts operation.
func WithAlertsReadMiddleware(m ...func(http.Handler) http.Handler) HandlerOption {
return func(h *handlerConfiguration) {
h.alertsReadMiddlewares = append(h.alertsReadMiddlewares, m...)
}
}

// WithSilenceReadMiddleware adds a middleware for the read silences operation.
func WithSilenceReadMiddleware(m ...func(http.Handler) http.Handler) HandlerOption {
return func(h *handlerConfiguration) {
h.silenceReadMiddlewares = append(h.silenceReadMiddlewares, m...)
}
}

// WithSilenceWriteMiddleware adds a middleware for the create silence operation.
func WithSilenceWriteMiddleware(m ...func(http.Handler) http.Handler) HandlerOption {
return func(h *handlerConfiguration) {
h.silenceWriteMiddlewares = append(h.silenceWriteMiddlewares, m...)
}
}

// WithSilenceIDReadMiddleware adds a middleware for the read silence by ID operation.
func WithSilenceIDReadMiddleware(m ...func(http.Handler) http.Handler) HandlerOption {
return func(h *handlerConfiguration) {
h.silenceIDReadMiddlewares = append(h.silenceIDReadMiddlewares, m...)
}
}

// WithSilenceIDWriteMiddleware adds a middleware for the delete silence by ID operation.
func WithSilenceIDWriteMiddleware(m ...func(http.Handler) http.Handler) HandlerOption {
return func(h *handlerConfiguration) {
h.silenceIDWriteMiddlewares = append(h.silenceIDWriteMiddlewares, m...)
}
}

// WithGlobalMiddleware adds a middleware for all operations.
func WithGlobalMiddleware(m ...func(http.Handler) http.Handler) HandlerOption {
return func(h *handlerConfiguration) {
h.alertsReadMiddlewares = append(h.alertsReadMiddlewares, m...)
h.silenceReadMiddlewares = append(h.silenceReadMiddlewares, m...)
h.silenceWriteMiddlewares = append(h.silenceWriteMiddlewares, m...)
h.silenceIDReadMiddlewares = append(h.silenceIDReadMiddlewares, m...)
h.silenceIDWriteMiddlewares = append(h.silenceIDWriteMiddlewares, m...)
}
}

type handlerInstrumenter interface {
NewHandler(labels prometheus.Labels, handler http.Handler) http.HandlerFunc
}

type nopInstrumentHandler struct{}

func (n nopInstrumentHandler) NewHandler(_ prometheus.Labels, handler http.Handler) http.HandlerFunc {
return handler.ServeHTTP
}

// NewHandler creates the new alerting v1 handler, proxying a subset of the
// Alertmanager v2 API (active alerts and silences).
func NewHandler(endpoint *url.URL, tlsOptions *tls.UpstreamOptions, opts ...HandlerOption) http.Handler {
c := &handlerConfiguration{
logger: log.NewNopLogger(),
registry: prometheus.NewRegistry(),
instrument: nopInstrumentHandler{},
}

for _, o := range opts {
o(c)
}

r := chi.NewRouter()
r.Use(tracing.WithChiRoutePattern)
r.Use(func(handler http.Handler) http.Handler {
return c.instrument.NewHandler(nil, handler)
})

if endpoint == nil {
return r
}

alertmanagerTransport := &http.Transport{
DialContext: (&net.Dialer{
Timeout: dialTimeout,
}).DialContext,
TLSClientConfig: tlsOptions.NewClientConfig(),
}

var proxyAlertmanager http.Handler
{
middlewares := proxy.Middlewares(
proxy.MiddlewareSetUpstream(endpoint),
proxy.MiddlewareSetPrefixHeader(),
proxy.MiddlewareLogger(c.logger),
proxy.MiddlewareMetrics(c.registry, prometheus.Labels{"proxy": "alertingv1-alertmanager"}),
)

proxyAlertmanager = &httputil.ReverseProxy{
Director: middlewares,
ErrorLog: proxy.Logger(c.logger),
Transport: otelhttp.NewTransport(alertmanagerTransport),
}
}

r.Group(func(r chi.Router) {
r.Use(func(handler http.Handler) http.Handler {
return server.InjectLabelsCtx(
prometheus.Labels{"group": "alertingv1", "handler": "alerts"},
handler,
)
})
r.Use(c.alertsReadMiddlewares...)
r.Use(server.StripTenantPrefix("/api/alerting/v1"))

r.Method(http.MethodGet, AlertsRoute, proxyAlertmanager)
})

r.Group(func(r chi.Router) {
r.Use(func(handler http.Handler) http.Handler {
return server.InjectLabelsCtx(
prometheus.Labels{"group": "alertingv1", "handler": "silences"},
handler,
)
})
r.Use(c.silenceReadMiddlewares...)
r.Use(server.StripTenantPrefix("/api/alerting/v1"))

r.Method(http.MethodGet, SilencesRoute, proxyAlertmanager)
})

r.Group(func(r chi.Router) {
r.Use(func(handler http.Handler) http.Handler {
return server.InjectLabelsCtx(
prometheus.Labels{"group": "alertingv1", "handler": "silences"},
handler,
)
})
r.Use(c.silenceWriteMiddlewares...)
r.Use(WithEnforceTenancyOnSilenceMatchers(c.tenantLabel))
r.Use(server.StripTenantPrefix("/api/alerting/v1"))

r.Method(http.MethodPost, SilencesRoute, proxyAlertmanager)
})

alertmanagerSilenceTransport := otelhttp.NewTransport(alertmanagerTransport)
enforceTenancyOnSilenceID := WithEnforceTenancyOnSilenceID(
c.tenantLabel,
endpoint,
alertmanagerSilenceTransport,
)

r.Group(func(r chi.Router) {
r.Use(func(handler http.Handler) http.Handler {
return server.InjectLabelsCtx(
prometheus.Labels{"group": "alertingv1", "handler": "silence"},
handler,
)
})
r.Use(enforceTenancyOnSilenceID)
r.Use(c.silenceIDReadMiddlewares...)
r.Use(server.StripTenantPrefix("/api/alerting/v1"))

r.Method(http.MethodGet, SilenceRoute, proxyAlertmanager)
})

r.Group(func(r chi.Router) {
r.Use(func(handler http.Handler) http.Handler {
return server.InjectLabelsCtx(
prometheus.Labels{"group": "alertingv1", "handler": "silence"},
handler,
)
})
r.Use(enforceTenancyOnSilenceID)
r.Use(c.silenceIDWriteMiddlewares...)
r.Use(server.StripTenantPrefix("/api/alerting/v1"))

r.Method(http.MethodDelete, SilenceRoute, proxyAlertmanager)
})

return r
}
Loading