Loki: Add dashboard and panel names as headers (#92096)

* feat(nameHeaders): add feature flag

* add safe parsing of headers

* use headers in loki datasource

* Loki: add option to pass headers to Loki

* Loki: add datasource tests for dashboard names

* cleanup

* DataSourceWithBackend: add test

* rename to `sanitizeHeader`

* Loki: add condition when to add headers

* Loki: add e2e tests

* Loki: change test name
This commit is contained in:
Sven Grossmann
2024-08-22 21:30:43 +02:00
committed by GitHub
parent a7b57be04f
commit ec857e1de9
15 changed files with 307 additions and 7 deletions
+6
View File
@@ -1393,6 +1393,12 @@ var (
Stage: FeatureStageExperimental,
Owner: grafanaDashboardsSquad,
},
{
Name: "lokiSendDashboardPanelNames",
Description: "Send dashboard and panel names to Loki when querying",
Stage: FeatureStageExperimental,
Owner: grafanaObservabilityLogsSquad,
},
}
)
+1
View File
@@ -183,3 +183,4 @@ prometheusAzureOverrideAudience,deprecated,@grafana/partner-datasources,false,fa
backgroundPluginInstaller,experimental,@grafana/plugins-platform-backend,false,true,false
dataplaneAggregator,experimental,@grafana/grafana-app-platform-squad,false,true,false
adhocFilterOneOf,experimental,@grafana/dashboards-squad,false,false,false
lokiSendDashboardPanelNames,experimental,@grafana/observability-logs,false,false,false
1 Name Stage Owner requiresDevMode RequiresRestart FrontendOnly
183 backgroundPluginInstaller experimental @grafana/plugins-platform-backend false true false
184 dataplaneAggregator experimental @grafana/grafana-app-platform-squad false true false
185 adhocFilterOneOf experimental @grafana/dashboards-squad false false false
186 lokiSendDashboardPanelNames experimental @grafana/observability-logs false false false
+4
View File
@@ -742,4 +742,8 @@ const (
// FlagAdhocFilterOneOf
// Exposes a new 'one of' operator for ad-hoc filters. This operator allows users to filter by multiple values in a single filter.
FlagAdhocFilterOneOf = "adhocFilterOneOf"
// FlagLokiSendDashboardPanelNames
// Send dashboard and panel names to Loki when querying
FlagLokiSendDashboardPanelNames = "lokiSendDashboardPanelNames"
)
+15
View File
@@ -1704,6 +1704,21 @@
"codeowner": "@grafana/observability-logs"
}
},
{
"metadata": {
"name": "lokiSendDashboardPanelNames",
"resourceVersion": "1724089497989",
"creationTimestamp": "2024-08-19T17:44:18Z",
"annotations": {
"grafana.app/updatedTimestamp": "2024-08-19 17:44:57.989565 +0000 UTC"
}
},
"spec": {
"description": "Send dashboard and panel names to Loki when querying",
"stage": "experimental",
"codeowner": "@grafana/observability-logs"
}
},
{
"metadata": {
"name": "lokiStructuredMetadata",
+86 -1
View File
@@ -15,6 +15,7 @@ import (
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/tests/testinfra"
@@ -30,7 +31,8 @@ func TestIntegrationLoki(t *testing.T) {
t.Skip("skipping integration test")
}
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
DisableAnonymous: true,
DisableAnonymous: true,
EnableFeatureToggles: []string{featuremgmt.FlagLokiSendDashboardPanelNames},
})
grafanaListeningAddr, testEnv := testinfra.StartGrafanaEnv(t, dir, path)
@@ -106,4 +108,87 @@ func TestIntegrationLoki(t *testing.T) {
require.Equal(t, "basicAuthUser", username)
require.Equal(t, "basicAuthPassword", pwd)
})
t.Run("should forward `X-Dashboard-Title` header but no `X-Panel-Title`", func(t *testing.T) {
query := simplejson.NewFromAny(map[string]interface{}{
"datasource": map[string]interface{}{
"uid": uid,
},
"expr": "{job=\"grafana\"}",
})
buf1 := &bytes.Buffer{}
err = json.NewEncoder(buf1).Encode(dtos.MetricRequest{
From: "now-1h",
To: "now",
Queries: []*simplejson.Json{query},
})
require.NoError(t, err)
u := fmt.Sprintf("http://admin:admin@%s/api/ds/query", grafanaListeningAddr)
req, err := http.NewRequest("POST", u, buf1)
if err != nil {
require.NoError(t, err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Dashboard-Title", "My Dashboard Title")
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
require.Equal(t, http.StatusBadRequest, resp.StatusCode)
t.Cleanup(func() {
err := resp.Body.Close()
require.NoError(t, err)
})
_, err = io.ReadAll(resp.Body)
require.NoError(t, err)
require.NotNil(t, outgoingRequest)
require.Equal(t, "My Dashboard Title", outgoingRequest.Header.Get("X-Dashboard-Title"))
require.Equal(t, "", outgoingRequest.Header.Get("X-Panel-Title"))
username, pwd, ok := outgoingRequest.BasicAuth()
require.True(t, ok)
require.Equal(t, "basicAuthUser", username)
require.Equal(t, "basicAuthPassword", pwd)
})
t.Run("should forward `X-Dashboard-Title` and `X-Panel-Title` header", func(t *testing.T) {
query := simplejson.NewFromAny(map[string]interface{}{
"datasource": map[string]interface{}{
"uid": uid,
},
"expr": "{job=\"grafana\"}",
})
buf1 := &bytes.Buffer{}
err = json.NewEncoder(buf1).Encode(dtos.MetricRequest{
From: "now-1h",
To: "now",
Queries: []*simplejson.Json{query},
})
require.NoError(t, err)
u := fmt.Sprintf("http://admin:admin@%s/api/ds/query", grafanaListeningAddr)
req, err := http.NewRequest("POST", u, buf1)
if err != nil {
require.NoError(t, err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Dashboard-Title", "My Dashboard Title")
req.Header.Set("X-Panel-Title", "My Panel Title")
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
require.Equal(t, http.StatusBadRequest, resp.StatusCode)
t.Cleanup(func() {
err := resp.Body.Close()
require.NoError(t, err)
})
_, err = io.ReadAll(resp.Body)
require.NoError(t, err)
require.NotNil(t, outgoingRequest)
require.Equal(t, "My Dashboard Title", outgoingRequest.Header.Get("X-Dashboard-Title"))
require.Equal(t, "My Panel Title", outgoingRequest.Header.Get("X-Panel-Title"))
username, pwd, ok := outgoingRequest.BasicAuth()
require.True(t, ok)
require.Equal(t, "basicAuthUser", username)
require.Equal(t, "basicAuthPassword", pwd)
})
}
+25 -3
View File
@@ -22,6 +22,7 @@ import (
"github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/services/contexthandler"
"github.com/grafana/grafana/pkg/services/featuremgmt"
ngalertmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/tsdb/loki/kinds/dataquery"
@@ -49,13 +50,13 @@ func ProvideService(httpClientProvider *httpclient.Provider, tracer tracing.Trac
var (
legendFormat = regexp.MustCompile(`\{\{\s*(.+?)\s*\}\}`)
)
// Used in logging to mark a stage
var (
stagePrepareRequest = "prepareRequest"
stageDatabaseRequest = "databaseRequest"
stageParseResponse = "parseResponse"
dashboardTitleHeader = "X-Dashboard-Title"
panelTitleHeader = "X-Panel-Title"
)
type datasourceInfo struct {
@@ -163,9 +164,30 @@ func (s *Service) QueryData(ctx context.Context, req *backend.QueryDataRequest)
logsDataplane: isFeatureEnabled(ctx, featuremgmt.FlagLokiLogsDataplane),
}
if isFeatureEnabled(ctx, featuremgmt.FlagLokiSendDashboardPanelNames) {
s.applyHeaders(ctx, req)
}
return queryData(ctx, req, dsInfo, responseOpts, s.tracer, logger, isFeatureEnabled(ctx, featuremgmt.FlagLokiRunQueriesInParallel), isFeatureEnabled(ctx, featuremgmt.FlagLokiStructuredMetadata))
}
func (s *Service) applyHeaders(ctx context.Context, req backend.ForwardHTTPHeaders) {
reqCtx := contexthandler.FromContext(ctx)
if req == nil || reqCtx == nil || reqCtx.Req == nil {
return
}
var hList = []string{dashboardTitleHeader, panelTitleHeader}
for _, hName := range hList {
hVal := reqCtx.Req.Header.Get(hName)
if hVal == "" {
continue
}
req.SetHTTPHeader(hName, hVal)
}
}
func queryData(ctx context.Context, req *backend.QueryDataRequest, dsInfo *datasourceInfo, responseOpts ResponseOpts, tracer tracing.Tracer, plog log.Logger, runInParallel bool, requestStructuredMetadata bool) (*backend.QueryDataResponse, error) {
result := backend.NewQueryDataResponse()