Skip to content

Commit bb25ba0

Browse files
MattiasMTSbossinc
andauthored
feat: grafana query metadata (#1743)
Co-authored-by: Andrew Hackmann <5140848+bossinc@users.noreply.github.com>
1 parent ebee535 commit bb25ba0

File tree

2 files changed

+160
-4
lines changed

2 files changed

+160
-4
lines changed

pkg/plugin/driver.go

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,16 @@ import (
3131
"golang.org/x/net/proxy"
3232
)
3333

34+
type grafanaHeadersKeyType struct{}
35+
36+
var grafanaHeadersKey = grafanaHeadersKeyType{}
37+
38+
type grafanaHeaders struct {
39+
DashboardUID string
40+
PanelID string
41+
RuleUID string
42+
}
43+
3444
// Clickhouse defines how to connect to a Clickhouse datasource
3545
type Clickhouse struct {
3646
SchemaDatasource *schemas.SchemaDatasource
@@ -125,14 +135,19 @@ func CheckMinServerVersion(conn *sql.DB, major, minor, patch uint64) (bool, erro
125135
version.Patch, _ = strconv.ParseUint(v, 10, 64)
126136
}
127137
}
128-
if version.Major < major || (version.Major == major && version.Minor < minor) || (version.Major == major && version.Minor == minor && version.Patch < patch) {
138+
if version.Major < major || (version.Major == major && version.Minor < minor) ||
139+
(version.Major == major && version.Minor == minor && version.Patch < patch) {
129140
return false, nil
130141
}
131142
return true, nil
132143
}
133144

134145
// Connect opens a sql.DB connection using datasource settings
135-
func (h *Clickhouse) Connect(ctx context.Context, config backend.DataSourceInstanceSettings, message json.RawMessage) (*sql.DB, error) {
146+
func (h *Clickhouse) Connect(
147+
ctx context.Context,
148+
config backend.DataSourceInstanceSettings,
149+
message json.RawMessage,
150+
) (*sql.DB, error) {
136151
ctx, span := tracing.DefaultTracer().Start(ctx, "clickhouse connect", trace.WithAttributes(
137152
attribute.String("db.system", "clickhouse"),
138153
))
@@ -354,7 +369,22 @@ func (h *Clickhouse) Settings(ctx context.Context, config backend.DataSourceInst
354369
}
355370
}
356371

357-
func (h *Clickhouse) MutateQueryData(ctx context.Context, req *backend.QueryDataRequest) (context.Context, *backend.QueryDataRequest) {
372+
// MutateQueryData extracts Grafana contextual headers from the request and
373+
// stores them in the context for ClickHouse query metadata injection.
374+
func (h *Clickhouse) MutateQueryData(
375+
ctx context.Context,
376+
req *backend.QueryDataRequest,
377+
) (context.Context, *backend.QueryDataRequest) {
378+
headers := req.GetHTTPHeaders()
379+
gh := grafanaHeaders{
380+
DashboardUID: headers.Get("X-Dashboard-Uid"),
381+
PanelID: headers.Get("X-Panel-Id"),
382+
RuleUID: headers.Get("X-Rule-Uid"),
383+
}
384+
if gh.DashboardUID != "" || gh.PanelID != "" || gh.RuleUID != "" {
385+
ctx = context.WithValue(ctx, grafanaHeadersKey, gh)
386+
}
387+
358388
req = preprocessGrafanaSQL(req)
359389
return ctx, req
360390
}
@@ -415,10 +445,28 @@ func (h *Clickhouse) MutateQuery(ctx context.Context, req backend.DataQuery) (co
415445

416446
defer span.End()
417447

448+
comments := make([]string, 0, 4)
449+
418450
if user := backend.UserFromContext(ctx); user != nil {
451+
comments = append(comments, "grafana_user:"+user.Login)
452+
}
453+
454+
if gh, ok := ctx.Value(grafanaHeadersKey).(grafanaHeaders); ok {
455+
if gh.DashboardUID != "" {
456+
comments = append(comments, "grafana_dashboard:"+gh.DashboardUID)
457+
}
458+
if gh.PanelID != "" {
459+
comments = append(comments, "grafana_panel:"+gh.PanelID)
460+
}
461+
if gh.RuleUID != "" {
462+
comments = append(comments, "grafana_rule:"+gh.RuleUID)
463+
}
464+
}
465+
466+
if len(comments) > 0 {
419467
ctx = clickhouse.Context(ctx, clickhouse.WithClientInfo(clickhouse.ClientInfo{
420468
Products: nil,
421-
Comment: []string{fmt.Sprintf("grafana_user:%s", user.Login)},
469+
Comment: comments,
422470
}))
423471
}
424472

pkg/plugin/driver_test.go

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
package plugin
22

33
import (
4+
"context"
45
"encoding/json"
56
"errors"
67
"fmt"
78
"testing"
89

910
"github.com/ClickHouse/clickhouse-go/v2"
11+
"github.com/grafana/grafana-plugin-sdk-go/backend"
1012
"github.com/grafana/grafana-plugin-sdk-go/data"
1113
"github.com/stretchr/testify/assert"
1214
)
@@ -317,3 +319,109 @@ func TestContainsClickHouseException(t *testing.T) {
317319
assert.True(t, result)
318320
})
319321
}
322+
323+
func TestMutateQueryData(t *testing.T) {
324+
h := &Clickhouse{}
325+
326+
tests := []struct {
327+
name string
328+
headers map[string]string
329+
want grafanaHeaders
330+
stored bool
331+
}{
332+
{
333+
name: "all headers",
334+
headers: map[string]string{
335+
"http_X-Dashboard-Uid": "dash-abc123",
336+
"http_X-Panel-Id": "42",
337+
"http_X-Rule-Uid": "rule-xyz",
338+
},
339+
want: grafanaHeaders{DashboardUID: "dash-abc123", PanelID: "42", RuleUID: "rule-xyz"},
340+
stored: true,
341+
},
342+
{
343+
name: "empty headers",
344+
headers: map[string]string{},
345+
stored: false,
346+
},
347+
{
348+
name: "only dashboard",
349+
headers: map[string]string{"http_X-Dashboard-Uid": "dash-only"},
350+
want: grafanaHeaders{DashboardUID: "dash-only"},
351+
stored: true,
352+
},
353+
{
354+
name: "only panel",
355+
headers: map[string]string{"http_X-Panel-Id": "99"},
356+
want: grafanaHeaders{PanelID: "99"},
357+
stored: true,
358+
},
359+
{
360+
name: "only rule",
361+
headers: map[string]string{"http_X-Rule-Uid": "alert-rule-1"},
362+
want: grafanaHeaders{RuleUID: "alert-rule-1"},
363+
stored: true,
364+
},
365+
}
366+
367+
for _, tt := range tests {
368+
t.Run(tt.name, func(t *testing.T) {
369+
req := &backend.QueryDataRequest{Headers: tt.headers}
370+
newCtx, _ := h.MutateQueryData(t.Context(), req)
371+
372+
gh, ok := newCtx.Value(grafanaHeadersKey).(grafanaHeaders)
373+
assert.Equal(t, tt.stored, ok)
374+
if tt.stored {
375+
assert.Equal(t, tt.want, gh)
376+
}
377+
})
378+
}
379+
380+
t.Run("nil headers does not panic", func(t *testing.T) {
381+
newCtx, newReq := h.MutateQueryData(t.Context(), &backend.QueryDataRequest{})
382+
assert.NotNil(t, newCtx)
383+
assert.NotNil(t, newReq)
384+
})
385+
}
386+
387+
func TestMutateQuery_GrafanaMetadata(t *testing.T) {
388+
h := &Clickhouse{}
389+
390+
t.Run("includes dashboard and panel from context", func(t *testing.T) {
391+
ctx := context.WithValue(t.Context(), grafanaHeadersKey, grafanaHeaders{
392+
DashboardUID: "my-dashboard",
393+
PanelID: "7",
394+
RuleUID: "alert-1",
395+
})
396+
397+
newCtx, _ := h.MutateQuery(ctx, backend.DataQuery{
398+
JSON: []byte(`{}`),
399+
})
400+
401+
assert.NotEqual(t, ctx, newCtx)
402+
})
403+
404+
t.Run("no grafana headers in context still works", func(t *testing.T) {
405+
ctx := t.Context()
406+
407+
newCtx, _ := h.MutateQuery(ctx, backend.DataQuery{
408+
JSON: []byte(`{}`),
409+
})
410+
411+
assert.NotNil(t, newCtx)
412+
_, ok := newCtx.Value(grafanaHeadersKey).(grafanaHeaders)
413+
assert.False(t, ok)
414+
})
415+
416+
t.Run("handles invalid JSON gracefully", func(t *testing.T) {
417+
ctx := context.WithValue(t.Context(), grafanaHeadersKey, grafanaHeaders{
418+
DashboardUID: "dash1",
419+
})
420+
421+
newCtx, _ := h.MutateQuery(ctx, backend.DataQuery{
422+
JSON: []byte(`invalid json`),
423+
})
424+
425+
assert.NotEqual(t, ctx, newCtx)
426+
})
427+
}

0 commit comments

Comments
 (0)