master
go 1,358 lines 38.7 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package mssql
4
5 import (
6 "context"
7 "database/sql"
8 "fmt"
9 "strconv"
10 "strings"
11 "time"
12
13 "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/cloudauth/sqladapter"
14 )
15
16 // noLatencySentinel is the value SQL Server returns when no latency data is available
17 const noLatencySentinel = 999999
18
19 const (
20 maxKBToBytes = int64(1<<63-1) / 1024
21 maxKBToCentiPercent = int64(1<<63-1) / 10000
22 )
23
24 func (c *Collector) collect() (map[string]int64, error) {
25 if c.db == nil {
26 db, err := c.openConnection()
27 if err != nil {
28 return nil, err
29 }
30 c.db = db
31 }
32
33 if c.version == "" {
34 ver, err := c.queryVersion()
35 if err != nil {
36 return nil, fmt.Errorf("failed to query version: %v", err)
37 }
38 c.version = ver
39 c.majorVersion = parseMajorVersion(c.version)
40 c.Debugf("connected to SQL Server version %s (major: %d)", c.version, c.majorVersion)
41 }
42
43 if !c.hadrChecked {
44 if err := c.checkHadrEnabled(); err != nil {
45 c.Debugf("HADR check failed: %v", err)
46 }
47 c.hadrChecked = true
48 }
49
50 mx := make(map[string]int64)
51
52 if err := c.collectInstanceMetrics(mx); err != nil {
53 return nil, err
54 }
55 if err := c.collectDatabaseMetrics(mx); err != nil {
56 return nil, err
57 }
58 if err := c.collectLockMetrics(mx); err != nil {
59 return nil, err
60 }
61 if err := c.collectWaitStats(mx); err != nil {
62 return nil, err
63 }
64 if err := c.collectJobStatus(mx); err != nil {
65 return nil, err
66 }
67 if err := c.collectReplicationStatus(mx); err != nil {
68 return nil, err
69 }
70 if c.hadrEnabled {
71 if err := c.collectAvailabilityGroups(mx); err != nil {
72 c.Warningf("AG metrics collection failed: %v", err)
73 }
74 }
75
76 return mx, nil
77 }
78
79 func (c *Collector) openConnection() (*sql.DB, error) {
80 driverName, dsn, err := c.resolveConnectionParams()
81 if err != nil {
82 return nil, err
83 }
84
85 db, err := sql.Open(driverName, dsn)
86 if err != nil {
87 return nil, fmt.Errorf("error opening connection: %v", err)
88 }
89
90 db.SetMaxOpenConns(1)
91 db.SetMaxIdleConns(1)
92 db.SetConnMaxLifetime(10 * time.Minute)
93
94 ctx, cancel := context.WithTimeout(context.Background(), c.Timeout.Duration())
95 defer cancel()
96
97 if err := db.PingContext(ctx); err != nil {
98 _ = db.Close()
99 return nil, fmt.Errorf("error pinging database: %v", err)
100 }
101
102 return db, nil
103 }
104
105 func (c *Collector) resolveConnectionParams() (string, string, error) {
106 driverName := sqladapter.MSSQLDriver(c.CloudAuth)
107 dsn := c.DSN
108 if c.CloudAuth.IsEnabled() {
109 var err error
110 dsn, err = sqladapter.BuildMSSQLAzureADDSN(c.DSN, c.CloudAuth)
111 if err != nil {
112 return "", "", fmt.Errorf("error preparing cloud auth SQL Server DSN: %v", err)
113 }
114 }
115 return driverName, dsn, nil
116 }
117
118 func (c *Collector) queryVersion() (string, error) {
119 ctx, cancel := context.WithTimeout(context.Background(), c.Timeout.Duration())
120 defer cancel()
121
122 var version string
123 err := c.db.QueryRowContext(ctx, queryVersion).Scan(&version)
124 if err != nil {
125 return "", err
126 }
127
128 return version, nil
129 }
130
131 func (c *Collector) collectInstanceMetrics(mx map[string]int64) error {
132 if err := c.collectUserConnections(mx); err != nil {
133 return err
134 }
135 if err := c.collectBlockedProcesses(mx); err != nil {
136 return err
137 }
138 if err := c.collectBatchRequests(mx); err != nil {
139 return err
140 }
141 if err := c.collectCompilations(mx); err != nil {
142 return err
143 }
144 if err := c.collectSQLErrors(mx); err != nil {
145 return err
146 }
147 if err := c.collectBufferManager(mx); err != nil {
148 return err
149 }
150 if err := c.collectMemoryManager(mx); err != nil {
151 return err
152 }
153 if err := c.collectAccessMethods(mx); err != nil {
154 return err
155 }
156 // Process and OS memory metrics (always collected)
157 if err := c.collectProcessMemory(mx); err != nil {
158 return err
159 }
160 if err := c.collectOSMemory(mx); err != nil {
161 return err
162 }
163
164 return nil
165 }
166
167 func (c *Collector) collectUserConnections(mx map[string]int64) error {
168 ctx, cancel := context.WithTimeout(context.Background(), c.Timeout.Duration())
169 defer cancel()
170
171 var userConns, sysConns int64
172 err := c.db.QueryRowContext(ctx, queryUserConnections).Scan(&userConns, &sysConns)
173 if err != nil {
174 return fmt.Errorf("user connections query failed: %v", err)
175 }
176
177 mx["user_connections"] = userConns
178 // Session connections: user vs internal (system)
179 mx["session_connections_user"] = userConns
180 mx["session_connections_internal"] = sysConns
181
182 return nil
183 }
184
185 func (c *Collector) collectBlockedProcesses(mx map[string]int64) error {
186 ctx, cancel := context.WithTimeout(context.Background(), c.Timeout.Duration())
187 defer cancel()
188
189 var blocked int64
190 err := c.db.QueryRowContext(ctx, queryBlockedProcesses).Scan(&blocked)
191 if err != nil {
192 return fmt.Errorf("blocked processes query failed: %v", err)
193 }
194
195 mx["blocked_processes"] = blocked
196
197 return nil
198 }
199
200 func (c *Collector) collectBatchRequests(mx map[string]int64) error {
201 ctx, cancel := context.WithTimeout(context.Background(), c.Timeout.Duration())
202 defer cancel()
203
204 var value int64
205 err := c.db.QueryRowContext(ctx, queryBatchRequests).Scan(&value)
206 if err != nil {
207 return fmt.Errorf("batch requests query failed: %v", err)
208 }
209
210 mx["batch_requests"] = value
211
212 return nil
213 }
214
215 func (c *Collector) collectCompilations(mx map[string]int64) error {
216 ctx, cancel := context.WithTimeout(context.Background(), c.Timeout.Duration())
217 defer cancel()
218
219 rows, err := c.db.QueryContext(ctx, queryCompilations)
220 if err != nil {
221 return fmt.Errorf("compilations query failed: %v", err)
222 }
223 defer rows.Close()
224
225 for rows.Next() {
226 var counterName string
227 var value int64
228 if err := rows.Scan(&counterName, &value); err != nil {
229 continue
230 }
231
232 counterName = strings.TrimSpace(counterName)
233 switch counterName {
234 case "SQL Compilations/sec":
235 mx["sql_compilations"] = value
236 case "SQL Re-Compilations/sec":
237 mx["sql_recompilations"] = value
238 case "Auto-Param Attempts/sec":
239 mx["auto_param_attempts"] = value
240 case "Safe Auto-Params/sec":
241 mx["auto_param_safe"] = value
242 case "Failed Auto-Params/sec":
243 mx["auto_param_failed"] = value
244 }
245 }
246
247 return rows.Err()
248 }
249
250 func (c *Collector) collectBufferManager(mx map[string]int64) error {
251 ctx, cancel := context.WithTimeout(context.Background(), c.Timeout.Duration())
252 defer cancel()
253
254 rows, err := c.db.QueryContext(ctx, queryBufferManager)
255 if err != nil {
256 return fmt.Errorf("buffer manager query failed: %v", err)
257 }
258 defer rows.Close()
259
260 var cacheHitRatio, cacheHitRatioBase int64
261
262 for rows.Next() {
263 var counterName string
264 var value int64
265 if err := rows.Scan(&counterName, &value); err != nil {
266 continue
267 }
268
269 counterName = strings.TrimSpace(counterName)
270 switch counterName {
271 case "Page reads/sec":
272 mx["buffer_page_reads"] = value
273 case "Page writes/sec":
274 mx["buffer_page_writes"] = value
275 case "Buffer cache hit ratio":
276 cacheHitRatio = value
277 case "Buffer cache hit ratio base":
278 cacheHitRatioBase = value
279 case "Checkpoint pages/sec":
280 mx["buffer_checkpoint_pages"] = value
281 case "Page life expectancy":
282 mx["buffer_page_life_expectancy"] = value
283 case "Lazy writes/sec":
284 mx["buffer_lazy_writes"] = value
285 case "Page lookups/sec":
286 mx["buffer_page_lookups"] = value
287 }
288 }
289
290 // Calculate hit ratio as percentage
291 if cacheHitRatioBase > 0 {
292 mx["buffer_cache_hit_ratio"] = (cacheHitRatio * 100) / cacheHitRatioBase
293 }
294
295 return rows.Err()
296 }
297
298 func (c *Collector) collectMemoryManager(mx map[string]int64) error {
299 ctx, cancel := context.WithTimeout(context.Background(), c.Timeout.Duration())
300 defer cancel()
301
302 rows, err := c.db.QueryContext(ctx, queryMemoryManager)
303 if err != nil {
304 return fmt.Errorf("memory manager query failed: %v", err)
305 }
306 defer rows.Close()
307
308 for rows.Next() {
309 var counterName string
310 var value int64
311 if err := rows.Scan(&counterName, &value); err != nil {
312 continue
313 }
314
315 counterName = strings.TrimSpace(counterName)
316 switch counterName {
317 case "Total Server Memory (KB)":
318 mx["memory_total"] = value * 1024 // Convert to bytes
319 case "Connection Memory (KB)":
320 mx["memory_connection"] = value * 1024
321 case "Memory Grants Pending":
322 mx["memory_pending_grants"] = value
323 case "External benefit of memory":
324 mx["memory_external_benefit"] = value
325 }
326 }
327
328 return rows.Err()
329 }
330
331 func (c *Collector) collectAccessMethods(mx map[string]int64) error {
332 ctx, cancel := context.WithTimeout(context.Background(), c.Timeout.Duration())
333 defer cancel()
334
335 var value int64
336 err := c.db.QueryRowContext(ctx, queryAccessMethods).Scan(&value)
337 if err != nil {
338 return fmt.Errorf("access methods query failed: %v", err)
339 }
340
341 mx["page_splits"] = value
342
343 return nil
344 }
345
346 func (c *Collector) collectDatabaseMetrics(mx map[string]int64) error {
347 if err := c.collectDatabaseCounters(mx); err != nil {
348 return err
349 }
350 if err := c.collectLockStatsByResourceType(mx); err != nil {
351 return err
352 }
353 if err := c.collectDatabaseSize(mx); err != nil {
354 return err
355 }
356 if err := c.collectDatabaseStatus(mx); err != nil {
357 return err
358 }
359 // Collect I/O stall and log growth metrics per database
360 if err := c.collectIOStall(mx); err != nil {
361 return err
362 }
363 if err := c.collectLogGrowths(mx); err != nil {
364 return err
365 }
366 if err := c.collectDatabaseLogCounters(mx); err != nil {
367 return err
368 }
369
370 return nil
371 }
372
373 func (c *Collector) collectDatabaseCounters(mx map[string]int64) error {
374 ctx, cancel := context.WithTimeout(context.Background(), c.Timeout.Duration())
375 defer cancel()
376
377 rows, err := c.db.QueryContext(ctx, queryDatabaseCounters)
378 if err != nil {
379 return fmt.Errorf("database counters query failed: %v", err)
380 }
381 defer rows.Close()
382
383 for rows.Next() {
384 var dbName, counterName string
385 var value int64
386 if err := rows.Scan(&dbName, &counterName, &value); err != nil {
387 continue
388 }
389
390 dbName = strings.TrimSpace(dbName)
391 counterName = strings.TrimSpace(counterName)
392
393 if !c.seenDatabases[dbName] {
394 c.seenDatabases[dbName] = true
395 c.addDatabaseCharts(dbName)
396 }
397
398 dbID := cleanDatabaseName(dbName)
399 switch counterName {
400 case "Active Transactions":
401 mx[fmt.Sprintf("database_%s_active_transactions", dbID)] = value
402 case "Transactions/sec":
403 mx[fmt.Sprintf("database_%s_transactions", dbID)] = value
404 case "Write Transactions/sec":
405 mx[fmt.Sprintf("database_%s_write_transactions", dbID)] = value
406 case "Backup/Restore Throughput/sec":
407 mx[fmt.Sprintf("database_%s_backup_restore_throughput", dbID)] = value
408 case "Log Bytes Flushed/sec":
409 mx[fmt.Sprintf("database_%s_log_flushed", dbID)] = value
410 case "Log Flushes/sec":
411 mx[fmt.Sprintf("database_%s_log_flushes", dbID)] = value
412 }
413 }
414
415 return rows.Err()
416 }
417
418 type databaseLogCounters struct {
419 sizeKB int64
420 usedKB int64
421 truncations int64
422 shrinks int64
423 hasSize bool
424 hasUsed bool
425 hasTruncations bool
426 hasShrinks bool
427 hasKnownCounter bool
428 }
429
430 func (c *Collector) collectDatabaseLogCounters(mx map[string]int64) error {
431 ctx, cancel := context.WithTimeout(context.Background(), c.Timeout.Duration())
432 defer cancel()
433
434 rows, err := c.db.QueryContext(ctx, queryDatabaseLogCounters)
435 if err != nil {
436 return fmt.Errorf("database log counters query failed: %v", err)
437 }
438 defer rows.Close()
439
440 counters := make(map[string]*databaseLogCounters)
441
442 for rows.Next() {
443 var dbName, counterName string
444 var value int64
445 if err := rows.Scan(&dbName, &counterName, &value); err != nil {
446 continue
447 }
448 if value < 0 {
449 continue
450 }
451
452 dbName = strings.TrimSpace(dbName)
453 counterName = strings.TrimSpace(counterName)
454 if dbName == "" {
455 continue
456 }
457
458 if !c.seenDatabasesWithLog[dbName] {
459 c.seenDatabasesWithLog[dbName] = true
460 c.addDatabaseLogCharts(dbName)
461 }
462
463 if counters[dbName] == nil {
464 counters[dbName] = &databaseLogCounters{}
465 }
466
467 switch counterName {
468 case "Log File(s) Size (KB)":
469 counters[dbName].sizeKB = value
470 counters[dbName].hasSize = true
471 counters[dbName].hasKnownCounter = true
472 case "Log File(s) Used Size (KB)":
473 counters[dbName].usedKB = value
474 counters[dbName].hasUsed = true
475 counters[dbName].hasKnownCounter = true
476 case "Log Truncations":
477 counters[dbName].truncations = value
478 counters[dbName].hasTruncations = true
479 counters[dbName].hasKnownCounter = true
480 case "Log Shrinks":
481 counters[dbName].shrinks = value
482 counters[dbName].hasShrinks = true
483 counters[dbName].hasKnownCounter = true
484 }
485 }
486
487 if err := rows.Err(); err != nil {
488 return err
489 }
490
491 for dbName, values := range counters {
492 if values == nil || !values.hasKnownCounter {
493 continue
494 }
495
496 dbID := cleanDatabaseName(dbName)
497
498 if values.hasSize && values.hasUsed && values.sizeKB > 0 &&
499 values.sizeKB <= maxKBToBytes && values.usedKB <= maxKBToBytes {
500 usedBytes := values.usedKB * 1024
501 freeKB := max(values.sizeKB-values.usedKB, 0)
502
503 mx[fmt.Sprintf("database_%s_log_size_used", dbID)] = usedBytes
504 mx[fmt.Sprintf("database_%s_log_size_free", dbID)] = freeKB * 1024
505
506 if values.usedKB <= maxKBToCentiPercent {
507 mx[fmt.Sprintf("database_%s_log_percent_used", dbID)] = values.usedKB * 10000 / values.sizeKB
508 }
509 }
510
511 if values.hasTruncations {
512 mx[fmt.Sprintf("database_%s_log_truncations", dbID)] = values.truncations
513 }
514 if values.hasShrinks {
515 mx[fmt.Sprintf("database_%s_log_shrinks", dbID)] = values.shrinks
516 }
517 }
518
519 return nil
520 }
521
522 func (c *Collector) collectLockStatsByResourceType(mx map[string]int64) error {
523 ctx, cancel := context.WithTimeout(context.Background(), c.Timeout.Duration())
524 defer cancel()
525
526 rows, err := c.db.QueryContext(ctx, queryDatabaseLocks)
527 if err != nil {
528 return fmt.Errorf("lock stats query failed: %v", err)
529 }
530 defer rows.Close()
531
532 for rows.Next() {
533 var resourceType, counterName string
534 var value int64
535 if err := rows.Scan(&resourceType, &counterName, &value); err != nil {
536 continue
537 }
538
539 resourceType = strings.TrimSpace(resourceType)
540 counterName = strings.TrimSpace(counterName)
541
542 if !c.seenLockStatsTypes[resourceType] {
543 c.seenLockStatsTypes[resourceType] = true
544 c.addLockStatsCharts(resourceType)
545 }
546
547 // Note: instance_name from Locks counter is the lock resource type, not database name
548 resID := cleanResourceTypeName(resourceType)
549 switch counterName {
550 case "Number of Deadlocks/sec":
551 mx[fmt.Sprintf("lock_stats_%s_deadlocks", resID)] = value
552 case "Lock Waits/sec":
553 mx[fmt.Sprintf("lock_stats_%s_waits", resID)] = value
554 case "Lock Timeouts/sec":
555 mx[fmt.Sprintf("lock_stats_%s_timeouts", resID)] = value
556 case "Lock Requests/sec":
557 mx[fmt.Sprintf("lock_stats_%s_requests", resID)] = value
558 }
559 }
560
561 return rows.Err()
562 }
563
564 func (c *Collector) collectDatabaseSize(mx map[string]int64) error {
565 ctx, cancel := context.WithTimeout(context.Background(), c.Timeout.Duration())
566 defer cancel()
567
568 rows, err := c.db.QueryContext(ctx, queryDatabaseSize)
569 if err != nil {
570 return fmt.Errorf("database size query failed: %v", err)
571 }
572 defer rows.Close()
573
574 for rows.Next() {
575 var dbName string
576 var size int64
577 if err := rows.Scan(&dbName, &size); err != nil {
578 continue
579 }
580
581 dbName = strings.TrimSpace(dbName)
582
583 if !c.seenDatabases[dbName] {
584 c.seenDatabases[dbName] = true
585 c.addDatabaseCharts(dbName)
586 }
587
588 dbID := cleanDatabaseName(dbName)
589 mx[fmt.Sprintf("database_%s_data_file_size", dbID)] = size
590 }
591
592 return rows.Err()
593 }
594
595 func (c *Collector) collectLockMetrics(mx map[string]int64) error {
596 ctx, cancel := context.WithTimeout(context.Background(), c.Timeout.Duration())
597 defer cancel()
598
599 rows, err := c.db.QueryContext(ctx, queryLocksByResource)
600 if err != nil {
601 return fmt.Errorf("locks by resource query failed: %v", err)
602 }
603 defer rows.Close()
604
605 for rows.Next() {
606 var resourceType string
607 var count int64
608 if err := rows.Scan(&resourceType, &count); err != nil {
609 continue
610 }
611
612 resourceType = strings.TrimSpace(resourceType)
613
614 if !c.seenLockTypes[resourceType] {
615 c.seenLockTypes[resourceType] = true
616 c.addLockResourceCharts(resourceType)
617 }
618
619 resID := cleanResourceTypeName(resourceType)
620 mx[fmt.Sprintf("locks_%s_count", resID)] = count
621 }
622
623 return rows.Err()
624 }
625
626 func (c *Collector) collectWaitStats(mx map[string]int64) error {
627 ctx, cancel := context.WithTimeout(context.Background(), c.Timeout.Duration())
628 defer cancel()
629
630 rows, err := c.db.QueryContext(ctx, queryWaitStats)
631 if err != nil {
632 return fmt.Errorf("wait stats query failed: %v", err)
633 }
634 defer rows.Close()
635
636 for rows.Next() {
637 var waitType string
638 var totalWait, resourceWait, signalWait, maxWait, waitingTasks int64
639 if err := rows.Scan(&waitType, &totalWait, &resourceWait, &signalWait, &maxWait, &waitingTasks); err != nil {
640 continue
641 }
642
643 waitType = strings.TrimSpace(waitType)
644 waitCategory := getWaitCategory(waitType)
645
646 if !c.seenWaitTypes[waitType] {
647 c.seenWaitTypes[waitType] = true
648 c.addWaitTypeCharts(waitType, waitCategory)
649 }
650
651 waitID := cleanWaitTypeName(waitType)
652 mx[fmt.Sprintf("wait_%s_total_ms", waitID)] = totalWait
653 mx[fmt.Sprintf("wait_%s_resource_ms", waitID)] = resourceWait
654 mx[fmt.Sprintf("wait_%s_signal_ms", waitID)] = signalWait
655 mx[fmt.Sprintf("wait_%s_max_ms", waitID)] = maxWait
656 mx[fmt.Sprintf("wait_%s_tasks", waitID)] = waitingTasks
657 }
658
659 return rows.Err()
660 }
661
662 func (c *Collector) collectJobStatus(mx map[string]int64) error {
663 ctx, cancel := context.WithTimeout(context.Background(), c.Timeout.Duration())
664 defer cancel()
665
666 rows, err := c.db.QueryContext(ctx, queryJobs)
667 if err != nil {
668 return fmt.Errorf("jobs query failed: %v", err)
669 }
670 defer rows.Close()
671
672 for rows.Next() {
673 var jobName string
674 var enabled int64
675 if err := rows.Scan(&jobName, &enabled); err != nil {
676 continue
677 }
678
679 jobName = strings.TrimSpace(jobName)
680
681 if !c.seenJobs[jobName] {
682 c.seenJobs[jobName] = true
683 c.addJobCharts(jobName)
684 }
685
686 jobID := cleanJobName(jobName)
687 if enabled == 1 {
688 mx[fmt.Sprintf("job_%s_enabled", jobID)] = 1
689 mx[fmt.Sprintf("job_%s_disabled", jobID)] = 0
690 } else {
691 mx[fmt.Sprintf("job_%s_enabled", jobID)] = 0
692 mx[fmt.Sprintf("job_%s_disabled", jobID)] = 1
693 }
694 }
695
696 return rows.Err()
697 }
698
699 func (c *Collector) collectSQLErrors(mx map[string]int64) error {
700 ctx, cancel := context.WithTimeout(context.Background(), c.Timeout.Duration())
701 defer cancel()
702
703 var value int64
704 err := c.db.QueryRowContext(ctx, querySQLErrors).Scan(&value)
705 if err != nil {
706 return fmt.Errorf("sql errors query failed: %v", err)
707 }
708
709 mx["sql_errors_total"] = value
710
711 return nil
712 }
713
714 func (c *Collector) collectDatabaseStatus(mx map[string]int64) error {
715 ctx, cancel := context.WithTimeout(context.Background(), c.Timeout.Duration())
716 defer cancel()
717
718 rows, err := c.db.QueryContext(ctx, queryDatabaseStatus)
719 if err != nil {
720 return fmt.Errorf("database status query failed: %v", err)
721 }
722 defer rows.Close()
723
724 for rows.Next() {
725 var dbName string
726 var state int64
727 var isReadOnly bool
728 if err := rows.Scan(&dbName, &state, &isReadOnly); err != nil {
729 continue
730 }
731
732 dbName = strings.TrimSpace(dbName)
733
734 if !c.seenDatabases[dbName] {
735 c.seenDatabases[dbName] = true
736 c.addDatabaseCharts(dbName)
737 }
738
739 dbID := cleanDatabaseName(dbName)
740
741 // Database state values:
742 // 0 = ONLINE, 1 = RESTORING, 2 = RECOVERING, 3 = RECOVERY_PENDING
743 // 4 = SUSPECT, 5 = EMERGENCY, 6 = OFFLINE
744 mx[fmt.Sprintf("database_%s_state_online", dbID)] = boolToInt(state == 0)
745 mx[fmt.Sprintf("database_%s_state_restoring", dbID)] = boolToInt(state == 1)
746 mx[fmt.Sprintf("database_%s_state_recovering", dbID)] = boolToInt(state == 2)
747 mx[fmt.Sprintf("database_%s_state_pending", dbID)] = boolToInt(state == 3)
748 mx[fmt.Sprintf("database_%s_state_suspect", dbID)] = boolToInt(state == 4)
749 mx[fmt.Sprintf("database_%s_state_emergency", dbID)] = boolToInt(state == 5)
750 mx[fmt.Sprintf("database_%s_state_offline", dbID)] = boolToInt(state == 6)
751
752 // Read-only status
753 mx[fmt.Sprintf("database_%s_read_only", dbID)] = boolToInt(isReadOnly)
754 mx[fmt.Sprintf("database_%s_read_write", dbID)] = boolToInt(!isReadOnly)
755 }
756
757 return rows.Err()
758 }
759
760 func (c *Collector) collectReplicationStatus(mx map[string]int64) error {
761 ctx, cancel := context.WithTimeout(context.Background(), c.Timeout.Duration())
762 defer cancel()
763
764 // First collect monitor data (status, latency, etc.)
765 rows, err := c.db.QueryContext(ctx, queryReplicationStatus)
766 if err != nil {
767 // Replication may not be configured, don't treat as error
768 c.Debugf("replication status query failed (may not be configured): %v", err)
769 return nil
770 }
771 defer rows.Close()
772
773 for rows.Next() {
774 var pubDB, publication string
775 var status, warning, worstLatency, bestLatency, avgLatency, runningAgents int64
776 if err := rows.Scan(&pubDB, &publication, &status, &warning, &worstLatency, &bestLatency, &avgLatency, &runningAgents); err != nil {
777 c.Debugf("replication scan error: %v", err)
778 continue
779 }
780
781 pubDB = strings.TrimSpace(pubDB)
782 publication = strings.TrimSpace(publication)
783
784 pubKey := pubDB + "_" + publication
785 if !c.seenReplications[pubKey] {
786 c.seenReplications[pubKey] = true
787 c.addReplicationCharts(pubDB, publication)
788 }
789
790 pubID := cleanPublicationName(pubDB, publication)
791
792 // Decode status into 6 discrete states (matching C implementation)
793 // 1=started, 2=succeeded, 3=in_progress, 4=idle, 5=retrying, 6=failed
794 mx[fmt.Sprintf("replication_%s_status_started", pubID)] = boolToInt(status == 1)
795 mx[fmt.Sprintf("replication_%s_status_succeeded", pubID)] = boolToInt(status == 2)
796 mx[fmt.Sprintf("replication_%s_status_in_progress", pubID)] = boolToInt(status == 3)
797 mx[fmt.Sprintf("replication_%s_status_idle", pubID)] = boolToInt(status == 4)
798 mx[fmt.Sprintf("replication_%s_status_retrying", pubID)] = boolToInt(status == 5)
799 mx[fmt.Sprintf("replication_%s_status_failed", pubID)] = boolToInt(status == 6)
800
801 // Decode warning into 7 individual flags (bitfield)
802 // Bit 0x01: expiration, 0x02: latency, 0x04: mergeexpiration
803 // 0x08: mergeslowrunduration, 0x10: mergefastrunduration
804 // 0x20: mergefastrunspeed, 0x40: mergeslowrunspeed
805 mx[fmt.Sprintf("replication_%s_warning_expiration", pubID)] = boolToInt(warning&0x01 != 0)
806 mx[fmt.Sprintf("replication_%s_warning_latency", pubID)] = boolToInt(warning&0x02 != 0)
807 mx[fmt.Sprintf("replication_%s_warning_mergeexpiration", pubID)] = boolToInt(warning&0x04 != 0)
808 mx[fmt.Sprintf("replication_%s_warning_mergeslowrunduration", pubID)] = boolToInt(warning&0x08 != 0)
809 mx[fmt.Sprintf("replication_%s_warning_mergefastrunduration", pubID)] = boolToInt(warning&0x10 != 0)
810 mx[fmt.Sprintf("replication_%s_warning_mergefastrunspeed", pubID)] = boolToInt(warning&0x20 != 0)
811 mx[fmt.Sprintf("replication_%s_warning_mergeslowrunspeed", pubID)] = boolToInt(warning&0x40 != 0)
812
813 mx[fmt.Sprintf("replication_%s_latency_avg", pubID)] = avgLatency
814 // Handle the noLatencySentinel for "no value" (only bestLatency uses sentinel in query)
815 if bestLatency == noLatencySentinel {
816 bestLatency = 0
817 }
818 mx[fmt.Sprintf("replication_%s_latency_best", pubID)] = bestLatency
819 mx[fmt.Sprintf("replication_%s_latency_worst", pubID)] = worstLatency
820 mx[fmt.Sprintf("replication_%s_agents_running", pubID)] = runningAgents
821 }
822
823 if err := rows.Err(); err != nil {
824 return err
825 }
826
827 // Now collect subscription counts from MSpublications/MSsubscriptions
828 rows2, err := c.db.QueryContext(ctx, querySubscriptionCount)
829 if err != nil {
830 c.Debugf("subscription count query failed: %v", err)
831 return nil
832 }
833 defer rows2.Close()
834
835 for rows2.Next() {
836 var pubDB, publication string
837 var subCount int64
838 if err := rows2.Scan(&pubDB, &publication, &subCount); err != nil {
839 continue
840 }
841
842 pubDB = strings.TrimSpace(pubDB)
843 publication = strings.TrimSpace(publication)
844 pubID := cleanPublicationName(pubDB, publication)
845 mx[fmt.Sprintf("replication_%s_subscriptions", pubID)] = subCount
846 }
847
848 return rows2.Err()
849 }
850
851 func boolToInt(b bool) int64 {
852 if b {
853 return 1
854 }
855 return 0
856 }
857
858 // parseMajorVersion extracts the major version from a string like "16.0.4175.1"
859 func parseMajorVersion(version string) int {
860 parts := strings.SplitN(version, ".", 2)
861 if len(parts) == 0 {
862 return 0
863 }
864 v, err := strconv.Atoi(parts[0])
865 if err != nil {
866 return 0
867 }
868 return v
869 }
870
871 func (c *Collector) checkHadrEnabled() error {
872 ctx, cancel := context.WithTimeout(context.Background(), c.Timeout.Duration())
873 defer cancel()
874
875 var enabled sql.NullInt64
876 err := c.db.QueryRowContext(ctx, queryHadrEnabled).Scan(&enabled)
877 if err != nil {
878 return fmt.Errorf("HADR enabled check failed: %v", err)
879 }
880
881 c.hadrEnabled = enabled.Valid && enabled.Int64 == 1
882
883 if c.hadrEnabled {
884 c.Debugf("Always On Availability Groups is enabled")
885 } else {
886 c.Debugf("Always On Availability Groups is not enabled, skipping AG metrics")
887 }
888
889 return nil
890 }
891
892 // agDatabaseReplicaQuery returns the appropriate query for the SQL Server version
893 func agDatabaseReplicaQuery(majorVersion int) string {
894 if majorVersion >= 13 { // SQL Server 2016+
895 return queryAGDatabaseReplicas16
896 }
897 return queryAGDatabaseReplicasPre16 // SQL Server 2012-2014
898 }
899
900 func (c *Collector) collectAvailabilityGroups(mx map[string]int64) error {
901 if err := c.collectAGHealth(mx); err != nil {
902 return err
903 }
904 if err := c.collectAGReplicaStates(mx); err != nil {
905 return err
906 }
907 if err := c.collectAGDatabaseReplicas(mx); err != nil {
908 return err
909 }
910 if err := c.collectAGCluster(mx); err != nil {
911 c.Debugf("AG cluster query failed (WSFC may not be configured): %v", err)
912 }
913 if err := c.collectAGClusterMembers(mx); err != nil {
914 c.Debugf("AG cluster members query failed: %v", err)
915 }
916 if err := c.collectAGFailoverReadiness(mx); err != nil {
917 c.Debugf("AG failover readiness query failed: %v", err)
918 }
919 if err := c.collectAGAutoPageRepair(mx); err != nil {
920 c.Debugf("AG auto page repair query failed: %v", err)
921 }
922 if c.majorVersion >= 15 { // SQL Server 2019+
923 if err := c.collectAGThreads(mx); err != nil {
924 c.Debugf("AG threads query failed: %v", err)
925 }
926 }
927 return nil
928 }
929
930 func (c *Collector) collectAGHealth(mx map[string]int64) error {
931 ctx, cancel := context.WithTimeout(context.Background(), c.Timeout.Duration())
932 defer cancel()
933
934 rows, err := c.db.QueryContext(ctx, queryAGHealth)
935 if err != nil {
936 return fmt.Errorf("AG health query failed: %v", err)
937 }
938 defer rows.Close()
939
940 for rows.Next() {
941 var agName string
942 var syncHealth, primaryRecoveryHealth, secondaryRecoveryHealth int64
943 if err := rows.Scan(&agName, &syncHealth, &primaryRecoveryHealth, &secondaryRecoveryHealth); err != nil {
944 c.Debugf("AG health scan failed: %v", err)
945 continue
946 }
947
948 agName = strings.TrimSpace(agName)
949
950 if !c.seenAGs[agName] {
951 c.seenAGs[agName] = true
952 c.addAGCharts(agName)
953 }
954
955 agID := cleanAGName(agName)
956
957 // sync health: 0=not_healthy, 1=partially_healthy, 2=healthy
958 mx[fmt.Sprintf("ag_%s_sync_health_not_healthy", agID)] = boolToInt(syncHealth == 0)
959 mx[fmt.Sprintf("ag_%s_sync_health_partially_healthy", agID)] = boolToInt(syncHealth == 1)
960 mx[fmt.Sprintf("ag_%s_sync_health_healthy", agID)] = boolToInt(syncHealth == 2)
961
962 // primary recovery health: -1=N/A (on secondary), 0=in_progress, 1=online
963 mx[fmt.Sprintf("ag_%s_primary_recovery_online", agID)] = boolToInt(primaryRecoveryHealth == 1)
964 mx[fmt.Sprintf("ag_%s_primary_recovery_in_progress", agID)] = boolToInt(primaryRecoveryHealth == 0)
965
966 // secondary recovery health: -1=N/A (on primary), 0=in_progress, 1=online
967 mx[fmt.Sprintf("ag_%s_secondary_recovery_online", agID)] = boolToInt(secondaryRecoveryHealth == 1)
968 mx[fmt.Sprintf("ag_%s_secondary_recovery_in_progress", agID)] = boolToInt(secondaryRecoveryHealth == 0)
969 }
970
971 return rows.Err()
972 }
973
974 func (c *Collector) collectAGReplicaStates(mx map[string]int64) error {
975 ctx, cancel := context.WithTimeout(context.Background(), c.Timeout.Duration())
976 defer cancel()
977
978 rows, err := c.db.QueryContext(ctx, queryAGReplicaStates)
979 if err != nil {
980 return fmt.Errorf("AG replica states query failed: %v", err)
981 }
982 defer rows.Close()
983
984 for rows.Next() {
985 var agName, replicaServer, availMode, failoverMode string
986 var role, connState, syncHealth int64
987 if err := rows.Scan(&agName, &replicaServer, &availMode, &failoverMode,
988 &role, &connState, &syncHealth); err != nil {
989 c.Debugf("AG replica state scan failed: %v", err)
990 continue
991 }
992
993 agName = strings.TrimSpace(agName)
994 replicaServer = strings.TrimSpace(replicaServer)
995
996 replicaKey := agName + "_" + replicaServer
997 if !c.seenAGReplicas[replicaKey] {
998 c.seenAGReplicas[replicaKey] = true
999 c.addAGReplicaCharts(agName, replicaServer, availMode, failoverMode)
1000 }
1001
1002 rID := cleanAGReplicaName(agName, replicaServer)
1003
1004 // role: -1=unknown, 0=resolving, 1=primary, 2=secondary
1005 mx[fmt.Sprintf("ag_replica_%s_role_resolving", rID)] = boolToInt(role == 0)
1006 mx[fmt.Sprintf("ag_replica_%s_role_primary", rID)] = boolToInt(role == 1)
1007 mx[fmt.Sprintf("ag_replica_%s_role_secondary", rID)] = boolToInt(role == 2)
1008 mx[fmt.Sprintf("ag_replica_%s_role_unknown", rID)] = boolToInt(role == -1)
1009
1010 // connected state: -1=unknown, 0=disconnected, 1=connected
1011 mx[fmt.Sprintf("ag_replica_%s_connected", rID)] = boolToInt(connState == 1)
1012 mx[fmt.Sprintf("ag_replica_%s_disconnected", rID)] = boolToInt(connState == 0)
1013 mx[fmt.Sprintf("ag_replica_%s_conn_unknown", rID)] = boolToInt(connState == -1)
1014
1015 // sync health: 0=not_healthy, 1=partially_healthy, 2=healthy
1016 mx[fmt.Sprintf("ag_replica_%s_sync_health_not_healthy", rID)] = boolToInt(syncHealth == 0)
1017 mx[fmt.Sprintf("ag_replica_%s_sync_health_partially_healthy", rID)] = boolToInt(syncHealth == 1)
1018 mx[fmt.Sprintf("ag_replica_%s_sync_health_healthy", rID)] = boolToInt(syncHealth == 2)
1019 }
1020
1021 return rows.Err()
1022 }
1023
1024 func (c *Collector) collectAGDatabaseReplicas(mx map[string]int64) error {
1025 ctx, cancel := context.WithTimeout(context.Background(), c.Timeout.Duration())
1026 defer cancel()
1027
1028 query := agDatabaseReplicaQuery(c.majorVersion)
1029 rows, err := c.db.QueryContext(ctx, query)
1030 if err != nil {
1031 return fmt.Errorf("AG database replicas query failed: %v", err)
1032 }
1033 defer rows.Close()
1034
1035 for rows.Next() {
1036 var agName, replicaServer, dbName string
1037 var syncState, isSuspended int64
1038 var logSendQueue, logSendRate, redoQueue, redoRate, filestreamRate int64
1039 var secondaryLag int64
1040
1041 if err := rows.Scan(
1042 &agName, &replicaServer, &dbName,
1043 &syncState, &isSuspended,
1044 &logSendQueue, &logSendRate,
1045 &redoQueue, &redoRate, &filestreamRate,
1046 &secondaryLag,
1047 ); err != nil {
1048 c.Debugf("AG database replica scan failed: %v", err)
1049 continue
1050 }
1051
1052 agName = strings.TrimSpace(agName)
1053 replicaServer = strings.TrimSpace(replicaServer)
1054 dbName = strings.TrimSpace(dbName)
1055
1056 if dbName == "" {
1057 continue
1058 }
1059
1060 drKey := agName + "_" + replicaServer + "_" + dbName
1061 if !c.seenAGDatabaseReplicas[drKey] {
1062 c.seenAGDatabaseReplicas[drKey] = true
1063 c.addAGDatabaseReplicaCharts(agName, replicaServer, dbName)
1064 }
1065
1066 drID := cleanAGDatabaseReplicaName(agName, replicaServer, dbName)
1067
1068 // sync state: 0=not_synchronizing, 1=synchronizing, 2=synchronized, 3=reverting, 4=initializing
1069 mx[fmt.Sprintf("ag_db_%s_sync_state_not_synchronizing", drID)] = boolToInt(syncState == 0)
1070 mx[fmt.Sprintf("ag_db_%s_sync_state_synchronizing", drID)] = boolToInt(syncState == 1)
1071 mx[fmt.Sprintf("ag_db_%s_sync_state_synchronized", drID)] = boolToInt(syncState == 2)
1072 mx[fmt.Sprintf("ag_db_%s_sync_state_reverting", drID)] = boolToInt(syncState == 3)
1073 mx[fmt.Sprintf("ag_db_%s_sync_state_initializing", drID)] = boolToInt(syncState == 4)
1074
1075 // queue sizes and rates (already converted to bytes in SQL)
1076 mx[fmt.Sprintf("ag_db_%s_log_send_queue_size", drID)] = logSendQueue
1077 mx[fmt.Sprintf("ag_db_%s_log_send_rate", drID)] = logSendRate
1078 mx[fmt.Sprintf("ag_db_%s_redo_queue_size", drID)] = redoQueue
1079 mx[fmt.Sprintf("ag_db_%s_redo_rate", drID)] = redoRate
1080 mx[fmt.Sprintf("ag_db_%s_filestream_send_rate", drID)] = filestreamRate
1081
1082 // suspended
1083 mx[fmt.Sprintf("ag_db_%s_suspended", drID)] = isSuspended
1084 mx[fmt.Sprintf("ag_db_%s_not_suspended", drID)] = boolToInt(isSuspended == 0)
1085
1086 // secondary lag (only meaningful on SQL 2016+, -1 = not available)
1087 if secondaryLag >= 0 {
1088 mx[fmt.Sprintf("ag_db_%s_secondary_lag_seconds", drID)] = secondaryLag
1089 }
1090 }
1091
1092 return rows.Err()
1093 }
1094
1095 func (c *Collector) collectAGCluster(mx map[string]int64) error {
1096 ctx, cancel := context.WithTimeout(context.Background(), c.Timeout.Duration())
1097 defer cancel()
1098
1099 var quorumState int64
1100 err := c.db.QueryRowContext(ctx, queryAGCluster).Scan(&quorumState)
1101 if err != nil {
1102 return fmt.Errorf("AG cluster query failed: %v", err)
1103 }
1104
1105 if !c.agClusterChartAdded {
1106 c.agClusterChartAdded = true
1107 if err := c.Charts().Add(agClusterQuorumStateChart.Copy()); err != nil {
1108 c.Warning(err)
1109 }
1110 }
1111
1112 // quorum state: 0=unknown, 1=normal, 2=forced
1113 mx["ag_cluster_quorum_state_unknown"] = boolToInt(quorumState == 0)
1114 mx["ag_cluster_quorum_state_normal"] = boolToInt(quorumState == 1)
1115 mx["ag_cluster_quorum_state_forced"] = boolToInt(quorumState == 2)
1116
1117 return nil
1118 }
1119
1120 func (c *Collector) collectAGClusterMembers(mx map[string]int64) error {
1121 ctx, cancel := context.WithTimeout(context.Background(), c.Timeout.Duration())
1122 defer cancel()
1123
1124 rows, err := c.db.QueryContext(ctx, queryAGClusterMembers)
1125 if err != nil {
1126 return fmt.Errorf("AG cluster members query failed: %v", err)
1127 }
1128 defer rows.Close()
1129
1130 for rows.Next() {
1131 var memberName string
1132 var memberState, quorumVotes int64
1133 if err := rows.Scan(&memberName, &memberState, &quorumVotes); err != nil {
1134 c.Debugf("AG cluster member scan failed: %v", err)
1135 continue
1136 }
1137
1138 memberName = strings.TrimSpace(memberName)
1139
1140 if !c.seenAGClusterMembers[memberName] {
1141 c.seenAGClusterMembers[memberName] = true
1142 c.addAGClusterMemberCharts(memberName)
1143 }
1144
1145 mID := cleanAGName(memberName)
1146
1147 // member state: 0=offline, 1=online
1148 mx[fmt.Sprintf("ag_cluster_member_%s_up", mID)] = boolToInt(memberState == 1)
1149 mx[fmt.Sprintf("ag_cluster_member_%s_down", mID)] = boolToInt(memberState == 0)
1150 mx[fmt.Sprintf("ag_cluster_member_%s_quorum_votes", mID)] = quorumVotes
1151 }
1152
1153 return rows.Err()
1154 }
1155
1156 func (c *Collector) collectAGFailoverReadiness(mx map[string]int64) error {
1157 ctx, cancel := context.WithTimeout(context.Background(), c.Timeout.Duration())
1158 defer cancel()
1159
1160 rows, err := c.db.QueryContext(ctx, queryAGFailoverReadiness)
1161 if err != nil {
1162 return fmt.Errorf("AG failover readiness query failed: %v", err)
1163 }
1164 defer rows.Close()
1165
1166 for rows.Next() {
1167 var agName, replicaServer, dbName string
1168 var isFailoverReady, isDatabaseJoined int64
1169 if err := rows.Scan(&agName, &replicaServer, &dbName, &isFailoverReady, &isDatabaseJoined); err != nil {
1170 c.Debugf("AG failover readiness scan failed: %v", err)
1171 continue
1172 }
1173
1174 agName = strings.TrimSpace(agName)
1175 replicaServer = strings.TrimSpace(replicaServer)
1176 dbName = strings.TrimSpace(dbName)
1177
1178 drID := cleanAGDatabaseReplicaName(agName, replicaServer, dbName)
1179
1180 mx[fmt.Sprintf("ag_db_%s_failover_ready", drID)] = isFailoverReady
1181 mx[fmt.Sprintf("ag_db_%s_failover_not_ready", drID)] = boolToInt(isFailoverReady == 0)
1182 mx[fmt.Sprintf("ag_db_%s_joined", drID)] = isDatabaseJoined
1183 mx[fmt.Sprintf("ag_db_%s_not_joined", drID)] = boolToInt(isDatabaseJoined == 0)
1184 }
1185
1186 return rows.Err()
1187 }
1188
1189 func (c *Collector) collectAGAutoPageRepair(mx map[string]int64) error {
1190 ctx, cancel := context.WithTimeout(context.Background(), c.Timeout.Duration())
1191 defer cancel()
1192
1193 rows, err := c.db.QueryContext(ctx, queryAGAutoPageRepair)
1194 if err != nil {
1195 return fmt.Errorf("AG auto page repair query failed: %v", err)
1196 }
1197 defer rows.Close()
1198
1199 for rows.Next() {
1200 var dbName string
1201 var successfulRepairs, failedRepairs int64
1202 if err := rows.Scan(&dbName, &successfulRepairs, &failedRepairs); err != nil {
1203 c.Debugf("AG page repair scan failed: %v", err)
1204 continue
1205 }
1206
1207 dbName = strings.TrimSpace(dbName)
1208
1209 if dbName == "" {
1210 continue
1211 }
1212
1213 if !c.seenAGPageRepairDBs[dbName] {
1214 c.seenAGPageRepairDBs[dbName] = true
1215 c.addAGPageRepairCharts(dbName)
1216 }
1217
1218 dbID := cleanDatabaseName(dbName)
1219
1220 mx[fmt.Sprintf("ag_page_repair_%s_successful", dbID)] = successfulRepairs
1221 mx[fmt.Sprintf("ag_page_repair_%s_failed", dbID)] = failedRepairs
1222 }
1223
1224 return rows.Err()
1225 }
1226
1227 func (c *Collector) collectAGThreads(mx map[string]int64) error {
1228 ctx, cancel := context.WithTimeout(context.Background(), c.Timeout.Duration())
1229 defer cancel()
1230
1231 rows, err := c.db.QueryContext(ctx, queryAGThreads)
1232 if err != nil {
1233 return fmt.Errorf("AG threads query failed: %v", err)
1234 }
1235 defer rows.Close()
1236
1237 for rows.Next() {
1238 var agName string
1239 var captureThreads, redoThreads, parallelRedoThreads int64
1240 if err := rows.Scan(&agName, &captureThreads, &redoThreads, &parallelRedoThreads); err != nil {
1241 c.Debugf("AG threads scan failed: %v", err)
1242 continue
1243 }
1244
1245 agID := cleanAGName(strings.TrimSpace(agName))
1246
1247 mx[fmt.Sprintf("ag_%s_capture_threads", agID)] = captureThreads
1248 mx[fmt.Sprintf("ag_%s_redo_threads", agID)] = redoThreads
1249 mx[fmt.Sprintf("ag_%s_parallel_redo_threads", agID)] = parallelRedoThreads
1250 }
1251
1252 return rows.Err()
1253 }
1254
1255 func (c *Collector) collectProcessMemory(mx map[string]int64) error {
1256 ctx, cancel := context.WithTimeout(context.Background(), c.Timeout.Duration())
1257 defer cancel()
1258
1259 var resident, virtual, utilization, pageFaults int64
1260 err := c.db.QueryRowContext(ctx, queryProcessMemory).Scan(&resident, &virtual, &utilization, &pageFaults)
1261 if err != nil {
1262 return fmt.Errorf("process memory query failed: %v", err)
1263 }
1264
1265 mx["process_memory_resident"] = resident
1266 mx["process_memory_virtual"] = virtual
1267 mx["process_memory_utilization"] = utilization
1268 mx["process_page_faults"] = pageFaults
1269
1270 return nil
1271 }
1272
1273 func (c *Collector) collectOSMemory(mx map[string]int64) error {
1274 ctx, cancel := context.WithTimeout(context.Background(), c.Timeout.Duration())
1275 defer cancel()
1276
1277 var memUsed, memAvailable, pagefileUsed, pagefileAvailable int64
1278 err := c.db.QueryRowContext(ctx, queryOSMemory).Scan(&memUsed, &memAvailable, &pagefileUsed, &pagefileAvailable)
1279 if err != nil {
1280 return fmt.Errorf("OS memory query failed: %v", err)
1281 }
1282
1283 mx["os_memory_used"] = memUsed
1284 mx["os_memory_available"] = memAvailable
1285 mx["os_pagefile_used"] = pagefileUsed
1286 mx["os_pagefile_available"] = pagefileAvailable
1287
1288 return nil
1289 }
1290
1291 func (c *Collector) collectIOStall(mx map[string]int64) error {
1292 ctx, cancel := context.WithTimeout(context.Background(), c.Timeout.Duration())
1293 defer cancel()
1294
1295 rows, err := c.db.QueryContext(ctx, queryIOStall)
1296 if err != nil {
1297 return fmt.Errorf("IO stall query failed: %v", err)
1298 }
1299 defer rows.Close()
1300
1301 for rows.Next() {
1302 var dbName string
1303 var readMs, writeMs, totalMs int64
1304 if err := rows.Scan(&dbName, &readMs, &writeMs, &totalMs); err != nil {
1305 continue
1306 }
1307
1308 dbName = strings.TrimSpace(dbName)
1309 if dbName == "" {
1310 continue
1311 }
1312
1313 if !c.seenDatabases[dbName] {
1314 c.seenDatabases[dbName] = true
1315 c.addDatabaseCharts(dbName)
1316 }
1317
1318 dbID := cleanDatabaseName(dbName)
1319 mx[fmt.Sprintf("database_%s_io_stall_read", dbID)] = readMs
1320 mx[fmt.Sprintf("database_%s_io_stall_write", dbID)] = writeMs
1321 }
1322
1323 return rows.Err()
1324 }
1325
1326 func (c *Collector) collectLogGrowths(mx map[string]int64) error {
1327 ctx, cancel := context.WithTimeout(context.Background(), c.Timeout.Duration())
1328 defer cancel()
1329
1330 rows, err := c.db.QueryContext(ctx, queryLogGrowths)
1331 if err != nil {
1332 return fmt.Errorf("log growths query failed: %v", err)
1333 }
1334 defer rows.Close()
1335
1336 for rows.Next() {
1337 var dbName string
1338 var growths int64
1339 if err := rows.Scan(&dbName, &growths); err != nil {
1340 continue
1341 }
1342
1343 dbName = strings.TrimSpace(dbName)
1344 if dbName == "" {
1345 continue
1346 }
1347
1348 if !c.seenDatabases[dbName] {
1349 c.seenDatabases[dbName] = true
1350 c.addDatabaseCharts(dbName)
1351 }
1352
1353 dbID := cleanDatabaseName(dbName)
1354 mx[fmt.Sprintf("database_%s_log_growths", dbID)] = growths
1355 }
1356
1357 return rows.Err()
1358 }