@cryptotaxi247 / netdata-1 / commits / fc1dc390b

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 {