From c1939a1a0bde9f44105cbd907edc4871eaad2803 Mon Sep 17 00:00:00 2001 From: Ben Ye Date: Thu, 20 Aug 2026 22:10:33 +0000 Subject: [PATCH] query-frontend: Log X-Grafana-User header in query stats When Grafana's [dataproxy] send_user_header is enabled, Grafana adds an X-Grafana-User header with the logged-in username to all data source proxy requests. This commit logs this header in the query frontend's query stats, slow query, and query request log messages, making it easier to attribute queries to specific Grafana users. The header is already logged alongside X-Dashboard-Uid and X-Panel-Id in the formatGrafanaStatsFields helper, so it appears in all three logging paths automatically. Signed-off-by: Ben Ye --- CHANGELOG.md | 1 + pkg/frontend/transport/handler.go | 7 ++++++- pkg/frontend/transport/handler_test.go | 14 ++++++++++++++ 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 16148eeae63..be8305e7288 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Changelog ## master / unreleased +* [ENHANCEMENT] Query Frontend: Log `X-Grafana-User` header in query stats, slow query, and query request logs when Grafana's `send_user_header` is enabled. #7797 * [FEATURE] Engine: Add `-querier.selector-batch-size` and `-ruler.selector-batch-size` flags to configure series batching in the Thanos promQL engine. 0 disables batching. #7763 * [CHANGE] Querier: Make query time range configurations per-tenant: `query_ingesters_within`, `query_store_after`, and `shuffle_sharding_ingesters_lookback_period`. Uses `model.Duration` instead of `time.Duration` to support serialization but has minimum unit of 1ms (nanoseconds/microseconds not supported). #7160 * [CHANGE] Cache: Setting `-blocks-storage.bucket-store.metadata-cache.bucket-index-content-ttl` to 0 will disable the bucket-index cache. #7446 diff --git a/pkg/frontend/transport/handler.go b/pkg/frontend/transport/handler.go index 432f63c2cd4..623c8ae507f 100644 --- a/pkg/frontend/transport/handler.go +++ b/pkg/frontend/transport/handler.go @@ -372,13 +372,18 @@ func (f *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { func formatGrafanaStatsFields(r *http.Request) []any { // NOTE(GiedriusS): see https://github.com/grafana/grafana/pull/60301 for more info. - fields := make([]any, 0, 4) + fields := make([]any, 0, 6) if dashboardUID := r.Header.Get("X-Dashboard-Uid"); dashboardUID != "" { fields = append(fields, "X-Dashboard-Uid", dashboardUID) } if panelID := r.Header.Get("X-Panel-Id"); panelID != "" { fields = append(fields, "X-Panel-Id", panelID) } + // X-Grafana-User is sent by Grafana when [dataproxy] send_user_header is enabled. + // See https://github.com/grafana/grafana/pull/15998 for more info. + if grafanaUser := r.Header.Get("X-Grafana-User"); grafanaUser != "" { + fields = append(fields, "X-Grafana-User", grafanaUser) + } return fields } diff --git a/pkg/frontend/transport/handler_test.go b/pkg/frontend/transport/handler_test.go index 71dda042801..2a66a7c7b06 100644 --- a/pkg/frontend/transport/handler_test.go +++ b/pkg/frontend/transport/handler_test.go @@ -519,6 +519,11 @@ func TestReportQueryStatsFormat(t *testing.T) { expectedLog: `level=info msg="query stats" component=query-frontend method=GET path=/prometheus/api/v1/query response_time=1s query_wall_time_seconds=0 response_series_count=0 fetched_series_count=0 fetched_chunks_count=0 fetched_samples_count=0 fetched_chunks_bytes=0 fetched_data_bytes=0 split_queries=0 status_code=200 response_size=1000 samples_scanned=0 user_agent=Grafana`, source: requestmeta.SourceAPI, }, + "should include grafana user header": { + header: http.Header{"X-Grafana-User": []string{"admin"}}, + expectedLog: `level=info msg="query stats" component=query-frontend method=GET path=/prometheus/api/v1/query response_time=1s query_wall_time_seconds=0 response_series_count=0 fetched_series_count=0 fetched_chunks_count=0 fetched_samples_count=0 fetched_chunks_bytes=0 fetched_data_bytes=0 split_queries=0 status_code=200 response_size=1000 samples_scanned=0 X-Grafana-User=admin`, + source: requestmeta.SourceAPI, + }, "should include engine type": { header: http.Header{http.CanonicalHeaderKey(engine.TypeHeader): []string{string(engine.Thanos)}}, expectedLog: `level=info msg="query stats" component=query-frontend method=GET path=/prometheus/api/v1/query response_time=1s query_wall_time_seconds=0 response_series_count=0 fetched_series_count=0 fetched_chunks_count=0 fetched_samples_count=0 fetched_chunks_bytes=0 fetched_data_bytes=0 split_queries=0 status_code=200 response_size=1000 samples_scanned=0 engine_type=thanos`, @@ -634,6 +639,15 @@ func TestReportSlowQueryFormat(t *testing.T) { }, expectedLog: `level=info msg="slow query detected" method=GET host=localhost:8080 path=/prometheus/api/v1/query source=api time_taken_ms=1000 X-Dashboard-Uid=dashboard-1 X-Panel-Id=panel-1`, }, + "should include grafana user header": { + source: requestmeta.SourceAPI, + header: http.Header{ + "X-Dashboard-Uid": []string{"dashboard-1"}, + "X-Panel-Id": []string{"panel-1"}, + "X-Grafana-User": []string{"admin"}, + }, + expectedLog: `level=info msg="slow query detected" method=GET host=localhost:8080 path=/prometheus/api/v1/query source=api time_taken_ms=1000 X-Dashboard-Uid=dashboard-1 X-Panel-Id=panel-1 X-Grafana-User=admin`, + }, "should include user agent, engine type and block store type headers": { source: requestmeta.SourceAPI, header: http.Header{