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: