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
22 changes: 22 additions & 0 deletions responseobs/counter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package responseobs

import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)

// largeResponsesCounter increments once per Observation that crosses a
// configured threshold. Cardinality is self-limiting because the counter
// only increments on threshold crossings — the number of active series is
// bounded by the number of datasource instances actually producing large
// responses, not by total query volume.
//
// The app_url label carries the stack identifier. It replaces the earlier
// "slug" label in the plan because backend.GrafanaConfig exposes no
// dedicated slug accessor; downstream operators can derive a slug by
// parsing the URL if needed.
var largeResponsesCounter = promauto.NewCounterVec(prometheus.CounterOpts{
Namespace: "plugins",
Name: "sql_large_responses_total",
Help: "Number of SQL datasource responses that crossed a configured size threshold (rows or bytes).",
}, []string{"datasource_type", "app_url", "datasource_uid"})
11 changes: 7 additions & 4 deletions responseobs/responseobs.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,15 +56,17 @@ type Observation struct {
}

// Observe compares obs against t. If a threshold is crossed, it emits a
// single structured warn log via backend.Logger and returns true.
// Otherwise it returns false and emits nothing. Intended to be called
// at most once per response.
// single structured warn log via backend.Logger, increments the
// plugins_sql_large_responses_total counter, and returns true. Otherwise
// it returns false and emits nothing. Intended to be called at most once
// per response.
func Observe(ctx context.Context, obs Observation, t Thresholds) bool {
if !crosses(obs, t) {
return false
}
appURL := appURLFromContext(ctx)
backend.Logger.Warn("large datasource response",
"app_url", appURLFromContext(ctx),
"app_url", appURL,
"datasource_type", obs.Datasource.Type,
"datasource_uid", obs.Datasource.UID,
"datasource_name", obs.Datasource.Name,
Expand All @@ -75,6 +77,7 @@ func Observe(ctx context.Context, obs Observation, t Thresholds) bool {
"ref_id", obs.RefID,
"query_hash", obs.QueryHash,
)
largeResponsesCounter.WithLabelValues(obs.Datasource.Type, appURL, obs.Datasource.UID).Inc()
return true
}

Expand Down
46 changes: 46 additions & 0 deletions responseobs/responseobs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
dto "github.com/prometheus/client_model/go"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
Expand Down Expand Up @@ -146,6 +147,51 @@ func TestObserve_AppURLAbsent_EmptyField(t *testing.T) {
assert.Equal(t, "", rec.entries[0].kv()["app_url"])
}

func TestObserve_Cross_IncrementsCounter(t *testing.T) {
swapLogger(t)

ds := backend.DataSourceInstanceSettings{Type: "counter-cross", UID: "ds-uid-1"}
appURL := "https://counter-cross.grafana.net/"
cfg := backend.NewGrafanaCfg(map[string]string{backend.AppURL: appURL})
ctx := backend.WithGrafanaConfig(context.Background(), cfg)

before := counterValue(t, ds.Type, appURL, ds.UID)
ok := Observe(ctx,
Observation{Datasource: ds, Rows: DefaultRowsThreshold},
Thresholds{Rows: DefaultRowsThreshold})

require.True(t, ok)
assert.Equal(t, before+1, counterValue(t, ds.Type, appURL, ds.UID))
}

func TestObserve_NoCross_DoesNotIncrementCounter(t *testing.T) {
swapLogger(t)

ds := backend.DataSourceInstanceSettings{Type: "counter-nocross", UID: "ds-uid-2"}

before := counterValue(t, ds.Type, "", ds.UID)
ok := Observe(context.Background(),
Observation{Datasource: ds, Rows: 10},
Thresholds{Rows: 1000})

require.False(t, ok)
assert.Equal(t, before, counterValue(t, ds.Type, "", ds.UID))
}

// counterValue reads the large-responses counter for a given label set.
// Returns 0 if the series has not been created yet.
func counterValue(t *testing.T, dsType, appURL, dsUID string) float64 {
t.Helper()
c, err := largeResponsesCounter.GetMetricWithLabelValues(dsType, appURL, dsUID)
require.NoError(t, err)
m := &dto.Metric{}
require.NoError(t, c.Write(m))
if m.Counter == nil {
return 0
}
return m.Counter.GetValue()
}

// swapLogger replaces backend.Logger with a recording logger for the
// duration of the test and returns it. Tests using this cannot run in
// parallel with each other because backend.Logger is a global.
Expand Down
Loading