master
go 1,678 lines 52.2 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 //go:build cgo
4
5 package db2
6
7 import (
8 "context"
9 "database/sql"
10 "fmt"
11 "slices"
12 "strconv"
13 "strings"
14 "time"
15
16 "github.com/netdata/netdata/go/plugins/pkg/stm"
17 )
18
19 const Precision = 1000 // Precision multiplier for floating-point values
20
21 func (c *Collector) collect(ctx context.Context) (map[string]int64, error) {
22 if err := c.ensureConnected(ctx); err != nil {
23 return nil, err
24 }
25 c.db = c.client.DB()
26
27 // Reset metrics
28 c.mx = &metricsData{
29 databases: make(map[string]databaseInstanceMetrics),
30 bufferpools: make(map[string]bufferpoolInstanceMetrics),
31 tablespaces: make(map[string]tablespaceInstanceMetrics),
32 connections: make(map[string]connectionInstanceMetrics),
33 tables: make(map[string]tableInstanceMetrics),
34 indexes: make(map[string]indexInstanceMetrics),
35 memoryPools: make(map[string]memoryPoolInstanceMetrics),
36 memorySets: make(map[string]memorySetInstanceMetrics),
37 tableIOs: make(map[string]tableIOInstanceMetrics),
38 }
39
40 // Collect global metrics
41 if err := c.collectGlobalMetrics(ctx); err != nil {
42 return nil, fmt.Errorf("failed to collect global metrics: %v", err)
43 }
44
45 // Collect per-instance metrics if enabled and supported
46 if c.CollectDatabaseMetrics.IsEnabled() {
47 c.Debugf("collecting database instance metrics (limit: %d)", c.MaxDatabases)
48 if err := c.collectDatabaseInstances(ctx); err != nil {
49 if isSQLFeatureError(err) {
50 c.logOnce("database_instances_unavailable", "Database instance collection failed (likely unsupported on this DB2 edition/version): %v", err)
51 } else {
52 c.Errorf("failed to collect database instances: %v", err)
53 }
54 }
55 }
56
57 if c.CollectBufferpoolMetrics.IsEnabled() {
58 c.Debugf("collecting bufferpool instance metrics (limit: %d)", c.MaxBufferpools)
59 if err := c.collectBufferpoolInstances(ctx); err != nil {
60 if isSQLFeatureError(err) {
61 c.logOnce("bufferpool_instances_unavailable", "Bufferpool instance collection failed (likely unsupported on this DB2 edition/version): %v", err)
62 } else {
63 c.Errorf("failed to collect bufferpool instances: %v", err)
64 }
65 }
66 }
67
68 if c.CollectTablespaceMetrics.IsEnabled() {
69 c.Debugf("collecting tablespace instance metrics (limit: %d)", c.MaxTablespaces)
70 if err := c.collectTablespaceInstances(ctx); err != nil {
71 if isSQLFeatureError(err) {
72 c.logOnce("tablespace_instances_unavailable", "Tablespace instance collection failed (likely unsupported on this DB2 edition/version): %v", err)
73 } else {
74 c.Errorf("failed to collect tablespace instances: %v", err)
75 }
76 }
77 }
78
79 if c.CollectConnectionMetrics.IsEnabled() {
80 c.Debugf("collecting connection instance metrics (limit: %d)", c.MaxConnections)
81 if err := c.collectConnectionInstances(ctx); err != nil {
82 if isSQLFeatureError(err) {
83 c.logOnce("connection_instances_unavailable", "Connection instance collection failed (likely unsupported on this DB2 edition/version): %v", err)
84 } else {
85 c.Errorf("failed to collect connection instances: %v", err)
86 }
87 }
88 }
89
90 if c.CollectTableMetrics.IsEnabled() {
91 c.Debugf("collecting table instance metrics (limit: %d)", c.MaxTables)
92 if err := c.collectTableInstances(ctx); err != nil {
93 if isSQLFeatureError(err) {
94 c.logOnce("table_instances_unavailable", "Table instance collection failed (likely unsupported on this DB2 edition/version): %v", err)
95 } else {
96 c.Errorf("failed to collect table instances: %v", err)
97 }
98 }
99 }
100
101 if c.CollectIndexMetrics.IsEnabled() {
102 c.Debugf("collecting index instance metrics (limit: %d)", c.MaxIndexes)
103 if err := c.collectIndexInstances(ctx); err != nil {
104 if isSQLFeatureError(err) {
105 c.logOnce("index_instances_unavailable", "Index instance collection failed (likely unsupported on this DB2 edition/version): %v", err)
106 } else {
107 c.Errorf("failed to collect index instances: %v", err)
108 }
109 }
110 }
111
112 // Collect new performance metrics
113
114 if c.CollectMemoryMetrics {
115 c.Debugf("collecting memory pool metrics")
116 if err := c.collectMemoryPoolInstances(ctx); err != nil {
117 if isSQLFeatureError(err) {
118 c.logOnce("memory_pool_unavailable", "Memory pool collection failed (likely unsupported on this DB2 edition/version): %v", err)
119 } else {
120 c.Errorf("failed to collect memory pools: %v", err)
121 }
122 }
123
124 // Screen 26: Collect Instance Memory Sets
125 c.Debugf("collecting memory set instances (Screen 26)")
126 if err := c.collectMemorySetInstances(ctx); err != nil {
127 if isSQLFeatureError(err) {
128 c.logOnce("memory_set_unavailable", "Memory set collection failed (likely unsupported on this DB2 edition/version): %v", err)
129 } else {
130 c.Errorf("failed to collect memory sets: %v", err)
131 }
132 }
133
134 // Screen 15: Collect Prefetchers
135 c.Debugf("collecting prefetcher instances (Screen 15)")
136 if err := c.collectPrefetcherInstances(ctx); err != nil {
137 if isSQLFeatureError(err) {
138 c.logOnce("prefetcher_unavailable", "Prefetcher collection failed (likely unsupported on this DB2 edition/version): %v", err)
139 } else {
140 c.Errorf("failed to collect prefetchers: %v", err)
141 }
142 }
143 }
144
145 if c.CollectWaitMetrics && c.CollectConnectionMetrics.IsEnabled() {
146 c.Debugf("collecting enhanced wait metrics")
147 if err := c.collectConnectionWaits(ctx); err != nil {
148 if isSQLFeatureError(err) {
149 c.logOnce("wait_metrics_unavailable", "Wait metrics collection failed (likely unsupported on this DB2 edition/version): %v", err)
150 } else {
151 c.Errorf("failed to collect wait metrics: %v", err)
152 }
153 }
154 }
155
156 if c.CollectTableIOMetrics {
157 c.Debugf("collecting table I/O metrics")
158 if err := c.collectTableIOInstances(ctx); err != nil {
159 if isSQLFeatureError(err) {
160 c.logOnce("table_io_unavailable", "Table I/O collection failed (likely unsupported on this DB2 edition/version): %v", err)
161 } else {
162 c.Errorf("failed to collect table I/O: %v", err)
163 }
164 }
165 }
166
167 // Build final metrics map
168 mx := stm.ToMap(c.mx)
169
170 // Add per-instance metrics
171 for name, metrics := range c.mx.databases {
172 cleanName := cleanName(name)
173 for k, v := range stm.ToMap(metrics) {
174 mx[fmt.Sprintf("database_%s_%s", cleanName, k)] = v
175 }
176 }
177
178 // Debug bufferpool count
179 if len(c.mx.bufferpools) > 0 {
180 c.Debugf("Processing %d bufferpools in mx", len(c.mx.bufferpools))
181 }
182
183 for name, metrics := range c.mx.bufferpools {
184 cleanName := cleanName(name)
185 for k, v := range stm.ToMap(metrics) {
186 mx[fmt.Sprintf("bufferpool_%s_%s", cleanName, k)] = v
187 }
188
189 // Calculate hit ratios for instance charts
190 // Overall hit ratio
191 totalReads := metrics.Hits + metrics.Misses
192 if totalReads > 0 {
193 hitRatio := float64(metrics.Hits) * 100.0 / float64(totalReads)
194 mx[fmt.Sprintf("bufferpool_%s_hit_ratio", cleanName)] = int64(hitRatio * Precision)
195 } else {
196 // No reads means 100% hit ratio (no misses)
197 mx[fmt.Sprintf("bufferpool_%s_hit_ratio", cleanName)] = 100 * Precision
198 }
199
200 // Data hit ratio
201 dataReads := metrics.DataHits + metrics.DataMisses
202 if dataReads > 0 {
203 dataHitRatio := float64(metrics.DataHits) * 100.0 / float64(dataReads)
204 mx[fmt.Sprintf("bufferpool_%s_data_hit_ratio", cleanName)] = int64(dataHitRatio * Precision)
205 } else {
206 mx[fmt.Sprintf("bufferpool_%s_data_hit_ratio", cleanName)] = 100 * Precision
207 }
208
209 // Index hit ratio
210 indexReads := metrics.IndexHits + metrics.IndexMisses
211 if indexReads > 0 {
212 indexHitRatio := float64(metrics.IndexHits) * 100.0 / float64(indexReads)
213 mx[fmt.Sprintf("bufferpool_%s_index_hit_ratio", cleanName)] = int64(indexHitRatio * Precision)
214 } else {
215 mx[fmt.Sprintf("bufferpool_%s_index_hit_ratio", cleanName)] = 100 * Precision
216 }
217
218 // XDA hit ratio
219 xdaReads := metrics.XDAHits + metrics.XDAMisses
220 if xdaReads > 0 {
221 xdaHitRatio := float64(metrics.XDAHits) * 100.0 / float64(xdaReads)
222 mx[fmt.Sprintf("bufferpool_%s_xda_hit_ratio", cleanName)] = int64(xdaHitRatio * Precision)
223 } else {
224 mx[fmt.Sprintf("bufferpool_%s_xda_hit_ratio", cleanName)] = 100 * Precision
225 }
226
227 // Column hit ratio
228 columnReads := metrics.ColumnHits + metrics.ColumnMisses
229 if columnReads > 0 {
230 columnHitRatio := float64(metrics.ColumnHits) * 100.0 / float64(columnReads)
231 mx[fmt.Sprintf("bufferpool_%s_column_hit_ratio", cleanName)] = int64(columnHitRatio * Precision)
232 } else {
233 mx[fmt.Sprintf("bufferpool_%s_column_hit_ratio", cleanName)] = 100 * Precision
234 }
235
236 // Debug
237 c.Debugf("Bufferpool %s: hits=%d, misses=%d, dataHits=%d, dataMisses=%d",
238 name, metrics.Hits, metrics.Misses, metrics.DataHits, metrics.DataMisses)
239 }
240
241 for name, metrics := range c.mx.tablespaces {
242 cleanName := cleanName(name)
243 for k, v := range stm.ToMap(metrics) {
244 mx[fmt.Sprintf("tablespace_%s_%s", cleanName, k)] = v
245 }
246 }
247
248 for id, metrics := range c.mx.connections {
249 cleanID := cleanName(id)
250 for k, v := range stm.ToMap(metrics) {
251 mx[fmt.Sprintf("connection_%s_%s", cleanID, k)] = v
252 }
253 }
254
255 for name, metrics := range c.mx.tables {
256 cleanName := cleanName(name)
257 for k, v := range stm.ToMap(metrics) {
258 mx[fmt.Sprintf("table_%s_%s", cleanName, k)] = v
259 }
260 }
261
262 for name, metrics := range c.mx.indexes {
263 cleanName := cleanName(name)
264 for k, v := range stm.ToMap(metrics) {
265 mx[fmt.Sprintf("index_%s_%s", cleanName, k)] = v
266 }
267 }
268
269 // Add new metric types
270
271 for poolType, metrics := range c.mx.memoryPools {
272 cleanType := cleanName(poolType)
273 for k, v := range stm.ToMap(metrics) {
274 mx[fmt.Sprintf("memory_pool_%s_%s", cleanType, k)] = v
275 }
276 }
277
278 for tableName, metrics := range c.mx.tableIOs {
279 cleanName := cleanName(tableName)
280 for k, v := range stm.ToMap(metrics) {
281 mx[fmt.Sprintf("table_io_%s_%s", cleanName, k)] = v
282 }
283 }
284
285 // Add memory set metrics
286 for setKey, metrics := range c.memorySets {
287 // Split the key to apply cleanName to each component
288 parts := strings.Split(setKey, ".")
289 if len(parts) >= 3 {
290 // Apply cleanName to match chart dimension IDs
291 setIdentifier := fmt.Sprintf("%s_%s_%s",
292 cleanName(parts[0]), // host name
293 cleanName(parts[1]), // db name
294 cleanName(parts[2])) // set type
295 // Add member if present
296 if len(parts) >= 4 {
297 setIdentifier += "_" + parts[3] // member number
298 }
299 for k, v := range stm.ToMap(metrics) {
300 mx[fmt.Sprintf("memory_set_%s_%s", setIdentifier, k)] = v
301 }
302 }
303 }
304
305 // Add prefetcher metrics
306 for bufferPoolName, metrics := range c.prefetchers {
307 cleanName := cleanName(bufferPoolName)
308 for k, v := range stm.ToMap(metrics) {
309 mx[fmt.Sprintf("prefetcher_%s_%s", cleanName, k)] = v
310 }
311 }
312
313 return mx, nil
314 }
315
316 func (c *Collector) collectServiceHealth(ctx context.Context) {
317 // Connection check
318 c.mx.CanConnect = 0
319 if err := c.doQuerySingleValue(ctx, queryCanConnect, &c.mx.CanConnect); err != nil {
320 c.mx.CanConnect = 0
321 }
322
323 // Database status check
324 // 0 = OK (active), 1 = WARNING (quiesce-pending, rollforward), 2 = CRITICAL (quiesced), 3 = UNKNOWN
325 c.mx.DatabaseStatus = 3
326 if err := c.doQuerySingleValue(ctx, queryDatabaseStatus, &c.mx.DatabaseStatus); err != nil {
327 c.mx.DatabaseStatus = 3
328 }
329 }
330
331 func (c *Collector) detectVersion(ctx context.Context) error {
332 // Try SYSIBMADM.ENV_INST_INFO (works on LUW)
333 query := queryDetectVersionLUW
334
335 var serviceLevel, hostName, instName sql.NullString
336 err := c.db.QueryRow(query).Scan(&serviceLevel, &hostName, &instName)
337 if err == nil {
338 c.serverInfo.version = serviceLevel.String
339 c.serverInfo.hostName = hostName.String
340 c.serverInfo.instanceName = instName.String
341
342 // Parse version to determine edition
343 if strings.Contains(serviceLevel.String, "DB2") {
344 if strings.Contains(serviceLevel.String, "LUW") || strings.Contains(serviceLevel.String, "Linux") || strings.Contains(serviceLevel.String, "Windows") {
345 c.edition = "LUW"
346 } else if strings.Contains(serviceLevel.String, "z/OS") {
347 c.edition = "z/OS"
348 } else {
349 c.edition = "LUW" // Default to LUW
350 }
351 }
352 c.version = serviceLevel.String
353 return nil
354 }
355
356 // If that fails, might be AS/400 (DB2 for i)
357 query = queryDetectVersionI
358 var dummy sql.NullString
359 err = c.db.QueryRow(query).Scan(&dummy)
360 if err == nil {
361 c.edition = "i"
362 c.version = "DB2 for i"
363 return nil
364 }
365
366 return fmt.Errorf("unable to detect DB2 version")
367 }
368
369 func (c *Collector) collectGlobalMetrics(ctx context.Context) error {
370 c.Debugf("starting global metrics collection")
371
372 // Service health checks - core functionality
373 if err := c.collectServiceHealthResilience(ctx); err != nil {
374 return err // Service health is critical
375 }
376
377 // Connection metrics - core functionality that should always work
378 if err := c.collectConnectionMetricsResilience(ctx); err != nil {
379 return err // Connection metrics are critical
380 }
381
382 // Database Overview metrics (Screen 01) - always collect if possible
383 if err := c.collectDatabaseOverview(ctx); err != nil {
384 c.Warningf("failed to collect database overview metrics: %v", err)
385 }
386
387 // Enhanced Logging Performance metrics (Screen 18)
388 if err := c.collectLoggingPerformance(ctx); err != nil {
389 c.Warningf("failed to collect enhanced logging performance metrics: %v", err)
390 }
391
392 // Federation metrics (Screen 32) - only if supported
393 if err := c.collectFederationMetrics(ctx); err != nil {
394 // Not logging as warning since federation might not be configured
395 c.Debugf("federation metrics collection skipped: %v", err)
396 }
397
398 // Always use modern MON_GET_* functions
399 // Collect all database-level metrics in one efficient call
400 if !c.isDisabled("advanced_monitoring") {
401 if err := c.collectMonGetDatabase(ctx); err != nil {
402 c.Warningf("failed to collect database metrics using MON_GET_DATABASE: %v", err)
403 // Try individual metric collection as fallback
404 c.collectLockMetricsResilience(ctx)
405 c.collectSortingMetricsResilience(ctx)
406 c.collectRowActivityMetricsResilience(ctx)
407 }
408 } else {
409 c.logOnce("advanced_monitoring_skipped", "Advanced monitoring metrics collection skipped - Not available on this DB2 edition/version")
410 }
411
412 // Buffer pool metrics using MON_GET_BUFFERPOOL
413 if !c.isDisabled("bufferpool_detailed_metrics") {
414 if err := c.collectMonGetBufferpoolAggregate(ctx); err != nil {
415 c.Warningf("failed to collect bufferpool metrics using MON_GET_BUFFERPOOL: %v", err)
416 c.collectBufferpoolMetricsResilience(ctx)
417 }
418 } else {
419 c.logOnce("bufferpool_detailed_skipped", "Detailed buffer pool metrics collection skipped - Limited on this DB2 edition")
420 }
421
422 // Log space metrics using MON_GET_TRANSACTION_LOG
423 if !c.isDisabled("system_level_metrics") {
424 if err := c.collectMonGetTransactionLog(ctx); err != nil {
425 c.Warningf("failed to collect log metrics using MON_GET_TRANSACTION_LOG: %v", err)
426 c.collectLogSpaceMetricsResilience(ctx)
427 }
428 } else {
429 c.logOnce("system_level_skipped", "System-level metrics collection skipped - Restricted on this DB2 edition")
430 }
431
432 // Long-running queries and backup status - collected separately as they don't have MON_GET equivalents
433 if !c.isDisabled("advanced_monitoring") {
434 // Long-running queries - graceful degradation
435 c.collectLongRunningQueriesResilience(ctx)
436
437 // Backup status - graceful degradation
438 c.collectBackupStatusResilience(ctx)
439 }
440
441 c.Debugf("completed global metrics collection")
442 return nil
443 }
444
445 func (c *Collector) collectLockMetrics(ctx context.Context) error {
446 // Choose query based on monitoring approach
447 // Always use modern MON_GET_DATABASE for lock metrics
448 query := queryMonGetDatabase
449 c.Debugf("using MON_GET_DATABASE for lock metrics")
450
451 return c.doQuery(ctx, query, func(column, value string, lineEnd bool) {
452 v, err := strconv.ParseInt(value, 10, 64)
453 if err != nil {
454 return
455 }
456
457 switch column {
458 case "LOCK_WAITS":
459 c.mx.LockWaits = v
460 case "LOCK_TIMEOUTS":
461 c.mx.LockTimeouts = v
462 case "DEADLOCKS":
463 c.mx.Deadlocks = v
464 case "LOCK_ESCALS":
465 c.mx.LockEscalations = v
466 case "LOCK_ACTIVE":
467 c.mx.LockActive = v
468 case "LOCK_WAIT_TIME":
469 c.mx.LockWaitTime = v * Precision // Convert to milliseconds with Precision
470 case "LOCK_WAITING_AGENTS":
471 c.mx.LockWaitingAgents = v
472 case "LOCK_MEMORY_PAGES":
473 c.mx.LockMemoryPages = v
474 case "TOTAL_SORTS":
475 c.mx.TotalSorts = v
476 case "SORT_OVERFLOWS":
477 c.mx.SortOverflows = v
478 case "ROWS_READ":
479 c.mx.RowsRead = v
480 case "ROWS_MODIFIED":
481 c.mx.RowsModified = v
482 case "ROWS_RETURNED":
483 c.mx.RowsReturned = v
484 }
485 })
486 }
487
488 func (c *Collector) collectBufferpoolAggregateMetrics(ctx context.Context) error {
489 // Always use modern MON_GET_BUFFERPOOL for aggregate metrics
490 c.Debugf("using MON_GET_BUFFERPOOL for aggregate metrics")
491 return c.collectMonGetBufferpoolAggregate(ctx)
492 }
493
494 func (c *Collector) collectLogSpaceMetrics(ctx context.Context) error {
495 // Always use MON_GET_TRANSACTION_LOG for log metrics
496 query := queryMonGetTransactionLog
497 c.Debugf("using MON_GET_TRANSACTION_LOG for log metrics")
498
499 return c.doQuery(ctx, query, func(column, value string, lineEnd bool) {
500 switch column {
501 case "TOTAL_LOG_USED":
502 if v, err := strconv.ParseInt(value, 10, 64); err == nil {
503 c.mx.LogUsedSpace = v
504 }
505 case "TOTAL_LOG_AVAILABLE":
506 if v, err := strconv.ParseInt(value, 10, 64); err == nil {
507 c.mx.LogAvailableSpace = v
508 }
509 case "LOG_UTILIZATION":
510 if v, err := strconv.ParseFloat(value, 64); err == nil {
511 c.mx.LogUtilization = int64(v * Precision)
512 }
513 case "LOG_READS":
514 if v, err := strconv.ParseInt(value, 10, 64); err == nil {
515 c.mx.LogIOReads = v
516 }
517 case "LOG_WRITES":
518 if v, err := strconv.ParseInt(value, 10, 64); err == nil {
519 c.mx.LogIOWrites = v
520 }
521 }
522 })
523 }
524
525 func (c *Collector) doQuery(ctx context.Context, query string, assign func(column, value string, lineEnd bool)) error {
526 queryCtx, cancel := context.WithTimeout(ctx, time.Duration(c.Timeout))
527 defer cancel()
528
529 rows, err := c.db.QueryContext(queryCtx, query)
530 if err != nil {
531 if isSQLFeatureError(err) {
532 c.Debugf("query failed with expected feature error: %s, error: %v", query, err)
533 } else {
534 c.Errorf("failed to execute query: %s, error: %v", query, err)
535 }
536 return err
537 }
538 defer rows.Close()
539
540 return c.readRows(rows, assign)
541 }
542
543 func (c *Collector) doQuerySingleValue(ctx context.Context, query string, target *int64) error {
544 queryCtx, cancel := context.WithTimeout(ctx, time.Duration(c.Timeout))
545 defer cancel()
546
547 var value sql.NullInt64
548 err := c.db.QueryRowContext(queryCtx, query).Scan(&value)
549 if err != nil {
550 if isSQLFeatureError(err) {
551 c.Debugf("query failed with expected feature error: %s, error: %v", query, err)
552 }
553 return err
554 }
555 if value.Valid {
556 *target = value.Int64
557 }
558 return nil
559 }
560
561 func (c *Collector) doQuerySingleFloatValue(ctx context.Context, query string, target *int64) error {
562 queryCtx, cancel := context.WithTimeout(ctx, time.Duration(c.Timeout))
563 defer cancel()
564
565 var value sql.NullFloat64
566 err := c.db.QueryRowContext(queryCtx, query).Scan(&value)
567 if err != nil {
568 if isSQLFeatureError(err) {
569 c.Debugf("query failed with expected feature error: %s, error: %v", query, err)
570 }
571 return err
572 }
573 if value.Valid {
574 *target = int64(value.Float64 * Precision)
575 }
576 return nil
577 }
578
579 func (c *Collector) readRows(rows *sql.Rows, assign func(column, value string, lineEnd bool)) error {
580 columns, err := rows.Columns()
581 if err != nil {
582 return err
583 }
584
585 values := make([]sql.NullString, len(columns))
586 valuePtrs := make([]any, len(columns))
587 for i := range values {
588 valuePtrs[i] = &values[i]
589 }
590
591 // Track which query is being processed for better error reporting
592 var currentQuery string
593 if len(columns) > 0 {
594 // Try to identify query by column pattern
595 switch {
596 case contains(columns, "MEMORY_SET_TYPE", "MEMORY_SET_USED"):
597 currentQuery = "MON_GET_MEMORY_SET"
598 case contains(columns, "MEMORY_POOL_TYPE", "MEMORY_POOL_USED"):
599 currentQuery = "MON_GET_MEMORY_POOL"
600 default:
601 currentQuery = "unknown"
602 }
603 }
604
605 // Universal fix for ODBC driver issue with DB2/AS400 negative values
606 defer func() {
607 if r := recover(); r != nil {
608 // This is a known ODBC driver bug where it incorrectly handles certain DB2 data types
609 // The driver attempts to use negative values as slice indices, causing panics
610 c.Debugf("ODBC driver panic in %s query (columns: %v): %v", currentQuery, columns, r)
611 c.Debugf("This is a known ODBC driver limitation with certain DB2 data types")
612 }
613 }()
614
615 for rows.Next() {
616 // Wrap Scan in panic recovery as well since it can also trigger ODBC issues
617 func() {
618 defer func() {
619 if r := recover(); r != nil {
620 c.Debugf("ODBC scan panic recovered: %v", r)
621 }
622 }()
623
624 if err := rows.Scan(valuePtrs...); err != nil {
625 c.Debugf("Row scan error: %v", err)
626 return
627 }
628
629 for i, column := range columns {
630 if values[i].Valid {
631 assign(column, values[i].String, i == len(columns)-1)
632 } else {
633 assign(column, "", i == len(columns)-1)
634 }
635 }
636 }()
637 }
638
639 return rows.Err()
640 }
641
642 func (c *Collector) collectLongRunningQueries(ctx context.Context) error {
643 // Query to find long-running queries from SYSIBMADM.LONG_RUNNING_SQL
644 // Warning threshold: 5 minutes, Critical threshold: 15 minutes
645 return c.doQuery(ctx, queryLongRunningQueries, func(column, value string, lineEnd bool) {
646 v, err := strconv.ParseInt(value, 10, 64)
647 if err != nil {
648 return
649 }
650
651 switch column {
652 case "TOTAL_COUNT":
653 c.mx.LongRunningQueries = v
654 case "WARNING_COUNT":
655 c.mx.LongRunningQueriesWarning = v
656 case "CRITICAL_COUNT":
657 c.mx.LongRunningQueriesCritical = v
658 }
659 })
660 }
661
662 func (c *Collector) collectBackupStatus(ctx context.Context) error {
663 now := time.Now()
664
665 // First check if we have ANY backup (successful or failed) in the last 7 days
666 var lastBackupSQLCode sql.NullInt64
667 var lastBackupTime sql.NullString
668 queryCtx, cancel := context.WithTimeout(ctx, time.Duration(c.Timeout))
669 defer cancel()
670
671 // Get the most recent backup attempt with ODBC-safe error handling
672 var err error
673 func() {
674 defer func() {
675 if r := recover(); r != nil {
676 c.Debugf("backup history query failed due to ODBC driver issue: %v", r)
677 err = fmt.Errorf("odbc driver error: %v", r)
678 }
679 }()
680 err = c.db.QueryRowContext(queryCtx, `
681 SELECT SQLCODE, START_TIME
682 FROM SYSIBMADM.DB_HISTORY
683 WHERE OPERATION = 'B'
684 AND OPERATIONTYPE = 'F'
685 ORDER BY START_TIME DESC
686 FETCH FIRST 1 ROW ONLY
687 `).Scan(&lastBackupSQLCode, &lastBackupTime)
688 }()
689
690 if err == nil && lastBackupSQLCode.Valid {
691 // Check if the last backup was successful (SQLCODE = 0)
692 if lastBackupSQLCode.Int64 == 0 {
693 c.mx.LastBackupStatus = 0 // Success
694 } else {
695 c.mx.LastBackupStatus = 1 // Failed (non-zero SQLCODE)
696 }
697 } else {
698 // No backup history found
699 c.mx.LastBackupStatus = 0 // Don't raise alert if no backup history
700 }
701
702 // Get the last successful full backup with ODBC-safe error handling
703 var lastFullBackup sql.NullString
704 func() {
705 defer func() {
706 if r := recover(); r != nil {
707 c.Debugf("full backup query failed due to ODBC driver issue: %v", r)
708 err = fmt.Errorf("odbc driver error: %v", r)
709 }
710 }()
711 err = c.db.QueryRowContext(queryCtx, `
712 SELECT MAX(START_TIME)
713 FROM SYSIBMADM.DB_HISTORY
714 WHERE OPERATION = 'B'
715 AND OPERATIONTYPE = 'F'
716 AND SQLCODE = 0
717 `).Scan(&lastFullBackup)
718 }()
719
720 if err == nil && lastFullBackup.Valid {
721 if t, err := time.Parse("2006-01-02-15.04.05", lastFullBackup.String); err == nil {
722 c.mx.LastFullBackupAge = int64(now.Sub(t).Hours())
723 } else {
724 c.Warningf("failed to parse last full backup time '%s': %v (expected format: YYYY-MM-DD-HH.MM.SS)", lastFullBackup.String, err)
725 c.mx.LastFullBackupAge = 0 // Parse error - report 0 hours (recent)
726 }
727 } else {
728 // No successful backup found
729 c.mx.LastFullBackupAge = 720 // 30 days in hours - old but reasonable
730 }
731
732 // Get the last successful incremental backup with ODBC-safe error handling
733 var lastIncrementalBackup sql.NullString
734 func() {
735 defer func() {
736 if r := recover(); r != nil {
737 c.Debugf("incremental backup query failed due to ODBC driver issue: %v", r)
738 err = fmt.Errorf("odbc driver error: %v", r)
739 }
740 }()
741 err = c.db.QueryRowContext(queryCtx, `
742 SELECT MAX(START_TIME)
743 FROM SYSIBMADM.DB_HISTORY
744 WHERE OPERATION = 'B'
745 AND OPERATIONTYPE IN ('I', 'O', 'D')
746 AND SQLCODE = 0
747 `).Scan(&lastIncrementalBackup)
748 }()
749
750 if err == nil && lastIncrementalBackup.Valid {
751 if t, err := time.Parse("2006-01-02-15.04.05", lastIncrementalBackup.String); err == nil {
752 c.mx.LastIncrementalBackupAge = int64(now.Sub(t).Hours())
753 } else {
754 c.Warningf("failed to parse last incremental backup time '%s': %v (expected format: YYYY-MM-DD-HH.MM.SS)", lastIncrementalBackup.String, err)
755 c.mx.LastIncrementalBackupAge = 0 // Parse error - report 0 hours (recent)
756 }
757 } else {
758 // No incremental backup found - this is normal for many setups
759 c.mx.LastIncrementalBackupAge = 0 // Report 0 to avoid false alerts
760 }
761
762 return nil
763 }
764
765 // Resilient collection functions following AS/400 pattern
766
767 func (c *Collector) collectServiceHealthResilience(ctx context.Context) error {
768 // Service health is critical - these must work
769 if err := c.collectSingleMetric(ctx, "can_connect", queryCanConnect, func(value string) {
770 if v, err := strconv.ParseInt(value, 10, 64); err == nil {
771 c.mx.CanConnect = v
772 }
773 }); err != nil {
774 return err // Fatal - basic connectivity must work
775 }
776
777 // Database status - optional on some editions
778 _ = c.collectSingleMetric(ctx, "database_status", queryDatabaseStatus, func(value string) {
779 if v, err := strconv.ParseInt(value, 10, 64); err == nil {
780 c.mx.DatabaseStatus = v
781 }
782 })
783
784 return nil
785 }
786
787 func (c *Collector) collectConnectionMetricsResilience(ctx context.Context) error {
788 // Always try MON_GET first for better performance
789 if err := c.collectMonGetConnections(ctx); err == nil {
790 return nil
791 }
792
793 // Fall back to individual queries if MON_GET fails
794 // Core connection metrics - must work on all DB2 editions
795 if err := c.collectSingleMetric(ctx, "total_connections", queryTotalConnections, func(value string) {
796 if v, err := strconv.ParseInt(value, 10, 64); err == nil {
797 c.mx.ConnTotal = v
798 }
799 }); err != nil {
800 return err // Fatal - basic connection count must work
801 }
802
803 // Optional connection breakdowns - graceful degradation
804 _ = c.collectSingleMetric(ctx, "active_connections", queryActiveConnections, func(value string) {
805 if v, err := strconv.ParseInt(value, 10, 64); err == nil {
806 c.mx.ConnActive = v
807 }
808 })
809
810 _ = c.collectSingleMetric(ctx, "executing_connections", queryExecutingConnections, func(value string) {
811 if v, err := strconv.ParseInt(value, 10, 64); err == nil {
812 c.mx.ConnExecuting = v
813 }
814 })
815
816 _ = c.collectSingleMetric(ctx, "idle_connections", queryIdleConnections, func(value string) {
817 if v, err := strconv.ParseInt(value, 10, 64); err == nil {
818 c.mx.ConnIdle = v
819 }
820 })
821
822 _ = c.collectSingleMetric(ctx, "max_connections", queryMaxConnections, func(value string) {
823 if v, err := strconv.ParseInt(value, 10, 64); err == nil {
824 c.mx.ConnMax = v
825 }
826 })
827
828 // Calculate idle if not available directly
829 if c.mx.ConnIdle == 0 && c.mx.ConnActive > 0 {
830 c.mx.ConnIdle = c.mx.ConnActive - c.mx.ConnExecuting
831 }
832
833 return nil
834 }
835
836 func (c *Collector) collectLockMetricsResilience(ctx context.Context) {
837 // Individual SNAP queries for lock metrics
838 _ = c.collectSingleMetric(ctx, "lock_waits", queryLockWaits, func(value string) {
839 if v, err := strconv.ParseInt(value, 10, 64); err == nil {
840 c.mx.LockWaits = v
841 }
842 })
843
844 _ = c.collectSingleMetric(ctx, "lock_timeouts", queryLockTimeouts, func(value string) {
845 if v, err := strconv.ParseInt(value, 10, 64); err == nil {
846 c.mx.LockTimeouts = v
847 }
848 })
849
850 _ = c.collectSingleMetric(ctx, "deadlocks", queryDeadlocks, func(value string) {
851 if v, err := strconv.ParseInt(value, 10, 64); err == nil {
852 c.mx.Deadlocks = v
853 }
854 })
855
856 _ = c.collectSingleMetric(ctx, "lock_escalations", queryLockEscalations, func(value string) {
857 if v, err := strconv.ParseInt(value, 10, 64); err == nil {
858 c.mx.LockEscalations = v
859 }
860 })
861
862 _ = c.collectSingleMetric(ctx, "active_locks", queryActiveLocks, func(value string) {
863 if v, err := strconv.ParseInt(value, 10, 64); err == nil {
864 c.mx.LockActive = v
865 }
866 })
867
868 _ = c.collectSingleMetric(ctx, "lock_wait_time", queryLockWaitTime, func(value string) {
869 if v, err := strconv.ParseInt(value, 10, 64); err == nil {
870 c.mx.LockWaitTime = v * Precision // Convert to milliseconds with Precision
871 }
872 })
873
874 _ = c.collectSingleMetric(ctx, "lock_waiting_agents", queryLockWaitingAgents, func(value string) {
875 if v, err := strconv.ParseInt(value, 10, 64); err == nil {
876 c.mx.LockWaitingAgents = v
877 }
878 })
879
880 _ = c.collectSingleMetric(ctx, "lock_memory_pages", queryLockMemoryPages, func(value string) {
881 if v, err := strconv.ParseInt(value, 10, 64); err == nil {
882 c.mx.LockMemoryPages = v
883 }
884 })
885 }
886
887 func (c *Collector) collectSortingMetricsResilience(ctx context.Context) {
888 // Individual SNAP queries for sorting metrics
889 _ = c.collectSingleMetric(ctx, "total_sorts", queryTotalSorts, func(value string) {
890 if v, err := strconv.ParseInt(value, 10, 64); err == nil {
891 c.mx.TotalSorts = v
892 }
893 })
894
895 _ = c.collectSingleMetric(ctx, "sort_overflows", querySortOverflows, func(value string) {
896 if v, err := strconv.ParseInt(value, 10, 64); err == nil {
897 c.mx.SortOverflows = v
898 }
899 })
900 }
901
902 func (c *Collector) collectRowActivityMetricsResilience(ctx context.Context) {
903 // Individual SNAP queries for row activity metrics
904 _ = c.collectSingleMetric(ctx, "rows_read", queryRowsRead, func(value string) {
905 if v, err := strconv.ParseInt(value, 10, 64); err == nil {
906 c.mx.RowsRead = v
907 }
908 })
909
910 _ = c.collectSingleMetric(ctx, "rows_modified", queryRowsModified, func(value string) {
911 if v, err := strconv.ParseInt(value, 10, 64); err == nil {
912 c.mx.RowsModified = v
913 }
914 })
915
916 _ = c.collectSingleMetric(ctx, "rows_returned", queryRowsReturned, func(value string) {
917 if v, err := strconv.ParseInt(value, 10, 64); err == nil {
918 c.mx.RowsReturned = v
919 }
920 })
921 }
922
923 func (c *Collector) collectBufferpoolMetricsResilience(ctx context.Context) {
924 // Individual SNAP queries for bufferpool metrics
925 // Collect individual components first
926 var dataLogical, dataHits int64
927 var indexLogical, indexHits int64
928 var xdaLogical, xdaHits int64
929 var colLogical, colHits int64
930
931 // Get reads
932 _ = c.collectSingleMetric(ctx, "bufferpool_logical_reads", queryBufferpoolLogicalReads, func(value string) {
933 if v, err := strconv.ParseInt(value, 10, 64); err == nil {
934 c.mx.BufferpoolLogicalReads = v
935 }
936 })
937
938 _ = c.collectSingleMetric(ctx, "bufferpool_physical_reads", queryBufferpoolPhysicalReads, func(value string) {
939 if v, err := strconv.ParseInt(value, 10, 64); err == nil {
940 c.mx.BufferpoolPhysicalReads = v
941 }
942 })
943
944 // Data reads
945 _ = c.collectSingleMetric(ctx, "bufferpool_data_logical", queryBufferpoolDataLogical, func(value string) {
946 if v, err := strconv.ParseInt(value, 10, 64); err == nil {
947 dataLogical = v
948 c.mx.BufferpoolDataLogicalReads = v
949 }
950 })
951
952 _ = c.collectSingleMetric(ctx, "bufferpool_data_physical", queryBufferpoolDataPhysical, func(value string) {
953 if v, err := strconv.ParseInt(value, 10, 64); err == nil {
954 c.mx.BufferpoolDataPhysicalReads = v
955 }
956 })
957
958 _ = c.collectSingleMetric(ctx, "bufferpool_data_hits", queryBufferpoolDataHits, func(value string) {
959 if v, err := strconv.ParseInt(value, 10, 64); err == nil {
960 dataHits = v
961 }
962 })
963
964 // Index reads
965 _ = c.collectSingleMetric(ctx, "bufferpool_index_logical", queryBufferpoolIndexLogical, func(value string) {
966 if v, err := strconv.ParseInt(value, 10, 64); err == nil {
967 indexLogical = v
968 c.mx.BufferpoolIndexLogicalReads = v
969 }
970 })
971
972 _ = c.collectSingleMetric(ctx, "bufferpool_index_physical", queryBufferpoolIndexPhysical, func(value string) {
973 if v, err := strconv.ParseInt(value, 10, 64); err == nil {
974 c.mx.BufferpoolIndexPhysicalReads = v
975 }
976 })
977
978 _ = c.collectSingleMetric(ctx, "bufferpool_index_hits", queryBufferpoolIndexHits, func(value string) {
979 if v, err := strconv.ParseInt(value, 10, 64); err == nil {
980 indexHits = v
981 }
982 })
983
984 // XDA reads
985 _ = c.collectSingleMetric(ctx, "bufferpool_xda_logical", queryBufferpoolXDALogical, func(value string) {
986 if v, err := strconv.ParseInt(value, 10, 64); err == nil {
987 xdaLogical = v
988 c.mx.BufferpoolXDALogicalReads = v
989 }
990 })
991
992 _ = c.collectSingleMetric(ctx, "bufferpool_xda_physical", queryBufferpoolXDAPhysical, func(value string) {
993 if v, err := strconv.ParseInt(value, 10, 64); err == nil {
994 c.mx.BufferpoolXDAPhysicalReads = v
995 }
996 })
997
998 _ = c.collectSingleMetric(ctx, "bufferpool_xda_hits", queryBufferpoolXDAHits, func(value string) {
999 if v, err := strconv.ParseInt(value, 10, 64); err == nil {
1000 xdaHits = v
1001 }
1002 })
1003
1004 // Column reads
1005 _ = c.collectSingleMetric(ctx, "bufferpool_column_logical", queryBufferpoolColumnLogical, func(value string) {
1006 if v, err := strconv.ParseInt(value, 10, 64); err == nil {
1007 colLogical = v
1008 c.mx.BufferpoolColumnLogicalReads = v
1009 }
1010 })
1011
1012 _ = c.collectSingleMetric(ctx, "bufferpool_column_physical", queryBufferpoolColumnPhysical, func(value string) {
1013 if v, err := strconv.ParseInt(value, 10, 64); err == nil {
1014 c.mx.BufferpoolColumnPhysicalReads = v
1015 }
1016 })
1017
1018 _ = c.collectSingleMetric(ctx, "bufferpool_column_hits", queryBufferpoolColumnHits, func(value string) {
1019 if v, err := strconv.ParseInt(value, 10, 64); err == nil {
1020 colHits = v
1021 }
1022 })
1023
1024 // Calculate hit ratios from components
1025 // Calculate hits and misses for each type
1026 c.mx.BufferpoolDataHits = dataHits
1027 c.mx.BufferpoolDataMisses = dataLogical - dataHits
1028
1029 c.mx.BufferpoolIndexHits = indexHits
1030 c.mx.BufferpoolIndexMisses = indexLogical - indexHits
1031
1032 c.mx.BufferpoolXDAHits = xdaHits
1033 c.mx.BufferpoolXDAMisses = xdaLogical - xdaHits
1034
1035 c.mx.BufferpoolColumnHits = colHits
1036 c.mx.BufferpoolColumnMisses = colLogical - colHits
1037
1038 // If misses are negative, it means prefetch brought more pages than were requested
1039 // In this case, set misses to 0 and reduce hits accordingly
1040 if c.mx.BufferpoolDataMisses < 0 {
1041 c.mx.BufferpoolDataHits = dataLogical
1042 c.mx.BufferpoolDataMisses = 0
1043 }
1044 if c.mx.BufferpoolIndexMisses < 0 {
1045 c.mx.BufferpoolIndexHits = indexLogical
1046 c.mx.BufferpoolIndexMisses = 0
1047 }
1048 if c.mx.BufferpoolXDAMisses < 0 {
1049 c.mx.BufferpoolXDAHits = xdaLogical
1050 c.mx.BufferpoolXDAMisses = 0
1051 }
1052 if c.mx.BufferpoolColumnMisses < 0 {
1053 c.mx.BufferpoolColumnHits = colLogical
1054 c.mx.BufferpoolColumnMisses = 0
1055 }
1056
1057 // Calculate overall hits and misses
1058 c.mx.BufferpoolHits = c.mx.BufferpoolDataHits + c.mx.BufferpoolIndexHits +
1059 c.mx.BufferpoolXDAHits + c.mx.BufferpoolColumnHits
1060 c.mx.BufferpoolMisses = c.mx.BufferpoolDataMisses + c.mx.BufferpoolIndexMisses +
1061 c.mx.BufferpoolXDAMisses + c.mx.BufferpoolColumnMisses
1062 }
1063
1064 func (c *Collector) collectLogSpaceMetricsResilience(ctx context.Context) {
1065 // Individual SNAP queries for log space metrics
1066 var logUsed, logAvailable int64
1067
1068 _ = c.collectSingleMetric(ctx, "log_used_space", queryLogUsedSpace, func(value string) {
1069 if v, err := strconv.ParseInt(value, 10, 64); err == nil {
1070 logUsed = v
1071 c.mx.LogUsedSpace = v
1072 }
1073 })
1074
1075 _ = c.collectSingleMetric(ctx, "log_available_space", queryLogAvailableSpace, func(value string) {
1076 if v, err := strconv.ParseInt(value, 10, 64); err == nil {
1077 logAvailable = v
1078 c.mx.LogAvailableSpace = v
1079 }
1080 })
1081
1082 _ = c.collectSingleMetric(ctx, "log_reads", queryLogReads, func(value string) {
1083 if v, err := strconv.ParseInt(value, 10, 64); err == nil {
1084 c.mx.LogIOReads = v
1085 }
1086 })
1087
1088 _ = c.collectSingleMetric(ctx, "log_writes", queryLogWrites, func(value string) {
1089 if v, err := strconv.ParseInt(value, 10, 64); err == nil {
1090 c.mx.LogIOWrites = v
1091 }
1092 })
1093
1094 // Calculate log utilization
1095 if logUsed > 0 && logAvailable > 0 {
1096 total := logUsed + logAvailable
1097 c.mx.LogUtilization = int64((float64(logUsed) * 100.0 * float64(Precision)) / float64(total))
1098 }
1099 }
1100
1101 func (c *Collector) collectLongRunningQueriesResilience(ctx context.Context) {
1102 _ = c.collectSingleMetric(ctx, "long_running_total", queryLongRunningTotal, func(value string) {
1103 if v, err := strconv.ParseInt(value, 10, 64); err == nil {
1104 c.mx.LongRunningQueries = v
1105 }
1106 })
1107
1108 _ = c.collectSingleMetric(ctx, "long_running_warning", queryLongRunningWarning, func(value string) {
1109 if v, err := strconv.ParseInt(value, 10, 64); err == nil {
1110 c.mx.LongRunningQueriesWarning = v
1111 }
1112 })
1113
1114 _ = c.collectSingleMetric(ctx, "long_running_critical", queryLongRunningCritical, func(value string) {
1115 if v, err := strconv.ParseInt(value, 10, 64); err == nil {
1116 c.mx.LongRunningQueriesCritical = v
1117 }
1118 })
1119 }
1120
1121 func (c *Collector) collectBackupStatusResilience(ctx context.Context) {
1122 // This will use the existing collectBackupStatus function as it's already resilient
1123 _ = c.collectBackupStatus(ctx)
1124 }
1125
1126 // MON_GET collection functions for modern monitoring approach
1127
1128 func (c *Collector) collectMonGetConnections(ctx context.Context) error {
1129 // First get connection counts
1130 err := c.doQuery(ctx, queryMonGetConnections, func(column, value string, lineEnd bool) {
1131 switch column {
1132 case "TOTAL_CONNS":
1133 if v, err := strconv.ParseInt(value, 10, 64); err == nil {
1134 c.mx.ConnTotal = v
1135 }
1136 case "ACTIVE_CONNS":
1137 if v, err := strconv.ParseInt(value, 10, 64); err == nil {
1138 c.mx.ConnActive = v
1139 }
1140 case "IDLE_CONNS":
1141 if v, err := strconv.ParseInt(value, 10, 64); err == nil {
1142 c.mx.ConnIdle = v
1143 }
1144 case "EXECUTING_CONNS":
1145 if v, err := strconv.ParseInt(value, 10, 64); err == nil {
1146 c.mx.ConnExecuting = v
1147 }
1148 }
1149 })
1150
1151 if err != nil {
1152 return err
1153 }
1154
1155 // Then get max connections from configuration
1156 return c.collectSingleMetric(ctx, "max_connections", queryMaxConnections, func(value string) {
1157 if v, err := strconv.ParseInt(value, 10, 64); err == nil {
1158 c.mx.ConnMax = v
1159 }
1160 })
1161 }
1162
1163 func (c *Collector) collectMonGetDatabase(ctx context.Context) error {
1164 return c.doQuery(ctx, queryMonGetDatabase, func(column, value string, lineEnd bool) {
1165 switch column {
1166 case "LOCK_WAITS":
1167 if v, err := strconv.ParseInt(value, 10, 64); err == nil {
1168 c.mx.LockWaits = v
1169 }
1170 case "LOCK_TIMEOUTS":
1171 if v, err := strconv.ParseInt(value, 10, 64); err == nil {
1172 c.mx.LockTimeouts = v
1173 }
1174 case "DEADLOCKS":
1175 if v, err := strconv.ParseInt(value, 10, 64); err == nil {
1176 c.mx.Deadlocks = v
1177 }
1178 case "LOCK_ESCALS":
1179 if v, err := strconv.ParseInt(value, 10, 64); err == nil {
1180 c.mx.LockEscalations = v
1181 }
1182 case "LOCK_ACTIVE":
1183 if v, err := strconv.ParseInt(value, 10, 64); err == nil {
1184 c.mx.LockActive = v
1185 }
1186 case "LOCK_WAIT_TIME":
1187 if v, err := strconv.ParseInt(value, 10, 64); err == nil {
1188 c.mx.LockWaitTime = v
1189 }
1190 case "LOCK_WAITING_AGENTS":
1191 if v, err := strconv.ParseInt(value, 10, 64); err == nil {
1192 c.mx.LockWaitingAgents = v
1193 }
1194 case "LOCK_MEMORY_PAGES":
1195 if v, err := strconv.ParseInt(value, 10, 64); err == nil {
1196 c.mx.LockMemoryPages = v
1197 }
1198 case "TOTAL_SORTS":
1199 if v, err := strconv.ParseInt(value, 10, 64); err == nil {
1200 c.mx.TotalSorts = v
1201 }
1202 case "SORT_OVERFLOWS":
1203 if v, err := strconv.ParseInt(value, 10, 64); err == nil {
1204 c.mx.SortOverflows = v
1205 }
1206 case "ROWS_READ":
1207 if v, err := strconv.ParseInt(value, 10, 64); err == nil {
1208 c.mx.RowsRead = v
1209 }
1210 case "ROWS_MODIFIED":
1211 if v, err := strconv.ParseInt(value, 10, 64); err == nil {
1212 c.mx.RowsModified = v
1213 }
1214 case "ROWS_RETURNED":
1215 if v, err := strconv.ParseInt(value, 10, 64); err == nil {
1216 c.mx.RowsReturned = v
1217 }
1218 }
1219 })
1220 }
1221
1222 func (c *Collector) collectMonGetBufferpoolAggregate(ctx context.Context) error {
1223 var dataLogical, dataPhysical, dataHits int64
1224 var indexLogical, indexPhysical, indexHits int64
1225 var xdaLogical, xdaPhysical, xdaHits int64
1226 var colLogical, colPhysical, colHits int64
1227
1228 err := c.doQuery(ctx, queryMonGetBufferpoolAggregate, func(column, value string, lineEnd bool) {
1229 v, err := strconv.ParseInt(value, 10, 64)
1230 if err != nil {
1231 return
1232 }
1233
1234 switch column {
1235 case "DATA_LOGICAL_READS":
1236 dataLogical = v
1237 c.mx.BufferpoolDataLogicalReads = v
1238 case "DATA_PHYSICAL_READS":
1239 dataPhysical = v
1240 c.mx.BufferpoolDataPhysicalReads = v
1241 case "DATA_HITS":
1242 dataHits = v
1243 case "INDEX_LOGICAL_READS":
1244 indexLogical = v
1245 c.mx.BufferpoolIndexLogicalReads = v
1246 case "INDEX_PHYSICAL_READS":
1247 indexPhysical = v
1248 c.mx.BufferpoolIndexPhysicalReads = v
1249 case "INDEX_HITS":
1250 indexHits = v
1251 case "XDA_LOGICAL_READS":
1252 xdaLogical = v
1253 c.mx.BufferpoolXDALogicalReads = v
1254 case "XDA_PHYSICAL_READS":
1255 xdaPhysical = v
1256 c.mx.BufferpoolXDAPhysicalReads = v
1257 case "XDA_HITS":
1258 xdaHits = v
1259 case "COLUMN_LOGICAL_READS":
1260 colLogical = v
1261 c.mx.BufferpoolColumnLogicalReads = v
1262 case "COLUMN_PHYSICAL_READS":
1263 colPhysical = v
1264 c.mx.BufferpoolColumnPhysicalReads = v
1265 case "COLUMN_HITS":
1266 colHits = v
1267 case "TOTAL_WRITES":
1268 c.mx.BufferpoolWrites = v
1269 }
1270 })
1271
1272 if err != nil {
1273 return err
1274 }
1275
1276 // Calculate totals
1277 c.mx.BufferpoolLogicalReads = dataLogical + indexLogical + xdaLogical + colLogical
1278 c.mx.BufferpoolPhysicalReads = dataPhysical + indexPhysical + xdaPhysical + colPhysical
1279 c.mx.BufferpoolTotalReads = c.mx.BufferpoolLogicalReads + c.mx.BufferpoolPhysicalReads
1280
1281 // Calculate data totals
1282 c.mx.BufferpoolDataTotalReads = dataLogical + dataPhysical
1283 c.mx.BufferpoolIndexTotalReads = indexLogical + indexPhysical
1284 c.mx.BufferpoolXDATotalReads = xdaLogical + xdaPhysical
1285 c.mx.BufferpoolColumnTotalReads = colLogical + colPhysical
1286
1287 // Calculate hits and misses for each type
1288 c.mx.BufferpoolDataHits = dataHits
1289 c.mx.BufferpoolDataMisses = dataLogical - dataHits
1290
1291 c.mx.BufferpoolIndexHits = indexHits
1292 c.mx.BufferpoolIndexMisses = indexLogical - indexHits
1293
1294 c.mx.BufferpoolXDAHits = xdaHits
1295 c.mx.BufferpoolXDAMisses = xdaLogical - xdaHits
1296
1297 c.mx.BufferpoolColumnHits = colHits
1298 c.mx.BufferpoolColumnMisses = colLogical - colHits
1299
1300 // If misses are negative, it means prefetch brought more pages than were requested
1301 // In this case, set misses to 0 and reduce hits accordingly
1302 if c.mx.BufferpoolDataMisses < 0 {
1303 c.mx.BufferpoolDataHits = dataLogical
1304 c.mx.BufferpoolDataMisses = 0
1305 }
1306 if c.mx.BufferpoolIndexMisses < 0 {
1307 c.mx.BufferpoolIndexHits = indexLogical
1308 c.mx.BufferpoolIndexMisses = 0
1309 }
1310 if c.mx.BufferpoolXDAMisses < 0 {
1311 c.mx.BufferpoolXDAHits = xdaLogical
1312 c.mx.BufferpoolXDAMisses = 0
1313 }
1314 if c.mx.BufferpoolColumnMisses < 0 {
1315 c.mx.BufferpoolColumnHits = colLogical
1316 c.mx.BufferpoolColumnMisses = 0
1317 }
1318
1319 // Calculate overall hits and misses
1320 c.mx.BufferpoolHits = c.mx.BufferpoolDataHits + c.mx.BufferpoolIndexHits +
1321 c.mx.BufferpoolXDAHits + c.mx.BufferpoolColumnHits
1322 c.mx.BufferpoolMisses = c.mx.BufferpoolDataMisses + c.mx.BufferpoolIndexMisses +
1323 c.mx.BufferpoolXDAMisses + c.mx.BufferpoolColumnMisses
1324
1325 return nil
1326 }
1327
1328 func (c *Collector) collectMonGetTransactionLog(ctx context.Context) error {
1329 return c.doQuery(ctx, queryMonGetTransactionLog, func(column, value string, lineEnd bool) {
1330 switch column {
1331 case "TOTAL_LOG_USED":
1332 if v, err := strconv.ParseInt(value, 10, 64); err == nil {
1333 c.mx.LogUsedSpace = v
1334 }
1335 case "TOTAL_LOG_AVAILABLE":
1336 if v, err := strconv.ParseInt(value, 10, 64); err == nil {
1337 c.mx.LogAvailableSpace = v
1338 }
1339 case "LOG_UTILIZATION":
1340 if v, err := strconv.ParseFloat(value, 64); err == nil {
1341 c.mx.LogUtilization = int64(v * Precision)
1342 }
1343 case "LOG_READS":
1344 if v, err := strconv.ParseInt(value, 10, 64); err == nil {
1345 c.mx.LogIOReads = v
1346 }
1347 case "LOG_WRITES":
1348 if v, err := strconv.ParseInt(value, 10, 64); err == nil {
1349 c.mx.LogIOWrites = v
1350 }
1351 }
1352 })
1353 }
1354
1355 // Database Overview collection (Screen 01)
1356 func (c *Collector) collectDatabaseOverview(ctx context.Context) error {
1357 // Use simple queries approach (based on dcmtop) for more resilience
1358 // Run multiple simple queries instead of one complex query
1359
1360 // Helper function to run a simple query and handle errors gracefully
1361 // It tries the MON_GET query first, then falls back to SNAP query if that fails
1362 runSimpleQuery := func(queryMonGet, querySnap string, handler func(column, value string)) {
1363 var err error
1364 if queryMonGet != "" {
1365 err = c.doQuery(ctx, queryMonGet, func(column, value string, lineEnd bool) {
1366 handler(column, value)
1367 })
1368 if err != nil && isSQLFeatureError(err) {
1369 c.Debugf("MON_GET query not supported, trying SNAP query: %v", err)
1370 if querySnap != "" {
1371 err = c.doQuery(ctx, querySnap, func(column, value string, lineEnd bool) {
1372 handler(column, value)
1373 })
1374 }
1375 }
1376 } else if querySnap != "" {
1377 err = c.doQuery(ctx, querySnap, func(column, value string, lineEnd bool) {
1378 handler(column, value)
1379 })
1380 }
1381 if err != nil {
1382 c.Debugf("query failed (will continue): %v", err)
1383 }
1384 }
1385
1386 // Database status (current connected database)
1387 runSimpleQuery(querySimpleDatabaseStatus, querySnapDatabaseStatus, func(column, value string) {
1388 if column == "DATABASE_STATUS" && value == "ACTIVE" {
1389 c.mx.DatabaseStatusActive = 1
1390 c.mx.DatabaseStatusInactive = 0
1391 } else {
1392 c.mx.DatabaseStatusActive = 0
1393 c.mx.DatabaseStatusInactive = 1
1394 }
1395 })
1396
1397 // Database count (all databases in the instance)
1398 // This will be updated by collectDatabaseInstances()
1399 // Initialize to zero here
1400 c.mx.DatabaseCountActive = 0
1401 c.mx.DatabaseCountInactive = 0
1402
1403 // CPU metrics
1404 // Temporary storage for raw nanosecond values
1405 var cpuUserNs, cpuSystemNs, cpuIdleNs, cpuIowaitNs float64
1406
1407 runSimpleQuery(querySimpleCPUSystem, "", func(column, value string) {
1408 switch column {
1409 case "CPU_USER_TOTAL":
1410 if v, err := strconv.ParseFloat(value, 64); err == nil {
1411 cpuUserNs = v // Store raw nanoseconds
1412 }
1413 case "CPU_SYSTEM_TOTAL":
1414 if v, err := strconv.ParseFloat(value, 64); err == nil {
1415 cpuSystemNs = v
1416 }
1417 case "CPU_IDLE_TOTAL":
1418 if v, err := strconv.ParseFloat(value, 64); err == nil {
1419 cpuIdleNs = v
1420 }
1421 case "CPU_IOWAIT_TOTAL":
1422 if v, err := strconv.ParseFloat(value, 64); err == nil {
1423 cpuIowaitNs = v
1424 }
1425 }
1426 })
1427
1428 // Convert nanoseconds to percentages
1429 totalNs := cpuUserNs + cpuSystemNs + cpuIdleNs + cpuIowaitNs
1430 if totalNs > 0 {
1431 // Calculate percentages and apply Precision for storage
1432 c.mx.CPUUser = int64((cpuUserNs / totalNs) * 100 * Precision)
1433 c.mx.CPUSystem = int64((cpuSystemNs / totalNs) * 100 * Precision)
1434 c.mx.CPUIdle = int64((cpuIdleNs / totalNs) * 100 * Precision)
1435 c.mx.CPUIowait = int64((cpuIowaitNs / totalNs) * 100 * Precision)
1436 }
1437
1438 // Connection metrics
1439 runSimpleQuery(querySimpleConnectionsActive, querySnapConnectionsActive, func(column, value string) {
1440 if column == "ACTIVE_CONNECTIONS" {
1441 if v, err := strconv.ParseInt(value, 10, 64); err == nil {
1442 c.mx.ConnectionsActive = v
1443 }
1444 }
1445 })
1446
1447 runSimpleQuery(querySimpleConnectionsTotal, querySnapConnectionsTotal, func(column, value string) {
1448 if column == "TOTAL_CONNECTIONS" {
1449 if v, err := strconv.ParseInt(value, 10, 64); err == nil {
1450 c.mx.ConnectionsTotal = v
1451 }
1452 }
1453 })
1454
1455 // Memory metrics
1456 runSimpleQuery(querySimpleMemoryInstance, "", func(column, value string) {
1457 if column == "INSTANCE_MEM_COMMITTED" {
1458 if v, err := strconv.ParseInt(value, 10, 64); err == nil {
1459 c.mx.MemoryInstanceCommitted = v
1460 }
1461 }
1462 })
1463
1464 runSimpleQuery(querySimpleMemoryDatabase, "", func(column, value string) {
1465 if column == "DATABASE_MEM_COMMITTED" {
1466 if v, err := strconv.ParseInt(value, 10, 64); err == nil {
1467 c.mx.MemoryDatabaseCommitted = v
1468 }
1469 }
1470 })
1471
1472 runSimpleQuery(querySimpleMemoryBufferpool, "", func(column, value string) {
1473 if column == "BUFFERPOOL_MEM_USED" {
1474 if v, err := strconv.ParseInt(value, 10, 64); err == nil {
1475 c.mx.MemoryBufferpoolUsed = v
1476 }
1477 }
1478 })
1479
1480 runSimpleQuery(querySimpleMemorySharedSort, "", func(column, value string) {
1481 if column == "SHARED_SORT_MEM_USED" {
1482 if v, err := strconv.ParseInt(value, 10, 64); err == nil {
1483 c.mx.MemorySharedSortUsed = v
1484 }
1485 }
1486 })
1487
1488 // Throughput metrics
1489 runSimpleQuery(querySimpleTransactions, querySnapTransactions, func(column, value string) {
1490 if column == "TRANSACTIONS" {
1491 if v, err := strconv.ParseInt(value, 10, 64); err == nil {
1492 c.mx.OpsTransactions = v
1493 }
1494 }
1495 })
1496
1497 runSimpleQuery(querySimpleSelectStmts, querySnapSelectStmts, func(column, value string) {
1498 if column == "SELECT_STMTS" {
1499 if v, err := strconv.ParseInt(value, 10, 64); err == nil {
1500 c.mx.OpsSelectStmts = v
1501 }
1502 }
1503 })
1504
1505 runSimpleQuery(querySimpleUIDStmts, querySnapUIDStmts, func(column, value string) {
1506 if column == "UID_STMTS" {
1507 if v, err := strconv.ParseInt(value, 10, 64); err == nil {
1508 c.mx.OpsUIDStmts = v
1509 }
1510 }
1511 })
1512
1513 runSimpleQuery(querySimpleActivitiesAborted, querySnapActivitiesAborted, func(column, value string) {
1514 if column == "ACTIVITIES_ABORTED" {
1515 if v, err := strconv.ParseInt(value, 10, 64); err == nil {
1516 c.mx.OpsActivitiesAborted = v
1517 }
1518 }
1519 })
1520
1521 // Time spent metrics
1522 runSimpleQuery(querySimpleAvgDirectReadTime, querySnapAvgDirectReadTime, func(column, value string) {
1523 if column == "AVG_DIRECT_READ_TIME" {
1524 if v, err := strconv.ParseFloat(value, 64); err == nil {
1525 c.mx.TimeAvgDirectRead = int64(v * Precision * 1000) // Convert to microseconds
1526 }
1527 }
1528 })
1529
1530 runSimpleQuery(querySimpleAvgDirectWriteTime, querySnapAvgDirectWriteTime, func(column, value string) {
1531 if column == "AVG_DIRECT_WRITE_TIME" {
1532 if v, err := strconv.ParseFloat(value, 64); err == nil {
1533 c.mx.TimeAvgDirectWrite = int64(v * Precision * 1000)
1534 }
1535 }
1536 })
1537
1538 runSimpleQuery(querySimpleAvgPoolReadTime, querySnapAvgPoolReadTime, func(column, value string) {
1539 if column == "AVG_POOL_READ_TIME" {
1540 if v, err := strconv.ParseFloat(value, 64); err == nil {
1541 c.mx.TimeAvgPoolRead = int64(v * Precision * 1000)
1542 }
1543 }
1544 })
1545
1546 runSimpleQuery(querySimpleAvgPoolWriteTime, querySnapAvgPoolWriteTime, func(column, value string) {
1547 if column == "AVG_POOL_WRITE_TIME" {
1548 if v, err := strconv.ParseFloat(value, 64); err == nil {
1549 c.mx.TimeAvgPoolWrite = int64(v * Precision * 1000)
1550 }
1551 }
1552 })
1553
1554 // Additional metrics that were not in the original complex query
1555 // but are collected by dcmtop
1556
1557 // Lock metrics
1558 runSimpleQuery(querySimpleLockHeld, querySnapLockHeld, func(column, value string) {
1559 if column == "LOCKS_HELD" {
1560 if v, err := strconv.ParseInt(value, 10, 64); err == nil {
1561 c.mx.LockActive = v
1562 }
1563 }
1564 })
1565
1566 runSimpleQuery(querySimpleLockWaits, querySnapLockWaits, func(column, value string) {
1567 if column == "LOCK_WAITS" {
1568 if v, err := strconv.ParseInt(value, 10, 64); err == nil {
1569 c.mx.LockWaits = v
1570 }
1571 }
1572 })
1573
1574 runSimpleQuery(querySimpleLockTimeouts, querySnapLockTimeouts, func(column, value string) {
1575 if column == "LOCK_TIMEOUTS" {
1576 if v, err := strconv.ParseInt(value, 10, 64); err == nil {
1577 c.mx.LockTimeouts = v
1578 }
1579 }
1580 })
1581
1582 runSimpleQuery(querySimpleDeadlocks, querySnapDeadlocks, func(column, value string) {
1583 if column == "DEADLOCKS" {
1584 if v, err := strconv.ParseInt(value, 10, 64); err == nil {
1585 c.mx.Deadlocks = v
1586 }
1587 }
1588 })
1589
1590 // Log operations
1591 runSimpleQuery(querySimpleLogReads, querySnapLogReads, func(column, value string) {
1592 if column == "LOG_READS" {
1593 if v, err := strconv.ParseInt(value, 10, 64); err == nil {
1594 c.mx.LogOpReads = v
1595 }
1596 }
1597 })
1598
1599 runSimpleQuery(querySimpleLogWrites, querySnapLogWrites, func(column, value string) {
1600 if column == "LOG_WRITES" {
1601 if v, err := strconv.ParseInt(value, 10, 64); err == nil {
1602 c.mx.LogOpWrites = v
1603 }
1604 }
1605 })
1606
1607 // Sorts
1608 runSimpleQuery(querySimpleSorts, querySnapSorts, func(column, value string) {
1609 if column == "SORTS" {
1610 if v, err := strconv.ParseInt(value, 10, 64); err == nil {
1611 c.mx.TotalSorts = v
1612 }
1613 }
1614 })
1615
1616 runSimpleQuery(querySimpleSortOverflows, querySnapSortOverflows, func(column, value string) {
1617 if column == "SORT_OVERFLOWS" {
1618 if v, err := strconv.ParseInt(value, 10, 64); err == nil {
1619 c.mx.SortOverflows = v
1620 }
1621 }
1622 })
1623
1624 return nil // Always return nil since we run queries individually and handle errors gracefully
1625 }
1626
1627 // Enhanced Logging Performance collection (Screen 18)
1628 func (c *Collector) collectLoggingPerformance(ctx context.Context) error {
1629 // Use individual tested queries from dcmtop instead of complex combined queries
1630
1631 // Collect log commits (from TOTAL_APP_COMMITS)
1632 _ = c.collectSingleMetric(ctx, "log_commits", queryLogCommits, func(value string) {
1633 if v, err := strconv.ParseInt(value, 10, 64); err == nil {
1634 c.mx.LogCommits = v
1635 }
1636 })
1637
1638 // Collect log rollbacks (from TOTAL_APP_ROLLBACKS)
1639 _ = c.collectSingleMetric(ctx, "log_rollbacks", queryLogRollbacks, func(value string) {
1640 if v, err := strconv.ParseInt(value, 10, 64); err == nil {
1641 c.mx.LogRollbacks = v
1642 }
1643 })
1644
1645 // Collect log I/O reads (from NUM_LOG_READ_IO)
1646 _ = c.collectSingleMetric(ctx, "log_io_reads", queryLoggingReads, func(value string) {
1647 if v, err := strconv.ParseInt(value, 10, 64); err == nil {
1648 c.mx.LogIOReads = v
1649 }
1650 })
1651
1652 // Collect log I/O writes (from NUM_LOG_WRITE_IO)
1653 _ = c.collectSingleMetric(ctx, "log_io_writes", queryLoggingWrites, func(value string) {
1654 if v, err := strconv.ParseInt(value, 10, 64); err == nil {
1655 c.mx.LogIOWrites = v
1656 }
1657 })
1658
1659 return nil
1660 }
1661
1662 // Federation metrics collection removed
1663 // The federation queries contained static values and have been removed
1664 func (c *Collector) collectFederationMetrics(ctx context.Context) error {
1665 // Federation support was removed along with Cloud-specific queries
1666 return nil
1667 }
1668
1669 // contains is a helper function to check if all target strings are present in the slice
1670 func contains(slice []string, targets ...string) bool {
1671 for _, target := range targets {
1672 found := slices.Contains(slice, target)
1673 if !found {
1674 return false
1675 }
1676 }
1677 return true
1678 }