master
go 895 lines 25 KB
Raw
1 package sql
2
3 import (
4 "context"
5 "os"
6 "testing"
7
8 "github.com/DATA-DOG/go-sqlmock"
9 "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/cloudauth"
10 "github.com/stretchr/testify/assert"
11 "github.com/stretchr/testify/require"
12
13 "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/collecttest"
14 )
15
16 var (
17 dataConfigJSON, _ = os.ReadFile("testdata/config.json")
18 dataConfigYAML, _ = os.ReadFile("testdata/config.yaml")
19 )
20
21 func TestCollector_ConfigurationSerialize(t *testing.T) {
22 collecttest.TestConfigurationSerialize(t, &Collector{}, dataConfigJSON, dataConfigYAML)
23 }
24
25 func TestCollector_Charts(t *testing.T) {
26 // Default: Charts() returns non-nil (metrics mode)
27 assert.NotNil(t, New().Charts())
28
29 // With metrics configured, Charts() returns non-nil
30 c := New()
31 c.Config.Metrics = []ConfigMetricBlock{{ID: "test"}}
32 assert.NotNil(t, c.Charts())
33
34 // Function-only mode: Charts() returns nil
35 c2 := New()
36 c2.Config.FunctionOnly = true
37 assert.Nil(t, c2.Charts())
38
39 // Combined mode (function_only: false with both): Charts() returns non-nil
40 c3 := New()
41 c3.Config.Metrics = []ConfigMetricBlock{{ID: "test"}}
42 c3.Config.Functions = []ConfigFunction{{ID: "test", Query: "SELECT 1"}}
43 assert.NotNil(t, c3.Charts())
44 }
45
46 func TestCollector_Init_ConfigValidation(t *testing.T) {
47 tests := map[string]struct {
48 setup func(*Collector)
49 wantFail bool
50 }{
51 "no metrics fails": {
52 setup: func(c *Collector) {
53 c.Driver = "pgx"
54 c.DSN = "postgres://user:pass@localhost/db"
55 // no metrics, function_only not set
56 },
57 wantFail: true,
58 },
59 "metrics only succeeds": {
60 setup: func(c *Collector) {
61 c.Driver = "pgx"
62 c.DSN = "postgres://user:pass@localhost/db"
63 c.Metrics = []ConfigMetricBlock{{
64 ID: "test",
65 Mode: "columns",
66 Query: "SELECT 1 AS val",
67 Charts: []ConfigChartConfig{{
68 Title: "test", Context: "test", Family: "test", Units: "x",
69 Dims: []ConfigDimConfig{{Name: "val", Source: "val"}},
70 }},
71 }}
72 },
73 wantFail: false,
74 },
75 "function_only with functions succeeds": {
76 setup: func(c *Collector) {
77 c.Driver = "pgx"
78 c.DSN = "postgres://user:pass@localhost/db"
79 c.FunctionOnly = true
80 c.Functions = []ConfigFunction{{ID: "test", Query: "SELECT 1"}}
81 },
82 wantFail: false,
83 },
84 "function_only without functions fails": {
85 setup: func(c *Collector) {
86 c.Driver = "pgx"
87 c.DSN = "postgres://user:pass@localhost/db"
88 c.FunctionOnly = true
89 // no functions
90 },
91 wantFail: true,
92 },
93 "function_only with metrics fails": {
94 setup: func(c *Collector) {
95 c.Driver = "pgx"
96 c.DSN = "postgres://user:pass@localhost/db"
97 c.FunctionOnly = true
98 c.Functions = []ConfigFunction{{ID: "func", Query: "SELECT 1"}}
99 c.Metrics = []ConfigMetricBlock{{
100 ID: "test",
101 Mode: "columns",
102 Query: "SELECT 1 AS val",
103 Charts: []ConfigChartConfig{{
104 Title: "test", Context: "test", Family: "test", Units: "x",
105 Dims: []ConfigDimConfig{{Name: "val", Source: "val"}},
106 }},
107 }}
108 },
109 wantFail: true,
110 },
111 "combined metrics and functions succeeds": {
112 setup: func(c *Collector) {
113 c.Driver = "pgx"
114 c.DSN = "postgres://user:pass@localhost/db"
115 c.Metrics = []ConfigMetricBlock{{
116 ID: "test",
117 Mode: "columns",
118 Query: "SELECT 1 AS val",
119 Charts: []ConfigChartConfig{{
120 Title: "test", Context: "test", Family: "test", Units: "x",
121 Dims: []ConfigDimConfig{{Name: "val", Source: "val"}},
122 }},
123 }}
124 c.Functions = []ConfigFunction{{ID: "func", Query: "SELECT 1"}}
125 },
126 wantFail: false,
127 },
128 "missing driver fails": {
129 setup: func(c *Collector) {
130 c.Driver = ""
131 c.DSN = "postgres://user:pass@localhost/db"
132 c.FunctionOnly = true
133 c.Functions = []ConfigFunction{{ID: "test", Query: "SELECT 1"}}
134 },
135 wantFail: true,
136 },
137 "missing dsn fails": {
138 setup: func(c *Collector) {
139 c.Driver = "pgx"
140 c.DSN = ""
141 c.FunctionOnly = true
142 c.Functions = []ConfigFunction{{ID: "test", Query: "SELECT 1"}}
143 },
144 wantFail: true,
145 },
146 "azure_ad with unsupported driver fails": {
147 setup: func(c *Collector) {
148 c.Driver = "mysql"
149 c.DSN = "user:pass@tcp(localhost:3306)/"
150 c.FunctionOnly = true
151 c.Functions = []ConfigFunction{{ID: "test", Query: "SELECT 1"}}
152 c.CloudAuth.Provider = "azure_ad"
153 c.CloudAuth.AzureAD = &cloudauth.AzureADAuthConfig{
154 Mode: "default",
155 }
156 },
157 wantFail: true,
158 },
159 "azure_ad service principal missing secret fails": {
160 setup: func(c *Collector) {
161 c.Driver = "pgx"
162 c.DSN = "postgres://user@localhost/db"
163 c.FunctionOnly = true
164 c.Functions = []ConfigFunction{{ID: "test", Query: "SELECT 1"}}
165 c.CloudAuth.Provider = "azure_ad"
166 c.CloudAuth.AzureAD = &cloudauth.AzureADAuthConfig{
167 Mode: "service_principal",
168 ModeServicePrincipal: &cloudauth.AzureADModeServicePrincipalConfig{
169 TenantID: "tenant",
170 ClientID: "client",
171 },
172 }
173 },
174 wantFail: true,
175 },
176 "azuresql driver accepted": {
177 setup: func(c *Collector) {
178 c.Driver = "azuresql"
179 c.DSN = "sqlserver://example.database.windows.net?database=master&fedauth=ActiveDirectoryDefault"
180 c.FunctionOnly = true
181 c.Functions = []ConfigFunction{{ID: "test", Query: "SELECT 1"}}
182 },
183 wantFail: false,
184 },
185 }
186
187 for name, tc := range tests {
188 t.Run(name, func(t *testing.T) {
189 c := New()
190 tc.setup(c)
191
192 err := c.Init(context.Background())
193
194 if tc.wantFail {
195 assert.Error(t, err)
196 } else {
197 assert.NoError(t, err)
198 }
199 })
200 }
201 }
202
203 func TestCollector_Check_FunctionOnly(t *testing.T) {
204 db, mock, err := sqlmock.New()
205 require.NoError(t, err)
206 defer func() { _ = db.Close() }()
207
208 c := New()
209 c.db = db
210 c.Driver = "pgx"
211 c.DSN = "postgres://user:pass@localhost/db"
212 c.FunctionOnly = true
213 c.Functions = []ConfigFunction{{ID: "test", Query: "SELECT 1"}}
214
215 require.NoError(t, c.Init(context.Background()))
216
217 // Check should succeed without collecting metrics
218 err = c.Check(context.Background())
219 assert.NoError(t, err)
220 assert.NoError(t, mock.ExpectationsWereMet())
221 }
222
223 func TestCollector_Collect_FunctionOnly(t *testing.T) {
224 db, _, err := sqlmock.New()
225 require.NoError(t, err)
226 defer func() { _ = db.Close() }()
227
228 c := New()
229 c.db = db
230 c.Driver = "pgx"
231 c.DSN = "postgres://user:pass@localhost/db"
232 c.FunctionOnly = true
233 c.Functions = []ConfigFunction{{ID: "test", Query: "SELECT 1"}}
234
235 require.NoError(t, c.Init(context.Background()))
236
237 // Collect should return nil in function-only mode
238 mx := c.Collect(context.Background())
239 assert.Nil(t, mx)
240 }
241
242 func TestCollector_Cleanup(t *testing.T) {
243 tests := map[string]func(t *testing.T) (collr *Collector, cleanup func()){
244 "db connection not initialized": func(t *testing.T) (collr *Collector, cleanup func()) {
245 return New(), func() {}
246 },
247 "db connection initialized": func(t *testing.T) (collr *Collector, cleanup func()) {
248 db, mock, err := sqlmock.New()
249 require.NoError(t, err)
250
251 mock.ExpectClose()
252 collr = New()
253 collr.db = db
254 cleanup = func() { _ = db.Close() }
255
256 return collr, cleanup
257 },
258 }
259
260 for name, prepare := range tests {
261 t.Run(name, func(t *testing.T) {
262 collr, cleanup := prepare(t)
263 defer cleanup()
264
265 assert.NotPanics(t, func() { collr.Cleanup(context.Background()) })
266 assert.Nil(t, collr.db)
267 })
268 }
269 }
270
271 func TestCollector_Check(t *testing.T) {
272 const query = "SELECT 1 AS value"
273
274 tests := map[string]struct {
275 prepareMock func(t *testing.T, m sqlmock.Sqlmock)
276 wantFail bool
277 }{
278 "success when metrics collected": {
279 wantFail: false,
280 prepareMock: func(t *testing.T, m sqlmock.Sqlmock) {
281 rows := sqlmock.NewRows([]string{"value"}).AddRow("1")
282 m.ExpectQuery(query).WillReturnRows(rows).RowsWillBeClosed()
283 },
284 },
285 "error when query fails": {
286 wantFail: true,
287 prepareMock: func(t *testing.T, m sqlmock.Sqlmock) {
288 m.ExpectQuery(query).WillReturnError(assert.AnError)
289 },
290 },
291 }
292
293 for name, test := range tests {
294 t.Run(name, func(t *testing.T) {
295 db, mock, err := sqlmock.New(
296 sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual),
297 )
298 require.NoError(t, err)
299 defer func() { _ = db.Close() }()
300
301 collr := New()
302 collr.db = db
303 collr.Driver = "pgx"
304 collr.DSN = "postgres://user:pass@localhost/db"
305 collr.Metrics = []ConfigMetricBlock{
306 {
307 ID: "m1",
308 Mode: "columns",
309 Query: query,
310 Charts: []ConfigChartConfig{
311 {
312 Title: "test",
313 Context: "pg.test",
314 Family: "test",
315 Units: "events",
316 Dims: []ConfigDimConfig{
317 {Name: "value", Source: "value"},
318 },
319 },
320 },
321 },
322 }
323
324 require.NoError(t, collr.Init(context.Background()))
325
326 test.prepareMock(t, mock)
327
328 if test.wantFail {
329 assert.Error(t, collr.Check(context.Background()))
330 } else {
331 assert.NoError(t, collr.Check(context.Background()))
332 }
333 assert.NoError(t, mock.ExpectationsWereMet())
334 })
335 }
336 }
337
338 func TestCollector_Collect(t *testing.T) {
339
340 type testCase struct {
341 prepare func(t *testing.T, mock sqlmock.Sqlmock, coll *Collector)
342 check func(t *testing.T, mx map[string]int64)
343 }
344
345 tests := map[string]testCase{
346 "columns: metric columns without labels (bgwriter-like)": {
347 prepare: func(t *testing.T, mock sqlmock.Sqlmock, coll *Collector) {
348 query := `
349 SELECT
350 checkpoints_timed,
351 checkpoints_req,
352 checkpoint_write_time,
353 checkpoint_sync_time,
354 buffers_checkpoint_bytes,
355 buffers_clean_bytes,
356 maxwritten_clean,
357 buffers_backend_bytes,
358 buffers_backend_fsync,
359 buffers_alloc_bytes
360 `
361 rows := sqlmock.NewRows([]string{
362 "checkpoints_timed",
363 "checkpoints_req",
364 "checkpoint_write_time",
365 "checkpoint_sync_time",
366 "buffers_checkpoint_bytes",
367 "buffers_clean_bytes",
368 "maxwritten_clean",
369 "buffers_backend_bytes",
370 "buffers_backend_fsync",
371 "buffers_alloc_bytes",
372 }).AddRow(
373 "1814",
374 "16",
375 "167",
376 "47",
377 "32768",
378 "0",
379 "0",
380 "0",
381 "0",
382 "27295744",
383 )
384
385 mock.ExpectQuery(query).WillReturnRows(rows).RowsWillBeClosed()
386
387 coll.Driver = "pgx"
388 coll.DSN = "postgres://user:pass@localhost/db"
389 coll.Metrics = []ConfigMetricBlock{
390 {
391 ID: "bgwriter",
392 Mode: "columns",
393 Query: query,
394 Charts: []ConfigChartConfig{
395 {
396 Title: "bgwriter",
397 Context: "pg.bgwriter",
398 Family: "bgwriter",
399 Units: "bytes",
400 Dims: []ConfigDimConfig{
401 {Name: "checkpoints_timed", Source: "checkpoints_timed"},
402 {Name: "checkpoints_req", Source: "checkpoints_req"},
403 {Name: "checkpoint_write_time", Source: "checkpoint_write_time"},
404 {Name: "checkpoint_sync_time", Source: "checkpoint_sync_time"},
405 {Name: "buffers_checkpoint_bytes", Source: "buffers_checkpoint_bytes"},
406 {Name: "buffers_clean_bytes", Source: "buffers_clean_bytes"},
407 {Name: "maxwritten_clean", Source: "maxwritten_clean"},
408 {Name: "buffers_backend_bytes", Source: "buffers_backend_bytes"},
409 {Name: "buffers_backend_fsync", Source: "buffers_backend_fsync"},
410 {Name: "buffers_alloc_bytes", Source: "buffers_alloc_bytes"},
411 },
412 },
413 },
414 },
415 }
416 },
417 check: func(t *testing.T, mx map[string]int64) {
418 chartID := "pgx_bgwriter_pg.bgwriter"
419
420 expected := map[string]int64{
421 buildDimID(chartID, "checkpoints_timed"): 1814,
422 buildDimID(chartID, "checkpoints_req"): 16,
423 buildDimID(chartID, "checkpoint_write_time"): 167,
424 buildDimID(chartID, "checkpoint_sync_time"): 47,
425 buildDimID(chartID, "buffers_checkpoint_bytes"): 32768,
426 buildDimID(chartID, "buffers_clean_bytes"): 0,
427 buildDimID(chartID, "maxwritten_clean"): 0,
428 buildDimID(chartID, "buffers_backend_bytes"): 0,
429 buildDimID(chartID, "buffers_backend_fsync"): 0,
430 buildDimID(chartID, "buffers_alloc_bytes"): 27295744,
431 }
432
433 for k, want := range expected {
434 got, ok := mx[k]
435 require.True(t, ok, "expected metric %s", k)
436 assert.EqualValues(t, want, got, "metric %s", k)
437 }
438 },
439 },
440
441 "columns: metrics as columns with label (datname)": {
442 prepare: func(t *testing.T, mock sqlmock.Sqlmock, coll *Collector) {
443 query := `
444 SELECT
445 datname,
446 confl_tablespace,
447 confl_lock,
448 confl_snapshot,
449 confl_bufferpin,
450 confl_deadlock
451 `
452 rows := sqlmock.NewRows([]string{
453 "datname",
454 "confl_tablespace",
455 "confl_lock",
456 "confl_snapshot",
457 "confl_bufferpin",
458 "confl_deadlock",
459 }).
460 AddRow("postgres", "0", "0", "0", "0", "0").
461 AddRow("production", "0", "0", "0", "0", "0")
462
463 mock.ExpectQuery(query).WillReturnRows(rows).RowsWillBeClosed()
464
465 coll.Driver = "pgx"
466 coll.DSN = "postgres://user:pass@localhost/db"
467 coll.Metrics = []ConfigMetricBlock{
468 {
469 ID: "conflicts",
470 Mode: "columns",
471 Query: query,
472 LabelsFromRow: []ConfigLabelFromRow{
473 {Source: "datname", Name: "db"},
474 },
475 Charts: []ConfigChartConfig{
476 {
477 Title: "conflicts",
478 Context: "pg.conflicts",
479 Family: "conflicts",
480 Units: "conflicts",
481 Dims: []ConfigDimConfig{
482 {Name: "confl_tablespace", Source: "confl_tablespace"},
483 {Name: "confl_lock", Source: "confl_lock"},
484 {Name: "confl_snapshot", Source: "confl_snapshot"},
485 {Name: "confl_bufferpin", Source: "confl_bufferpin"},
486 {Name: "confl_deadlock", Source: "confl_deadlock"},
487 },
488 },
489 },
490 },
491 }
492 },
493 check: func(t *testing.T, mx map[string]int64) {
494 chartPostgres := "pgx_conflicts_pg.conflicts_postgres"
495 chartProduction := "pgx_conflicts_pg.conflicts_production"
496
497 keys := []string{
498 "confl_tablespace",
499 "confl_lock",
500 "confl_snapshot",
501 "confl_bufferpin",
502 "confl_deadlock",
503 }
504
505 for _, dim := range keys {
506 k1 := buildDimID(chartPostgres, dim)
507 k2 := buildDimID(chartProduction, dim)
508
509 v1, ok1 := mx[k1]
510 v2, ok2 := mx[k2]
511
512 require.True(t, ok1, "expected metric %s", k1)
513 require.True(t, ok2, "expected metric %s", k2)
514
515 assert.EqualValues(t, 0, v1, "metric %s", k1)
516 assert.EqualValues(t, 0, v2, "metric %s", k2)
517 }
518 },
519 },
520
521 "columns: single column table": {
522 prepare: func(t *testing.T, mock sqlmock.Sqlmock, coll *Collector) {
523 query := `SELECT extract`
524 rows := sqlmock.NewRows([]string{"extract"}).
525 AddRow("499906.075943")
526
527 mock.ExpectQuery(query).WillReturnRows(rows).RowsWillBeClosed()
528
529 coll.Driver = "pgx"
530 coll.DSN = "postgres://user:pass@localhost/db"
531 coll.Metrics = []ConfigMetricBlock{
532 {
533 ID: "uptime",
534 Mode: "columns",
535 Query: query,
536 Charts: []ConfigChartConfig{
537 {
538 Title: "uptime",
539 Context: "pg.uptime",
540 Family: "uptime",
541 Units: "seconds",
542 Dims: []ConfigDimConfig{
543 {Name: "extract", Source: "extract"},
544 },
545 },
546 },
547 },
548 }
549 },
550 check: func(t *testing.T, mx map[string]int64) {
551 chartID := "pgx_uptime_pg.uptime"
552 key := buildDimID(chartID, "extract")
553
554 got, ok := mx[key]
555 require.True(t, ok, "expected metric %s", key)
556 // 499906.075943 -> 499906 after truncation
557 assert.EqualValues(t, 499906, got)
558 },
559 },
560
561 "kv: metric names as row keys (state -> count)": {
562 prepare: func(t *testing.T, mock sqlmock.Sqlmock, coll *Collector) {
563 query := `
564 SELECT
565 state,
566 count
567 `
568 rows := sqlmock.NewRows([]string{"state", "count"}).
569 AddRow("active", "1").
570 AddRow("idle", "14").
571 AddRow("idle in transaction", "7").
572 AddRow("idle in transaction (aborted)", "1").
573 AddRow("fastpath function call", "1").
574 AddRow("disabled", "1")
575
576 mock.ExpectQuery(query).WillReturnRows(rows).RowsWillBeClosed()
577
578 coll.Driver = "pgx"
579 coll.DSN = "postgres://user:pass@localhost/db"
580 coll.Metrics = []ConfigMetricBlock{
581 {
582 ID: "activity_states",
583 Mode: "kv",
584 Query: query,
585 KVMode: &ConfigKVMode{
586 NameCol: "state",
587 ValueCol: "count",
588 },
589 Charts: []ConfigChartConfig{
590 {
591 Title: "activity states",
592 Context: "pg.activity_states",
593 Family: "activity states",
594 Units: "events",
595 Dims: []ConfigDimConfig{
596 {Name: "active", Source: "active"},
597 {Name: "idle", Source: "idle"},
598 {Name: "idle_in_transaction", Source: "idle in transaction"},
599 {Name: "idle_in_transaction_aborted", Source: "idle in transaction (aborted)"},
600 {Name: "fastpath_function_call", Source: "fastpath function call"},
601 {Name: "disabled", Source: "disabled"},
602 },
603 },
604 },
605 },
606 }
607 },
608 check: func(t *testing.T, mx map[string]int64) {
609 chartID := "pgx_activity_states_pg.activity_states"
610
611 expected := map[string]int64{
612 buildDimID(chartID, "active"): 1,
613 buildDimID(chartID, "idle"): 14,
614 buildDimID(chartID, "idle_in_transaction"): 7,
615 buildDimID(chartID, "idle_in_transaction_aborted"): 1,
616 buildDimID(chartID, "fastpath_function_call"): 1,
617 buildDimID(chartID, "disabled"): 1,
618 }
619
620 for k, want := range expected {
621 got, ok := mx[k]
622 require.True(t, ok, "expected metric %s", k)
623 assert.EqualValues(t, want, got, "metric %s", k)
624 }
625 },
626 },
627
628 "columns: state-type metric with label from state": {
629 prepare: func(t *testing.T, mock sqlmock.Sqlmock, coll *Collector) {
630 query := `
631 SELECT
632 datname,
633 state,
634 xact_running_time,
635 query_running_time
636 `
637 rows := sqlmock.NewRows([]string{
638 "datname",
639 "state",
640 "xact_running_time",
641 "query_running_time",
642 }).
643 AddRow("some_db", "idle in transaction", "574.530219", "574.315061").
644 AddRow("some_db", "idle in transaction", "574.867167", "574.330322").
645 AddRow("postgres", "active", "0.000000", "0.000000").
646 AddRow("some_db", "idle in transaction", "574.807256", "574.377105").
647 AddRow("some_db", "idle in transaction", "574.680244", "574.357246").
648 AddRow("some_db", "idle in transaction", "574.800283", "574.330328").
649 AddRow("some_db", "idle in transaction", "574.396730", "574.290165").
650 AddRow("some_db", "idle in transaction", "574.665428", "574.337164")
651
652 mock.ExpectQuery(query).WillReturnRows(rows).RowsWillBeClosed()
653
654 coll.Driver = "pgx"
655 coll.DSN = "postgres://user:pass@localhost/db"
656 coll.Metrics = []ConfigMetricBlock{
657 {
658 ID: "xact_state",
659 Mode: "columns",
660 Query: query,
661 LabelsFromRow: []ConfigLabelFromRow{
662 {Source: "state", Name: "state"},
663 },
664 Charts: []ConfigChartConfig{
665 {
666 Title: "activity",
667 Context: "pg.activity",
668 Family: "activity",
669 Units: "seconds",
670 Dims: []ConfigDimConfig{
671 {Name: "xact_running_time", Source: "xact_running_time"},
672 {Name: "query_running_time", Source: "query_running_time"},
673 },
674 },
675 },
676 },
677 }
678 },
679 check: func(t *testing.T, mx map[string]int64) {
680 // Chart instances are split by state label.
681 idleChartID := "pgx_xact_state_pg.activity_idle in transaction"
682 activeChartID := "pgx_xact_state_pg.activity_active"
683
684 xactIdleKey := buildDimID(idleChartID, "xact_running_time")
685 queryIdleKey := buildDimID(idleChartID, "query_running_time")
686 xactActiveKey := buildDimID(activeChartID, "xact_running_time")
687 queryActiveKey := buildDimID(activeChartID, "query_running_time")
688
689 // idle in transaction: aggregated sum over multiple rows -> > 0
690 xIdle, ok := mx[xactIdleKey]
691 require.True(t, ok, "expected metric %s", xactIdleKey)
692 assert.Greater(t, xIdle, int64(0))
693
694 qIdle, ok := mx[queryIdleKey]
695 require.True(t, ok, "expected metric %s", queryIdleKey)
696 assert.Greater(t, qIdle, int64(0))
697
698 // active row has 0 durations.
699 xAct, ok := mx[xactActiveKey]
700 require.True(t, ok, "expected metric %s", xactActiveKey)
701 assert.EqualValues(t, 0, xAct)
702
703 qAct, ok := mx[queryActiveKey]
704 require.True(t, ok, "expected metric %s", queryActiveKey)
705 assert.EqualValues(t, 0, qAct)
706 },
707 },
708
709 "columns: state-type metric with state mapped to metrics": {
710 prepare: func(t *testing.T, mock sqlmock.Sqlmock, coll *Collector) {
711 query := `
712 SELECT
713 datname,
714 state,
715 xact_running_time,
716 query_running_time
717 `
718 rows := sqlmock.NewRows([]string{
719 "datname",
720 "state",
721 "xact_running_time",
722 "query_running_time",
723 }).
724 AddRow("some_db", "idle in transaction", "574.530219", "574.315061").
725 AddRow("some_db", "idle in transaction", "574.867167", "574.330322").
726 AddRow("postgres", "active", "0.000000", "0.000000").
727 AddRow("some_db", "idle in transaction", "574.807256", "574.377105").
728 AddRow("some_db", "idle in transaction", "574.680244", "574.357246").
729 AddRow("some_db", "idle in transaction", "574.800283", "574.330328").
730 AddRow("some_db", "idle in transaction", "574.396730", "574.290165").
731 AddRow("some_db", "idle in transaction", "574.665428", "574.337164")
732
733 mock.ExpectQuery(query).WillReturnRows(rows).RowsWillBeClosed()
734
735 coll.Driver = "pgx"
736 coll.DSN = "postgres://user:pass@localhost/db"
737 coll.Metrics = []ConfigMetricBlock{
738 {
739 ID: "xact_state_map",
740 Mode: "columns",
741 Query: query,
742 Charts: []ConfigChartConfig{
743 {
744 Title: "activity",
745 Context: "pg.activity",
746 Family: "activity",
747 Units: "status",
748 Dims: []ConfigDimConfig{
749 {
750 Name: "state_active",
751 Source: "state",
752 StatusWhen: &ConfigStatusWhen{
753 Equals: "active",
754 },
755 },
756 {
757 Name: "state_idle_in_transaction",
758 Source: "state",
759 StatusWhen: &ConfigStatusWhen{
760 Equals: "idle in transaction",
761 },
762 },
763 },
764 },
765 },
766 },
767 }
768 },
769 check: func(t *testing.T, mx map[string]int64) {
770 chartID := "pgx_xact_state_map_pg.activity"
771
772 activeKey := buildDimID(chartID, "state_active")
773 idleKey := buildDimID(chartID, "state_idle_in_transaction")
774
775 active, ok := mx[activeKey]
776 require.True(t, ok, "expected metric %s", activeKey)
777 assert.EqualValues(t, 1, active)
778
779 idle, ok := mx[idleKey]
780 require.True(t, ok, "expected metric %s", idleKey)
781 assert.EqualValues(t, 1, idle)
782 },
783 },
784 }
785
786 for name, tt := range tests {
787 t.Run(name, func(t *testing.T) {
788 db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual))
789 require.NoError(t, err)
790 defer func() { _ = db.Close() }()
791
792 coll := New()
793 coll.db = db
794
795 tt.prepare(t, mock, coll)
796
797 require.NoError(t, coll.Init(context.Background()))
798
799 mx := coll.Collect(context.Background())
800 require.NotNil(t, mx)
801
802 tt.check(t, mx)
803
804 assert.NoError(t, mock.ExpectationsWereMet())
805 })
806 }
807 }
808
809 func TestRedactDSN(t *testing.T) {
810 tests := map[string]struct {
811 input string
812 expected string
813 }{
814 "simple user:pass@host": {
815 input: "user:password@localhost",
816 expected: "user:****@localhost",
817 },
818 "simple user:pass@host:port": {
819 input: "user:password@localhost:5432",
820 expected: "user:****@localhost:5432",
821 },
822 "no credentials just host": {
823 input: "localhost:5432",
824 expected: "localhost:5432",
825 },
826 "URL without credentials": {
827 input: "postgresql://localhost/dbname",
828 expected: "postgresql://localhost/dbname",
829 },
830 "colon in host but no password": {
831 input: "localhost:5432/db",
832 expected: "localhost:5432/db",
833 },
834 "empty string": {
835 input: "",
836 expected: "",
837 },
838 "just scheme": {
839 input: "postgresql://",
840 expected: "postgresql://",
841 },
842 "simple user@host (no password)": {
843 input: "user@localhost",
844 expected: "****@localhost",
845 },
846 "postgresql URL with password": {
847 input: "postgresql://user:password@localhost/dbname",
848 expected: "postgresql://user:****@localhost/dbname",
849 },
850 "postgresql URL with password and port": {
851 input: "postgresql://user:password@localhost:5432/dbname",
852 expected: "postgresql://user:****@localhost:5432/dbname",
853 },
854 "postgresql URL without password": {
855 input: "postgresql://user@localhost/dbname",
856 expected: "postgresql://****@localhost/dbname",
857 },
858 "mysql URL with password": {
859 input: "mysql://root:secret@localhost:3306/mydb",
860 expected: "mysql://root:****@localhost:3306/mydb",
861 },
862 "postgres URL with complex password": {
863 input: "postgres://admin:p@ss:w0rd@localhost/db",
864 expected: "postgres://admin:****@localhost/db",
865 },
866 "URL with query params": {
867 input: "postgresql://user:pass@localhost/db?sslmode=disable",
868 expected: "postgresql://user:****@localhost/db?sslmode=disable",
869 },
870 "user with special chars in password": {
871 input: "user:p@ssw0rd!@localhost",
872 expected: "user:****@localhost",
873 },
874 "redis URL": {
875 input: "redis://user:password@localhost:6379/0",
876 expected: "redis://user:****@localhost:6379/0",
877 },
878 "mongodb URL": {
879 input: "mongodb://admin:secret@localhost:27017/mydb",
880 expected: "mongodb://admin:****@localhost:27017/mydb",
881 },
882 "URL with IP address": {
883 input: "postgresql://user:pass@192.168.1.1:5432/db",
884 expected: "postgresql://user:****@192.168.1.1:5432/db",
885 },
886 }
887
888 for name, tc := range tests {
889 t.Run(name, func(t *testing.T) {
890 got := redactDSN(tc.input)
891
892 assert.Equal(t, tc.expected, got)
893 })
894 }
895 }