diff --git a/pkg/api/api.go b/pkg/api/api.go index 12e066c509b..7359a643ac3 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -33,6 +33,7 @@ import ( "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/middleware" + "github.com/grafana/grafana/pkg/middleware/requestmeta" ac "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/apikey" "github.com/grafana/grafana/pkg/services/auth" @@ -70,7 +71,7 @@ func (hs *HTTPServer) registerRoutes() { // not logged in views 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", hs.LoginView) 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("/", routing.Wrap(hs.GetAlerts)) alertsRoute.Get("/states-for-dashboard", routing.Wrap(hs.GetAlertStatesForDashboard)) - }) + }, requestmeta.SetOwner(requestmeta.TeamAlerting)) var notifiersAuthHandler web.Handler if hs.Cfg.UnifiedAlerting.IsEnabled() { @@ -548,7 +549,7 @@ func (hs *HTTPServer) registerRoutes() { 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())), ) @@ -562,12 +563,12 @@ func (hs *HTTPServer) registerRoutes() { alertNotifications.Get("/uid/:uid", routing.Wrap(hs.GetAlertNotificationByUID)) alertNotifications.Put("/uid/:uid", routing.Wrap(hs.UpdateAlertNotificationByUID)) alertNotifications.Delete("/uid/:uid", routing.Wrap(hs.DeleteAlertNotificationByUID)) - }, reqEditorRole) + }, reqEditorRole, requestmeta.SetOwner(requestmeta.TeamAlerting)) // alert notifications without requirement of user to be org editor apiRoute.Group("/alert-notifications", func(orgRoute routing.RouteRegister) { orgRoute.Get("/lookup", routing.Wrap(hs.GetAlertNotificationLookup)) - }) + }, requestmeta.SetOwner(requestmeta.TeamAlerting)) 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)) diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index 4ece9a1021d..f830f89c194 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -31,6 +31,7 @@ import ( "github.com/grafana/grafana/pkg/middleware" "github.com/grafana/grafana/pkg/middleware/csrf" "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/pluginscdn" "github.com/grafana/grafana/pkg/registry/corekind" @@ -203,6 +204,7 @@ type HTTPServer struct { statsService stats.Service authnService authn.Service starApi *starApi.API + promRegister prometheus.Registerer } type ServerOptions struct { @@ -244,7 +246,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi accesscontrolService accesscontrol.Service, navTreeService navtree.Service, annotationRepo annotations.Repository, tagService tag.Service, searchv2HTTPService searchV2.SearchHTTPService, oauthTokenService oauthtoken.OAuthTokenService, statsService stats.Service, authnService authn.Service, pluginsCDNService *pluginscdn.Service, - starApi *starApi.API, + starApi *starApi.API, promRegister prometheus.Registerer, ) (*HTTPServer, error) { web.Env = cfg.Env @@ -346,6 +348,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi authnService: authnService, pluginsCDNService: pluginsCDNService, starApi: starApi, + promRegister: promRegister, } if hs.Listener != nil { hs.log.Debug("Using provided listener") @@ -568,8 +571,9 @@ func (hs *HTTPServer) applyRoutes() { func (hs *HTTPServer) addMiddlewaresAndStaticRoutes() { m := hs.web + m.Use(requestmeta.SetupRequestMetadata()) 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()) diff --git a/pkg/middleware/request_metadata_test.go b/pkg/middleware/request_metadata_test.go new file mode 100644 index 00000000000..64cc37aec79 --- /dev/null +++ b/pkg/middleware/request_metadata_test.go @@ -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) +} diff --git a/pkg/middleware/request_metrics.go b/pkg/middleware/request_metrics.go index 0296a43ad43..554a799510e 100644 --- a/pkg/middleware/request_metrics.go +++ b/pkg/middleware/request_metrics.go @@ -11,21 +11,23 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/metrics" "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/setting" "github.com/grafana/grafana/pkg/web" ) var ( - httpRequestsInFlight prometheus.Gauge - httpRequestDurationHistogram *prometheus.HistogramVec - // DefBuckets are histogram buckets for the response time (in seconds) // 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} ) -func init() { - httpRequestsInFlight = prometheus.NewGauge( +// RequestMetrics is a middleware handler that instruments the request. +func RequestMetrics(features featuremgmt.FeatureToggles, cfg *setting.Cfg, promRegister prometheus.Registerer) web.Middleware { + log := log.New("middleware.request-metrics") + + httpRequestsInFlight := prometheus.NewGauge( prometheus.GaugeOpts{ Namespace: "grafana", 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{ Namespace: "grafana", Name: "http_request_duration_seconds", Help: "Histogram of latencies for HTTP requests.", Buckets: defBuckets, }, - []string{"handler", "status_code", "method"}, + histogramLabels, ) - prometheus.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") + promRegister.MustRegister(httpRequestsInFlight, httpRequestDurationHistogram) return func(next http.Handler) http.Handler { 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 // since they dont make much sense. We should remove them later. histogram := httpRequestDurationHistogram. - WithLabelValues(handler, code, r.Method) + WithLabelValues(labelValues...) + if traceID := tracing.TraceIDFromContext(r.Context(), true); traceID != "" { // Need to type-convert the Observer to an // ExemplarObserver. This will always work for a diff --git a/pkg/middleware/requestmeta/request_metadata.go b/pkg/middleware/requestmeta/request_metadata.go new file mode 100644 index 00000000000..24ea23a50ef --- /dev/null +++ b/pkg/middleware/requestmeta/request_metadata.go @@ -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, + } +} diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 3593cb81dbe..7779aa58705 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -256,6 +256,10 @@ type Cfg struct { MetricsEndpointBasicAuthUsername string MetricsEndpointBasicAuthPassword string 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 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.MetricsEndpointBasicAuthPassword = valueAsString(iniFile.Section("metrics"), "basic_auth_password", "") 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) analytics := iniFile.Section("analytics")