Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 44 additions & 1 deletion pkg/plugin/driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,16 @@ import (
"golang.org/x/net/proxy"
)

type grafanaHeadersKeyType struct{}

var grafanaHeadersKey = grafanaHeadersKeyType{}

type grafanaHeaders struct {
DashboardUID string
PanelID string
RuleUID string
}

// Clickhouse defines how to connect to a Clickhouse datasource
type Clickhouse struct{}

Expand Down Expand Up @@ -351,17 +361,50 @@ func (h *Clickhouse) Settings(ctx context.Context, config backend.DataSourceInst
}
}

// MutateQueryData extracts Grafana contextual headers from the request and
// stores them in the context for ClickHouse query metadata injection.
func (h *Clickhouse) MutateQueryData(ctx context.Context, req *backend.QueryDataRequest) (context.Context, *backend.QueryDataRequest) {
headers := req.GetHTTPHeaders()
gh := grafanaHeaders{
DashboardUID: headers.Get("X-Dashboard-Uid"),
PanelID: headers.Get("X-Panel-Id"),
RuleUID: headers.Get("X-Rule-Uid"),
}
if gh.DashboardUID != "" || gh.PanelID != "" || gh.RuleUID != "" {
ctx = context.WithValue(ctx, grafanaHeadersKey, gh)
}
return ctx, req
}

func (h *Clickhouse) MutateQuery(ctx context.Context, req backend.DataQuery) (context.Context, backend.DataQuery) {
ctx, span := tracing.DefaultTracer().Start(ctx, "clickhouse mutate_query", trace.WithAttributes(
attribute.String("db.system", "clickhouse"),
))

defer span.End()

comments := make([]string, 0, 4)

if user := backend.UserFromContext(ctx); user != nil {
comments = append(comments, "grafana_user:"+user.Login)
}

if gh, ok := ctx.Value(grafanaHeadersKey).(grafanaHeaders); ok {
if gh.DashboardUID != "" {
comments = append(comments, "grafana_dashboard:"+gh.DashboardUID)
}
if gh.PanelID != "" {
comments = append(comments, "grafana_panel:"+gh.PanelID)
}
if gh.RuleUID != "" {
comments = append(comments, "grafana_rule:"+gh.RuleUID)
}
}

if len(comments) > 0 {
ctx = clickhouse.Context(ctx, clickhouse.WithClientInfo(clickhouse.ClientInfo{
Products: nil,
Comment: []string{fmt.Sprintf("grafana_user:%s", user.Login)},
Comment: comments,
}))
}

Expand Down
106 changes: 106 additions & 0 deletions pkg/plugin/driver_test.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package plugin

import (
"context"
"encoding/json"
"errors"
"fmt"
"testing"

"github.com/ClickHouse/clickhouse-go/v2"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/data"
"github.com/stretchr/testify/assert"
)
Expand Down Expand Up @@ -317,3 +319,107 @@ func TestContainsClickHouseException(t *testing.T) {
assert.True(t, result)
})
}

func TestMutateQueryData(t *testing.T) {
h := &Clickhouse{}

tests := []struct {
name string
headers map[string]string
want grafanaHeaders
stored bool
}{
{
name: "all headers",
headers: map[string]string{
"http_X-Dashboard-Uid": "dash-abc123",
"http_X-Panel-Id": "42",
"http_X-Rule-Uid": "rule-xyz",
},
want: grafanaHeaders{DashboardUID: "dash-abc123", PanelID: "42", RuleUID: "rule-xyz"},
stored: true,
},
{
name: "empty headers",
headers: map[string]string{},
stored: false,
},
{
name: "only dashboard",
headers: map[string]string{"http_X-Dashboard-Uid": "dash-only"},
want: grafanaHeaders{DashboardUID: "dash-only"},
stored: true,
},
{
name: "only panel",
headers: map[string]string{"http_X-Panel-Id": "99"},
want: grafanaHeaders{PanelID: "99"},
stored: true,
},
{
name: "only rule",
headers: map[string]string{"http_X-Rule-Uid": "alert-rule-1"},
want: grafanaHeaders{RuleUID: "alert-rule-1"},
stored: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req := &backend.QueryDataRequest{Headers: tt.headers}
newCtx, _ := h.MutateQueryData(context.Background(), req)

gh, ok := newCtx.Value(grafanaHeadersKey).(grafanaHeaders)
assert.Equal(t, tt.stored, ok)
if tt.stored {
assert.Equal(t, tt.want, gh)
}
})
}

t.Run("nil headers does not panic", func(t *testing.T) {
newCtx, newReq := h.MutateQueryData(context.Background(), &backend.QueryDataRequest{})
assert.NotNil(t, newCtx)
assert.NotNil(t, newReq)
})
}

func TestMutateQuery_GrafanaMetadata(t *testing.T) {
h := &Clickhouse{}

t.Run("includes dashboard and panel from context", func(t *testing.T) {
ctx := context.WithValue(context.Background(), grafanaHeadersKey, grafanaHeaders{
DashboardUID: "my-dashboard",
PanelID: "7",
RuleUID: "alert-1",
})

newCtx, _ := h.MutateQuery(ctx, backend.DataQuery{
JSON: []byte(`{}`),
})

assert.NotEqual(t, ctx, newCtx)
})

t.Run("no grafana headers in context still works", func(t *testing.T) {
ctx := context.Background()

newCtx, _ := h.MutateQuery(ctx, backend.DataQuery{
JSON: []byte(`{}`),
})

assert.Equal(t, ctx, newCtx)
})

t.Run("handles invalid JSON gracefully", func(t *testing.T) {
ctx := context.WithValue(context.Background(), grafanaHeadersKey, grafanaHeaders{
DashboardUID: "dash1",
})

newCtx, _ := h.MutateQuery(ctx, backend.DataQuery{
JSON: []byte(`invalid json`),
})

assert.NotEqual(t, ctx, newCtx)
})
}
Loading