1 package metrics
2
3 import (
4 "regexp"
5 "strings"
6 "testing"
7
8 "github.com/stretchr/testify/assert"
9 "gopkg.in/yaml.v3"
10 )
11
12
13
14
15 var knownMetrics = []string{
16 "archiver",
17 "autovacuum_scores",
18 "backends",
19 "backup_age_pgbackrest",
20 "backup_age_walg",
21 "bgwriter",
22 "blocking_locks",
23 "buffercache_by_db",
24 "buffercache_by_type",
25 "change_events",
26 "checkpointer",
27 "configuration_hashes",
28 "cpu_load",
29 "database_conflicts",
30 "db_size",
31 "db_size_approx",
32 "db_stats",
33 "index_hashes",
34 "index_stats",
35 "instance_up",
36 "invalid_indexes",
37 "kpi",
38 "locks",
39 "locks_mode",
40 "logical_subscriptions",
41 "pgbouncer_stats",
42 "pgbouncer_clients",
43 "pgpool_processes",
44 "pgpool_stats",
45 "privilege_changes",
46 "psutil_cpu",
47 "psutil_disk",
48 "psutil_disk_io_total",
49 "psutil_mem",
50 "reco_add_index",
51 "reco_default_public_schema",
52 "reco_disabled_triggers",
53 "reco_drop_index",
54 "reco_nested_views",
55 "reco_partial_index_candidates",
56 "reco_sprocs_wo_search_path",
57 "reco_superusers",
58 "recovery",
59 "replication",
60 "replication_slot_stats",
61 "replication_slots",
62 "sequence_health",
63 "server_log_event_counts",
64 "settings",
65 "smart_health_per_disk",
66 "sproc_hashes",
67 "sproc_stats",
68 "stat_activity",
69 "stat_lock",
70 "stat_io",
71 "stat_ssl",
72 "stat_statements",
73 "stat_statements_calls",
74 "stat_statements_no_query_text",
75 "subscription_stats",
76 "table_bloat_approx_stattuple",
77 "table_bloat_approx_summary",
78 "table_bloat_approx_summary_sql",
79 "table_hashes",
80 "table_io_stats",
81 "table_stats",
82 "table_stats_approx",
83 "unused_indexes",
84 "vmstat",
85 "wait_events",
86 "wal",
87 "wal_receiver",
88 "wal_size",
89 "wal_stats",
90 "datfrozenxid",
91 "postgres_role",
92 "archiver_pending_count",
93 }
94
95
96
97
98
99
100
101 func metricsRoot(t *testing.T) *yaml.Node {
102 t.Helper()
103 var root yaml.Node
104 assert.NoError(t, yaml.Unmarshal(defaultMetricsYAML, &root),
105 "metrics.yaml must be valid YAML")
106 assert.Equal(t, yaml.DocumentNode, root.Kind, "metrics.yaml root must be a document")
107 assert.Len(t, root.Content, 1, "metrics.yaml document must contain exactly one node")
108 top := root.Content[0]
109 assert.Equal(t, yaml.MappingNode, top.Kind, "metrics.yaml top-level must be a mapping")
110 return &root
111 }
112
113
114 func metricsBlock(t *testing.T, root *yaml.Node) *yaml.Node {
115 t.Helper()
116 top := root.Content[0]
117 for i := 0; i < len(top.Content); i += 2 {
118 if top.Content[i].Value == "metrics" {
119 return top.Content[i+1]
120 }
121 }
122 t.Fatalf("metrics.yaml: top-level 'metrics:' key not found")
123 return nil
124 }
125
126
127
128
129
130
131
132 var (
133 sqlNonTerminal = regexp.MustCompile(`(?im)^\s*(where|group\s+by|having)\b`)
134 sqlTerminal = regexp.MustCompile(`(?im)^\s*(limit|order\s+by)\b`)
135 )
136
137
138
139
140
141
142 func clauseOrderViolation(sql string) int {
143 lastTerminal := 0
144 depth := 0
145 for i, raw := range strings.Split(sql, "\n") {
146 line := raw
147
148 for _, r := range line {
149 switch r {
150 case '(':
151 depth++
152 case ')':
153 if depth > 0 {
154 depth--
155 }
156 }
157 }
158 if depth != 0 {
159 continue
160 }
161 if sqlNonTerminal.MatchString(line) {
162 if lastTerminal > 0 {
163 return i + 1
164 }
165 } else if sqlTerminal.MatchString(line) {
166 if i+1 > lastTerminal {
167 lastTerminal = i + 1
168 }
169 }
170 }
171 return 0
172 }
173
174
175
176
177
178
179
180
181 func TestAllKnownMetricsPresent(t *testing.T) {
182 a := assert.New(t)
183 block := metricsBlock(t, metricsRoot(t))
184 a.Equal(yaml.MappingNode, block.Kind, "'metrics:' block must be a mapping")
185
186 got := make(map[string]bool, len(block.Content)/2)
187 for i := 0; i < len(block.Content); i += 2 {
188 got[block.Content[i].Value] = true
189 }
190
191 for _, name := range knownMetrics {
192 a.True(got[name],
193 "metric %q missing from metrics.yaml top-level (was it silently removed by an edit?)", name)
194 }
195 }
196
197
198
199
200
201
202
203
204
205
206
207
208 func TestSQLScalarsAreStructurallyIntact(t *testing.T) {
209 a := assert.New(t)
210 block := metricsBlock(t, metricsRoot(t))
211
212 for i := 0; i < len(block.Content); i += 2 {
213 name := block.Content[i].Value
214 def := block.Content[i+1]
215 if def.Kind != yaml.MappingNode {
216 continue
217 }
218 var sqlsNode *yaml.Node
219 for j := 0; j < len(def.Content); j += 2 {
220 if def.Content[j].Value == "sqls" {
221 sqlsNode = def.Content[j+1]
222 break
223 }
224 }
225 if sqlsNode == nil || sqlsNode.Kind != yaml.MappingNode {
226 continue
227 }
228
229 for k := 0; k < len(sqlsNode.Content); k += 2 {
230 version := sqlsNode.Content[k].Value
231 scalar := sqlsNode.Content[k+1]
232 if scalar.Kind != yaml.ScalarNode {
233 a.Failf("expected literal block scalar",
234 "%s sqls[%s]: expected a literal block scalar, got kind=%d (%q)",
235 name, version, scalar.Kind, scalar.Tag)
236 continue
237 }
238 if line := clauseOrderViolation(scalar.Value); line > 0 {
239 a.Failf("scalar-tail bleed detected",
240 "%s sqls[%s]: terminal clause precedes non-terminal clause at line %d. Tail: %q",
241 name, version, line, tail(scalar.Value, 200))
242 }
243 }
244 }
245 }
246
247
248
249
250
251
252
253 func TestSQLScalarBleedRegression(t *testing.T) {
254 a := assert.New(t)
255 const fixture = `
256 metrics:
257 metric_a:
258 sqls:
259 14: |-
260 select /* pgwatch_generated */
261 1::int as a_value
262 from
263 some_table
264 LIMIT 300
265 19: |-
266 select /* pgwatch_generated */
267 1::int as a_value
268 from
269 some_table
270 LIMIT 300
271 where s.datname = current_database()
272 and s.state = 'active'
273 group by s.query
274 metric_b:
275 sqls:
276 14: |-
277 select /* pgwatch_generated */
278 1::int as count
279 from some_view s
280 where s.datname = current_database()
281 and s.state = 'active'
282 group by s.query
283 `
284
285 var root yaml.Node
286 a.NoError(yaml.Unmarshal([]byte(fixture), &root),
287 "synthetic YAML must still parse (Go absorbs the bleed silently)")
288
289 block := metricsBlock(t, &root)
290 metricA := findMetricNode(t, block, "metric_a")
291 sqls := findChildMapping(t, metricA, "sqls")
292
293 v14 := sqlScalarValue(t, sqls, "14")
294 a.Zero(clauseOrderViolation(v14),
295 "clean v14 should pass the invariant but failed:\n%s", v14)
296
297 v19 := sqlScalarValue(t, sqls, "19")
298 a.Contains(v19, "where s.datname",
299 "fixture is wrong — bleed lines missing from v19 scalar:\n%s", v19)
300 a.NotZero(clauseOrderViolation(v19),
301 "guard did NOT catch the bleed — v19 scalar still looks OK:\n%s", v19)
302 }
303
304 func findMetricNode(t *testing.T, block *yaml.Node, name string) *yaml.Node {
305 t.Helper()
306 if block.Kind != yaml.MappingNode {
307 t.Fatalf("metrics block is not a mapping (kind=%d)", block.Kind)
308 }
309 for i := 0; i < len(block.Content); i += 2 {
310 if block.Content[i].Value == name {
311 return block.Content[i+1]
312 }
313 }
314 t.Fatalf("metric %q not found", name)
315 return nil
316 }
317
318 func findChildMapping(t *testing.T, parent *yaml.Node, key string) *yaml.Node {
319 t.Helper()
320 if parent.Kind != yaml.MappingNode {
321 t.Fatalf("parent is not a mapping (kind=%d)", parent.Kind)
322 }
323 for i := 0; i < len(parent.Content); i += 2 {
324 if parent.Content[i].Value == key {
325 if parent.Content[i+1].Kind != yaml.MappingNode {
326 t.Fatalf("%q child is not a mapping (kind=%d)", key, parent.Content[i+1].Kind)
327 }
328 return parent.Content[i+1]
329 }
330 }
331 t.Fatalf("child %q not found", key)
332 return nil
333 }
334
335 func sqlScalarValue(t *testing.T, sqls *yaml.Node, version string) string {
336 t.Helper()
337 for i := 0; i < len(sqls.Content); i += 2 {
338 if sqls.Content[i].Value == version {
339 return sqls.Content[i+1].Value
340 }
341 }
342 t.Fatalf("sqls[%s] not found", version)
343 return ""
344 }
345
346 func tail(s string, n int) string {
347 if len(s) <= n {
348 return s
349 }
350 return "..." + s[len(s)-n:]
351 }
352