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:
@@ -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,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user