Middleware: Add team metadata to HTTP handlers (#71010)

Signed-off-by: bergquist <carl.bergquist@gmail.com>
pull/71777/head^2
Carl Bergquist 2 years ago committed by GitHub
parent 8ec4c1bdc8
commit 243b757168
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 11
      pkg/api/api.go
  2. 8
      pkg/api/http_server.go
  3. 40
      pkg/middleware/request_metadata_test.go
  4. 37
      pkg/middleware/request_metrics.go
  5. 75
      pkg/middleware/requestmeta/request_metadata.go
  6. 5
      pkg/setting/setting.go

@ -33,6 +33,7 @@ import (
"github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/middleware" "github.com/grafana/grafana/pkg/middleware"
"github.com/grafana/grafana/pkg/middleware/requestmeta"
ac "github.com/grafana/grafana/pkg/services/accesscontrol" ac "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/apikey" "github.com/grafana/grafana/pkg/services/apikey"
"github.com/grafana/grafana/pkg/services/auth" "github.com/grafana/grafana/pkg/services/auth"
@ -70,7 +71,7 @@ func (hs *HTTPServer) registerRoutes() {
// not logged in views // not logged in views
r.Get("/logout", hs.Logout) r.Get("/logout", hs.Logout)
r.Post("/login", quota(string(auth.QuotaTargetSrv)), routing.Wrap(hs.LoginPost)) r.Post("/login", requestmeta.SetOwner(requestmeta.TeamAuth), quota(string(auth.QuotaTargetSrv)), routing.Wrap(hs.LoginPost))
r.Get("/login/:name", quota(string(auth.QuotaTargetSrv)), hs.OAuthLogin) r.Get("/login/:name", quota(string(auth.QuotaTargetSrv)), hs.OAuthLogin)
r.Get("/login", hs.LoginView) r.Get("/login", hs.LoginView)
r.Get("/invite/:code", hs.Index) r.Get("/invite/:code", hs.Index)
@ -539,7 +540,7 @@ func (hs *HTTPServer) registerRoutes() {
alertsRoute.Get("/:alertId", hs.ValidateOrgAlert, routing.Wrap(hs.GetAlert)) alertsRoute.Get("/:alertId", hs.ValidateOrgAlert, routing.Wrap(hs.GetAlert))
alertsRoute.Get("/", routing.Wrap(hs.GetAlerts)) alertsRoute.Get("/", routing.Wrap(hs.GetAlerts))
alertsRoute.Get("/states-for-dashboard", routing.Wrap(hs.GetAlertStatesForDashboard)) alertsRoute.Get("/states-for-dashboard", routing.Wrap(hs.GetAlertStatesForDashboard))
}) }, requestmeta.SetOwner(requestmeta.TeamAlerting))
var notifiersAuthHandler web.Handler var notifiersAuthHandler web.Handler
if hs.Cfg.UnifiedAlerting.IsEnabled() { if hs.Cfg.UnifiedAlerting.IsEnabled() {
@ -548,7 +549,7 @@ func (hs *HTTPServer) registerRoutes() {
notifiersAuthHandler = reqEditorRole notifiersAuthHandler = reqEditorRole
} }
apiRoute.Get("/alert-notifiers", notifiersAuthHandler, routing.Wrap( apiRoute.Get("/alert-notifiers", notifiersAuthHandler, requestmeta.SetOwner(requestmeta.TeamAlerting), routing.Wrap(
hs.GetAlertNotifiers(hs.Cfg.UnifiedAlerting.IsEnabled())), hs.GetAlertNotifiers(hs.Cfg.UnifiedAlerting.IsEnabled())),
) )
@ -562,12 +563,12 @@ func (hs *HTTPServer) registerRoutes() {
alertNotifications.Get("/uid/:uid", routing.Wrap(hs.GetAlertNotificationByUID)) alertNotifications.Get("/uid/:uid", routing.Wrap(hs.GetAlertNotificationByUID))
alertNotifications.Put("/uid/:uid", routing.Wrap(hs.UpdateAlertNotificationByUID)) alertNotifications.Put("/uid/:uid", routing.Wrap(hs.UpdateAlertNotificationByUID))
alertNotifications.Delete("/uid/:uid", routing.Wrap(hs.DeleteAlertNotificationByUID)) alertNotifications.Delete("/uid/:uid", routing.Wrap(hs.DeleteAlertNotificationByUID))
}, reqEditorRole) }, reqEditorRole, requestmeta.SetOwner(requestmeta.TeamAlerting))
// alert notifications without requirement of user to be org editor // alert notifications without requirement of user to be org editor
apiRoute.Group("/alert-notifications", func(orgRoute routing.RouteRegister) { apiRoute.Group("/alert-notifications", func(orgRoute routing.RouteRegister) {
orgRoute.Get("/lookup", routing.Wrap(hs.GetAlertNotificationLookup)) orgRoute.Get("/lookup", routing.Wrap(hs.GetAlertNotificationLookup))
}) }, requestmeta.SetOwner(requestmeta.TeamAlerting))
apiRoute.Get("/annotations", authorize(ac.EvalPermission(ac.ActionAnnotationsRead)), routing.Wrap(hs.GetAnnotations)) apiRoute.Get("/annotations", authorize(ac.EvalPermission(ac.ActionAnnotationsRead)), routing.Wrap(hs.GetAnnotations))
apiRoute.Post("/annotations/mass-delete", authorize(ac.EvalPermission(ac.ActionAnnotationsDelete)), routing.Wrap(hs.MassDeleteAnnotations)) apiRoute.Post("/annotations/mass-delete", authorize(ac.EvalPermission(ac.ActionAnnotationsDelete)), routing.Wrap(hs.MassDeleteAnnotations))

@ -31,6 +31,7 @@ import (
"github.com/grafana/grafana/pkg/middleware" "github.com/grafana/grafana/pkg/middleware"
"github.com/grafana/grafana/pkg/middleware/csrf" "github.com/grafana/grafana/pkg/middleware/csrf"
"github.com/grafana/grafana/pkg/middleware/loggermw" "github.com/grafana/grafana/pkg/middleware/loggermw"
"github.com/grafana/grafana/pkg/middleware/requestmeta"
"github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/pluginscdn" "github.com/grafana/grafana/pkg/plugins/pluginscdn"
"github.com/grafana/grafana/pkg/registry/corekind" "github.com/grafana/grafana/pkg/registry/corekind"
@ -203,6 +204,7 @@ type HTTPServer struct {
statsService stats.Service statsService stats.Service
authnService authn.Service authnService authn.Service
starApi *starApi.API starApi *starApi.API
promRegister prometheus.Registerer
} }
type ServerOptions struct { type ServerOptions struct {
@ -244,7 +246,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
accesscontrolService accesscontrol.Service, navTreeService navtree.Service, accesscontrolService accesscontrol.Service, navTreeService navtree.Service,
annotationRepo annotations.Repository, tagService tag.Service, searchv2HTTPService searchV2.SearchHTTPService, oauthTokenService oauthtoken.OAuthTokenService, annotationRepo annotations.Repository, tagService tag.Service, searchv2HTTPService searchV2.SearchHTTPService, oauthTokenService oauthtoken.OAuthTokenService,
statsService stats.Service, authnService authn.Service, pluginsCDNService *pluginscdn.Service, statsService stats.Service, authnService authn.Service, pluginsCDNService *pluginscdn.Service,
starApi *starApi.API, starApi *starApi.API, promRegister prometheus.Registerer,
) (*HTTPServer, error) { ) (*HTTPServer, error) {
web.Env = cfg.Env web.Env = cfg.Env
@ -346,6 +348,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
authnService: authnService, authnService: authnService,
pluginsCDNService: pluginsCDNService, pluginsCDNService: pluginsCDNService,
starApi: starApi, starApi: starApi,
promRegister: promRegister,
} }
if hs.Listener != nil { if hs.Listener != nil {
hs.log.Debug("Using provided listener") hs.log.Debug("Using provided listener")
@ -568,8 +571,9 @@ func (hs *HTTPServer) applyRoutes() {
func (hs *HTTPServer) addMiddlewaresAndStaticRoutes() { func (hs *HTTPServer) addMiddlewaresAndStaticRoutes() {
m := hs.web m := hs.web
m.Use(requestmeta.SetupRequestMetadata())
m.Use(middleware.RequestTracing(hs.tracer)) m.Use(middleware.RequestTracing(hs.tracer))
m.Use(middleware.RequestMetrics(hs.Features)) m.Use(middleware.RequestMetrics(hs.Features, hs.Cfg, hs.promRegister))
m.UseMiddleware(hs.LoggerMiddleware.Middleware()) m.UseMiddleware(hs.LoggerMiddleware.Middleware())

@ -0,0 +1,40 @@
package middleware
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/grafana/grafana/pkg/middleware/requestmeta"
"github.com/grafana/grafana/pkg/web"
"github.com/stretchr/testify/assert"
)
func TestRequestMetaDefault(t *testing.T) {
m := web.New()
m.Use(requestmeta.SetupRequestMetadata())
m.Get("/", func(rw http.ResponseWriter, req *http.Request) {
v := requestmeta.GetRequestMetaData(req.Context())
assert.Equal(t, requestmeta.TeamCore, v.Team)
})
req, _ := http.NewRequest(http.MethodGet, "/", nil)
m.ServeHTTP(httptest.NewRecorder(), req)
}
func TestRequestMetaNewTeam(t *testing.T) {
m := web.New()
m.Use(requestmeta.SetupRequestMetadata())
m.Get("/",
requestmeta.SetOwner(requestmeta.TeamAlerting), // set new owner for this route.
func(rw http.ResponseWriter, req *http.Request) {
v := requestmeta.GetRequestMetaData(req.Context())
assert.Equal(t, requestmeta.TeamAlerting, v.Team)
})
r, err := http.NewRequest(http.MethodGet, "/", nil)
assert.NoError(t, err)
m.ServeHTTP(httptest.NewRecorder(), r)
}

@ -11,21 +11,23 @@ import (
"github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/metrics" "github.com/grafana/grafana/pkg/infra/metrics"
"github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/middleware/requestmeta"
"github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/web" "github.com/grafana/grafana/pkg/web"
) )
var ( var (
httpRequestsInFlight prometheus.Gauge
httpRequestDurationHistogram *prometheus.HistogramVec
// DefBuckets are histogram buckets for the response time (in seconds) // DefBuckets are histogram buckets for the response time (in seconds)
// of a network service, including one that is responding very slowly. // of a network service, including one that is responding very slowly.
defBuckets = []float64{.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10, 25} defBuckets = []float64{.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10, 25}
) )
func init() { // RequestMetrics is a middleware handler that instruments the request.
httpRequestsInFlight = prometheus.NewGauge( func RequestMetrics(features featuremgmt.FeatureToggles, cfg *setting.Cfg, promRegister prometheus.Registerer) web.Middleware {
log := log.New("middleware.request-metrics")
httpRequestsInFlight := prometheus.NewGauge(
prometheus.GaugeOpts{ prometheus.GaugeOpts{
Namespace: "grafana", Namespace: "grafana",
Name: "http_request_in_flight", Name: "http_request_in_flight",
@ -33,22 +35,22 @@ func init() {
}, },
) )
httpRequestDurationHistogram = prometheus.NewHistogramVec( histogramLabels := []string{"handler", "status_code", "method"}
if cfg.MetricsIncludeTeamLabel {
histogramLabels = append(histogramLabels, "team")
}
httpRequestDurationHistogram := prometheus.NewHistogramVec(
prometheus.HistogramOpts{ prometheus.HistogramOpts{
Namespace: "grafana", Namespace: "grafana",
Name: "http_request_duration_seconds", Name: "http_request_duration_seconds",
Help: "Histogram of latencies for HTTP requests.", Help: "Histogram of latencies for HTTP requests.",
Buckets: defBuckets, Buckets: defBuckets,
}, },
[]string{"handler", "status_code", "method"}, histogramLabels,
) )
prometheus.MustRegister(httpRequestsInFlight, httpRequestDurationHistogram) promRegister.MustRegister(httpRequestsInFlight, httpRequestDurationHistogram)
}
// RequestMetrics is a middleware handler that instruments the request.
func RequestMetrics(features featuremgmt.FeatureToggles) web.Middleware {
log := log.New("middleware.request-metrics")
return func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@ -77,10 +79,17 @@ func RequestMetrics(features featuremgmt.FeatureToggles) web.Middleware {
} }
} }
labelValues := []string{handler, code, r.Method}
if cfg.MetricsIncludeTeamLabel {
rmd := requestmeta.GetRequestMetaData(r.Context())
labelValues = append(labelValues, rmd.Team)
}
// avoiding the sanitize functions for in the new instrumentation // avoiding the sanitize functions for in the new instrumentation
// since they dont make much sense. We should remove them later. // since they dont make much sense. We should remove them later.
histogram := httpRequestDurationHistogram. histogram := httpRequestDurationHistogram.
WithLabelValues(handler, code, r.Method) WithLabelValues(labelValues...)
if traceID := tracing.TraceIDFromContext(r.Context(), true); traceID != "" { if traceID := tracing.TraceIDFromContext(r.Context(), true); traceID != "" {
// Need to type-convert the Observer to an // Need to type-convert the Observer to an
// ExemplarObserver. This will always work for a // ExemplarObserver. This will always work for a

@ -0,0 +1,75 @@
package requestmeta
import (
"context"
"net/http"
"github.com/grafana/grafana/pkg/web"
)
const (
TeamAlerting = "alerting"
TeamAuth = "auth"
TeamCore = "core"
)
type rMDContextKey struct{}
type RequestMetaData struct {
Team string
}
var requestMetaDataContextKey = rMDContextKey{}
// SetupRequestMetadata injects defaul request metadata values
// on the request context.
func SetupRequestMetadata() web.Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
rmd := defaultRequestMetadata()
ctx := context.WithValue(r.Context(), requestMetaDataContextKey, rmd)
*r = *r.WithContext(ctx)
next.ServeHTTP(w, r)
})
}
}
// GetRequestMetaData returns the request metadata for the context.
// if request metadata is missing it will return the default values.
func GetRequestMetaData(ctx context.Context) *RequestMetaData {
val := ctx.Value(requestMetaDataContextKey)
value, ok := val.(*RequestMetaData)
if ok {
return value
}
return defaultRequestMetadata()
}
// SetRequestMetaData returns an `web.Handler` that overrides the request metadata
// with the provided param.
func SetRequestMetaData(rmd RequestMetaData) web.Handler {
return func(w http.ResponseWriter, r *http.Request) {
v := GetRequestMetaData(r.Context())
if rmd.Team != "" {
v.Team = rmd.Team
}
}
}
// SetOwner returns an `web.Handler` that sets the team name for an request.
func SetOwner(team string) web.Handler {
return func(w http.ResponseWriter, r *http.Request) {
v := GetRequestMetaData(r.Context())
v.Team = team
}
}
func defaultRequestMetadata() *RequestMetaData {
return &RequestMetaData{
Team: TeamCore,
}
}

@ -256,6 +256,10 @@ type Cfg struct {
MetricsEndpointBasicAuthUsername string MetricsEndpointBasicAuthUsername string
MetricsEndpointBasicAuthPassword string MetricsEndpointBasicAuthPassword string
MetricsEndpointDisableTotalStats bool MetricsEndpointDisableTotalStats bool
// MetricsIncludeTeamLabel configures grafana to set a label for
// the team responsible for the code at Grafana labs. We don't expect anyone else to
// use this setting.
MetricsIncludeTeamLabel bool
MetricsTotalStatsIntervalSeconds int MetricsTotalStatsIntervalSeconds int
MetricsGrafanaEnvironmentInfo map[string]string MetricsGrafanaEnvironmentInfo map[string]string
@ -1088,6 +1092,7 @@ func (cfg *Cfg) Load(args CommandLineArgs) error {
cfg.MetricsEndpointBasicAuthUsername = valueAsString(iniFile.Section("metrics"), "basic_auth_username", "") cfg.MetricsEndpointBasicAuthUsername = valueAsString(iniFile.Section("metrics"), "basic_auth_username", "")
cfg.MetricsEndpointBasicAuthPassword = valueAsString(iniFile.Section("metrics"), "basic_auth_password", "") cfg.MetricsEndpointBasicAuthPassword = valueAsString(iniFile.Section("metrics"), "basic_auth_password", "")
cfg.MetricsEndpointDisableTotalStats = iniFile.Section("metrics").Key("disable_total_stats").MustBool(false) cfg.MetricsEndpointDisableTotalStats = iniFile.Section("metrics").Key("disable_total_stats").MustBool(false)
cfg.MetricsIncludeTeamLabel = iniFile.Section("metrics").Key("include_team_label").MustBool(false)
cfg.MetricsTotalStatsIntervalSeconds = iniFile.Section("metrics").Key("total_stats_collector_interval_seconds").MustInt(1800) cfg.MetricsTotalStatsIntervalSeconds = iniFile.Section("metrics").Key("total_stats_collector_interval_seconds").MustInt(1800)
analytics := iniFile.Section("analytics") analytics := iniFile.Section("analytics")

Loading…
Cancel
Save