improve(go.d/sql): add per-query execution time charts (#21313)
Ilya Mashchenko committed
Nov 17, 2025 at 22:30 UTC
fc1dc390bd3801d217ce042ee09190bb762d807a
5 files changed
+86
-28
src/go/plugin/go.d/collector/sql/charts.go
+38
-4
@@ -9,10 +9,11 @@ import (
9
)
10
11
const (
12
- prioChart = module.Priority + iota
12
+ prioMetricChart = module.Priority + iota
13
+ prioQueryTimingChart
14
)
15
15
-func (c *Collector) createChart(chartID string, m ConfigMetricBlock, ch ConfigChartConfig, row map[string]string) {
16
+func (c *Collector) createMetricBlockChart(chartID string, m ConfigMetricBlock, ch ConfigChartConfig, row map[string]string) {
17
if c.seenCharts[chartID] {
18
return
19
}
@@ -25,13 +26,15 @@ func (c *Collector) createChart(chartID string, m ConfigMetricBlock, ch ConfigCh
26
Type: module.ChartType(ch.Type),
27
Ctx: fmt.Sprintf("sql.%s_%s", c.Driver, ch.Context),
28
Fam: ch.Family,
28
- Priority: prioChart,
29
+ Priority: prioMetricChart,
30
+ Labels: []module.Label{
31
+ {Key: "driver", Value: c.Driver},
32
+ },
33
}
34
35
for k, v := range c.StaticLabels {
36
chart.Labels = append(chart.Labels, module.Label{Key: k, Value: v})
37
}
34
- chart.Labels = append(chart.Labels, module.Label{Key: "driver", Value: c.Driver})
38
39
for _, lf := range m.LabelsFromRow {
40
if v, ok := row[lf.Source]; ok {
@@ -51,3 +54,34 @@ func (c *Collector) createChart(chartID string, m ConfigMetricBlock, ch ConfigCh
54
c.Warningf("failed to add chart %q: %v", chartID, err)
55
}
56
}
57
+
58
+func (c *Collector) createQueryTimingChart(chartID, label string) {
59
+ if c.seenCharts[chartID] {
60
+ return
61
+ }
62
+ c.seenCharts[chartID] = true
63
+
64
+ chart := &module.Chart{
65
+ ID: chartID,
66
+ Title: "SQL query execution time",
67
+ Units: "ms",
68
+ Ctx: fmt.Sprintf("sql.%s_query_time", c.Driver),
69
+ Fam: "Query/Timings",
70
+ Priority: prioQueryTimingChart,
71
+ Labels: []module.Label{
72
+ {Key: "driver", Value: c.Driver},
73
+ {Key: "query_id", Value: label},
74
+ },
75
+ Dims: []*module.Dim{
76
+ {
77
+ ID: buildDimID(chartID, "duration"),
78
+ Name: "duration",
79
+ },
80
+ },
81
+ }
82
+
83
+ if err := c.Charts().Add(chart); err != nil {
84
+ c.Warningf("failed to add query timing chart %q: %v", chartID, err)
85
+ return
86
+ }
87
+}
src/go/plugin/go.d/collector/sql/collect.go
+41
-14
@@ -16,30 +16,32 @@ import (
16
17
func (c *Collector) collect(ctx context.Context) (map[string]int64, error) {
18
if c.db == nil {
19
- if err := c.openConnection(); err != nil {
19
+ if err := c.openConnection(ctx); err != nil {
20
return nil, err
21
}
22
}
23
24
- qcache, _, err := c.execReusableQueries(ctx)
24
+ qcache, qdur, err := c.execReusableQueries(ctx)
25
if err != nil {
26
return nil, err
27
}
28
29
- mcache, _, err := c.execMetricQueries(ctx, qcache)
29
+ mcache, mdur, err := c.execMetricQueries(ctx, qcache)
30
if err != nil {
31
return nil, err
32
}
33
34
mx := make(map[string]int64)
35
+
36
if err := c.collectMetrics(mx, mcache); err != nil {
37
return nil, err
38
}
39
+ c.collectQueryTimingMetrics(mx, qdur, mdur)
40
41
return mx, nil
42
}
43
42
-func (c *Collector) collectMetrics(mx map[string]int64, mcache QueryRowsCache) error {
44
+func (c *Collector) collectMetrics(mx map[string]int64, mcache queryRowsCache) error {
45
for i, m := range c.Metrics {
46
rows, ok := mcache[m.ID]
47
if !ok {
@@ -65,11 +67,11 @@ func (c *Collector) collectMetrics(mx map[string]int64, mcache QueryRowsCache) e
67
func (c *Collector) collectMetricsModeColumns(mx map[string]int64, m ConfigMetricBlock, rows []map[string]string) error {
68
for _, ch := range m.Charts {
69
for _, row := range rows {
68
- chartID := c.buildChartID(m, ch, row)
70
+ chartID := c.buildMetricChartID(m, ch, row)
71
if chartID == "" {
72
continue
73
}
72
- c.createChart(chartID, m, ch, row)
74
+ c.createMetricBlockChart(chartID, m, ch, row)
75
76
for _, d := range ch.Dims {
77
raw, ok := row[d.Source]
@@ -100,11 +102,11 @@ func (c *Collector) collectMetricsModeKV(mx map[string]int64, m ConfigMetricBloc
102
103
for _, ch := range m.Charts {
104
for _, row := range rows {
103
- chartID := c.buildChartID(m, ch, row)
105
+ chartID := c.buildMetricChartID(m, ch, row)
106
if chartID == "" {
107
continue
108
}
107
- c.createChart(chartID, m, ch, row)
109
+ c.createMetricBlockChart(chartID, m, ch, row)
110
111
k, ok1 := row[nameCol]
112
vraw, ok2 := row[valCol]
@@ -130,6 +132,22 @@ func (c *Collector) collectMetricsModeKV(mx map[string]int64, m ConfigMetricBloc
132
return nil
133
}
134
135
+func (c *Collector) collectQueryTimingMetrics(mx map[string]int64, qdur, mdur map[string]int64) {
136
+ collect := func(durations map[string]int64) {
137
+ // Reusable queries: label is <query_id>
138
+ // Metric-block inline queries: label is <metric_block_id>
139
+ for qid, dur := range durations {
140
+ chartID := c.buildTimingChartIDFromQueryID(qid)
141
+ c.createQueryTimingChart(chartID, qid)
142
+
143
+ dimID := buildDimID(chartID, "duration")
144
+ mx[dimID] = dur
145
+ }
146
+ }
147
+ collect(qdur)
148
+ collect(mdur)
149
+}
150
+
151
func (c *Collector) evalStatusWhen(sw *ConfigStatusWhen, value string) bool {
152
switch {
153
case sw.Equals != "":
@@ -143,28 +161,31 @@ func (c *Collector) evalStatusWhen(sw *ConfigStatusWhen, value string) bool {
161
}
162
}
163
146
-func (c *Collector) openConnection() error {
164
+func (c *Collector) openConnection(ctx context.Context) error {
165
db, err := sql.Open(c.Driver, c.DSN)
166
if err != nil {
167
return fmt.Errorf("open %s: %w (dsn=%s)", c.Driver, err, redactDSN(c.DSN))
168
}
169
152
- db.SetConnMaxLifetime(time.Minute * 10)
170
+ db.SetConnMaxLifetime(10 * time.Minute)
171
154
- ctx, cancel := context.WithTimeout(context.Background(), c.Timeout.Duration())
172
+ pingCtx := ctx
173
+ cancel := func() {}
174
+ if d := c.Timeout.Duration(); d > 0 {
175
+ pingCtx, cancel = context.WithTimeout(ctx, d)
176
+ }
177
defer cancel()
178
157
- if err := db.PingContext(ctx); err != nil {
179
+ if err := db.PingContext(pingCtx); err != nil {
180
_ = db.Close()
181
return fmt.Errorf("ping %s: %w (dsn=%s)", c.Driver, err, redactDSN(c.DSN))
182
}
183
184
c.db = db
163
-
185
return nil
186
}
187
167
-func (c *Collector) buildChartID(m ConfigMetricBlock, ch ConfigChartConfig, row map[string]string) string {
188
+func (c *Collector) buildMetricChartID(m ConfigMetricBlock, ch ConfigChartConfig, row map[string]string) string {
189
var b strings.Builder
190
b.Grow(128)
191
@@ -181,6 +202,12 @@ func (c *Collector) buildChartID(m ConfigMetricBlock, ch ConfigChartConfig, row
202
return normalizeID(b.String())
203
}
204
205
+func (c *Collector) buildTimingChartIDFromQueryID(queryID string) string {
206
+ // “reusable” query id or metric block id;
207
+ raw := fmt.Sprintf("%s_query_time_%s", c.Driver, queryID)
208
+ return normalizeID(raw)
209
+}
210
+
211
func buildDimID(chartID, dimName string) string {
212
return normalizeID(chartID + "." + dimName)
213
}
src/go/plugin/go.d/collector/sql/collector.go
-2
@@ -32,7 +32,6 @@ func New() *Collector {
32
},
33
charts: &module.Charts{},
34
seenCharts: make(map[string]bool),
35
- skipValues: make(map[string]bool),
35
}
36
}
37
@@ -45,7 +44,6 @@ type Collector struct {
44
db *sql.DB
45
46
seenCharts map[string]bool
48
- skipValues map[string]bool
47
}
48
49
func (c *Collector) Configuration() any {
src/go/plugin/go.d/collector/sql/metadata.yaml
+1
-1
@@ -229,7 +229,7 @@ modules:
229
- name: dsn
230
description: >
231
Database connection string (DSN). The format depends on the selected driver (
232
- ([MySQL](https://github.com/go-sql-driver/mysql#dsn-data-source-name),
232
+ [MySQL](https://github.com/go-sql-driver/mysql#dsn-data-source-name),
233
[PostgreSQL](https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING-URIS),
234
[MS SQL Server](https://github.com/denisenkom/go-mssqldb#connection-parameters-and-dsn)).
235
default_value: ""
src/go/plugin/go.d/collector/sql/query.go
+6
-7
@@ -9,13 +9,13 @@ import (
9
"time"
10
)
11
12
-// QueryRowsCache maps query_id -> slice of row maps (col -> string value)
13
-type QueryRowsCache map[string][]map[string]string
12
+// queryRowsCache maps query_id -> slice of row maps (col -> string value)
13
+type queryRowsCache map[string][]map[string]string
14
15
// execReusableQueries runs all queries declared in Config.Queries (new schema)
16
// and returns their full rowsets in memory. It also returns per-query durations (ms).
17
-func (c *Collector) execReusableQueries(ctx context.Context) (QueryRowsCache, map[string]int64, error) {
18
- cache := make(QueryRowsCache, len(c.Queries))
17
+func (c *Collector) execReusableQueries(ctx context.Context) (queryRowsCache, map[string]int64, error) {
18
+ cache := make(queryRowsCache, len(c.Queries))
19
durations := make(map[string]int64, len(c.Queries))
20
21
for i, q := range c.Queries {
@@ -40,8 +40,8 @@ func (c *Collector) execReusableQueries(ctx context.Context) (QueryRowsCache, ma
40
// execMetricQueries resolves and executes the query for each metric block.
41
// If a metric uses query_ref, it reuses rows from qcache (no re-query).
42
// If a metric has inline query, it executes it and stores the rows.
43
-func (c *Collector) execMetricQueries(ctx context.Context, qcache QueryRowsCache) (QueryRowsCache, map[string]int64, error) {
44
- cache := make(QueryRowsCache, len(c.Metrics))
43
+func (c *Collector) execMetricQueries(ctx context.Context, qcache queryRowsCache) (queryRowsCache, map[string]int64, error) {
44
+ cache := make(queryRowsCache, len(c.Metrics))
45
durations := make(map[string]int64, len(c.Metrics))
46
47
for i, m := range c.Metrics {
@@ -57,7 +57,6 @@ func (c *Collector) execMetricQueries(ctx context.Context, qcache QueryRowsCache
57
return nil, nil, fmt.Errorf("metrics[%d] query_ref %q not found in queries cache", i+1, m.QueryRef)
58
}
59
cache[m.ID] = rows
60
- durations[m.ID] = 0
60
case m.Query != "":
61
rows, dur, err := c.runSQL(ctx, m.Query)
62
if err != nil {