@cryptotaxi247 / netdata-1 / commits / 26f852c4b

Fix AS400 metrics (#21147)

* Fix AS400 CPU metrics scaling and docs * fixed bug in odbc union parsing that resulted in wrong columns selected from as400 results * added defensive checks in odbc C code to ensure there is no misuse * updated auto-generated files * documented cardinality management * removed debugging code * more fixes * more fixes * remove negative multipliers

Costa Tsaousis committed Oct 15, 2025 at 01:53 UTC 26f852c4b302372ecd508372c30f2e968dea42fb
16 files changed +876 -180
src/go/plugin/ibm.d/AGENTS.md
+78 -6
@@ -23,6 +23,75 @@ This guide is for developers contributing to the IBM.d plugin. For end-user docu
23 | `docgen/` | Tooling to generate docs/config metadata straight from module sources. |
24 | `metricgen/` | Experimental helper for generating boilerplate metric exports. |
25
26 +## Auto-Generated Files
27 +
28 +The IBM.D plugin uses code generation to keep contexts, documentation, and metadata in sync. Understanding which files are generated vs. editable is crucial for development.
29 +
30 +### Generated Files (DO NOT EDIT)
31 +
32 +Each module generates these files automatically:
33 +
34 +| File | Generator | Source | Purpose |
35 +|------|-----------|--------|---------|
36 +| `zz_generated_contexts.go` | `metricgen` | `contexts.yaml` | Type-safe Go structs for metric contexts |
37 +| `README.md` | `docgen` | `contexts.yaml` + `config.go` + `module.yaml` | Module documentation |
38 +| `metadata.yaml` | `docgen` | `contexts.yaml` + `config.go` + `module.yaml` | Netdata integrations metadata |
39 +
40 +**⚠️ Warning:** Direct edits to these files will be overwritten on the next `go generate` run.
41 +
42 +### Source Files (EDITABLE)
43 +
44 +| File | Purpose |
45 +|------|---------|
46 +| `contexts/contexts.yaml` | **Source of truth** for all metrics, charts, dimensions, families, priorities |
47 +| `config.go` | Collector configuration structure (exported to JSON schema by docgen) |
48 +| `module.yaml` | Module metadata (name, description, categories) |
49 +| All other `.go` files | Module implementation code |
50 +
51 +### Regenerating Code
52 +
53 +#### Regenerate a Single Module
54 +
55 +From the module directory:
56 +```bash
57 +cd modules/as400
58 +go generate ./...
59 +```
60 +
61 +This runs both generators:
62 +1. **metricgen** (via `contexts/doc.go`) → regenerates `zz_generated_contexts.go`
63 +2. **docgen** (via `generate.go`) → regenerates `README.md` and `metadata.yaml`
64 +
65 +#### Regenerate All Modules
66 +
67 +From the plugin root:
68 +```bash
69 +cd src/go/plugin/ibm.d
70 +go generate ./modules/...
71 +```
72 +
73 +#### After Regeneration
74 +
75 +Always run `gofmt` on generated Go code:
76 +```bash
77 +gofmt -w modules/*/contexts/zz_generated_contexts.go
78 +```
79 +
80 +### When to Regenerate
81 +
82 +Regenerate after modifying:
83 +- ✅ `contexts/contexts.yaml` (metrics definitions)
84 +- ✅ `config.go` (configuration structure)
85 +- ✅ `module.yaml` (module metadata)
86 +- ❌ Implementation `.go` files (no regeneration needed)
87 +
88 +### Verifying Generated Code
89 +
90 +After regeneration, verify the module works:
91 +```bash
92 +sudo script -c '/usr/libexec/netdata/plugins.d/ibm.d.plugin -d -m MODULE --dump=3s --dump-summary 2>&1' /dev/null
93 +```
94 +
95 ## Building the Plugin
96
97 The plugin is built automatically by Netdata's CMake tree when `ENABLE_PLUGIN_IBM=On` and the IBM CLI driver is available:
@@ -37,10 +106,11 @@ The build target downloads the driver if it is not already present; see the pack
106
107 ## Module Development Workflow
108
40 -1. Update `contexts/contexts.yaml` and `config.go`.
41 -2. Run `go generate` in the module directory (invokes docgen/metricgen).
42 -3. Sync metadata, config schema, README, and health alerts.
109 +1. Update `contexts/contexts.yaml` and `config.go` (see [Source Files](#source-files-editable)).
110 +2. Run `go generate ./...` in the module directory (see [Regenerating Code](#regenerating-code)).
111 +3. Run `gofmt -w contexts/zz_generated_contexts.go` to format generated code.
112 4. Validate with `script -c 'sudo /usr/libexec/netdata/plugins.d/ibm.d.plugin -d -m MODULE --dump=3s --dump-summary 2>&1' /dev/null`.
113 +5. Commit **both** source files and generated files together.
114
115 ## Testing & Debugging
116
@@ -61,9 +131,11 @@ The flag implicitly enables dump mode and exits once every job has produced at l
131
132 1. Review [`framework/README.md`](framework/README.md) for IBM.D framework details.
133 2. Follow [General collector best practices](../BEST-PRACTICES.md).
64 -3. Use `go generate` to refresh contexts, metadata, and schemas whenever `contexts.yaml` or config structs change.
65 -4. Keep documentation in `modules/<name>/README.md`, metadata.yaml, config schemas, and health alerts in sync – docgen simplifies this.
66 -5. Each module directory (`modules/<name>/`) contains its own README with module-specific notes.
134 +3. **Never edit auto-generated files** – see [Auto-Generated Files](#auto-generated-files) section.
135 +4. Always regenerate code after modifying `contexts.yaml`, `config.go`, or `module.yaml`.
136 +5. Run `gofmt` on generated Go files before committing.
137 +6. Commit **both** source and generated files together to keep them in sync.
138 +7. Each module directory (`modules/<name>/`) contains its own README with module-specific notes.
139
140 ## Runtime Internals
141
src/go/plugin/ibm.d/docgen/config_parser.go
+8 -4
@@ -62,7 +62,7 @@ func (g *DocGenerator) parseConfigFromGoFile() ([]ConfigField, map[string]interf
62 var missingDefaults []string
63 for i := range fields {
64 field := &fields[i]
65 - if field.GoType == "framework.AutoBool" {
65 + if isAutoBoolType(field.GoType) {
66 field.Type = "string"
67 field.Enum = toStringSlice(confopt.AutoBoolEnum)
68 }
@@ -70,7 +70,7 @@ func (g *DocGenerator) parseConfigFromGoFile() ([]ConfigField, map[string]interf
70 if defaultValue, exists := defaults[field.Name]; exists {
71 field.Default = normalizeDefaultValue(*field, defaultValue)
72 field.Required = false
73 - } else if field.GoType == "framework.AutoBool" {
73 + } else if isAutoBoolType(field.GoType) {
74 field.Default = confopt.AutoBoolAuto.String()
75 field.Required = false
76 } else if field.Pointer {
@@ -517,7 +517,7 @@ func toStringSlice(values []confopt.AutoBool) []string {
517 }
518
519 func normalizeDefaultValue(field ConfigField, value interface{}) interface{} {
520 - if field.GoType != "framework.AutoBool" {
520 + if !isAutoBoolType(field.GoType) {
521 return value
522 }
523 switch v := value.(type) {
@@ -711,7 +711,7 @@ func (g *DocGenerator) extractValue(expr ast.Expr) interface{} {
711 return duration
712 }
713 }
714 - if ident.Name == "framework" {
714 + if ident.Name == "framework" || ident.Name == "confopt" {
715 switch v.Sel.Name {
716 case "AutoBoolAuto":
717 return confopt.AutoBoolAuto.String()
@@ -916,3 +916,7 @@ func timeConstant(name string) (int64, bool) {
916 }
917 return 0, false
918 }
919 +
920 +func isAutoBoolType(goType string) bool {
921 + return goType == "framework.AutoBool" || goType == "confopt.AutoBool"
922 +}
src/go/plugin/ibm.d/modules/as400/README.md
+89 -1
@@ -13,6 +13,94 @@ expose CPU, memory, storage, job, and subsystem activity.
13 - libodbc.so (provided by unixODBC)
14 - IBM i Access Client Solutions
15
16 +**CPU Collection Methods:**
17 +
18 +The collector uses a hybrid approach for CPU utilization metrics to handle IBM i 7.4+ where
19 +`AVERAGE_CPU_*` columns were deprecated:
20 +
21 +1. **Primary Method - TOTAL_CPU_TIME**: Uses the monotonic `TOTAL_CPU_TIME` counter from
22 + `QSYS2.SYSTEM_STATUS()` to calculate CPU utilization via delta-based calculation. This is
23 + the most accurate method but requires `*JOBCTL` special authority. TOTAL_CPU_TIME is a
24 + cumulative counter in nanoseconds representing CPU-seconds consumed, naturally in per-core
25 + scale.
26 +
27 +2. **Fallback Method - ELAPSED_CPU_USED**: If `*JOBCTL` authority is not available, falls back
28 + to `ELAPSED_CPU_USED` with automatic reset detection. This method tracks when IBM i statistics
29 + are reset (either manually or via `reset_statistics` configuration) and re-establishes a
30 + baseline after detecting resets. The values are already in per-core scale.
31 +
32 +3. **Legacy Method - AVERAGE_CPU_UTILIZATION**: For IBM i versions before 7.4, uses the now-
33 + deprecated `AVERAGE_CPU_UTILIZATION` column, which IBM reports in the same per-core scale.
34 +
35 +The collector automatically selects the appropriate method based on available permissions and
36 +logs which method is being used.
37 +
38 +**CPU Metric Scale:**
39 +
40 +CPU utilization is reported using the "100% = 1 CPU core" semantic. This means:
41 +- 100% indicates one CPU core is fully utilized
42 +- 400% indicates four CPU cores are fully utilized
43 +- Values are limited to 100% × ConfiguredCPUs, matching the partition's configured capacity
44 +
45 +For shared LPARs, the metrics show absolute CPU consumption in per-core scale, not relative to
46 +entitled capacity. For example, a shared LPAR entitled to 0.20 cores can show 150% utilization
47 +when bursting above entitlement.
48 +
49 +**Statistics Reset Behavior:**
50 +
51 +The `reset_statistics` configuration option controls whether the collector resets IBM i system
52 +statistics on each query via `SYSTEM_STATUS(RESET_STATISTICS=>'YES')`. When enabled:
53 +
54 +- System-level statistics (CPU, memory pools, etc.) are reset after each collection cycle
55 +- Matches legacy behavior but clears global statistics that other tools may rely on
56 +- The ELAPSED_CPU_USED fallback method will detect and handle these resets automatically
57 +- **Caution**: Enabling this affects all users and applications on the IBM i system
58 +
59 +Default: `false` (statistics are not reset, using `RESET_STATISTICS=>'NO'`)
60 +
61 +**Cardinality Management:**
62 +
63 +To prevent performance issues from excessive metric creation, the collector enforces cardinality
64 +limits on per-instance metrics (disks, subsystems, job queues, message queues, output queues,
65 +active jobs, network interfaces, HTTP servers).
66 +
67 +**How Limits Work:**
68 +- The collector counts instances before collecting metrics
69 +- If count exceeds the configured `max_*` limit, **collection is skipped entirely** for that category
70 +- The collector logs a warning: `"[category] count (X) exceeds limit (Y), skipping collection"`
71 +- No metrics are collected for that category until you adjust the configuration
72 +
73 +**Configuration Options:**
74 +
75 +Use **both** limit and selector options together to manage high-cardinality environments:
76 +
77 +| Option | Purpose | Default |
78 +|--------|---------|---------|
79 +| `max_disks` | Maximum disk units to monitor | 100 |
80 +| `max_subsystems` | Maximum subsystems to monitor | 100 |
81 +| `max_job_queues` | Maximum job queues to monitor | 100 |
82 +| `max_message_queues` | Maximum message queues to monitor | 100 |
83 +| `max_output_queues` | Maximum output queues to monitor | 100 |
84 +| `max_active_jobs` | Maximum active jobs to monitor | 100 |
85 +| `collect_disks_matching` | Glob pattern to filter disks (e.g., `"001* 002*"`) | `""` (match all) |
86 +| `collect_subsystems_matching` | Glob pattern to filter subsystems (e.g., `"QINTER QBATCH"`) | `""` (match all) |
87 +| `collect_job_queues_matching` | Glob pattern to filter job queues (e.g., `"QSYS/*"`) | `""` (match all) |
88 +
89 +**Example Workflow:**
90 +
91 +1. System has 500 disks, collector skips disk metrics (exceeds default limit of 100)
92 +2. Check logs: `"disk count (500) exceeds limit (100), skipping per-disk metrics"`
93 +3. Two options:
94 + - **Option A**: Increase limit: `max_disks: 500` (collects all 500 disks)
95 + - **Option B**: Use selector: `collect_disks_matching: "00[1-5]*"` (cherry-pick specific disks)
96 +
97 +**Best Practices:**
98 +- Use selectors to monitor only business-critical objects in large environments
99 +- Set limits based on your Netdata server's capacity (each instance = multiple charts)
100 +- Start with defaults and adjust based on actual usage patterns
101 +
102 +Network interface metrics have a fixed internal limit of 50 instances, and HTTP server metrics are capped at 200 instances; these limits are currently not configurable.
103 +
104
105 This collector is part of the [Netdata](https://github.com/netdata/netdata) monitoring solution.
106
@@ -39,7 +127,7 @@ Metrics:
127 | as400.total_jobs | total | jobs |
128 | as400.active_jobs_by_type | batch, interactive, active | jobs |
129 | as400.job_queue_length | waiting | jobs |
42 -| as400.main_storage_size | total | KiB |
130 +| as400.main_storage_size | total | bytes |
131 | as400.temporary_storage | current, maximum | MiB |
132 | as400.memory_pool_usage | machine, base, interactive, spool | bytes |
133 | as400.memory_pool_defined | machine, base | bytes |
src/go/plugin/ibm.d/modules/as400/collect_data.go
+338 -125
@@ -16,10 +16,11 @@ import (
16
17 const precision = 1000 // Precision multiplier for floating-point values
18
19 -func parseNumericValue(value string, multiplier int64) (int64, bool) {
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") {
22 - return 0, false
23 + return ""
24 }
25 cleaned := strings.Map(func(r rune) rune {
26 switch {
@@ -27,35 +28,81 @@ func parseNumericValue(value string, multiplier int64) (int64, bool) {
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)
34 - if cleaned == "" || cleaned == "-" || cleaned == "." {
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 {
38 - // Too many decimal separators, treat as invalid
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 }
44 - if strings.Contains(cleaned, ".") {
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 }
55 - if multiplier == 1 {
56 - return v, true
77 + if multiplier != 1 {
78 + return v * multiplier, true
79 }
58 - return v * multiplier, true
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) parseFloat64Value(value string) (float64, bool) {
86 + cleaned := cleanNumericString(value)
87 + if cleaned == "" || cleaned == "-" || cleaned == "." || cleaned == "+" {
88 + a.Debugf("parseFloat64Value: empty/invalid value='%s', cleaned='%s'", value, cleaned)
89 + return 0, false
90 + }
91 + if strings.Count(cleaned, ".") > 1 {
92 + a.Debugf("parseFloat64Value: too many decimal points, value='%s', cleaned='%s'", value, cleaned)
93 + return 0, false
94 + }
95 + if strings.Count(cleaned, "e")+strings.Count(cleaned, "E") > 1 {
96 + a.Debugf("parseFloat64Value: too many exponents, value='%s', cleaned='%s'", value, cleaned)
97 + return 0, false
98 + }
99 +
100 + f, err := strconv.ParseFloat(cleaned, 64)
101 + if err != nil {
102 + a.Debugf("parseFloat64Value: ParseFloat failed, value='%s', cleaned='%s', error=%v", value, cleaned, err)
103 + return 0, false
104 + }
105 + return f, true
106 }
107
108 func planCacheMetricKey(heading string) string {
@@ -99,8 +146,29 @@ func normalizeValue(value string) string {
146 return trimmed
147 }
148
149 +// parseInt64OrZero parses value as int64, returns 0 on any error (no logging)
150 func parseInt64OrZero(value string) int64 {
103 - if v, err := strconv.ParseInt(strings.TrimSpace(value), 10, 64); err == nil {
151 + cleaned := cleanNumericString(value)
152 + if cleaned == "" || cleaned == "-" || cleaned == "." || cleaned == "+" {
153 + return 0
154 + }
155 + if strings.Count(cleaned, ".") > 1 {
156 + return 0
157 + }
158 + if strings.Count(cleaned, "e")+strings.Count(cleaned, "E") > 1 {
159 + return 0
160 + }
161 +
162 + // Handle floats/exponentials
163 + if strings.Contains(cleaned, ".") || strings.ContainsAny(cleaned, "eE") {
164 + if f, err := strconv.ParseFloat(cleaned, 64); err == nil {
165 + return int64(math.Round(f))
166 + }
167 + return 0
168 + }
169 +
170 + // Handle integers
171 + if v, err := strconv.ParseInt(cleaned, 10, 64); err == nil {
172 return v
173 }
174 return 0
@@ -138,6 +206,11 @@ func (a *Collector) collect(ctx context.Context) error {
206 func (a *Collector) collectSystemStatus(ctx context.Context) error {
207 // Use comprehensive query to get all system status metrics at once
208 err := a.doQuery(ctx, a.systemStatusQuery(), func(column, value string, lineEnd bool) {
209 + // Debug log all columns to see what we're receiving
210 + if strings.Contains(column, "STORAGE") || strings.Contains(column, "MEMORY") {
211 + a.Debugf("collectSystemStatus: column='%s', value='%s'", column, value)
212 + }
213 +
214 // Skip empty values
215 if value == "" {
216 return
@@ -146,71 +219,75 @@ func (a *Collector) collectSystemStatus(ctx context.Context) error {
219 switch column {
220 // CPU metrics
221 case "AVERAGE_CPU_UTILIZATION":
149 - if v, err := strconv.ParseFloat(value, 64); err == nil {
150 - a.mx.CPUPercentage = int64(v * precision)
222 + // AVERAGE_CPU_UTILIZATION is system-wide 0-100% (deprecated in IBM i 7.4+)
223 + if v, ok := a.parseInt64Value(value, precision); ok {
224 + a.mx.CPUPercentage = v
225 }
226 case "CURRENT_CPU_CAPACITY":
153 - if v, err := strconv.ParseFloat(value, 64); err == nil {
154 - a.mx.CurrentCPUCapacity = int64(v * precision)
227 + // CURRENT_CPU_CAPACITY comes from IBM as decimal fraction (0.0-1.0)
228 + // Convert to percentage by multiplying by 100
229 + if v, ok := a.parseInt64Value(value, precision); ok {
230 + // Convert decimal fraction (e.g., 0.20) to percentage scale
231 + a.mx.CurrentCPUCapacity = v * 100
232 }
233 case "CONFIGURED_CPUS":
157 - if v, err := strconv.ParseInt(value, 10, 64); err == nil {
234 + if v, ok := a.parseInt64Value(value, 1); ok {
235 a.mx.ConfiguredCPUs = v
236 }
237
238 // Memory metrics
239 case "MAIN_STORAGE_SIZE":
163 - if v, err := strconv.ParseInt(value, 10, 64); err == nil {
164 - a.mx.MainStorageSize = v // KB
240 + if v, ok := a.parseInt64Value(value, 1024); ok { // Convert KB to bytes
241 + a.mx.MainStorageSize = v
242 }
243 case "CURRENT_TEMPORARY_STORAGE":
167 - if v, err := strconv.ParseInt(value, 10, 64); err == nil {
244 + if v, ok := a.parseInt64Value(value, 1); ok {
245 a.mx.CurrentTemporaryStorage = v // MB
246 }
247 case "MAXIMUM_TEMPORARY_STORAGE_USED":
171 - if v, err := strconv.ParseInt(value, 10, 64); err == nil {
248 + if v, ok := a.parseInt64Value(value, 1); ok {
249 a.mx.MaximumTemporaryStorageUsed = v // MB
250 }
251
252 // Job metrics
253 case "TOTAL_JOBS_IN_SYSTEM":
177 - if v, err := strconv.ParseInt(value, 10, 64); err == nil {
254 + if v, ok := a.parseInt64Value(value, 1); ok {
255 a.mx.TotalJobsInSystem = v
256 }
257 case "ACTIVE_JOBS_IN_SYSTEM":
181 - if v, err := strconv.ParseInt(value, 10, 64); err == nil {
258 + if v, ok := a.parseInt64Value(value, 1); ok {
259 a.mx.ActiveJobsInSystem = v
260 }
261 case "INTERACTIVE_JOBS_IN_SYSTEM":
185 - if v, err := strconv.ParseInt(value, 10, 64); err == nil {
262 + if v, ok := a.parseInt64Value(value, 1); ok {
263 a.mx.InteractiveJobsInSystem = v
264 }
265 case "BATCH_RUNNING":
189 - if v, err := strconv.ParseInt(value, 10, 64); err == nil {
266 + if v, ok := a.parseInt64Value(value, 1); ok {
267 a.mx.BatchJobsRunning = v
268 }
269
270 // Storage metrics
271 case "SYSTEM_ASP_USED":
195 - if v, err := strconv.ParseFloat(value, 64); err == nil {
196 - a.mx.SystemASPUsed = int64(v * precision)
272 + if v, ok := a.parseInt64Value(value, precision); ok {
273 + a.mx.SystemASPUsed = v
274 }
275 case "SYSTEM_ASP_STORAGE":
199 - if v, err := strconv.ParseInt(value, 10, 64); err == nil {
276 + if v, ok := a.parseInt64Value(value, 1); ok {
277 a.mx.SystemASPStorage = v // MB
278 }
279 case "TOTAL_AUXILIARY_STORAGE":
203 - if v, err := strconv.ParseInt(value, 10, 64); err == nil {
280 + if v, ok := a.parseInt64Value(value, 1); ok {
281 a.mx.TotalAuxiliaryStorage = v // MB
282 }
283
284 // Thread metrics
285 case "ACTIVE_THREADS_IN_SYSTEM":
209 - if v, err := strconv.ParseInt(value, 10, 64); err == nil {
286 + if v, ok := a.parseInt64Value(value, 1); ok {
287 a.mx.ActiveThreadsInSystem = v
288 }
289 case "THREADS_PER_PROCESSOR":
213 - if v, err := strconv.ParseInt(value, 10, 64); err == nil {
290 + if v, ok := a.parseInt64Value(value, 1); ok {
291 a.mx.ThreadsPerProcessor = v
292 }
293 }
@@ -228,9 +305,9 @@ func (a *Collector) collectMemoryPools(ctx context.Context) error {
305 return a.doQuery(ctx, a.memoryPoolQuery(), func(column, value string, lineEnd bool) {
306 switch column {
307 case "POOL_NAME":
231 - currentPoolName = value
308 + currentPoolName = strings.TrimSpace(value)
309 case "CURRENT_SIZE":
233 - if v, err := strconv.ParseInt(value, 10, 64); err == nil {
310 + if v, ok := a.parseInt64Value(value, 1024*1024); ok { // Convert MB to bytes
311 switch currentPoolName {
312 case "*MACHINE":
313 a.mx.MachinePoolSize = v
@@ -243,7 +320,7 @@ func (a *Collector) collectMemoryPools(ctx context.Context) error {
320 }
321 }
322 case "DEFINED_SIZE":
246 - if v, err := strconv.ParseInt(value, 10, 64); err == nil {
323 + if v, ok := a.parseInt64Value(value, 1024*1024); ok { // Convert MB to bytes
324 switch currentPoolName {
325 case "*MACHINE":
326 a.mx.MachinePoolDefinedSize = v
@@ -252,7 +329,7 @@ func (a *Collector) collectMemoryPools(ctx context.Context) error {
329 }
330 }
331 case "RESERVED_SIZE":
255 - if v, err := strconv.ParseInt(value, 10, 64); err == nil {
332 + if v, ok := a.parseInt64Value(value, 1024*1024); ok { // Convert MB to bytes
333 switch currentPoolName {
334 case "*MACHINE":
335 a.mx.MachinePoolReservedSize = v
@@ -261,7 +338,7 @@ func (a *Collector) collectMemoryPools(ctx context.Context) error {
338 }
339 }
340 case "CURRENT_THREADS":
264 - if v, err := strconv.ParseInt(value, 10, 64); err == nil {
341 + if v, ok := a.parseInt64Value(value, 1); ok {
342 switch currentPoolName {
343 case "*MACHINE":
344 a.mx.MachinePoolThreads = v
@@ -270,7 +347,7 @@ func (a *Collector) collectMemoryPools(ctx context.Context) error {
347 }
348 }
349 case "MAXIMUM_ACTIVE_THREADS":
273 - if v, err := strconv.ParseInt(value, 10, 64); err == nil {
350 + if v, ok := a.parseInt64Value(value, 1); ok {
351 switch currentPoolName {
352 case "*MACHINE":
353 a.mx.MachinePoolMaxThreads = v
@@ -286,8 +363,8 @@ func (a *Collector) collectDiskStatus(ctx context.Context) error {
363 // Try modern query first
364 err := a.doQuery(ctx, queryDiskStatus, func(column, value string, lineEnd bool) {
365 if column == "AVG_DISK_BUSY" {
289 - if v, err := strconv.ParseFloat(value, 64); err == nil {
290 - a.mx.DiskBusyPercentage = int64(v * precision)
366 + if v, ok := a.parseInt64Value(value, precision); ok {
367 + a.mx.DiskBusyPercentage = v
368 }
369 }
370 })
@@ -299,7 +376,7 @@ func (a *Collector) collectJobInfo(ctx context.Context) error {
376 // Try modern query first
377 err := a.doQuery(ctx, queryJobInfo, func(column, value string, lineEnd bool) {
378 if column == "JOB_QUEUE_LENGTH" {
302 - if v, err := strconv.ParseInt(value, 10, 64); err == nil {
379 + if v, ok := a.parseInt64Value(value, 1); ok {
380 a.mx.JobQueueLength = v
381 }
382 }
@@ -567,8 +644,8 @@ func (a *Collector) collectDiskInstances(ctx context.Context) error {
644 case "PERCENT_BUSY":
645 if currentUnit != "" && a.disks[currentUnit] != nil {
646 disk := a.disks[currentUnit]
570 - if v, err := strconv.ParseFloat(value, 64); err == nil {
571 - disk.busyPercent = int64(v * precision)
647 + if v, ok := a.parseInt64Value(value, precision); ok {
648 + disk.busyPercent = v
649 if m, ok := a.mx.disks[currentUnit]; ok {
650 m.BusyPercent = disk.busyPercent
651 a.mx.disks[currentUnit] = m
@@ -582,67 +659,75 @@ func (a *Collector) collectDiskInstances(ctx context.Context) error {
659 case "READ_REQUESTS":
660 if currentUnit != "" && a.disks[currentUnit] != nil {
661 disk := a.disks[currentUnit]
585 - if v, err := strconv.ParseInt(value, 10, 64); err == nil {
662 + if v, ok := a.parseInt64Value(value, 1); ok {
663 disk.readRequests = v
664 if m, ok := a.mx.disks[currentUnit]; ok {
665 m.ReadRequests = v
666 a.mx.disks[currentUnit] = m
667 + } else {
668 + a.mx.disks[currentUnit] = diskInstanceMetrics{
669 + ReadRequests: v,
670 + }
671 }
672 }
673 }
674 case "WRITE_REQUESTS":
675 if currentUnit != "" && a.disks[currentUnit] != nil {
676 disk := a.disks[currentUnit]
596 - if v, err := strconv.ParseInt(value, 10, 64); err == nil {
677 + if v, ok := a.parseInt64Value(value, 1); ok {
678 disk.writeRequests = v
679 if m, ok := a.mx.disks[currentUnit]; ok {
680 m.WriteRequests = v
681 a.mx.disks[currentUnit] = m
682 + } else {
683 + a.mx.disks[currentUnit] = diskInstanceMetrics{
684 + WriteRequests: v,
685 + }
686 }
687 }
688 }
689 case "PERCENT_USED":
690 if currentUnit != "" && a.disks[currentUnit] != nil {
606 - if v, err := strconv.ParseFloat(value, 64); err == nil {
691 + if v, ok := a.parseInt64Value(value, precision); ok {
692 if m, ok := a.mx.disks[currentUnit]; ok {
608 - m.PercentUsed = int64(v * precision)
693 + m.PercentUsed = v
694 a.mx.disks[currentUnit] = m
695 } else {
696 a.mx.disks[currentUnit] = diskInstanceMetrics{
612 - PercentUsed: int64(v * precision),
697 + PercentUsed: v,
698 }
699 }
700 }
701 }
702 case "UNIT_SPACE_AVAILABLE_GB":
703 if currentUnit != "" && a.disks[currentUnit] != nil {
619 - if v, err := strconv.ParseFloat(value, 64); err == nil {
704 + if v, ok := a.parseInt64Value(value, precision); ok {
705 if m, ok := a.mx.disks[currentUnit]; ok {
621 - m.AvailableGB = int64(v * precision)
706 + m.AvailableGB = v
707 a.mx.disks[currentUnit] = m
708 } else {
709 a.mx.disks[currentUnit] = diskInstanceMetrics{
625 - AvailableGB: int64(v * precision),
710 + AvailableGB: v,
711 }
712 }
713 }
714 }
715 case "UNIT_STORAGE_CAPACITY":
716 if currentUnit != "" && a.disks[currentUnit] != nil {
632 - if v, err := strconv.ParseFloat(value, 64); err == nil {
717 + if v, ok := a.parseInt64Value(value, precision); ok {
718 if m, ok := a.mx.disks[currentUnit]; ok {
634 - m.CapacityGB = int64(v * precision)
719 + m.CapacityGB = v
720 a.mx.disks[currentUnit] = m
721 } else {
722 a.mx.disks[currentUnit] = diskInstanceMetrics{
638 - CapacityGB: int64(v * precision),
723 + CapacityGB: v,
724 }
725 }
726 }
727 }
728 case "TOTAL_BLOCKS_READ":
729 if currentUnit != "" && a.disks[currentUnit] != nil {
645 - if v, err := strconv.ParseInt(value, 10, 64); err == nil {
730 + if v, ok := a.parseInt64Value(value, 1); ok {
731 if m, ok := a.mx.disks[currentUnit]; ok {
732 m.BlocksRead = v
733 a.mx.disks[currentUnit] = m
@@ -655,7 +740,7 @@ func (a *Collector) collectDiskInstances(ctx context.Context) error {
740 }
741 case "TOTAL_BLOCKS_WRITTEN":
742 if currentUnit != "" && a.disks[currentUnit] != nil {
658 - if v, err := strconv.ParseInt(value, 10, 64); err == nil {
743 + if v, ok := a.parseInt64Value(value, 1); ok {
744 if m, ok := a.mx.disks[currentUnit]; ok {
745 m.BlocksWritten = v
746 a.mx.disks[currentUnit] = m
@@ -668,7 +753,7 @@ func (a *Collector) collectDiskInstances(ctx context.Context) error {
753 }
754 case "SSD_LIFE_REMAINING":
755 if currentUnit != "" && a.disks[currentUnit] != nil {
671 - if v, err := strconv.ParseInt(value, 10, 64); err == nil && v > 0 {
756 + if v := parseInt64OrZero(value); v > 0 {
757 disk := a.disks[currentUnit]
758 disk.ssdLifeRemaining = v
759 if m, ok := a.mx.disks[currentUnit]; ok {
@@ -683,7 +768,7 @@ func (a *Collector) collectDiskInstances(ctx context.Context) error {
768 }
769 case "SSD_POWER_ON_DAYS":
770 if currentUnit != "" && a.disks[currentUnit] != nil {
686 - if v, err := strconv.ParseInt(value, 10, 64); err == nil && v > 0 {
771 + if v := parseInt64OrZero(value); v > 0 {
772 disk := a.disks[currentUnit]
773 disk.ssdPowerOnDays = v
774 if m, ok := a.mx.disks[currentUnit]; ok {
@@ -760,9 +845,7 @@ func (a *Collector) countDisks(ctx context.Context) (int, error) {
845 var count int
846 err := a.doQuery(ctx, queryCountDisks, func(column, value string, lineEnd bool) {
847 if column == "COUNT" {
763 - if v, err := strconv.Atoi(value); err == nil {
764 - count = v
765 - }
848 + count = int(parseInt64OrZero(value))
849 }
850 })
851 return count, err
@@ -773,19 +856,19 @@ func (a *Collector) collectNetworkConnections(ctx context.Context) error {
856 return a.doQuery(ctx, queryNetworkConnections, func(column, value string, lineEnd bool) {
857 switch column {
858 case "REMOTE_CONNECTIONS":
776 - if v, err := strconv.ParseInt(value, 10, 64); err == nil {
859 + if v, ok := a.parseInt64Value(value, 1); ok {
860 a.mx.RemoteConnections = v
861 }
862 case "TOTAL_CONNECTIONS":
780 - if v, err := strconv.ParseInt(value, 10, 64); err == nil {
863 + if v, ok := a.parseInt64Value(value, 1); ok {
864 a.mx.TotalConnections = v
865 }
866 case "LISTEN_CONNECTIONS":
784 - if v, err := strconv.ParseInt(value, 10, 64); err == nil {
867 + if v, ok := a.parseInt64Value(value, 1); ok {
868 a.mx.ListenConnections = v
869 }
870 case "CLOSEWAIT_CONNECTIONS":
788 - if v, err := strconv.ParseInt(value, 10, 64); err == nil {
871 + if v, ok := a.parseInt64Value(value, 1); ok {
872 a.mx.CloseWaitConnections = v
873 }
874 }
@@ -796,7 +879,7 @@ func (a *Collector) countNetworkInterfaces(ctx context.Context) (int, error) {
879 var count int
880 err := a.doQueryRow(ctx, queryCountNetworkInterfaces, func(column, value string) {
881 if column == "COUNT" {
799 - if v, err := strconv.ParseInt(value, 10, 64); err == nil {
882 + if v, ok := a.parseInt64Value(value, 1); ok {
883 count = int(v)
884 }
885 }
@@ -808,7 +891,7 @@ func (a *Collector) countMessageQueues(ctx context.Context) (int, error) {
891 var count int
892 err := a.doQueryRow(ctx, queryCountMessageQueues, func(column, value string) {
893 if column == "COUNT" {
811 - if v, err := strconv.ParseInt(value, 10, 64); err == nil {
894 + if v, ok := a.parseInt64Value(value, 1); ok {
895 count = int(v)
896 }
897 }
@@ -820,7 +903,7 @@ func (a *Collector) countOutputQueues(ctx context.Context) (int, error) {
903 var count int
904 err := a.doQueryRow(ctx, queryCountOutputQueues, func(column, value string) {
905 if column == "COUNT" {
823 - if v, err := strconv.ParseInt(value, 10, 64); err == nil {
906 + if v, ok := a.parseInt64Value(value, 1); ok {
907 count = int(v)
908 }
909 }
@@ -832,7 +915,7 @@ func (a *Collector) countHTTPServers(ctx context.Context) (int, error) {
915 var count int64
916 err := a.doQueryRow(ctx, queryCountHTTPServers, func(column, value string) {
917 if column == "COUNT" {
835 - if v, err := strconv.ParseInt(value, 10, 64); err == nil {
918 + if v, ok := a.parseInt64Value(value, 1); ok {
919 count = v
920 }
921 }
@@ -856,7 +939,7 @@ func (a *Collector) countSubsystems(ctx context.Context) (int, error) {
939 var count int64
940 err := a.doQueryRow(ctx, queryCountSubsystems, func(column, value string) {
941 if column == "COUNT" {
859 - if v, err := strconv.ParseInt(value, 10, 64); err == nil {
942 + if v, ok := a.parseInt64Value(value, 1); ok {
943 count = v
944 }
945 }
@@ -868,7 +951,7 @@ func (a *Collector) countJobQueues(ctx context.Context) (int, error) {
951 var count int64
952 err := a.doQueryRow(ctx, queryCountJobQueues, func(column, value string) {
953 if column == "COUNT" {
871 - if v, err := strconv.ParseInt(value, 10, 64); err == nil {
954 + if v, ok := a.parseInt64Value(value, 1); ok {
955 count = v
956 }
957 }
@@ -882,11 +965,11 @@ func (a *Collector) collectTempStorage(ctx context.Context) error {
965 err := a.doQuery(ctx, queryTempStorageTotal, func(column, value string, lineEnd bool) {
966 switch column {
967 case "CURRENT_SIZE":
885 - if v, err := strconv.ParseInt(value, 10, 64); err == nil {
968 + if v, ok := a.parseInt64Value(value, 1); ok {
969 a.mx.TempStorageCurrentTotal = v
970 }
971 case "PEAK_SIZE":
889 - if v, err := strconv.ParseInt(value, 10, 64); err == nil {
972 + if v, ok := a.parseInt64Value(value, 1); ok {
973 a.mx.TempStoragePeakTotal = v
974 }
975 }
@@ -905,7 +988,7 @@ func (a *Collector) collectTempStorage(ctx context.Context) error {
988
989 case "CURRENT_SIZE":
990 if currentBucket != "" && a.tempStorageNamed[currentBucket] != nil {
908 - if v, err := strconv.ParseInt(value, 10, 64); err == nil {
991 + if v, ok := a.parseInt64Value(value, 1); ok {
992 if m, ok := a.mx.tempStorageNamed[currentBucket]; ok {
993 m.CurrentSize = v
994 a.mx.tempStorageNamed[currentBucket] = m
@@ -918,7 +1001,7 @@ func (a *Collector) collectTempStorage(ctx context.Context) error {
1001 }
1002 case "PEAK_SIZE":
1003 if currentBucket != "" && a.tempStorageNamed[currentBucket] != nil {
921 - if v, err := strconv.ParseInt(value, 10, 64); err == nil {
1004 + if v, ok := a.parseInt64Value(value, 1); ok {
1005 if m, ok := a.mx.tempStorageNamed[currentBucket]; ok {
1006 m.PeakSize = v
1007 a.mx.tempStorageNamed[currentBucket] = m
@@ -968,7 +1051,7 @@ func (a *Collector) collectSubsystems(ctx context.Context) error {
1051
1052 case "CURRENT_ACTIVE_JOBS":
1053 if currentSubsystem != "" && a.subsystems[currentSubsystem] != nil {
971 - if v, err := strconv.ParseInt(value, 10, 64); err == nil {
1054 + if v, ok := a.parseInt64Value(value, 1); ok {
1055 if m, ok := a.mx.subsystems[currentSubsystem]; ok {
1056 m.CurrentActiveJobs = v
1057 a.mx.subsystems[currentSubsystem] = m
@@ -981,7 +1064,7 @@ func (a *Collector) collectSubsystems(ctx context.Context) error {
1064 }
1065 case "MAXIMUM_ACTIVE_JOBS":
1066 if currentSubsystem != "" && a.subsystems[currentSubsystem] != nil {
984 - if v, err := strconv.ParseInt(value, 10, 64); err == nil {
1067 + if v, ok := a.parseInt64Value(value, 1); ok {
1068 if m, ok := a.mx.subsystems[currentSubsystem]; ok {
1069 m.MaximumActiveJobs = v
1070 a.mx.subsystems[currentSubsystem] = m
@@ -1036,7 +1119,7 @@ func (a *Collector) collectJobQueues(ctx context.Context) error {
1119
1120 case "NUMBER_OF_JOBS":
1121 if currentQueue != "" && a.jobQueues[currentQueue] != nil {
1039 - if v, err := strconv.ParseInt(value, 10, 64); err == nil {
1122 + if v, ok := a.parseInt64Value(value, 1); ok {
1123 if m, ok := a.mx.jobQueues[currentQueue]; ok {
1124 m.NumberOfJobs = v
1125 a.mx.jobQueues[currentQueue] = m
@@ -1099,83 +1182,99 @@ func (a *Collector) collectDiskInstancesEnhanced(ctx context.Context) error {
1182 }
1183 case "PERCENT_USED":
1184 if currentUnit != "" && a.disks[currentUnit] != nil {
1102 - if v, err := strconv.ParseFloat(value, 64); err == nil {
1185 + if v, ok := a.parseInt64Value(value, precision); ok {
1186 if m, ok := a.mx.disks[currentUnit]; ok {
1104 - m.PercentUsed = int64(v * precision)
1187 + m.PercentUsed = v
1188 a.mx.disks[currentUnit] = m
1189 } else {
1190 a.mx.disks[currentUnit] = diskInstanceMetrics{
1108 - PercentUsed: int64(v * precision),
1191 + PercentUsed: v,
1192 }
1193 }
1194 }
1195 }
1196 case "UNIT_SPACE_AVAILABLE_GB":
1197 if currentUnit != "" && a.disks[currentUnit] != nil {
1115 - if v, err := strconv.ParseFloat(value, 64); err == nil {
1198 + if v, ok := a.parseInt64Value(value, precision); ok {
1199 if m, ok := a.mx.disks[currentUnit]; ok {
1117 - m.AvailableGB = int64(v * precision)
1200 + m.AvailableGB = v
1201 a.mx.disks[currentUnit] = m
1202 }
1203 }
1204 }
1205 case "UNIT_STORAGE_CAPACITY":
1206 if currentUnit != "" && a.disks[currentUnit] != nil {
1124 - if v, err := strconv.ParseFloat(value, 64); err == nil {
1207 + if v, ok := a.parseInt64Value(value, precision); ok {
1208 if m, ok := a.mx.disks[currentUnit]; ok {
1126 - m.CapacityGB = int64(v * precision)
1209 + m.CapacityGB = v
1210 a.mx.disks[currentUnit] = m
1211 }
1212 }
1213 }
1214 case "TOTAL_READ_REQUESTS":
1215 if currentUnit != "" && a.disks[currentUnit] != nil {
1133 - if v, err := strconv.ParseInt(value, 10, 64); err == nil {
1216 + if v, ok := a.parseInt64Value(value, 1); ok {
1217 if m, ok := a.mx.disks[currentUnit]; ok {
1218 m.ReadRequests = v
1219 a.mx.disks[currentUnit] = m
1220 + } else {
1221 + a.mx.disks[currentUnit] = diskInstanceMetrics{
1222 + ReadRequests: v,
1223 + }
1224 }
1225 }
1226 }
1227 case "TOTAL_WRITE_REQUESTS":
1228 if currentUnit != "" && a.disks[currentUnit] != nil {
1142 - if v, err := strconv.ParseInt(value, 10, 64); err == nil {
1229 + if v, ok := a.parseInt64Value(value, 1); ok {
1230 if m, ok := a.mx.disks[currentUnit]; ok {
1231 m.WriteRequests = v
1232 a.mx.disks[currentUnit] = m
1233 + } else {
1234 + a.mx.disks[currentUnit] = diskInstanceMetrics{
1235 + WriteRequests: v,
1236 + }
1237 }
1238 }
1239 }
1240 case "TOTAL_BLOCKS_READ":
1241 if currentUnit != "" && a.disks[currentUnit] != nil {
1151 - if v, err := strconv.ParseInt(value, 10, 64); err == nil {
1242 + if v, ok := a.parseInt64Value(value, 1); ok {
1243 if m, ok := a.mx.disks[currentUnit]; ok {
1244 m.BlocksRead = v
1245 a.mx.disks[currentUnit] = m
1246 + } else {
1247 + a.mx.disks[currentUnit] = diskInstanceMetrics{
1248 + BlocksRead: v,
1249 + }
1250 }
1251 }
1252 }
1253 case "TOTAL_BLOCKS_WRITTEN":
1254 if currentUnit != "" && a.disks[currentUnit] != nil {
1160 - if v, err := strconv.ParseInt(value, 10, 64); err == nil {
1255 + if v, ok := a.parseInt64Value(value, 1); ok {
1256 if m, ok := a.mx.disks[currentUnit]; ok {
1257 m.BlocksWritten = v
1258 a.mx.disks[currentUnit] = m
1259 + } else {
1260 + a.mx.disks[currentUnit] = diskInstanceMetrics{
1261 + BlocksWritten: v,
1262 + }
1263 }
1264 }
1265 }
1266 case "ELAPSED_PERCENT_BUSY":
1267 if currentUnit != "" && a.disks[currentUnit] != nil {
1169 - if v, err := strconv.ParseFloat(value, 64); err == nil {
1268 + if v, ok := a.parseInt64Value(value, precision); ok {
1269 if m, ok := a.mx.disks[currentUnit]; ok {
1171 - m.BusyPercent = int64(v * precision)
1270 + m.BusyPercent = v
1271 a.mx.disks[currentUnit] = m
1272 }
1273 }
1274 }
1275 case "SSD_LIFE_REMAINING":
1276 if currentUnit != "" && a.disks[currentUnit] != nil {
1178 - if v, err := strconv.ParseInt(value, 10, 64); err == nil && v > 0 {
1277 + if v := parseInt64OrZero(value); v > 0 {
1278 disk := a.disks[currentUnit]
1279 disk.ssdLifeRemaining = v
1280 if m, ok := a.mx.disks[currentUnit]; ok {
@@ -1186,7 +1285,7 @@ func (a *Collector) collectDiskInstancesEnhanced(ctx context.Context) error {
1285 }
1286 case "SSD_POWER_ON_DAYS":
1287 if currentUnit != "" && a.disks[currentUnit] != nil {
1189 - if v, err := strconv.ParseInt(value, 10, 64); err == nil && v > 0 {
1288 + if v := parseInt64OrZero(value); v > 0 {
1289 disk := a.disks[currentUnit]
1290 disk.ssdPowerOnDays = v
1291 if m, ok := a.mx.disks[currentUnit]; ok {
@@ -1310,7 +1409,7 @@ func (a *Collector) collectNetworkInterfaces(ctx context.Context) error {
1409 return
1410 }
1411 intf := a.getNetworkInterfaceMetrics(currentInterface)
1313 - if v, err := strconv.ParseInt(value, 10, 64); err == nil {
1412 + if v, ok := a.parseInt64Value(value, 1); ok {
1413 intf.mtu = v
1414 clean := cleanName(currentInterface)
1415 entry := a.mx.networkInterfaces[clean]
@@ -1366,7 +1465,7 @@ func (a *Collector) collectHTTPServerInfo(ctx context.Context) error {
1465 if currentKey == "" {
1466 return
1467 }
1369 - if v, err := strconv.ParseInt(value, 10, 64); err == nil {
1468 + if v, ok := a.parseInt64Value(value, 1); ok {
1469 entry := a.mx.httpServers[currentKey]
1470 entry.NormalConnections = v
1471 a.mx.httpServers[currentKey] = entry
@@ -1375,7 +1474,7 @@ func (a *Collector) collectHTTPServerInfo(ctx context.Context) error {
1474 if currentKey == "" {
1475 return
1476 }
1378 - if v, err := strconv.ParseInt(value, 10, 64); err == nil {
1477 + if v, ok := a.parseInt64Value(value, 1); ok {
1478 entry := a.mx.httpServers[currentKey]
1479 entry.SSLConnections = v
1480 a.mx.httpServers[currentKey] = entry
@@ -1384,7 +1483,7 @@ func (a *Collector) collectHTTPServerInfo(ctx context.Context) error {
1483 if currentKey == "" {
1484 return
1485 }
1387 - if v, err := strconv.ParseInt(value, 10, 64); err == nil {
1486 + if v, ok := a.parseInt64Value(value, 1); ok {
1487 entry := a.mx.httpServers[currentKey]
1488 entry.ActiveThreads = v
1489 a.mx.httpServers[currentKey] = entry
@@ -1393,7 +1492,7 @@ func (a *Collector) collectHTTPServerInfo(ctx context.Context) error {
1492 if currentKey == "" {
1493 return
1494 }
1396 - if v, err := strconv.ParseInt(value, 10, 64); err == nil {
1495 + if v, ok := a.parseInt64Value(value, 1); ok {
1496 entry := a.mx.httpServers[currentKey]
1497 entry.IdleThreads = v
1498 a.mx.httpServers[currentKey] = entry
@@ -1402,7 +1501,7 @@ func (a *Collector) collectHTTPServerInfo(ctx context.Context) error {
1501 if currentKey == "" {
1502 return
1503 }
1405 - if v, err := strconv.ParseInt(value, 10, 64); err == nil {
1504 + if v, ok := a.parseInt64Value(value, 1); ok {
1505 entry := a.mx.httpServers[currentKey]
1506 entry.TotalRequests = v
1507 a.mx.httpServers[currentKey] = entry
@@ -1411,7 +1510,7 @@ func (a *Collector) collectHTTPServerInfo(ctx context.Context) error {
1510 if currentKey == "" {
1511 return
1512 }
1414 - if v, err := strconv.ParseInt(value, 10, 64); err == nil {
1513 + if v, ok := a.parseInt64Value(value, 1); ok {
1514 entry := a.mx.httpServers[currentKey]
1515 entry.TotalResponses = v
1516 a.mx.httpServers[currentKey] = entry
@@ -1420,7 +1519,7 @@ func (a *Collector) collectHTTPServerInfo(ctx context.Context) error {
1519 if currentKey == "" {
1520 return
1521 }
1423 - if v, err := strconv.ParseInt(value, 10, 64); err == nil {
1522 + if v, ok := a.parseInt64Value(value, 1); ok {
1523 entry := a.mx.httpServers[currentKey]
1524 entry.TotalRequestsRejected = v
1525 a.mx.httpServers[currentKey] = entry
@@ -1429,7 +1528,7 @@ func (a *Collector) collectHTTPServerInfo(ctx context.Context) error {
1528 if currentKey == "" {
1529 return
1530 }
1432 - if v, err := strconv.ParseInt(value, 10, 64); err == nil {
1531 + if v, ok := a.parseInt64Value(value, 1); ok {
1532 entry := a.mx.httpServers[currentKey]
1533 entry.BytesReceived = v
1534 a.mx.httpServers[currentKey] = entry
@@ -1438,7 +1537,7 @@ func (a *Collector) collectHTTPServerInfo(ctx context.Context) error {
1537 if currentKey == "" {
1538 return
1539 }
1441 - if v, err := strconv.ParseInt(value, 10, 64); err == nil {
1540 + if v, ok := a.parseInt64Value(value, 1); ok {
1541 entry := a.mx.httpServers[currentKey]
1542 entry.BytesSent = v
1543 a.mx.httpServers[currentKey] = entry
@@ -1477,7 +1576,7 @@ func (a *Collector) collectPlanCache(ctx context.Context) error {
1576 if key == "" {
1577 return
1578 }
1480 - if parsed, ok := parseNumericValue(value, precision); ok {
1579 + if parsed, ok := a.parseInt64Value(value, precision); ok {
1580 if meta := a.getPlanCacheMetrics(key, currentHeading); meta != nil {
1581 meta.heading = currentHeading
1582 }
@@ -1493,36 +1592,43 @@ func (a *Collector) collectPlanCache(ctx context.Context) error {
1592 }
1593
1594 func (a *Collector) collectSystemActivity(ctx context.Context) error {
1496 - // Query SYSTEM_STATUS_INFO view for CPU utilization metrics
1497 - // This is more reliable than SYSTEM_ACTIVITY_INFO() table function
1498 - query := `SELECT
1499 - AVERAGE_CPU_RATE,
1500 - AVERAGE_CPU_UTILIZATION,
1501 - MINIMUM_CPU_UTILIZATION,
1502 - MAXIMUM_CPU_UTILIZATION
1503 - FROM QSYS2.SYSTEM_STATUS_INFO`
1595 + // IBM deprecated AVERAGE_CPU_* columns in 7.4, so we use a hybrid approach:
1596 + // 1. Try TOTAL_CPU_TIME (requires *JOBCTL authority) - monotonic counter, most accurate
1597 + // 2. Fall back to ELAPSED_CPU_USED with reset detection if *JOBCTL unavailable
1598
1505 - err := a.doQuery(ctx, query, func(column, value string, lineEnd bool) {
1506 - if value == "" {
1507 - return
1508 - }
1599 + // Query both potential data sources in one query
1600 + query := a.systemActivityQuery()
1601
1602 + var (
1603 + totalCPUTime int64 // Nanoseconds since IPL (NULL if no *JOBCTL)
1604 + elapsedTime int64 // Seconds since last reset
1605 + elapsedCPUUsed float64 // Average CPU% since last reset
1606 + hasTotalCPUTime bool
1607 + hasElapsedData bool
1608 + )
1609 +
1610 + err := a.doQuery(ctx, query, func(column, value string, lineEnd bool) {
1611 switch column {
1511 - case "AVERAGE_CPU_RATE":
1512 - if v, err := strconv.ParseFloat(value, 64); err == nil {
1513 - a.mx.systemActivity.AverageCPURate = int64(v * precision)
1514 - }
1515 - case "AVERAGE_CPU_UTILIZATION":
1516 - if v, err := strconv.ParseFloat(value, 64); err == nil {
1517 - a.mx.systemActivity.AverageCPUUtilization = int64(v * precision)
1612 + case "TOTAL_CPU_TIME":
1613 + // This will be NULL if user doesn't have *JOBCTL authority
1614 + if value != "" && !strings.EqualFold(value, "NULL") {
1615 + if v, ok := a.parseInt64Value(value, 1); ok {
1616 + totalCPUTime = v
1617 + hasTotalCPUTime = true
1618 + }
1619 }
1519 - case "MINIMUM_CPU_UTILIZATION":
1520 - if v, err := strconv.ParseFloat(value, 64); err == nil {
1521 - a.mx.systemActivity.MinimumCPUUtilization = int64(v * precision)
1620 + case "ELAPSED_TIME":
1621 + if value != "" && !strings.EqualFold(value, "NULL") {
1622 + if v, ok := a.parseInt64Value(value, 1); ok {
1623 + elapsedTime = v
1624 + hasElapsedData = true
1625 + }
1626 }
1523 - case "MAXIMUM_CPU_UTILIZATION":
1524 - if v, err := strconv.ParseFloat(value, 64); err == nil {
1525 - a.mx.systemActivity.MaximumCPUUtilization = int64(v * precision)
1627 + case "ELAPSED_CPU_USED":
1628 + if value != "" && !strings.EqualFold(value, "NULL") {
1629 + if v, ok := a.parseFloat64Value(value); ok {
1630 + elapsedCPUUsed = v
1631 + }
1632 }
1633 }
1634 })
@@ -1531,5 +1637,112 @@ func (a *Collector) collectSystemActivity(ctx context.Context) error {
1637 return fmt.Errorf("failed to collect system activity: %w", err)
1638 }
1639
1640 + // Determine which method to use
1641 + if hasTotalCPUTime {
1642 + // Primary method: Use TOTAL_CPU_TIME (requires *JOBCTL)
1643 + if a.cpuCollectionMethod == "" {
1644 + a.cpuCollectionMethod = "total_cpu_time"
1645 + a.Debugf("CPU collection: using TOTAL_CPU_TIME method (*JOBCTL authority available)")
1646 + }
1647 +
1648 + if a.hasCPUBaseline {
1649 + // Calculate CPU utilization from delta
1650 + deltaNanos := totalCPUTime - a.prevTotalCPUTime
1651 +
1652 + // Convert to per-core percentage based on update_every interval
1653 + // TOTAL_CPU_TIME is cumulative CPU-seconds across all processors in nanoseconds
1654 + // The delta/interval ratio directly gives us cores consumed (already in per-core scale)
1655 + // Formula: (delta_nanoseconds / 1e9) / update_every_seconds * 100
1656 + // Example: 2.0 CPU-seconds consumed in 1 second = 200% (2 cores fully utilized)
1657 + if a.UpdateEvery > 0 {
1658 + deltaSeconds := float64(deltaNanos) / 1e9
1659 + intervalSeconds := float64(a.UpdateEvery)
1660 +
1661 + // TOTAL_CPU_TIME is naturally in per-core scale - do NOT divide by ConfiguredCPUs
1662 + cpuUtilization := (deltaSeconds / intervalSeconds) * 100.0 * precision
1663 + maxAllowed := float64(a.mx.ConfiguredCPUs) * 100.0 * precision
1664 + if cpuUtilization >= 0 && (a.mx.ConfiguredCPUs <= 0 || cpuUtilization <= maxAllowed) {
1665 + a.mx.systemActivity.AverageCPUUtilization = int64(cpuUtilization)
1666 + a.mx.systemActivity.AverageCPURate = int64(cpuUtilization)
1667 + a.mx.CPUPercentage = int64(cpuUtilization)
1668 + } else {
1669 + if cpuUtilization < 0 {
1670 + a.Warningf("CPU collection: calculated utilization negative (%.2f%%), skipping this sample", cpuUtilization/precision)
1671 + } else {
1672 + a.Warningf("CPU collection: calculated utilization (%.2f%%) exceeds configured capacity (%d CPUs), skipping this sample",
1673 + cpuUtilization/precision, a.mx.ConfiguredCPUs)
1674 + }
1675 + }
1676 + }
1677 + } else {
1678 + a.Debugf("CPU collection: establishing baseline for TOTAL_CPU_TIME method")
1679 + }
1680 +
1681 + // Save current values for next iteration
1682 + a.prevTotalCPUTime = totalCPUTime
1683 + a.hasCPUBaseline = true
1684 +
1685 + } else if hasElapsedData {
1686 + // Fallback method: Use ELAPSED_CPU_USED with reset detection
1687 + if a.cpuCollectionMethod == "" {
1688 + a.cpuCollectionMethod = "elapsed_cpu_used"
1689 + a.Warningf("CPU collection: *JOBCTL authority not available, using ELAPSED_CPU_USED fallback method")
1690 + a.Warningf("CPU collection: This method is affected by SYSTEM_STATUS(RESET_STATISTICS=>'YES') calls")
1691 + }
1692 +
1693 + // Calculate product for reset detection
1694 + cpuProduct := int64(elapsedCPUUsed * float64(elapsedTime) * precision)
1695 +
1696 + if a.hasCPUBaseline {
1697 + // Detect if statistics were reset
1698 + resetDetected := false
1699 + if elapsedTime < a.prevElapsedTime {
1700 + resetDetected = true
1701 + a.Warningf("CPU collection: statistics reset detected (ELAPSED_TIME decreased from %d to %d)", a.prevElapsedTime, elapsedTime)
1702 + } else if cpuProduct < a.prevElapsedCPUProduct {
1703 + resetDetected = true
1704 + a.Warningf("CPU collection: statistics reset detected (CPU product decreased from %d to %d)", a.prevElapsedCPUProduct, cpuProduct)
1705 + }
1706 +
1707 + if !resetDetected {
1708 + // Calculate delta-based CPU utilization
1709 + deltaProduct := cpuProduct - a.prevElapsedCPUProduct
1710 + deltaTime := elapsedTime - a.prevElapsedTime
1711 +
1712 + if deltaTime > 0 {
1713 + // ELAPSED_CPU_USED is already in per-core scaling
1714 + intervalCPU := float64(deltaProduct) / float64(deltaTime)
1715 + cpuUtilization := intervalCPU
1716 + maxAllowed := float64(a.mx.ConfiguredCPUs) * 100.0 * precision
1717 + if cpuUtilization >= 0 && (a.mx.ConfiguredCPUs <= 0 || cpuUtilization <= maxAllowed) {
1718 + a.mx.systemActivity.AverageCPUUtilization = int64(cpuUtilization)
1719 + a.mx.systemActivity.AverageCPURate = int64(cpuUtilization)
1720 + a.mx.CPUPercentage = int64(cpuUtilization)
1721 + } else {
1722 + if cpuUtilization < 0 {
1723 + a.Warningf("CPU collection: interval utilization negative (%.2f%%), skipping this sample", cpuUtilization/precision)
1724 + } else {
1725 + a.Warningf("CPU collection: interval utilization (%.2f%%) exceeds configured capacity (%d CPUs), skipping this sample",
1726 + cpuUtilization/precision, a.mx.ConfiguredCPUs)
1727 + }
1728 + }
1729 + }
1730 + } else {
1731 + a.Debugf("CPU collection: re-establishing baseline after reset")
1732 + a.hasCPUBaseline = false
1733 + }
1734 + } else {
1735 + a.Debugf("CPU collection: establishing baseline for ELAPSED_CPU_USED method")
1736 + }
1737 +
1738 + // Save current values for next iteration
1739 + a.prevElapsedTime = elapsedTime
1740 + a.prevElapsedCPUProduct = cpuProduct
1741 + a.hasCPUBaseline = true
1742 +
1743 + } else {
1744 + return fmt.Errorf("failed to collect CPU data: no usable CPU metrics available")
1745 + }
1746 +
1747 return nil
1748 }
src/go/plugin/ibm.d/modules/as400/collector.go
+27 -1
@@ -72,6 +72,13 @@ type Collector struct {
72 dump *dumpContext
73 groups []collectionGroup
74
75 + // CPU collection state for delta-based calculation
76 + cpuCollectionMethod string // "total_cpu_time" or "elapsed_cpu_used"
77 + prevTotalCPUTime int64 // Previous TOTAL_CPU_TIME value (nanoseconds)
78 + prevElapsedTime int64 // Previous ELAPSED_TIME value (seconds)
79 + prevElapsedCPUProduct int64 // Previous ELAPSED_CPU_USED * ELAPSED_TIME product
80 + hasCPUBaseline bool // Whether we have a previous measurement
81 +
82 once sync.Once
83 }
84
@@ -385,8 +392,27 @@ func (c *Collector) exportSystemMetrics() {
392 Base: c.mx.BasePoolMaxThreads,
393 })
394
395 + avgDiskBusy := c.mx.DiskBusyPercentage
396 + if avgDiskBusy == 0 {
397 + var (
398 + sum int64
399 + count int64
400 + )
401 + for _, values := range c.mx.disks {
402 + sum += values.BusyPercent
403 + count++
404 + }
405 + if count > 0 {
406 + avgDiskBusy = sum / count
407 + }
408 + }
409 +
410 + if avgDiskBusy != c.mx.DiskBusyPercentage {
411 + c.mx.DiskBusyPercentage = avgDiskBusy
412 + }
413 +
414 contexts.System.DiskBusyAverage.Set(c.State, labels, contexts.SystemDiskBusyAverageValues{
389 - Busy: c.mx.DiskBusyPercentage,
415 + Busy: avgDiskBusy,
416 })
417
418 contexts.System.SystemASPUsage.Set(c.State, labels, contexts.SystemSystemASPUsageValues{
src/go/plugin/ibm.d/modules/as400/contexts/contexts.yaml
+2 -5
@@ -3,7 +3,7 @@ System:
3 contexts:
4 - name: CPUUtilization
5 context: as400.cpu_utilization
6 - title: CPU Utilization
6 + title: CPU Utilization (100% = 1 CPU core)
7 family: compute/cpu
8 units: percentage
9 type: line
@@ -71,7 +71,7 @@ System:
71 context: as400.main_storage_size
72 title: Main Storage Size
73 family: memory/overview
74 - units: KiB
74 + units: bytes
75 type: line
76 priority: 201
77 dimensions:
@@ -302,7 +302,6 @@ Disk:
302 algo: incremental
303 - name: write
304 algo: incremental
305 - mul: -1
305 - name: SpaceUsage
306 context: as400.disk_space_usage
307 title: Disk Space Usage
@@ -340,7 +339,6 @@ Disk:
339 algo: incremental
340 - name: write
341 algo: incremental
343 - mul: -1
342 - name: SSDHealth
343 context: as400.disk_ssd_health
344 title: Disk SSD Health
@@ -463,7 +461,6 @@ ActiveJob:
461 algo: incremental
462 - name: interactive_transactions
463 algo: incremental
466 - mul: -1
464 - name: Threads
465 context: as400.activejob_threads
466 title: Active Job Thread Count
src/go/plugin/ibm.d/modules/as400/contexts/zz_generated_contexts.go
+5 -5
@@ -277,7 +277,7 @@ var ActiveJob = struct {
277 {
278 Name: "interactive_transactions",
279 Algorithm: module.Incremental,
280 - Mul: -1,
280 + Mul: 1,
281 Div: 1,
282 Precision: 1,
283 },
@@ -552,7 +552,7 @@ var Disk = struct {
552 {
553 Name: "write",
554 Algorithm: module.Incremental,
555 - Mul: -1,
555 + Mul: 1,
556 Div: 1,
557 Precision: 1,
558 },
@@ -647,7 +647,7 @@ var Disk = struct {
647 {
648 Name: "write",
649 Algorithm: module.Incremental,
650 - Mul: -1,
650 + Mul: 1,
651 Div: 1,
652 Precision: 1,
653 },
@@ -2204,7 +2204,7 @@ var System = struct {
2204 Context: framework.Context[EmptyLabels]{
2205 Name: "as400.cpu_utilization",
2206 Family: "compute/cpu",
2207 - Title: "CPU Utilization",
2207 + Title: "CPU Utilization (100% = 1 CPU core)",
2208 Units: "percentage",
2209 Type: module.Line,
2210 Priority: 101,
@@ -2345,7 +2345,7 @@ var System = struct {
2345 Name: "as400.main_storage_size",
2346 Family: "memory/overview",
2347 Title: "Main Storage Size",
2348 - Units: "KiB",
2348 + Units: "bytes",
2349 Type: module.Line,
2350 Priority: 201,
2351 UpdateEvery: 1,
src/go/plugin/ibm.d/modules/as400/helpers.go
+7
@@ -294,3 +294,10 @@ func (c *Collector) memoryPoolQuery() string {
294 }
295 return queryMemoryPoolsNoReset
296 }
297 +
298 +func (c *Collector) systemActivityQuery() string {
299 + if c.ResetStatistics {
300 + return querySystemActivityReset
301 + }
302 + return querySystemActivityNoReset
303 +}
src/go/plugin/ibm.d/modules/as400/metadata.yaml
+90 -2
@@ -32,6 +32,94 @@ modules:
32 - libodbc.so (provided by unixODBC)
33 - IBM i Access Client Solutions
34
35 + **CPU Collection Methods:**
36 +
37 + The collector uses a hybrid approach for CPU utilization metrics to handle IBM i 7.4+ where
38 + `AVERAGE_CPU_*` columns were deprecated:
39 +
40 + 1. **Primary Method - TOTAL_CPU_TIME**: Uses the monotonic `TOTAL_CPU_TIME` counter from
41 + `QSYS2.SYSTEM_STATUS()` to calculate CPU utilization via delta-based calculation. This is
42 + the most accurate method but requires `*JOBCTL` special authority. TOTAL_CPU_TIME is a
43 + cumulative counter in nanoseconds representing CPU-seconds consumed, naturally in per-core
44 + scale.
45 +
46 + 2. **Fallback Method - ELAPSED_CPU_USED**: If `*JOBCTL` authority is not available, falls back
47 + to `ELAPSED_CPU_USED` with automatic reset detection. This method tracks when IBM i statistics
48 + are reset (either manually or via `reset_statistics` configuration) and re-establishes a
49 + baseline after detecting resets. The values are already in per-core scale.
50 +
51 + 3. **Legacy Method - AVERAGE_CPU_UTILIZATION**: For IBM i versions before 7.4, uses the now-
52 + deprecated `AVERAGE_CPU_UTILIZATION` column, which IBM reports in the same per-core scale.
53 +
54 + The collector automatically selects the appropriate method based on available permissions and
55 + logs which method is being used.
56 +
57 + **CPU Metric Scale:**
58 +
59 + CPU utilization is reported using the "100% = 1 CPU core" semantic. This means:
60 + - 100% indicates one CPU core is fully utilized
61 + - 400% indicates four CPU cores are fully utilized
62 + - Values are limited to 100% × ConfiguredCPUs, matching the partition's configured capacity
63 +
64 + For shared LPARs, the metrics show absolute CPU consumption in per-core scale, not relative to
65 + entitled capacity. For example, a shared LPAR entitled to 0.20 cores can show 150% utilization
66 + when bursting above entitlement.
67 +
68 + **Statistics Reset Behavior:**
69 +
70 + The `reset_statistics` configuration option controls whether the collector resets IBM i system
71 + statistics on each query via `SYSTEM_STATUS(RESET_STATISTICS=>'YES')`. When enabled:
72 +
73 + - System-level statistics (CPU, memory pools, etc.) are reset after each collection cycle
74 + - Matches legacy behavior but clears global statistics that other tools may rely on
75 + - The ELAPSED_CPU_USED fallback method will detect and handle these resets automatically
76 + - **Caution**: Enabling this affects all users and applications on the IBM i system
77 +
78 + Default: `false` (statistics are not reset, using `RESET_STATISTICS=>'NO'`)
79 +
80 + **Cardinality Management:**
81 +
82 + To prevent performance issues from excessive metric creation, the collector enforces cardinality
83 + limits on per-instance metrics (disks, subsystems, job queues, message queues, output queues,
84 + active jobs, network interfaces, HTTP servers).
85 +
86 + **How Limits Work:**
87 + - The collector counts instances before collecting metrics
88 + - If count exceeds the configured `max_*` limit, **collection is skipped entirely** for that category
89 + - The collector logs a warning: `"[category] count (X) exceeds limit (Y), skipping collection"`
90 + - No metrics are collected for that category until you adjust the configuration
91 +
92 + **Configuration Options:**
93 +
94 + Use **both** limit and selector options together to manage high-cardinality environments:
95 +
96 + | Option | Purpose | Default |
97 + |--------|---------|---------|
98 + | `max_disks` | Maximum disk units to monitor | 100 |
99 + | `max_subsystems` | Maximum subsystems to monitor | 100 |
100 + | `max_job_queues` | Maximum job queues to monitor | 100 |
101 + | `max_message_queues` | Maximum message queues to monitor | 100 |
102 + | `max_output_queues` | Maximum output queues to monitor | 100 |
103 + | `max_active_jobs` | Maximum active jobs to monitor | 100 |
104 + | `collect_disks_matching` | Glob pattern to filter disks (e.g., `"001* 002*"`) | `""` (match all) |
105 + | `collect_subsystems_matching` | Glob pattern to filter subsystems (e.g., `"QINTER QBATCH"`) | `""` (match all) |
106 + | `collect_job_queues_matching` | Glob pattern to filter job queues (e.g., `"QSYS/*"`) | `""` (match all) |
107 +
108 + **Example Workflow:**
109 +
110 + 1. System has 500 disks, collector skips disk metrics (exceeds default limit of 100)
111 + 2. Check logs: `"disk count (500) exceeds limit (100), skipping per-disk metrics"`
112 + 3. Two options:
113 + - **Option A**: Increase limit: `max_disks: 500` (collects all 500 disks)
114 + - **Option B**: Use selector: `collect_disks_matching: "00[1-5]*"` (cherry-pick specific disks)
115 +
116 + **Best Practices:**
117 + - Use selectors to monitor only business-critical objects in large environments
118 + - Set limits based on your Netdata server's capacity (each instance = multiple charts)
119 + - Start with defaults and adjust based on actual usage patterns
120 +
121 + Network interface metrics have a fixed internal limit of 50 instances, and HTTP server metrics are capped at 200 instances; these limits are currently not configurable.
122 +
123 method_description: |
124 The collector connects to IBM i (AS/400) and collects metrics via its monitoring interface.
125 supported_platforms:
@@ -358,7 +446,7 @@ modules:
446 labels: []
447 metrics:
448 - name: as400.cpu_utilization
361 - description: CPU Utilization
449 + description: CPU Utilization (100% = 1 CPU core)
450 unit: percentage
451 chart_type: line
452 dimensions:
@@ -397,7 +485,7 @@ modules:
485 - name: waiting
486 - name: as400.main_storage_size
487 description: Main Storage Size
400 - unit: KiB
488 + unit: bytes
489 chart_type: line
490 dimensions:
491 - name: total
src/go/plugin/ibm.d/modules/as400/module.yaml
+88
@@ -11,6 +11,94 @@ description: |
11 **Required Libraries:**
12 - libodbc.so (provided by unixODBC)
13 - IBM i Access Client Solutions
14 +
15 + **CPU Collection Methods:**
16 +
17 + The collector uses a hybrid approach for CPU utilization metrics to handle IBM i 7.4+ where
18 + `AVERAGE_CPU_*` columns were deprecated:
19 +
20 + 1. **Primary Method - TOTAL_CPU_TIME**: Uses the monotonic `TOTAL_CPU_TIME` counter from
21 + `QSYS2.SYSTEM_STATUS()` to calculate CPU utilization via delta-based calculation. This is
22 + the most accurate method but requires `*JOBCTL` special authority. TOTAL_CPU_TIME is a
23 + cumulative counter in nanoseconds representing CPU-seconds consumed, naturally in per-core
24 + scale.
25 +
26 + 2. **Fallback Method - ELAPSED_CPU_USED**: If `*JOBCTL` authority is not available, falls back
27 + to `ELAPSED_CPU_USED` with automatic reset detection. This method tracks when IBM i statistics
28 + are reset (either manually or via `reset_statistics` configuration) and re-establishes a
29 + baseline after detecting resets. The values are already in per-core scale.
30 +
31 + 3. **Legacy Method - AVERAGE_CPU_UTILIZATION**: For IBM i versions before 7.4, uses the now-
32 + deprecated `AVERAGE_CPU_UTILIZATION` column, which IBM reports in the same per-core scale.
33 +
34 + The collector automatically selects the appropriate method based on available permissions and
35 + logs which method is being used.
36 +
37 + **CPU Metric Scale:**
38 +
39 + CPU utilization is reported using the "100% = 1 CPU core" semantic. This means:
40 + - 100% indicates one CPU core is fully utilized
41 + - 400% indicates four CPU cores are fully utilized
42 + - Values are limited to 100% × ConfiguredCPUs, matching the partition's configured capacity
43 +
44 + For shared LPARs, the metrics show absolute CPU consumption in per-core scale, not relative to
45 + entitled capacity. For example, a shared LPAR entitled to 0.20 cores can show 150% utilization
46 + when bursting above entitlement.
47 +
48 + **Statistics Reset Behavior:**
49 +
50 + The `reset_statistics` configuration option controls whether the collector resets IBM i system
51 + statistics on each query via `SYSTEM_STATUS(RESET_STATISTICS=>'YES')`. When enabled:
52 +
53 + - System-level statistics (CPU, memory pools, etc.) are reset after each collection cycle
54 + - Matches legacy behavior but clears global statistics that other tools may rely on
55 + - The ELAPSED_CPU_USED fallback method will detect and handle these resets automatically
56 + - **Caution**: Enabling this affects all users and applications on the IBM i system
57 +
58 + Default: `false` (statistics are not reset, using `RESET_STATISTICS=>'NO'`)
59 +
60 + **Cardinality Management:**
61 +
62 + To prevent performance issues from excessive metric creation, the collector enforces cardinality
63 + limits on per-instance metrics (disks, subsystems, job queues, message queues, output queues,
64 + active jobs, network interfaces, HTTP servers).
65 +
66 + **How Limits Work:**
67 + - The collector counts instances before collecting metrics
68 + - If count exceeds the configured `max_*` limit, **collection is skipped entirely** for that category
69 + - The collector logs a warning: `"[category] count (X) exceeds limit (Y), skipping collection"`
70 + - No metrics are collected for that category until you adjust the configuration
71 +
72 + **Configuration Options:**
73 +
74 + Use **both** limit and selector options together to manage high-cardinality environments:
75 +
76 + | Option | Purpose | Default |
77 + |--------|---------|---------|
78 + | `max_disks` | Maximum disk units to monitor | 100 |
79 + | `max_subsystems` | Maximum subsystems to monitor | 100 |
80 + | `max_job_queues` | Maximum job queues to monitor | 100 |
81 + | `max_message_queues` | Maximum message queues to monitor | 100 |
82 + | `max_output_queues` | Maximum output queues to monitor | 100 |
83 + | `max_active_jobs` | Maximum active jobs to monitor | 100 |
84 + | `collect_disks_matching` | Glob pattern to filter disks (e.g., `"001* 002*"`) | `""` (match all) |
85 + | `collect_subsystems_matching` | Glob pattern to filter subsystems (e.g., `"QINTER QBATCH"`) | `""` (match all) |
86 + | `collect_job_queues_matching` | Glob pattern to filter job queues (e.g., `"QSYS/*"`) | `""` (match all) |
87 +
88 + **Example Workflow:**
89 +
90 + 1. System has 500 disks, collector skips disk metrics (exceeds default limit of 100)
91 + 2. Check logs: `"disk count (500) exceeds limit (100), skipping per-disk metrics"`
92 + 3. Two options:
93 + - **Option A**: Increase limit: `max_disks: 500` (collects all 500 disks)
94 + - **Option B**: Use selector: `collect_disks_matching: "00[1-5]*"` (cherry-pick specific disks)
95 +
96 + **Best Practices:**
97 + - Use selectors to monitor only business-critical objects in large environments
98 + - Set limits based on your Netdata server's capacity (each instance = multiple charts)
99 + - Start with defaults and adjust based on actual usage patterns
100 +
101 + Network interface metrics have a fixed internal limit of 50 instances, and HTTP server metrics are capped at 200 instances; these limits are currently not configurable.
102 icon: ibm-i.svg
103 categories:
104 - data-collection.infrastructure
src/go/plugin/ibm.d/modules/as400/sql_queries.go
+16 -1
@@ -11,6 +11,21 @@ const (
11 querySystemStatusReset = `SELECT * FROM TABLE(QSYS2.SYSTEM_STATUS(RESET_STATISTICS=>'YES',DETAILED_INFO=>'ALL')) X`
12 querySystemStatusNoReset = `SELECT * FROM TABLE(QSYS2.SYSTEM_STATUS(RESET_STATISTICS=>'NO',DETAILED_INFO=>'ALL')) X`
13
14 + // CPU collection using SYSTEM_STATUS with specific columns for hybrid method
15 + // TOTAL_CPU_TIME: Monotonic counter in nanoseconds (requires *JOBCTL authority)
16 + // ELAPSED_CPU_USED: Average CPU percentage since last statistics reset
17 + // ELAPSED_TIME: Seconds since last statistics reset
18 + querySystemActivityReset = `SELECT
19 + TOTAL_CPU_TIME,
20 + ELAPSED_TIME,
21 + ELAPSED_CPU_USED
22 +FROM TABLE(QSYS2.SYSTEM_STATUS('YES','ALL'))`
23 + querySystemActivityNoReset = `SELECT
24 + TOTAL_CPU_TIME,
25 + ELAPSED_TIME,
26 + ELAPSED_CPU_USED
27 +FROM TABLE(QSYS2.SYSTEM_STATUS('NO','ALL'))`
28 +
29 // VERIFIED: Memory pool monitoring using MEMORY_POOL() function
30 // The reset variant matches legacy behaviour; NO leaves system statistics intact.
31 queryMemoryPoolsReset = `
@@ -114,7 +129,7 @@ const (
129 COALESCE(CONNECTION_TYPE, 'UNKNOWN') as CONNECTION_TYPE,
130 COALESCE(INTERNET_ADDRESS, '') as INTERNET_ADDRESS,
131 COALESCE(NETWORK_ADDRESS, '') as NETWORK_ADDRESS,
117 - COALESCE(MAXIMUM_TRANSMISSION_UNIT, 0) as MTU
132 + COALESCE(MAXIMUM_TRANSMISSION_UNIT, 0) as MAXIMUM_TRANSMISSION_UNIT
133 FROM QSYS2.NETSTAT_INTERFACE_INFO
134 WHERE LINE_DESCRIPTION != '*LOOPBACK'
135 ORDER BY LINE_DESCRIPTION
src/go/plugin/ibm.d/modules/mq/config_schema.json
+75
@@ -169,5 +169,80 @@
169 },
170 "title": "mq collector configuration",
171 "type": "object"
172 + },
173 + "uiSchema": {
174 + "password": {
175 + "ui:widget": "password"
176 + },
177 + "ui:flavour": "tabs",
178 + "ui:options": {
179 + "tabs": [
180 + {
181 + "fields": [
182 + "update_every"
183 + ],
184 + "title": "Connection"
185 + },
186 + {
187 + "fields": [
188 + "queue_manager",
189 + "channel",
190 + "host",
191 + "port",
192 + "user",
193 + "statistics_interval,omitempty",
194 + "sys_topic_interval,omitempty"
195 + ],
196 + "title": "Advanced"
197 + },
198 + {
199 + "fields": [
200 + "password"
201 + ],
202 + "title": "Auth"
203 + },
204 + {
205 + "fields": [
206 + "collect_queues",
207 + "collect_channels",
208 + "collect_topics",
209 + "collect_listeners",
210 + "collect_subscriptions",
211 + "collect_system_queues",
212 + "collect_system_channels",
213 + "collect_system_topics",
214 + "collect_system_listeners",
215 + "collect_channel_config",
216 + "collect_queue_config",
217 + "collect_reset_queue_stats",
218 + "collect_statistics_queue",
219 + "collect_sys_topics"
220 + ],
221 + "title": "Collection"
222 + },
223 + {
224 + "fields": [
225 + "queue_selector",
226 + "channel_selector",
227 + "topic_selector",
228 + "listener_selector",
229 + "subscription_selector"
230 + ],
231 + "title": "Filters"
232 + },
233 + {
234 + "fields": [
235 + "max_queues",
236 + "max_channels",
237 + "max_topics",
238 + "max_listeners"
239 + ],
240 + "title": "Limits"
241 + }
242 + ]
243 + },
244 + "uiOptions": {
245 + "fullPage": true
246 + }
247 }
248 }
\ No newline at end of file
src/go/plugin/ibm.d/modules/mq/contexts/zz_generated_contexts.go
+20 -20
@@ -542,7 +542,7 @@ var Channel = struct {
542 Status: ChannelStatusContext{
543 Context: framework.Context[ChannelLabels]{
544 Name: "mq.channel.status",
545 - Family: "channels",
545 + Family: "channels/overview",
546 Title: "Channel Status",
547 Units: "status",
548 Type: module.Stacked,
@@ -643,7 +643,7 @@ var Channel = struct {
643 Messages: ChannelMessagesContext{
644 Context: framework.Context[ChannelLabels]{
645 Name: "mq.channel.messages",
646 - Family: "channels",
646 + Family: "channels/overview",
647 Title: "Channel Message Rate",
648 Units: "messages/s",
649 Type: module.Line,
@@ -667,7 +667,7 @@ var Channel = struct {
667 Bytes: ChannelBytesContext{
668 Context: framework.Context[ChannelLabels]{
669 Name: "mq.channel.bytes",
670 - Family: "channels",
670 + Family: "channels/overview",
671 Title: "Channel Data Transfer Rate",
672 Units: "bytes/s",
673 Type: module.Line,
@@ -691,7 +691,7 @@ var Channel = struct {
691 Batches: ChannelBatchesContext{
692 Context: framework.Context[ChannelLabels]{
693 Name: "mq.channel.batches",
694 - Family: "channels",
694 + Family: "channels/overview",
695 Title: "Channel Batch Rate",
696 Units: "batches/s",
697 Type: module.Line,
@@ -715,7 +715,7 @@ var Channel = struct {
715 BatchSize: ChannelBatchSizeContext{
716 Context: framework.Context[ChannelLabels]{
717 Name: "mq.channel.batch_size",
718 - Family: "channels",
718 + Family: "channels/overview",
719 Title: "Channel Batch Size",
720 Units: "messages",
721 Type: module.Line,
@@ -739,7 +739,7 @@ var Channel = struct {
739 BatchInterval: ChannelBatchIntervalContext{
740 Context: framework.Context[ChannelLabels]{
741 Name: "mq.channel.batch_interval",
742 - Family: "channels",
742 + Family: "channels/overview",
743 Title: "Channel Batch Interval",
744 Units: "milliseconds",
745 Type: module.Line,
@@ -763,7 +763,7 @@ var Channel = struct {
763 Intervals: ChannelIntervalsContext{
764 Context: framework.Context[ChannelLabels]{
765 Name: "mq.channel.intervals",
766 - Family: "channels",
766 + Family: "channels/overview",
767 Title: "Channel Intervals",
768 Units: "seconds",
769 Type: module.Line,
@@ -801,7 +801,7 @@ var Channel = struct {
801 ShortRetryCount: ChannelShortRetryCountContext{
802 Context: framework.Context[ChannelLabels]{
803 Name: "mq.channel.short_retry_count",
804 - Family: "channels",
804 + Family: "channels/overview",
805 Title: "Channel Short Retry Count",
806 Units: "retries",
807 Type: module.Line,
@@ -825,7 +825,7 @@ var Channel = struct {
825 LongRetryInterval: ChannelLongRetryIntervalContext{
826 Context: framework.Context[ChannelLabels]{
827 Name: "mq.channel.long_retry_interval",
828 - Family: "channels",
828 + Family: "channels/overview",
829 Title: "Channel Long Retry Interval",
830 Units: "seconds",
831 Type: module.Line,
@@ -849,7 +849,7 @@ var Channel = struct {
849 MaxMessageLength: ChannelMaxMessageLengthContext{
850 Context: framework.Context[ChannelLabels]{
851 Name: "mq.channel.max_msg_length",
852 - Family: "channels",
852 + Family: "channels/overview",
853 Title: "Channel Max Message Length",
854 Units: "bytes",
855 Type: module.Line,
@@ -873,7 +873,7 @@ var Channel = struct {
873 SharingConversations: ChannelSharingConversationsContext{
874 Context: framework.Context[ChannelLabels]{
875 Name: "mq.channel.sharing_conversations",
876 - Family: "channels",
876 + Family: "channels/overview",
877 Title: "Channel Sharing Conversations",
878 Units: "conversations",
879 Type: module.Line,
@@ -897,7 +897,7 @@ var Channel = struct {
897 NetworkPriority: ChannelNetworkPriorityContext{
898 Context: framework.Context[ChannelLabels]{
899 Name: "mq.channel.network_priority",
900 - Family: "channels",
900 + Family: "channels/overview",
901 Title: "Channel Network Priority",
902 Units: "priority",
903 Type: module.Line,
@@ -921,7 +921,7 @@ var Channel = struct {
921 BufferCounts: ChannelBufferCountsContext{
922 Context: framework.Context[ChannelLabels]{
923 Name: "mq.channel.buffer_counts",
924 - Family: "channels",
924 + Family: "channels/overview",
925 Title: "Channel Buffer Counts",
926 Units: "buffers",
927 Type: module.Line,
@@ -952,7 +952,7 @@ var Channel = struct {
952 CurrentMessages: ChannelCurrentMessagesContext{
953 Context: framework.Context[ChannelLabels]{
954 Name: "mq.channel.current_messages",
955 - Family: "channels",
955 + Family: "channels/overview",
956 Title: "Channel Current Messages",
957 Units: "messages",
958 Type: module.Line,
@@ -976,7 +976,7 @@ var Channel = struct {
976 XmitQueueTime: ChannelXmitQueueTimeContext{
977 Context: framework.Context[ChannelLabels]{
978 Name: "mq.channel.xmitq_time",
979 - Family: "channels",
979 + Family: "channels/overview",
980 Title: "Channel Transmission Queue Time",
981 Units: "milliseconds",
982 Type: module.Line,
@@ -1000,7 +1000,7 @@ var Channel = struct {
1000 MCAStatus: ChannelMCAStatusContext{
1001 Context: framework.Context[ChannelLabels]{
1002 Name: "mq.channel.mca_status",
1003 - Family: "channels",
1003 + Family: "channels/overview",
1004 Title: "Channel MCA Status",
1005 Units: "status",
1006 Type: module.Line,
@@ -1024,7 +1024,7 @@ var Channel = struct {
1024 InDoubtStatus: ChannelInDoubtStatusContext{
1025 Context: framework.Context[ChannelLabels]{
1026 Name: "mq.channel.indoubt_status",
1027 - Family: "channels",
1027 + Family: "channels/overview",
1028 Title: "Channel In-Doubt Status",
1029 Units: "status",
1030 Type: module.Line,
@@ -1048,7 +1048,7 @@ var Channel = struct {
1048 SSLKeyResets: ChannelSSLKeyResetsContext{
1049 Context: framework.Context[ChannelLabels]{
1050 Name: "mq.channel.ssl_key_resets",
1051 - Family: "channels",
1051 + Family: "channels/overview",
1052 Title: "Channel SSL Key Resets",
1053 Units: "resets",
1054 Type: module.Line,
@@ -1072,7 +1072,7 @@ var Channel = struct {
1072 NPMSpeed: ChannelNPMSpeedContext{
1073 Context: framework.Context[ChannelLabels]{
1074 Name: "mq.channel.npm_speed",
1075 - Family: "channels",
1075 + Family: "channels/overview",
1076 Title: "Channel Non-Persistent Message Speed",
1077 Units: "speed",
1078 Type: module.Line,
@@ -1096,7 +1096,7 @@ var Channel = struct {
1096 CurrentSharingConversations: ChannelCurrentSharingConversationsContext{
1097 Context: framework.Context[ChannelLabels]{
1098 Name: "mq.channel.current_sharing_convs",
1099 - Family: "channels",
1099 + Family: "channels/overview",
1100 Title: "Channel Current Sharing Conversations",
1101 Units: "conversations",
1102 Type: module.Line,
src/go/plugin/ibm.d/pkg/odbcbridge/bridge.c
+22 -1
@@ -540,6 +540,27 @@ void odbc_disconnect(odbc_conn_t conn_handle) {
540 free(conn);
541 }
542
543 +int64_t odbc_value_get_int64(const odbc_value_t* value) {
544 + if (!value || value->is_null || value->type != ODBC_TYPE_INT64) {
545 + return 0;
546 + }
547 + return value->data.int_val;
548 +}
549 +
550 +double odbc_value_get_double(const odbc_value_t* value) {
551 + if (!value || value->is_null || value->type != ODBC_TYPE_DOUBLE) {
552 + return 0.0;
553 + }
554 + return value->data.double_val;
555 +}
556 +
557 +const char* odbc_value_get_string(const odbc_value_t* value) {
558 + if (!value || value->is_null || value->type != ODBC_TYPE_STRING) {
559 + return NULL;
560 + }
561 + return value->data.string_val;
562 +}
563 +
564 // Check connection status
565 int odbc_is_connected(odbc_conn_t conn_handle) {
566 if (!conn_handle) return 0;
@@ -563,4 +584,4 @@ int odbc_get_sqlstate(odbc_conn_t conn_handle, char* state, size_t state_size) {
584 state[state_size - 1] = '\0';
585
586 return ODBC_SUCCESS;
566 -}
\ No newline at end of file
587 +}
src/go/plugin/ibm.d/pkg/odbcbridge/bridge.h
+6 -1
@@ -85,4 +85,9 @@ int odbc_bind_column(odbc_conn_t conn, int column_index, void* buffer, size_t bu
85 const char* odbc_get_last_error(odbc_conn_t conn);
86 int odbc_get_sqlstate(odbc_conn_t conn, char* state, size_t state_size);
87
88 -#endif // ODBC_BRIDGE_H
\ No newline at end of file
88 +// Helpers for extracting values from odbc_value_t (return neutral values when NULL or type mismatch)
89 +int64_t odbc_value_get_int64(const odbc_value_t* value);
90 +double odbc_value_get_double(const odbc_value_t* value);
91 +const char* odbc_value_get_string(const odbc_value_t* value);
92 +
93 +#endif // ODBC_BRIDGE_H
src/go/plugin/ibm.d/pkg/odbcbridge/connection.go
+5 -8
@@ -346,16 +346,13 @@ func convertValue(cValue *C.odbc_value_t) driver.Value {
346
347 switch DataType(cValue._type) {
348 case TypeInt64:
349 - return int64(cValue.data[0])
349 + return int64(C.odbc_value_get_int64(cValue))
350 case TypeDouble:
351 - // Handle double properly
352 - doublePtr := (*float64)(unsafe.Pointer(&cValue.data[0]))
353 - return *doublePtr
351 + return float64(C.odbc_value_get_double(cValue))
352 case TypeString:
355 - // String is stored as a pointer in the union
356 - strPtr := (*unsafe.Pointer)(unsafe.Pointer(&cValue.data[0]))
357 - if *strPtr != nil {
358 - return C.GoString((*C.char)(*strPtr))
353 + str := C.odbc_value_get_string(cValue)
354 + if str != nil {
355 + return C.GoString(str)
356 }
357 return ""
358 default: