master
go 1,833 lines 50.4 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 //go:build cgo
4
5 package as400
6
7 import (
8 "context"
9 "fmt"
10 "maps"
11 "math"
12 "strconv"
13 "strings"
14 "time"
15 )
16
17 const precision = 1000 // Precision multiplier for floating-point values
18
19 // cleanNumericString removes all non-numeric characters except digits, minus, and decimal point
20 func cleanNumericString(value string) string {
21 trimmed := strings.TrimSpace(value)
22 if trimmed == "" || strings.EqualFold(trimmed, "NULL") || strings.EqualFold(trimmed, "N/A") {
23 return ""
24 }
25 cleaned := strings.Map(func(r rune) rune {
26 switch {
27 case r >= '0' && r <= '9':
28 return r
29 case r == '-' || r == '.':
30 return r
31 case r == 'e' || r == 'E' || r == '+':
32 return r
33 default:
34 return -1
35 }
36 }, trimmed)
37 return cleaned
38 }
39
40 // parseInt64Value parses a value as int64 with optional multiplier, returns (result, ok)
41 // Automatically handles both integers and floats from IBM i
42 // Logs all parse attempts in debug mode
43 func (a *Collector) parseInt64Value(value string, multiplier int64) (int64, bool) {
44 cleaned := cleanNumericString(value)
45 if cleaned == "" || cleaned == "-" || cleaned == "." || cleaned == "+" {
46 a.Debugf("parseInt64Value: empty/invalid value='%s', cleaned='%s'", value, cleaned)
47 return 0, false
48 }
49 if strings.Count(cleaned, ".") > 1 {
50 a.Debugf("parseInt64Value: too many decimal points, value='%s', cleaned='%s'", value, cleaned)
51 return 0, false
52 }
53 if strings.Count(cleaned, "e")+strings.Count(cleaned, "E") > 1 {
54 a.Debugf("parseInt64Value: too many exponents, value='%s', cleaned='%s'", value, cleaned)
55 return 0, false
56 }
57 if multiplier <= 0 {
58 multiplier = 1
59 }
60
61 // Handle floats/exponentials from IBM i (like memory sizes: "8192.00" or "7.8e+09")
62 if strings.Contains(cleaned, ".") || strings.ContainsAny(cleaned, "eE") {
63 f, err := strconv.ParseFloat(cleaned, 64)
64 if err != nil {
65 a.Debugf("parseInt64Value: ParseFloat failed, value='%s', cleaned='%s', error=%v", value, cleaned, err)
66 return 0, false
67 }
68 return int64(math.Round(f * float64(multiplier))), true
69 }
70
71 // Handle integers
72 v, err := strconv.ParseInt(cleaned, 10, 64)
73 if err != nil {
74 a.Debugf("parseInt64Value: ParseInt failed, value='%s', cleaned='%s', error=%v", value, cleaned, err)
75 return 0, false
76 }
77 if multiplier != 1 {
78 return v * multiplier, true
79 }
80 return v, true
81 }
82
83 // parseFloat64Value parses a value as float64, returns (result, ok)
84 // Logs all parse attempts in debug mode
85 func (a *Collector) computeEntitledCPUPercentage(cpuUtilization float64) int64 {
86 if a.mx.CurrentCPUCapacity <= 0 {
87 return 0
88 }
89 capacityPercent := float64(a.mx.CurrentCPUCapacity) / float64(precision)
90 if capacityPercent <= 0 {
91 return 0
92 }
93 perCorePercent := cpuUtilization / float64(precision)
94 entitled := (perCorePercent / capacityPercent) * 100.0
95 if entitled < 0 {
96 entitled = 0
97 }
98 return int64(math.Round(entitled * float64(precision)))
99 }
100
101 func (a *Collector) applyCPUUtilization(method string, cpuUtilization float64) {
102 adjusted := cpuUtilization
103 if adjusted < 0 {
104 a.Warningf("CPU collection (%s): interval utilization negative (%.2f%%), clamping to 0", method, adjusted/float64(precision))
105 adjusted = 0
106 }
107 if cpus := a.mx.ConfiguredCPUs; cpus > 0 {
108 maxAllowed := float64(cpus) * 100.0 * float64(precision)
109 if adjusted > maxAllowed {
110 a.Warningf("CPU collection (%s): interval utilization (%.2f%%) exceeds configured capacity (%d CPUs), clamping to %.2f%%",
111 method, adjusted/float64(precision), cpus, maxAllowed/float64(precision))
112 adjusted = maxAllowed
113 }
114 }
115 value := int64(math.Round(adjusted))
116 a.mx.systemActivity.AverageCPUUtilization = value
117 a.mx.systemActivity.AverageCPURate = value
118 a.mx.CPUPercentage = value
119 a.mx.EntitledCPUPercentage = a.computeEntitledCPUPercentage(adjusted)
120 }
121
122 func (a *Collector) parseFloat64Value(value string) (float64, bool) {
123 cleaned := cleanNumericString(value)
124 if cleaned == "" || cleaned == "-" || cleaned == "." || cleaned == "+" {
125 a.Debugf("parseFloat64Value: empty/invalid value='%s', cleaned='%s'", value, cleaned)
126 return 0, false
127 }
128 if strings.Count(cleaned, ".") > 1 {
129 a.Debugf("parseFloat64Value: too many decimal points, value='%s', cleaned='%s'", value, cleaned)
130 return 0, false
131 }
132 if strings.Count(cleaned, "e")+strings.Count(cleaned, "E") > 1 {
133 a.Debugf("parseFloat64Value: too many exponents, value='%s', cleaned='%s'", value, cleaned)
134 return 0, false
135 }
136
137 f, err := strconv.ParseFloat(cleaned, 64)
138 if err != nil {
139 a.Debugf("parseFloat64Value: ParseFloat failed, value='%s', cleaned='%s', error=%v", value, cleaned, err)
140 return 0, false
141 }
142 return f, true
143 }
144
145 func planCacheMetricKey(heading string) string {
146 trimmed := strings.TrimSpace(heading)
147 if trimmed == "" {
148 return ""
149 }
150 trimmed = strings.ToLower(trimmed)
151 var builder strings.Builder
152 lastUnderscore := false
153 for _, r := range trimmed {
154 switch {
155 case r >= 'a' && r <= 'z', r >= '0' && r <= '9':
156 builder.WriteRune(r)
157 lastUnderscore = false
158 case r == ' ' || r == '-' || r == '/' || r == '\\' || r == ':' || r == '%' || r == '(' || r == ')' || r == '.':
159 if !lastUnderscore {
160 builder.WriteRune('_')
161 lastUnderscore = true
162 }
163 default:
164 // skip other punctuation
165 }
166 }
167 result := builder.String()
168 result = strings.Trim(result, "_")
169 if result == "" {
170 result = cleanName(trimmed)
171 }
172 if result == "" {
173 return "plan_cache_metric"
174 }
175 return result
176 }
177
178 func normalizeValue(value string) string {
179 trimmed := strings.TrimSpace(value)
180 if trimmed == "" || strings.EqualFold(trimmed, "NULL") {
181 return ""
182 }
183 return trimmed
184 }
185
186 // parseInt64OrZero parses value as int64, returns 0 on any error (no logging)
187 func parseInt64OrZero(value string) int64 {
188 cleaned := cleanNumericString(value)
189 if cleaned == "" || cleaned == "-" || cleaned == "." || cleaned == "+" {
190 return 0
191 }
192 if strings.Count(cleaned, ".") > 1 {
193 return 0
194 }
195 if strings.Count(cleaned, "e")+strings.Count(cleaned, "E") > 1 {
196 return 0
197 }
198
199 // Handle floats/exponentials
200 if strings.Contains(cleaned, ".") || strings.ContainsAny(cleaned, "eE") {
201 if f, err := strconv.ParseFloat(cleaned, 64); err == nil {
202 return int64(math.Round(f))
203 }
204 return 0
205 }
206
207 // Handle integers
208 if v, err := strconv.ParseInt(cleaned, 10, 64); err == nil {
209 return v
210 }
211 return 0
212 }
213
214 func boolToInt(cond bool) int64 {
215 if cond {
216 return 1
217 }
218 return 0
219 }
220
221 func (a *Collector) collect(ctx context.Context) error {
222 startTime := time.Now()
223 defer func() {
224 duration := time.Since(startTime)
225 a.Debugf("collection iteration completed in %v", duration)
226 }()
227
228 a.prepareIterationState()
229 a.initGroups()
230
231 for _, grp := range a.groups {
232 if grp == nil || !grp.Enabled() {
233 continue
234 }
235 if err := grp.Collect(ctx); err != nil {
236 return fmt.Errorf("%s: %w", grp.Name(), err)
237 }
238 }
239
240 return nil
241 }
242
243 func (a *Collector) recordQueryLatency(queryName string, duration time.Duration) {
244 if queryName == "" {
245 queryName = "unknown_query"
246 }
247
248 sanitized := cleanName(queryName)
249 if sanitized == "" {
250 sanitized = "unknown_query"
251 }
252
253 latency := duration.Microseconds()
254 if latency == 0 && duration > 0 {
255 latency = 1
256 }
257
258 if a.fastQueryLatencyCounters == nil {
259 a.fastQueryLatencyCounters = make(map[string]int64)
260 }
261 a.fastQueryLatencyCounters[sanitized] += latency
262 }
263
264 func (a *Collector) collectSystemStatus(ctx context.Context) error {
265 // Use comprehensive query to get all system status metrics at once
266 err := a.doQuery(ctx, "system_status", a.systemStatusQuery(), func(column, value string, lineEnd bool) {
267 // Debug log all columns to see what we're receiving
268 if strings.Contains(column, "STORAGE") || strings.Contains(column, "MEMORY") {
269 a.Debugf("collectSystemStatus: column='%s', value='%s'", column, value)
270 }
271
272 // Skip empty values
273 if value == "" {
274 return
275 }
276
277 switch column {
278 // CPU metrics
279 case "AVERAGE_CPU_UTILIZATION":
280 // AVERAGE_CPU_UTILIZATION is system-wide 0-100% (deprecated in IBM i 7.4+)
281 if v, ok := a.parseInt64Value(value, precision); ok {
282 a.mx.CPUPercentage = v
283 a.mx.EntitledCPUPercentage = a.computeEntitledCPUPercentage(float64(v))
284 }
285 case "CURRENT_CPU_CAPACITY":
286 // CURRENT_CPU_CAPACITY comes from IBM as decimal fraction (0.0-1.0)
287 // Convert to percentage by multiplying by 100
288 if v, ok := a.parseInt64Value(value, precision); ok {
289 // Convert decimal fraction (e.g., 0.20) to percentage scale
290 a.mx.CurrentCPUCapacity = v * 100
291 }
292 case "CONFIGURED_CPUS":
293 if v, ok := a.parseInt64Value(value, 1); ok {
294 a.mx.ConfiguredCPUs = v
295 }
296
297 // Memory metrics
298 case "MAIN_STORAGE_SIZE":
299 if v, ok := a.parseInt64Value(value, 1024); ok { // Convert KB to bytes
300 a.mx.MainStorageSize = v
301 }
302 case "CURRENT_TEMPORARY_STORAGE":
303 if v, ok := a.parseInt64Value(value, 1); ok {
304 a.mx.CurrentTemporaryStorage = v // MB
305 }
306 case "MAXIMUM_TEMPORARY_STORAGE_USED":
307 if v, ok := a.parseInt64Value(value, 1); ok {
308 a.mx.MaximumTemporaryStorageUsed = v // MB
309 }
310
311 // Job metrics
312 case "TOTAL_JOBS_IN_SYSTEM":
313 if v, ok := a.parseInt64Value(value, 1); ok {
314 a.mx.TotalJobsInSystem = v
315 }
316 case "ACTIVE_JOBS_IN_SYSTEM":
317 if v, ok := a.parseInt64Value(value, 1); ok {
318 a.mx.ActiveJobsInSystem = v
319 }
320 case "INTERACTIVE_JOBS_IN_SYSTEM":
321 if v, ok := a.parseInt64Value(value, 1); ok {
322 a.mx.InteractiveJobsInSystem = v
323 }
324 case "BATCH_RUNNING":
325 if v, ok := a.parseInt64Value(value, 1); ok {
326 a.mx.BatchJobsRunning = v
327 }
328
329 // Storage metrics
330 case "SYSTEM_ASP_USED":
331 if v, ok := a.parseInt64Value(value, precision); ok {
332 a.mx.SystemASPUsed = v
333 }
334 case "SYSTEM_ASP_STORAGE":
335 if v, ok := a.parseInt64Value(value, 1); ok {
336 a.mx.SystemASPStorage = v // MB
337 }
338 case "TOTAL_AUXILIARY_STORAGE":
339 if v, ok := a.parseInt64Value(value, 1); ok {
340 a.mx.TotalAuxiliaryStorage = v // MB
341 }
342
343 // Thread metrics
344 case "ACTIVE_THREADS_IN_SYSTEM":
345 if v, ok := a.parseInt64Value(value, 1); ok {
346 a.mx.ActiveThreadsInSystem = v
347 }
348 case "THREADS_PER_PROCESSOR":
349 if v, ok := a.parseInt64Value(value, 1); ok {
350 a.mx.ThreadsPerProcessor = v
351 }
352 }
353 })
354
355 if err != nil {
356 return fmt.Errorf("failed to collect system status: %v", err)
357 }
358
359 return nil
360 }
361
362 func (a *Collector) collectMemoryPools(ctx context.Context) error {
363 var currentPoolName string
364 return a.doQuery(ctx, "memory_pools", a.memoryPoolQuery(), func(column, value string, lineEnd bool) {
365 switch column {
366 case "POOL_NAME":
367 currentPoolName = strings.TrimSpace(value)
368 case "CURRENT_SIZE":
369 if v, ok := a.parseInt64Value(value, 1024*1024); ok { // Convert MB to bytes
370 switch currentPoolName {
371 case "*MACHINE":
372 a.mx.MachinePoolSize = v
373 case "*BASE":
374 a.mx.BasePoolSize = v
375 case "*INTERACT":
376 a.mx.InteractivePoolSize = v
377 case "*SPOOL":
378 a.mx.SpoolPoolSize = v
379 }
380 }
381 case "DEFINED_SIZE":
382 if v, ok := a.parseInt64Value(value, 1024*1024); ok { // Convert MB to bytes
383 switch currentPoolName {
384 case "*MACHINE":
385 a.mx.MachinePoolDefinedSize = v
386 case "*BASE":
387 a.mx.BasePoolDefinedSize = v
388 }
389 }
390 case "RESERVED_SIZE":
391 if v, ok := a.parseInt64Value(value, 1024*1024); ok { // Convert MB to bytes
392 switch currentPoolName {
393 case "*MACHINE":
394 a.mx.MachinePoolReservedSize = v
395 case "*BASE":
396 a.mx.BasePoolReservedSize = v
397 }
398 }
399 case "CURRENT_THREADS":
400 if v, ok := a.parseInt64Value(value, 1); ok {
401 switch currentPoolName {
402 case "*MACHINE":
403 a.mx.MachinePoolThreads = v
404 case "*BASE":
405 a.mx.BasePoolThreads = v
406 }
407 }
408 case "MAXIMUM_ACTIVE_THREADS":
409 if v, ok := a.parseInt64Value(value, 1); ok {
410 switch currentPoolName {
411 case "*MACHINE":
412 a.mx.MachinePoolMaxThreads = v
413 case "*BASE":
414 a.mx.BasePoolMaxThreads = v
415 }
416 }
417 }
418 })
419 }
420
421 func (a *Collector) collectDiskStatus(ctx context.Context) error {
422 // Try modern query first
423 err := a.doQuery(ctx, "disk_status", queryDiskStatus, func(column, value string, lineEnd bool) {
424 if column == "AVG_DISK_BUSY" {
425 if v, ok := a.parseInt64Value(value, precision); ok {
426 a.mx.DiskBusyPercentage = v
427 }
428 }
429 })
430
431 return err
432 }
433
434 func (a *Collector) collectJobInfo(ctx context.Context) error {
435 // Try modern query first
436 err := a.doQuery(ctx, "job_info", queryJobInfo, func(column, value string, lineEnd bool) {
437 if column == "JOB_QUEUE_LENGTH" {
438 if v, ok := a.parseInt64Value(value, 1); ok {
439 a.mx.JobQueueLength = v
440 }
441 }
442 })
443
444 return err
445 }
446
447 func (a *Collector) collectMessageQueues(ctx context.Context) error {
448 if len(a.messageQueueTargets) == 0 {
449 return nil
450 }
451
452 if a.slowPathActive() {
453 snapshot := a.slow.cache.getMessageQueues()
454 for _, target := range a.messageQueueTargets {
455 key := target.ID()
456 meta := a.getMessageQueueMetrics(key)
457 *meta = messageQueueMetrics{
458 library: target.Library,
459 name: target.Name,
460 }
461 if snapshotMeta, ok := snapshot.meta[key]; ok {
462 if snapshotMeta.library != "" {
463 meta.library = snapshotMeta.library
464 }
465 if snapshotMeta.name != "" {
466 meta.name = snapshotMeta.name
467 }
468 }
469 a.messageQueues[key] = meta
470 a.mx.messageQueues[key] = snapshot.metrics[key]
471 }
472 return snapshot.err
473 }
474
475 var firstErr error
476
477 for _, target := range a.messageQueueTargets {
478 key := target.ID()
479 meta := a.getMessageQueueMetrics(key)
480 meta.library = target.Library
481 meta.name = target.Name
482 a.messageQueues[key] = meta
483
484 metrics := messageQueueInstanceMetrics{}
485 found := false
486
487 queryName := fmt.Sprintf("message_queue_%s_%s", target.Library, target.Name)
488 query := buildMessageQueueQuery(target, a.supportsMessageQueueTableFunction())
489
490 err := a.doQuery(ctx, queryName, query, func(column, value string, lineEnd bool) {
491 switch column {
492 case "MESSAGE_COUNT":
493 metrics.Total = parseInt64OrZero(value)
494 case "INFORMATIONAL_MESSAGES":
495 metrics.Informational = parseInt64OrZero(value)
496 case "INQUIRY_MESSAGES":
497 metrics.Inquiry = parseInt64OrZero(value)
498 case "DIAGNOSTIC_MESSAGES":
499 metrics.Diagnostic = parseInt64OrZero(value)
500 case "ESCAPE_MESSAGES":
501 metrics.Escape = parseInt64OrZero(value)
502 case "NOTIFY_MESSAGES":
503 metrics.Notify = parseInt64OrZero(value)
504 case "SENDER_COPY_MESSAGES":
505 metrics.SenderCopy = parseInt64OrZero(value)
506 case "MAX_SEVERITY":
507 metrics.MaxSeverity = parseInt64OrZero(value)
508 }
509
510 if lineEnd {
511 found = true
512 }
513 })
514
515 if err != nil {
516 if firstErr == nil {
517 firstErr = fmt.Errorf("message queue %s: %w", key, err)
518 }
519 continue
520 }
521
522 if !found {
523 metrics = messageQueueInstanceMetrics{}
524 }
525
526 a.mx.messageQueues[key] = metrics
527 }
528
529 return firstErr
530 }
531
532 func (a *Collector) collectOutputQueues(ctx context.Context) error {
533 if len(a.outputQueueTargets) == 0 {
534 return nil
535 }
536
537 if a.slowPathActive() {
538 snapshot := a.slow.cache.getOutputQueues()
539 for _, target := range a.outputQueueTargets {
540 key := target.ID()
541 metaPtr := a.getOutputQueueMetrics(key)
542 defaultMeta := outputQueueMetrics{
543 library: target.Library,
544 name: target.Name,
545 status: "UNKNOWN",
546 }
547 if snapshotMeta, ok := snapshot.meta[key]; ok {
548 *metaPtr = snapshotMeta
549 } else {
550 *metaPtr = defaultMeta
551 }
552 a.outputQueues[key] = metaPtr
553 a.mx.outputQueues[key] = snapshot.metrics[key]
554 }
555 return snapshot.err
556 }
557
558 var firstErr error
559
560 for _, target := range a.outputQueueTargets {
561 key := target.ID()
562 meta := a.getOutputQueueMetrics(key)
563 meta.library = target.Library
564 meta.name = target.Name
565 meta.status = "UNKNOWN"
566 a.outputQueues[key] = meta
567
568 metrics := outputQueueInstanceMetrics{}
569 entriesCount := int64(0)
570 entriesUsed := false
571
572 queryName := fmt.Sprintf("output_queue_%s_%s", target.Library, target.Name)
573 err := a.doQuery(ctx, queryName, buildOutputQueueEntriesQuery(target), func(column, value string, lineEnd bool) {
574 if lineEnd {
575 entriesCount++
576 }
577 })
578 if err != nil {
579 if isSQLFeatureError(err) {
580 a.Debugf("output queue entries function unavailable for %s/%s, falling back to view", target.Library, target.Name)
581 } else if firstErr == nil {
582 firstErr = fmt.Errorf("output queue %s (entries): %w", key, err)
583 }
584 } else {
585 entriesUsed = true
586 metrics.Files = entriesCount
587 }
588
589 viewErr := a.doQuery(ctx, queryName+"_view", buildOutputQueueInfoQuery(target), func(column, value string, lineEnd bool) {
590 switch column {
591 case "OUTPUT_QUEUE_STATUS":
592 meta.status = strings.TrimSpace(value)
593 case "NUMBER_OF_WRITERS":
594 metrics.Writers = parseInt64OrZero(value)
595 case "NUMBER_OF_FILES":
596 if !entriesUsed {
597 metrics.Files = parseInt64OrZero(value)
598 }
599 }
600 })
601 if viewErr != nil {
602 if firstErr == nil {
603 firstErr = fmt.Errorf("output queue %s (info): %w", key, viewErr)
604 }
605 continue
606 }
607
608 metrics.Released = boolToInt(strings.EqualFold(meta.status, "RELEASED"))
609 a.outputQueues[key] = meta
610 a.mx.outputQueues[key] = metrics
611 }
612
613 return firstErr
614 }
615
616 func (a *Collector) doQuery(ctx context.Context, queryName, query string, assign func(column, value string, lineEnd bool)) error {
617 var (
618 capture bool
619 columnsSaved []string
620 rowsSaved [][]string
621 )
622 if a.dump != nil {
623 capture = true
624 }
625
626 start := time.Now()
627 defer func() {
628 a.recordQueryLatency(queryName, time.Since(start))
629 }()
630
631 err := a.client.Query(ctx, query, func(columns []string, values []string) error {
632 for i, col := range columns {
633 assign(col, values[i], i == len(columns)-1)
634 }
635 if capture {
636 if columnsSaved == nil {
637 columnsSaved = append(columnsSaved, columns...)
638 }
639 rowCopy := make([]string, len(values))
640 copy(rowCopy, values)
641 rowsSaved = append(rowsSaved, rowCopy)
642 }
643 return nil
644 })
645 if err != nil {
646 if isSQLFeatureError(err) {
647 a.Debugf("query failed with expected feature error: %s, error: %v", query, err)
648 } else if isSQLTemporaryError(err) {
649 a.Debugf("query failed with temporary database error: %s, error: %v", query, err)
650 } else {
651 a.Errorf("failed to execute query: %s, error: %v", query, err)
652 }
653 return err
654 }
655 if capture && columnsSaved != nil {
656 a.dump.recordQuery(query, columnsSaved, rowsSaved)
657 }
658 return nil
659 }
660
661 // doQueryRow executes a query that returns a single row
662 func (a *Collector) doQueryRow(ctx context.Context, queryName, query string, assign func(column, value string)) error {
663 var (
664 capture bool
665 columnsSaved []string
666 rowsSaved [][]string
667 )
668 if a.dump != nil {
669 capture = true
670 }
671
672 start := time.Now()
673 defer func() {
674 a.recordQueryLatency(queryName, time.Since(start))
675 }()
676
677 err := a.client.QueryWithLimit(ctx, query, 1, func(columns []string, values []string) error {
678 for i, col := range columns {
679 assign(col, values[i])
680 }
681 if capture {
682 columnsSaved = append([]string{}, columns...)
683 rowCopy := make([]string, len(values))
684 copy(rowCopy, values)
685 rowsSaved = append(rowsSaved, rowCopy)
686 }
687 return nil
688 })
689 if err != nil {
690 if isSQLFeatureError(err) {
691 a.Debugf("query failed with expected feature error: %s, error: %v", query, err)
692 } else if isSQLTemporaryError(err) {
693 a.Debugf("query failed with temporary database error: %s, error: %v", query, err)
694 } else {
695 a.Errorf("failed to execute query: %s, error: %v", query, err)
696 }
697 return err
698 }
699 if capture && columnsSaved != nil {
700 a.dump.recordQuery(query, columnsSaved, rowsSaved)
701 }
702 return nil
703 }
704
705 // Per-instance collection methods
706
707 func (a *Collector) collectDiskInstances(ctx context.Context) error {
708 allowed, count, err := a.diskCardinality.Allow(ctx, a.countDisks)
709 if err != nil {
710 return err
711 }
712 if !allowed {
713 a.logOnce("disk_cardinality", "disk count (%d) exceeds limit (%d), skipping per-disk metrics", count, a.MaxDisks)
714 return nil
715 }
716
717 var currentUnit string
718 return a.doQuery(ctx, "disk_instances", queryDiskInstances, func(column, value string, lineEnd bool) {
719
720 switch column {
721 case "UNIT_NUMBER":
722 currentUnit = value
723
724 // Apply selector if configured
725 if a.diskSelector != nil && !a.diskSelector.MatchString(currentUnit) {
726 currentUnit = "" // Skip this disk
727 return
728 }
729
730 _ = a.getDiskMetrics(currentUnit)
731
732 case "UNIT_TYPE":
733 if currentUnit != "" && a.disks[currentUnit] != nil {
734 // Map IBM i disk type values to meaningful labels
735 switch value {
736 case "0":
737 a.disks[currentUnit].typeField = "HDD"
738 case "1":
739 a.disks[currentUnit].typeField = "SSD"
740 case "":
741 a.disks[currentUnit].typeField = "UNKNOWN"
742 default:
743 a.disks[currentUnit].typeField = value // Keep unknown values as-is
744 }
745 }
746 case "UNIT_MODEL":
747 if currentUnit != "" && a.disks[currentUnit] != nil {
748 a.disks[currentUnit].model = value
749 }
750 case "PERCENT_BUSY":
751 if currentUnit != "" && a.disks[currentUnit] != nil {
752 disk := a.disks[currentUnit]
753 if v, ok := a.parseInt64Value(value, precision); ok {
754 disk.busyPercent = v
755 if m, ok := a.mx.disks[currentUnit]; ok {
756 m.BusyPercent = disk.busyPercent
757 a.mx.disks[currentUnit] = m
758 } else {
759 a.mx.disks[currentUnit] = diskInstanceMetrics{
760 BusyPercent: disk.busyPercent,
761 }
762 }
763 }
764 }
765 case "READ_REQUESTS":
766 if currentUnit != "" && a.disks[currentUnit] != nil {
767 disk := a.disks[currentUnit]
768 if v, ok := a.parseInt64Value(value, 1); ok {
769 disk.readRequests = v
770 if m, ok := a.mx.disks[currentUnit]; ok {
771 m.ReadRequests = v
772 a.mx.disks[currentUnit] = m
773 } else {
774 a.mx.disks[currentUnit] = diskInstanceMetrics{
775 ReadRequests: v,
776 }
777 }
778 }
779 }
780 case "WRITE_REQUESTS":
781 if currentUnit != "" && a.disks[currentUnit] != nil {
782 disk := a.disks[currentUnit]
783 if v, ok := a.parseInt64Value(value, 1); ok {
784 disk.writeRequests = v
785 if m, ok := a.mx.disks[currentUnit]; ok {
786 m.WriteRequests = v
787 a.mx.disks[currentUnit] = m
788 } else {
789 a.mx.disks[currentUnit] = diskInstanceMetrics{
790 WriteRequests: v,
791 }
792 }
793 }
794 }
795 case "PERCENT_USED":
796 if currentUnit != "" && a.disks[currentUnit] != nil {
797 if v, ok := a.parseInt64Value(value, precision); ok {
798 if m, ok := a.mx.disks[currentUnit]; ok {
799 m.PercentUsed = v
800 a.mx.disks[currentUnit] = m
801 } else {
802 a.mx.disks[currentUnit] = diskInstanceMetrics{
803 PercentUsed: v,
804 }
805 }
806 }
807 }
808 case "UNIT_SPACE_AVAILABLE_GB":
809 if currentUnit != "" && a.disks[currentUnit] != nil {
810 if v, ok := a.parseInt64Value(value, precision); ok {
811 if m, ok := a.mx.disks[currentUnit]; ok {
812 m.AvailableGB = v
813 a.mx.disks[currentUnit] = m
814 } else {
815 a.mx.disks[currentUnit] = diskInstanceMetrics{
816 AvailableGB: v,
817 }
818 }
819 }
820 }
821 case "UNIT_STORAGE_CAPACITY":
822 if currentUnit != "" && a.disks[currentUnit] != nil {
823 if v, ok := a.parseInt64Value(value, precision); ok {
824 if m, ok := a.mx.disks[currentUnit]; ok {
825 m.CapacityGB = v
826 a.mx.disks[currentUnit] = m
827 } else {
828 a.mx.disks[currentUnit] = diskInstanceMetrics{
829 CapacityGB: v,
830 }
831 }
832 }
833 }
834 case "TOTAL_BLOCKS_READ":
835 if currentUnit != "" && a.disks[currentUnit] != nil {
836 if v, ok := a.parseInt64Value(value, 1); ok {
837 if m, ok := a.mx.disks[currentUnit]; ok {
838 m.BlocksRead = v
839 a.mx.disks[currentUnit] = m
840 } else {
841 a.mx.disks[currentUnit] = diskInstanceMetrics{
842 BlocksRead: v,
843 }
844 }
845 }
846 }
847 case "TOTAL_BLOCKS_WRITTEN":
848 if currentUnit != "" && a.disks[currentUnit] != nil {
849 if v, ok := a.parseInt64Value(value, 1); ok {
850 if m, ok := a.mx.disks[currentUnit]; ok {
851 m.BlocksWritten = v
852 a.mx.disks[currentUnit] = m
853 } else {
854 a.mx.disks[currentUnit] = diskInstanceMetrics{
855 BlocksWritten: v,
856 }
857 }
858 }
859 }
860 case "SSD_LIFE_REMAINING":
861 if currentUnit != "" && a.disks[currentUnit] != nil {
862 if v := parseInt64OrZero(value); v > 0 {
863 disk := a.disks[currentUnit]
864 disk.ssdLifeRemaining = v
865 if m, ok := a.mx.disks[currentUnit]; ok {
866 m.SSDLifeRemaining = v
867 a.mx.disks[currentUnit] = m
868 } else {
869 a.mx.disks[currentUnit] = diskInstanceMetrics{
870 SSDLifeRemaining: v,
871 }
872 }
873 }
874 }
875 case "SSD_POWER_ON_DAYS":
876 if currentUnit != "" && a.disks[currentUnit] != nil {
877 if v := parseInt64OrZero(value); v > 0 {
878 disk := a.disks[currentUnit]
879 disk.ssdPowerOnDays = v
880 if m, ok := a.mx.disks[currentUnit]; ok {
881 m.SSDPowerOnDays = v
882 a.mx.disks[currentUnit] = m
883 } else {
884 a.mx.disks[currentUnit] = diskInstanceMetrics{
885 SSDPowerOnDays: v,
886 }
887 }
888 }
889 }
890 case "HARDWARE_STATUS":
891 if currentUnit != "" && a.disks[currentUnit] != nil {
892 disk := a.disks[currentUnit]
893 disk.hardwareStatus = value
894 if m, ok := a.mx.disks[currentUnit]; ok {
895 m.HardwareStatus = value
896 a.mx.disks[currentUnit] = m
897 } else {
898 a.mx.disks[currentUnit] = diskInstanceMetrics{
899 HardwareStatus: value,
900 }
901 }
902 }
903 case "DISK_MODEL":
904 if currentUnit != "" && a.disks[currentUnit] != nil {
905 disk := a.disks[currentUnit]
906 disk.diskModel = value
907 if m, ok := a.mx.disks[currentUnit]; ok {
908 m.DiskModel = value
909 a.mx.disks[currentUnit] = m
910 } else {
911 a.mx.disks[currentUnit] = diskInstanceMetrics{
912 DiskModel: value,
913 }
914 }
915 }
916 case "SERIAL_NUMBER":
917 if currentUnit != "" && a.disks[currentUnit] != nil {
918 disk := a.disks[currentUnit]
919 disk.serialNumber = value
920 if m, ok := a.mx.disks[currentUnit]; ok {
921 m.SerialNumber = value
922 a.mx.disks[currentUnit] = m
923 } else {
924 a.mx.disks[currentUnit] = diskInstanceMetrics{
925 SerialNumber: value,
926 }
927 }
928 }
929 }
930
931 // After processing all columns for this disk, calculate used_gb if we have the required data
932 if lineEnd && currentUnit != "" {
933 if m, ok := a.mx.disks[currentUnit]; ok {
934 // Calculate used_gb from capacity - available
935 // Always calculate used_gb if we have capacity information
936 if m.CapacityGB > 0 {
937 usedGB := max(
938 // Ensure used_gb is not negative
939 m.CapacityGB-m.AvailableGB, 0)
940 m.UsedGB = usedGB
941 a.mx.disks[currentUnit] = m
942 }
943 }
944 }
945 })
946 }
947
948 func (a *Collector) countDisks(ctx context.Context) (int, error) {
949 var count int
950 err := a.doQuery(ctx, "count_disks", queryCountDisks, func(column, value string, lineEnd bool) {
951 if column == "COUNT" {
952 count = int(parseInt64OrZero(value))
953 }
954 })
955 return count, err
956 }
957
958 // Network connections collection
959 func (a *Collector) collectNetworkConnections(ctx context.Context) error {
960 return a.doQuery(ctx, "network_connections", queryNetworkConnections, func(column, value string, lineEnd bool) {
961 switch column {
962 case "REMOTE_CONNECTIONS":
963 if v, ok := a.parseInt64Value(value, 1); ok {
964 a.mx.RemoteConnections = v
965 }
966 case "TOTAL_CONNECTIONS":
967 if v, ok := a.parseInt64Value(value, 1); ok {
968 a.mx.TotalConnections = v
969 }
970 case "LISTEN_CONNECTIONS":
971 if v, ok := a.parseInt64Value(value, 1); ok {
972 a.mx.ListenConnections = v
973 }
974 case "CLOSEWAIT_CONNECTIONS":
975 if v, ok := a.parseInt64Value(value, 1); ok {
976 a.mx.CloseWaitConnections = v
977 }
978 }
979 })
980 }
981
982 func (a *Collector) countNetworkInterfaces(ctx context.Context) (int, error) {
983 var count int
984 err := a.doQueryRow(ctx, "count_network_interfaces", queryCountNetworkInterfaces, func(column, value string) {
985 if column == "COUNT" {
986 if v, ok := a.parseInt64Value(value, 1); ok {
987 count = int(v)
988 }
989 }
990 })
991 return count, err
992 }
993
994 func (a *Collector) countHTTPServers(ctx context.Context) (int, error) {
995 var count int64
996 err := a.doQueryRow(ctx, "count_http_servers", queryCountHTTPServers, func(column, value string) {
997 if column == "COUNT" {
998 if v, ok := a.parseInt64Value(value, 1); ok {
999 count = v
1000 }
1001 }
1002 })
1003 return int(count), err
1004 }
1005
1006 func withFetchLimit(query string, limit int) string {
1007 if limit <= 0 {
1008 return query
1009 }
1010 trimmed := strings.TrimSpace(query)
1011 upper := strings.ToUpper(trimmed)
1012 if strings.Contains(upper, "FETCH FIRST") {
1013 return trimmed
1014 }
1015 return fmt.Sprintf("%s FETCH FIRST %d ROWS ONLY", trimmed, limit)
1016 }
1017
1018 func (a *Collector) countSubsystems(ctx context.Context) (int, error) {
1019 var count int64
1020 err := a.doQueryRow(ctx, "count_subsystems", queryCountSubsystems, func(column, value string) {
1021 if column == "COUNT" {
1022 if v, ok := a.parseInt64Value(value, 1); ok {
1023 count = v
1024 }
1025 }
1026 })
1027 return int(count), err
1028 }
1029
1030 // Temporary storage collection
1031 func (a *Collector) collectTempStorage(ctx context.Context) error {
1032 // Collect total temp storage
1033 err := a.doQuery(ctx, "temp_storage_total", queryTempStorageTotal, func(column, value string, lineEnd bool) {
1034 switch column {
1035 case "CURRENT_SIZE":
1036 if v, ok := a.parseInt64Value(value, 1); ok {
1037 a.mx.TempStorageCurrentTotal = v
1038 }
1039 case "PEAK_SIZE":
1040 if v, ok := a.parseInt64Value(value, 1); ok {
1041 a.mx.TempStoragePeakTotal = v
1042 }
1043 }
1044 })
1045 if err != nil {
1046 return err
1047 }
1048
1049 // Collect named temp storage buckets
1050 var currentBucket string
1051 return a.doQuery(ctx, "temp_storage_named", queryTempStorageNamed, func(column, value string, lineEnd bool) {
1052 switch column {
1053 case "NAME":
1054 currentBucket = value
1055 _ = a.getTempStorageMetrics(currentBucket)
1056
1057 case "CURRENT_SIZE":
1058 if currentBucket != "" && a.tempStorageNamed[currentBucket] != nil {
1059 if v, ok := a.parseInt64Value(value, 1); ok {
1060 if m, ok := a.mx.tempStorageNamed[currentBucket]; ok {
1061 m.CurrentSize = v
1062 a.mx.tempStorageNamed[currentBucket] = m
1063 } else {
1064 a.mx.tempStorageNamed[currentBucket] = tempStorageInstanceMetrics{
1065 CurrentSize: v,
1066 }
1067 }
1068 }
1069 }
1070 case "PEAK_SIZE":
1071 if currentBucket != "" && a.tempStorageNamed[currentBucket] != nil {
1072 if v, ok := a.parseInt64Value(value, 1); ok {
1073 if m, ok := a.mx.tempStorageNamed[currentBucket]; ok {
1074 m.PeakSize = v
1075 a.mx.tempStorageNamed[currentBucket] = m
1076 }
1077 }
1078 }
1079 }
1080 })
1081 }
1082
1083 // Subsystems collection
1084 func (a *Collector) collectSubsystems(ctx context.Context) error {
1085 if a.slowPathActive() {
1086 snapshot := a.slow.cache.getSubsystems()
1087 for key, meta := range snapshot.meta {
1088 ptr := a.getSubsystemMetrics(key)
1089 *ptr = meta
1090 a.subsystems[key] = ptr
1091 }
1092 maps.Copy(a.mx.subsystems, snapshot.metrics)
1093 return snapshot.err
1094 }
1095
1096 query := querySubsystems
1097 if a.MaxSubsystems > 0 {
1098 if total, err := a.countSubsystems(ctx); err != nil {
1099 a.logOnce("subsystem_count_failed", "failed to count subsystems before applying limit: %v", err)
1100 } else if total > a.MaxSubsystems {
1101 a.logOnce("subsystem_limit", "subsystem count (%d) exceeds limit (%d); truncating results", total, a.MaxSubsystems)
1102 }
1103 query = withFetchLimit(query, a.MaxSubsystems)
1104 }
1105
1106 var currentSubsystem string
1107 return a.doQuery(ctx, "subsystems", query, func(column, value string, lineEnd bool) {
1108 switch column {
1109 case "SUBSYSTEM_NAME":
1110 name := strings.TrimSpace(value)
1111 if name == "" {
1112 currentSubsystem = ""
1113 return
1114 }
1115 if a.subsystemSelector != nil && !a.subsystemSelector.MatchString(name) {
1116 currentSubsystem = ""
1117 return
1118 }
1119 currentSubsystem = name
1120 subsystem := a.getSubsystemMetrics(currentSubsystem)
1121 parts := strings.SplitN(name, "/", 2)
1122 if len(parts) == 2 {
1123 subsystem.library = parts[0]
1124 subsystem.name = parts[1]
1125 } else {
1126 subsystem.name = name
1127 subsystem.library = ""
1128 }
1129 subsystem.status = "ACTIVE"
1130
1131 case "CURRENT_ACTIVE_JOBS":
1132 if currentSubsystem != "" && a.subsystems[currentSubsystem] != nil {
1133 if v, ok := a.parseInt64Value(value, 1); ok {
1134 if m, ok := a.mx.subsystems[currentSubsystem]; ok {
1135 m.CurrentActiveJobs = v
1136 a.mx.subsystems[currentSubsystem] = m
1137 } else {
1138 a.mx.subsystems[currentSubsystem] = subsystemInstanceMetrics{
1139 CurrentActiveJobs: v,
1140 }
1141 }
1142 }
1143 }
1144 case "MAXIMUM_ACTIVE_JOBS":
1145 if currentSubsystem != "" && a.subsystems[currentSubsystem] != nil {
1146 if v, ok := a.parseInt64Value(value, 1); ok {
1147 if m, ok := a.mx.subsystems[currentSubsystem]; ok {
1148 m.MaximumActiveJobs = v
1149 a.mx.subsystems[currentSubsystem] = m
1150 }
1151 }
1152 }
1153 // Note: HELD_JOB_COUNT and STORAGE_USED_KB columns removed - they don't exist in SUBSYSTEM_INFO table
1154 }
1155
1156 if lineEnd {
1157 currentSubsystem = ""
1158 }
1159 })
1160 }
1161
1162 // Job queues collection
1163 func (a *Collector) collectJobQueues(ctx context.Context) error {
1164 if len(a.jobQueueTargets) == 0 {
1165 return nil
1166 }
1167
1168 if a.slowPathActive() {
1169 snapshot := a.slow.cache.getJobQueues()
1170 for _, target := range a.jobQueueTargets {
1171 key := target.ID()
1172 metaPtr := a.getJobQueueMetrics(key)
1173 defaultMeta := jobQueueMetrics{
1174 library: target.Library,
1175 name: target.Name,
1176 status: "NOT_FOUND",
1177 }
1178 if snapshotMeta, ok := snapshot.meta[key]; ok {
1179 *metaPtr = snapshotMeta
1180 } else {
1181 *metaPtr = defaultMeta
1182 }
1183 a.jobQueues[key] = metaPtr
1184 a.mx.jobQueues[key] = snapshot.metrics[key]
1185 }
1186 return snapshot.err
1187 }
1188
1189 var firstErr error
1190
1191 for _, target := range a.jobQueueTargets {
1192 key := target.ID()
1193 queue := a.getJobQueueMetrics(key)
1194 queue.library = target.Library
1195 queue.name = target.Name
1196 queue.status = "UNKNOWN"
1197 a.jobQueues[key] = queue
1198
1199 metrics := jobQueueInstanceMetrics{}
1200 found := false
1201
1202 queryName := fmt.Sprintf("job_queue_%s_%s", target.Library, target.Name)
1203 err := a.doQuery(ctx, queryName, buildJobQueueQuery(target), func(column, value string, lineEnd bool) {
1204 switch column {
1205 case "JOB_QUEUE_STATUS":
1206 queue.status = strings.TrimSpace(value)
1207 case "NUMBER_OF_JOBS":
1208 metrics.NumberOfJobs = parseInt64OrZero(value)
1209 case "RELEASED_JOBS":
1210 queue.jobsWaiting = parseInt64OrZero(value)
1211 case "SCHEDULED_JOBS":
1212 queue.jobsScheduled = parseInt64OrZero(value)
1213 case "HELD_JOBS":
1214 queue.jobsHeld = parseInt64OrZero(value)
1215 }
1216
1217 if lineEnd {
1218 found = true
1219 }
1220 })
1221
1222 if err != nil {
1223 if firstErr == nil {
1224 firstErr = fmt.Errorf("job queue %s: %w", key, err)
1225 }
1226 continue
1227 }
1228
1229 if !found {
1230 queue.status = "NOT_FOUND"
1231 }
1232
1233 a.mx.jobQueues[key] = metrics
1234 }
1235
1236 return firstErr
1237 }
1238
1239 // Enhanced disk collection with all metrics
1240 func (a *Collector) collectDiskInstancesEnhanced(ctx context.Context) error {
1241 // First check cardinality if we haven't yet
1242 if len(a.disks) == 0 && a.MaxDisks > 0 {
1243 count, err := a.countDisks(ctx)
1244 if err != nil {
1245 return err
1246 }
1247 if count > a.MaxDisks {
1248 return fmt.Errorf("disk count (%d) exceeds limit (%d), skipping per-disk metrics", count, a.MaxDisks)
1249 }
1250 }
1251
1252 var currentUnit string
1253 return a.doQuery(ctx, "disk_instances_enhanced", queryDiskInstancesEnhanced, func(column, value string, lineEnd bool) {
1254 switch column {
1255 case "UNIT_NUMBER":
1256 currentUnit = value
1257
1258 // Apply selector if configured
1259 if a.diskSelector != nil && !a.diskSelector.MatchString(currentUnit) {
1260 currentUnit = "" // Skip this disk
1261 return
1262 }
1263
1264 _ = a.getDiskMetrics(currentUnit)
1265
1266 case "UNIT_TYPE":
1267 if currentUnit != "" && a.disks[currentUnit] != nil {
1268 // Map IBM i disk type values to meaningful labels
1269 switch value {
1270 case "0":
1271 a.disks[currentUnit].typeField = "HDD"
1272 case "1":
1273 a.disks[currentUnit].typeField = "SSD"
1274 case "":
1275 a.disks[currentUnit].typeField = "UNKNOWN"
1276 default:
1277 a.disks[currentUnit].typeField = value // Keep unknown values as-is
1278 }
1279 }
1280 case "PERCENT_USED":
1281 if currentUnit != "" && a.disks[currentUnit] != nil {
1282 if v, ok := a.parseInt64Value(value, precision); ok {
1283 if m, ok := a.mx.disks[currentUnit]; ok {
1284 m.PercentUsed = v
1285 a.mx.disks[currentUnit] = m
1286 } else {
1287 a.mx.disks[currentUnit] = diskInstanceMetrics{
1288 PercentUsed: v,
1289 }
1290 }
1291 }
1292 }
1293 case "UNIT_SPACE_AVAILABLE_GB":
1294 if currentUnit != "" && a.disks[currentUnit] != nil {
1295 if v, ok := a.parseInt64Value(value, precision); ok {
1296 if m, ok := a.mx.disks[currentUnit]; ok {
1297 m.AvailableGB = v
1298 a.mx.disks[currentUnit] = m
1299 }
1300 }
1301 }
1302 case "UNIT_STORAGE_CAPACITY":
1303 if currentUnit != "" && a.disks[currentUnit] != nil {
1304 if v, ok := a.parseInt64Value(value, precision); ok {
1305 if m, ok := a.mx.disks[currentUnit]; ok {
1306 m.CapacityGB = v
1307 a.mx.disks[currentUnit] = m
1308 }
1309 }
1310 }
1311 case "TOTAL_READ_REQUESTS":
1312 if currentUnit != "" && a.disks[currentUnit] != nil {
1313 if v, ok := a.parseInt64Value(value, 1); ok {
1314 if m, ok := a.mx.disks[currentUnit]; ok {
1315 m.ReadRequests = v
1316 a.mx.disks[currentUnit] = m
1317 } else {
1318 a.mx.disks[currentUnit] = diskInstanceMetrics{
1319 ReadRequests: v,
1320 }
1321 }
1322 }
1323 }
1324 case "TOTAL_WRITE_REQUESTS":
1325 if currentUnit != "" && a.disks[currentUnit] != nil {
1326 if v, ok := a.parseInt64Value(value, 1); ok {
1327 if m, ok := a.mx.disks[currentUnit]; ok {
1328 m.WriteRequests = v
1329 a.mx.disks[currentUnit] = m
1330 } else {
1331 a.mx.disks[currentUnit] = diskInstanceMetrics{
1332 WriteRequests: v,
1333 }
1334 }
1335 }
1336 }
1337 case "TOTAL_BLOCKS_READ":
1338 if currentUnit != "" && a.disks[currentUnit] != nil {
1339 if v, ok := a.parseInt64Value(value, 1); ok {
1340 if m, ok := a.mx.disks[currentUnit]; ok {
1341 m.BlocksRead = v
1342 a.mx.disks[currentUnit] = m
1343 } else {
1344 a.mx.disks[currentUnit] = diskInstanceMetrics{
1345 BlocksRead: v,
1346 }
1347 }
1348 }
1349 }
1350 case "TOTAL_BLOCKS_WRITTEN":
1351 if currentUnit != "" && a.disks[currentUnit] != nil {
1352 if v, ok := a.parseInt64Value(value, 1); ok {
1353 if m, ok := a.mx.disks[currentUnit]; ok {
1354 m.BlocksWritten = v
1355 a.mx.disks[currentUnit] = m
1356 } else {
1357 a.mx.disks[currentUnit] = diskInstanceMetrics{
1358 BlocksWritten: v,
1359 }
1360 }
1361 }
1362 }
1363 case "ELAPSED_PERCENT_BUSY":
1364 if currentUnit != "" && a.disks[currentUnit] != nil {
1365 if v, ok := a.parseInt64Value(value, precision); ok {
1366 if m, ok := a.mx.disks[currentUnit]; ok {
1367 m.BusyPercent = v
1368 a.mx.disks[currentUnit] = m
1369 }
1370 }
1371 }
1372 case "SSD_LIFE_REMAINING":
1373 if currentUnit != "" && a.disks[currentUnit] != nil {
1374 if v := parseInt64OrZero(value); v > 0 {
1375 disk := a.disks[currentUnit]
1376 disk.ssdLifeRemaining = v
1377 if m, ok := a.mx.disks[currentUnit]; ok {
1378 m.SSDLifeRemaining = v
1379 a.mx.disks[currentUnit] = m
1380 }
1381 }
1382 }
1383 case "SSD_POWER_ON_DAYS":
1384 if currentUnit != "" && a.disks[currentUnit] != nil {
1385 if v := parseInt64OrZero(value); v > 0 {
1386 disk := a.disks[currentUnit]
1387 disk.ssdPowerOnDays = v
1388 if m, ok := a.mx.disks[currentUnit]; ok {
1389 m.SSDPowerOnDays = v
1390 a.mx.disks[currentUnit] = m
1391 }
1392 }
1393 }
1394 case "HARDWARE_STATUS":
1395 if currentUnit != "" && a.disks[currentUnit] != nil {
1396 a.disks[currentUnit].hardwareStatus = value
1397 if m, ok := a.mx.disks[currentUnit]; ok {
1398 m.HardwareStatus = value
1399 a.mx.disks[currentUnit] = m
1400 }
1401 }
1402 case "DISK_MODEL":
1403 if currentUnit != "" && a.disks[currentUnit] != nil {
1404 a.disks[currentUnit].diskModel = value
1405 if m, ok := a.mx.disks[currentUnit]; ok {
1406 m.DiskModel = value
1407 a.mx.disks[currentUnit] = m
1408 }
1409 }
1410 case "SERIAL_NUMBER":
1411 if currentUnit != "" && a.disks[currentUnit] != nil {
1412 a.disks[currentUnit].serialNumber = value
1413 if m, ok := a.mx.disks[currentUnit]; ok {
1414 m.SerialNumber = value
1415 a.mx.disks[currentUnit] = m
1416 }
1417 }
1418 }
1419 })
1420 }
1421
1422 func (a *Collector) collectNetworkInterfaces(ctx context.Context) error {
1423 allowed, count, err := a.networkInterfacesCardinality.Allow(ctx, a.countNetworkInterfaces)
1424 if err != nil {
1425 return fmt.Errorf("failed to count network interfaces: %w", err)
1426 }
1427 if !allowed {
1428 a.logOnce("network_interfaces_cardinality", "too many network interfaces (%d), skipping collection to avoid performance issues", count)
1429 return nil
1430 }
1431
1432 var currentInterface string
1433 return a.doQuery(ctx, "network_interfaces", queryNetworkInterfaces, func(column, value string, lineEnd bool) {
1434 switch column {
1435 case "LINE_DESCRIPTION":
1436 iface := strings.TrimSpace(value)
1437 if iface == "" {
1438 currentInterface = ""
1439 return
1440 }
1441 currentInterface = iface
1442 intf := a.getNetworkInterfaceMetrics(currentInterface)
1443 intf.name = iface
1444
1445 case "INTERFACE_LINE_TYPE":
1446 if currentInterface == "" {
1447 return
1448 }
1449 intf := a.getNetworkInterfaceMetrics(currentInterface)
1450 intf.interfaceType = value
1451 clean := cleanName(currentInterface)
1452 entry := a.mx.networkInterfaces[clean]
1453 entry.InterfaceType = value
1454 a.mx.networkInterfaces[clean] = entry
1455
1456 case "INTERFACE_STATUS":
1457 if currentInterface == "" {
1458 return
1459 }
1460 intf := a.getNetworkInterfaceMetrics(currentInterface)
1461 intf.interfaceStatus = value
1462 clean := cleanName(currentInterface)
1463 entry := a.mx.networkInterfaces[clean]
1464 if strings.EqualFold(value, "ACTIVE") {
1465 entry.InterfaceStatus = 1
1466 } else {
1467 entry.InterfaceStatus = 0
1468 }
1469 a.mx.networkInterfaces[clean] = entry
1470
1471 case "CONNECTION_TYPE":
1472 if currentInterface == "" {
1473 return
1474 }
1475 intf := a.getNetworkInterfaceMetrics(currentInterface)
1476 intf.connectionType = value
1477 clean := cleanName(currentInterface)
1478 entry := a.mx.networkInterfaces[clean]
1479 entry.ConnectionType = value
1480 a.mx.networkInterfaces[clean] = entry
1481
1482 case "INTERNET_ADDRESS":
1483 if currentInterface == "" {
1484 return
1485 }
1486 intf := a.getNetworkInterfaceMetrics(currentInterface)
1487 intf.internetAddress = value
1488 clean := cleanName(currentInterface)
1489 entry := a.mx.networkInterfaces[clean]
1490 entry.InternetAddress = value
1491 a.mx.networkInterfaces[clean] = entry
1492
1493 case "NETWORK_ADDRESS":
1494 if currentInterface == "" {
1495 return
1496 }
1497 intf := a.getNetworkInterfaceMetrics(currentInterface)
1498 intf.networkAddress = value
1499 clean := cleanName(currentInterface)
1500 entry := a.mx.networkInterfaces[clean]
1501 entry.NetworkAddress = value
1502 a.mx.networkInterfaces[clean] = entry
1503
1504 case "MAXIMUM_TRANSMISSION_UNIT":
1505 if currentInterface == "" {
1506 return
1507 }
1508 intf := a.getNetworkInterfaceMetrics(currentInterface)
1509 if v, ok := a.parseInt64Value(value, 1); ok {
1510 intf.mtu = v
1511 clean := cleanName(currentInterface)
1512 entry := a.mx.networkInterfaces[clean]
1513 entry.MTU = v
1514 a.mx.networkInterfaces[clean] = entry
1515 }
1516 }
1517
1518 if lineEnd {
1519 currentInterface = ""
1520 }
1521 })
1522 }
1523
1524 func (a *Collector) collectHTTPServerInfo(ctx context.Context) error {
1525 if !a.CollectHTTPServerMetrics.IsEnabled() {
1526 return nil
1527 }
1528
1529 allowed, count, err := a.httpServersCardinality.Allow(ctx, a.countHTTPServers)
1530 if err != nil {
1531 return fmt.Errorf("failed to count HTTP server rows: %w", err)
1532 }
1533 if !allowed {
1534 a.logOnce("http_server_cardinality", "too many HTTP server entries (%d), skipping collection to avoid performance issues", count)
1535 return nil
1536 }
1537
1538 var (
1539 serverName string
1540 functionName string
1541 currentKey string
1542 )
1543
1544 return a.doQuery(ctx, "http_server_info", queryHTTPServerInfo, func(column, value string, lineEnd bool) {
1545 switch column {
1546 case "SERVER_NAME":
1547 serverName = strings.TrimSpace(value)
1548 case "HTTP_FUNCTION":
1549 functionName = strings.TrimSpace(value)
1550 if serverName == "" {
1551 serverName = "UNKNOWN"
1552 }
1553 if functionName == "" {
1554 functionName = "UNKNOWN"
1555 }
1556 currentKey = httpServerKey(serverName, functionName)
1557 if meta := a.getHTTPServerMetrics(serverName, functionName); meta != nil {
1558 meta.serverName = serverName
1559 meta.httpFunction = functionName
1560 }
1561 case "SERVER_NORMAL_CONNECTIONS":
1562 if currentKey == "" {
1563 return
1564 }
1565 if v, ok := a.parseInt64Value(value, 1); ok {
1566 entry := a.mx.httpServers[currentKey]
1567 entry.NormalConnections = v
1568 a.mx.httpServers[currentKey] = entry
1569 }
1570 case "SERVER_SSL_CONNECTIONS":
1571 if currentKey == "" {
1572 return
1573 }
1574 if v, ok := a.parseInt64Value(value, 1); ok {
1575 entry := a.mx.httpServers[currentKey]
1576 entry.SSLConnections = v
1577 a.mx.httpServers[currentKey] = entry
1578 }
1579 case "SERVER_ACTIVE_THREADS":
1580 if currentKey == "" {
1581 return
1582 }
1583 if v, ok := a.parseInt64Value(value, 1); ok {
1584 entry := a.mx.httpServers[currentKey]
1585 entry.ActiveThreads = v
1586 a.mx.httpServers[currentKey] = entry
1587 }
1588 case "SERVER_IDLE_THREADS":
1589 if currentKey == "" {
1590 return
1591 }
1592 if v, ok := a.parseInt64Value(value, 1); ok {
1593 entry := a.mx.httpServers[currentKey]
1594 entry.IdleThreads = v
1595 a.mx.httpServers[currentKey] = entry
1596 }
1597 case "SERVER_TOTAL_REQUESTS":
1598 if currentKey == "" {
1599 return
1600 }
1601 if v, ok := a.parseInt64Value(value, 1); ok {
1602 entry := a.mx.httpServers[currentKey]
1603 entry.TotalRequests = v
1604 a.mx.httpServers[currentKey] = entry
1605 }
1606 case "SERVER_TOTAL_RESPONSES":
1607 if currentKey == "" {
1608 return
1609 }
1610 if v, ok := a.parseInt64Value(value, 1); ok {
1611 entry := a.mx.httpServers[currentKey]
1612 entry.TotalResponses = v
1613 a.mx.httpServers[currentKey] = entry
1614 }
1615 case "SERVER_TOTAL_REQUESTS_REJECTED":
1616 if currentKey == "" {
1617 return
1618 }
1619 if v, ok := a.parseInt64Value(value, 1); ok {
1620 entry := a.mx.httpServers[currentKey]
1621 entry.TotalRequestsRejected = v
1622 a.mx.httpServers[currentKey] = entry
1623 }
1624 case "BYTES_RECEIVED":
1625 if currentKey == "" {
1626 return
1627 }
1628 if v, ok := a.parseInt64Value(value, 1); ok {
1629 entry := a.mx.httpServers[currentKey]
1630 entry.BytesReceived = v
1631 a.mx.httpServers[currentKey] = entry
1632 }
1633 case "BYTES_SENT":
1634 if currentKey == "" {
1635 return
1636 }
1637 if v, ok := a.parseInt64Value(value, 1); ok {
1638 entry := a.mx.httpServers[currentKey]
1639 entry.BytesSent = v
1640 a.mx.httpServers[currentKey] = entry
1641 }
1642 }
1643
1644 if lineEnd {
1645 serverName = ""
1646 functionName = ""
1647 currentKey = ""
1648 }
1649 })
1650 }
1651
1652 func (a *Collector) collectPlanCache(ctx context.Context) error {
1653 if !a.CollectPlanCacheMetrics.IsEnabled() {
1654 return nil
1655 }
1656
1657 if a.slowPathActive() {
1658 snapshot := a.slow.cache.getPlanCache()
1659 for key, meta := range snapshot.meta {
1660 if ptr := a.getPlanCacheMetrics(key, meta.heading); ptr != nil {
1661 a.planCache[key] = ptr
1662 }
1663 }
1664 maps.Copy(a.mx.planCache, snapshot.values)
1665 return snapshot.err
1666 }
1667
1668 start := time.Now()
1669 if err := a.client.Exec(ctx, callAnalyzePlanCache); err != nil {
1670 return fmt.Errorf("failed to analyze plan cache: %w", err)
1671 }
1672 elapsed := time.Since(start)
1673 a.Debugf("plan cache analysis completed in %v", elapsed)
1674 a.recordQueryLatency("analyze_plan_cache", elapsed)
1675
1676 var currentHeading string
1677 return a.doQuery(ctx, "plan_cache_summary", queryPlanCacheSummary, func(column, value string, lineEnd bool) {
1678 switch column {
1679 case "HEADING":
1680 currentHeading = strings.TrimSpace(value)
1681 case "VALUE":
1682 if currentHeading == "" {
1683 return
1684 }
1685 key := planCacheMetricKey(currentHeading)
1686 if key == "" {
1687 return
1688 }
1689 if parsed, ok := a.parseInt64Value(value, precision); ok {
1690 if meta := a.getPlanCacheMetrics(key, currentHeading); meta != nil {
1691 meta.heading = currentHeading
1692 }
1693 entry := a.mx.planCache[key]
1694 entry.Value = parsed
1695 a.mx.planCache[key] = entry
1696 }
1697 }
1698 if lineEnd {
1699 currentHeading = ""
1700 }
1701 })
1702 }
1703
1704 func (a *Collector) collectSystemActivity(ctx context.Context) error {
1705 // IBM deprecated AVERAGE_CPU_* columns in 7.4, so we use a hybrid approach:
1706 // 1. Try TOTAL_CPU_TIME (requires *JOBCTL authority) - monotonic counter, most accurate
1707 // 2. Fall back to ELAPSED_CPU_USED with reset detection if *JOBCTL unavailable
1708
1709 // Query both potential data sources in one query
1710 query := a.systemActivityQuery()
1711
1712 a.mx.EntitledCPUPercentage = 0
1713
1714 var (
1715 totalCPUTime int64 // Nanoseconds since IPL (NULL if no *JOBCTL)
1716 elapsedTime int64 // Seconds since last reset
1717 elapsedCPUUsed float64 // Average CPU% since last reset
1718 hasTotalCPUTime bool
1719 hasElapsedData bool
1720 )
1721
1722 err := a.doQuery(ctx, "system_activity", query, func(column, value string, lineEnd bool) {
1723 switch column {
1724 case "TOTAL_CPU_TIME":
1725 // This will be NULL if user doesn't have *JOBCTL authority
1726 if value != "" && !strings.EqualFold(value, "NULL") {
1727 if v, ok := a.parseInt64Value(value, 1); ok {
1728 totalCPUTime = v
1729 hasTotalCPUTime = true
1730 }
1731 }
1732 case "ELAPSED_TIME":
1733 if value != "" && !strings.EqualFold(value, "NULL") {
1734 if v, ok := a.parseInt64Value(value, 1); ok {
1735 elapsedTime = v
1736 hasElapsedData = true
1737 }
1738 }
1739 case "ELAPSED_CPU_USED":
1740 if value != "" && !strings.EqualFold(value, "NULL") {
1741 if v, ok := a.parseFloat64Value(value); ok {
1742 elapsedCPUUsed = v
1743 }
1744 }
1745 }
1746 })
1747
1748 if err != nil {
1749 return fmt.Errorf("failed to collect system activity: %w", err)
1750 }
1751
1752 // Determine which method to use
1753 if hasTotalCPUTime {
1754 // Primary method: Use TOTAL_CPU_TIME (requires *JOBCTL)
1755 if a.cpuCollectionMethod == "" {
1756 a.cpuCollectionMethod = "total_cpu_time"
1757 a.Debugf("CPU collection: using TOTAL_CPU_TIME method (*JOBCTL authority available)")
1758 }
1759
1760 if a.hasCPUBaseline {
1761 // Calculate CPU utilization from delta
1762 deltaNanos := totalCPUTime - a.prevTotalCPUTime
1763
1764 // Convert to per-core percentage based on update_every interval
1765 // TOTAL_CPU_TIME is cumulative CPU-seconds across all processors in nanoseconds
1766 // The delta/interval ratio directly gives us cores consumed (already in per-core scale)
1767 // Formula: (delta_nanoseconds / 1e9) / update_every_seconds * 100
1768 // Example: 2.0 CPU-seconds consumed in 1 second = 200% (2 cores fully utilized)
1769 if a.UpdateEvery > 0 {
1770 deltaSeconds := float64(deltaNanos) / 1e9
1771 intervalSeconds := float64(a.UpdateEvery)
1772 cpuUtilization := (deltaSeconds / intervalSeconds) * 100.0 * float64(precision)
1773 a.applyCPUUtilization("TOTAL_CPU_TIME", cpuUtilization)
1774 }
1775 } else {
1776 a.Debugf("CPU collection: establishing baseline for TOTAL_CPU_TIME method")
1777 }
1778
1779 // Save current values for next iteration
1780 a.prevTotalCPUTime = totalCPUTime
1781 a.hasCPUBaseline = true
1782
1783 } else if hasElapsedData {
1784 // Fallback method: Use ELAPSED_CPU_USED with reset detection
1785 if a.cpuCollectionMethod == "" {
1786 a.cpuCollectionMethod = "elapsed_cpu_used"
1787 a.Warningf("CPU collection: *JOBCTL authority not available, using ELAPSED_CPU_USED fallback method")
1788 a.Warningf("CPU collection: This method is affected by SYSTEM_STATUS(RESET_STATISTICS=>'YES') calls")
1789 }
1790
1791 // Calculate product for reset detection
1792 cpuProduct := int64(elapsedCPUUsed * float64(elapsedTime) * precision)
1793
1794 if a.hasCPUBaseline {
1795 // Detect if statistics were reset
1796 resetDetected := false
1797 if elapsedTime < a.prevElapsedTime {
1798 resetDetected = true
1799 a.Warningf("CPU collection: statistics reset detected (ELAPSED_TIME decreased from %d to %d)", a.prevElapsedTime, elapsedTime)
1800 } else if cpuProduct < a.prevElapsedCPUProduct {
1801 resetDetected = true
1802 a.Warningf("CPU collection: statistics reset detected (CPU product decreased from %d to %d)", a.prevElapsedCPUProduct, cpuProduct)
1803 }
1804
1805 if !resetDetected {
1806 // Calculate delta-based CPU utilization
1807 deltaProduct := cpuProduct - a.prevElapsedCPUProduct
1808 deltaTime := elapsedTime - a.prevElapsedTime
1809
1810 if deltaTime > 0 {
1811 // ELAPSED_CPU_USED is already in per-core scaling
1812 intervalCPU := float64(deltaProduct) / float64(deltaTime)
1813 a.applyCPUUtilization("ELAPSED_CPU_USED", intervalCPU)
1814 }
1815 } else {
1816 a.Debugf("CPU collection: re-establishing baseline after reset")
1817 a.hasCPUBaseline = false
1818 }
1819 } else {
1820 a.Debugf("CPU collection: establishing baseline for ELAPSED_CPU_USED method")
1821 }
1822
1823 // Save current values for next iteration
1824 a.prevElapsedTime = elapsedTime
1825 a.prevElapsedCPUProduct = cpuProduct
1826 a.hasCPUBaseline = true
1827
1828 } else {
1829 return fmt.Errorf("failed to collect CPU data: no usable CPU metrics available")
1830 }
1831
1832 return nil
1833 }