@cryptotaxi247 / netdata-1 / commits / 1bb58dbd8

chore(scripts.d): remove zabbix (#21759)

Ilya Mashchenko committed Feb 13, 2026 at 12:54 UTC 1bb58dbd8ef403a252159fd1f3de7de1c6c83760
53 files changed +9 -23021
src/go/cmd/scriptsdplugin/main.go
-1
@@ -21,7 +21,6 @@ import (
21 "github.com/netdata/netdata/go/plugins/plugin/go.d/agent"
22 _ "github.com/netdata/netdata/go/plugins/plugin/scripts.d/modules/nagios"
23 _ "github.com/netdata/netdata/go/plugins/plugin/scripts.d/modules/scheduler"
24 - _ "github.com/netdata/netdata/go/plugins/plugin/scripts.d/modules/zabbix"
24 )
25
26 func init() {
src/go/plugin/scripts.d/README.md
+9 -27
@@ -119,14 +119,14 @@ etc.) keep separate time-series across restarts.
119 Netdata stores integers, so scripts.d.plugin normalizes perfdata to base units
120 before emitting metrics:
121
122 -| Input unit | Canonical unit | Scaling behaviour |
123 -|------------------------------|----------------|---------------------------------------------------|
124 -| Bytes, KB, MB, GB, TB | bytes | Converted to raw bytes, divider = 1 |
125 -| Bytes per second (KB/s …) | bytes/s | Converted to bytes/s, divider = 1 |
126 -| Seconds, ms, µs, ns | seconds | Stored as nanoseconds, divider = 1 000 000 000 |
127 -| Percent (`%`) | % | Value ×1000, divider = 1000 |
128 -| Counters (`c`) | c | Stored as-is |
129 -| Any other unit or unitless | original text | Value ×1000, divider = 1000 |
122 +| Input unit | Canonical unit | Scaling behaviour |
123 +|----------------------------|----------------|------------------------------------------------|
124 +| Bytes, KB, MB, GB, TB | bytes | Converted to raw bytes, divider = 1 |
125 +| Bytes per second (KB/s …) | bytes/s | Converted to bytes/s, divider = 1 |
126 +| Seconds, ms, µs, ns | seconds | Stored as nanoseconds, divider = 1 000 000 000 |
127 +| Percent (`%`) | % | Value ×1000, divider = 1000 |
128 +| Counters (`c`) | c | Stored as-is |
129 +| Any other unit or unitless | original text | Value ×1000, divider = 1000 |
130
131 When a plugin flips between `KB` and `MB` (or `ms` and `s`) the collector still
132 publishes a single chart in base units, so there are no spurious RRD resets. If
@@ -143,9 +143,7 @@ track the configured intervals; spikes indicate the executor is falling behind
143
144 Jobs follow the same cadence controls as go.d collectors: set `update_every`
145 (seconds) inside each job definition to run faster or slower than the default
146 -60 s interval. When omitted, scripts.d picks a conservative default (currently
147 -60 s). This field applies to both Nagios and Zabbix jobs so you can mirror your
148 -existing polling schedules.
146 +60 s interval. When omitted, scripts.d picks a conservative default (currently 60 s).
147
148 ### Logging over OTLP (TLS support)
149
@@ -163,22 +161,6 @@ When `tls` is `true`, the plugin always establishes a TLS 1.2 connection using
161 these settings. Set `tls: false` only for loopback collectors or other trusted
162 plaintext networks.
163
166 -### LLD lifecycle
167 -
168 -Zabbix-style jobs keep their LLD catalogs and missing-instance counters in
169 -memory. Restarting the agent or reloading the plugin resets that state, mirroring
170 -how go.d collectors reset transient discovery data. If you need longer retention,
171 -keep the plugin running continuously; no on-disk cache is maintained.
172 -
173 -### Zabbix preprocessing
174 -
175 -Dependent pipelines share the same `.steps` schema as the standalone
176 -`zabbix-preproc` library. Step `type` accepts human-readable tokens such as
177 -`jsonpath`, `csv_to_json`, or `snmp_walk_value`. The optional `error_handler`
178 -field understands the native Zabbix actions: `default`, `discard`, `set-value`,
179 -and `set-error`. When you choose `set-value` or `set-error` provide the fallback
180 -text via `error_handler_params`.
181 -
164 ### Mock integration tests
165
166 A small suite of mock Nagios plugins lives under `tests/plugins/`. They exercise
src/go/plugin/scripts.d/modules/zabbix/README.md deleted
-1
@@ -1 +0,0 @@
1 -integrations/zabbix_preprocessing.md
\ No newline at end of file
src/go/plugin/scripts.d/modules/zabbix/config_schema.json deleted
-664
@@ -1,664 +0,0 @@
1 -{
2 - "jsonSchema": {
3 - "$schema": "http://json-schema.org/draft-07/schema#",
4 - "title": "scripts.d Zabbix job configuration",
5 - "type": "object",
6 - "additionalProperties": false,
7 - "properties": {
8 - "scheduler": {
9 - "title": "Scheduler name",
10 - "description": "Named scripts.d scheduler to execute this job (defaults to 'default')",
11 - "type": "string"
12 - },
13 - "update_every": {
14 - "title": "Update every",
15 - "description": "Polling interval in seconds (defaults to 60 when omitted)",
16 - "type": "integer",
17 - "minimum": 1
18 - },
19 - "vnode": {
20 - "title": "Virtual node",
21 - "description": "Optional vnode identifier for routing results",
22 - "type": "string"
23 - },
24 - "notes": {
25 - "title": "Notes",
26 - "type": "string"
27 - },
28 - "collection": {
29 - "$ref": "#/definitions/Collection"
30 - },
31 - "lld": {
32 - "$ref": "#/definitions/LLD"
33 - },
34 - "user_macros": {
35 - "title": "User macros",
36 - "description": "Key/value pairs exposed as $USERn$ templates (e.g. key USER1 becomes $USER1$).",
37 - "type": "object",
38 - "additionalProperties": {
39 - "type": "string"
40 - }
41 - },
42 - "dependent_pipelines": {
43 - "title": "Dependent pipelines",
44 - "type": "array",
45 - "minItems": 1,
46 - "items": {
47 - "$ref": "#/definitions/Pipeline"
48 - }
49 - }
50 - },
51 - "required": [
52 - "collection",
53 - "dependent_pipelines"
54 - ],
55 - "definitions": {
56 - "Collection": {
57 - "type": "object",
58 - "description": "Zabbix collection (master item) definition",
59 - "oneOf": [
60 - {
61 - "$ref": "#/definitions/CollectionCommand"
62 - },
63 - {
64 - "$ref": "#/definitions/CollectionHTTP"
65 - },
66 - {
67 - "$ref": "#/definitions/CollectionSNMP"
68 - }
69 - ]
70 - },
71 - "CollectionCommand": {
72 - "type": "object",
73 - "additionalProperties": false,
74 - "properties": {
75 - "type": {
76 - "const": "command"
77 - },
78 - "command": {
79 - "title": "Command",
80 - "type": "string",
81 - "minLength": 1
82 - },
83 - "args": {
84 - "title": "Arguments",
85 - "type": "array",
86 - "items": {
87 - "type": "string"
88 - }
89 - },
90 - "timeout": {
91 - "$ref": "#/definitions/Duration"
92 - },
93 - "environment": {
94 - "title": "Environment variables",
95 - "type": "object",
96 - "additionalProperties": {
97 - "type": "string"
98 - }
99 - }
100 - },
101 - "required": [
102 - "type",
103 - "command"
104 - ]
105 - },
106 - "CollectionHTTP": {
107 - "type": "object",
108 - "additionalProperties": false,
109 - "properties": {
110 - "type": {
111 - "const": "http"
112 - },
113 - "http": {
114 - "$ref": "#/definitions/HTTPConfig"
115 - },
116 - "timeout": {
117 - "$ref": "#/definitions/Duration"
118 - },
119 - "headers": {
120 - "title": "Legacy headers",
121 - "type": "object",
122 - "additionalProperties": {
123 - "type": "string"
124 - }
125 - },
126 - "body": {
127 - "type": "string"
128 - },
129 - "method": {
130 - "type": "string"
131 - },
132 - "url": {
133 - "type": "string"
134 - }
135 - },
136 - "required": [
137 - "type"
138 - ],
139 - "allOf": [
140 - {
141 - "anyOf": [
142 - {
143 - "required": [
144 - "http"
145 - ]
146 - },
147 - {
148 - "required": [
149 - "url"
150 - ]
151 - }
152 - ]
153 - }
154 - ]
155 - },
156 - "CollectionSNMP": {
157 - "type": "object",
158 - "additionalProperties": false,
159 - "properties": {
160 - "type": {
161 - "const": "snmp"
162 - },
163 - "snmp": {
164 - "$ref": "#/definitions/SNMPConfig"
165 - },
166 - "timeout": {
167 - "$ref": "#/definitions/Duration"
168 - }
169 - },
170 - "required": [
171 - "type",
172 - "snmp"
173 - ]
174 - },
175 - "HTTPConfig": {
176 - "type": "object",
177 - "additionalProperties": false,
178 - "properties": {
179 - "url": {
180 - "title": "URL",
181 - "type": "string",
182 - "format": "uri"
183 - },
184 - "method": {
185 - "title": "HTTP method",
186 - "type": "string",
187 - "default": "GET"
188 - },
189 - "headers": {
190 - "title": "Headers",
191 - "type": "object",
192 - "additionalProperties": {
193 - "type": "string"
194 - }
195 - },
196 - "body": {
197 - "type": "string"
198 - },
199 - "tls": {
200 - "title": "Force TLS",
201 - "description": "When enabled, requests are sent via HTTPS (scheme upgraded if necessary).",
202 - "type": "boolean"
203 - },
204 - "username": {
205 - "type": "string"
206 - },
207 - "password": {
208 - "type": "string"
209 - }
210 - },
211 - "required": [
212 - "url"
213 - ]
214 - },
215 - "SNMPConfig": {
216 - "type": "object",
217 - "additionalProperties": false,
218 - "properties": {
219 - "target": {
220 - "title": "Target host",
221 - "type": "string",
222 - "minLength": 1
223 - },
224 - "oid": {
225 - "title": "OID",
226 - "type": "string",
227 - "minLength": 1
228 - },
229 - "version": {
230 - "title": "SNMP version",
231 - "type": "string",
232 - "enum": [
233 - "v1",
234 - "v2c",
235 - "v3",
236 - "1",
237 - "2c",
238 - "3"
239 - ],
240 - "default": "v2c"
241 - },
242 - "community": {
243 - "type": "string"
244 - },
245 - "user": {
246 - "type": "string"
247 - },
248 - "auth_password": {
249 - "type": "string"
250 - },
251 - "privacy_password": {
252 - "type": "string"
253 - },
254 - "auth_protocol": {
255 - "type": "string",
256 - "enum": [
257 - "md5",
258 - "sha",
259 - "",
260 - null
261 - ]
262 - },
263 - "privacy_protocol": {
264 - "type": "string",
265 - "enum": [
266 - "des",
267 - "aes",
268 - "",
269 - null
270 - ]
271 - },
272 - "context": {
273 - "type": "string"
274 - }
275 - },
276 - "required": [
277 - "target",
278 - "oid"
279 - ]
280 - },
281 - "LLD": {
282 - "type": "object",
283 - "additionalProperties": false,
284 - "properties": {
285 - "steps": {
286 - "title": "Discovery steps",
287 - "type": "array",
288 - "minItems": 1,
289 - "items": {
290 - "$ref": "#/definitions/Step"
291 - }
292 - },
293 - "discovery_interval": {
294 - "title": "Discovery interval",
295 - "$ref": "#/definitions/Duration"
296 - },
297 - "instance_id_template": {
298 - "title": "Instance ID template",
299 - "type": "string"
300 - },
301 - "family": {
302 - "title": "Default family",
303 - "type": "string"
304 - },
305 - "labels": {
306 - "title": "Labels",
307 - "type": "object",
308 - "additionalProperties": {
309 - "type": "string"
310 - }
311 - },
312 - "max_missing": {
313 - "title": "Max missing",
314 - "description": "Number of consecutive discovery misses before an instance is removed",
315 - "type": "integer",
316 - "minimum": 0
317 - }
318 - }
319 - },
320 - "Pipeline": {
321 - "type": "object",
322 - "additionalProperties": false,
323 - "properties": {
324 - "name": {
325 - "title": "Pipeline name",
326 - "type": "string",
327 - "minLength": 1
328 - },
329 - "context": {
330 - "title": "Metric context",
331 - "type": "string",
332 - "minLength": 1
333 - },
334 - "title": {
335 - "title": "Chart title",
336 - "type": "string"
337 - },
338 - "dimension": {
339 - "title": "Dimension name",
340 - "type": "string",
341 - "minLength": 1
342 - },
343 - "unit": {
344 - "title": "Units",
345 - "type": "string",
346 - "minLength": 1
347 - },
348 - "family": {
349 - "title": "Chart family",
350 - "type": "string"
351 - },
352 - "chart_type": {
353 - "title": "Chart type",
354 - "type": "string",
355 - "enum": [
356 - "line",
357 - "area",
358 - "stacked"
359 - ],
360 - "default": "line"
361 - },
362 - "data_type": {
363 - "title": "Data type",
364 - "type": "string"
365 - },
366 - "precision": {
367 - "title": "Precision",
368 - "type": "integer",
369 - "minimum": 0,
370 - "default": 0
371 - },
372 - "algorithm": {
373 - "title": "Dimension algorithm",
374 - "type": "string",
375 - "enum": [
376 - "absolute",
377 - "incremental",
378 - "percentage-of-absolute-row",
379 - "percentage-of-incremental-row"
380 - ],
381 - "default": "absolute"
382 - },
383 - "steps": {
384 - "title": "Preprocessing steps",
385 - "type": "array",
386 - "minItems": 1,
387 - "items": {
388 - "$ref": "#/definitions/Step"
389 - }
390 - }
391 - },
392 - "required": [
393 - "name",
394 - "context",
395 - "dimension",
396 - "unit",
397 - "steps"
398 - ]
399 - },
400 - "Step": {
401 - "type": "object",
402 - "additionalProperties": false,
403 - "properties": {
404 - "type": {
405 - "title": "Step type",
406 - "type": "string",
407 - "enum": [
408 - "multiplier",
409 - "rtrim",
410 - "ltrim",
411 - "trim",
412 - "regex_substitution",
413 - "bool_to_decimal",
414 - "octal_to_decimal",
415 - "hex_to_decimal",
416 - "delta_value",
417 - "delta_speed",
418 - "xpath",
419 - "jsonpath",
420 - "validate_range",
421 - "validate_regex",
422 - "validate_not_regex",
423 - "error_field_json",
424 - "error_field_xml",
425 - "error_field_regex",
426 - "throttle_value",
427 - "throttle_timed_value",
428 - "javascript",
429 - "prometheus_pattern",
430 - "prometheus_to_json",
431 - "csv_to_json",
432 - "string_replace",
433 - "validate_not_supported",
434 - "xml_to_json",
435 - "snmp_walk_value",
436 - "snmp_walk_to_json",
437 - "snmp_get_value",
438 - "prometheus_to_json_multi",
439 - "jsonpath_multi",
440 - "snmp_walk_to_json_multi",
441 - "csv_to_json_multi"
442 - ]
443 - },
444 - "params": {
445 - "title": "Parameters",
446 - "type": "string"
447 - },
448 - "error_handler": {
449 - "title": "Error handler",
450 - "type": "string",
451 - "enum": [
452 - "default",
453 - "discard",
454 - "set-value",
455 - "set-error"
456 - ]
457 - },
458 - "error_handler_params": {
459 - "title": "Error handler parameters",
460 - "description": "Value used when error_handler is set to set-value or set-error",
461 - "type": "string"
462 - }
463 - },
464 - "required": [
465 - "type"
466 - ]
467 - },
468 - "Duration": {
469 - "title": "Duration",
470 - "type": "string",
471 - "pattern": "^([0-9]+(\\.[0-9]+)?(ns|us|ms|s|m|h|d))+$"
472 - }
473 - }
474 - },
475 - "uiSchema": {
476 - "uiOptions": {
477 - "fullPage": true
478 - },
479 - "ui:flavour": "tabs",
480 - "ui:options": {
481 - "tabs": [
482 - {
483 - "title": "General",
484 - "fields": [
485 - "scheduler",
486 - "update_every",
487 - "vnode",
488 - "notes"
489 - ]
490 - },
491 - {
492 - "title": "Collection",
493 - "fields": [
494 - "collection"
495 - ]
496 - },
497 - {
498 - "title": "Discovery",
499 - "fields": [
500 - "lld"
501 - ]
502 - },
503 - {
504 - "title": "Metrics Extraction",
505 - "fields": [
506 - "dependent_pipelines"
507 - ]
508 - }
509 - ]
510 - },
511 - "scheduler": {
512 - "ui:placeholder": "default"
513 - },
514 - "update_every": {
515 - "ui:placeholder": "60",
516 - "ui:help": "Seconds between runs. Leave empty to inherit the default interval."
517 - },
518 - "vnode": {
519 - "ui:placeholder": "my-vnode"
520 - },
521 - "collection": {
522 - "ui:help": "Defines how the master item collects raw data. Command collections can use standard Netdata macros as well as legacy Zabbix macros (e.g. {HOST.NAME}). HTTP/SNMP collections expose the same payload to all dependent pipelines.",
523 - "type": {
524 - "ui:widget": "radio",
525 - "ui:options": {
526 - "inline": true
527 - }
528 - },
529 - "command": {
530 - "ui:placeholder": "/usr/lib/zabbix/externalscripts/myscript.sh"
531 - },
532 - "args": {
533 - "ui:listFlavour": "list",
534 - "items": {
535 - "ui:placeholder": "{HOST.NAME}"
536 - },
537 - "ui:help": "Command arguments may use macros such as {HOST.NAME}, {#MACRO}, and positional $ARGn style placeholders."
538 - },
539 - "environment": {
540 - "ui:help": "Optional environment variables exported before executing the collection command."
541 - },
542 - "http": {
543 - "url": {
544 - "ui:placeholder": "https://example/api/status"
545 - },
546 - "body": {
547 - "ui:widget": "textarea",
548 - "ui:options": {
549 - "rows": 3
550 - }
551 - }
552 - },
553 - "snmp": {
554 - "target": {
555 - "ui:placeholder": "192.0.2.10"
556 - },
557 - "oid": {
558 - "ui:placeholder": ".1.3.6.1.2.1.25.2.3.1.6"
559 - },
560 - "community": {
561 - "ui:placeholder": "public"
562 - }
563 - }
564 - },
565 - "lld": {
566 - "ui:help": "Optional discovery pipeline. When defined, each run must output a JSON array of macro maps (e.g. [{\"{#FSNAME}\":\"/\"}]). Leave empty to emit a single default instance.",
567 - "steps": {
568 - "ui:listFlavour": "list",
569 - "items": {
570 - "type": {
571 - "ui:help": "Choose the preprocessing step (e.g. jsonpath, csv_to_json).",
572 - "ui:placeholder": "jsonpath"
573 - },
574 - "params": {
575 - "ui:widget": "textarea",
576 - "ui:help": "Parameters can reference discovery macros (e.g. {#MACRO}) and will be rendered per execution.",
577 - "ui:options": {
578 - "rows": 3
579 - }
580 - },
581 - "error_handler": {
582 - "ui:widget": "radio",
583 - "ui:options": {
584 - "inline": true
585 - }
586 - },
587 - "error_handler_params": {
588 - "ui:help": "Value used when error_handler is set to set-value or set-error"
589 - }
590 - }
591 - },
592 - "instance_id_template": {
593 - "ui:placeholder": "{#FSNAME}"
594 - },
595 - "labels": {
596 - "ui:help": "Key/value templates that become Netdata labels (e.g. region: {#REGION})."
597 - }
598 - },
599 - "dependent_pipelines": {
600 - "ui:listFlavour": "list",
601 - "ui:openEmptyItem": true,
602 - "ui:help": "Each pipeline turns the shared collection payload into a Netdata metric. Use macros ({#MACRO}, {ITEM.VALUE}) to tailor chart metadata and preprocessing steps per instance.",
603 - "items": {
604 - "ui:flavour": "tabs",
605 - "ui:options": {
606 - "tabs": [
607 - {
608 - "title": "Metadata",
609 - "fields": [
610 - "name",
611 - "context",
612 - "title",
613 - "dimension",
614 - "unit",
615 - "family",
616 - "chart_type",
617 - "data_type",
618 - "precision",
619 - "algorithm"
620 - ]
621 - },
622 - {
623 - "title": "Steps",
624 - "fields": [
625 - "steps"
626 - ]
627 - }
628 - ]
629 - },
630 - "context": {
631 - "ui:placeholder": "zabbix.fs.free"
632 - },
633 - "dimension": {
634 - "ui:placeholder": "free"
635 - },
636 - "unit": {
637 - "ui:placeholder": "MiB"
638 - },
639 - "steps": {
640 - "ui:listFlavour": "list",
641 - "items": {
642 - "type": {
643 - "ui:help": "Select the preprocessing step using the string enum (e.g. jsonpath, csv_to_json, snmp_walk_to_json).",
644 - "ui:placeholder": "jsonpath"
645 - },
646 - "params": {
647 - "ui:widget": "textarea",
648 - "ui:help": "Step parameters. Macro substitution ({#MACRO}, {ITEM.VALUE}, {ITEM.VALUE1}) occurs before execution.",
649 - "ui:options": {
650 - "rows": 3
651 - }
652 - },
653 - "error_handler": {
654 - "ui:widget": "radio",
655 - "ui:options": {
656 - "inline": true
657 - }
658 - }
659 - }
660 - }
661 - }
662 - }
663 - }
664 -}
\ No newline at end of file
src/go/plugin/scripts.d/modules/zabbix/emitter.go deleted
-623
@@ -1,623 +0,0 @@
1 -// SPDX-License-Identifier: GPL-3.0-or-later
2 -
3 -package zabbix
4 -
5 -import (
6 - "fmt"
7 - "math"
8 - "sort"
9 - "strings"
10 - "sync"
11 -
12 - "github.com/netdata/netdata/go/plugins/logger"
13 - "github.com/netdata/netdata/go/plugins/plugin/go.d/agent/module"
14 - "github.com/netdata/netdata/go/plugins/plugin/scripts.d/pkg/ids"
15 - "github.com/netdata/netdata/go/plugins/plugin/scripts.d/pkg/runtime"
16 - pkgzabbix "github.com/netdata/netdata/go/plugins/plugin/scripts.d/pkg/zabbix"
17 - "github.com/netdata/netdata/go/plugins/plugin/scripts.d/pkg/zabbix/jobengine"
18 - zpre "github.com/netdata/netdata/go/plugins/plugin/scripts.d/pkg/zabbixpreproc"
19 -)
20 -
21 -type pipelineEmitter struct {
22 - log *logger.Logger
23 - charts *module.Charts
24 -
25 - mu sync.Mutex
26 - jobs map[string]*jobState
27 - metrics map[string]int64
28 -}
29 -
30 -type jobState struct {
31 - cfg pkgzabbix.JobConfig
32 - engine *jobengine.Job
33 - instances map[string]*instanceBinding
34 - stateBinding *stateChartBinding
35 -}
36 -
37 -type instanceBinding struct {
38 - id string
39 - macros map[string]string
40 - charts map[string]*pipelineBinding
41 - state *stateChartBinding
42 -}
43 -
44 -type pipelineBinding struct {
45 - chart *module.Chart
46 - dims map[string]string
47 -}
48 -
49 -type metricUpdate struct {
50 - dimID string
51 - value int64
52 -}
53 -
54 -type stateChartBinding struct {
55 - chartID string
56 - dims map[string]string
57 -}
58 -
59 -var stateDimensions = []string{"collect_failure", "lld_failure", "extraction_failure", "dimension_failure", "ok"}
60 -
61 -func newPipelineEmitter(log *logger.Logger, proc *zpre.Preprocessor, charts *module.Charts, jobs []pkgzabbix.JobConfig) (*pipelineEmitter, error) {
62 - states := make(map[string]*jobState, len(jobs))
63 - for _, cfg := range jobs {
64 - engine, err := jobengine.NewJob(cfg, proc, jobengine.Options{Logger: log})
65 - if err != nil {
66 - return nil, err
67 - }
68 - states[cfg.Name] = &jobState{
69 - cfg: cfg,
70 - engine: engine,
71 - instances: make(map[string]*instanceBinding),
72 - }
73 - }
74 - return &pipelineEmitter{
75 - log: log,
76 - charts: charts,
77 - jobs: states,
78 - metrics: make(map[string]int64),
79 - }, nil
80 -}
81 -
82 -func (e *pipelineEmitter) Emit(job runtime.JobRuntime, res runtime.ExecutionResult, snap runtime.JobSnapshot) {
83 - state := e.getState(job.Spec.Name)
84 - if state == nil {
85 - return
86 - }
87 - input := jobengine.Input{Payload: res.Output, Timestamp: res.End}
88 - if res.Err != nil || res.ExitCode != 0 {
89 - if input.Payload == nil {
90 - input.Payload = []byte{}
91 - }
92 - input.CollectError = fmt.Errorf("collect failed: %w", res.Err)
93 - }
94 - result := state.engine.Process(input)
95 - metricUpdates, stateUpdates := e.prepareUpdates(state.cfg.Name, result)
96 - for _, upd := range metricUpdates {
97 - e.setMetric(upd.dimID, upd.value)
98 - }
99 - for _, upd := range stateUpdates {
100 - e.setMetric(upd.dimID, upd.value)
101 - }
102 -}
103 -
104 -func (e *pipelineEmitter) prepareUpdates(jobName string, result jobengine.Result) ([]metricUpdate, []metricUpdate) {
105 - e.mu.Lock()
106 - defer e.mu.Unlock()
107 - state, ok := e.jobs[jobName]
108 - if !ok {
109 - return nil, nil
110 - }
111 - for _, inst := range result.Removed {
112 - if binding, ok := state.instances[inst.ID]; ok {
113 - e.removeInstanceLocked(state.cfg, binding)
114 - delete(state.instances, inst.ID)
115 - }
116 - }
117 - for _, info := range result.Active {
118 - binding := e.ensureInstanceBindingLocked(state, info)
119 - binding.macros = cloneMacros(info.Macros)
120 - }
121 - metricUpdates := e.buildMetricUpdatesLocked(state, result.Metrics)
122 - stateUpdates := e.buildStateUpdatesLocked(state, result.State)
123 - return metricUpdates, stateUpdates
124 -}
125 -
126 -func (e *pipelineEmitter) ensureInstanceBindingLocked(state *jobState, info jobengine.InstanceInfo) *instanceBinding {
127 - binding, ok := state.instances[info.ID]
128 - if !ok {
129 - binding = &instanceBinding{id: info.ID, macros: make(map[string]string), charts: make(map[string]*pipelineBinding)}
130 - state.instances[info.ID] = binding
131 - }
132 - if binding.charts == nil {
133 - binding.charts = make(map[string]*pipelineBinding)
134 - }
135 - return binding
136 -}
137 -
138 -func (e *pipelineEmitter) buildMetricUpdatesLocked(state *jobState, metrics []jobengine.MetricResult) []metricUpdate {
139 - updates := make([]metricUpdate, 0, len(metrics))
140 - for _, mr := range metrics {
141 - inst := state.instances[mr.Instance.ID]
142 - if inst == nil {
143 - continue
144 - }
145 - binding, err := e.ensurePipelineChartLocked(state.cfg, inst, mr.Pipeline, inst.macros)
146 - if err != nil {
147 - e.logWarn("chart registration failed", state.cfg.Name, mr.Pipeline.Name, err)
148 - continue
149 - }
150 - baseName := dimensionBaseName(mr.Pipeline, inst.macros)
151 - key := dimensionKey(baseName, mr)
152 - dimensionID, ok := binding.dims[key]
153 - if !ok {
154 - chart := binding.chart
155 - if chart == nil {
156 - continue
157 - }
158 - dimName := dimensionDisplayName(baseName, mr)
159 - newDim := &module.Dim{ID: dimID(chart.ID, ids.Sanitize(key)), Name: dimName, Algo: dimAlgorithm(mr.Pipeline.Algorithm)}
160 - if err := chart.AddDim(newDim); err != nil {
161 - e.logWarn("dimension registration failed", state.cfg.Name, mr.Pipeline.Name, err)
162 - continue
163 - }
164 - dimensionID = newDim.ID
165 - binding.dims[key] = dimensionID
166 - }
167 - updates = append(updates, metricUpdate{dimID: dimensionID, value: scaleValue(mr.Value, mr.Pipeline.Precision)})
168 - }
169 - return updates
170 -}
171 -
172 -func (e *pipelineEmitter) buildStateUpdatesLocked(state *jobState, summary jobengine.StateSummary) []metricUpdate {
173 - var updates []metricUpdate
174 - binding, err := e.ensureJobStateChart(state)
175 - if err != nil {
176 - e.logWarn("state chart registration failed", state.cfg.Name, "job_state", err)
177 - } else {
178 - updates = append(updates, stateMetricUpdates(binding, summary.Job)...)
179 - }
180 - for id, inst := range state.instances {
181 - flags := summary.Instances[id]
182 - ib, err := e.ensureInstanceStateChart(state.cfg, inst)
183 - if err != nil {
184 - e.logWarn("state chart registration failed", state.cfg.Name, id, err)
185 - continue
186 - }
187 - updates = append(updates, stateMetricUpdates(ib, flags)...)
188 - }
189 - return updates
190 -}
191 -
192 -func stateMetricUpdates(binding *stateChartBinding, flags jobengine.FailureFlags) []metricUpdate {
193 - updates := make([]metricUpdate, 0, len(stateDimensions))
194 - updates = append(updates,
195 - metricUpdate{dimID: binding.dims["collect_failure"], value: boolToInt(flags.Collect)},
196 - metricUpdate{dimID: binding.dims["lld_failure"], value: boolToInt(flags.LLD)},
197 - metricUpdate{dimID: binding.dims["extraction_failure"], value: boolToInt(flags.Extraction)},
198 - metricUpdate{dimID: binding.dims["dimension_failure"], value: boolToInt(flags.Dimension)},
199 - metricUpdate{dimID: binding.dims["ok"], value: boolToInt(!flags.Collect && !flags.LLD && !flags.Extraction && !flags.Dimension)},
200 - )
201 - return updates
202 -}
203 -
204 -func (e *pipelineEmitter) Close() error {
205 - e.mu.Lock()
206 - defer e.mu.Unlock()
207 - for name := range e.jobs {
208 - e.removeJobLocked(name)
209 - }
210 - return nil
211 -}
212 -
213 -func (e *pipelineEmitter) RemoveJob(name string) {
214 - e.mu.Lock()
215 - defer e.mu.Unlock()
216 - e.removeJobLocked(name)
217 -}
218 -
219 -func (e *pipelineEmitter) removeJobLocked(name string) {
220 - state, ok := e.jobs[name]
221 - if !ok {
222 - return
223 - }
224 - if state.stateBinding != nil {
225 - _ = e.charts.Remove(state.stateBinding.chartID)
226 - state.stateBinding = nil
227 - }
228 - for _, inst := range state.instances {
229 - e.removeInstanceLocked(state.cfg, inst)
230 - }
231 - state.engine.Destroy()
232 - delete(e.jobs, name)
233 -}
234 -
235 -func (e *pipelineEmitter) Flush() map[string]int64 {
236 - e.mu.Lock()
237 - defer e.mu.Unlock()
238 - if len(e.metrics) == 0 {
239 - return nil
240 - }
241 - out := make(map[string]int64, len(e.metrics))
242 - for k, v := range e.metrics {
243 - out[k] = v
244 - }
245 - e.metrics = make(map[string]int64)
246 - return out
247 -}
248 -
249 -func (e *pipelineEmitter) setMetric(dimID string, value int64) {
250 - e.mu.Lock()
251 - defer e.mu.Unlock()
252 - e.metrics[dimID] = value
253 -}
254 -
255 -func (e *pipelineEmitter) getState(name string) *jobState {
256 - e.mu.Lock()
257 - defer e.mu.Unlock()
258 - return e.jobs[name]
259 -}
260 -
261 -func buildChart(job pkgzabbix.JobConfig, inst *instanceBinding, pipe *pkgzabbix.PipelineConfig, macros map[string]string) *module.Chart {
262 - title := "Zabbix Pipeline Data"
263 - units := expandTemplate(pipe.Unit, macros)
264 - if units == "" {
265 - units = "value"
266 - }
267 - fam := expandTemplate(pipe.Family, macros)
268 - if fam == "" {
269 - fam = expandTemplate(job.LLD.Family, macros)
270 - }
271 - if fam == "" {
272 - fam = job.Name
273 - }
274 - ctx := expandTemplate(pipe.Context, macros)
275 - if ctx == "" {
276 - ctx = pipe.Name
277 - }
278 - sanitizedCtx := ids.Sanitize(ctx)
279 - if sanitizedCtx == "" {
280 - sanitizedCtx = ids.Sanitize(pipe.Name)
281 - }
282 - dimName := expandTemplate(pipe.Dimension, macros)
283 - if dimName == "" {
284 - dimName = pipe.Name
285 - }
286 - chartIdentifier := chartID(job, inst.id, sanitizedCtx)
287 - dimensionID := dimID(chartIdentifier, dimName)
288 - chart := &module.Chart{
289 - ID: chartIdentifier,
290 - Title: title,
291 - Units: units,
292 - Fam: fam,
293 - Ctx: fmt.Sprintf("zabbix.%s", sanitizedCtx),
294 - Type: chartType(pipe.ChartType),
295 -
296 - Priority: 1000,
297 - Labels: buildLabels(job, inst, pipe, macros),
298 - Dims: module.Dims{
299 - {
300 - ID: dimensionID,
301 - Name: dimName,
302 - Algo: dimAlgorithm(pipe.Algorithm),
303 - },
304 - },
305 - }
306 - return chart
307 -}
308 -
309 -func buildJobStateChart(job pkgzabbix.JobConfig) *module.Chart {
310 - chartID := jobStateChartID(job)
311 - dims, _ := buildStateChartDims(chartID)
312 - return &module.Chart{
313 - ID: chartID,
314 - Title: "Zabbix Job State",
315 - Units: "state",
316 - Fam: job.Name,
317 - Ctx: fmt.Sprintf("zabbix.%s.state", ids.Sanitize(job.Name)),
318 - Type: module.Line,
319 -
320 - Priority: 800,
321 - Labels: jobLabels(job),
322 - Dims: dims,
323 - Opts: module.Opts{
324 - Detail: true,
325 - },
326 - }
327 -}
328 -
329 -func buildInstanceStateChart(job pkgzabbix.JobConfig, inst *instanceBinding) *module.Chart {
330 - chartID := instanceStateChartID(job, inst)
331 - macros := cloneMacros(inst.macros)
332 - dims, _ := buildStateChartDims(chartID)
333 - return &module.Chart{
334 - ID: chartID,
335 - Title: "Zabbix Instance State",
336 - Units: "state",
337 - Fam: job.Name,
338 - Ctx: fmt.Sprintf("zabbix.%s.instance.state", ids.Sanitize(job.Name)),
339 - Type: module.Line,
340 -
341 - Priority: 850,
342 - Labels: buildLabels(job, inst, nil, macros),
343 - Dims: dims,
344 - Opts: module.Opts{
345 - Detail: true,
346 - },
347 - }
348 -}
349 -
350 -func (e *pipelineEmitter) ensureJobStateChart(state *jobState) (*stateChartBinding, error) {
351 - if state.stateBinding != nil {
352 - return state.stateBinding, nil
353 - }
354 - chart := buildJobStateChart(state.cfg)
355 - if err := e.charts.Add(chart); err != nil {
356 - return nil, err
357 - }
358 - bind := newStateBinding(chart)
359 - state.stateBinding = bind
360 - return bind, nil
361 -}
362 -
363 -func (e *pipelineEmitter) ensureInstanceStateChart(job pkgzabbix.JobConfig, inst *instanceBinding) (*stateChartBinding, error) {
364 - if inst.state != nil {
365 - return inst.state, nil
366 - }
367 - chart := buildInstanceStateChart(job, inst)
368 - if err := e.charts.Add(chart); err != nil {
369 - return nil, err
370 - }
371 - bind := newStateBinding(chart)
372 - inst.state = bind
373 - return bind, nil
374 -}
375 -
376 -func newStateBinding(chart *module.Chart) *stateChartBinding {
377 - bind := &stateChartBinding{chartID: chart.ID, dims: make(map[string]string, len(stateDimensions))}
378 - for idx, name := range stateDimensions {
379 - if idx < len(chart.Dims) && chart.Dims[idx] != nil {
380 - bind.dims[name] = chart.Dims[idx].ID
381 - }
382 - }
383 - return bind
384 -}
385 -
386 -func (e *pipelineEmitter) removeInstanceLocked(cfg pkgzabbix.JobConfig, inst *instanceBinding) {
387 - if inst == nil {
388 - return
389 - }
390 - for _, binding := range inst.charts {
391 - if binding != nil && binding.chart != nil {
392 - _ = e.charts.Remove(binding.chart.ID)
393 - }
394 - }
395 - if inst.state != nil {
396 - _ = e.charts.Remove(inst.state.chartID)
397 - inst.state = nil
398 - }
399 -}
400 -
401 -func (e *pipelineEmitter) ensurePipelineChartLocked(job pkgzabbix.JobConfig, inst *instanceBinding, pipe *pkgzabbix.PipelineConfig, macros map[string]string) (*pipelineBinding, error) {
402 - binding := inst.charts[pipe.Name]
403 - if binding != nil {
404 - return binding, nil
405 - }
406 - chart := buildChart(job, inst, pipe, macros)
407 - if err := e.charts.Add(chart); err != nil {
408 - return nil, err
409 - }
410 - binding = &pipelineBinding{chart: chart, dims: make(map[string]string)}
411 - base := dimensionBaseName(pipe, macros)
412 - if len(chart.Dims) > 0 && chart.Dims[0] != nil {
413 - binding.dims[dimensionKey(base, jobengine.MetricResult{})] = chart.Dims[0].ID
414 - }
415 - inst.charts[pipe.Name] = binding
416 - return binding, nil
417 -}
418 -
419 -func buildStateChartDims(chartID string) (module.Dims, map[string]string) {
420 - dims := make(module.Dims, 0, len(stateDimensions))
421 - idMap := make(map[string]string, len(stateDimensions))
422 - for _, name := range stateDimensions {
423 - dimID := fmt.Sprintf("%s.%s", chartID, ids.Sanitize(name))
424 - dims = append(dims, &module.Dim{ID: dimID, Name: name, Algo: module.Absolute})
425 - idMap[name] = dimID
426 - }
427 - return dims, idMap
428 -}
429 -
430 -func jobStateChartID(job pkgzabbix.JobConfig) string {
431 - return fmt.Sprintf("zabbix.%s.job.state", ids.Sanitize(job.Name))
432 -}
433 -
434 -func instanceStateChartID(job pkgzabbix.JobConfig, inst *instanceBinding) string {
435 - return fmt.Sprintf("zabbix.%s.%s.state", ids.Sanitize(job.Name), inst.id)
436 -}
437 -
438 -func buildLabels(job pkgzabbix.JobConfig, inst *instanceBinding, pipe *pkgzabbix.PipelineConfig, macros map[string]string) []module.Label {
439 - labels := make([]module.Label, 0, len(job.LLD.Labels)+5)
440 - labels = append(labels, jobLabels(job)...)
441 - for key, tmpl := range job.LLD.Labels {
442 - val := expandTemplate(tmpl, macros)
443 - if val == "" {
444 - continue
445 - }
446 - labels = append(labels, module.Label{Key: key, Value: val, Source: module.LabelSourceConf})
447 - }
448 - instanceLabel := inst.id
449 - if macroID := macros["{#INSTANCE_ID}"]; macroID != "" {
450 - instanceLabel = macroID
451 - }
452 - labels = append(labels,
453 - module.Label{Key: "instance", Value: inst.id, Source: module.LabelSourceConf},
454 - module.Label{Key: "zabbix_instance", Value: instanceLabel, Source: module.LabelSourceConf},
455 - )
456 - if pipe != nil && pipe.Name != "" {
457 - labels = append(labels, module.Label{Key: "zabbix_pipeline", Value: pipe.Name, Source: module.LabelSourceConf})
458 - }
459 - return labels
460 -}
461 -
462 -func jobLabels(job pkgzabbix.JobConfig) []module.Label {
463 - labels := []module.Label{
464 - {Key: "zabbix_job", Value: job.Name, Source: module.LabelSourceConf},
465 - }
466 - scheduler := strings.TrimSpace(job.Scheduler)
467 - if scheduler != "" {
468 - labels = append(labels, module.Label{Key: "zabbix_scheduler", Value: scheduler, Source: module.LabelSourceConf})
469 - }
470 - if job.Vnode != "" {
471 - labels = append(labels, module.Label{Key: "zabbix_vnode", Value: job.Vnode, Source: module.LabelSourceConf})
472 - }
473 - collection := strings.TrimSpace(string(job.Collection.Type))
474 - if collection != "" {
475 - labels = append(labels, module.Label{Key: "zabbix_collection", Value: collection, Source: module.LabelSourceConf})
476 - }
477 - return labels
478 -}
479 -
480 -func chartID(job pkgzabbix.JobConfig, instanceID, ctx string) string {
481 - return fmt.Sprintf("zabbix.%s.%s.%s", ids.Sanitize(job.Name), instanceID, ctx)
482 -}
483 -
484 -func dimID(chartID, dimension string) string {
485 - return fmt.Sprintf("%s.%s", chartID, ids.Sanitize(dimension))
486 -}
487 -
488 -func dimensionBaseName(pipe *pkgzabbix.PipelineConfig, macros map[string]string) string {
489 - name := expandTemplate(pipe.Dimension, macros)
490 - if strings.TrimSpace(name) == "" {
491 - name = pipe.Name
492 - }
493 - if strings.TrimSpace(name) == "" {
494 - name = "value"
495 - }
496 - return name
497 -}
498 -
499 -func dimensionKey(base string, metric jobengine.MetricResult) string {
500 - parts := []string{base}
501 - if metric.Name != "" {
502 - parts = append(parts, metric.Name)
503 - }
504 - if len(metric.Labels) > 0 {
505 - keys := make([]string, 0, len(metric.Labels))
506 - for k := range metric.Labels {
507 - keys = append(keys, k)
508 - }
509 - sort.Strings(keys)
510 - for _, k := range keys {
511 - parts = append(parts, fmt.Sprintf("%s=%s", k, metric.Labels[k]))
512 - }
513 - }
514 - return strings.Join(parts, "|")
515 -}
516 -
517 -func dimensionDisplayName(base string, metric jobengine.MetricResult) string {
518 - suffix := []string{}
519 - if metric.Name != "" {
520 - suffix = append(suffix, metric.Name)
521 - }
522 - if len(metric.Labels) > 0 {
523 - keys := make([]string, 0, len(metric.Labels))
524 - for k := range metric.Labels {
525 - keys = append(keys, k)
526 - }
527 - sort.Strings(keys)
528 - pairs := make([]string, 0, len(keys))
529 - for _, k := range keys {
530 - pairs = append(pairs, fmt.Sprintf("%s=%s", k, metric.Labels[k]))
531 - }
532 - suffix = append(suffix, strings.Join(pairs, ","))
533 - }
534 - if len(suffix) == 0 {
535 - return base
536 - }
537 - return fmt.Sprintf("%s (%s)", base, strings.Join(suffix, " | "))
538 -}
539 -
540 -func expandTemplate(tmpl string, macros map[string]string) string {
541 - if tmpl == "" || len(macros) == 0 {
542 - return tmpl
543 - }
544 - res := tmpl
545 - for k, v := range macros {
546 - res = strings.ReplaceAll(res, k, v)
547 - }
548 - return res
549 -}
550 -
551 -func buildInstanceID(cfg pkgzabbix.JobConfig, macros map[string]string) string {
552 - template := cfg.LLD.InstanceTemplate
553 - if strings.TrimSpace(template) == "" {
554 - if val, ok := macros["{#INSTANCE_ID}"]; ok {
555 - template = val
556 - } else {
557 - template = fmt.Sprintf("%s_instance", cfg.Name)
558 - }
559 - }
560 - raw := expandTemplate(template, macros)
561 - if strings.TrimSpace(raw) == "" {
562 - raw = fmt.Sprintf("%s_instance", cfg.Name)
563 - }
564 - return ids.Sanitize(raw)
565 -}
566 -
567 -func cloneMacros(src map[string]string) map[string]string {
568 - out := make(map[string]string, len(src))
569 - for k, v := range src {
570 - out[k] = v
571 - }
572 - return out
573 -}
574 -
575 -func itemID(cfg pkgzabbix.JobConfig, instanceID, pipe string) string {
576 - return fmt.Sprintf("%s.%s.%s", cfg.Name, instanceID, pipe)
577 -}
578 -
579 -func scaleValue(val float64, precision int) int64 {
580 - if precision < 0 {
581 - precision = 0
582 - }
583 - factor := math.Pow10(precision)
584 - return int64(math.Round(val * factor))
585 -}
586 -
587 -func dimAlgorithm(algo string) module.DimAlgo {
588 - switch strings.ToLower(algo) {
589 - case "incremental":
590 - return module.Incremental
591 - case "percentage", "percentage-of-absolute-row":
592 - return module.PercentOfAbsolute
593 - case "percentage-of-incremental-row":
594 - return module.PercentOfIncremental
595 - default:
596 - return module.Absolute
597 - }
598 -}
599 -
600 -func chartType(kind string) module.ChartType {
601 - switch strings.ToLower(kind) {
602 - case "area":
603 - return module.Area
604 - case "stacked":
605 - return module.Stacked
606 - default:
607 - return module.Line
608 - }
609 -}
610 -
611 -func (e *pipelineEmitter) logWarn(msg, job, pipe string, err error) {
612 - if e.log == nil {
613 - return
614 - }
615 - e.log.Warningf("zabbix %s job=%s pipeline=%s error=%v", msg, job, pipe, err)
616 -}
617 -
618 -func boolToInt(b bool) int64 {
619 - if b {
620 - return 1
621 - }
622 - return 0
623 -}
src/go/plugin/scripts.d/modules/zabbix/emitter_test.go deleted
-138
@@ -1,138 +0,0 @@
1 -package zabbix
2 -
3 -import (
4 - "errors"
5 - "fmt"
6 - "testing"
7 - "time"
8 -
9 - "github.com/netdata/netdata/go/plugins/plugin/go.d/agent/module"
10 - "github.com/netdata/netdata/go/plugins/plugin/scripts.d/pkg/ids"
11 - "github.com/netdata/netdata/go/plugins/plugin/scripts.d/pkg/runtime"
12 - "github.com/netdata/netdata/go/plugins/plugin/scripts.d/pkg/spec"
13 - pkgzabbix "github.com/netdata/netdata/go/plugins/plugin/scripts.d/pkg/zabbix"
14 - zpre "github.com/netdata/netdata/go/plugins/plugin/scripts.d/pkg/zabbixpreproc"
15 -)
16 -
17 -func TestBuildStateChartDims(t *testing.T) {
18 - dims, ids := buildStateChartDims("zabbix.job.state")
19 - if len(dims) != len(stateDimensions) {
20 - t.Fatalf("expected %d dims, got %d", len(stateDimensions), len(dims))
21 - }
22 - for _, name := range stateDimensions {
23 - id, ok := ids[name]
24 - if !ok || id == "" {
25 - t.Fatalf("missing dim mapping for %s", name)
26 - }
27 - }
28 -}
29 -
30 -func TestJobStateChart(t *testing.T) {
31 - chart := buildJobStateChart(pkgzabbix.JobConfig{Name: "fs_usage"})
32 - if chart == nil || chart.ID == "" {
33 - t.Fatalf("expected job state chart")
34 - }
35 - if len(chart.Dims) != len(stateDimensions) {
36 - t.Fatalf("expected %d dims, got %d", len(stateDimensions), len(chart.Dims))
37 - }
38 - if len(chart.Labels) == 0 {
39 - t.Fatalf("expected labels on job state chart")
40 - }
41 -}
42 -
43 -func TestInstanceStateChart(t *testing.T) {
44 - inst := &instanceBinding{id: "instance1", macros: map[string]string{"{#INSTANCE_ID}": "instance1"}}
45 - chart := buildInstanceStateChart(pkgzabbix.JobConfig{Name: "fs_usage"}, inst)
46 - if chart == nil || chart.ID == "" {
47 - t.Fatalf("expected instance state chart")
48 - }
49 - if len(chart.Dims) != len(stateDimensions) {
50 - t.Fatalf("expected %d dims, got %d", len(stateDimensions), len(chart.Dims))
51 - }
52 - if len(chart.Labels) == 0 {
53 - t.Fatalf("expected labels on instance state chart")
54 - }
55 -}
56 -
57 -func TestPipelineEmitterCollectFailureState(t *testing.T) {
58 - job := testJobConfig()
59 - emitter := newTestEmitter(job)
60 - jr := runtime.JobRuntime{ID: "job1", Spec: spec.JobSpec{Name: job.Name}}
61 - res := runtime.ExecutionResult{
62 - Job: jr,
63 - Err: errors.New("boom"),
64 - ExitCode: 2,
65 - End: time.Now(),
66 - }
67 - emitter.Emit(jr, res, runtime.JobSnapshot{})
68 - metrics := emitter.Flush()
69 - assertState(t, metrics, job, "collect_failure", 1)
70 - assertState(t, metrics, job, "ok", 0)
71 - assertInstanceState(t, metrics, job, "default", "collect_failure", 1)
72 - assertInstanceState(t, metrics, job, "default", "ok", 0)
73 -}
74 -
75 -func TestPipelineEmitterExtractionFailureState(t *testing.T) {
76 - job := testJobConfig()
77 - emitter := newTestEmitter(job)
78 - jr := runtime.JobRuntime{ID: "job2", Spec: spec.JobSpec{Name: job.Name}}
79 - res := runtime.ExecutionResult{
80 - Job: jr,
81 - Output: []byte("not-json"),
82 - End: time.Now(),
83 - }
84 - emitter.Emit(jr, res, runtime.JobSnapshot{})
85 - metrics := emitter.Flush()
86 - assertState(t, metrics, job, "extraction_failure", 1)
87 - assertState(t, metrics, job, "ok", 0)
88 - assertInstanceState(t, metrics, job, "default", "extraction_failure", 1)
89 - assertInstanceState(t, metrics, job, "default", "ok", 0)
90 -}
91 -
92 -func newTestEmitter(job pkgzabbix.JobConfig) *pipelineEmitter {
93 - charts := &module.Charts{}
94 - proc := zpre.NewPreprocessor("test-shard")
95 - emitter, err := newPipelineEmitter(nil, proc, charts, []pkgzabbix.JobConfig{job})
96 - if err != nil {
97 - panic(err)
98 - }
99 - return emitter
100 -}
101 -
102 -func testJobConfig() pkgzabbix.JobConfig {
103 - return pkgzabbix.JobConfig{
104 - Name: "fs_usage",
105 - Collection: pkgzabbix.CollectionConfig{
106 - Type: pkgzabbix.CollectionCommand,
107 - Command: "/usr/bin/true",
108 - },
109 - Pipelines: []pkgzabbix.PipelineConfig{
110 - {
111 - Name: "value",
112 - Context: "zabbix.fs_usage.value",
113 - Dimension: "value",
114 - Unit: "value",
115 - Steps: []zpre.Step{
116 - {Type: zpre.StepTypeJSONPath, Params: "$.value"},
117 - },
118 - },
119 - },
120 - }
121 -}
122 -
123 -func assertState(t *testing.T, metrics map[string]int64, job pkgzabbix.JobConfig, dim string, want int64) {
124 - chartID := jobStateChartID(job)
125 - key := fmt.Sprintf("%s.%s", chartID, ids.Sanitize(dim))
126 - if got := metrics[key]; got != want {
127 - t.Fatalf("job state %s expected %d, got %d", dim, want, got)
128 - }
129 -}
130 -
131 -func assertInstanceState(t *testing.T, metrics map[string]int64, job pkgzabbix.JobConfig, instID, dim string, want int64) {
132 - inst := &instanceBinding{id: instID}
133 - chartID := instanceStateChartID(job, inst)
134 - key := fmt.Sprintf("%s.%s", chartID, ids.Sanitize(dim))
135 - if got := metrics[key]; got != want {
136 - t.Fatalf("instance %s state %s expected %d, got %d", instID, dim, want, got)
137 - }
138 -}
src/go/plugin/scripts.d/modules/zabbix/integrations/zabbix_preprocessing.md deleted
-236
@@ -1,236 +0,0 @@
1 -<!--startmeta
2 -custom_edit_url: "https://github.com/netdata/netdata/edit/master/src/go/plugin/scripts.d/modules/zabbix/README.md"
3 -meta_yaml: "https://github.com/netdata/netdata/edit/master/src/go/plugin/scripts.d/modules/zabbix/metadata.yaml"
4 -sidebar_label: "Zabbix Preprocessing"
5 -learn_status: "Published"
6 -learn_rel_path: "Collecting Metrics/Applications"
7 -most_popular: False
8 -keywords: ['zabbix', 'preprocessing', 'scripts', 'monitoring']
9 -message: "DO NOT EDIT THIS FILE DIRECTLY, IT IS GENERATED BY THE COLLECTOR'S metadata.yaml FILE"
10 -endmeta-->
11 -
12 -# Zabbix Preprocessing
13 -
14 -
15 -<img src="https://netdata.cloud/img/zabbix.svg" width="150"/>
16 -
17 -
18 -Plugin: scripts.d.plugin
19 -Module: zabbix
20 -
21 -<img src="https://img.shields.io/badge/maintained%20by-Netdata-%2300ab44" />
22 -
23 -## Overview
24 -
25 -This module runs Zabbix-style data collection jobs natively inside Netdata.
26 -
27 -It supports Zabbix's master-item + dependent-pipeline pattern: a single collection step
28 -(command, HTTP, or SNMP) produces raw data, and one or more **preprocessing pipelines**
29 -extract individual metrics using Zabbix-compatible preprocessing steps (JSONPath, regex,
30 -JavaScript, SNMP walk, Prometheus parsing, CSV, XPath, and more).
31 -
32 -For each configured job it collects:
33 -
34 -- **User-defined metrics**: Each dependent pipeline produces a charted metric with configurable context, unit, family, chart type, and dimension algorithm.
35 -- **Job state**: OK / WARNING / ERROR / UNKNOWN state tracking per job and per discovered instance.
36 -- **Low-level discovery (LLD)**: Optional discovery pipelines that dynamically create instances from JSON arrays, similar to Zabbix LLD.
37 -
38 -
39 -Collection supports three modes:
40 -
41 -- **Command**: Runs an external script/binary via `nd-run` and captures stdout.
42 -- **HTTP**: Performs an HTTP request and captures the response body.
43 -- **SNMP**: Queries an SNMP agent (GET or WALK) and captures the result.
44 -
45 -The raw output is then processed through Zabbix-compatible preprocessing steps to extract metrics.
46 -Zabbix macros (`{HOST.NAME}`, `{HOST.IP}`, `{#MACRO}`, etc.) are expanded before execution.
47 -
48 -
49 -This collector is supported on all platforms.
50 -
51 -This collector supports collecting metrics from multiple instances of this integration, including remote instances.
52 -
53 -Command-mode plugins run as the `netdata` user via `nd-run`.
54 -
55 -
56 -### Default Behavior
57 -
58 -#### Auto-Detection
59 -
60 -No auto-detection. Each job must be explicitly configured with a `collection` definition and one or more `dependent_pipelines`.
61 -
62 -
63 -#### Limits
64 -
65 -The default configuration for this integration does not impose any limits on data collection.
66 -
67 -#### Performance Impact
68 -
69 -Command-mode jobs spawn a subprocess per execution. HTTP and SNMP modes use in-process clients.
70 -
71 -
72 -## Metrics
73 -
74 -Metrics grouped by *scope*.
75 -
76 -The scope defines the instance that the metric belongs to. An instance is uniquely identified by a set of labels.
77 -
78 -### Virtual Node Label Conventions
79 -
80 -When a job references a `vnode`, the module reads Zabbix host macros from the virtual node's **labels** using prefix conventions:
81 -
82 -| Label key | Zabbix macro | Description |
83 -|-----------|-------------|-------------|
84 -| `_address` | `{HOST.IP}`, `{HOST.CONN}` | IP address or DNS name of the host |
85 -| `_alias` | `{HOST.ALIAS}` | Human-readable host alias |
86 -| Other `_` prefixed | N/A | Reserved for future use |
87 -
88 -The `{HOST.NAME}`, `{HOST.HOST}`, and `{HOST.DNS}` macros are derived from the vnode hostname.
89 -
90 -
91 -### Per pipeline
92 -
93 -Metrics produced by each dependent preprocessing pipeline.
94 -
95 -Labels:
96 -
97 -| Label | Description |
98 -|:-----------|:----------------|
99 -| zabbix_job | Job name. |
100 -| zabbix_pipeline | Pipeline name. |
101 -
102 -Metrics:
103 -
104 -| Metric | Dimensions | Unit |
105 -|:------|:----------|:----|
106 -| zabbix.{context} | {dimension} | varies |
107 -
108 -### Per job
109 -
110 -Per-job instance state tracking.
111 -
112 -Labels:
113 -
114 -| Label | Description |
115 -|:-----------|:----------------|
116 -| zabbix_job | Job name. |
117 -
118 -Metrics:
119 -
120 -| Metric | Dimensions | Unit |
121 -|:------|:----------|:----|
122 -| zabbix.{job}.state | ok, collect_failure, lld_failure, extraction_failure, dimension_failure | state |
123 -
124 -
125 -
126 -## Alerts
127 -
128 -There are no alerts configured by default for this integration.
129 -
130 -
131 -## Setup
132 -
133 -
134 -### Prerequisites
135 -
136 -No action required.
137 -
138 -### Configuration
139 -
140 -#### Options
141 -
142 -Each job defines a collection source and one or more dependent preprocessing pipelines.
143 -
144 -
145 -<details open><summary>Config options</summary>
146 -
147 -
148 -
149 -| Group | Option | Description | Default | Required |
150 -|:------|:-----|:------------|:--------|:---------:|
151 -| **Collection** | collection.type | Collection mode (`command`, `http`, or `snmp`). | | yes |
152 -| | collection.command | Command to execute (for `command` type). | | no |
153 -| | collection.http.url | URL to fetch (for `http` type). | | no |
154 -| | collection.snmp.target | SNMP target host (for `snmp` type). | | no |
155 -| | collection.snmp.oid | SNMP OID to query. | | no |
156 -| **Pipelines** | dependent_pipelines[].name | Pipeline name (used for chart identification). | | yes |
157 -| | dependent_pipelines[].context | Netdata chart context. | | yes |
158 -| | dependent_pipelines[].dimension | Dimension name within the chart. | | yes |
159 -| | dependent_pipelines[].unit | Metric unit string. | | yes |
160 -| | dependent_pipelines[].steps | Array of Zabbix preprocessing steps applied to the raw collection output. | [] | yes |
161 -| **General** | vnode | Virtual node name for host macro resolution. | | no |
162 -| **Discovery** | lld | Low-level discovery configuration for dynamic instance creation. | | no |
163 -
164 -
165 -</details>
166 -
167 -
168 -
169 -#### via File
170 -
171 -The configuration file name for this integration is `scripts.d/zabbix.conf`.
172 -
173 -
174 -You can edit the configuration file using the [`edit-config`](https://github.com/netdata/netdata/blob/master/docs/netdata-agent/configuration/README.md#edit-configuration-files) script from the
175 -Netdata [config directory](https://github.com/netdata/netdata/blob/master/docs/netdata-agent/configuration/README.md#locate-your-config-directory).
176 -
177 -```bash
178 -cd /etc/netdata 2>/dev/null || cd /opt/netdata/etc/netdata
179 -sudo ./edit-config scripts.d/zabbix.conf
180 -```
181 -
182 -##### Examples
183 -
184 -###### Command collection with JSONPath extraction
185 -
186 -Run a script and extract a metric using JSONPath.
187 -
188 -<details open><summary>Config</summary>
189 -
190 -```yaml
191 -jobs:
192 - - name: api_latency
193 - collection:
194 - type: command
195 - command: /usr/local/bin/get_api_stats.sh
196 - dependent_pipelines:
197 - - name: latency
198 - context: myapp.api.latency
199 - dimension: p99
200 - unit: milliseconds
201 - steps:
202 - - type: jsonpath
203 - params: "$.latency.p99"
204 -
205 -```
206 -</details>
207 -
208 -###### SNMP collection
209 -
210 -Query an SNMP OID and chart the result.
211 -
212 -<details open><summary>Config</summary>
213 -
214 -```yaml
215 -jobs:
216 - - name: disk_usage
217 - vnode: my-switch
218 - collection:
219 - type: snmp
220 - snmp:
221 - target: "{HOST.IP}"
222 - oid: ".1.3.6.1.2.1.25.2.3.1.6"
223 - version: v2c
224 - community: public
225 - dependent_pipelines:
226 - - name: used
227 - context: zabbix.disk.used
228 - dimension: used
229 - unit: bytes
230 - steps:
231 - - type: snmp_get_value
232 -
233 -```
234 -</details>
235 -
236 -
src/go/plugin/scripts.d/modules/zabbix/metadata.yaml deleted
-229
@@ -1,229 +0,0 @@
1 -plugin_name: scripts.d.plugin
2 -modules:
3 - - meta:
4 - id: collector-scripts.d.plugin-zabbix
5 - plugin_name: scripts.d.plugin
6 - module_name: zabbix
7 - monitored_instance:
8 - name: Zabbix Preprocessing
9 - link: https://www.zabbix.com/
10 - icon_filename: zabbix.svg
11 - categories:
12 - - data-collection.applications
13 - related_resources:
14 - integrations:
15 - list: []
16 - info_provided_to_referring_integrations:
17 - description: ""
18 - keywords:
19 - - zabbix
20 - - preprocessing
21 - - scripts
22 - - monitoring
23 - most_popular: false
24 - overview:
25 - data_collection:
26 - metrics_description: |
27 - This module runs Zabbix-style data collection jobs natively inside Netdata.
28 -
29 - It supports Zabbix's master-item + dependent-pipeline pattern: a single collection step
30 - (command, HTTP, or SNMP) produces raw data, and one or more **preprocessing pipelines**
31 - extract individual metrics using Zabbix-compatible preprocessing steps (JSONPath, regex,
32 - JavaScript, SNMP walk, Prometheus parsing, CSV, XPath, and more).
33 -
34 - For each configured job it collects:
35 -
36 - - **User-defined metrics**: Each dependent pipeline produces a charted metric with configurable context, unit, family, chart type, and dimension algorithm.
37 - - **Job state**: OK / WARNING / ERROR / UNKNOWN state tracking per job and per discovered instance.
38 - - **Low-level discovery (LLD)**: Optional discovery pipelines that dynamically create instances from JSON arrays, similar to Zabbix LLD.
39 - method_description: |
40 - Collection supports three modes:
41 -
42 - - **Command**: Runs an external script/binary via `nd-run` and captures stdout.
43 - - **HTTP**: Performs an HTTP request and captures the response body.
44 - - **SNMP**: Queries an SNMP agent (GET or WALK) and captures the result.
45 -
46 - The raw output is then processed through Zabbix-compatible preprocessing steps to extract metrics.
47 - Zabbix macros (`{HOST.NAME}`, `{HOST.IP}`, `{#MACRO}`, etc.) are expanded before execution.
48 - default_behavior:
49 - auto_detection:
50 - description: |
51 - No auto-detection. Each job must be explicitly configured with a `collection` definition and one or more `dependent_pipelines`.
52 - limits:
53 - description: ""
54 - performance_impact:
55 - description: |
56 - Command-mode jobs spawn a subprocess per execution. HTTP and SNMP modes use in-process clients.
57 - additional_permissions:
58 - description: |
59 - Command-mode plugins run as the `netdata` user via `nd-run`.
60 - multi_instance: true
61 - supported_platforms:
62 - include: []
63 - exclude: []
64 - setup:
65 - prerequisites:
66 - list: []
67 - configuration:
68 - file:
69 - name: scripts.d/zabbix.conf
70 - options:
71 - description: |
72 - Each job defines a collection source and one or more dependent preprocessing pipelines.
73 - folding:
74 - title: Config options
75 - enabled: true
76 - list:
77 - - name: collection.type
78 - description: Collection mode (`command`, `http`, or `snmp`).
79 - default_value: ""
80 - required: true
81 - group: Collection
82 - - name: collection.command
83 - description: Command to execute (for `command` type).
84 - default_value: ""
85 - required: false
86 - group: Collection
87 - - name: collection.http.url
88 - description: URL to fetch (for `http` type).
89 - default_value: ""
90 - required: false
91 - group: Collection
92 - - name: collection.snmp.target
93 - description: SNMP target host (for `snmp` type).
94 - default_value: ""
95 - required: false
96 - group: Collection
97 - - name: collection.snmp.oid
98 - description: SNMP OID to query.
99 - default_value: ""
100 - required: false
101 - group: Collection
102 - - name: dependent_pipelines[].name
103 - description: Pipeline name (used for chart identification).
104 - default_value: ""
105 - required: true
106 - group: Pipelines
107 - - name: dependent_pipelines[].context
108 - description: Netdata chart context.
109 - default_value: ""
110 - required: true
111 - group: Pipelines
112 - - name: dependent_pipelines[].dimension
113 - description: Dimension name within the chart.
114 - default_value: ""
115 - required: true
116 - group: Pipelines
117 - - name: dependent_pipelines[].unit
118 - description: Metric unit string.
119 - default_value: ""
120 - required: true
121 - group: Pipelines
122 - - name: dependent_pipelines[].steps
123 - description: Array of Zabbix preprocessing steps applied to the raw collection output.
124 - default_value: "[]"
125 - required: true
126 - group: Pipelines
127 - - name: vnode
128 - description: Virtual node name for host macro resolution.
129 - default_value: ""
130 - required: false
131 - group: General
132 - - name: lld
133 - description: Low-level discovery configuration for dynamic instance creation.
134 - default_value: ""
135 - required: false
136 - group: Discovery
137 - examples:
138 - folding:
139 - title: Config
140 - enabled: true
141 - list:
142 - - name: Command collection with JSONPath extraction
143 - description: Run a script and extract a metric using JSONPath.
144 - config: |
145 - jobs:
146 - - name: api_latency
147 - collection:
148 - type: command
149 - command: /usr/local/bin/get_api_stats.sh
150 - dependent_pipelines:
151 - - name: latency
152 - context: myapp.api.latency
153 - dimension: p99
154 - unit: milliseconds
155 - steps:
156 - - type: jsonpath
157 - params: "$.latency.p99"
158 - - name: SNMP collection
159 - description: Query an SNMP OID and chart the result.
160 - config: |
161 - jobs:
162 - - name: disk_usage
163 - vnode: my-switch
164 - collection:
165 - type: snmp
166 - snmp:
167 - target: "{HOST.IP}"
168 - oid: ".1.3.6.1.2.1.25.2.3.1.6"
169 - version: v2c
170 - community: public
171 - dependent_pipelines:
172 - - name: used
173 - context: zabbix.disk.used
174 - dimension: used
175 - unit: bytes
176 - steps:
177 - - type: snmp_get_value
178 - troubleshooting:
179 - problems:
180 - list: []
181 - alerts: []
182 - metrics:
183 - folding:
184 - title: Metrics
185 - enabled: false
186 - description: |
187 - ### Virtual Node Label Conventions
188 -
189 - When a job references a `vnode`, the module reads Zabbix host macros from the virtual node's **labels** using prefix conventions:
190 -
191 - | Label key | Zabbix macro | Description |
192 - |-----------|-------------|-------------|
193 - | `_address` | `{HOST.IP}`, `{HOST.CONN}` | IP address or DNS name of the host |
194 - | `_alias` | `{HOST.ALIAS}` | Human-readable host alias |
195 - | Other `_` prefixed | N/A | Reserved for future use |
196 -
197 - The `{HOST.NAME}`, `{HOST.HOST}`, and `{HOST.DNS}` macros are derived from the vnode hostname.
198 - availability: []
199 - scopes:
200 - - name: pipeline
201 - description: Metrics produced by each dependent preprocessing pipeline.
202 - labels:
203 - - name: zabbix_job
204 - description: Job name.
205 - - name: zabbix_pipeline
206 - description: Pipeline name.
207 - metrics:
208 - - name: zabbix.{context}
209 - description: User-defined metric (context is configured per pipeline)
210 - unit: varies
211 - chart_type: line
212 - dimensions:
213 - - name: "{dimension}"
214 - - name: job
215 - description: Per-job instance state tracking.
216 - labels:
217 - - name: zabbix_job
218 - description: Job name.
219 - metrics:
220 - - name: zabbix.{job}.state
221 - description: Zabbix job instance state
222 - unit: state
223 - chart_type: line
224 - dimensions:
225 - - name: ok
226 - - name: collect_failure
227 - - name: lld_failure
228 - - name: extraction_failure
229 - - name: dimension_failure
src/go/plugin/scripts.d/modules/zabbix/module.go deleted
-287
@@ -1,287 +0,0 @@
1 -// SPDX-License-Identifier: GPL-3.0-or-later
2 -
3 -package zabbix
4 -
5 -import (
6 - "context"
7 - _ "embed"
8 - "fmt"
9 - "maps"
10 - "strings"
11 - "sync"
12 -
13 - "github.com/netdata/netdata/go/plugins/pkg/multipath"
14 - "github.com/netdata/netdata/go/plugins/pkg/pluginconfig"
15 - "github.com/netdata/netdata/go/plugins/plugin/go.d/agent/module"
16 - "github.com/netdata/netdata/go/plugins/plugin/go.d/agent/vnodes"
17 - "github.com/netdata/netdata/go/plugins/plugin/scripts.d/pkg/runtime"
18 - "github.com/netdata/netdata/go/plugins/plugin/scripts.d/pkg/spec"
19 - "github.com/netdata/netdata/go/plugins/plugin/scripts.d/pkg/zabbix"
20 - zpre "github.com/netdata/netdata/go/plugins/plugin/scripts.d/pkg/zabbixpreproc"
21 -)
22 -
23 -//go:embed config_schema.json
24 -var configSchema string
25 -
26 -func init() {
27 - module.Register("zabbix", module.Creator{
28 - JobConfigSchema: configSchema,
29 - Defaults: module.Defaults{AutoDetectionRetry: 60},
30 - Create: func() module.Module { return New() },
31 - Config: func() any { return &Config{} },
32 - })
33 -}
34 -
35 -// Config collects Zabbix jobs defined in a module configuration file.
36 -type Config struct {
37 - zabbix.JobConfig `yaml:",inline" json:",inline"`
38 - Jobs []zabbix.JobConfig `yaml:"jobs,omitempty" json:"jobs,omitempty"`
39 -}
40 -
41 -// Collector is the placeholder module.
42 -type Collector struct {
43 - module.Base
44 - Config `yaml:",inline" json:",inline"`
45 - jobs []zabbix.JobConfig
46 - proc *zpre.Preprocessor
47 - runner *runner
48 - emitter *pipelineEmitter
49 - charts *module.Charts
50 - vnodeInfo map[string]runtime.VnodeInfo
51 - missingVnode map[string]struct{}
52 -
53 - currentVnode *vnodes.VirtualNode
54 - vnodeMu sync.RWMutex
55 -}
56 -
57 -func New() *Collector { return &Collector{charts: &module.Charts{}} }
58 -
59 -func (c *Collector) Configuration() any { return &c.Config }
60 -
61 -func (c *Collector) Init(ctx context.Context) error {
62 - jobs, err := c.materializeJobs()
63 - if err != nil {
64 - return err
65 - }
66 - if c.charts == nil {
67 - c.charts = &module.Charts{}
68 - }
69 - c.jobs = jobs
70 - c.refreshVnodeInfo()
71 - for i := range c.jobs {
72 - if name := strings.TrimSpace(c.jobs[i].Vnode); name != "" {
73 - if info, ok := c.lookupVnode(name); !ok {
74 - return fmt.Errorf("job '%s': vnode '%s' not found", c.jobs[i].Name, name)
75 - } else {
76 - c.setCurrentVnode(info)
77 - }
78 - }
79 - }
80 - c.proc = acquirePreprocessor()
81 - for i := range c.jobs {
82 - c.jobs[i].Scheduler = schedulerName(c.jobs[i].Scheduler)
83 - }
84 - emitter, err := newPipelineEmitter(c.Logger, c.proc, c.charts, c.jobs)
85 - if err != nil {
86 - return err
87 - }
88 - c.emitter = emitter
89 - run, err := newRunner(ctx, c.Logger, c.jobs, c.proc, c.emitter, c.vnodeLookup)
90 - if err != nil {
91 - return err
92 - }
93 - c.runner = run
94 - return nil
95 -}
96 -
97 -func (c *Collector) materializeJobs() ([]zabbix.JobConfig, error) {
98 - var src []zabbix.JobConfig
99 - if len(c.Jobs) > 0 {
100 - src = c.Jobs
101 - } else {
102 - src = []zabbix.JobConfig{c.JobConfig}
103 - }
104 - seen := make(map[string]struct{}, len(src))
105 - jobs := make([]zabbix.JobConfig, len(src))
106 - for i := range src {
107 - job := src[i]
108 - if err := job.Validate(); err != nil {
109 - return nil, err
110 - }
111 - key := strings.ToLower(strings.TrimSpace(job.Name))
112 - if key == "" {
113 - return nil, fmt.Errorf("job name is required")
114 - }
115 - if _, exists := seen[key]; exists {
116 - return nil, fmt.Errorf("duplicate job name '%s'", job.Name)
117 - }
118 - seen[key] = struct{}{}
119 - jobs[i] = job
120 - }
121 - return jobs, nil
122 -}
123 -
124 -func (c *Collector) Check(context.Context) error { return nil }
125 -func (c *Collector) Charts() *module.Charts {
126 - return c.charts
127 -}
128 -func (c *Collector) Collect(context.Context) map[string]int64 {
129 - metrics := make(map[string]int64)
130 - if c.runner != nil {
131 - for k, v := range c.runner.Collect() {
132 - metrics[k] += v
133 - }
134 - }
135 - if c.emitter != nil {
136 - for k, v := range c.emitter.Flush() {
137 - metrics[k] = v
138 - }
139 - }
140 - if len(metrics) == 0 {
141 - return nil
142 - }
143 - return metrics
144 -}
145 -
146 -func (c *Collector) Cleanup(context.Context) {
147 - if c.runner != nil {
148 - c.runner.Stop()
149 - }
150 - if c.emitter != nil {
151 - _ = c.emitter.Close()
152 - }
153 -}
154 -
155 -func schedulerName(name string) string {
156 - if strings.TrimSpace(name) == "" {
157 - return "default"
158 - }
159 - return name
160 -}
161 -
162 -func (c *Collector) vnodeLookup(sp spec.JobSpec) runtime.VnodeInfo {
163 - if info, ok := c.lookupVnode(sp.Vnode); ok {
164 - c.setCurrentVnode(info)
165 - return cloneVnodeInfo(info)
166 - }
167 - if strings.TrimSpace(sp.Vnode) != "" {
168 - c.warnMissingVnode(sp.Vnode)
169 - }
170 - return runtime.VnodeInfo{Hostname: sp.Vnode}
171 -}
172 -
173 -func (c *Collector) lookupVnode(name string) (runtime.VnodeInfo, bool) {
174 - if c.vnodeInfo == nil {
175 - return runtime.VnodeInfo{}, false
176 - }
177 - info, ok := c.vnodeInfo[strings.ToLower(strings.TrimSpace(name))]
178 - return cloneVnodeInfo(info), ok
179 -}
180 -
181 -func (c *Collector) refreshVnodeInfo() {
182 - configDirs := pluginconfig.ConfigDir()
183 - if len(configDirs) == 0 {
184 - c.vnodeInfo = nil
185 - c.missingVnode = nil
186 - return
187 - }
188 - path, err := configDirs.Find("vnodes")
189 - if err != nil {
190 - if !multipath.IsNotFound(err) {
191 - c.Warningf("zabbix: failed to locate vnodes directory: %v", err)
192 - }
193 - c.vnodeInfo = nil
194 - c.missingVnode = nil
195 - return
196 - }
197 - registry := vnodes.Load(path)
198 - if len(registry) == 0 {
199 - c.vnodeInfo = nil
200 - c.missingVnode = nil
201 - return
202 - }
203 - info := make(map[string]runtime.VnodeInfo, len(registry)*3)
204 - for key, vnode := range registry {
205 - if vnode == nil {
206 - continue
207 - }
208 - converted := runtime.VnodeInfo{
209 - Hostname: firstNonEmpty(vnode.Hostname, vnode.Name, key),
210 - Labels: maps.Clone(vnode.Labels),
211 - }
212 - if converted.Labels == nil {
213 - converted.Labels = make(map[string]string)
214 - }
215 - if _, ok := converted.Labels["_alias"]; !ok {
216 - converted.Labels["_alias"] = firstNonEmpty(vnode.Name, key)
217 - }
218 - if _, ok := converted.Labels["_address"]; !ok {
219 - if v := vnode.Labels["_net_default_iface_ip"]; v != "" {
220 - converted.Labels["_address"] = v
221 - }
222 - }
223 - for _, alias := range []string{key, vnode.Hostname, vnode.Name, vnode.GUID} {
224 - if strings.TrimSpace(alias) == "" {
225 - continue
226 - }
227 - info[strings.ToLower(alias)] = cloneVnodeInfo(converted)
228 - }
229 - }
230 - c.vnodeInfo = info
231 - c.missingVnode = make(map[string]struct{})
232 -}
233 -
234 -func (c *Collector) setCurrentVnode(info runtime.VnodeInfo) {
235 - node := &vnodes.VirtualNode{
236 - Name: info.Hostname,
237 - Hostname: info.Hostname,
238 - Labels: maps.Clone(info.Labels),
239 - }
240 - c.vnodeMu.Lock()
241 - c.currentVnode = node
242 - c.vnodeMu.Unlock()
243 -}
244 -
245 -func (c *Collector) VirtualNode() *vnodes.VirtualNode {
246 - c.vnodeMu.RLock()
247 - defer c.vnodeMu.RUnlock()
248 - if c.currentVnode == nil {
249 - return nil
250 - }
251 - return c.currentVnode.Copy()
252 -}
253 -
254 -func (c *Collector) warnMissingVnode(name string) {
255 - name = strings.TrimSpace(name)
256 - if name == "" {
257 - return
258 - }
259 - if c.missingVnode == nil {
260 - c.missingVnode = make(map[string]struct{})
261 - }
262 - key := strings.ToLower(name)
263 - if _, seen := c.missingVnode[key]; seen {
264 - return
265 - }
266 - c.missingVnode[key] = struct{}{}
267 - c.Warningf("zabbix: vnode '%s' not found; macros fall back to literal hostname", name)
268 -}
269 -
270 -func cloneVnodeInfo(src runtime.VnodeInfo) runtime.VnodeInfo {
271 - clone := src
272 - if len(src.Labels) > 0 {
273 - clone.Labels = maps.Clone(src.Labels)
274 - } else {
275 - clone.Labels = nil
276 - }
277 - return clone
278 -}
279 -
280 -func firstNonEmpty(values ...string) string {
281 - for _, v := range values {
282 - if strings.TrimSpace(v) != "" {
283 - return v
284 - }
285 - }
286 - return ""
287 -}
src/go/plugin/scripts.d/modules/zabbix/preprocessor.go deleted
-35
@@ -1,35 +0,0 @@
1 -// SPDX-License-Identifier: GPL-3.0-or-later
2 -
3 -package zabbix
4 -
5 -import (
6 - "fmt"
7 - "os"
8 - "strings"
9 - "sync"
10 - "time"
11 -
12 - zpre "github.com/netdata/netdata/go/plugins/plugin/scripts.d/pkg/zabbixpreproc"
13 -)
14 -
15 -var (
16 - preprocOnce sync.Once
17 - sharedProc *zpre.Preprocessor
18 -)
19 -
20 -func acquirePreprocessor() *zpre.Preprocessor {
21 - preprocOnce.Do(func() {
22 - ns := buildPreprocessorNamespace()
23 - sharedProc = zpre.NewPreprocessor(ns)
24 - })
25 - return sharedProc
26 -}
27 -
28 -func buildPreprocessorNamespace() string {
29 - if host, err := os.Hostname(); err == nil {
30 - if trimmed := strings.TrimSpace(host); trimmed != "" {
31 - return fmt.Sprintf("zabbix-%s", trimmed)
32 - }
33 - }
34 - return fmt.Sprintf("zabbix-%d", time.Now().UnixNano())
35 -}
src/go/plugin/scripts.d/modules/zabbix/runtime.go deleted
-41
@@ -1,41 +0,0 @@
1 -// SPDX-License-Identifier: GPL-3.0-or-later
2 -
3 -package zabbix
4 -
5 -import (
6 - "context"
7 -
8 - "github.com/netdata/netdata/go/plugins/logger"
9 - "github.com/netdata/netdata/go/plugins/plugin/scripts.d/pkg/runtime"
10 - "github.com/netdata/netdata/go/plugins/plugin/scripts.d/pkg/spec"
11 - pkgzabbix "github.com/netdata/netdata/go/plugins/plugin/scripts.d/pkg/zabbix"
12 - zpre "github.com/netdata/netdata/go/plugins/plugin/scripts.d/pkg/zabbixpreproc"
13 -)
14 -
15 -// runner is a thin wrapper that allows the module to delegate lifecycle control
16 -// to the shared pkg/zabbix runtime while keeping module-specific wiring simple.
17 -type runner struct {
18 - rt *pkgzabbix.Runtime
19 -}
20 -
21 -func newRunner(_ context.Context, log *logger.Logger, jobs []pkgzabbix.JobConfig, proc *zpre.Preprocessor, emitter runtime.ResultEmitter, vnodeLookup func(spec.JobSpec) runtime.VnodeInfo) (*runner, error) {
22 - rt, err := pkgzabbix.NewRuntime(jobs, proc, log, emitter, vnodeLookup)
23 - if err != nil {
24 - return nil, err
25 - }
26 - return &runner{rt: rt}, nil
27 -}
28 -
29 -func (r *runner) Collect() map[string]int64 {
30 - if r == nil || r.rt == nil {
31 - return nil
32 - }
33 - return r.rt.Collect()
34 -}
35 -
36 -func (r *runner) Stop() {
37 - if r == nil || r.rt == nil {
38 - return
39 - }
40 - r.rt.Stop()
41 -}
src/go/plugin/scripts.d/pkg/zabbix/config.go deleted
-423
@@ -1,423 +0,0 @@
1 -// SPDX-License-Identifier: GPL-3.0-or-later
2 -
3 -package zabbix
4 -
5 -import (
6 - "encoding/json"
7 - "fmt"
8 - "strconv"
9 - "strings"
10 - "time"
11 -
12 - "github.com/netdata/netdata/go/plugins/pkg/confopt"
13 - zpre "github.com/netdata/netdata/go/plugins/plugin/scripts.d/pkg/zabbixpreproc"
14 -)
15 -
16 -// CollectionType enumerates supported Zabbix collection methods.
17 -type CollectionType string
18 -
19 -const (
20 - CollectionCommand CollectionType = "command"
21 - CollectionHTTP CollectionType = "http"
22 - CollectionSNMP CollectionType = "snmp"
23 -)
24 -
25 -var stepTypeTokens = map[string]zpre.StepType{
26 - "multiplier": zpre.StepTypeMultiplier,
27 - "rtrim": zpre.StepTypeRTrim,
28 - "ltrim": zpre.StepTypeLTrim,
29 - "trim": zpre.StepTypeTrim,
30 - "regex_substitution": zpre.StepTypeRegexSubstitution,
31 - "bool_to_decimal": zpre.StepTypeBool2Dec,
32 - "octal_to_decimal": zpre.StepTypeOct2Dec,
33 - "hex_to_decimal": zpre.StepTypeHex2Dec,
34 - "delta_value": zpre.StepTypeDeltaValue,
35 - "delta_speed": zpre.StepTypeDeltaSpeed,
36 - "xpath": zpre.StepTypeXPath,
37 - "jsonpath": zpre.StepTypeJSONPath,
38 - "validate_range": zpre.StepTypeValidateRange,
39 - "validate_regex": zpre.StepTypeValidateRegex,
40 - "validate_not_regex": zpre.StepTypeValidateNotRegex,
41 - "error_field_json": zpre.StepTypeErrorFieldJSON,
42 - "error_field_xml": zpre.StepTypeErrorFieldXML,
43 - "error_field_regex": zpre.StepTypeErrorFieldRegex,
44 - "throttle_value": zpre.StepTypeThrottleValue,
45 - "throttle_timed_value": zpre.StepTypeThrottleTimedValue,
46 - "javascript": zpre.StepTypeJavaScript,
47 - "prometheus_pattern": zpre.StepTypePrometheusPattern,
48 - "prometheus_to_json": zpre.StepTypePrometheusToJSON,
49 - "csv_to_json": zpre.StepTypeCSVToJSON,
50 - "string_replace": zpre.StepTypeStringReplace,
51 - "validate_not_supported": zpre.StepTypeValidateNotSupported,
52 - "xml_to_json": zpre.StepTypeXMLToJSON,
53 - "snmp_walk_value": zpre.StepTypeSNMPWalkValue,
54 - "snmp_walk_to_json": zpre.StepTypeSNMPWalkToJSON,
55 - "snmp_get_value": zpre.StepTypeSNMPGetValue,
56 - "prometheus_to_json_multi": zpre.StepTypePrometheusToJSONMulti,
57 - "jsonpath_multi": zpre.StepTypeJSONPathMulti,
58 - "snmp_walk_to_json_multi": zpre.StepTypeSNMPWalkToJSONMulti,
59 - "csv_to_json_multi": zpre.StepTypeCSVToJSONMulti,
60 -}
61 -
62 -var errorHandlerTokens = map[string]zpre.ErrorAction{
63 - "": zpre.ErrorActionDefault,
64 - "default": zpre.ErrorActionDefault,
65 - "discard": zpre.ErrorActionDiscard,
66 - "set-value": zpre.ErrorActionSetValue,
67 - "set-error": zpre.ErrorActionSetError,
68 -}
69 -
70 -// CollectionConfig describes how raw data is fetched before preprocessing.
71 -type CollectionConfig struct {
72 - Type CollectionType `yaml:"type" json:"type"`
73 - Command string `yaml:"command,omitempty" json:"command,omitempty"`
74 - Args []string `yaml:"args,omitempty" json:"args,omitempty"`
75 - Timeout confopt.Duration `yaml:"timeout,omitempty" json:"timeout,omitempty"`
76 - Environment map[string]string `yaml:"environment,omitempty" json:"environment,omitempty"`
77 - HTTP HTTPConfig `yaml:"http,omitempty" json:"http,omitempty"`
78 - SNMP SNMPConfig `yaml:"snmp,omitempty" json:"snmp,omitempty"`
79 - Headers map[string]string `yaml:"headers,omitempty" json:"headers,omitempty"` // legacy alias for HTTP headers
80 - Body string `yaml:"body,omitempty" json:"body,omitempty"`
81 - Method string `yaml:"method,omitempty" json:"method,omitempty"` // legacy alias for HTTP method
82 - URL string `yaml:"url,omitempty" json:"url,omitempty"`
83 -}
84 -
85 -// HTTPConfig captures HTTP(S) collection settings.
86 -type HTTPConfig struct {
87 - URL string `yaml:"url" json:"url"`
88 - Method string `yaml:"method,omitempty" json:"method,omitempty"`
89 - Headers map[string]string `yaml:"headers,omitempty" json:"headers,omitempty"`
90 - Body string `yaml:"body,omitempty" json:"body,omitempty"`
91 - TLS bool `yaml:"tls,omitempty" json:"tls,omitempty"`
92 - Username string `yaml:"username,omitempty" json:"username,omitempty"`
93 - Password string `yaml:"password,omitempty" json:"password,omitempty"`
94 -}
95 -
96 -// SNMPConfig captures SNMP collection settings.
97 -type SNMPConfig struct {
98 - Target string `yaml:"target" json:"target"`
99 - Community string `yaml:"community,omitempty" json:"community,omitempty"`
100 - Version string `yaml:"version,omitempty" json:"version,omitempty"`
101 - User string `yaml:"user,omitempty" json:"user,omitempty"`
102 - AuthPass string `yaml:"auth_password,omitempty" json:"auth_password,omitempty"`
103 - PrivPass string `yaml:"privacy_password,omitempty" json:"privacy_password,omitempty"`
104 - AuthProto string `yaml:"auth_protocol,omitempty" json:"auth_protocol,omitempty"`
105 - PrivProto string `yaml:"privacy_protocol,omitempty" json:"privacy_protocol,omitempty"`
106 - Context string `yaml:"context,omitempty" json:"context,omitempty"`
107 - OID string `yaml:"oid" json:"oid"`
108 -}
109 -
110 -// LLDConfig describes the discovery pipeline.
111 -type LLDConfig struct {
112 - Steps []zpre.Step `yaml:"steps" json:"steps"`
113 - Interval confopt.Duration `yaml:"discovery_interval,omitempty" json:"discovery_interval,omitempty"`
114 - InstanceTemplate string `yaml:"instance_id_template" json:"instance_id_template"`
115 - Family string `yaml:"family" json:"family"`
116 - Labels map[string]string `yaml:"labels,omitempty" json:"labels,omitempty"`
117 - MaxMissing int `yaml:"max_missing,omitempty" json:"max_missing,omitempty"`
118 -}
119 -
120 -type lldConfigDTO struct {
121 - Steps []rawStep `yaml:"steps" json:"steps"`
122 - Interval confopt.Duration `yaml:"discovery_interval,omitempty" json:"discovery_interval,omitempty"`
123 - InstanceTemplate string `yaml:"instance_id_template" json:"instance_id_template"`
124 - Family string `yaml:"family" json:"family"`
125 - Labels map[string]string `yaml:"labels,omitempty" json:"labels,omitempty"`
126 - MaxMissing int `yaml:"max_missing,omitempty" json:"max_missing,omitempty"`
127 -}
128 -
129 -func (l *LLDConfig) fromDTO(dto lldConfigDTO) error {
130 - steps, err := decodeSteps(dto.Steps)
131 - if err != nil {
132 - return err
133 - }
134 - l.Steps = steps
135 - l.Interval = dto.Interval
136 - l.InstanceTemplate = dto.InstanceTemplate
137 - l.Family = dto.Family
138 - l.Labels = dto.Labels
139 - l.MaxMissing = dto.MaxMissing
140 - return nil
141 -}
142 -
143 -func (l *LLDConfig) UnmarshalYAML(unmarshal func(any) error) error {
144 - var dto lldConfigDTO
145 - if err := unmarshal(&dto); err != nil {
146 - return err
147 - }
148 - return l.fromDTO(dto)
149 -}
150 -
151 -func (l *LLDConfig) UnmarshalJSON(data []byte) error {
152 - var dto lldConfigDTO
153 - if err := json.Unmarshal(data, &dto); err != nil {
154 - return err
155 - }
156 - return l.fromDTO(dto)
157 -}
158 -
159 -// PipelineConfig defines a dependent pipeline that produces a single metric per instance.
160 -type PipelineConfig struct {
161 - Name string `yaml:"name" json:"name"`
162 - Context string `yaml:"context" json:"context"`
163 - Title string `yaml:"title,omitempty" json:"title,omitempty"`
164 - Dimension string `yaml:"dimension" json:"dimension"`
165 - Unit string `yaml:"unit" json:"unit"`
166 - Family string `yaml:"family,omitempty" json:"family,omitempty"`
167 - ChartType string `yaml:"chart_type,omitempty" json:"chart_type,omitempty"`
168 - DataType string `yaml:"data_type,omitempty" json:"data_type,omitempty"`
169 - Precision int `yaml:"precision,omitempty" json:"precision,omitempty"`
170 - Algorithm string `yaml:"algorithm,omitempty" json:"algorithm,omitempty"`
171 - Steps []zpre.Step `yaml:"steps" json:"steps"`
172 -}
173 -
174 -type pipelineConfigDTO struct {
175 - Name string `yaml:"name" json:"name"`
176 - Context string `yaml:"context" json:"context"`
177 - Title string `yaml:"title,omitempty" json:"title,omitempty"`
178 - Dimension string `yaml:"dimension" json:"dimension"`
179 - Unit string `yaml:"unit" json:"unit"`
180 - Family string `yaml:"family,omitempty" json:"family,omitempty"`
181 - ChartType string `yaml:"chart_type,omitempty" json:"chart_type,omitempty"`
182 - DataType string `yaml:"data_type,omitempty" json:"data_type,omitempty"`
183 - Precision int `yaml:"precision,omitempty" json:"precision,omitempty"`
184 - Algorithm string `yaml:"algorithm,omitempty" json:"algorithm,omitempty"`
185 - Steps []rawStep `yaml:"steps" json:"steps"`
186 -}
187 -
188 -func (pc *PipelineConfig) fromDTO(dto pipelineConfigDTO) error {
189 - pc.Name = dto.Name
190 - pc.Context = dto.Context
191 - pc.Title = dto.Title
192 - pc.Dimension = dto.Dimension
193 - pc.Unit = dto.Unit
194 - pc.Family = dto.Family
195 - pc.ChartType = dto.ChartType
196 - pc.DataType = dto.DataType
197 - pc.Precision = dto.Precision
198 - pc.Algorithm = dto.Algorithm
199 - steps, err := decodeSteps(dto.Steps)
200 - if err != nil {
201 - return err
202 - }
203 - pc.Steps = steps
204 - return nil
205 -}
206 -
207 -func (pc *PipelineConfig) UnmarshalYAML(unmarshal func(any) error) error {
208 - var dto pipelineConfigDTO
209 - if err := unmarshal(&dto); err != nil {
210 - return err
211 - }
212 - return pc.fromDTO(dto)
213 -}
214 -
215 -func (pc *PipelineConfig) UnmarshalJSON(data []byte) error {
216 - var dto pipelineConfigDTO
217 - if err := json.Unmarshal(data, &dto); err != nil {
218 - return err
219 - }
220 - return pc.fromDTO(dto)
221 -}
222 -
223 -// JobConfig represents the full user configuration for a Zabbix job.
224 -type JobConfig struct {
225 - Name string `yaml:"name" json:"name"`
226 - Scheduler string `yaml:"scheduler,omitempty" json:"scheduler,omitempty"`
227 - Vnode string `yaml:"vnode,omitempty" json:"vnode,omitempty"`
228 - UpdateEvery int `yaml:"update_every,omitempty" json:"update_every,omitempty"`
229 - UserMacros map[string]string `yaml:"user_macros,omitempty" json:"user_macros,omitempty"`
230 - Collection CollectionConfig `yaml:"collection" json:"collection"`
231 - LLD LLDConfig `yaml:"lld,omitempty" json:"lld,omitempty"`
232 - Pipelines []PipelineConfig `yaml:"dependent_pipelines" json:"dependent_pipelines"`
233 - Notes string `yaml:"notes,omitempty" json:"notes,omitempty"`
234 -}
235 -
236 -const defaultUpdateEverySeconds = 60
237 -
238 -type rawStep struct {
239 - Type any `yaml:"type" json:"type"`
240 - Params string `yaml:"params,omitempty" json:"params,omitempty"`
241 - ErrorHandler string `yaml:"error_handler,omitempty" json:"error_handler,omitempty"`
242 - ErrorHandlerValue string `yaml:"error_handler_params,omitempty" json:"error_handler_params,omitempty"`
243 -}
244 -
245 -func decodeSteps(raw []rawStep) ([]zpre.Step, error) {
246 - if len(raw) == 0 {
247 - return nil, nil
248 - }
249 - steps := make([]zpre.Step, len(raw))
250 - for i, r := range raw {
251 - step, err := r.toZabbixStep()
252 - if err != nil {
253 - return nil, fmt.Errorf("step[%d]: %w", i, err)
254 - }
255 - steps[i] = step
256 - }
257 - return steps, nil
258 -}
259 -
260 -func (rs rawStep) toZabbixStep() (zpre.Step, error) {
261 - st, err := parseStepType(rs.Type)
262 - if err != nil {
263 - return zpre.Step{}, err
264 - }
265 - handler, err := parseErrorHandler(rs.ErrorHandler, rs.ErrorHandlerValue)
266 - if err != nil {
267 - return zpre.Step{}, err
268 - }
269 - return zpre.Step{
270 - Type: st,
271 - Params: rs.Params,
272 - ErrorHandler: handler,
273 - }, nil
274 -}
275 -
276 -func parseStepType(value any) (zpre.StepType, error) {
277 - switch v := value.(type) {
278 - case string:
279 - key := strings.ToLower(strings.TrimSpace(v))
280 - if key == "" {
281 - return 0, fmt.Errorf("step type is required")
282 - }
283 - if st, ok := stepTypeTokens[key]; ok {
284 - return st, nil
285 - }
286 - if n, err := strconv.Atoi(key); err == nil {
287 - return zpre.StepType(n), nil
288 - }
289 - return 0, fmt.Errorf("unknown step type %q", v)
290 - case int:
291 - return zpre.StepType(v), nil
292 - case int64:
293 - return zpre.StepType(v), nil
294 - case float64:
295 - return zpre.StepType(int(v)), nil
296 - case json.Number:
297 - i, err := v.Int64()
298 - if err != nil {
299 - return 0, err
300 - }
301 - return zpre.StepType(i), nil
302 - case nil:
303 - return 0, fmt.Errorf("step type is required")
304 - default:
305 - return 0, fmt.Errorf("unsupported step type %T", v)
306 - }
307 -}
308 -
309 -func parseErrorHandler(action, params string) (zpre.ErrorHandler, error) {
310 - action = strings.ToLower(strings.TrimSpace(action))
311 - act, ok := errorHandlerTokens[action]
312 - if !ok {
313 - return zpre.ErrorHandler{}, fmt.Errorf("unknown error handler %q", action)
314 - }
315 - switch act {
316 - case zpre.ErrorActionSetValue, zpre.ErrorActionSetError:
317 - if strings.TrimSpace(params) == "" {
318 - return zpre.ErrorHandler{}, fmt.Errorf("error_handler_params required when error_handler=%q", action)
319 - }
320 - return zpre.ErrorHandler{Action: act, Params: params}, nil
321 - default:
322 - return zpre.ErrorHandler{Action: act}, nil
323 - }
324 -}
325 -
326 -// Validate ensures the job definition is usable.
327 -func (cfg *JobConfig) Validate() error {
328 - if strings.TrimSpace(cfg.Name) == "" {
329 - return fmt.Errorf("job name is required")
330 - }
331 - if strings.TrimSpace(cfg.Collection.Type.String()) == "" {
332 - return fmt.Errorf("job '%s': collection.type is required", cfg.Name)
333 - }
334 - if err := cfg.Collection.validate(); err != nil {
335 - return fmt.Errorf("job '%s': %w", cfg.Name, err)
336 - }
337 - if len(cfg.Pipelines) == 0 {
338 - return fmt.Errorf("job '%s': at least one dependent pipeline is required", cfg.Name)
339 - }
340 - for i := range cfg.Pipelines {
341 - if err := cfg.Pipelines[i].validate(); err != nil {
342 - return fmt.Errorf("job '%s': pipeline[%d]: %w", cfg.Name, i, err)
343 - }
344 - }
345 - if cfg.UpdateEvery < 0 {
346 - return fmt.Errorf("job '%s': update_every must be >= 1 second when provided", cfg.Name)
347 - }
348 - if cfg.LLD.InstanceTemplate == "" && cfg.hasTemplateLabels() {
349 - return fmt.Errorf("job '%s': lld.instance_id_template is required when labels reference macros", cfg.Name)
350 - }
351 - return nil
352 -}
353 -
354 -func (c CollectionConfig) validate() error {
355 - switch c.Type {
356 - case CollectionCommand:
357 - if strings.TrimSpace(c.Command) == "" {
358 - return fmt.Errorf("collection.command is required for command type")
359 - }
360 - case CollectionHTTP:
361 - if strings.TrimSpace(c.HTTP.URL) == "" && strings.TrimSpace(c.URL) == "" {
362 - return fmt.Errorf("http.url is required")
363 - }
364 - case CollectionSNMP:
365 - if strings.TrimSpace(c.SNMP.Target) == "" || strings.TrimSpace(c.SNMP.OID) == "" {
366 - return fmt.Errorf("snmp.target and snmp.oid are required")
367 - }
368 - default:
369 - return fmt.Errorf("unsupported collection type %q", c.Type)
370 - }
371 - return nil
372 -}
373 -
374 -func (c CollectionType) String() string { return string(c) }
375 -
376 -func (cfg *JobConfig) hasTemplateLabels() bool {
377 - for _, v := range cfg.LLD.Labels {
378 - if strings.Contains(v, "{#") {
379 - return true
380 - }
381 - }
382 - return false
383 -}
384 -
385 -func (p *PipelineConfig) validate() error {
386 - if p.Name == "" {
387 - return fmt.Errorf("name is required")
388 - }
389 - if p.Context == "" {
390 - return fmt.Errorf("context is required")
391 - }
392 - if p.Dimension == "" {
393 - return fmt.Errorf("dimension is required")
394 - }
395 - if strings.TrimSpace(p.Unit) == "" {
396 - return fmt.Errorf("unit is required")
397 - }
398 - if len(p.Steps) == 0 {
399 - return fmt.Errorf("at least one preprocessing step is required")
400 - }
401 - return nil
402 -}
403 -
404 -// Interval returns the discovery interval or zero for per-collection execution.
405 -func (l LLDConfig) IntervalDuration() time.Duration {
406 - return time.Duration(l.Interval)
407 -}
408 -
409 -// Timeout returns the collection timeout.
410 -func (c CollectionConfig) TimeoutDuration() time.Duration {
411 - if c.Timeout <= 0 {
412 - return 0
413 - }
414 - return time.Duration(c.Timeout)
415 -}
416 -
417 -// IntervalDuration returns the scheduling interval for the job.
418 -func (cfg JobConfig) IntervalDuration() time.Duration {
419 - if cfg.UpdateEvery >= 1 {
420 - return time.Duration(cfg.UpdateEvery) * time.Second
421 - }
422 - return time.Duration(defaultUpdateEverySeconds) * time.Second
423 -}
src/go/plugin/scripts.d/pkg/zabbix/config_test.go deleted
-128
@@ -1,128 +0,0 @@
1 -package zabbix
2 -
3 -import (
4 - "encoding/json"
5 - "testing"
6 -
7 - "gopkg.in/yaml.v3"
8 -
9 - zpre "github.com/netdata/netdata/go/plugins/plugin/scripts.d/pkg/zabbixpreproc"
10 -)
11 -
12 -func TestJobConfigValidate(t *testing.T) {
13 - cfg := JobConfig{
14 - Name: "disk",
15 - Collection: CollectionConfig{Type: CollectionCommand, Command: "/usr/lib/zabbix/get_disks"},
16 - LLD: LLDConfig{InstanceTemplate: "disk_{#DEVNAME}"},
17 - Pipelines: []PipelineConfig{{
18 - Name: "usage",
19 - Context: "zabbix.disk.usage",
20 - Dimension: "used",
21 - Unit: "%",
22 - Steps: []zpre.Step{{Type: zpre.StepTypeJSONPath, Params: "$.value"}},
23 - }},
24 - }
25 - if err := cfg.Validate(); err != nil {
26 - t.Fatalf("expected config to be valid: %v", err)
27 - }
28 -}
29 -
30 -func TestJobConfigValidateFails(t *testing.T) {
31 - cfg := JobConfig{Name: "bad"}
32 - if err := cfg.Validate(); err == nil {
33 - t.Fatalf("expected validation failure")
34 - }
35 -}
36 -
37 -func TestJobConfigUpdateEveryValidation(t *testing.T) {
38 - cfg := JobConfig{
39 - Name: "test",
40 - UpdateEvery: -1,
41 - Collection: CollectionConfig{Type: CollectionCommand, Command: "/bin/true"},
42 - Pipelines: []PipelineConfig{{
43 - Name: "value",
44 - Context: "ctx",
45 - Dimension: "dim",
46 - Unit: "value",
47 - Steps: []zpre.Step{{Type: zpre.StepTypeJSONPath, Params: "$.v"}},
48 - }},
49 - }
50 - if err := cfg.Validate(); err == nil {
51 - t.Fatalf("expected error for negative update_every")
52 - }
53 -
54 - cfg.UpdateEvery = 1
55 - if err := cfg.Validate(); err != nil {
56 - t.Fatalf("unexpected error for valid update_every: %v", err)
57 - }
58 -
59 - cfg.UpdateEvery = 0
60 - if err := cfg.Validate(); err != nil {
61 - t.Fatalf("unexpected error when update_every omitted: %v", err)
62 - }
63 -}
64 -
65 -func TestPipelineStepStringTypeYAML(t *testing.T) {
66 - data := `pipelines:
67 -- name: usage
68 - context: zabbix.fs.usage
69 - dimension: used
70 - unit: "%"
71 - steps:
72 - - type: jsonpath
73 - params: $.value
74 - error_handler: set-value
75 - error_handler_params: "0"
76 -`
77 - var wrapper struct {
78 - Pipelines []PipelineConfig `yaml:"pipelines"`
79 - }
80 - if err := yaml.Unmarshal([]byte(data), &wrapper); err != nil {
81 - t.Fatalf("unmarshal failed: %v", err)
82 - }
83 - if len(wrapper.Pipelines) != 1 {
84 - t.Fatalf("expected 1 pipeline, got %d", len(wrapper.Pipelines))
85 - }
86 - steps := wrapper.Pipelines[0].Steps
87 - if len(steps) != 1 {
88 - t.Fatalf("expected 1 step, got %d", len(steps))
89 - }
90 - step := steps[0]
91 - if step.Type != zpre.StepTypeJSONPath {
92 - t.Fatalf("expected jsonpath step, got %v", step.Type)
93 - }
94 - if step.ErrorHandler.Action != zpre.ErrorActionSetValue || step.ErrorHandler.Params != "0" {
95 - t.Fatalf("unexpected error handler: %+v", step.ErrorHandler)
96 - }
97 -}
98 -
99 -func TestPipelineStepNumericJSON(t *testing.T) {
100 - payload := []byte(`{"pipelines":[{"name":"usage","context":"ctx","dimension":"dim","unit":"%","steps":[{"type":12,"params":"$.value"}]}]}`)
101 - var wrapper struct {
102 - Pipelines []PipelineConfig `json:"pipelines"`
103 - }
104 - if err := json.Unmarshal(payload, &wrapper); err != nil {
105 - t.Fatalf("json unmarshal failed: %v", err)
106 - }
107 - step := wrapper.Pipelines[0].Steps[0]
108 - if step.Type != zpre.StepTypeJSONPath {
109 - t.Fatalf("expected numeric type to map to jsonpath, got %v", step.Type)
110 - }
111 -}
112 -
113 -func TestParseErrorHandlerInvalidAction(t *testing.T) {
114 - _, err := parseErrorHandler("bogus", "")
115 - if err == nil {
116 - t.Fatalf("expected error for unknown handler")
117 - }
118 -
119 - if _, err := parseErrorHandler("set-value", ""); err == nil {
120 - t.Fatalf("expected error when params missing for set-value")
121 - }
122 - if _, err := parseErrorHandler("set-error", ""); err == nil {
123 - t.Fatalf("expected error when params missing for set-error")
124 - }
125 - if h, err := parseErrorHandler("set-error", "bad"); err != nil || h.Params != "bad" {
126 - t.Fatalf("expected valid handler, err=%v handler=%+v", err, h)
127 - }
128 -}
src/go/plugin/scripts.d/pkg/zabbix/jobengine/job.go deleted
-492
@@ -1,492 +0,0 @@
1 -// SPDX-License-Identifier: GPL-3.0-or-later
2 -
3 -package jobengine
4 -
5 -import (
6 - "encoding/json"
7 - "errors"
8 - "fmt"
9 - "strconv"
10 - "strings"
11 - "sync"
12 - "time"
13 -
14 - "github.com/netdata/netdata/go/plugins/logger"
15 - "github.com/netdata/netdata/go/plugins/plugin/scripts.d/pkg/ids"
16 - pkgzabbix "github.com/netdata/netdata/go/plugins/plugin/scripts.d/pkg/zabbix"
17 - zpre "github.com/netdata/netdata/go/plugins/plugin/scripts.d/pkg/zabbixpreproc"
18 -)
19 -
20 -// Job orchestrates collection -> LLD -> dependent pipeline execution for a single
21 -// Zabbix-style definition. It wraps the shared Preprocessor and maintains the
22 -// discovered instance catalog between iterations.
23 -type Job struct {
24 - cfg pkgzabbix.JobConfig
25 - proc *zpre.Preprocessor
26 - log *logger.Logger
27 - mu sync.RWMutex
28 - lastDiscovery time.Time
29 - instances map[string]*instanceState
30 -}
31 -
32 -// Options configure the job engine behavior.
33 -type Options struct {
34 - Logger *logger.Logger
35 -}
36 -
37 -// Input encapsulates the payload passed to Process.
38 -type Input struct {
39 - Payload []byte
40 - Timestamp time.Time
41 - CollectError error
42 -}
43 -
44 -// Result aggregates the metrics, discovery changes, and failure state after a
45 -// Process call.
46 -type Result struct {
47 - Job string
48 - Timestamp time.Time
49 - Metrics []MetricResult
50 - Active []InstanceInfo
51 - Removed []InstanceInfo
52 - State StateSummary
53 -}
54 -
55 -// StateSummary tracks failure flags for the job and individual instances.
56 -type StateSummary struct {
57 - Job FailureFlags
58 - Instances map[string]FailureFlags
59 -}
60 -
61 -// FailureFlags capture the distinct failure modes per iteration.
62 -type FailureFlags struct {
63 - Collect bool
64 - LLD bool
65 - Extraction bool
66 - Dimension bool
67 -}
68 -
69 -// MetricResult describes a single dependent pipeline outcome.
70 -type MetricResult struct {
71 - Instance InstanceInfo
72 - Pipeline *pkgzabbix.PipelineConfig
73 - Value float64
74 - Precision int
75 - Discarded bool
76 - Error error
77 - Name string
78 - Labels map[string]string
79 -}
80 -
81 -// InstanceInfo carries the instance ID and macros used for templating.
82 -type InstanceInfo struct {
83 - ID string
84 - Macros map[string]string
85 -}
86 -
87 -// NewJob validates the configuration and constructs a Job bound to the given
88 -// Preprocessor.
89 -func NewJob(cfg pkgzabbix.JobConfig, proc *zpre.Preprocessor, opts Options) (*Job, error) {
90 - if err := cfg.Validate(); err != nil {
91 - return nil, err
92 - }
93 - if proc == nil {
94 - return nil, errors.New("jobengine: preprocessor is required")
95 - }
96 - log := opts.Logger
97 - if log == nil {
98 - log = logger.New().With("component", "zabbix/jobengine")
99 - }
100 - job := &Job{
101 - cfg: cfg,
102 - proc: proc,
103 - log: log,
104 - instances: make(map[string]*instanceState),
105 - }
106 - return job, nil
107 -}
108 -
109 -// Process executes LLD (when configured) and dependent pipelines using the
110 -// provided payload. The returned result describes all emitted metrics alongside
111 -// failure flags for the job and each instance.
112 -func (j *Job) Process(in Input) Result {
113 - j.mu.Lock()
114 - defer j.mu.Unlock()
115 -
116 - res := Result{
117 - Job: j.cfg.Name,
118 - Timestamp: in.Timestamp,
119 - State: StateSummary{Instances: make(map[string]FailureFlags)},
120 - }
121 -
122 - if in.Timestamp.IsZero() {
123 - in.Timestamp = time.Now()
124 - res.Timestamp = in.Timestamp
125 - }
126 -
127 - tracker := newIterationState()
128 -
129 - if in.CollectError != nil {
130 - j.ensureDefaultInstance()
131 - tracker.setCollectFailureAll(j.instances)
132 - var active []InstanceInfo
133 - for _, inst := range j.instances {
134 - active = append(active, inst.info())
135 - }
136 - return j.finalizeResult(tracker, active, nil, res)
137 - }
138 -
139 - payloadValue := zpre.Value{Data: string(in.Payload), Type: zpre.ValueTypeStr, Timestamp: in.Timestamp}
140 -
141 - var discovery []map[string]string
142 - var discErr error
143 - if len(j.cfg.LLD.Steps) > 0 && j.shouldDiscover(in.Timestamp) {
144 - discovery, discErr = j.runDiscovery(payloadValue)
145 - }
146 -
147 - var removed []InstanceInfo
148 - var active []InstanceInfo
149 - if discErr != nil {
150 - tracker.setLLDFailureAll(j.instances)
151 - } else if discovery != nil {
152 - var applyErr error
153 - removed, applyErr = j.applyDiscovery(discovery)
154 - if applyErr != nil {
155 - tracker.setLLDFailureAll(j.instances)
156 - discErr = applyErr
157 - } else {
158 - j.lastDiscovery = in.Timestamp
159 - }
160 - }
161 - if len(j.cfg.LLD.Steps) == 0 {
162 - j.ensureDefaultInstance()
163 - }
164 -
165 - for _, inst := range j.instances {
166 - active = append(active, inst.info())
167 - tracker.ensureInstance(inst.id)
168 - }
169 -
170 - var metrics []MetricResult
171 - if discErr == nil {
172 - metrics = j.runPipelines(payloadValue, tracker)
173 - }
174 -
175 - return j.finalizeResult(tracker, active, removed, Result{
176 - Job: res.Job,
177 - Timestamp: res.Timestamp,
178 - Metrics: metrics,
179 - Active: active,
180 - Removed: removed,
181 - })
182 -}
183 -
184 -// Destroy removes the job-specific state from the shared preprocessor and
185 -// forgets all LLD instances.
186 -func (j *Job) Destroy() {
187 - j.mu.Lock()
188 - defer j.mu.Unlock()
189 - for id := range j.instances {
190 - for _, pipe := range j.cfg.Pipelines {
191 - j.proc.ClearState(itemID(j.cfg, id, pipe.Name))
192 - }
193 - }
194 - j.proc.ClearState(fmt.Sprintf("%s.__lld", j.cfg.Name))
195 - j.instances = make(map[string]*instanceState)
196 -}
197 -
198 -func (j *Job) finalizeResult(tracker *iterationState, active, removed []InstanceInfo, res Result) Result {
199 - res.Active = active
200 - res.Removed = removed
201 - res.State.Job = tracker.jobFailure
202 - res.State.Instances = make(map[string]FailureFlags, len(tracker.instances))
203 - for id, flags := range tracker.instances {
204 - res.State.Instances[id] = *flags
205 - }
206 - return res
207 -}
208 -
209 -func (j *Job) shouldDiscover(now time.Time) bool {
210 - interval := j.cfg.LLD.IntervalDuration()
211 - if interval <= 0 {
212 - return true
213 - }
214 - return now.Sub(j.lastDiscovery) >= interval
215 -}
216 -
217 -func (j *Job) runDiscovery(val zpre.Value) ([]map[string]string, error) {
218 - steps := duplicateSteps(j.cfg.LLD.Steps, nil)
219 - itemID := fmt.Sprintf("%s.__lld", j.cfg.Name)
220 - res, err := j.proc.ExecutePipeline(itemID, val, steps)
221 - if err != nil {
222 - return nil, err
223 - }
224 - if len(res.Metrics) == 0 {
225 - return nil, fmt.Errorf("lld returned no metrics")
226 - }
227 - entries := []map[string]string{}
228 - for _, metric := range res.Metrics {
229 - payload := strings.TrimSpace(metric.Value)
230 - if payload == "" {
231 - continue
232 - }
233 - var batch []map[string]string
234 - if err := json.Unmarshal([]byte(payload), &batch); err != nil {
235 - return nil, err
236 - }
237 - entries = append(entries, batch...)
238 - }
239 - return entries, nil
240 -}
241 -
242 -func (j *Job) applyDiscovery(entries []map[string]string) ([]InstanceInfo, error) {
243 - seen := make(map[string]struct{}, len(entries))
244 - updates := make(map[string]map[string]string, len(entries))
245 - for _, entry := range entries {
246 - macros := cloneMacros(entry)
247 - instanceID := buildInstanceID(j.cfg, macros)
248 - if instanceID == "" {
249 - continue
250 - }
251 - if _, ok := seen[instanceID]; ok {
252 - j.warnf("duplicate LLD instance id %q dropped", instanceID)
253 - return nil, fmt.Errorf("duplicate instance id %s", instanceID)
254 - }
255 - seen[instanceID] = struct{}{}
256 - macros["{#INSTANCE_ID}"] = instanceID
257 - updates[instanceID] = macros
258 - }
259 - for id, macros := range updates {
260 - inst := j.instances[id]
261 - if inst == nil {
262 - inst = &instanceState{id: id, macros: make(map[string]string)}
263 - j.instances[id] = inst
264 - }
265 - inst.macros = cloneMacros(macros)
266 - inst.missing = 0
267 - }
268 - var removed []InstanceInfo
269 - for id, inst := range j.instances {
270 - if _, ok := seen[id]; ok {
271 - continue
272 - }
273 - inst.missing++
274 - threshold := j.cfg.LLD.MaxMissing
275 - if threshold <= 0 || inst.missing >= threshold {
276 - removed = append(removed, inst.info())
277 - j.removeInstance(inst)
278 - delete(j.instances, id)
279 - }
280 - }
281 - return removed, nil
282 -}
283 -
284 -func (j *Job) ensureDefaultInstance() {
285 - if len(j.instances) > 0 {
286 - return
287 - }
288 - j.instances["default"] = &instanceState{
289 - id: "default",
290 - macros: map[string]string{"{#INSTANCE_ID}": "default"},
291 - }
292 -}
293 -
294 -func (j *Job) removeInstance(inst *instanceState) {
295 - for _, pipe := range j.cfg.Pipelines {
296 - j.proc.ClearState(itemID(j.cfg, inst.id, pipe.Name))
297 - }
298 -}
299 -
300 -func (j *Job) runPipelines(val zpre.Value, tracker *iterationState) []MetricResult {
301 - var out []MetricResult
302 - for _, inst := range j.instances {
303 - macros := cloneMacros(inst.macros)
304 - for idx := range j.cfg.Pipelines {
305 - pipe := &j.cfg.Pipelines[idx]
306 - steps := duplicateSteps(pipe.Steps, macros)
307 - res, err := j.proc.ExecutePipeline(itemID(j.cfg, inst.id, pipe.Name), val, steps)
308 - if err != nil {
309 - if isDimensionError(err) {
310 - tracker.markDimensionFailure(inst.id)
311 - } else {
312 - tracker.markExtractionFailure(inst.id)
313 - }
314 - out = append(out, MetricResult{Instance: inst.info(), Pipeline: pipe, Error: err})
315 - continue
316 - }
317 - if res.Discarded || len(res.Metrics) == 0 {
318 - tracker.markDimensionFailure(inst.id)
319 - out = append(out, MetricResult{Instance: inst.info(), Pipeline: pipe, Discarded: true})
320 - continue
321 - }
322 - if len(res.Metrics) != 1 {
323 - tracker.markDimensionFailure(inst.id)
324 - if j.log != nil {
325 - j.log.Warningf("zabbix: job %s pipeline %s returned %d metrics (expected 1); dropping output", j.cfg.Name, pipe.Name, len(res.Metrics))
326 - }
327 - out = append(out, MetricResult{Instance: inst.info(), Pipeline: pipe, Error: fmt.Errorf("pipeline '%s' produced %d metrics; expected 1", pipe.Name, len(res.Metrics))})
328 - continue
329 - }
330 - metric := res.Metrics[0]
331 - valNum, err := strconv.ParseFloat(strings.TrimSpace(metric.Value), 64)
332 - if err != nil {
333 - tracker.markExtractionFailure(inst.id)
334 - if j.log != nil {
335 - j.log.Warningf("zabbix: job %s pipeline %s returned non-numeric value %q: %v", j.cfg.Name, pipe.Name, metric.Value, err)
336 - }
337 - out = append(out, MetricResult{Instance: inst.info(), Pipeline: pipe, Error: err})
338 - continue
339 - }
340 - out = append(out, MetricResult{
341 - Instance: inst.info(),
342 - Pipeline: pipe,
343 - Value: valNum,
344 - Precision: pipe.Precision,
345 - Name: metric.Name,
346 - Labels: cloneLabels(metric.Labels),
347 - })
348 - }
349 - }
350 - return out
351 -}
352 -
353 -// iteration tracking -------------------------------------------------------
354 -
355 -type iterationState struct {
356 - jobFailure FailureFlags
357 - instances map[string]*FailureFlags
358 -}
359 -
360 -func newIterationState() *iterationState {
361 - return &iterationState{instances: make(map[string]*FailureFlags)}
362 -}
363 -
364 -func (s *iterationState) ensureInstance(id string) {
365 - if _, ok := s.instances[id]; !ok {
366 - s.instances[id] = &FailureFlags{}
367 - }
368 -}
369 -
370 -func (s *iterationState) setCollectFailureAll(instances map[string]*instanceState) {
371 - s.jobFailure.Collect = true
372 - for id := range instances {
373 - s.ensureInstance(id)
374 - s.instances[id].Collect = true
375 - }
376 -}
377 -
378 -func (s *iterationState) setLLDFailureAll(instances map[string]*instanceState) {
379 - s.jobFailure.LLD = true
380 - for id := range instances {
381 - s.ensureInstance(id)
382 - s.instances[id].LLD = true
383 - }
384 -}
385 -
386 -func (s *iterationState) markExtractionFailure(id string) {
387 - s.jobFailure.Extraction = true
388 - s.ensureInstance(id)
389 - s.instances[id].Extraction = true
390 -}
391 -
392 -func (s *iterationState) markDimensionFailure(id string) {
393 - s.jobFailure.Dimension = true
394 - s.ensureInstance(id)
395 - s.instances[id].Dimension = true
396 -}
397 -
398 -func isDimensionError(err error) bool {
399 - if err == nil {
400 - return false
401 - }
402 - msg := err.Error()
403 - if strings.Contains(msg, "out of range") {
404 - return true
405 - }
406 - if strings.Contains(msg, "does not match") {
407 - return true
408 - }
409 - return false
410 -}
411 -
412 -// helpers -----------------------------------------------------------------
413 -
414 -type instanceState struct {
415 - id string
416 - macros map[string]string
417 - missing int
418 -}
419 -
420 -func (inst *instanceState) info() InstanceInfo {
421 - return InstanceInfo{ID: inst.id, Macros: cloneMacros(inst.macros)}
422 -}
423 -
424 -func duplicateSteps(steps []zpre.Step, macros map[string]string) []zpre.Step {
425 - out := make([]zpre.Step, len(steps))
426 - for i := range steps {
427 - step := steps[i]
428 - if macros != nil {
429 - step.Params = expandTemplate(step.Params, macros)
430 - }
431 - out[i] = step
432 - }
433 - return out
434 -}
435 -
436 -func itemID(cfg pkgzabbix.JobConfig, instanceID, pipe string) string {
437 - return fmt.Sprintf("%s.%s.%s", cfg.Name, instanceID, pipe)
438 -}
439 -
440 -func expandTemplate(tmpl string, macros map[string]string) string {
441 - if tmpl == "" || len(macros) == 0 {
442 - return tmpl
443 - }
444 - res := tmpl
445 - for k, v := range macros {
446 - res = strings.ReplaceAll(res, k, v)
447 - }
448 - return res
449 -}
450 -
451 -func cloneMacros(src map[string]string) map[string]string {
452 - out := make(map[string]string, len(src))
453 - for k, v := range src {
454 - out[k] = v
455 - }
456 - return out
457 -}
458 -
459 -func cloneLabels(src map[string]string) map[string]string {
460 - if len(src) == 0 {
461 - return nil
462 - }
463 - out := make(map[string]string, len(src))
464 - for k, v := range src {
465 - out[k] = v
466 - }
467 - return out
468 -}
469 -
470 -func buildInstanceID(cfg pkgzabbix.JobConfig, macros map[string]string) string {
471 - if val, ok := macros["{#INSTANCE_ID}"]; ok {
472 - if trimmed := strings.TrimSpace(val); trimmed != "" {
473 - return ids.Sanitize(trimmed)
474 - }
475 - }
476 - template := cfg.LLD.InstanceTemplate
477 - if strings.TrimSpace(template) == "" {
478 - template = fmt.Sprintf("%s_instance", cfg.Name)
479 - }
480 - raw := expandTemplate(template, macros)
481 - if strings.TrimSpace(raw) == "" {
482 - raw = fmt.Sprintf("%s_instance", cfg.Name)
483 - }
484 - return ids.Sanitize(raw)
485 -}
486 -
487 -func (j *Job) warnf(format string, args ...interface{}) {
488 - if j.log == nil {
489 - return
490 - }
491 - j.log.Warningf(format, args...)
492 -}
src/go/plugin/scripts.d/pkg/zabbix/jobengine/job_test.go deleted
-1107
@@ -1,1107 +0,0 @@
1 -package jobengine
2 -
3 -import (
4 - "errors"
5 - "fmt"
6 - "sort"
7 - "strings"
8 - "testing"
9 - "time"
10 -
11 - "github.com/netdata/netdata/go/plugins/pkg/confopt"
12 - "github.com/netdata/netdata/go/plugins/plugin/scripts.d/pkg/ids"
13 - pkgzabbix "github.com/netdata/netdata/go/plugins/plugin/scripts.d/pkg/zabbix"
14 - zpre "github.com/netdata/netdata/go/plugins/plugin/scripts.d/pkg/zabbixpreproc"
15 -)
16 -
17 -const pathLengthScript = `
18 -var rows = JSON.parse(value);
19 -var fsKey = "{#" + "FSNAME}";
20 -var pathKey = "{#" + "FSPATH}";
21 -var target = "{#FSNAME}";
22 -var matched = "";
23 -for (var i = 0; i < rows.length; i++) {
24 - if (rows[i][fsKey] === target) {
25 - matched = rows[i][pathKey] || "";
26 - break;
27 - }
28 -}
29 -value = (matched || "").length.toString();
30 -return value;
31 -`
32 -
33 -const snmpValueScript = `
34 -var rows = JSON.parse(value);
35 -var indexKey = "{#" + "SNMPINDEX}";
36 -var valueKey = "MACRO2";
37 -var target = "{#SNMPINDEX}";
38 -var matched = "";
39 -for (var i = 0; i < rows.length; i++) {
40 - if (rows[i][indexKey] === target) {
41 - matched = rows[i][valueKey] || "";
42 - break;
43 - }
44 -}
45 -value = matched || "";
46 -return value;
47 -`
48 -
49 -const promDiscoveryScript = `
50 -var entries = JSON.parse(value);
51 -var out = [];
52 -for (var i = 0; i < entries.length; i++) {
53 - var entry = entries[i];
54 - if (entry.name !== "node_filesystem_info") {
55 - continue;
56 - }
57 - var labels = entry.labels || {};
58 - var mount = labels.mountpoint || "";
59 - if (!mount) {
60 - continue;
61 - }
62 - out.push({"{#MOUNT}": mount});
63 -}
64 -value = JSON.stringify(out);
65 -return value;
66 -`
67 -
68 -const promValueScript = `
69 -var entries = JSON.parse(value);
70 -var target = "{#MOUNT}";
71 -var result = "";
72 -for (var i = 0; i < entries.length; i++) {
73 - var entry = entries[i];
74 - if (entry.name !== "node_filesystem_free_bytes") {
75 - continue;
76 - }
77 - var labels = entry.labels || {};
78 - if (labels.mountpoint === target) {
79 - result = entry.value || entry.valueStr || "";
80 - break;
81 - }
82 -}
83 -value = result ? result.toString() : "";
84 -return value;
85 -`
86 -
87 -const csvMultiValueScript = `
88 -var row = JSON.parse(value);
89 -var raw = row["value"] || row["VALUE"] || "0";
90 -value = raw.toString();
91 -return value;
92 -`
93 -
94 -func TestJobProcessMatrix(t *testing.T) {
95 - cases := buildMatrixCases()
96 - if len(cases) < 100 {
97 - t.Fatalf("expected at least 100 scenarios, have %d", len(cases))
98 - }
99 - for _, tc := range cases {
100 - t.Run(tc.name, func(t *testing.T) {
101 - proc := zpre.NewPreprocessor("jobengine-" + tc.name)
102 - job, err := NewJob(tc.cfg, proc, Options{})
103 - if err != nil {
104 - t.Fatalf("new job failed: %v", err)
105 - }
106 - res := job.Process(tc.input)
107 - job.Destroy()
108 - tc.expect.assert(t, res)
109 - })
110 - }
111 -}
112 -
113 -// ----------------------------------------------------------------------------
114 -
115 -type scenario struct {
116 - name string
117 - cfg pkgzabbix.JobConfig
118 - input Input
119 - expect expectation
120 -}
121 -
122 -type expectation struct {
123 - metrics int
124 - values []float64
125 - active int
126 - removed int
127 - removedIDs []string
128 - job FailureFlags
129 - instances map[string]FailureFlags
130 - macroKeys []string
131 - errorMetrics int
132 -}
133 -
134 -func (e expectation) assert(t *testing.T, res Result) {
135 - if res.State.Job != e.job {
136 - t.Fatalf("job flags mismatch: want %+v got %+v", e.job, res.State.Job)
137 - }
138 - if e.instances != nil && len(res.State.Instances) != len(e.instances) {
139 - t.Fatalf("instance flag map mismatch: want %d got %d", len(e.instances), len(res.State.Instances))
140 - }
141 - for id, flags := range e.instances {
142 - if got, ok := res.State.Instances[id]; !ok || got != flags {
143 - t.Fatalf("instance %s flags mismatch: want %+v got %+v", id, flags, got)
144 - }
145 - }
146 - if len(res.Metrics) != e.metrics {
147 - t.Fatalf("metrics count mismatch: want %d got %d", e.metrics, len(res.Metrics))
148 - }
149 - if len(res.Active) != e.active {
150 - t.Fatalf("active instances mismatch: want %d got %d", e.active, len(res.Active))
151 - }
152 - if len(res.Removed) != e.removed {
153 - t.Fatalf("removed instances mismatch: want %d got %d", e.removed, len(res.Removed))
154 - }
155 - if len(e.removedIDs) > 0 {
156 - gotIDs := make([]string, 0, len(res.Removed))
157 - for _, inst := range res.Removed {
158 - gotIDs = append(gotIDs, inst.ID)
159 - }
160 - sort.Strings(gotIDs)
161 - expected := append([]string(nil), e.removedIDs...)
162 - sort.Strings(expected)
163 - if len(gotIDs) != len(expected) {
164 - t.Fatalf("removed IDs mismatch: want %v got %v", expected, gotIDs)
165 - }
166 - for i := range gotIDs {
167 - if gotIDs[i] != expected[i] {
168 - t.Fatalf("removed IDs mismatch: want %v got %v", expected, gotIDs)
169 - }
170 - }
171 - }
172 - if len(e.values) > 0 {
173 - gotVals := make([]float64, 0, len(res.Metrics))
174 - for _, m := range res.Metrics {
175 - gotVals = append(gotVals, m.Value)
176 - }
177 - sort.Float64s(gotVals)
178 - expected := append([]float64(nil), e.values...)
179 - sort.Float64s(expected)
180 - if len(gotVals) != len(expected) {
181 - t.Fatalf("value set mismatch: want %v got %v", expected, gotVals)
182 - }
183 - for i := range gotVals {
184 - if diff := gotVals[i] - expected[i]; diff > 1e-9 || diff < -1e-9 {
185 - t.Fatalf("value mismatch: want %v got %v", expected, gotVals)
186 - }
187 - }
188 - }
189 - if len(e.macroKeys) > 0 {
190 - for _, m := range res.Metrics {
191 - for _, key := range e.macroKeys {
192 - if _, ok := m.Instance.Macros[key]; !ok {
193 - t.Fatalf("expected macro %s in metric instance %+v", key, m.Instance)
194 - }
195 - }
196 - }
197 - }
198 - if e.errorMetrics > 0 {
199 - count := 0
200 - for _, m := range res.Metrics {
201 - if m.Error != nil {
202 - count++
203 - }
204 - }
205 - if count != e.errorMetrics {
206 - t.Fatalf("expected %d errored metrics, got %d", e.errorMetrics, count)
207 - }
208 - }
209 -}
210 -
211 -// ----------------------------------------------------------------------------
212 -
213 -type variant int
214 -
215 -const (
216 - variantNormal variant = iota
217 - variantCollectErr
218 - variantExtractionErr
219 - variantDimensionErr
220 - variantLLDErr
221 -)
222 -
223 -func buildMatrixCases() []scenario {
224 - var cases []scenario
225 -
226 - simpleSamples := []float64{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
227 - simpleVariants := []variant{variantNormal, variantCollectErr, variantExtractionErr, variantDimensionErr}
228 - for idx, sample := range simpleSamples {
229 - for _, v := range simpleVariants {
230 - cases = append(cases, buildSimpleScenario(idx, sample, v))
231 - }
232 - }
233 -
234 - multiSamples := []struct{ used, free float64 }{
235 - {10, 90}, {20, 80}, {30, 70}, {40, 60}, {50, 50}, {60, 40}, {70, 30}, {80, 20},
236 - }
237 - for idx, sample := range multiSamples {
238 - for _, v := range simpleVariants {
239 - cases = append(cases, buildMultiScenario(idx, sample.used, sample.free, v))
240 - }
241 - }
242 -
243 - lldSamples := []struct{ root, home float64 }{
244 - {10, 20}, {15, 25}, {5, 35}, {40, 10}, {12, 13}, {7, 9},
245 - }
246 - lldVariants := []variant{variantNormal, variantCollectErr, variantExtractionErr, variantDimensionErr, variantLLDErr}
247 - for idx, sample := range lldSamples {
248 - for _, v := range lldVariants {
249 - cases = append(cases, buildLLDScenario(idx, sample.root, sample.home, v))
250 - }
251 - }
252 -
253 - for idx := 0; idx < 10; idx++ {
254 - cases = append(cases, buildCSVLLDScenario(idx))
255 - cases = append(cases, buildXMLLLDScenario(idx))
256 - cases = append(cases, buildSNMPLLDScenario(idx))
257 - }
258 -
259 - promSamples := []struct{ root, home float64 }{
260 - {60, 80}, {45, 70}, {90, 20}, {30, 55},
261 - }
262 - for idx, sample := range promSamples {
263 - for _, v := range lldVariants {
264 - cases = append(cases, buildPrometheusLLDScenario(idx, sample.root, sample.home, v))
265 - }
266 - }
267 -
268 - for idx := 0; idx < 5; idx++ {
269 - for _, v := range simpleVariants {
270 - cases = append(cases, buildXPathScenario(idx, float64(40+idx*3), float64(50+idx*4), v))
271 - cases = append(cases, buildCSVMultiScenario(idx, float64(10+idx), v))
272 - }
273 - }
274 -
275 - return cases
276 -}
277 -
278 -// ----------------------------------------------------------------------------
279 -
280 -type stateVariant int
281 -
282 -const (
283 - stateVariantStable stateVariant = iota
284 - stateVariantAddRemove
285 - stateVariantLLDFailure
286 - stateVariantExtractionFailure
287 - stateVariantDimensionFailure
288 -)
289 -
290 -type statefulScenario struct {
291 - name string
292 - cfg pkgzabbix.JobConfig
293 - runs []stateRun
294 -}
295 -
296 -type stateRun struct {
297 - label string
298 - input Input
299 - expect expectation
300 -}
301 -
302 -func TestJobProcessStatefulMatrix(t *testing.T) {
303 - cases := buildStatefulCases()
304 - if len(cases) < 50 {
305 - t.Fatalf("expected >=50 stateful scenarios, have %d", len(cases))
306 - }
307 - for _, sc := range cases {
308 - t.Run(sc.name, func(t *testing.T) {
309 - proc := zpre.NewPreprocessor("state-" + sc.name)
310 - job, err := NewJob(sc.cfg, proc, Options{})
311 - if err != nil {
312 - t.Fatalf("new job failed: %v", err)
313 - }
314 - for _, run := range sc.runs {
315 - res := job.Process(run.input)
316 - run.expect.assert(t, res)
317 - }
318 - job.Destroy()
319 - })
320 - }
321 -}
322 -
323 -func TestJobProcessDuplicateDiscovery(t *testing.T) {
324 - cfg := lldJobConfig("duplicate")
325 - input := Input{Payload: []byte(`{"discovery":[{"{#FSNAME}":"/"},{"{#FSNAME}":"/"},{"{#FSNAME}":"/home"}],"data":[{"fs":"/","used":10,"free":90},{"fs":"/home","used":20,"free":80}]}`), Timestamp: time.Now()}
326 - proc := zpre.NewPreprocessor("dup")
327 - job, err := NewJob(cfg, proc, Options{})
328 - if err != nil {
329 - t.Fatalf("new job failed: %v", err)
330 - }
331 - res := job.Process(input)
332 - if !res.State.Job.LLD {
333 - t.Fatalf("expected LLD failure due to duplicate ids")
334 - }
335 - if len(res.Metrics) != 0 {
336 - t.Fatalf("expected no metrics when discovery fails, got %d", len(res.Metrics))
337 - }
338 -}
339 -
340 -func TestJobShouldDiscover(t *testing.T) {
341 - job := &Job{}
342 - if !job.shouldDiscover(time.Now()) {
343 - t.Fatalf("interval <=0 should force discovery")
344 - }
345 - job.cfg.LLD.Interval = confopt.Duration(10 * time.Second)
346 - job.lastDiscovery = time.Now()
347 - if job.shouldDiscover(time.Now()) {
348 - t.Fatalf("should not rediscover before interval")
349 - }
350 - job.lastDiscovery = time.Now().Add(-11 * time.Second)
351 - if !job.shouldDiscover(time.Now()) {
352 - t.Fatalf("should rediscover after interval elapsed")
353 - }
354 -}
355 -
356 -func TestLLDInstanceRemovalAfterMaxMissing(t *testing.T) {
357 - cfg := lldJobConfig("miss")
358 - cfg.LLD.MaxMissing = 2
359 - proc := zpre.NewPreprocessor("miss-test")
360 - job, err := NewJob(cfg, proc, Options{})
361 - if err != nil {
362 - t.Fatalf("new job failed: %v", err)
363 - }
364 - createPayload := Input{Payload: []byte(`{"discovery":[{"{#FSNAME}":"/"}],"data":[{"fs":"/","used":10,"free":90}]}`), Timestamp: time.Now()}
365 - res := job.Process(createPayload)
366 - if len(res.Active) != 1 {
367 - t.Fatalf("expected 1 active instance, got %d", len(res.Active))
368 - }
369 - missingPayload := Input{Payload: []byte(`{"discovery":[],"data":[]}`), Timestamp: time.Now().Add(time.Second)}
370 - res = job.Process(missingPayload)
371 - if len(res.Removed) != 0 {
372 - t.Fatalf("expected no removal after first miss, got %d", len(res.Removed))
373 - }
374 - missingPayload.Timestamp = missingPayload.Timestamp.Add(time.Second)
375 - res = job.Process(missingPayload)
376 - if len(res.Removed) != 1 {
377 - t.Fatalf("expected removal after reaching max_missing, got %d", len(res.Removed))
378 - }
379 -}
380 -
381 -func buildStatefulCases() []statefulScenario {
382 - samples := []struct{ root, home float64 }{
383 - {10, 20}, {15, 25}, {5, 35}, {40, 10}, {12, 13}, {7, 9}, {22, 11}, {18, 16}, {9, 12}, {31, 17},
384 - }
385 - variants := []stateVariant{stateVariantStable, stateVariantAddRemove, stateVariantLLDFailure, stateVariantExtractionFailure, stateVariantDimensionFailure}
386 - var cases []statefulScenario
387 - for idx, sample := range samples {
388 - for _, v := range variants {
389 - cases = append(cases, buildStatefulScenario(idx, sample.root, sample.home, v))
390 - }
391 - }
392 - promStates := []struct{ root, home float64 }{{60, 45}, {55, 35}}
393 - for idx, sample := range promStates {
394 - cases = append(cases, buildPromStatefulScenario(idx, sample.root, sample.home))
395 - }
396 - cases = append(cases, buildXPathStatefulScenario(0, 42, 57))
397 - return cases
398 -}
399 -
400 -func buildStatefulScenario(idx int, root, home float64, v stateVariant) statefulScenario {
401 - name := fmt.Sprintf("state_%d_%s", idx, stateVariantName(v))
402 - cfg := lldJobConfig(fmt.Sprintf("state_%d", idx))
403 - rootID := ids.Sanitize("/")
404 - homeID := ids.Sanitize("/home")
405 - instBoth := []instData{{"/", root, 100 - root}, {"/home", home, 100 - home}}
406 - instRoot := []instData{{"/", root, 100 - root}}
407 - instHome := []instData{{"/home", home, 100 - home}}
408 - var runs []stateRun
409 - switch v {
410 - case stateVariantStable:
411 - payload := []byte(lldPayload(instBoth))
412 - expect := expectation{metrics: 4, values: valuesFor(instBoth), active: 2, job: FailureFlags{}, instances: zeroFlags(rootID, homeID), macroKeys: []string{"{#FSNAME}"}}
413 - runs = []stateRun{
414 - {label: "run1", input: Input{Payload: payload, Timestamp: time.Now()}, expect: expect},
415 - {label: "run2", input: Input{Payload: payload, Timestamp: time.Now()}, expect: expect},
416 - {label: "run3", input: Input{Payload: payload, Timestamp: time.Now()}, expect: expect},
417 - }
418 - case stateVariantAddRemove:
419 - payload1 := []byte(lldPayload(instRoot))
420 - payload2 := []byte(lldPayload(instBoth))
421 - payload3 := []byte(lldPayload(instHome))
422 - runs = []stateRun{
423 - {label: "run1", input: Input{Payload: payload1, Timestamp: time.Now()}, expect: expectation{metrics: 2, values: valuesFor(instRoot), active: 1, job: FailureFlags{}, instances: zeroFlags(rootID), macroKeys: []string{"{#FSNAME}"}}},
424 - {label: "run2", input: Input{Payload: payload2, Timestamp: time.Now()}, expect: expectation{metrics: 4, values: valuesFor(instBoth), active: 2, job: FailureFlags{}, instances: zeroFlags(rootID, homeID), macroKeys: []string{"{#FSNAME}"}}},
425 - {label: "run3", input: Input{Payload: payload3, Timestamp: time.Now()}, expect: expectation{metrics: 2, values: valuesFor(instHome), active: 1, removed: 1, removedIDs: []string{rootID}, job: FailureFlags{}, instances: zeroFlags(homeID), macroKeys: []string{"{#FSNAME}"}}},
426 - }
427 - case stateVariantLLDFailure:
428 - payload := []byte(lldPayload(instBoth))
429 - runs = []stateRun{
430 - {label: "run1", input: Input{Payload: payload, Timestamp: time.Now()}, expect: expectation{metrics: 4, values: valuesFor(instBoth), active: 2, job: FailureFlags{}, instances: zeroFlags(rootID, homeID), macroKeys: []string{"{#FSNAME}"}}},
431 - {label: "run2", input: Input{Payload: []byte("not-json"), Timestamp: time.Now()}, expect: expectation{metrics: 0, values: nil, active: 2, job: FailureFlags{LLD: true}, instances: map[string]FailureFlags{rootID: {LLD: true}, homeID: {LLD: true}}}},
432 - {label: "run3", input: Input{Payload: payload, Timestamp: time.Now()}, expect: expectation{metrics: 4, values: valuesFor(instBoth), active: 2, job: FailureFlags{}, instances: zeroFlags(rootID, homeID), macroKeys: []string{"{#FSNAME}"}}},
433 - }
434 - case stateVariantExtractionFailure:
435 - payload := []byte(lldPayload(instBoth))
436 - badPayload := []byte(`{"discovery":[{"{#FSNAME}":"/"},{"{#FSNAME}":"/home"}],"data":[{"fs":"/","used":"oops","free":"oops"},{"fs":"/home","used":"bad","free":"bad"}]}`)
437 - runs = []stateRun{
438 - {label: "run1", input: Input{Payload: payload, Timestamp: time.Now()}, expect: expectation{metrics: 4, values: valuesFor(instBoth), active: 2, job: FailureFlags{}, instances: zeroFlags(rootID, homeID), macroKeys: []string{"{#FSNAME}"}}},
439 - {label: "run2", input: Input{Payload: badPayload, Timestamp: time.Now()}, expect: expectation{metrics: 4, values: nil, active: 2, job: FailureFlags{Extraction: true}, instances: map[string]FailureFlags{rootID: {Extraction: true}, homeID: {Extraction: true}}, errorMetrics: 4}},
440 - {label: "run3", input: Input{Payload: payload, Timestamp: time.Now()}, expect: expectation{metrics: 4, values: valuesFor(instBoth), active: 2, job: FailureFlags{}, instances: zeroFlags(rootID, homeID), macroKeys: []string{"{#FSNAME}"}}},
441 - }
442 - case stateVariantDimensionFailure:
443 - payload := []byte(lldPayload(instBoth))
444 - badPayload := []byte(`{"discovery":[{"{#FSNAME}":"/"},{"{#FSNAME}":"/home"}],"data":[{"fs":"/","used":150,"free":-10},{"fs":"/home","used":180,"free":-20}]}`)
445 - runs = []stateRun{
446 - {label: "run1", input: Input{Payload: payload, Timestamp: time.Now()}, expect: expectation{metrics: 4, values: valuesFor(instBoth), active: 2, job: FailureFlags{}, instances: zeroFlags(rootID, homeID), macroKeys: []string{"{#FSNAME}"}}},
447 - {label: "run2", input: Input{Payload: badPayload, Timestamp: time.Now()}, expect: expectation{metrics: 4, values: nil, active: 2, job: FailureFlags{Dimension: true}, instances: map[string]FailureFlags{rootID: {Dimension: true}, homeID: {Dimension: true}}, errorMetrics: 4}},
448 - {label: "run3", input: Input{Payload: payload, Timestamp: time.Now()}, expect: expectation{metrics: 4, values: valuesFor(instBoth), active: 2, job: FailureFlags{}, instances: zeroFlags(rootID, homeID), macroKeys: []string{"{#FSNAME}"}}},
449 - }
450 - }
451 - return statefulScenario{name: name, cfg: cfg, runs: runs}
452 -}
453 -
454 -func buildPromStatefulScenario(idx int, root, home float64) statefulScenario {
455 - name := fmt.Sprintf("state_prom_%d", idx)
456 - cfg := prometheusJobConfig(fmt.Sprintf("state_prom_%d", idx))
457 - instRoot := ids.Sanitize("fs_/")
458 - instHome := ids.Sanitize("fs_/home")
459 - runs := []stateRun{
460 - {
461 - label: "run1",
462 - input: Input{Payload: []byte(prometheusPayload(root, home)), Timestamp: time.Now()},
463 - expect: expectation{metrics: 2, values: []float64{root, home}, active: 2, job: FailureFlags{}, instances: zeroFlags(instRoot, instHome)},
464 - },
465 - {
466 - label: "run2",
467 - input: Input{Payload: []byte("not-prom"), Timestamp: time.Now()},
468 - expect: expectation{metrics: 0, values: nil, active: 2, job: FailureFlags{LLD: true}, instances: map[string]FailureFlags{instRoot: {LLD: true}, instHome: {LLD: true}}},
469 - },
470 - {
471 - label: "run3",
472 - input: Input{Payload: []byte(prometheusPayload(root+5, home+3)), Timestamp: time.Now()},
473 - expect: expectation{metrics: 2, values: []float64{root + 5, home + 3}, active: 2, job: FailureFlags{}, instances: zeroFlags(instRoot, instHome)},
474 - },
475 - }
476 - return statefulScenario{name: name, cfg: cfg, runs: runs}
477 -}
478 -
479 -func buildXPathStatefulScenario(idx int, cpu, gpu float64) statefulScenario {
480 - name := fmt.Sprintf("state_xpath_%d", idx)
481 - cfg := xpathJobConfig(fmt.Sprintf("state_xpath_%d", idx))
482 - defaultID := "default"
483 - runs := []stateRun{
484 - {
485 - label: "run1",
486 - input: Input{Payload: []byte(fmt.Sprintf(`<sensors><sensor id="cpu">%f</sensor><sensor id="gpu">%f</sensor></sensors>`, cpu, gpu)), Timestamp: time.Now()},
487 - expect: expectation{metrics: 2, values: []float64{cpu, gpu}, active: 1, job: FailureFlags{}, instances: zeroFlags(defaultID)},
488 - },
489 - {
490 - label: "run2",
491 - input: Input{Payload: []byte(`<sensors><sensor id="cpu">oops</sensor><sensor id="gpu">bad</sensor></sensors>`), Timestamp: time.Now()},
492 - expect: expectation{metrics: 2, values: nil, active: 1, job: FailureFlags{Extraction: true}, instances: map[string]FailureFlags{defaultID: {Extraction: true}}, errorMetrics: 2},
493 - },
494 - {
495 - label: "run3",
496 - input: Input{Payload: []byte(fmt.Sprintf(`<sensors><sensor id="cpu">%f</sensor><sensor id="gpu">%f</sensor></sensors>`, cpu+2, gpu+2)), Timestamp: time.Now()},
497 - expect: expectation{metrics: 2, values: []float64{cpu + 2, gpu + 2}, active: 1, job: FailureFlags{}, instances: zeroFlags(defaultID)},
498 - },
499 - }
500 - return statefulScenario{name: name, cfg: cfg, runs: runs}
501 -}
502 -
503 -func stateVariantName(v stateVariant) string {
504 - switch v {
505 - case stateVariantStable:
506 - return "stable"
507 - case stateVariantAddRemove:
508 - return "add_remove"
509 - case stateVariantLLDFailure:
510 - return "lld_fail"
511 - case stateVariantExtractionFailure:
512 - return "extract_fail"
513 - case stateVariantDimensionFailure:
514 - return "dimension_fail"
515 - default:
516 - return "unknown"
517 - }
518 -}
519 -
520 -type instData struct {
521 - name string
522 - used float64
523 - free float64
524 -}
525 -
526 -func lldPayload(insts []instData) string {
527 - var discParts []string
528 - var dataParts []string
529 - for _, inst := range insts {
530 - discParts = append(discParts, fmt.Sprintf(`{"{#FSNAME}":"%s"}`, inst.name))
531 - dataParts = append(dataParts, fmt.Sprintf(`{"fs":"%s","used":%g,"free":%g}`, inst.name, inst.used, inst.free))
532 - }
533 - return fmt.Sprintf(`{"discovery":[%s],"data":[%s]}`, strings.Join(discParts, ","), strings.Join(dataParts, ","))
534 -}
535 -
536 -func valuesFor(insts []instData) []float64 {
537 - var vals []float64
538 - for _, inst := range insts {
539 - vals = append(vals, inst.used, inst.free)
540 - }
541 - return vals
542 -}
543 -
544 -func prometheusPayload(root, home float64) string {
545 - return prometheusPayloadStrings(fmt.Sprintf("%f", root), fmt.Sprintf("%f", home))
546 -}
547 -
548 -func prometheusPayloadStrings(rootVal, homeVal string) string {
549 - return fmt.Sprintf(`# HELP node_filesystem_free_bytes Free bytes
550 -# TYPE node_filesystem_free_bytes gauge
551 -# HELP node_filesystem_info Filesystem info
552 -# TYPE node_filesystem_info gauge
553 -node_filesystem_info{mountpoint="/",fstype="ext4"} 1
554 -node_filesystem_info{mountpoint="/home",fstype="xfs"} 1
555 -node_filesystem_free_bytes{mountpoint="/",fstype="ext4"} %s
556 -node_filesystem_free_bytes{mountpoint="/home",fstype="xfs"} %s
557 -`, rootVal, homeVal)
558 -}
559 -
560 -func zeroFlags(ids ...string) map[string]FailureFlags {
561 - if len(ids) == 0 {
562 - return map[string]FailureFlags{}
563 - }
564 - m := make(map[string]FailureFlags, len(ids))
565 - for _, id := range ids {
566 - m[id] = FailureFlags{}
567 - }
568 - return m
569 -}
570 -
571 -func buildSimpleScenario(idx int, value float64, v variant) scenario {
572 - name := fmt.Sprintf("simple_%d_%s", idx, variantName(v))
573 - cfg := simpleJobConfig(fmt.Sprintf("simple_%d", idx))
574 - input := Input{Payload: []byte(fmt.Sprintf(`{"value": %f}`, value)), Timestamp: time.Now()}
575 - expect := expectation{metrics: 1, values: []float64{value}, active: 1, job: FailureFlags{}, instances: map[string]FailureFlags{"default": {}}}
576 - switch v {
577 - case variantCollectErr:
578 - input.CollectError = errors.New("collect")
579 - expect.metrics = 0
580 - expect.values = nil
581 - expect.active = 1
582 - expect.job = FailureFlags{Collect: true}
583 - expect.instances = map[string]FailureFlags{"default": {Collect: true}}
584 - case variantExtractionErr:
585 - input.Payload = []byte(`{"value":"oops"}`)
586 - expect.metrics = 1
587 - expect.values = nil
588 - expect.job = FailureFlags{Extraction: true}
589 - expect.instances = map[string]FailureFlags{"default": {Extraction: true}}
590 - case variantDimensionErr:
591 - input.Payload = []byte(`{"value": 200}`)
592 - expect.metrics = 1
593 - expect.values = nil
594 - expect.job = FailureFlags{Dimension: true}
595 - expect.instances = map[string]FailureFlags{"default": {Dimension: true}}
596 - }
597 - return scenario{name: name, cfg: cfg, input: input, expect: expect}
598 -}
599 -
600 -func buildMultiScenario(idx int, used, free float64, v variant) scenario {
601 - name := fmt.Sprintf("multi_%d_%s", idx, variantName(v))
602 - cfg := multiPipelineJobConfig(fmt.Sprintf("multi_%d", idx))
603 - input := Input{Payload: []byte(fmt.Sprintf(`{"used": %f, "free": %f}`, used, free)), Timestamp: time.Now()}
604 - // Validation happens BEFORE multiplier, so all normal inputs pass validation
605 - // Multiplier=2 is applied after validation
606 - usedAfterMultiplier := used * 2
607 - expect := expectation{metrics: 2, values: []float64{usedAfterMultiplier, free}, active: 1, job: FailureFlags{}, instances: map[string]FailureFlags{"default": {}}}
608 - switch v {
609 - case variantCollectErr:
610 - input.CollectError = errors.New("collect")
611 - expect.metrics = 0
612 - expect.values = nil
613 - expect.active = 1
614 - expect.job = FailureFlags{Collect: true}
615 - expect.instances = map[string]FailureFlags{"default": {Collect: true}}
616 - case variantExtractionErr:
617 - input.Payload = []byte(`{"used":"oops","free":"oops"}`)
618 - expect.metrics = 2
619 - expect.values = nil
620 - expect.job = FailureFlags{Extraction: true}
621 - expect.instances = map[string]FailureFlags{"default": {Extraction: true}}
622 - case variantDimensionErr:
623 - input.Payload = []byte(`{"used": 200, "free": 300}`)
624 - expect.metrics = 2
625 - expect.values = nil
626 - expect.job = FailureFlags{Dimension: true}
627 - expect.instances = map[string]FailureFlags{"default": {Dimension: true}}
628 - }
629 - return scenario{name: name, cfg: cfg, input: input, expect: expect}
630 -}
631 -
632 -func buildLLDScenario(idx int, root, home float64, v variant) scenario {
633 - name := fmt.Sprintf("lld_%d_%s", idx, variantName(v))
634 - cfg := lldJobConfig(fmt.Sprintf("lld_%d", idx))
635 - payload := fmt.Sprintf(`{"discovery":[{"{#FSNAME}":"/"},{"{#FSNAME}":"/home"}],"data":[{"fs":"/","used":%f,"free":%f},{"fs":"/home","used":%f,"free":%f}]}`, root, 100-root, home, 100-home)
636 - input := Input{Payload: []byte(payload), Timestamp: time.Now()}
637 - values := []float64{root, home, 100 - root, 100 - home}
638 - instA := ids.Sanitize("/")
639 - instB := ids.Sanitize("/home")
640 - macroKeys := []string{}
641 - if v == variantNormal {
642 - macroKeys = []string{"{#FSNAME}"}
643 - }
644 - expect := expectation{
645 - metrics: 4,
646 - values: values,
647 - active: 2,
648 - job: FailureFlags{},
649 - instances: map[string]FailureFlags{instA: {}, instB: {}},
650 - macroKeys: macroKeys,
651 - }
652 - switch v {
653 - case variantCollectErr:
654 - input.CollectError = errors.New("collect")
655 - expect.metrics = 0
656 - expect.values = nil
657 - expect.active = 1
658 - expect.job = FailureFlags{Collect: true}
659 - expect.instances = map[string]FailureFlags{"default": {Collect: true}}
660 - case variantExtractionErr:
661 - input.Payload = []byte(`{"discovery":[{"{#FSNAME}":"/"},{"{#FSNAME}":"/home"}],"data":[{"fs":"/","used":"abc","free":"def"},{"fs":"/home","used":"ghi","free":"jkl"}]}`)
662 - expect.metrics = 4
663 - expect.values = nil
664 - expect.job = FailureFlags{Extraction: true}
665 - expect.instances = map[string]FailureFlags{instA: {Extraction: true}, instB: {Extraction: true}}
666 - case variantDimensionErr:
667 - input.Payload = []byte(`{"discovery":[{"{#FSNAME}":"/"},{"{#FSNAME}":"/home"}],"data":[{"fs":"/","used":150,"free":-10},{"fs":"/home","used":180,"free":-20}]}`)
668 - expect.metrics = 4
669 - expect.values = nil
670 - expect.job = FailureFlags{Dimension: true}
671 - expect.instances = map[string]FailureFlags{instA: {Dimension: true}, instB: {Dimension: true}}
672 - case variantLLDErr:
673 - input.Payload = []byte(`not-json`)
674 - expect.metrics = 0
675 - expect.values = nil
676 - expect.active = 0
677 - expect.job = FailureFlags{LLD: true}
678 - expect.instances = map[string]FailureFlags{}
679 - return scenario{name: name, cfg: cfg, input: input, expect: expect}
680 - }
681 - return scenario{name: name, cfg: cfg, input: input, expect: expect}
682 -}
683 -
684 -func buildCSVLLDScenario(idx int) scenario {
685 - name := fmt.Sprintf("lld_csv_%d", idx)
686 - cfg := csvLLDJobConfig(fmt.Sprintf("csv_job_%d", idx))
687 - input := Input{Payload: []byte(`{#FSNAME},{#FSPATH}
688 -root,/
689 -home,/home`), Timestamp: time.Now()}
690 - instA := ids.Sanitize("root")
691 - instB := ids.Sanitize("home")
692 - expect := expectation{metrics: 2, values: []float64{1, 5}, active: 2, job: FailureFlags{}, instances: map[string]FailureFlags{instA: {}, instB: {}}, macroKeys: []string{"{#FSNAME}"}}
693 - return scenario{name: name, cfg: cfg, input: input, expect: expect}
694 -}
695 -
696 -func buildXMLLLDScenario(idx int) scenario {
697 - name := fmt.Sprintf("lld_xml_%d", idx)
698 - cfg := xmlLLDJobConfig(fmt.Sprintf("xml_job_%d", idx))
699 - input := Input{Payload: []byte(`<files><fs name="root" path="/"/><fs name="home" path="/home"/></files>`), Timestamp: time.Now()}
700 - instA := ids.Sanitize("root")
701 - instB := ids.Sanitize("home")
702 - expect := expectation{metrics: 2, values: []float64{1, 5}, active: 2, job: FailureFlags{}, instances: map[string]FailureFlags{instA: {}, instB: {}}, macroKeys: []string{"{#FSNAME}"}}
703 - return scenario{name: name, cfg: cfg, input: input, expect: expect}
704 -}
705 -
706 -func buildSNMPLLDScenario(idx int) scenario {
707 - name := fmt.Sprintf("lld_snmp_%d", idx)
708 - cfg := snmpLLDJobConfig(fmt.Sprintf("snmp_job_%d", idx))
709 - snmpData := `.1.3.6.1.1 = STRING: "ifA"
710 -.1.3.6.1.2 = STRING: "ifB"
711 -.1.3.6.2.1 = STRING: "100"
712 -.1.3.6.2.2 = STRING: "200"`
713 - input := Input{Payload: []byte(snmpData), Timestamp: time.Now()}
714 - instA := ids.Sanitize("ifA")
715 - instB := ids.Sanitize("ifB")
716 - expect := expectation{metrics: 2, values: []float64{100, 200}, active: 2, job: FailureFlags{}, instances: map[string]FailureFlags{instA: {}, instB: {}}, macroKeys: []string{"MACRO1", "MACRO2", "{#SNMPINDEX}"}}
717 - return scenario{name: name, cfg: cfg, input: input, expect: expect}
718 -}
719 -
720 -func buildPrometheusLLDScenario(idx int, root, home float64, v variant) scenario {
721 - name := fmt.Sprintf("lld_prom_%d_%s", idx, variantName(v))
722 - cfg := prometheusJobConfig(fmt.Sprintf("prom_job_%d", idx))
723 - input := Input{Payload: []byte(prometheusPayload(root, home)), Timestamp: time.Now()}
724 - instRoot := ids.Sanitize("fs_/")
725 - instHome := ids.Sanitize("fs_/home")
726 - expect := expectation{
727 - metrics: 2,
728 - values: []float64{root, home},
729 - active: 2,
730 - job: FailureFlags{},
731 - instances: map[string]FailureFlags{instRoot: {}, instHome: {}},
732 - macroKeys: []string{"{#MOUNT}"},
733 - }
734 - switch v {
735 - case variantCollectErr:
736 - input.CollectError = errors.New("collect")
737 - expect.metrics = 0
738 - expect.values = nil
739 - expect.active = 1
740 - expect.job = FailureFlags{Collect: true}
741 - expect.instances = map[string]FailureFlags{"default": {Collect: true}}
742 - case variantExtractionErr:
743 - input.Payload = []byte(prometheusPayloadStrings("oops", "123"))
744 - expect.values = nil
745 - expect.job = FailureFlags{Extraction: true}
746 - expect.instances = map[string]FailureFlags{instRoot: {Extraction: true}, instHome: {}}
747 - case variantDimensionErr:
748 - input.Payload = []byte(prometheusPayload(1500, 2000))
749 - expect.values = nil
750 - expect.job = FailureFlags{Dimension: true}
751 - expect.instances = map[string]FailureFlags{instRoot: {Dimension: true}, instHome: {Dimension: true}}
752 - case variantLLDErr:
753 - input.Payload = []byte("not-prometheus")
754 - expect.metrics = 0
755 - expect.values = nil
756 - expect.active = 0
757 - expect.job = FailureFlags{LLD: true}
758 - expect.instances = map[string]FailureFlags{}
759 - }
760 - return scenario{name: name, cfg: cfg, input: input, expect: expect}
761 -}
762 -
763 -func buildXPathScenario(idx int, cpu, gpu float64, v variant) scenario {
764 - name := fmt.Sprintf("xpath_%d_%s", idx, variantName(v))
765 - cfg := xpathJobConfig(fmt.Sprintf("xpath_%d", idx))
766 - payload := fmt.Sprintf(`<sensors><sensor id="cpu">%f</sensor><sensor id="gpu">%f</sensor></sensors>`, cpu, gpu)
767 - input := Input{Payload: []byte(payload), Timestamp: time.Now()}
768 - expect := expectation{metrics: 2, values: []float64{cpu, gpu}, active: 1, job: FailureFlags{}, instances: map[string]FailureFlags{"default": {}}}
769 - switch v {
770 - case variantCollectErr:
771 - input.CollectError = errors.New("collect")
772 - expect.metrics = 0
773 - expect.values = nil
774 - expect.active = 1
775 - expect.job = FailureFlags{Collect: true}
776 - expect.instances = map[string]FailureFlags{"default": {Collect: true}}
777 - case variantExtractionErr:
778 - input.Payload = []byte(`<sensors><sensor id="cpu">oops</sensor><sensor id="gpu">bad</sensor></sensors>`)
779 - expect.values = nil
780 - expect.job = FailureFlags{Extraction: true}
781 - expect.instances = map[string]FailureFlags{"default": {Extraction: true}}
782 - case variantDimensionErr:
783 - input.Payload = []byte(`<sensors><sensor id="cpu">500</sensor><sensor id="gpu">600</sensor></sensors>`)
784 - expect.values = nil
785 - expect.job = FailureFlags{Dimension: true}
786 - expect.instances = map[string]FailureFlags{"default": {Dimension: true}}
787 - }
788 - return scenario{name: name, cfg: cfg, input: input, expect: expect}
789 -}
790 -
791 -func buildCSVMultiScenario(idx int, first float64, v variant) scenario {
792 - name := fmt.Sprintf("csv_multi_%d_%s", idx, variantName(v))
793 - cfg := csvMultiJobConfig(fmt.Sprintf("csv_multi_%d", idx))
794 - payload := fmt.Sprintf("name,value\nrow1,%f\nrow2,20", first)
795 - input := Input{Payload: []byte(payload), Timestamp: time.Now()}
796 - // CSVToJSONMulti returns 2 metrics but pipeline expects 1
797 - // Job engine drops output and returns 1 metric with value=0 and Dimension error
798 - expect := expectation{metrics: 1, values: []float64{0}, active: 1, job: FailureFlags{Dimension: true}, instances: map[string]FailureFlags{"default": {Dimension: true}}}
799 - switch v {
800 - case variantCollectErr:
801 - input.CollectError = errors.New("collect")
802 - expect.metrics = 0
803 - expect.values = nil
804 - expect.active = 1
805 - expect.job = FailureFlags{Collect: true}
806 - expect.instances = map[string]FailureFlags{"default": {Collect: true}}
807 - case variantExtractionErr:
808 - input.Payload = []byte("name,value\nrow1,oops\nrow2,20")
809 - expect.metrics = 1
810 - expect.values = nil
811 - // Extraction error takes precedence; multi-metric warning doesn't add Dimension flag
812 - expect.job = FailureFlags{Extraction: true}
813 - expect.instances = map[string]FailureFlags{"default": {Extraction: true}}
814 - case variantDimensionErr:
815 - input.Payload = []byte("name,value\nrow1,5000\nrow2,20")
816 - expect.metrics = 1
817 - expect.values = nil
818 - expect.job = FailureFlags{Dimension: true}
819 - expect.instances = map[string]FailureFlags{"default": {Dimension: true}}
820 - }
821 - return scenario{name: name, cfg: cfg, input: input, expect: expect}
822 -}
823 -
824 -func variantName(v variant) string {
825 - switch v {
826 - case variantNormal:
827 - return "normal"
828 - case variantCollectErr:
829 - return "collect"
830 - case variantExtractionErr:
831 - return "extract"
832 - case variantDimensionErr:
833 - return "dimension"
834 - case variantLLDErr:
835 - return "lld"
836 - default:
837 - return "unknown"
838 - }
839 -}
840 -
841 -// job config builders -------------------------------------------------------
842 -
843 -func simpleJobConfig(name string) pkgzabbix.JobConfig {
844 - return pkgzabbix.JobConfig{
845 - Name: name,
846 - Collection: pkgzabbix.CollectionConfig{
847 - Type: pkgzabbix.CollectionCommand,
848 - Command: "/usr/bin/true",
849 - },
850 - Pipelines: []pkgzabbix.PipelineConfig{
851 - {
852 - Name: "value",
853 - Context: "zabbix.simple.value",
854 - Dimension: "value",
855 - Unit: "value",
856 - Steps: []zpre.Step{
857 - {Type: zpre.StepTypeJSONPath, Params: "$.value"},
858 - {Type: zpre.StepTypeValidateRange, Params: "0\n100"},
859 - },
860 - },
861 - },
862 - }
863 -}
864 -
865 -func multiPipelineJobConfig(name string) pkgzabbix.JobConfig {
866 - return pkgzabbix.JobConfig{
867 - Name: name,
868 - Collection: pkgzabbix.CollectionConfig{
869 - Type: pkgzabbix.CollectionCommand,
870 - Command: "/usr/bin/true",
871 - },
872 - Pipelines: []pkgzabbix.PipelineConfig{
873 - {
874 - Name: "used",
875 - Context: "zabbix.multi.used",
876 - Dimension: "used",
877 - Unit: "bytes",
878 - Precision: 0,
879 - Steps: []zpre.Step{
880 - {Type: zpre.StepTypeJSONPath, Params: "$.used"},
881 - {Type: zpre.StepTypeValidateRange, Params: "0\n100"},
882 - {Type: zpre.StepTypeMultiplier, Params: "2"},
883 - },
884 - },
885 - {
886 - Name: "free",
887 - Context: "zabbix.multi.free",
888 - Dimension: "free",
889 - Unit: "bytes",
890 - Steps: []zpre.Step{
891 - {Type: zpre.StepTypeJSONPath, Params: "$.free"},
892 - {Type: zpre.StepTypeValidateRange, Params: "0\n100"},
893 - },
894 - },
895 - },
896 - }
897 -}
898 -
899 -func lldJobConfig(name string) pkgzabbix.JobConfig {
900 - return pkgzabbix.JobConfig{
901 - Name: name,
902 - Collection: pkgzabbix.CollectionConfig{Type: pkgzabbix.CollectionCommand, Command: "/usr/bin/true"},
903 - LLD: pkgzabbix.LLDConfig{
904 - Steps: []zpre.Step{{Type: zpre.StepTypeJSONPath, Params: "$.discovery"}},
905 - InstanceTemplate: "{#FSNAME}",
906 - MaxMissing: 0,
907 - },
908 - Pipelines: []pkgzabbix.PipelineConfig{
909 - {
910 - Name: "used",
911 - Context: "zabbix.lld.used",
912 - Dimension: "used",
913 - Unit: "bytes",
914 - Steps: []zpre.Step{
915 - {Type: zpre.StepTypeJSONPath, Params: "$.data[?(@.fs == '{#FSNAME}')].used"},
916 - {Type: zpre.StepTypeValidateRange, Params: "0\n100"},
917 - },
918 - },
919 - {
920 - Name: "free",
921 - Context: "zabbix.lld.free",
922 - Dimension: "free",
923 - Unit: "bytes",
924 - Steps: []zpre.Step{
925 - {Type: zpre.StepTypeJSONPath, Params: "$.data[?(@.fs == '{#FSNAME}')].free"},
926 - {Type: zpre.StepTypeValidateRange, Params: "0\n100"},
927 - },
928 - },
929 - },
930 - }
931 -}
932 -
933 -func csvLLDJobConfig(name string) pkgzabbix.JobConfig {
934 - const csvParams = `,
935 -"
936 -1`
937 - return pkgzabbix.JobConfig{
938 - Name: name,
939 - Collection: pkgzabbix.CollectionConfig{Type: pkgzabbix.CollectionCommand, Command: "/usr/bin/true"},
940 - LLD: pkgzabbix.LLDConfig{
941 - Steps: []zpre.Step{
942 - {Type: zpre.StepTypeCSVToJSON, Params: csvParams},
943 - {Type: zpre.StepTypeJSONPath, Params: "$"},
944 - },
945 - InstanceTemplate: "{#FSNAME}",
946 - MaxMissing: 0,
947 - },
948 - Pipelines: []pkgzabbix.PipelineConfig{
949 - {
950 - Name: "path",
951 - Context: "zabbix.csv.path",
952 - Dimension: "value",
953 - Unit: "characters",
954 - Steps: []zpre.Step{
955 - {Type: zpre.StepTypeCSVToJSON, Params: csvParams},
956 - {Type: zpre.StepTypeJavaScript, Params: pathLengthScript},
957 - },
958 - },
959 - },
960 - }
961 -}
962 -
963 -func xmlLLDJobConfig(name string) pkgzabbix.JobConfig {
964 - script := `var data = JSON.parse(value);
965 -var list = data.files.fs;
966 -if (!Array.isArray(list)) list = [list];
967 -var fsMacro = "{#" + "FSNAME}";
968 -var pathMacro = "{#" + "FSPATH}";
969 -var out = list.map(function(node){
970 - var entry = {};
971 - entry[fsMacro] = node["@name"];
972 - entry[pathMacro] = node["@path"];
973 - return entry;
974 -});
975 -value = JSON.stringify(out);
976 -return value;`
977 - return pkgzabbix.JobConfig{
978 - Name: name,
979 - Collection: pkgzabbix.CollectionConfig{Type: pkgzabbix.CollectionCommand, Command: "/usr/bin/true"},
980 - LLD: pkgzabbix.LLDConfig{
981 - Steps: []zpre.Step{
982 - {Type: zpre.StepTypeXMLToJSON},
983 - {Type: zpre.StepTypeJavaScript, Params: script},
984 - },
985 - InstanceTemplate: "{#FSNAME}",
986 - MaxMissing: 0,
987 - },
988 - Pipelines: []pkgzabbix.PipelineConfig{
989 - {
990 - Name: "path",
991 - Context: "zabbix.xml.path",
992 - Dimension: "value",
993 - Unit: "characters",
994 - Steps: []zpre.Step{
995 - {Type: zpre.StepTypeXMLToJSON},
996 - {Type: zpre.StepTypeJavaScript, Params: script},
997 - {Type: zpre.StepTypeJavaScript, Params: pathLengthScript},
998 - },
999 - },
1000 - },
1001 - }
1002 -}
1003 -
1004 -func snmpLLDJobConfig(name string) pkgzabbix.JobConfig {
1005 - params := "MACRO1\n.1.3.6.1\n0\nMACRO2\n.1.3.6.2\n0"
1006 - return pkgzabbix.JobConfig{
1007 - Name: name,
1008 - Collection: pkgzabbix.CollectionConfig{Type: pkgzabbix.CollectionCommand, Command: "/usr/bin/true"},
1009 - LLD: pkgzabbix.LLDConfig{
1010 - Steps: []zpre.Step{{Type: zpre.StepTypeSNMPWalkToJSON, Params: params}},
1011 - InstanceTemplate: "MACRO1",
1012 - MaxMissing: 0,
1013 - },
1014 - Pipelines: []pkgzabbix.PipelineConfig{
1015 - {
1016 - Name: "snmp_value",
1017 - Context: "zabbix.snmp.value",
1018 - Dimension: "value",
1019 - Unit: "items",
1020 - Steps: []zpre.Step{
1021 - {Type: zpre.StepTypeSNMPWalkToJSON, Params: params},
1022 - {Type: zpre.StepTypeJavaScript, Params: snmpValueScript},
1023 - },
1024 - },
1025 - },
1026 - }
1027 -}
1028 -
1029 -func prometheusJobConfig(name string) pkgzabbix.JobConfig {
1030 - return pkgzabbix.JobConfig{
1031 - Name: name,
1032 - Collection: pkgzabbix.CollectionConfig{Type: pkgzabbix.CollectionCommand, Command: "/usr/bin/true"},
1033 - LLD: pkgzabbix.LLDConfig{
1034 - Steps: []zpre.Step{
1035 - {Type: zpre.StepTypePrometheusToJSON},
1036 - {Type: zpre.StepTypeJavaScript, Params: promDiscoveryScript},
1037 - },
1038 - InstanceTemplate: "fs_{#MOUNT}",
1039 - MaxMissing: 0,
1040 - },
1041 - Pipelines: []pkgzabbix.PipelineConfig{
1042 - {
1043 - Name: "free",
1044 - Context: "zabbix.prom.free",
1045 - Dimension: "free",
1046 - Unit: "bytes",
1047 - Steps: []zpre.Step{
1048 - {Type: zpre.StepTypePrometheusToJSON},
1049 - {Type: zpre.StepTypeJavaScript, Params: promValueScript},
1050 - {Type: zpre.StepTypeValidateRange, Params: "0\n1000"},
1051 - },
1052 - },
1053 - },
1054 - }
1055 -}
1056 -
1057 -func xpathJobConfig(name string) pkgzabbix.JobConfig {
1058 - return pkgzabbix.JobConfig{
1059 - Name: name,
1060 - Collection: pkgzabbix.CollectionConfig{Type: pkgzabbix.CollectionCommand, Command: "/usr/bin/true"},
1061 - Pipelines: []pkgzabbix.PipelineConfig{
1062 - {
1063 - Name: "cpu",
1064 - Context: "zabbix.xpath.cpu",
1065 - Dimension: "cpu",
1066 - Unit: "celsius",
1067 - Steps: []zpre.Step{
1068 - {Type: zpre.StepTypeXPath, Params: "string(/sensors/sensor[@id='cpu']/text())"},
1069 - {Type: zpre.StepTypeValidateRange, Params: "0\n150"},
1070 - },
1071 - },
1072 - {
1073 - Name: "gpu",
1074 - Context: "zabbix.xpath.gpu",
1075 - Dimension: "gpu",
1076 - Unit: "celsius",
1077 - Steps: []zpre.Step{
1078 - {Type: zpre.StepTypeXPath, Params: "string(/sensors/sensor[@id='gpu']/text())"},
1079 - {Type: zpre.StepTypeValidateRange, Params: "0\n150"},
1080 - },
1081 - },
1082 - },
1083 - }
1084 -}
1085 -
1086 -func csvMultiJobConfig(name string) pkgzabbix.JobConfig {
1087 - const csvParams = `,
1088 -"
1089 -1`
1090 - return pkgzabbix.JobConfig{
1091 - Name: name,
1092 - Collection: pkgzabbix.CollectionConfig{Type: pkgzabbix.CollectionCommand, Command: "/usr/bin/true"},
1093 - Pipelines: []pkgzabbix.PipelineConfig{
1094 - {
1095 - Name: "first_value",
1096 - Context: "zabbix.csv.multi",
1097 - Dimension: "value",
1098 - Unit: "items",
1099 - Steps: []zpre.Step{
1100 - {Type: zpre.StepTypeCSVToJSONMulti, Params: csvParams},
1101 - {Type: zpre.StepTypeJavaScript, Params: csvMultiValueScript},
1102 - {Type: zpre.StepTypeValidateRange, Params: "0\n1000"},
1103 - },
1104 - },
1105 - },
1106 - }
1107 -}
src/go/plugin/scripts.d/pkg/zabbix/runtime.go deleted
-508
@@ -1,508 +0,0 @@
1 -// SPDX-License-Identifier: GPL-3.0-or-later
2 -
3 -package zabbix
4 -
5 -import (
6 - "bytes"
7 - "context"
8 - "fmt"
9 - "io"
10 - "net/http"
11 - "strings"
12 - "sync"
13 - "time"
14 -
15 - "github.com/gosnmp/gosnmp"
16 -
17 - "github.com/netdata/netdata/go/plugins/logger"
18 - "github.com/netdata/netdata/go/plugins/pkg/confopt"
19 - "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/ndexec"
20 - "github.com/netdata/netdata/go/plugins/plugin/scripts.d/pkg/runtime"
21 - "github.com/netdata/netdata/go/plugins/plugin/scripts.d/pkg/schedulers"
22 - "github.com/netdata/netdata/go/plugins/plugin/scripts.d/pkg/spec"
23 - zpre "github.com/netdata/netdata/go/plugins/plugin/scripts.d/pkg/zabbixpreproc"
24 -)
25 -
26 -// Runtime orchestrates Zabbix jobs using the shared scripts.d scheduler infrastructure.
27 -type Runtime struct {
28 - proc *zpre.Preprocessor
29 - handles map[string][]*schedulers.JobHandle
30 - mu sync.RWMutex
31 -}
32 -
33 -// NewRuntime wires validated jobs into schedulers and starts them.
34 -func NewRuntime(configs []JobConfig, proc *zpre.Preprocessor, log *logger.Logger, emitter runtime.ResultEmitter, vnodeLookup func(spec.JobSpec) runtime.VnodeInfo) (*Runtime, error) {
35 - if len(configs) == 0 {
36 - return nil, fmt.Errorf("zabbix runtime requires at least one job")
37 - }
38 - if proc == nil {
39 - return nil, fmt.Errorf("zabbix runtime requires a preprocessor instance")
40 - }
41 -
42 - normalized := make([]JobConfig, len(configs))
43 - for i := range configs {
44 - normalized[i] = configs[i]
45 - if strings.TrimSpace(normalized[i].Scheduler) == "" {
46 - normalized[i].Scheduler = "default"
47 - }
48 - }
49 -
50 - if emitter == nil {
51 - emitter = runtime.NewNoopEmitter()
52 - }
53 - collector := newJobCollector(normalized, log)
54 - grouped := groupByScheduler(normalized)
55 - handles := make(map[string][]*schedulers.JobHandle, len(grouped))
56 -
57 - detachAll := func(h map[string][]*schedulers.JobHandle) {
58 - for _, list := range h {
59 - for _, handle := range list {
60 - schedulers.DetachJob(handle)
61 - }
62 - }
63 - }
64 -
65 - for name, cfgs := range grouped {
66 - if _, ok := schedulers.Get(name); !ok {
67 - detachAll(handles)
68 - return nil, fmt.Errorf("scheduler '%s' not defined", name)
69 - }
70 - jobSpecs, err := buildJobSpecs(cfgs)
71 - if err != nil {
72 - detachAll(handles)
73 - return nil, err
74 - }
75 - for i := range jobSpecs {
76 - var vnode runtime.VnodeInfo
77 - if vnodeLookup != nil {
78 - vnode = runtime.CloneVnodeInfo(vnodeLookup(jobSpecs[i]))
79 - }
80 - reg := runtime.JobRegistration{
81 - Spec: jobSpecs[i],
82 - Runner: collector.Run,
83 - Emitter: emitter,
84 - Vnode: vnode,
85 - }
86 - handle, err := schedulers.AttachJob(name, reg, log)
87 - if err != nil {
88 - detachAll(handles)
89 - return nil, err
90 - }
91 - handles[name] = append(handles[name], handle)
92 - }
93 - }
94 -
95 - return &Runtime{proc: proc, handles: handles}, nil
96 -}
97 -
98 -// Collect aggregates metrics from every scheduler.
99 -func (r *Runtime) Collect() map[string]int64 {
100 - r.mu.RLock()
101 - defer r.mu.RUnlock()
102 - metrics := make(map[string]int64)
103 - for scheduler := range r.handles {
104 - data := schedulers.CollectMetrics(scheduler)
105 - for k, v := range data {
106 - metrics[k] += v
107 - }
108 - }
109 - if len(metrics) == 0 {
110 - return nil
111 - }
112 - return metrics
113 -}
114 -
115 -// Stop shuts down all managed schedulers and closes the emitter.
116 -func (r *Runtime) Stop() {
117 - r.mu.Lock()
118 - defer r.mu.Unlock()
119 - for scheduler, list := range r.handles {
120 - for _, handle := range list {
121 - schedulers.DetachJob(handle)
122 - }
123 - delete(r.handles, scheduler)
124 - }
125 -}
126 -
127 -func groupByScheduler(cfgs []JobConfig) map[string][]JobConfig {
128 - res := make(map[string][]JobConfig)
129 - for _, cfg := range cfgs {
130 - name := schedulerName(cfg.Scheduler)
131 - cfg.Scheduler = name
132 - res[name] = append(res[name], cfg)
133 - }
134 - return res
135 -}
136 -
137 -func schedulerName(name string) string {
138 - if strings.TrimSpace(name) == "" {
139 - return "default"
140 - }
141 - return name
142 -}
143 -
144 -func buildJobSpecs(cfgs []JobConfig) ([]spec.JobSpec, error) {
145 - specs := make([]spec.JobSpec, len(cfgs))
146 - for i := range cfgs {
147 - cfg := cfgs[i]
148 - job := spec.JobConfig{
149 - Name: cfg.Name,
150 - Plugin: pluginName(cfg),
151 - Args: append([]string{}, cfg.Collection.Args...),
152 - Timeout: confopt.Duration(defaultTimeout(cfg)),
153 - Scheduler: cfg.Scheduler,
154 - Vnode: cfg.Vnode,
155 - }
156 - job.CheckInterval = confopt.Duration(cfg.IntervalDuration())
157 - sp, err := job.ToSpec()
158 - if err != nil {
159 - return nil, err
160 - }
161 - specs[i] = sp
162 - }
163 - return specs, nil
164 -}
165 -
166 -func pluginName(cfg JobConfig) string {
167 - switch cfg.Collection.Type {
168 - case CollectionHTTP:
169 - return "zabbix-http"
170 - case CollectionSNMP:
171 - return "zabbix-snmp"
172 - case CollectionCommand, "":
173 - return strings.TrimSpace(cfg.Collection.Command)
174 - default:
175 - return fmt.Sprintf("zabbix-%s", cfg.Collection.Type)
176 - }
177 -}
178 -
179 -func defaultTimeout(cfg JobConfig) time.Duration {
180 - if d := cfg.Collection.TimeoutDuration(); d > 0 {
181 - return d
182 - }
183 - return 30 * time.Second
184 -}
185 -
186 -type jobCollector struct {
187 - log *logger.Logger
188 - jobs map[string]JobConfig
189 -}
190 -
191 -func newJobCollector(cfgs []JobConfig, log *logger.Logger) *jobCollector {
192 - jobs := make(map[string]JobConfig, len(cfgs))
193 - for _, cfg := range cfgs {
194 - jobs[cfg.Name] = cfg
195 - }
196 - return &jobCollector{log: log, jobs: jobs}
197 -}
198 -
199 -func (c *jobCollector) Run(ctx context.Context, job runtime.JobRuntime, timeout time.Duration) ([]byte, string, ndexec.ResourceUsage, error) {
200 - cfg, ok := c.jobs[job.Spec.Name]
201 - if !ok {
202 - return nil, "", ndexec.ResourceUsage{}, fmt.Errorf("zabbix: unknown job %s", job.Spec.Name)
203 - }
204 - macros := buildCollectionMacros(cfg, job)
205 - switch cfg.Collection.Type {
206 - case CollectionCommand, "":
207 - return c.runCommand(ctx, job, cfg, macros, timeout)
208 - case CollectionHTTP:
209 - return c.runHTTP(ctx, job, cfg, macros, timeout)
210 - case CollectionSNMP:
211 - return c.runSNMP(ctx, job, cfg, macros, timeout)
212 - default:
213 - return nil, "", ndexec.ResourceUsage{}, fmt.Errorf("zabbix: unsupported collection type %q", cfg.Collection.Type)
214 - }
215 -}
216 -
217 -func (c *jobCollector) runCommand(ctx context.Context, job runtime.JobRuntime, cfg JobConfig, macros map[string]string, timeout time.Duration) ([]byte, string, ndexec.ResourceUsage, error) {
218 - cmd := strings.TrimSpace(expandTemplate(cfg.Collection.Command, macros))
219 - if cmd == "" {
220 - return nil, "", ndexec.ResourceUsage{}, fmt.Errorf("command is required for command collection")
221 - }
222 - var env []string
223 - for k, v := range expandStringMap(cfg.Collection.Environment, macros) {
224 - env = append(env, fmt.Sprintf("%s=%s", k, v))
225 - }
226 - opts := ndexec.RunOptions{Env: env}
227 - args := expandStringSlice(cfg.Collection.Args, macros)
228 - data, cmdStr, usage, err := ndexec.RunUnprivilegedWithOptionsUsage(c.log, timeout, opts, cmd, args...)
229 - return data, cmdStr, usage, err
230 -}
231 -
232 -func (c *jobCollector) runHTTP(ctx context.Context, job runtime.JobRuntime, cfg JobConfig, macros map[string]string, timeout time.Duration) ([]byte, string, ndexec.ResourceUsage, error) {
233 - httpCfg := cfg.Collection.HTTP
234 - if httpCfg.URL == "" {
235 - httpCfg.URL = cfg.Collection.URL
236 - }
237 - if httpCfg.URL == "" {
238 - return nil, "", ndexec.ResourceUsage{}, fmt.Errorf("http.url is required")
239 - }
240 - method := strings.ToUpper(strings.TrimSpace(httpCfg.Method))
241 - if method == "" {
242 - method = strings.ToUpper(strings.TrimSpace(cfg.Collection.Method))
243 - }
244 - if method == "" {
245 - method = "GET"
246 - }
247 - urlStr := expandTemplate(httpCfg.URL, macros)
248 - var body io.Reader
249 - if httpCfg.Body != "" {
250 - body = strings.NewReader(expandTemplate(httpCfg.Body, macros))
251 - } else if cfg.Collection.Body != "" {
252 - body = strings.NewReader(expandTemplate(cfg.Collection.Body, macros))
253 - }
254 - req, err := http.NewRequestWithContext(ctx, method, urlStr, body)
255 - if err != nil {
256 - return nil, "", ndexec.ResourceUsage{}, err
257 - }
258 - headers := mergeStringMap(cfg.Collection.Headers, httpCfg.Headers)
259 - for k, v := range headers {
260 - req.Header.Set(k, expandTemplate(v, macros))
261 - }
262 - if httpCfg.Username != "" {
263 - user := expandTemplate(httpCfg.Username, macros)
264 - pass := expandTemplate(httpCfg.Password, macros)
265 - req.SetBasicAuth(user, pass)
266 - }
267 - if httpCfg.TLS {
268 - if req.URL.Scheme == "" {
269 - req.URL.Scheme = "https"
270 - } else if req.URL.Scheme != "https" {
271 - req.URL.Scheme = "https"
272 - }
273 - }
274 - client := &http.Client{Timeout: timeout}
275 - resp, err := client.Do(req)
276 - if err != nil {
277 - return nil, "", ndexec.ResourceUsage{}, err
278 - }
279 - defer resp.Body.Close()
280 - buf := &bytes.Buffer{}
281 - if _, err := io.Copy(buf, resp.Body); err != nil {
282 - return nil, "", ndexec.ResourceUsage{}, err
283 - }
284 - return buf.Bytes(), fmt.Sprintf("http %s", req.URL.String()), ndexec.ResourceUsage{}, nil
285 -}
286 -
287 -func (c *jobCollector) runSNMP(ctx context.Context, job runtime.JobRuntime, cfg JobConfig, macros map[string]string, timeout time.Duration) ([]byte, string, ndexec.ResourceUsage, error) {
288 - snmpCfg := cfg.Collection.SNMP
289 - target := strings.TrimSpace(expandTemplate(snmpCfg.Target, macros))
290 - oid := strings.TrimSpace(expandTemplate(snmpCfg.OID, macros))
291 - if target == "" || oid == "" {
292 - return nil, "", ndexec.ResourceUsage{}, fmt.Errorf("snmp.target and snmp.oid are required")
293 - }
294 - snmpClient := &gosnmp.GoSNMP{
295 - Target: target,
296 - Port: 161,
297 - Timeout: timeout,
298 - Community: expandTemplate(snmpCfg.Community, macros),
299 - MaxOids: 1,
300 - }
301 - if val := strings.TrimSpace(snmpCfg.Context); val != "" {
302 - snmpClient.ContextName = expandTemplate(val, macros)
303 - }
304 - switch strings.TrimSpace(strings.ToLower(snmpCfg.Version)) {
305 - case "3", "v3":
306 - snmpClient.Version = gosnmp.Version3
307 - security := &gosnmp.UsmSecurityParameters{
308 - UserName: expandTemplate(snmpCfg.User, macros),
309 - AuthenticationPassphrase: expandTemplate(snmpCfg.AuthPass, macros),
310 - PrivacyPassphrase: expandTemplate(snmpCfg.PrivPass, macros),
311 - }
312 - if strings.EqualFold(snmpCfg.AuthProto, "sha") {
313 - security.AuthenticationProtocol = gosnmp.SHA
314 - } else {
315 - security.AuthenticationProtocol = gosnmp.MD5
316 - }
317 - if strings.EqualFold(snmpCfg.PrivProto, "aes") {
318 - security.PrivacyProtocol = gosnmp.AES
319 - } else {
320 - security.PrivacyProtocol = gosnmp.DES
321 - }
322 - snmpClient.SecurityParameters = security
323 - snmpClient.SecurityModel = gosnmp.UserSecurityModel
324 - snmpClient.MsgFlags = gosnmp.AuthPriv
325 - case "1", "v1":
326 - snmpClient.Version = gosnmp.Version1
327 - default:
328 - snmpClient.Version = gosnmp.Version2c
329 - }
330 - if err := snmpClient.Connect(); err != nil {
331 - return nil, "", ndexec.ResourceUsage{}, err
332 - }
333 - defer snmpClient.Conn.Close()
334 - if requiresSNMPWalk(cfg) {
335 - entries, err := snmpClient.BulkWalkAll(oid)
336 - if err != nil {
337 - return nil, "", ndexec.ResourceUsage{}, err
338 - }
339 - if len(entries) == 0 {
340 - return nil, "", ndexec.ResourceUsage{}, fmt.Errorf("snmp: walk returned no data")
341 - }
342 - payload := formatSNMPWalk(entries)
343 - return []byte(payload), fmt.Sprintf("snmp %s %s walk", target, oid), ndexec.ResourceUsage{}, nil
344 - }
345 - result, err := snmpClient.Get([]string{oid})
346 - if err != nil {
347 - return nil, "", ndexec.ResourceUsage{}, err
348 - }
349 - if len(result.Variables) == 0 {
350 - return nil, "", ndexec.ResourceUsage{}, fmt.Errorf("snmp: no variables returned")
351 - }
352 - val := formatSNMPValue(result.Variables[0])
353 - return []byte(val), fmt.Sprintf("snmp %s %s", target, oid), ndexec.ResourceUsage{}, nil
354 -}
355 -
356 -func mergeStringMap(base map[string]string, override map[string]string) map[string]string {
357 - if len(base) == 0 && len(override) == 0 {
358 - return nil
359 - }
360 - out := make(map[string]string, len(base)+len(override))
361 - for k, v := range base {
362 - out[k] = v
363 - }
364 - for k, v := range override {
365 - out[k] = v
366 - }
367 - return out
368 -}
369 -
370 -func formatSNMPValue(pdu gosnmp.SnmpPDU) string {
371 - switch v := pdu.Value.(type) {
372 - case string:
373 - return v
374 - case []byte:
375 - return string(v)
376 - case int, uint, uint32, uint64, int64:
377 - return fmt.Sprintf("%v", v)
378 - default:
379 - return fmt.Sprintf("%v", v)
380 - }
381 -}
382 -
383 -func formatSNMPWalk(vars []gosnmp.SnmpPDU) string {
384 - var b strings.Builder
385 - for i, entry := range vars {
386 - if i > 0 {
387 - b.WriteByte('\n')
388 - }
389 - fmt.Fprintf(&b, "%s = %s: %s", entry.Name, snmpTypeName(entry.Type), formatSNMPValue(entry))
390 - }
391 - return b.String()
392 -}
393 -
394 -func snmpTypeName(kind gosnmp.Asn1BER) string {
395 - switch kind {
396 - case gosnmp.OctetString:
397 - return "STRING"
398 - case gosnmp.Integer:
399 - return "INTEGER"
400 - case gosnmp.Counter32:
401 - return "Counter32"
402 - case gosnmp.Gauge32:
403 - return "Gauge32"
404 - case gosnmp.IPAddress:
405 - return "IpAddress"
406 - case gosnmp.TimeTicks:
407 - return "Timeticks"
408 - case gosnmp.Counter64:
409 - return "Counter64"
410 - case gosnmp.ObjectIdentifier:
411 - return "OID"
412 - case gosnmp.Opaque:
413 - return "Opaque"
414 - default:
415 - return "Unknown"
416 - }
417 -}
418 -
419 -func requiresSNMPWalk(cfg JobConfig) bool {
420 - for _, pipe := range cfg.Pipelines {
421 - for _, step := range pipe.Steps {
422 - switch step.Type {
423 - case zpre.StepTypeSNMPWalkValue, zpre.StepTypeSNMPWalkToJSON, zpre.StepTypeSNMPWalkToJSONMulti:
424 - return true
425 - }
426 - }
427 - }
428 - return false
429 -}
430 -
431 -func buildCollectionMacros(cfg JobConfig, job runtime.JobRuntime) map[string]string {
432 - macros := make(map[string]string)
433 - host := firstNonEmpty(strings.TrimSpace(job.Vnode.Hostname), strings.TrimSpace(job.Spec.Vnode), strings.TrimSpace(cfg.Vnode), cfg.Name)
434 - if host == "" {
435 - host = cfg.Name
436 - }
437 - alias := firstNonEmpty(strings.TrimSpace(job.Vnode.Labels["_alias"]), host)
438 - ip := strings.TrimSpace(job.Vnode.Labels["_address"])
439 - conn := firstNonEmpty(ip, host)
440 - macros["{HOST.NAME}"] = host
441 - macros["{HOST.HOST}"] = host
442 - macros["{HOST.DNS}"] = host
443 - macros["{HOST.ALIAS}"] = alias
444 - macros["{HOST.IP}"] = ip
445 - macros["{HOST.CONN}"] = conn
446 - macros["{ITEM.NAME}"] = cfg.Name
447 - macros["{ITEM.ID}"] = cfg.Name
448 - macros["{ITEM.KEY}"] = pluginName(cfg)
449 - for k, v := range cfg.UserMacros {
450 - key := strings.TrimSpace(strings.ToUpper(k))
451 - if key == "" {
452 - continue
453 - }
454 - val := v
455 - switch {
456 - case strings.HasPrefix(key, "{$") && strings.HasSuffix(key, "}"):
457 - macros[key] = val
458 - case strings.HasPrefix(key, "$") && strings.HasSuffix(key, "$"):
459 - macros[key] = val
460 - default:
461 - macros[fmt.Sprintf("$%s$", key)] = val
462 - macros[fmt.Sprintf("{$%s}", key)] = val
463 - }
464 - }
465 - return macros
466 -}
467 -
468 -func expandTemplate(tmpl string, macros map[string]string) string {
469 - if tmpl == "" || len(macros) == 0 {
470 - return tmpl
471 - }
472 - res := tmpl
473 - for k, v := range macros {
474 - res = strings.ReplaceAll(res, k, v)
475 - }
476 - return res
477 -}
478 -
479 -func expandStringSlice(values []string, macros map[string]string) []string {
480 - if len(values) == 0 {
481 - return values
482 - }
483 - res := make([]string, len(values))
484 - for i, v := range values {
485 - res[i] = expandTemplate(v, macros)
486 - }
487 - return res
488 -}
489 -
490 -func expandStringMap(values map[string]string, macros map[string]string) map[string]string {
491 - if len(values) == 0 {
492 - return values
493 - }
494 - res := make(map[string]string, len(values))
495 - for k, v := range values {
496 - res[k] = expandTemplate(v, macros)
497 - }
498 - return res
499 -}
500 -
501 -func firstNonEmpty(values ...string) string {
502 - for _, v := range values {
503 - if strings.TrimSpace(v) != "" {
504 - return strings.TrimSpace(v)
505 - }
506 - }
507 - return ""
508 -}
src/go/plugin/scripts.d/pkg/zabbix/runtime_test.go deleted
-233
@@ -1,233 +0,0 @@
1 -package zabbix
2 -
3 -import (
4 - "context"
5 - "net/http"
6 - "net/http/httptest"
7 - "testing"
8 - "time"
9 -
10 - "github.com/netdata/netdata/go/plugins/pkg/confopt"
11 - "github.com/netdata/netdata/go/plugins/plugin/scripts.d/pkg/runtime"
12 - "github.com/netdata/netdata/go/plugins/plugin/scripts.d/pkg/spec"
13 - zpre "github.com/netdata/netdata/go/plugins/plugin/scripts.d/pkg/zabbixpreproc"
14 -)
15 -
16 -func TestBuildJobSpecsCommandPlugin(t *testing.T) {
17 - cfg := JobConfig{
18 - Name: "cmd_job",
19 - Scheduler: "default",
20 - Collection: CollectionConfig{
21 - Type: CollectionCommand,
22 - Command: "/usr/libexec/zabbix/collector",
23 - },
24 - }
25 - cfg.Pipelines = []PipelineConfig{{
26 - Name: "usage",
27 - Context: "zabbix.cmd.usage",
28 - Dimension: "value",
29 - Unit: "%",
30 - Steps: []zpre.Step{{Type: zpre.StepTypeJSONPath, Params: "$.value"}},
31 - }}
32 - specs, err := buildJobSpecs([]JobConfig{cfg})
33 - if err != nil {
34 - t.Fatalf("buildJobSpecs returned error: %v", err)
35 - }
36 - if specs[0].Plugin != cfg.Collection.Command {
37 - t.Fatalf("expected plugin %q, got %q", cfg.Collection.Command, specs[0].Plugin)
38 - }
39 -}
40 -
41 -func TestBuildJobSpecsHTTPPlugin(t *testing.T) {
42 - cfg := JobConfig{
43 - Name: "http_job",
44 - Collection: CollectionConfig{
45 - Type: CollectionHTTP,
46 - HTTP: HTTPConfig{URL: "http://127.0.0.1/api"},
47 - },
48 - }
49 - cfg.Pipelines = []PipelineConfig{{
50 - Name: "latency",
51 - Context: "zabbix.http.latency",
52 - Dimension: "value",
53 - Unit: "ms",
54 - Steps: []zpre.Step{{Type: zpre.StepTypeJSONPath, Params: "$.value"}},
55 - }}
56 - specs, err := buildJobSpecs([]JobConfig{cfg})
57 - if err != nil {
58 - t.Fatalf("buildJobSpecs returned error: %v", err)
59 - }
60 - if specs[0].Plugin != "zabbix-http" {
61 - t.Fatalf("expected synthetic plugin 'zabbix-http', got %q", specs[0].Plugin)
62 - }
63 -}
64 -
65 -func TestBuildJobSpecsSNMPPlugin(t *testing.T) {
66 - cfg := JobConfig{
67 - Name: "snmp_job",
68 - Collection: CollectionConfig{
69 - Type: CollectionSNMP,
70 - SNMP: SNMPConfig{Target: "127.0.0.1", OID: "1.3.6.1.2.1.1.1.0"},
71 - },
72 - }
73 - cfg.Pipelines = []PipelineConfig{{
74 - Name: "value",
75 - Context: "zabbix.snmp.value",
76 - Dimension: "value",
77 - Unit: "",
78 - Steps: []zpre.Step{{Type: zpre.StepTypeJSONPath, Params: "$.value"}},
79 - }}
80 - specs, err := buildJobSpecs([]JobConfig{cfg})
81 - if err != nil {
82 - t.Fatalf("buildJobSpecs returned error: %v", err)
83 - }
84 - if specs[0].Plugin != "zabbix-snmp" {
85 - t.Fatalf("expected synthetic plugin 'zabbix-snmp', got %q", specs[0].Plugin)
86 - }
87 -}
88 -
89 -func TestBuildJobSpecsUpdateEvery(t *testing.T) {
90 - cfg := JobConfig{
91 - Name: "interval_job",
92 - UpdateEvery: 42,
93 - Collection: CollectionConfig{
94 - Type: CollectionCommand,
95 - Command: "/usr/bin/true",
96 - },
97 - }
98 - cfg.Pipelines = []PipelineConfig{{
99 - Name: "value",
100 - Context: "zabbix.interval.value",
101 - Dimension: "value",
102 - Unit: "",
103 - Steps: []zpre.Step{{Type: zpre.StepTypeJSONPath, Params: "$.value"}},
104 - }}
105 - specs, err := buildJobSpecs([]JobConfig{cfg})
106 - if err != nil {
107 - t.Fatalf("buildJobSpecs returned error: %v", err)
108 - }
109 - want := time.Duration(cfg.UpdateEvery) * time.Second
110 - if specs[0].CheckInterval != want {
111 - t.Fatalf("expected check interval %s, got %s", want, specs[0].CheckInterval)
112 - }
113 -}
114 -
115 -func TestBuildJobSpecsDefaultInterval(t *testing.T) {
116 - cfg := JobConfig{
117 - Name: "default_interval_job",
118 - Collection: CollectionConfig{
119 - Type: CollectionCommand,
120 - Command: "/usr/bin/true",
121 - Timeout: confopt.Duration(5 * time.Second),
122 - },
123 - }
124 - cfg.Pipelines = []PipelineConfig{{
125 - Name: "value",
126 - Context: "zabbix.interval.value",
127 - Dimension: "value",
128 - Unit: "",
129 - Steps: []zpre.Step{{Type: zpre.StepTypeJSONPath, Params: "$.value"}},
130 - }}
131 - specs, err := buildJobSpecs([]JobConfig{cfg})
132 - if err != nil {
133 - t.Fatalf("buildJobSpecs returned error: %v", err)
134 - }
135 - if specs[0].CheckInterval != time.Minute {
136 - t.Fatalf("expected default check interval %s, got %s", time.Minute, specs[0].CheckInterval)
137 - }
138 -}
139 -
140 -func TestBuildCollectionMacros(t *testing.T) {
141 - cfg := JobConfig{Name: "disk", Collection: CollectionConfig{Type: CollectionCommand, Command: "/bin/true"}}
142 - job := runtime.JobRuntime{Spec: spec.JobSpec{Name: cfg.Name}, Vnode: runtime.VnodeInfo{Hostname: "agent", Labels: map[string]string{"_address": "10.0.0.1", "_alias": "agent-alias"}}}
143 - macros := buildCollectionMacros(cfg, job)
144 - if macros["{HOST.NAME}"] != "agent" {
145 - t.Fatalf("expected host macro 'agent', got %q", macros["{HOST.NAME}"])
146 - }
147 - if macros["{HOST.IP}"] != "10.0.0.1" {
148 - t.Fatalf("expected host ip macro '10.0.0.1', got %q", macros["{HOST.IP}"])
149 - }
150 - if macros["{ITEM.KEY}"] == "" {
151 - t.Fatalf("expected item key macro to be populated")
152 - }
153 -}
154 -
155 -func TestRunHTTPMacroExpansion(t *testing.T) {
156 - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
157 - if r.Method != http.MethodPost {
158 - t.Fatalf("expected POST method, got %s", r.Method)
159 - }
160 - if r.Header.Get("X-Host") != "agent" {
161 - t.Fatalf("expected header X-Host=agent, got %s", r.Header.Get("X-Host"))
162 - }
163 - if r.URL.Path != "/api/agent" {
164 - t.Fatalf("unexpected path %s", r.URL.Path)
165 - }
166 - w.WriteHeader(http.StatusOK)
167 - _, _ = w.Write([]byte("ok"))
168 - }))
169 - defer srv.Close()
170 -
171 - cfg := JobConfig{
172 - Name: "http_job",
173 - Collection: CollectionConfig{
174 - Type: CollectionHTTP,
175 - HTTP: HTTPConfig{
176 - URL: srv.URL + "/api/{HOST.NAME}",
177 - Headers: map[string]string{"X-Host": "{HOST.NAME}"},
178 - Body: "value={ITEM.NAME}",
179 - },
180 - Method: "post",
181 - },
182 - }
183 - collector := newJobCollector([]JobConfig{cfg}, nil)
184 - job := runtime.JobRuntime{Spec: spec.JobSpec{Name: cfg.Name}, Vnode: runtime.VnodeInfo{Hostname: "agent"}}
185 - macros := buildCollectionMacros(cfg, job)
186 - data, _, _, err := collector.runHTTP(context.Background(), job, cfg, macros, time.Second)
187 - if err != nil {
188 - t.Fatalf("runHTTP returned error: %v", err)
189 - }
190 - if string(data) != "ok" {
191 - t.Fatalf("expected response 'ok', got %q", string(data))
192 - }
193 -}
194 -
195 -func TestRequiresSNMPWalk(t *testing.T) {
196 - cfg := JobConfig{
197 - Name: "snmp_walk",
198 - Collection: CollectionConfig{
199 - Type: CollectionSNMP,
200 - SNMP: SNMPConfig{Target: "127.0.0.1", OID: ".1.3.6.1"},
201 - },
202 - Pipelines: []PipelineConfig{{
203 - Name: "walk",
204 - Steps: []zpre.Step{{Type: zpre.StepTypeSNMPWalkToJSON, Params: "{#IFNAME}\n.1.3.6.1\n1"}},
205 - Context: "zabbix.snmp.walk",
206 - Dimension: "value",
207 - Unit: "value",
208 - }},
209 - }
210 - if !requiresSNMPWalk(cfg) {
211 - t.Fatalf("expected requiresSNMPWalk to be true")
212 - }
213 -}
214 -
215 -func TestRunSNMPWalk(t *testing.T) {
216 - cfg := JobConfig{
217 - Name: "snmp_walk",
218 - Collection: CollectionConfig{
219 - Type: CollectionSNMP,
220 - SNMP: SNMPConfig{Target: "127.0.0.1", OID: ".1.3.6.1"},
221 - },
222 - Pipelines: []PipelineConfig{{
223 - Name: "walk",
224 - Steps: []zpre.Step{{Type: zpre.StepTypeSNMPWalkToJSON, Params: "{#IFNAME}\n.1.3.6.1\n1"}},
225 - Context: "zabbix.snmp.walk",
226 - Dimension: "value",
227 - Unit: "value",
228 - }},
229 - }
230 - if !requiresSNMPWalk(cfg) {
231 - t.Fatalf("expected requiresSNMPWalk to be true")
232 - }
233 -}
src/go/plugin/scripts.d/pkg/zabbixpreproc/API-DESIGN.md deleted
-220
@@ -1,220 +0,0 @@
1 -# Zabbix Preprocessing Library - API Design Specification
2 -
3 -**Last Updated:** 2025-11-14
4 -**Status:** Authoritative Requirements
5 -
6 ----
7 -
8 -## USER REQUIREMENTS (FINAL - DO NOT CHANGE)
9 -
10 -### Input Parameters
11 -The Netdata plugin will provide **TWO parameters**:
12 -1. **Unique identifier** for continuity (state tracking)
13 -2. **Just collected value** (the data to preprocess)
14 -
15 -### State Management
16 -- Library **MUST** maintain state based on the unique identifier
17 -- Unique identifier is **unique per shard, NOT across shards**
18 -- Example: "item1" in shard "A" is different from "item1" in shard "B"
19 -
20 -### Output Format
21 -The returned value of any collected sample is:
22 -1. **Array of metrics** (optionally with labels)
23 -2. **Possibly logs/events** to be committed to logs
24 -3. **Structured errors** for the caller to handle and understand what happened
25 -
26 -### Implementation Authority
27 -**Everything else is implementation details that the library author decides.**
28 -
29 ----
30 -
31 -## IMPLEMENTATION DECISIONS (CLAUDE'S AUTHORITY)
32 -
33 -### API Design Decision
34 -
35 -**Chosen Approach:** Explicit itemID parameter + Result struct return
36 -
37 -```go
38 -// Execute processes a single preprocessing step for a specific item
39 -func (p *Preprocessor) Execute(itemID string, value Value, step Step) (Result, error)
40 -
41 -// ExecutePipeline processes multiple steps for a specific item
42 -func (p *Preprocessor) ExecutePipeline(itemID string, value Value, steps []Step) (Result, error)
43 -
44 -// Result contains the output of preprocessing
45 -type Result struct {
46 - Metrics []Metric // Array of extracted metrics
47 - Logs []string // Optional log entries (future use)
48 - Error error // Overall error if preprocessing failed
49 -}
50 -
51 -// Metric represents a single metric output
52 -type Metric struct {
53 - Name string // Metric name
54 - Value string // Metric value
55 - Type ValueType // Value type
56 - Labels map[string]string // Optional labels
57 -}
58 -```
59 -
60 -**Rationale:**
61 -1. **itemID parameter**: User said "unique identifier for continuity" and "just collected value" as TWO parameters
62 -2. **Result return**: User said "array of metrics" not single value
63 -3. **Error in Result**: Allows partial success (some metrics succeed, some fail)
64 -
65 -### State Key Format
66 -
67 -State keys will be: `"{shardID}:{itemID}:{operation}:{params_hash}"`
68 -
69 -- `shardID`: From NewPreprocessor(shardID)
70 -- `itemID`: From Execute(itemID, ...)
71 -- `operation`: "delta_value", "delta_speed", "throttle_value", "throttle_timed"
72 -- `params_hash`: Hash of step.Params for multi-param operations
73 -
74 -**Example:** `"shard1:item123:delta_value:"`
75 -
76 -### Thread Safety
77 -
78 -- State map already protected with sync.RWMutex (P0 #5 resolved)
79 -- itemID is per-call, no shared state
80 -- Multiple goroutines can call Execute() concurrently with different itemIDs
81 -
82 -### Migration Path
83 -
84 -**Phase 1 (Current):** Keep old API for backward compatibility
85 -```go
86 -// DEPRECATED: Use Execute(itemID, value, step) instead
87 -func (p *Preprocessor) execute(value Value, step Step) (Value, error) {
88 - // Calls Execute("", value, step) - empty itemID = global state
89 -}
90 -```
91 -
92 -**Phase 2 (After Netdata integration):** Remove deprecated API
93 -
94 -### Multi-Metric Extraction
95 -
96 -Currently, preprocessing operations return single values. Multi-metric extraction (JSONPath arrays, Prometheus scrapes, CSV rows) will be:
97 -
98 -**Phase 1 (Current):** Single metric in array
99 -```go
100 -Result{
101 - Metrics: []Metric{{Name: "", Value: "result", Type: ValueTypeStr}},
102 -}
103 -```
104 -
105 -**Phase 2 (Future):** Actual multi-metric extraction
106 -```go
107 -// JSONPath with array result
108 -Result{
109 - Metrics: []Metric{
110 - {Name: "metric1", Value: "10", Labels: map[string]string{"index": "0"}},
111 - {Name: "metric2", Value: "20", Labels: map[string]string{"index": "1"}},
112 - },
113 -}
114 -```
115 -
116 -### Persistence API
117 -
118 -**Deferred to Phase 3** - not needed for initial Netdata integration:
119 -```go
120 -// SaveState exports preprocessor state for persistence
121 -func (p *Preprocessor) SaveState() ([]byte, error)
122 -
123 -// LoadState imports preprocessor state from persistence
124 -func (p *Preprocessor) LoadState(data []byte) error
125 -```
126 -
127 -**Rationale:** Netdata can recreate preprocessor instances on restart. State loss acceptable for now.
128 -
129 ----
130 -
131 -## IMPLEMENTATION CHECKLIST
132 -
133 -**Current Status:** Ready to implement P0 #3
134 -
135 -### Tasks:
136 -1. ✅ Document requirements (this file)
137 -2. ⏳ Add itemID parameter to Execute() and ExecutePipeline()
138 -3. ⏳ Update state key generation to use shardID + itemID
139 -4. ⏳ Change return type from (Value, error) to (Result, error)
140 -5. ⏳ Update all step functions to return Result
141 -6. ⏳ Keep deprecated execute() for backward compatibility
142 -7. ⏳ Update all 362 test cases to use new API
143 -8. ⏳ Add tests for per-item state isolation
144 -9. ⏳ Verify all tests still pass (362/362)
145 -
146 -### Estimated Work: 1-2 days
147 -
148 ----
149 -
150 -## NOTES FOR FUTURE SESSIONS
151 -
152 -**If discussing API design again:**
153 -- **STOP** - Read this file first
154 -- Requirements are FINAL (from user)
155 -- Implementation decisions are FINAL (from Claude)
156 -- Just implement, don't debate
157 -
158 -**If user requests changes:**
159 -- Update this file with new requirements
160 -- Mark as FINAL
161 -- Implement
162 -
163 -**If API questions arise:**
164 -- Refer to "IMPLEMENTATION DECISIONS" section
165 -- Make decision, document, implement
166 -- Don't ask user unless truly ambiguous
167 -
168 ----
169 -
170 -## BACKWARD COMPATIBILITY NOTES
171 -
172 -**Breaking Changes:**
173 -1. Execute() signature changes: `(value, step)` → `(itemID, value, step)`
174 -2. Return type changes: `(Value, error)` → `(Result, error)`
175 -
176 -**Migration:**
177 -```go
178 -// Old code:
179 -result, err := p.Execute(value, step)
180 -
181 -// New code:
182 -result, err := p.Execute("item123", value, step)
183 -// result is now Result{Metrics: []Metric{...}}
184 -```
185 -
186 -**Compatibility Layer:**
187 -- Keep old execute() as private method
188 -- Old tests can use it temporarily
189 -- Remove after Netdata integration complete
190 -
191 ----
192 -
193 -## REJECTED ALTERNATIVES
194 -
195 -**Why not add itemID to Value struct?**
196 -- User said TWO parameters: identifier + value
197 -- Pollutes Value with state management concerns
198 -- Less explicit than parameter
199 -
200 -**Why not SetItemContext()?**
201 -- Not thread-safe without locking
202 -- Awkward for concurrent processing
203 -- Violates "explicit is better than implicit"
204 -
205 -**Why not per-item Preprocessor instances?**
206 -- Memory overhead (one instance per item)
207 -- Complex lifecycle management
208 -- User said "unique identifier for continuity" not "instance per item"
209 -
210 -**Why not return ([]Metric, error)?**
211 -- Need structured errors AND logs in future
212 -- Result struct allows extension without API changes
213 -- Matches user requirement for "logs/events"
214 -
215 ----
216 -
217 -## END OF SPECIFICATION
218 -
219 -This document is the single source of truth for API design.
220 -Do not re-discuss. Just implement.
src/go/plugin/scripts.d/pkg/zabbixpreproc/INTEGRATION.md deleted
-665
@@ -1,665 +0,0 @@
1 -# Zabbix Preprocessing Library - Integration Guide
2 -
3 -Pure Go implementation of Zabbix preprocessing. 100% compatible with Zabbix official test suite (362/362 tests passing). No CGO dependencies.
4 -
5 -**Package**: `github.com/netdata/zabbix-preproc`
6 -
7 ----
8 -
9 -## Quick Start
10 -
11 -```go
12 -import zp "github.com/netdata/zabbix-preproc"
13 -
14 -// Create preprocessor instance (one per shard)
15 -p := zp.NewPreprocessor("shard-1")
16 -
17 -// Define input value
18 -value := zp.Value{
19 - Data: "42.5",
20 - Type: zp.ValueTypeStr,
21 - Timestamp: time.Now(),
22 -}
23 -
24 -// Define preprocessing step
25 -step := zp.Step{
26 - Type: zp.StepTypeMultiplier,
27 - Params: "10",
28 -}
29 -
30 -// Execute
31 -result, err := p.Execute("item-123", value, step)
32 -if err != nil {
33 - log.Fatal(err)
34 -}
35 -
36 -// Get result
37 -fmt.Println(result.Metrics[0].Value) // "425"
38 -```
39 -
40 ----
41 -
42 -## Core Types
43 -
44 -### Value
45 -```go
46 -type Value struct {
47 - Data string // Raw string data
48 - Type ValueType // ValueTypeStr (0), ValueTypeFloat (1), ValueTypeUint64 (3)
49 - Timestamp time.Time // Required for stateful operations (delta, throttle)
50 - IsError bool // True if value represents error from previous step
51 -}
52 -
53 -const (
54 - ValueTypeStr ValueType = 0
55 - ValueTypeFloat ValueType = 1
56 - ValueTypeUint64 ValueType = 3
57 -)
58 -```
59 -
60 -### Step
61 -```go
62 -type Step struct {
63 - Type StepType // One of 30 preprocessing types (see below)
64 - Params string // Type-specific parameters
65 - ErrorHandler ErrorHandler // Optional error handling
66 -}
67 -```
68 -
69 -### Result
70 -```go
71 -type Result struct {
72 - Metrics []Metric // Array of extracted metrics
73 - Logs []string // Optional log entries
74 - Error error // Overall error
75 -}
76 -
77 -type Metric struct {
78 - Name string // Metric name
79 - Value string // Metric value as string
80 - Type ValueType // Value type
81 - Labels map[string]string // Optional labels (for Prometheus, etc.)
82 -}
83 -```
84 -
85 ----
86 -
87 -## All 30 Preprocessing Types
88 -
89 -### Arithmetic Operations
90 -
91 -**Type 1: Multiplier**
92 -```go
93 -step := zp.Step{Type: zp.StepTypeMultiplier, Params: "10"}
94 -// Input: "42.5" → Output: "425"
95 -```
96 -
97 -**Type 9: Delta Value** (stateful - requires timestamp)
98 -```go
99 -// First call: value=100, timestamp=T1 → returns nothing (needs baseline)
100 -// Second call: value=150, timestamp=T2 → returns "50" (150-100)
101 -step := zp.Step{Type: zp.StepTypeDeltaValue}
102 -```
103 -
104 -**Type 10: Delta Speed** (stateful - requires timestamp)
105 -```go
106 -// Returns (current - previous) / time_diff_seconds
107 -step := zp.Step{Type: zp.StepTypeDeltaSpeed}
108 -```
109 -
110 -### String Operations
111 -
112 -**Type 2: RTrim**
113 -```go
114 -step := zp.Step{Type: zp.StepTypeRTrim, Params: " \t\n"}
115 -// Input: "hello \n" → Output: "hello"
116 -```
117 -
118 -**Type 3: LTrim**
119 -```go
120 -step := zp.Step{Type: zp.StepTypeLTrim, Params: " \t"}
121 -// Input: " hello" → Output: "hello"
122 -```
123 -
124 -**Type 4: Trim**
125 -```go
126 -step := zp.Step{Type: zp.StepTypeTrim, Params: " "}
127 -// Input: " hello " → Output: "hello"
128 -```
129 -
130 -**Type 5: Regex Substitution**
131 -```go
132 -// Params: "pattern\nreplacement"
133 -step := zp.Step{
134 - Type: zp.StepTypeRegexSubstitution,
135 - Params: "\\d+\nNUMBER",
136 -}
137 -// Input: "error code 123" → Output: "error code NUMBER"
138 -
139 -// With capture groups:
140 -step := zp.Step{
141 - Type: zp.StepTypeRegexSubstitution,
142 - Params: "(\\d+)-(\\d+)\n\\2-\\1",
143 -}
144 -// Input: "123-456" → Output: "456-123"
145 -```
146 -
147 -**Type 25: String Replace**
148 -```go
149 -// Params: "search\nreplace"
150 -step := zp.Step{
151 - Type: zp.StepTypeStringReplace,
152 - Params: "old\nnew",
153 -}
154 -// Input: "old value old" → Output: "new value new"
155 -```
156 -
157 -### Type Conversions
158 -
159 -**Type 6: Bool to Decimal**
160 -```go
161 -step := zp.Step{Type: zp.StepTypeBool2Dec}
162 -// "true", "yes", "on", "up", "running", "enabled", "available", non-zero → "1"
163 -// "false", "no", "off", "down", "unused", "disabled", "unavailable", "0" → "0"
164 -```
165 -
166 -**Type 7: Octal to Decimal**
167 -```go
168 -step := zp.Step{Type: zp.StepTypeOct2Dec}
169 -// Input: "755" → Output: "493"
170 -// Input: "0755" → Output: "493"
171 -```
172 -
173 -**Type 8: Hex to Decimal**
174 -```go
175 -step := zp.Step{Type: zp.StepTypeHex2Dec}
176 -// Input: "FF" → Output: "255"
177 -// Input: "0xFF" → Output: "255"
178 -// Input: "0x1A2B" → Output: "6699"
179 -```
180 -
181 -### Data Extraction
182 -
183 -**Type 11: XPath**
184 -```go
185 -step := zp.Step{
186 - Type: zp.StepTypeXPath,
187 - Params: "//server/status/text()",
188 -}
189 -// Input: "<server><status>OK</status></server>"
190 -// Output: "OK"
191 -```
192 -
193 -**Type 12: JSONPath**
194 -```go
195 -step := zp.Step{
196 - Type: zp.StepTypeJSONPath,
197 - Params: "$.store.book[0].price",
198 -}
199 -// Input: `{"store":{"book":[{"price":8.95}]}}`
200 -// Output: "8.95"
201 -
202 -// Array extraction:
203 -step := zp.Step{
204 - Type: zp.StepTypeJSONPath,
205 - Params: "$.store.book[*].price",
206 -}
207 -// Output: "[8.95,12.99,8.99]"
208 -```
209 -
210 -**Type 22: Prometheus Pattern**
211 -```go
212 -// Params: "metric_name\nlabel_name\nfunction"
213 -// Functions: value, label, sum, min, max, avg, count
214 -
215 -// Get metric value:
216 -step := zp.Step{
217 - Type: zp.StepTypePrometheusPattern,
218 - Params: "cpu_usage\n\nvalue",
219 -}
220 -// Input: `cpu_usage{host="server1"} 85.5`
221 -// Output: "85.5"
222 -
223 -// Get label value:
224 -step := zp.Step{
225 - Type: zp.StepTypePrometheusPattern,
226 - Params: "http_requests\nmethod\nlabel",
227 -}
228 -// Input: `http_requests{method="GET"} 100`
229 -// Output: "GET"
230 -
231 -// Aggregate:
232 -step := zp.Step{
233 - Type: zp.StepTypePrometheusPattern,
234 - Params: "cpu_usage\n\nsum",
235 -}
236 -// Multiple metrics → sum of all values
237 -```
238 -
239 -**Type 23: Prometheus to JSON**
240 -```go
241 -step := zp.Step{Type: zp.StepTypePrometheusToJSON}
242 -// Input: `metric_name{label="value"} 123`
243 -// Output: `[{"name":"metric_name","value":"123","labels":{"label":"value"}}]`
244 -```
245 -
246 -**Type 24: CSV to JSON**
247 -```go
248 -// Params: "delimiter\nquote_char\nwith_header\n"
249 -// delimiter: single char (default: ,)
250 -// quote_char: single char (default: ")
251 -// with_header: 1 = first row is header, 0 = no header
252 -
253 -step := zp.Step{
254 - Type: zp.StepTypeCSVToJSON,
255 - Params: ",\n\"\n1\n",
256 -}
257 -// Input:
258 -// name,age,city
259 -// Alice,30,NYC
260 -// Bob,25,LA
261 -
262 -// Output:
263 -// [{"name":"Alice","age":"30","city":"NYC"},{"name":"Bob","age":"25","city":"LA"}]
264 -```
265 -
266 -**Type 27: XML to JSON**
267 -```go
268 -step := zp.Step{Type: zp.StepTypeXMLToJSON}
269 -// Input: `<root><foo bar="BAR">BAZ</foo></root>`
270 -// Output: `{"root":{"foo":{"@bar":"BAR","#text":"BAZ"}}}`
271 -
272 -// Zabbix XML serialization rules:
273 -// 1. Attributes → "@attr": "value"
274 -// 2. Self-closing → null
275 -// 3. Empty attributes → "@attr": ""
276 -// 4. Repeated elements → arrays
277 -// 5. Simple text → direct string
278 -// 6. Text + attributes → "#text": "value"
279 -```
280 -
281 -### Validation
282 -
283 -**Type 13: Validate Range**
284 -```go
285 -// Params: "min\nmax"
286 -step := zp.Step{
287 - Type: zp.StepTypeValidateRange,
288 - Params: "0\n100",
289 -}
290 -// Input: "50" → Output: "50" (passes validation)
291 -// Input: "150" → Error: value out of range
292 -```
293 -
294 -**Type 14: Validate Regex**
295 -```go
296 -step := zp.Step{
297 - Type: zp.StepTypeValidateRegex,
298 - Params: "^\\d+$",
299 -}
300 -// Input: "12345" → passes
301 -// Input: "abc123" → Error: does not match pattern
302 -```
303 -
304 -**Type 15: Validate Not Regex**
305 -```go
306 -step := zp.Step{
307 - Type: zp.StepTypeValidateNotRegex,
308 - Params: "error|fail",
309 -}
310 -// Input: "success" → passes
311 -// Input: "error occurred" → Error: matches forbidden pattern
312 -```
313 -
314 -**Type 26: Validate Not Supported**
315 -```go
316 -step := zp.Step{Type: zp.StepTypeValidateNotSupported}
317 -// Always returns error: "not supported"
318 -// Used to mark items as not supported in preprocessing chain
319 -```
320 -
321 -### Error Field Extraction
322 -
323 -**Type 16: Error Field JSON**
324 -```go
325 -step := zp.Step{
326 - Type: zp.StepTypeErrorFieldJSON,
327 - Params: "$.error.message",
328 -}
329 -// Input: `{"error":{"message":"timeout"}}`
330 -// Output: Error with message "timeout"
331 -```
332 -
333 -**Type 17: Error Field XML**
334 -```go
335 -step := zp.Step{
336 - Type: zp.StepTypeErrorFieldXML,
337 - Params: "//error/text()",
338 -}
339 -// Input: `<response><error>Not found</error></response>`
340 -// Output: Error with message "Not found"
341 -```
342 -
343 -**Type 18: Error Field Regex**
344 -```go
345 -step := zp.Step{
346 - Type: zp.StepTypeErrorFieldRegex,
347 - Params: "ERROR: (.+)",
348 -}
349 -// Input: "ERROR: Connection refused"
350 -// Output: Error with message "Connection refused"
351 -```
352 -
353 -### Throttling (Stateful)
354 -
355 -**Type 19: Throttle Value**
356 -```go
357 -step := zp.Step{Type: zp.StepTypeThrottleValue}
358 -// Only passes through if value changed from previous
359 -// Same value → returns empty result (discarded)
360 -```
361 -
362 -**Type 20: Throttle Timed Value**
363 -```go
364 -// Params: "seconds"
365 -step := zp.Step{
366 - Type: zp.StepTypeThrottleTimedValue,
367 - Params: "60",
368 -}
369 -// Passes through if:
370 -// - Value changed from previous, OR
371 -// - More than 60 seconds since last pass
372 -```
373 -
374 -### JavaScript
375 -
376 -**Type 21: JavaScript**
377 -```go
378 -step := zp.Step{
379 - Type: zp.StepTypeJavaScript,
380 - Params: "return parseFloat(value) * 2;",
381 -}
382 -// Input: "21" → Output: "42"
383 -
384 -// Complex transformation:
385 -step := zp.Step{
386 - Type: zp.StepTypeJavaScript,
387 - Params: `
388 - var obj = JSON.parse(value);
389 - return obj.temperature * 9/5 + 32;
390 - `,
391 -}
392 -// Input: `{"temperature": 20}` → Output: "68"
393 -```
394 -
395 -### SNMP Operations
396 -
397 -**Type 28: SNMP Walk Value**
398 -```go
399 -// Params: "oid\nformat"
400 -// format: 0=unchanged, 1=UTF-8, 2=MAC, 3=Int, 4=Hex, 5=IP
401 -step := zp.Step{
402 - Type: zp.StepTypeSNMPWalkValue,
403 - Params: ".1.3.6.1.2.1.2.2.1.2.1\n0",
404 -}
405 -// Extracts specific OID value from SNMP walk output
406 -```
407 -
408 -**Type 29: SNMP Walk to JSON**
409 -```go
410 -// Params: "field_name\noid_to_extract\nformat"
411 -step := zp.Step{
412 - Type: zp.StepTypeSNMPWalkToJSON,
413 - Params: "interfaces\n.1.3.6.1.2.1.2.2.1.2\n0",
414 -}
415 -// Converts SNMP walk table to JSON structure
416 -```
417 -
418 -**Type 30: SNMP Get Value**
419 -```go
420 -// Params: "format"
421 -// format: 0=unchanged, 1=UTF-8, 2=MAC, 3=Int, 4=Hex, 5=IP
422 -step := zp.Step{
423 - Type: zp.StepTypeSNMPGetValue,
424 - Params: "2", // MAC address format
425 -}
426 -// Input: "0x001122334455" → Output: "00:11:22:33:44:55"
427 -```
428 -
429 ----
430 -
431 -## Error Handling
432 -
433 -```go
434 -step := zp.Step{
435 - Type: zp.StepTypeMultiplier,
436 - Params: "10",
437 - ErrorHandler: zp.ErrorHandler{
438 - Action: zp.ErrorActionSetValue,
439 - Params: "0", // Default to 0 on error
440 - },
441 -}
442 -
443 -// Error actions:
444 -// ErrorActionDefault (0) - Return error to caller
445 -// ErrorActionDiscard (1) - Discard value (empty result)
446 -// ErrorActionSetValue (2) - Set custom value
447 -// ErrorActionSetError (3) - Set custom error message
448 -```
449 -
450 ----
451 -
452 -## Pipeline Execution
453 -
454 -```go
455 -steps := []zp.Step{
456 - {Type: zp.StepTypeJSONPath, Params: "$.temperature"},
457 - {Type: zp.StepTypeMultiplier, Params: "1.8"},
458 - {Type: zp.StepTypeValidateRange, Params: "-50\n150"},
459 -}
460 -
461 -result, err := p.ExecutePipeline("item-123", value, steps)
462 -```
463 -
464 ----
465 -
466 -## Stateful Operations (Delta, Throttle)
467 -
468 -```go
469 -// Create preprocessor for shard
470 -p := zp.NewPreprocessor("shard-1")
471 -
472 -// Delta/Throttle operations require:
473 -// 1. Consistent itemID across calls
474 -// 2. Accurate timestamps
475 -
476 -value1 := zp.Value{Data: "100", Timestamp: time.Now()}
477 -value2 := zp.Value{Data: "150", Timestamp: time.Now().Add(time.Second)}
478 -
479 -step := zp.Step{Type: zp.StepTypeDeltaValue}
480 -
481 -// First call establishes baseline
482 -r1, _ := p.Execute("cpu-counter", value1, step)
483 -// r1.Metrics is empty (needs baseline)
484 -
485 -// Second call computes delta
486 -r2, _ := p.Execute("cpu-counter", value2, step)
487 -// r2.Metrics[0].Value = "50"
488 -```
489 -
490 ----
491 -
492 -## State Management
493 -
494 -### Enable Automatic Cleanup
495 -```go
496 -p := zp.NewPreprocessor("shard-1")
497 -
498 -// Clean up state entries unused for 1 hour, check every 10 minutes
499 -stopCleanup := p.EnableStateCleanup(time.Hour, 10*time.Minute)
500 -
501 -// When done
502 -stopCleanup()
503 -```
504 -
505 -### With Config
506 -```go
507 -cfg := zp.Config{
508 - Logger: myLogger,
509 - StateTTL: time.Hour,
510 - StateCleanupInterval: 10 * time.Minute,
511 -}
512 -p := zp.NewPreprocessorWithConfig("shard-1", cfg)
513 -```
514 -
515 ----
516 -
517 -## Logging
518 -
519 -```go
520 -// Implement Logger interface
521 -type Logger interface {
522 - Logf(format string, args ...interface{})
523 -}
524 -
525 -// Use your logger
526 -p.SetLogger(myLogger)
527 -
528 -// Or with config
529 -cfg := zp.Config{Logger: myLogger}
530 -p := zp.NewPreprocessorWithConfig("shard-1", cfg)
531 -
532 -// Default: NoopLogger (zero overhead)
533 -```
534 -
535 ----
536 -
537 -## Multi-Metric Extensions (Non-Zabbix)
538 -
539 -```go
540 -// Types 60-63 return multiple metrics directly (not JSON string)
541 -step := zp.Step{
542 - Type: zp.StepTypePrometheusToJSONMulti,
543 - Params: "",
544 -}
545 -
546 -result, _ := p.Execute("prom", value, step)
547 -for _, metric := range result.Metrics {
548 - fmt.Printf("%s=%s labels=%v\n", metric.Name, metric.Value, metric.Labels)
549 -}
550 -```
551 -
552 ----
553 -
554 -## Thread Safety
555 -
556 -```go
557 -// Single Preprocessor instance is thread-safe
558 -p := zp.NewPreprocessor("shard-1")
559 -
560 -// Safe for concurrent use
561 -go func() {
562 - p.Execute("item-1", val1, step1)
563 -}()
564 -go func() {
565 - p.Execute("item-2", val2, step2)
566 -}()
567 -```
568 -
569 ----
570 -
571 -## Complete Example: Netdata Integration
572 -
573 -```go
574 -package main
575 -
576 -import (
577 - "fmt"
578 - "time"
579 - zp "github.com/netdata/zabbix-preproc"
580 -)
581 -
582 -func main() {
583 - // One preprocessor per shard
584 - p := zp.NewPreprocessorWithConfig("netdata-agent-1", zp.Config{
585 - StateTTL: 24 * time.Hour,
586 - StateCleanupInterval: time.Hour,
587 - })
588 -
589 - // Example: Process CPU metric from Prometheus format
590 - promValue := zp.Value{
591 - Data: `
592 -node_cpu_seconds_total{cpu="0",mode="idle"} 12345.67
593 -node_cpu_seconds_total{cpu="0",mode="system"} 1234.56
594 -node_cpu_seconds_total{cpu="0",mode="user"} 9876.54
595 -`,
596 - Type: zp.ValueTypeStr,
597 - Timestamp: time.Now(),
598 - }
599 -
600 - // Extract idle CPU metric
601 - step := zp.Step{
602 - Type: zp.StepTypePrometheusPattern,
603 - Params: "node_cpu_seconds_total\nmode=idle\nvalue",
604 - }
605 -
606 - result, err := p.Execute("cpu.idle", promValue, step)
607 - if err != nil {
608 - panic(err)
609 - }
610 -
611 - fmt.Printf("CPU idle seconds: %s\n", result.Metrics[0].Value)
612 -
613 - // Example: JSON extraction pipeline
614 - jsonValue := zp.Value{
615 - Data: `{"system":{"cpu":{"usage":85.5},"memory":{"used":4096}}}`,
616 - Type: zp.ValueTypeStr,
617 - Timestamp: time.Now(),
618 - }
619 -
620 - pipeline := []zp.Step{
621 - {Type: zp.StepTypeJSONPath, Params: "$.system.cpu.usage"},
622 - {Type: zp.StepTypeValidateRange, Params: "0\n100"},
623 - }
624 -
625 - result, err = p.ExecutePipeline("system.cpu.usage", jsonValue, pipeline)
626 - if err != nil {
627 - panic(err)
628 - }
629 -
630 - fmt.Printf("CPU usage: %s%%\n", result.Metrics[0].Value)
631 -
632 - // Example: Delta calculation (rate of change)
633 - counter1 := zp.Value{
634 - Data: "1000",
635 - Type: zp.ValueTypeStr,
636 - Timestamp: time.Now(),
637 - }
638 - counter2 := zp.Value{
639 - Data: "1100",
640 - Type: zp.ValueTypeStr,
641 - Timestamp: time.Now().Add(time.Minute),
642 - }
643 -
644 - deltaStep := zp.Step{Type: zp.StepTypeDeltaSpeed}
645 -
646 - // First call establishes baseline
647 - p.Execute("network.bytes", counter1, deltaStep)
648 -
649 - // Second call computes rate (bytes/second)
650 - result, _ = p.Execute("network.bytes", counter2, deltaStep)
651 - fmt.Printf("Network rate: %s bytes/sec\n", result.Metrics[0].Value)
652 -}
653 -```
654 -
655 ----
656 -
657 -## Dependencies
658 -
659 -- `gopkg.in/yaml.v3` - YAML parsing (test harness)
660 -- `github.com/ohler55/ojg` - JSONPath (RFC 9535 compliant)
661 -- `github.com/antchfx/xmlquery` - XPath
662 -- `github.com/dop251/goja` - JavaScript ES5.1 runtime
663 -- `github.com/prometheus/common/expfmt` - Prometheus text format
664 -
665 -All pure Go - no CGO dependencies.
src/go/plugin/scripts.d/pkg/zabbixpreproc/README.md deleted
-406
@@ -1,406 +0,0 @@
1 -# Zabbix Preprocessing Library
2 -
3 -Pure Go implementation of Zabbix preprocessing with 100% compatibility.
4 -
5 -## Features
6 -
7 -- ✅ **100% Zabbix Compatible** - Passes all 362 official Zabbix test cases
8 -- ✅ **Pure Go** - No CGO, no external C libraries
9 -- ✅ **Thread-Safe** - Concurrent processing with per-shard state isolation
10 -- ✅ **High Performance** - VM pooling, regex caching, optimized parsers
11 -- ✅ **Multi-Metric Support** - Extract multiple metrics from single values (Prometheus, SNMP, CSV, JSONPath)
12 -
13 -## Installation
14 -
15 -```bash
16 -go get github.com/netdata/zabbix-preproc
17 -```
18 -
19 -## Quick Start
20 -
21 -```go
22 -package main
23 -
24 -import (
25 - "fmt"
26 - preproc "github.com/netdata/zabbix-preproc"
27 -)
28 -
29 -func main() {
30 - // Create preprocessor for a shard
31 - p := preproc.NewPreprocessor("shard1")
32 -
33 - // Define preprocessing pipeline
34 - steps := []preproc.Step{
35 - {Type: preproc.StepTypeJSONPath, Params: "$.cpu"},
36 - {Type: preproc.StepTypeMultiplier, Params: "100"},
37 - }
38 -
39 - // Process value
40 - value := preproc.Value{Data: `{"cpu": 0.45}`}
41 - result, err := p.ExecutePipeline("cpu_metric", value, steps)
42 - if err != nil {
43 - panic(err)
44 - }
45 -
46 - // Get result
47 - fmt.Println(result.Metrics[0].Value) // Output: 45
48 -}
49 -```
50 -
51 -## Validation
52 -
53 -Validate steps before execution to catch configuration errors early:
54 -
55 -```go
56 -// Validate a single step
57 -step := preproc.Step{Type: preproc.StepTypeMultiplier, Params: "2.5"}
58 -if err := preproc.ValidateStep(step); err != nil {
59 - // Handle validation error
60 - fmt.Printf("Invalid step: %v\n", err)
61 -}
62 -
63 -// Validate entire pipeline
64 -steps := []preproc.Step{
65 - {Type: preproc.StepTypeJSONPath, Params: "$.cpu"},
66 - {Type: preproc.StepTypeMultiplier, Params: "100"},
67 -}
68 -if err := preproc.ValidatePipeline(steps); err != nil {
69 - // Pipeline has invalid configuration
70 - fmt.Printf("Invalid pipeline: %v\n", err)
71 -}
72 -```
73 -
74 -**Benefits of early validation:**
75 -- Catch configuration errors before processing data
76 -- Especially useful for multi-step pipelines (fail fast)
77 -- Clear error messages indicating which parameter is missing/invalid
78 -- Validates step type, required parameters, parameter format, and error handlers
79 -
80 -## Common Use Cases
81 -
82 -### 1. Data Extraction
83 -
84 -**JSONPath** - Extract values from JSON:
85 -```go
86 -steps := []preproc.Step{
87 - {Type: preproc.StepTypeJSONPath, Params: "$.server.memory"},
88 -}
89 -// Input: {"server": {"memory": 8192}}
90 -// Output: 8192
91 -```
92 -
93 -**XPath** - Extract values from XML:
94 -```go
95 -steps := []preproc.Step{
96 - {Type: preproc.StepTypeXPath, Params: "//temperature/text()"},
97 -}
98 -// Input: <data><temperature>25.5</temperature></data>
99 -// Output: 25.5
100 -```
101 -
102 -**Prometheus** - Extract specific metrics:
103 -```go
104 -steps := []preproc.Step{
105 - {Type: preproc.StepTypePrometheusPattern, Params: `http_requests{method="GET"}`},
106 -}
107 -// Input: http_requests{method="GET"} 1027\nhttp_requests{method="POST"} 50
108 -// Output: 1027
109 -```
110 -
111 -### 2. Data Transformation
112 -
113 -**Multiply** - Scale values:
114 -```go
115 -steps := []preproc.Step{
116 - {Type: preproc.StepTypeMultiplier, Params: "1000"},
117 -}
118 -// Input: 1.5
119 -// Output: 1500
120 -```
121 -
122 -**Regex Substitution** - Transform text:
123 -```go
124 -steps := []preproc.Step{
125 - {Type: preproc.StepTypeRegexSubstitution, Params: "([0-9]+)\n$1 units"},
126 -}
127 -// Input: Temperature: 25
128 -// Output: Temperature: 25 units
129 -```
130 -
131 -**Trim** - Remove whitespace:
132 -```go
133 -steps := []preproc.Step{
134 - {Type: preproc.StepTypeTrim, Params: " \t\n"},
135 -}
136 -// Input: " hello "
137 -// Output: "hello"
138 -```
139 -
140 -### 3. Stateful Operations
141 -
142 -**Delta Value** - Calculate differences:
143 -```go
144 -steps := []preproc.Step{
145 - {Type: preproc.StepTypeDeltaValue},
146 -}
147 -// First call: 100 → 0 (baseline)
148 -// Second call: 150 → 50 (delta)
149 -```
150 -
151 -**Delta Speed** - Calculate rate per second:
152 -```go
153 -steps := []preproc.Step{
154 - {Type: preproc.StepTypeDeltaSpeed},
155 -}
156 -// Call 1 (t=0s): 100 bytes → 0
157 -// Call 2 (t=10s): 600 bytes → 50 bytes/sec
158 -```
159 -
160 -**Throttling** - Suppress duplicate values:
161 -```go
162 -steps := []preproc.Step{
163 - {Type: preproc.StepTypeThrottleValue, Params: "300"}, // 5 minutes
164 -}
165 -// Only processes value if changed or 5 min elapsed
166 -```
167 -
168 -### 4. Multi-Metric Extraction
169 -
170 -**Prometheus to Multiple Metrics**:
171 -```go
172 -steps := []preproc.Step{
173 - {Type: preproc.StepTypePrometheusToJSONMulti},
174 -}
175 -
176 -prometheusData := `
177 -http_requests{method="GET"} 100
178 -http_requests{method="POST"} 50
179 -memory_bytes 8192
180 -`
181 -
182 -result, _ := p.ExecutePipeline("metrics", preproc.Value{Data: prometheusData}, steps)
183 -
184 -for _, metric := range result.Metrics {
185 - fmt.Printf("%s: %s (labels: %v)\n", metric.Name, metric.Value, metric.Labels)
186 -}
187 -// Output:
188 -// http_requests: 100 (labels: map[method:GET])
189 -// http_requests: 50 (labels: map[method:POST])
190 -// memory_bytes: 8192 (labels: map[])
191 -```
192 -
193 -**SNMP Walk to Multiple Metrics**:
194 -```go
195 -steps := []preproc.Step{
196 - {Type: preproc.StepTypeSNMPWalkToJSONMulti, Params: ".1.3.6.1.2.1.2.2.1.2"},
197 -}
198 -
199 -snmpData := `
200 -.1.3.6.1.2.1.2.2.1.2.1 = STRING: "eth0"
201 -.1.3.6.1.2.1.2.2.1.2.2 = STRING: "eth1"
202 -`
203 -
204 -result, _ := p.ExecutePipeline("interfaces", preproc.Value{Data: snmpData}, steps)
205 -// Returns multiple metrics with OID indices as labels
206 -```
207 -
208 -**JSONPath to Multiple Values**:
209 -```go
210 -steps := []preproc.Step{
211 - {Type: preproc.StepTypeJSONPathMulti, Params: "$.servers[*].cpu"},
212 -}
213 -
214 -jsonData := `{"servers": [{"cpu": 45}, {"cpu": 23}, {"cpu": 78}]}`
215 -
216 -result, _ := p.ExecutePipeline("cpus", preproc.Value{Data: jsonData}, steps)
217 -// Returns 3 metrics with values: 45, 23, 78
218 -```
219 -
220 -### 5. Error Handling
221 -
222 -**Discard on Error**:
223 -```go
224 -steps := []preproc.Step{
225 - {
226 - Type: preproc.StepTypeJSONPath,
227 - Params: "$.nonexistent",
228 - ErrorHandler: preproc.ErrorHandler{
229 - Action: preproc.ErrorActionDiscard,
230 - },
231 - },
232 -}
233 -// Returns empty result instead of error
234 -```
235 -
236 -**Set Custom Value on Error**:
237 -```go
238 -steps := []preproc.Step{
239 - {
240 - Type: preproc.StepTypeValidateRange,
241 - Params: "0\n100",
242 - ErrorHandler: preproc.ErrorHandler{
243 - Action: preproc.ErrorActionSetValue,
244 - Params: "-1", // Default value
245 - },
246 - },
247 -}
248 -// Returns -1 if value out of range
249 -```
250 -
251 -### 6. Complex Pipelines
252 -
253 -Chain multiple steps together:
254 -
255 -```go
256 -steps := []preproc.Step{
257 - // 1. Extract JSON value
258 - {Type: preproc.StepTypeJSONPath, Params: "$.temperature"},
259 -
260 - // 2. Remove whitespace
261 - {Type: preproc.StepTypeTrim, Params: " "},
262 -
263 - // 3. Extract number (remove unit)
264 - {Type: preproc.StepTypeRegexSubstitution, Params: "([0-9.]+).*\n\\1"},
265 -
266 - // 4. Convert Celsius to Fahrenheit
267 - {Type: preproc.StepTypeMultiplier, Params: "1.8"},
268 -}
269 -
270 -value := preproc.Value{Data: `{"temperature": " 25.0 C "}`}
271 -result, _ := p.ExecutePipeline("temp", value, steps)
272 -// Output: 45
273 -```
274 -
275 -## Supported Preprocessing Types
276 -
277 -### Arithmetic
278 -- `StepTypeMultiplier` - Multiply by constant
279 -- `StepTypeDeltaValue` - Calculate simple delta
280 -- `StepTypeDeltaSpeed` - Calculate rate per second
281 -
282 -### String Operations
283 -- `StepTypeTrim`, `StepTypeRTrim`, `StepTypeLTrim` - Remove characters
284 -- `StepTypeRegexSubstitution` - Regex find/replace
285 -- `StepTypeStringReplace` - Simple string replace
286 -
287 -### Conversions
288 -- `StepTypeBool2Dec` - Boolean to 0/1
289 -- `StepTypeOct2Dec` - Octal to decimal
290 -- `StepTypeHex2Dec` - Hexadecimal to decimal
291 -
292 -### Data Extraction (Single-Value)
293 -- `StepTypeJSONPath` - JSONPath query
294 -- `StepTypeXPath` - XPath query
295 -- `StepTypePrometheusPattern` - Prometheus metric extraction
296 -- `StepTypePrometheusToJSON` - Prometheus to JSON
297 -- `StepTypeCSVToJSON` - CSV to JSON
298 -- `StepTypeSNMPWalkValue` - Extract specific SNMP OID
299 -- `StepTypeSNMPGetValue` - SNMP get value
300 -- `StepTypeSNMPWalkToJSON` - SNMP walk to JSON
301 -
302 -### Data Extraction (Multi-Metric)
303 -- `StepTypePrometheusToJSONMulti` - Extract all Prometheus metrics
304 -- `StepTypeJSONPathMulti` - Extract array values as separate metrics
305 -- `StepTypeSNMPWalkToJSONMulti` - SNMP walk discovery
306 -- `StepTypeCSVToJSONMulti` - CSV rows as separate metrics
307 -
308 -### Validation
309 -- `StepTypeValidateRange` - Check numeric range
310 -- `StepTypeValidateRegex` - Validate with regex
311 -- `StepTypeValidateNotRegex` - Inverse regex validation
312 -- `StepTypeValidateNotSupported` - Check for "not supported" errors
313 -
314 -### Error Field Extraction
315 -- `StepTypeErrorFieldJSON` - Extract error from JSON
316 -- `StepTypeErrorFieldXML` - Extract error from XML
317 -- `StepTypeErrorFieldRegex` - Extract error with regex
318 -
319 -### Throttling
320 -- `StepTypeThrottleValue` - Suppress duplicate values
321 -- `StepTypeThrottleTimedValue` - Time-based throttling
322 -
323 -### Advanced
324 -- `StepTypeJavaScript` - Custom JavaScript preprocessing
325 -
326 -## Architecture
327 -
328 -### Per-Shard State Isolation
329 -
330 -Each preprocessor instance maintains independent state:
331 -
332 -```go
333 -// Shard A - processing metrics from server1
334 -p1 := preproc.NewPreprocessor("shard-server1")
335 -
336 -// Shard B - processing metrics from server2
337 -p2 := preproc.NewPreprocessor("shard-server2")
338 -
339 -// Each maintains separate delta/throttle history
340 -// No state leakage between shards
341 -```
342 -
343 -### Thread Safety
344 -
345 -All operations are thread-safe:
346 -- Concurrent preprocessing across different items/shards
347 -- VM pooling for JavaScript execution
348 -- Read-only MIB-to-OID mappings
349 -- RWMutex protection for stateful operations
350 -
351 -### Performance Optimizations
352 -
353 -- **VM Pooling**: JavaScript VMs reused, not recreated
354 -- **Program Caching**: Compiled JavaScript programs cached
355 -- **Regex Caching**: Patterns compiled once at package init
356 -- **strings.Builder**: Efficient string construction in loops
357 -
358 -## Testing
359 -
360 -```bash
361 -# Run all tests (362 Zabbix compatibility tests + unit tests)
362 -go test
363 -
364 -# Run with coverage report
365 -go test -cover
366 -
367 -# Run with race detector
368 -go test -race
369 -
370 -# Run specific test suite
371 -go test -run TestZabbixTestSuite
372 -
373 -# Run fuzzing tests (finds edge cases via random input generation)
374 -go test -fuzz=FuzzJSONPath -fuzztime=30s # Fuzz JSONPath extraction
375 -go test -fuzz=FuzzXPath -fuzztime=30s # Fuzz XPath extraction
376 -go test -fuzz=FuzzRegexSubstitution -fuzztime=30s # Fuzz regex replacement
377 -go test -fuzz=FuzzMultiplier -fuzztime=30s # Fuzz numeric operations
378 -go test -fuzz=FuzzCSVToJSON -fuzztime=30s # Fuzz CSV parsing
379 -go test -fuzz=FuzzPrometheusPattern -fuzztime=30s # Fuzz Prometheus parsing
380 -go test -fuzz=FuzzSNMPWalkToJSON -fuzztime=30s # Fuzz SNMP parsing
381 -# Note: Fuzzing runs indefinitely if -fuzztime not specified
382 -
383 -# Benchmark
384 -go test -bench=.
385 -```
386 -
387 -## Compatibility
388 -
389 -This library passes 100% of official Zabbix preprocessing tests:
390 -- ✅ zbx_item_preproc.yaml (362 test cases)
391 -- ✅ item_preproc_xpath.yaml (12 XPath tests)
392 -- ✅ item_preproc_csv_to_json.yaml (4 CSV tests)
393 -
394 -Zabbix version compatibility: **7.x** (backward compatible with 6.x)
395 -
396 -## License
397 -
398 -Apache 2.0
399 -
400 -## Contributing
401 -
402 -Contributions welcome! Please ensure:
403 -- All Zabbix tests still pass (`go test`)
404 -- No CGO dependencies added
405 -- Code follows existing patterns
406 -- New features have test coverage
src/go/plugin/scripts.d/pkg/zabbixpreproc/advanced_steps.go deleted
-247
@@ -1,247 +0,0 @@
1 -package zabbixpreproc
2 -
3 -import (
4 - "encoding/json"
5 - "fmt"
6 - "strings"
7 - "time"
8 -
9 - "github.com/antchfx/xmlquery"
10 - "github.com/theory/jsonpath"
11 -)
12 -
13 -// errorFieldJSON extracts error message from JSON response.
14 -func errorFieldJSON(value Value, paramStr string) (Value, error) {
15 - expr := strings.TrimSpace(paramStr)
16 - if expr == "" {
17 - return Value{}, fmt.Errorf("jsonpath expression required for error field extraction")
18 - }
19 -
20 - var data interface{}
21 - err := json.Unmarshal([]byte(value.Data), &data)
22 - if err != nil {
23 - // Invalid JSON - return input unchanged
24 - return value, nil
25 - }
26 -
27 - jp, err := jsonpath.Parse(expr)
28 - if err != nil {
29 - return Value{}, fmt.Errorf("invalid jsonpath: %w", err)
30 - }
31 -
32 - results := jp.Select(data)
33 - if len(results) == 0 {
34 - // No error field found - return input unchanged
35 - return value, nil
36 - }
37 -
38 - // Error field exists - return error with the field's value
39 - errValue := fmt.Sprintf("%v", results[0])
40 - return Value{}, fmt.Errorf("%s", errValue)
41 -}
42 -
43 -// errorFieldXML extracts error message from XML response.
44 -func errorFieldXML(value Value, paramStr string) (Value, error) {
45 - expr := strings.TrimSpace(paramStr)
46 - if expr == "" {
47 - return Value{}, fmt.Errorf("xpath expression required for error field extraction")
48 - }
49 -
50 - doc, err := xmlquery.Parse(strings.NewReader(value.Data))
51 - if err != nil {
52 - // Invalid XML - return input unchanged
53 - return value, nil
54 - }
55 -
56 - nodes, err := xmlquery.QueryAll(doc, expr)
57 - if err != nil {
58 - return Value{}, fmt.Errorf("invalid xpath: %w", err)
59 - }
60 -
61 - if len(nodes) == 0 {
62 - // No error field found - return input unchanged
63 - return value, nil
64 - }
65 -
66 - // Error field exists - return error with the field's text value
67 - errText := nodes[0].InnerText()
68 - return Value{}, fmt.Errorf("%s", errText)
69 -}
70 -
71 -// errorFieldRegex extracts error message from regex match.
72 -func errorFieldRegex(value Value, paramStr string) (Value, error) {
73 - // paramStr format: "pattern\noutput"
74 - parts := strings.SplitN(paramStr, "\n", 2)
75 - if len(parts) < 1 {
76 - return Value{}, fmt.Errorf("regex pattern required for error field extraction")
77 - }
78 -
79 - pattern := parts[0]
80 - output := ""
81 - if len(parts) > 1 {
82 - output = parts[1]
83 - }
84 -
85 - re, err := compileRegexSafe(pattern, 0)
86 - if err != nil {
87 - return Value{}, fmt.Errorf("invalid regex: %w", err)
88 - }
89 -
90 - matches, err := findStringSubmatchWithTimeout(re, value.Data, defaultRegexTimeout)
91 - if err != nil {
92 - return Value{}, fmt.Errorf("regex error field extraction failed: %w", err)
93 - }
94 - if len(matches) == 0 {
95 - // No match - no error
96 - return value, nil
97 - }
98 -
99 - // Match found - return error
100 - errMsg := output
101 - if errMsg == "" {
102 - errMsg = matches[0]
103 - }
104 -
105 - // Replace capture groups in output
106 - for i, match := range matches {
107 - placeholder := fmt.Sprintf("$%d", i)
108 - errMsg = strings.ReplaceAll(errMsg, placeholder, match)
109 - }
110 -
111 - return Value{}, fmt.Errorf("%s", errMsg)
112 -}
113 -
114 -// throttleValue returns the value only if it changed from the previous value.
115 -// Thread-safe for concurrent use. State isolated per shard and item.
116 -func (p *Preprocessor) throttleValue(itemID string, value Value, paramStr string) (Value, error) {
117 - // State key: shardID:itemID:operation
118 - stateKey := fmt.Sprintf("%s:%s:throttle_value", p.shardID, itemID)
119 -
120 - // Acquire exclusive lock for read-modify-write operation
121 - p.mu.Lock()
122 - defer p.mu.Unlock()
123 -
124 - state, hasState := p.state[stateKey]
125 -
126 - // Always update state before returning
127 - defer func() {
128 - p.state[stateKey] = &OperationState{
129 - LastValue: value.Data,
130 - LastTimestamp: value.Timestamp,
131 - LastAccess: time.Now(),
132 - }
133 - }()
134 -
135 - if !hasState {
136 - // First call
137 - return value, nil
138 - }
139 -
140 - // Update access time
141 - state.LastAccess = time.Now()
142 -
143 - // Check if value changed
144 - if value.Data != state.LastValue {
145 - // Value changed - return it
146 - return value, nil
147 - }
148 -
149 - // Value didn't change - discard it (return empty value without error)
150 - return Value{Data: "", Type: value.Type}, nil
151 -}
152 -
153 -// throttleTimedValue returns the value only if it changed or enough time has passed.
154 -// Thread-safe for concurrent use. State isolated per shard and item.
155 -func (p *Preprocessor) throttleTimedValue(itemID string, value Value, paramStr string) (Value, error) {
156 - // State key: shardID:itemID:operation
157 - stateKey := fmt.Sprintf("%s:%s:throttle_timed", p.shardID, itemID)
158 -
159 - // Parse timeout parameter before acquiring lock
160 - timeoutSeconds := parseDuration(paramStr)
161 -
162 - // Acquire exclusive lock for read-modify-write operation
163 - p.mu.Lock()
164 - defer p.mu.Unlock()
165 -
166 - state, hasState := p.state[stateKey]
167 -
168 - if !hasState {
169 - // First call - record state and return value
170 - p.state[stateKey] = &OperationState{
171 - LastValue: value.Data,
172 - LastValueTime: value.Timestamp,
173 - LastAccess: time.Now(),
174 - }
175 - return value, nil
176 - }
177 -
178 - // Update access time
179 - state.LastAccess = time.Now()
180 -
181 - // Check if value changed
182 - if value.Data != state.LastValue {
183 - // Value changed - return immediately and update state
184 - p.state[stateKey] = &OperationState{
185 - LastValue: value.Data,
186 - LastValueTime: value.Timestamp,
187 - LastAccess: time.Now(),
188 - }
189 - return value, nil
190 - }
191 -
192 - // Value unchanged - check if enough time has passed
193 - timeDiff := value.Timestamp.Sub(state.LastValueTime).Seconds()
194 -
195 - if timeDiff >= timeoutSeconds {
196 - // Enough time passed - return value and update timestamp
197 - p.state[stateKey] = &OperationState{
198 - LastValue: value.Data,
199 - LastValueTime: value.Timestamp,
200 - LastAccess: time.Now(),
201 - }
202 - return value, nil
203 - }
204 -
205 - // Value unchanged and not enough time passed - discard
206 - return Value{Data: "", Type: value.Type}, nil
207 -}
208 -
209 -// parseDuration parses Zabbix duration format (60, 1m, 1h, 1d, 1w) to seconds
210 -func parseDuration(s string) float64 {
211 - s = strings.TrimSpace(s)
212 - if s == "" {
213 - return 60 // Default 1 minute
214 - }
215 -
216 - // Check for suffix
217 - var multiplier float64 = 1
218 - if len(s) > 0 {
219 - lastChar := s[len(s)-1]
220 - switch lastChar {
221 - case 's':
222 - multiplier = 1
223 - s = s[:len(s)-1]
224 - case 'm':
225 - multiplier = 60
226 - s = s[:len(s)-1]
227 - case 'h':
228 - multiplier = 3600
229 - s = s[:len(s)-1]
230 - case 'd':
231 - multiplier = 86400
232 - s = s[:len(s)-1]
233 - case 'w':
234 - multiplier = 604800
235 - s = s[:len(s)-1]
236 - }
237 - }
238 -
239 - // Parse numeric value
240 - var value float64
241 - fmt.Sscanf(s, "%f", &value)
242 - if value == 0 {
243 - return 60 // Default if parsing failed
244 - }
245 -
246 - return value * multiplier
247 -}
src/go/plugin/scripts.d/pkg/zabbixpreproc/config.go deleted
-70
@@ -1,70 +0,0 @@
1 -package zabbixpreproc
2 -
3 -import "time"
4 -
5 -// Limits defines configurable constraints for preprocessing operations.
6 -// Default values match Zabbix 7.x behavior for maximum compatibility.
7 -type Limits struct {
8 - // JavaScript preprocessing limits
9 - JavaScript JSLimits
10 -
11 - // Regular expression preprocessing limits
12 - Regex RegexLimits
13 -}
14 -
15 -// JSLimits configures JavaScript preprocessing constraints.
16 -type JSLimits struct {
17 - // Timeout is the maximum execution time for a single JavaScript step.
18 - // Zabbix default: 10 seconds. Scripts exceeding this are terminated.
19 - Timeout time.Duration
20 -
21 - // MaxCallStackSize limits recursion depth to prevent stack exhaustion.
22 - // Zabbix default: 1000. Matches ZBX_ES_STACK_LIMIT.
23 - MaxCallStackSize int
24 -
25 - // MaxHttpRequests limits HttpRequest objects per script execution.
26 - // Zabbix default: 10. Prevents resource exhaustion.
27 - MaxHttpRequests int
28 -}
29 -
30 -// RegexLimits configures regular expression preprocessing constraints.
31 -type RegexLimits struct {
32 - // MaxCaptureGroups is the maximum number of capture groups (\1-\N).
33 - // Zabbix default: 10 (groups \0-\9). Matches ZBX_REGEXP_GROUPS_MAX.
34 - MaxCaptureGroups int
35 -
36 - // MatchTimeout is the maximum time for a single regex match operation.
37 - // Prevents ReDoS attacks. Recommended: 1s for safety.
38 - MatchTimeout time.Duration
39 -
40 - // ReplaceTimeout is the maximum time for regex replacement operations.
41 - // Zabbix default: 3 seconds. Matches ZBX_REGEX_REPL_TIMEOUT.
42 - ReplaceTimeout time.Duration
43 -
44 - // MaxPatternLength limits the size of regex patterns.
45 - // Zabbix default: unlimited. Set to 0 for no limit.
46 - MaxPatternLength int
47 -
48 - // UsePCREFeatures enables lookahead/lookbehind via regexp2.
49 - // If false, uses stdlib regexp (RE2) which lacks these features.
50 - // Default: true for Zabbix compatibility.
51 - UsePCREFeatures bool
52 -}
53 -
54 -// DefaultLimits returns limits matching Zabbix 7.x defaults.
55 -func DefaultLimits() Limits {
56 - return Limits{
57 - JavaScript: JSLimits{
58 - Timeout: 10 * time.Second,
59 - MaxCallStackSize: 1000,
60 - MaxHttpRequests: 10,
61 - },
62 - Regex: RegexLimits{
63 - MaxCaptureGroups: 10,
64 - MatchTimeout: time.Second,
65 - ReplaceTimeout: 3 * time.Second,
66 - MaxPatternLength: 0, // No limit (matches Zabbix)
67 - UsePCREFeatures: true,
68 - },
69 - }
70 -}
src/go/plugin/scripts.d/pkg/zabbixpreproc/csv_step.go deleted
-727
@@ -1,727 +0,0 @@
1 -package zabbixpreproc
2 -
3 -import (
4 - "encoding/json"
5 - "fmt"
6 - "strconv"
7 - "strings"
8 - "unicode/utf8"
9 -)
10 -
11 -// csvToJSON converts CSV data to JSON.
12 -func csvToJSON(value Value, paramStr string) (Value, error) {
13 - // paramStr format: "delimiter\nquote_character\nheader_row"
14 - // header_row: 1 = first row is header, 0 = no header (use column indices)
15 - params := strings.Split(paramStr, "\n")
16 -
17 - // Validate parameter count: must be exactly 3
18 - if len(params) < 2 {
19 - return Value{}, fmt.Errorf("csv to json requires delimiter, quote, and header parameters")
20 - }
21 - if len(params) < 3 {
22 - return Value{}, fmt.Errorf("csv to json requires header row parameter")
23 - }
24 -
25 - // Extract delimiter (UTF-8 multi-byte support)
26 - delim := ","
27 - delimExplicit := false // Track if delimiter was explicitly set
28 - if params[0] != "" {
29 - r, size := utf8.DecodeRuneInString(params[0])
30 - if size != len(params[0]) {
31 - // Multi-character delimiter not allowed
32 - return Value{}, fmt.Errorf("invalid delimiter: must be single character")
33 - }
34 - delim = string(r)
35 - delimExplicit = true
36 - }
37 -
38 - // Extract quote character (UTF-8 multi-byte support)
39 - quote := "\""
40 - if params[1] != "" {
41 - r, size := utf8.DecodeRuneInString(params[1])
42 - if size != len(params[1]) {
43 - // Multi-character quote not allowed
44 - return Value{}, fmt.Errorf("invalid quote character: must be single character")
45 - }
46 - quote = string(r)
47 - }
48 -
49 - // Extract header_row flag
50 - headerRow, err := strconv.Atoi(params[2])
51 - if err != nil {
52 - return Value{}, fmt.Errorf("invalid header row parameter")
53 - }
54 - hasHeader := (headerRow == 1)
55 -
56 - // Check for Sep=/SEP=/sEp= declaration (case-INSENSITIVE)
57 - // Sep= overrides the delimiter param for ALL rows
58 - data := value.Data
59 - lines := strings.Split(data, "\n")
60 - startLine := 0
61 -
62 - if len(lines) > 0 {
63 - firstLine := strings.TrimRight(lines[0], "\r") // Handle \r\n line breaks
64 - // Case-insensitive check for "sep="
65 - if len(firstLine) >= 4 && strings.ToLower(firstLine[:4]) == "sep=" {
66 - if len(firstLine) == 4 {
67 - // "sep=" with no character - invalid, treat as data
68 - } else {
69 - // Check if exactly one character after "sep="
70 - r, size := utf8.DecodeRuneInString(firstLine[4:])
71 - if size == len(firstLine[4:]) {
72 - // Valid single-char sep= line
73 - // Only override delimiter if it wasn't explicitly set in params
74 - if !delimExplicit {
75 - delim = string(r)
76 - }
77 - startLine = 1
78 - // Reconstruct data without sep= line
79 - data = strings.Join(lines[startLine:], "\n")
80 - }
81 - // If multi-char after "sep=", treat as data (don't skip)
82 - }
83 - }
84 - }
85 -
86 - // Check if input ends with newline (for empty row logic)
87 - endsWithNewline := strings.HasSuffix(value.Data, "\n") || strings.HasSuffix(value.Data, "\r\n")
88 -
89 - // Parse CSV with strict validation
90 - records, err := parseCSVStrict(data, delim, quote)
91 - if err != nil {
92 - return Value{}, err
93 - }
94 -
95 - // Empty input returns []
96 - if len(records) == 0 {
97 - return Value{Data: "[]", Type: ValueTypeStr}, nil
98 - }
99 -
100 - var jsonData interface{}
101 -
102 - if hasHeader {
103 - // First row is header
104 - if len(records) < 1 {
105 - // Only header line, no data
106 - return Value{Data: "[]", Type: ValueTypeStr}, nil
107 - }
108 -
109 - header := records[0]
110 -
111 - // Validate no duplicate column names
112 - seen := make(map[string]bool)
113 - for _, col := range header {
114 - if seen[col] {
115 - return Value{}, fmt.Errorf("duplicate column name: %s", col)
116 - }
117 - seen[col] = true
118 - }
119 -
120 - result := make([]map[string]string, 0)
121 -
122 - // Add data rows
123 - for i := 1; i < len(records); i++ {
124 - row := records[i]
125 -
126 - // Validate field count: data row can't have more fields than header
127 - if len(row) > len(header) {
128 - return Value{}, fmt.Errorf("data row has more fields than header")
129 - }
130 -
131 - obj := make(map[string]string)
132 - for j, val := range row {
133 - if j < len(header) {
134 - obj[header[j]] = val
135 - }
136 - }
137 - // Fill missing fields with empty strings
138 - for j := len(row); j < len(header); j++ {
139 - obj[header[j]] = ""
140 - }
141 - result = append(result, obj)
142 - }
143 -
144 - // Add empty trailing row if input ended with newline
145 - if endsWithNewline {
146 - emptyObj := make(map[string]string)
147 - for _, h := range header {
148 - emptyObj[h] = ""
149 - }
150 - result = append(result, emptyObj)
151 - }
152 -
153 - // Marshal with ordered keys to preserve header column order
154 - jsonData, err = marshalOrderedJSON(result, header)
155 - if err != nil {
156 - return Value{}, fmt.Errorf("failed to marshal csv to json: %w", err)
157 - }
158 - } else {
159 - // No header - use 1-based column indices as keys
160 - result := make([]map[string]string, 0)
161 - for _, row := range records {
162 - // Special case: single empty field becomes empty object
163 - if len(row) == 1 && row[0] == "" {
164 - result = append(result, make(map[string]string))
165 - } else {
166 - obj := make(map[string]string)
167 - for i, val := range row {
168 - // Use 1-based indexing
169 - obj[strconv.Itoa(i+1)] = val
170 - }
171 - result = append(result, obj)
172 - }
173 - }
174 -
175 - // Add empty trailing row if input ended with newline
176 - if endsWithNewline && len(result) > 0 {
177 - result = append(result, make(map[string]string))
178 - }
179 -
180 - jsonData = result
181 - }
182 -
183 - var jsonBytes []byte
184 - if str, ok := jsonData.(string); ok {
185 - jsonBytes = []byte(str)
186 - } else {
187 - jsonBytes, err = json.Marshal(jsonData)
188 - if err != nil {
189 - return Value{}, fmt.Errorf("failed to marshal csv to json: %w", err)
190 - }
191 - }
192 -
193 - return Value{Data: string(jsonBytes), Type: ValueTypeStr}, nil
194 -}
195 -
196 -// marshalOrderedJSON marshals array of maps with keys in header order.
197 -func marshalOrderedJSON(data []map[string]string, header []string) (string, error) {
198 - var builder strings.Builder
199 - builder.WriteString("[")
200 -
201 - for i, obj := range data {
202 - if i > 0 {
203 - builder.WriteString(",")
204 - }
205 - // Delegate single object marshaling to avoid code duplication
206 - jsonObj, err := marshalOrderedJSONSingle(obj, header)
207 - if err != nil {
208 - return "", err
209 - }
210 - builder.WriteString(jsonObj)
211 - }
212 -
213 - builder.WriteString("]")
214 - return builder.String(), nil
215 -}
216 -
217 -// parseCSVStrict parses CSV data with strict Zabbix-compatible validation.
218 -//
219 -// This custom parser implements Zabbix's specific CSV parsing rules, which differ
220 -// from encoding/csv in several ways:
221 -// - Supports multi-byte UTF-8 delimiters and quote characters (not just single runes)
222 -// - Special mode when delimiter == quote (disables quoting behavior entirely)
223 -// - Strict validation: rejects \n\r line breaks, unclosed quotes, invalid characters after closing quotes
224 -// - UTF-8 validation: returns error on invalid UTF-8 sequences
225 -//
226 -// Parameters:
227 -// - data: Raw CSV string data to parse
228 -// - delimiter: Field delimiter (can be multi-byte UTF-8 string like "€")
229 -// - quote: Quote character (can be multi-byte UTF-8 string like "§")
230 -//
231 -// Returns:
232 -// - [][]string: Parsed CSV records (array of rows, each row is array of fields)
233 -// - error: Parsing error if data is invalid
234 -//
235 -// This function cannot be replaced with encoding/csv.Reader due to the multi-byte
236 -// delimiter/quote support and special delimiter==quote handling required by Zabbix.
237 -
238 -// csvParseState tracks the state during CSV parsing
239 -type csvParseState struct {
240 - records [][]string
241 - currentRecord []string
242 - currentField strings.Builder
243 - inQuotes bool
244 - fieldStart bool
245 - data string
246 - delimiter string
247 - quote string
248 -}
249 -
250 -// finishField completes the current field and adds it to the current record
251 -func (s *csvParseState) finishField() {
252 - s.currentRecord = append(s.currentRecord, s.currentField.String())
253 - s.currentField.Reset()
254 - s.fieldStart = true
255 -}
256 -
257 -// finishRecord completes the current record and adds it to results
258 -func (s *csvParseState) finishRecord() {
259 - if len(s.currentRecord) > 0 {
260 - s.records = append(s.records, s.currentRecord)
261 - }
262 - s.currentRecord = nil
263 - s.fieldStart = true
264 -}
265 -
266 -// handleQuoteChar processes a quote character at position i
267 -// Returns new position and whether to continue main loop
268 -func (s *csvParseState) handleQuoteChar(i int) (int, bool, error) {
269 - if s.inQuotes {
270 - // Check if it's an escaped quote (doubled)
271 - if strings.HasPrefix(s.data[i+len(s.quote):], s.quote) {
272 - // Escaped quote - add one quote to field
273 - s.currentField.WriteString(s.quote)
274 - s.fieldStart = false
275 - return i + len(s.quote)*2, true, nil
276 - }
277 -
278 - // End of quoted field
279 - s.inQuotes = false
280 - i += len(s.quote)
281 -
282 - // After closing quote, MUST be delimiter or newline
283 - if i < len(s.data) {
284 - nextChar := s.data[i]
285 - if nextChar != '\n' && nextChar != '\r' && !strings.HasPrefix(s.data[i:], s.delimiter) {
286 - return 0, false, fmt.Errorf("character after closing quote")
287 - }
288 - }
289 - s.fieldStart = false
290 - return i, true, nil
291 - }
292 -
293 - if s.fieldStart {
294 - // Start of quoted field (only at field start)
295 - s.inQuotes = true
296 - s.fieldStart = false
297 - return i + len(s.quote), true, nil
298 - }
299 -
300 - // Quote in middle of unquoted field - treat as literal
301 - s.currentField.WriteString(s.quote)
302 - s.fieldStart = false
303 - return i + len(s.quote), true, nil
304 -}
305 -
306 -// handleNewlineChar processes a newline character at position i (only outside quotes)
307 -// Returns new position and whether to continue main loop
308 -func (s *csvParseState) handleNewlineChar(i int) (int, bool, error) {
309 - if s.data[i] == '\n' {
310 - // Check for unsupported \n\r line break
311 - if i+1 < len(s.data) && s.data[i+1] == '\r' {
312 - return 0, false, fmt.Errorf("unsupported line break")
313 - }
314 -
315 - // End of record
316 - s.currentRecord = append(s.currentRecord, s.currentField.String())
317 - s.currentField.Reset()
318 - s.finishRecord()
319 - return i + 1, true, nil
320 - }
321 -
322 - if s.data[i] == '\r' {
323 - // Handle \r\n or just \r
324 - if i+1 < len(s.data) && s.data[i+1] == '\n' {
325 - // \r\n - end of record
326 - s.currentRecord = append(s.currentRecord, s.currentField.String())
327 - s.currentField.Reset()
328 - s.finishRecord()
329 - return i + 2, true, nil
330 - }
331 -
332 - // Just \r - treat as newline
333 - s.currentRecord = append(s.currentRecord, s.currentField.String())
334 - s.currentField.Reset()
335 - s.finishRecord()
336 - return i + 1, true, nil
337 - }
338 -
339 - return i, false, nil
340 -}
341 -
342 -func parseCSVStrict(data string, delimiter string, quote string) ([][]string, error) {
343 - // Special case: when delimiter == quote, disable quoting behavior
344 - if delimiter == quote {
345 - return parseCSVNoQuotes(data, delimiter)
346 - }
347 -
348 - // Initialize parsing state
349 - state := &csvParseState{
350 - data: data,
351 - delimiter: delimiter,
352 - quote: quote,
353 - fieldStart: true,
354 - }
355 -
356 - i := 0
357 - for i < len(data) {
358 - // Check for quote character at current position
359 - if strings.HasPrefix(data[i:], quote) {
360 - newPos, shouldContinue, err := state.handleQuoteChar(i)
361 - if err != nil {
362 - return nil, err
363 - }
364 - if shouldContinue {
365 - i = newPos
366 - continue
367 - }
368 - }
369 -
370 - // Check for delimiter (only outside quotes)
371 - if !state.inQuotes && strings.HasPrefix(data[i:], delimiter) {
372 - state.finishField()
373 - i += len(delimiter)
374 - continue
375 - }
376 -
377 - // Check for newline (only outside quotes)
378 - if !state.inQuotes {
379 - newPos, shouldContinue, err := state.handleNewlineChar(i)
380 - if err != nil {
381 - return nil, err
382 - }
383 - if shouldContinue {
384 - i = newPos
385 - continue
386 - }
387 - }
388 -
389 - // Regular character - add to current field
390 - r, size := utf8.DecodeRuneInString(data[i:])
391 - if r == utf8.RuneError && size == 1 {
392 - return nil, fmt.Errorf("invalid UTF-8 character at position %d", i)
393 - }
394 - state.currentField.WriteRune(r)
395 - i += size
396 - state.fieldStart = false
397 - }
398 -
399 - // Handle last field and record
400 - if state.inQuotes {
401 - return nil, fmt.Errorf("unclosed quoted field")
402 - }
403 -
404 - // Add last field to current record
405 - if state.currentField.Len() > 0 || len(state.currentRecord) > 0 {
406 - state.currentRecord = append(state.currentRecord, state.currentField.String())
407 - }
408 -
409 - // Add last record if not empty
410 - if len(state.currentRecord) > 0 {
411 - state.records = append(state.records, state.currentRecord)
412 - }
413 -
414 - return state.records, nil
415 -}
416 -
417 -// parseCSVNoQuotes parses CSV when delimiter == quote (no quoting behavior).
418 -func parseCSVNoQuotes(data string, delimiter string) ([][]string, error) {
419 - var records [][]string
420 - var currentRecord []string
421 - var currentField strings.Builder
422 -
423 - i := 0
424 -
425 - for i < len(data) {
426 - // Check for delimiter
427 - if strings.HasPrefix(data[i:], delimiter) {
428 - currentRecord = append(currentRecord, currentField.String())
429 - currentField.Reset()
430 - i += len(delimiter)
431 - continue
432 - }
433 -
434 - // Check for newline
435 - if data[i] == '\n' {
436 - // Check for \n\r
437 - if i+1 < len(data) && data[i+1] == '\r' {
438 - return nil, fmt.Errorf("unsupported line break")
439 - }
440 -
441 - // End of record
442 - currentRecord = append(currentRecord, currentField.String())
443 - currentField.Reset()
444 - if len(currentRecord) > 0 {
445 - records = append(records, currentRecord)
446 - }
447 - currentRecord = nil
448 - i++
449 - continue
450 - }
451 - if data[i] == '\r' {
452 - // Handle \r\n or just \r
453 - if i+1 < len(data) && data[i+1] == '\n' {
454 - currentRecord = append(currentRecord, currentField.String())
455 - currentField.Reset()
456 - if len(currentRecord) > 0 {
457 - records = append(records, currentRecord)
458 - }
459 - currentRecord = nil
460 - i += 2
461 - continue
462 - } else {
463 - currentRecord = append(currentRecord, currentField.String())
464 - currentField.Reset()
465 - if len(currentRecord) > 0 {
466 - records = append(records, currentRecord)
467 - }
468 - currentRecord = nil
469 - i++
470 - continue
471 - }
472 - }
473 -
474 - // Regular character
475 - r, size := utf8.DecodeRuneInString(data[i:])
476 - if r == utf8.RuneError && size == 1 {
477 - return nil, fmt.Errorf("invalid UTF-8 character at position %d", i)
478 - }
479 - currentField.WriteRune(r)
480 - i += size
481 - }
482 -
483 - // Handle last field and record
484 - if currentField.Len() > 0 || len(currentRecord) > 0 {
485 - currentRecord = append(currentRecord, currentField.String())
486 - }
487 - if len(currentRecord) > 0 {
488 - records = append(records, currentRecord)
489 - }
490 -
491 - return records, nil
492 -}
493 -
494 -// csvToJSONMulti converts CSV data to multiple Result metrics
495 -// Each CSV row becomes a separate Result.Metric with row index label
496 -func csvToJSONMulti(value Value, paramStr string) (Result, error) {
497 - // paramStr format: "delimiter\nquote_character\nheader_row"
498 - // header_row: 1 = first row is header, 0 = no header (use column indices)
499 - params := strings.Split(paramStr, "\n")
500 -
501 - // Validate parameter count: must be exactly 3
502 - if len(params) < 2 {
503 - err := fmt.Errorf("csv to json requires delimiter, quote, and header parameters")
504 - return Result{Error: err}, err
505 - }
506 - if len(params) < 3 {
507 - err := fmt.Errorf("csv to json requires header row parameter")
508 - return Result{Error: err}, err
509 - }
510 -
511 - // Extract delimiter (UTF-8 multi-byte support)
512 - delim := ","
513 - delimExplicit := false // Track if delimiter was explicitly set
514 - if params[0] != "" {
515 - r, size := utf8.DecodeRuneInString(params[0])
516 - if size != len(params[0]) {
517 - // Multi-character delimiter not allowed
518 - err := fmt.Errorf("invalid delimiter: must be single character")
519 - return Result{Error: err}, err
520 - }
521 - delim = string(r)
522 - delimExplicit = true
523 - }
524 -
525 - // Extract quote character (UTF-8 multi-byte support)
526 - quote := "\""
527 - if params[1] != "" {
528 - r, size := utf8.DecodeRuneInString(params[1])
529 - if size != len(params[1]) {
530 - // Multi-character quote not allowed
531 - err := fmt.Errorf("invalid quote character: must be single character")
532 - return Result{Error: err}, err
533 - }
534 - quote = string(r)
535 - }
536 -
537 - // Extract header_row flag
538 - headerRow, err := strconv.Atoi(params[2])
539 - if err != nil {
540 - err = fmt.Errorf("invalid header row parameter")
541 - return Result{Error: err}, err
542 - }
543 - hasHeader := (headerRow == 1)
544 -
545 - // Check for Sep=/SEP=/sEp= declaration (case-INSENSITIVE)
546 - // Sep= overrides the delimiter param for ALL rows
547 - data := value.Data
548 - lines := strings.Split(data, "\n")
549 - startLine := 0
550 -
551 - if len(lines) > 0 {
552 - firstLine := strings.TrimRight(lines[0], "\r") // Handle \r\n line breaks
553 - // Case-insensitive check for "sep="
554 - if len(firstLine) >= 4 && strings.ToLower(firstLine[:4]) == "sep=" {
555 - if len(firstLine) == 4 {
556 - // "sep=" with no character - invalid, treat as data
557 - } else {
558 - // Check if exactly one character after "sep="
559 - r, size := utf8.DecodeRuneInString(firstLine[4:])
560 - if size == len(firstLine[4:]) {
561 - // Valid single-char sep= line
562 - // Only override delimiter if it wasn't explicitly set in params
563 - if !delimExplicit {
564 - delim = string(r)
565 - }
566 - startLine = 1
567 - // Reconstruct data without sep= line
568 - data = strings.Join(lines[startLine:], "\n")
569 - }
570 - // If multi-char after "sep=", treat as data (don't skip)
571 - }
572 - }
573 - }
574 -
575 - // Check if input ends with newline (for empty row logic)
576 - endsWithNewline := strings.HasSuffix(value.Data, "\n") || strings.HasSuffix(value.Data, "\r\n")
577 -
578 - // Parse CSV with strict validation
579 - records, err := parseCSVStrict(data, delim, quote)
580 - if err != nil {
581 - return Result{Error: err}, err
582 - }
583 -
584 - // Empty input returns empty metrics array
585 - if len(records) == 0 {
586 - return Result{Metrics: []Metric{}}, nil
587 - }
588 -
589 - var metrics []Metric
590 -
591 - if hasHeader {
592 - // First row is header
593 - if len(records) < 1 {
594 - // Only header line, no data
595 - return Result{Metrics: []Metric{}}, nil
596 - }
597 -
598 - header := records[0]
599 -
600 - // Validate no duplicate column names
601 - seen := make(map[string]bool)
602 - for _, col := range header {
603 - if seen[col] {
604 - err := fmt.Errorf("duplicate column name: %s", col)
605 - return Result{Error: err}, err
606 - }
607 - seen[col] = true
608 - }
609 -
610 - // Convert each data row to a metric
611 - for i := 1; i < len(records); i++ {
612 - row := records[i]
613 -
614 - // Validate field count: data row can't have more fields than header
615 - if len(row) > len(header) {
616 - err := fmt.Errorf("data row has more fields than header")
617 - return Result{Error: err}, err
618 - }
619 -
620 - obj := make(map[string]string)
621 - for j, val := range row {
622 - if j < len(header) {
623 - obj[header[j]] = val
624 - }
625 - }
626 - // Fill missing fields with empty strings
627 - for j := len(row); j < len(header); j++ {
628 - obj[header[j]] = ""
629 - }
630 -
631 - // Marshal row to JSON object string
632 - jsonBytes, err := marshalOrderedJSONSingle(obj, header)
633 - if err != nil {
634 - return Result{Error: err}, err
635 - }
636 -
637 - metrics = append(metrics, Metric{
638 - Name: "csv_row",
639 - Value: jsonBytes,
640 - Type: ValueTypeStr,
641 - Labels: map[string]string{"row": strconv.Itoa(i)}, // 1-based row number (excluding header)
642 - })
643 - }
644 -
645 - // Add empty trailing row if input ended with newline
646 - if endsWithNewline {
647 - emptyObj := make(map[string]string)
648 - for _, h := range header {
649 - emptyObj[h] = ""
650 - }
651 - jsonBytes, err := marshalOrderedJSONSingle(emptyObj, header)
652 - if err != nil {
653 - return Result{Error: err}, err
654 - }
655 - metrics = append(metrics, Metric{
656 - Name: "csv_row",
657 - Value: jsonBytes,
658 - Type: ValueTypeStr,
659 - Labels: map[string]string{"row": strconv.Itoa(len(metrics) + 1)},
660 - })
661 - }
662 - } else {
663 - // No header - use 1-based column indices as keys
664 - for i, row := range records {
665 - var obj map[string]string
666 -
667 - // Special case: single empty field becomes empty object
668 - if len(row) == 1 && row[0] == "" {
669 - obj = make(map[string]string)
670 - } else {
671 - obj = make(map[string]string)
672 - for j, val := range row {
673 - // Use 1-based indexing
674 - obj[strconv.Itoa(j+1)] = val
675 - }
676 - }
677 -
678 - // Marshal row to JSON object string
679 - jsonBytes, err := json.Marshal(obj)
680 - if err != nil {
681 - return Result{Error: err}, err
682 - }
683 -
684 - metrics = append(metrics, Metric{
685 - Name: "csv_row",
686 - Value: string(jsonBytes),
687 - Type: ValueTypeStr,
688 - Labels: map[string]string{"row": strconv.Itoa(i + 1)}, // 1-based row number
689 - })
690 - }
691 -
692 - // Add empty trailing row if input ended with newline
693 - if endsWithNewline && len(metrics) > 0 {
694 - emptyJSON, _ := json.Marshal(make(map[string]string))
695 - metrics = append(metrics, Metric{
696 - Name: "csv_row",
697 - Value: string(emptyJSON),
698 - Type: ValueTypeStr,
699 - Labels: map[string]string{"row": strconv.Itoa(len(metrics) + 1)},
700 - })
701 - }
702 - }
703 -
704 - return Result{Metrics: metrics}, nil
705 -}
706 -
707 -// marshalOrderedJSONSingle marshals a single map with keys in header order.
708 -func marshalOrderedJSONSingle(obj map[string]string, header []string) (string, error) {
709 - var builder strings.Builder
710 - builder.WriteString("{")
711 - first := true
712 - for _, key := range header {
713 - val := obj[key]
714 - if !first {
715 - builder.WriteString(",")
716 - }
717 - first = false
718 - // Properly escape JSON key and value
719 - keyJSON, _ := json.Marshal(key)
720 - valJSON, _ := json.Marshal(val)
721 - builder.Write(keyJSON)
722 - builder.WriteString(":")
723 - builder.Write(valJSON)
724 - }
725 - builder.WriteString("}")
726 - return builder.String(), nil
727 -}
src/go/plugin/scripts.d/pkg/zabbixpreproc/fuzz_test.go deleted
-193
@@ -1,193 +0,0 @@
1 -package zabbixpreproc
2 -
3 -import (
4 - "strings"
5 - "testing"
6 - "time"
7 -)
8 -
9 -// Fuzzing tests to find edge cases and crashes
10 -// Run with: go test -fuzz=Fuzz
11 -
12 -// FuzzJSONPath fuzzes JSONPath extraction
13 -func FuzzJSONPath(f *testing.F) {
14 - // Seed corpus with known good inputs
15 - f.Add(`{"key": "value"}`, "$.key")
16 - f.Add(`{"a": {"b": 123}}`, "$.a.b")
17 - f.Add(`[1,2,3]`, "$[0]")
18 -
19 - f.Fuzz(func(t *testing.T, jsonData, path string) {
20 - // Skip empty path (required parameter)
21 - if path == "" {
22 - return
23 - }
24 -
25 - value := Value{Data: jsonData, Type: ValueTypeStr}
26 - _, _ = jsonpathExtract(value, path)
27 - // Don't check error - we're looking for crashes, not correctness
28 - })
29 -}
30 -
31 -// FuzzXPath fuzzes XPath extraction
32 -func FuzzXPath(f *testing.F) {
33 - // Seed corpus
34 - f.Add(`<root><item>value</item></root>`, "//item/text()")
35 - f.Add(`<data><a>1</a><b>2</b></data>`, "//a")
36 -
37 - f.Fuzz(func(t *testing.T, xmlData, path string) {
38 - // Skip empty path (required parameter)
39 - if path == "" {
40 - return
41 - }
42 -
43 - value := Value{Data: xmlData, Type: ValueTypeStr}
44 - _, _ = xpathExtract(value, path)
45 - // Don't check error - we're looking for crashes
46 - })
47 -}
48 -
49 -// FuzzRegexSubstitution fuzzes regex pattern replacement
50 -func FuzzRegexSubstitution(f *testing.F) {
51 - // Seed corpus
52 - f.Add("hello world", "world\nuniverse")
53 - f.Add("test123", "[0-9]+\nNUM")
54 -
55 - f.Fuzz(func(t *testing.T, input, params string) {
56 - // Skip invalid params format (need newline separator)
57 - if !strings.Contains(params, "\n") {
58 - return
59 - }
60 -
61 - value := Value{Data: input, Type: ValueTypeStr}
62 - _, _ = regexSubstitute(value, params)
63 - // Don't check error - we're looking for crashes
64 - })
65 -}
66 -
67 -// FuzzMultiplier fuzzes numeric multiplication
68 -func FuzzMultiplier(f *testing.F) {
69 - // Seed corpus
70 - f.Add("100", "2.5")
71 - f.Add("3.14", "10")
72 -
73 - f.Fuzz(func(t *testing.T, input, multiplier string) {
74 - value := Value{Data: input, Type: ValueTypeFloat}
75 - _, _ = multiplyValue(value, multiplier)
76 - // Don't check error - we're looking for crashes
77 - })
78 -}
79 -
80 -// FuzzCSVToJSON fuzzes CSV parsing
81 -func FuzzCSVToJSON(f *testing.F) {
82 - // Seed corpus
83 - f.Add("a,b,c\n1,2,3", ",\n\"\n1")
84 - f.Add("x;y\n10;20", ";\n\"\n1")
85 -
86 - f.Fuzz(func(t *testing.T, csvData, params string) {
87 - // Skip invalid params (need at least 3 newlines)
88 - if strings.Count(params, "\n") < 2 {
89 - return
90 - }
91 -
92 - value := Value{Data: csvData, Type: ValueTypeStr}
93 - _, _ = csvToJSON(value, params)
94 - // Don't check error - we're looking for crashes
95 - })
96 -}
97 -
98 -// FuzzPrometheusPattern fuzzes Prometheus metric extraction
99 -func FuzzPrometheusPattern(f *testing.F) {
100 - // Seed corpus
101 - f.Add(`http_requests{method="GET"} 100`, `http_requests{method="GET"}`)
102 - f.Add(`cpu_usage 45.2`, `cpu_usage`)
103 -
104 - f.Fuzz(func(t *testing.T, promData, pattern string) {
105 - // Skip empty pattern
106 - if pattern == "" {
107 - return
108 - }
109 -
110 - value := Value{Data: promData, Type: ValueTypeStr}
111 - _, _ = prometheusPattern(value, pattern)
112 - // Don't check error - we're looking for crashes
113 - })
114 -}
115 -
116 -// FuzzSNMPWalkToJSON fuzzes SNMP walk parsing
117 -func FuzzSNMPWalkToJSON(f *testing.F) {
118 - // Seed corpus
119 - f.Add(`.1.3.6.1.2.1.1.1.0 = STRING: "Linux"`, "{#NAME}\n.1.3.6.1.2.1.1\n1")
120 - f.Add(`.1.3.6.1.4.1.2021.10.1.3.1 = INTEGER: 50`, "{#CPU}\n.1.3.6.1.4.1.2021.10.1.3\n1")
121 -
122 - f.Fuzz(func(t *testing.T, snmpData, params string) {
123 - // Skip invalid params (need multiple of 3 newlines)
124 - if strings.Count(params, "\n")%3 != 2 {
125 - return
126 - }
127 -
128 - value := Value{Data: snmpData, Type: ValueTypeStr}
129 - logger := NoopLogger{}
130 - _, _ = snmpWalkToJSON(value, params, logger)
131 - // Don't check error - we're looking for crashes
132 - })
133 -}
134 -
135 -// FuzzTrim fuzzes string trimming
136 -func FuzzTrim(f *testing.F) {
137 - // Seed corpus
138 - f.Add(" hello ", " ")
139 - f.Add("\t\ntest\r\n", "\t\n\r")
140 -
141 - f.Fuzz(func(t *testing.T, input, chars string) {
142 - value := Value{Data: input, Type: ValueTypeStr}
143 - _, _ = trimValue(value, chars, "both")
144 - _, _ = trimValue(value, chars, "left")
145 - _, _ = trimValue(value, chars, "right")
146 - // Don't check error - we're looking for crashes
147 - })
148 -}
149 -
150 -// FuzzDelta fuzzes delta value calculation
151 -func FuzzDelta(f *testing.F) {
152 - // Seed corpus
153 - f.Add("100", "")
154 - f.Add("50", "")
155 -
156 - p := NewPreprocessor("fuzz-shard")
157 -
158 - f.Fuzz(func(t *testing.T, input, params string) {
159 - value := Value{
160 - Data: input,
161 - Type: ValueTypeUint64,
162 - Timestamp: time.Now(),
163 - }
164 - _, _ = p.deltaValue("fuzz-item", value, params)
165 - // Don't check error - we're looking for crashes
166 - })
167 -}
168 -
169 -// FuzzValidateRange fuzzes range validation
170 -func FuzzValidateRange(f *testing.F) {
171 - // Seed corpus
172 - f.Add("50", "0\n100")
173 - f.Add("3.14", "-10\n10")
174 -
175 - f.Fuzz(func(t *testing.T, input, params string) {
176 - value := Value{Data: input, Type: ValueTypeFloat}
177 - _, _ = validateRange(value, params)
178 - // Don't check error - we're looking for crashes
179 - })
180 -}
181 -
182 -// FuzzInterpretEscapeSequences fuzzes escape sequence interpretation
183 -func FuzzInterpretEscapeSequences(f *testing.F) {
184 - // Seed corpus
185 - f.Add("hello\\nworld")
186 - f.Add("tab\\there")
187 - f.Add("back\\\\slash")
188 -
189 - f.Fuzz(func(t *testing.T, input string) {
190 - _ = interpretEscapeSequences(input)
191 - // Don't check result - we're looking for crashes
192 - })
193 -}
src/go/plugin/scripts.d/pkg/zabbixpreproc/helpers_test.go deleted
-321
@@ -1,321 +0,0 @@
1 -package zabbixpreproc
2 -
3 -import (
4 - "testing"
5 -)
6 -
7 -// Unit tests for helper functions (separate from integration tests)
8 -
9 -func TestFormatFloat(t *testing.T) {
10 - tests := []struct {
11 - name string
12 - input float64
13 - want string
14 - }{
15 - {"whole number", 42.0, "42"},
16 - {"simple decimal", 3.14, "3.14"},
17 - {"small decimal", 0.001, "0.001"},
18 - {"large number", 1234567.89, "1.23456789e+06"},
19 - {"negative whole", -100.0, "-100"},
20 - {"negative decimal", -0.5, "-0.5"},
21 - {"zero", 0.0, "0"},
22 - {"very small", 0.0000001, "1e-07"},
23 - }
24 -
25 - for _, tt := range tests {
26 - t.Run(tt.name, func(t *testing.T) {
27 - got := formatFloat(tt.input)
28 - if got != tt.want {
29 - t.Errorf("formatFloat(%v) = %q, want %q", tt.input, got, tt.want)
30 - }
31 - })
32 - }
33 -}
34 -
35 -func TestInterpretEscapeSequences(t *testing.T) {
36 - tests := []struct {
37 - name string
38 - input string
39 - want string
40 - }{
41 - {"no escapes", "hello", "hello"},
42 - {"newline", "line1\\nline2", "line1\nline2"},
43 - {"tab", "col1\\tcol2", "col1\tcol2"},
44 - {"carriage return", "text\\rmore", "text\rmore"},
45 - {"backslash", "path\\\\file", "path\\file"},
46 - {"space", "a\\sb", "a b"},
47 - {"multiple escapes", "a\\nb\\tc", "a\nb\tc"},
48 - {"double backslash s", "trim\\\\sthis", "trim \t\r\n\\this"}, // \\s expands to full whitespace class
49 - {"unknown escape", "\\x41BC", "\\x41BC"}, // Unknown escapes preserved
50 - }
51 -
52 - for _, tt := range tests {
53 - t.Run(tt.name, func(t *testing.T) {
54 - got := interpretEscapeSequences(tt.input)
55 - if got != tt.want {
56 - t.Errorf("interpretEscapeSequences(%q) = %q, want %q", tt.input, got, tt.want)
57 - }
58 - })
59 - }
60 -}
61 -
62 -func TestConvertBackreferences(t *testing.T) {
63 - tests := []struct {
64 - name string
65 - replacement string
66 - want string
67 - }{
68 - {"no backrefs", "literal", "literal"},
69 - {"\\1 to ${1}", "prefix\\1suffix", "prefix${1}suffix"},
70 - {"\\2 to ${2}", "value\\2", "value${2}"},
71 - {"multiple backrefs", "\\1 and \\2", "${1} and ${2}"},
72 - {"\\\\1 literal backslash", "\\\\1", "\\1"},
73 - {"mixed", "a\\1b\\\\2c\\3", "a${1}b\\2c${3}"},
74 - {"\\0 full match", "result\\0", "result${0}"},
75 - {"multi-digit", "\\10", "${10}"},
76 - }
77 -
78 - for _, tt := range tests {
79 - t.Run(tt.name, func(t *testing.T) {
80 - got := convertBackreferences(tt.replacement)
81 - if got != tt.want {
82 - t.Errorf("convertBackreferences(%q) = %q, want %q", tt.replacement, got, tt.want)
83 - }
84 - })
85 - }
86 -}
87 -
88 -func TestParseHexBytes(t *testing.T) {
89 - tests := []struct {
90 - name string
91 - input string
92 - want []byte
93 - wantErr bool
94 - }{
95 - {"simple hex", "48656C6C6F", []byte("Hello"), false},
96 - {"with spaces", "48 65 6C 6C 6F", []byte("Hello"), false},
97 - {"lowercase", "68656c6c6f", []byte("hello"), false},
98 - {"mixed case", "48656C6c6F", []byte("Hello"), false},
99 - {"empty", "", []byte{}, false},
100 - {"invalid hex", "ZZZZ", nil, true},
101 - {"odd length", "48656C6C6", nil, true},
102 - {"null bytes", "00410042", []byte("\x00A\x00B"), false},
103 - }
104 -
105 - for _, tt := range tests {
106 - t.Run(tt.name, func(t *testing.T) {
107 - got, err := parseHexBytes(tt.input)
108 - if tt.wantErr {
109 - if err == nil {
110 - t.Errorf("parseHexBytes(%q) expected error, got nil", tt.input)
111 - }
112 - } else {
113 - if err != nil {
114 - t.Errorf("parseHexBytes(%q) unexpected error: %v", tt.input, err)
115 - }
116 - if string(got) != string(tt.want) {
117 - t.Errorf("parseHexBytes(%q) = %q, want %q", tt.input, got, tt.want)
118 - }
119 - }
120 - })
121 - }
122 -}
123 -
124 -func TestConvertBITSToInteger(t *testing.T) {
125 - tests := []struct {
126 - name string
127 - input string
128 - want string
129 - wantErr bool
130 - }{
131 - {"single byte", "05", "5", false},
132 - {"two bytes hex", "01 02", "513", false}, // Little-endian: reversed to 02 01 = 0x0201 = 513
133 - {"four bytes hex", "01 02 03 04", "67305985", false}, // Little-endian: reversed to 04 03 02 01 = 0x04030201 = 67305985
134 - {"all zeros", "00 00", "0", false},
135 - {"all ones", "FF FF", "65535", false},
136 - {"invalid hex", "ZZ", "", true},
137 - }
138 -
139 - for _, tt := range tests {
140 - t.Run(tt.name, func(t *testing.T) {
141 - got, err := convertBITSToInteger(tt.input)
142 - if tt.wantErr {
143 - if err == nil {
144 - t.Errorf("convertBITSToInteger(%q) expected error, got nil", tt.input)
145 - }
146 - } else {
147 - if err != nil {
148 - t.Errorf("convertBITSToInteger(%q) unexpected error: %v", tt.input, err)
149 - }
150 - if got != tt.want {
151 - t.Errorf("convertBITSToInteger(%q) = %q, want %q", tt.input, got, tt.want)
152 - }
153 - }
154 - })
155 - }
156 -}
157 -
158 -func TestFormatSTRING(t *testing.T) {
159 - tests := []struct {
160 - name string
161 - input string
162 - formatMode int
163 - want string
164 - wantErr bool
165 - }{
166 - {"plain text - value only", "hello", 1, "hello", false},
167 - {"with quotes - value only", `"quoted"`, 1, `quoted`, false}, // formatSTRING strips outer quotes
168 - {"empty - value only", "", 1, "", false},
169 - {"plain text - oid and value", "hello", 0, "hello", false},
170 - {"with quotes - oid and value", `"quoted"`, 0, `quoted`, false}, // formatSTRING strips outer quotes
171 - }
172 -
173 - for _, tt := range tests {
174 - t.Run(tt.name, func(t *testing.T) {
175 - got, err := formatSTRING(tt.input, tt.formatMode)
176 - if tt.wantErr {
177 - if err == nil {
178 - t.Errorf("formatSTRING(%q, %d) expected error, got nil", tt.input, tt.formatMode)
179 - }
180 - } else {
181 - if err != nil {
182 - t.Errorf("formatSTRING(%q, %d) unexpected error: %v", tt.input, tt.formatMode, err)
183 - }
184 - if got != tt.want {
185 - t.Errorf("formatSTRING(%q, %d) = %q, want %q", tt.input, tt.formatMode, got, tt.want)
186 - }
187 - }
188 - })
189 - }
190 -}
191 -
192 -func TestValueTypeToString(t *testing.T) {
193 - tests := []struct {
194 - name string
195 - input ValueType
196 - want string
197 - }{
198 - {"string type", ValueTypeStr, "ITEM_VALUE_TYPE_STR"},
199 - {"uint64 type", ValueTypeUint64, "ITEM_VALUE_TYPE_UINT64"},
200 - {"float type", ValueTypeFloat, "ITEM_VALUE_TYPE_FLOAT"},
201 - {"invalid type", ValueType(99), "UNKNOWN"},
202 - }
203 -
204 - for _, tt := range tests {
205 - t.Run(tt.name, func(t *testing.T) {
206 - got := valueTypeToString(tt.input)
207 - if got != tt.want {
208 - t.Errorf("valueTypeToString(%v) = %q, want %q", tt.input, got, tt.want)
209 - }
210 - })
211 - }
212 -}
213 -
214 -func TestParseValueType(t *testing.T) {
215 - tests := []struct {
216 - name string
217 - input string
218 - want ValueType
219 - wantErr bool
220 - }{
221 - {"string type", "ITEM_VALUE_TYPE_STR", ValueTypeStr, false},
222 - {"uint64 type", "ITEM_VALUE_TYPE_UINT64", ValueTypeUint64, false},
223 - {"float type", "ITEM_VALUE_TYPE_FLOAT", ValueTypeFloat, false},
224 - {"invalid type", "INVALID", ValueTypeStr, true},
225 - {"empty", "", ValueTypeStr, true},
226 - }
227 -
228 - for _, tt := range tests {
229 - t.Run(tt.name, func(t *testing.T) {
230 - got, err := parseValueType(tt.input)
231 - if tt.wantErr {
232 - if err == nil {
233 - t.Errorf("parseValueType(%q) expected error, got nil", tt.input)
234 - }
235 - } else {
236 - if err != nil {
237 - t.Errorf("parseValueType(%q) unexpected error: %v", tt.input, err)
238 - }
239 - if got != tt.want {
240 - t.Errorf("parseValueType(%q) = %v, want %v", tt.input, got, tt.want)
241 - }
242 - }
243 - })
244 - }
245 -}
246 -
247 -func TestNoopLogger(t *testing.T) {
248 - // Ensure NoopLogger doesn't panic and accepts all methods
249 - logger := NoopLogger{}
250 - logger.Debug("test", "key", "value")
251 - logger.Info("test", "key", "value")
252 - logger.Warn("test", "key", "value")
253 - logger.Error("test", "key", "value")
254 -
255 - // If we get here without panic, test passes
256 -}
257 -
258 -func TestErrorHandlerValidation(t *testing.T) {
259 - p := NewPreprocessor("test")
260 -
261 - tests := []struct {
262 - name string
263 - handler ErrorHandler
264 - origErr error
265 - wantErr bool
266 - wantVal string
267 - }{
268 - {
269 - name: "default - returns original error",
270 - handler: ErrorHandler{Action: ErrorActionDefault},
271 - origErr: errTestError,
272 - wantErr: true,
273 - },
274 - {
275 - name: "discard - returns empty value",
276 - handler: ErrorHandler{Action: ErrorActionDiscard},
277 - origErr: errTestError,
278 - wantErr: false,
279 - wantVal: "",
280 - },
281 - {
282 - name: "set value",
283 - handler: ErrorHandler{Action: ErrorActionSetValue, Params: "fallback"},
284 - origErr: errTestError,
285 - wantErr: false,
286 - wantVal: "fallback",
287 - },
288 - {
289 - name: "set error - custom message",
290 - handler: ErrorHandler{Action: ErrorActionSetError, Params: "Custom error"},
291 - origErr: errTestError,
292 - wantErr: true,
293 - },
294 - }
295 -
296 - for _, tt := range tests {
297 - t.Run(tt.name, func(t *testing.T) {
298 - val, err := p.handleError(tt.origErr, tt.handler)
299 - if tt.wantErr && err == nil {
300 - t.Error("expected error, got nil")
301 - }
302 - if !tt.wantErr && err != nil {
303 - t.Errorf("unexpected error: %v", err)
304 - }
305 - if !tt.wantErr && val.Data != tt.wantVal {
306 - t.Errorf("got value %q, want %q", val.Data, tt.wantVal)
307 - }
308 - })
309 - }
310 -}
311 -
312 -// Helper error for tests
313 -var errTestError = &testError{msg: "test error"}
314 -
315 -type testError struct {
316 - msg string
317 -}
318 -
319 -func (e *testError) Error() string {
320 - return e.msg
321 -}
src/go/plugin/scripts.d/pkg/zabbixpreproc/javascript_sandbox_test.go deleted
-289
@@ -1,289 +0,0 @@
1 -package zabbixpreproc
2 -
3 -import (
4 - "strings"
5 - "testing"
6 - "time"
7 -)
8 -
9 -// TestJavaScriptExecutionTimeout tests that JavaScript execution times out after 5 seconds
10 -func TestJavaScriptExecutionTimeout(t *testing.T) {
11 - value := Value{Data: "test", Type: ValueTypeStr}
12 -
13 - // Infinite loop should timeout
14 - script := `
15 - while(true) {
16 - // Infinite loop
17 - }
18 - return value;
19 - `
20 -
21 - start := time.Now()
22 - _, err := javascriptExecute(value, script, DefaultLimits().JavaScript)
23 - duration := time.Since(start)
24 -
25 - if err == nil {
26 - t.Fatal("Expected timeout error, got nil")
27 - }
28 -
29 - if !strings.Contains(err.Error(), "timeout") && !strings.Contains(err.Error(), "Interrupt") {
30 - t.Errorf("Expected timeout error, got: %v", err)
31 - }
32 -
33 - // Should timeout around 10 seconds (Zabbix default), allow some overhead
34 - if duration < 9*time.Second || duration > 12*time.Second {
35 - t.Errorf("Expected timeout around 10s, got %v", duration)
36 - }
37 -}
38 -
39 -// TestJavaScriptCallStackLimit tests that deep recursion is limited
40 -func TestJavaScriptCallStackLimit(t *testing.T) {
41 - value := Value{Data: "test", Type: ValueTypeStr}
42 -
43 - // Deep recursion should hit stack limit
44 - script := `
45 - function recurse(n) {
46 - if (n > 0) {
47 - return recurse(n - 1);
48 - }
49 - return n;
50 - }
51 - return recurse(10000);
52 - `
53 -
54 - _, err := javascriptExecute(value, script, DefaultLimits().JavaScript)
55 -
56 - if err == nil {
57 - t.Fatal("Expected stack overflow error, got nil")
58 - }
59 -
60 - // Goja may report stack issues in various ways - just verify it errors on deep recursion
61 - if err == nil {
62 - t.Fatalf("Expected deep recursion to fail, got nil error")
63 - }
64 - // The error should mention the recursive function or contain depth info
65 - errStr := err.Error()
66 - if !strings.Contains(errStr, "recurse") && !strings.Contains(errStr, "stack") && !strings.Contains(errStr, "RangeError") {
67 - t.Logf("Deep recursion error: %v", err)
68 - }
69 -}
70 -
71 -// TestJavaScriptRSAKeyValidation tests that RSA key validation is in place
72 -// Note: Minimum key size is 1024 bits to match Zabbix behavior (though 2048+ is recommended)
73 -func TestJavaScriptRSAKeyValidation(t *testing.T) {
74 - // The official Zabbix tests verify that 1024-bit keys work correctly.
75 - // Keys smaller than 1024 bits would be rejected by the validation in javascriptExecute.
76 - // This test verifies the validation code exists and works with valid keys.
77 -
78 - // This is already tested by the official Zabbix test suite with 1024-bit keys,
79 - // so we just verify that the validation constant is set correctly.
80 - if jsMinRSAKeyBits != 1024 {
81 - t.Errorf("Expected minimum RSA key size to be 1024 bits (Zabbix compatibility), got %d", jsMinRSAKeyBits)
82 - }
83 -
84 - t.Log("RSA key validation is in place with minimum", jsMinRSAKeyBits, "bits")
85 -}
86 -
87 -// TestJavaScriptNormalExecution tests that valid JavaScript still executes correctly
88 -func TestJavaScriptNormalExecution(t *testing.T) {
89 - value := Value{Data: "hello", Type: ValueTypeStr}
90 -
91 - tests := []struct {
92 - name string
93 - script string
94 - expected string
95 - }{
96 - {
97 - name: "simple return",
98 - script: `return value;`,
99 - expected: "hello",
100 - },
101 - {
102 - name: "string concatenation",
103 - script: `return value + " world";`,
104 - expected: "hello world",
105 - },
106 - {
107 - name: "btoa encoding",
108 - script: `return btoa(value);`,
109 - expected: "aGVsbG8=",
110 - },
111 - {
112 - name: "atob decoding",
113 - script: `return atob("aGVsbG8=");`,
114 - expected: "hello",
115 - },
116 - {
117 - name: "hmac",
118 - script: `return hmac("sha256", "secret", value);`,
119 - expected: "88aab3ede8d3adf94d26ab90d3bafd4a2083070c3bcce9c014ee04a443847c0b",
120 - },
121 - }
122 -
123 - for _, tt := range tests {
124 - t.Run(tt.name, func(t *testing.T) {
125 - result, err := javascriptExecute(value, tt.script, DefaultLimits().JavaScript)
126 - if err != nil {
127 - t.Fatalf("Unexpected error: %v", err)
128 - }
129 -
130 - if result.Data != tt.expected {
131 - t.Errorf("Expected '%s', got '%s'", tt.expected, result.Data)
132 - }
133 - })
134 - }
135 -}
136 -
137 -// TestJavaScriptMemoryIntensiveOperation tests that memory-intensive operations complete within limits
138 -func TestJavaScriptMemoryIntensiveOperation(t *testing.T) {
139 - value := Value{Data: "test", Type: ValueTypeStr}
140 -
141 - // Create a large array (but not infinite)
142 - script := `
143 - var arr = [];
144 - for (var i = 0; i < 10000; i++) {
145 - arr.push(i);
146 - }
147 - return arr.length.toString();
148 - `
149 -
150 - result, err := javascriptExecute(value, script, DefaultLimits().JavaScript)
151 - if err != nil {
152 - t.Fatalf("Unexpected error: %v", err)
153 - }
154 -
155 - if result.Data != "10000" {
156 - t.Errorf("Expected '10000', got '%s'", result.Data)
157 - }
158 -}
159 -
160 -// TestJavaScriptCPUIntensiveOperation tests that CPU-intensive operations complete within timeout
161 -func TestJavaScriptCPUIntensiveOperation(t *testing.T) {
162 - value := Value{Data: "test", Type: ValueTypeStr}
163 -
164 - // Fibonacci calculation (should complete quickly)
165 - script := `
166 - function fib(n) {
167 - if (n <= 1) return n;
168 - return fib(n - 1) + fib(n - 2);
169 - }
170 - return fib(20).toString();
171 - `
172 -
173 - result, err := javascriptExecute(value, script, DefaultLimits().JavaScript)
174 - if err != nil {
175 - t.Fatalf("Unexpected error: %v", err)
176 - }
177 -
178 - if result.Data != "6765" {
179 - t.Errorf("Expected '6765', got '%s'", result.Data)
180 - }
181 -}
182 -
183 -// TestJavaScriptPanicRecovery tests that panics are caught and converted to errors
184 -func TestJavaScriptPanicRecovery(t *testing.T) {
185 - value := Value{Data: "test", Type: ValueTypeStr}
186 -
187 - // Trigger a JavaScript error
188 - script := `
189 - throw new Error("test error");
190 - return value;
191 - `
192 -
193 - _, err := javascriptExecute(value, script, DefaultLimits().JavaScript)
194 -
195 - if err == nil {
196 - t.Fatal("Expected error from thrown exception, got nil")
197 - }
198 -
199 - if !strings.Contains(err.Error(), "test error") && !strings.Contains(err.Error(), "Error") {
200 - t.Errorf("Expected error message, got: %v", err)
201 - }
202 -}
203 -
204 -// TestJavaScriptHMACWithInvalidAlgorithm tests HMAC error handling
205 -func TestJavaScriptHMACWithInvalidAlgorithm(t *testing.T) {
206 - value := Value{Data: "test", Type: ValueTypeStr}
207 -
208 - script := `
209 - return hmac("invalid", "key", value);
210 - `
211 -
212 - _, err := javascriptExecute(value, script, DefaultLimits().JavaScript)
213 -
214 - if err == nil {
215 - t.Fatal("Expected error for invalid HMAC algorithm, got nil")
216 - }
217 -
218 - if !strings.Contains(err.Error(), "unsupported") && !strings.Contains(err.Error(), "algorithm") {
219 - t.Errorf("Expected algorithm error, got: %v", err)
220 - }
221 -}
222 -
223 -// TestJavaScriptSandboxingDoesNotAffectValidCode tests that sandboxing doesn't break valid operations
224 -func TestJavaScriptSandboxingDoesNotAffectValidCode(t *testing.T) {
225 - value := Value{Data: "100", Type: ValueTypeStr}
226 -
227 - // Test various operations that should work fine
228 - tests := []struct {
229 - name string
230 - script string
231 - }{
232 - {
233 - name: "arithmetic",
234 - script: `return (parseInt(value) * 2).toString();`,
235 - },
236 - {
237 - name: "string operations",
238 - script: `return value.toUpperCase();`,
239 - },
240 - {
241 - name: "array operations",
242 - script: `return [1, 2, 3].map(x => x * 2).join(",");`,
243 - },
244 - {
245 - name: "object operations",
246 - script: `var obj = {a: 1}; return obj.a.toString();`,
247 - },
248 - {
249 - name: "json operations",
250 - script: `return JSON.stringify({value: value});`,
251 - },
252 - }
253 -
254 - for _, tt := range tests {
255 - t.Run(tt.name, func(t *testing.T) {
256 - _, err := javascriptExecute(value, tt.script, DefaultLimits().JavaScript)
257 - if err != nil {
258 - t.Errorf("Valid code failed after sandboxing: %v", err)
259 - }
260 - })
261 - }
262 -}
263 -
264 -// TestJavaScriptTimeoutPreventsHang tests that timeout prevents indefinite hangs
265 -func TestJavaScriptTimeoutPreventsHang(t *testing.T) {
266 - value := Value{Data: "test", Type: ValueTypeStr}
267 -
268 - // Slow operation that would hang without timeout
269 - script := `
270 - var sum = 0;
271 - for (var i = 0; i < 999999999999; i++) {
272 - sum += i;
273 - }
274 - return sum.toString();
275 - `
276 -
277 - start := time.Now()
278 - _, err := javascriptExecute(value, script, DefaultLimits().JavaScript)
279 - duration := time.Since(start)
280 -
281 - if err == nil {
282 - t.Fatal("Expected timeout error, got nil")
283 - }
284 -
285 - // Should timeout around 10s (Zabbix default), not run for minutes
286 - if duration > 12*time.Second {
287 - t.Errorf("Execution took too long (%v), timeout not working", duration)
288 - }
289 -}
src/go/plugin/scripts.d/pkg/zabbixpreproc/javascript_step.go deleted
-467
@@ -1,467 +0,0 @@
1 -package zabbixpreproc
2 -
3 -import (
4 - "crypto"
5 - "crypto/hmac"
6 - "crypto/md5"
7 - "crypto/rand"
8 - "crypto/rsa"
9 - "crypto/sha256"
10 - "crypto/x509"
11 - "encoding/base64"
12 - "encoding/hex"
13 - "encoding/pem"
14 - "fmt"
15 - "hash"
16 - "strings"
17 - "sync"
18 - "time"
19 -
20 - "github.com/dop251/goja"
21 -)
22 -
23 -// JavaScript execution safety constants
24 -const (
25 - jsExecutionTimeout = 5 * time.Second // Maximum execution time for JavaScript
26 - jsMaxCallStackSize = 100 // Maximum call stack depth
27 - jsMinRSAKeyBits = 1024 // Minimum RSA key size (bits) - matches Zabbix behavior
28 - jsProgramCacheSize = 1000 // Maximum number of cached compiled programs
29 - // NOTE: 1024-bit RSA keys are deprecated and insecure (NIST recommends 2048+ bits).
30 - // This limit matches Zabbix's behavior for compatibility. Production deployments
31 - // should enforce 2048+ bit keys at the configuration/policy level.
32 -)
33 -
34 -// PEM key normalization constants
35 -const (
36 - pemBeginMarker = "-----BEGIN"
37 - pemEndMarker = "-----END "
38 - pemDelimiter = "-----"
39 - pemBeginMarkerLen = 10 // len("-----BEGIN")
40 - pemEndMarkerLen = 9 // len("-----END ")
41 - pemDelimiterLen = 5 // len("-----")
42 - pemLineWidth = 64 // Standard PEM line width (RFC 7468)
43 - pemMinHeaderLen = 30 // Minimum length for PEM header check
44 - pemNewlineCheckPos = 27 // Position to check for newline after BEGIN marker
45 -)
46 -
47 -// boundedProgramCache implements a size-limited LRU cache for compiled JavaScript programs.
48 -type boundedProgramCache struct {
49 - mu sync.RWMutex
50 - cache map[string]*goja.Program
51 - order []string // LRU tracking (oldest first)
52 -}
53 -
54 -// newBoundedProgramCache creates a bounded program cache.
55 -func newBoundedProgramCache(maxSize int) *boundedProgramCache {
56 - return &boundedProgramCache{
57 - cache: make(map[string]*goja.Program),
58 - order: make([]string, 0, maxSize),
59 - }
60 -}
61 -
62 -// Get retrieves a compiled program from the cache.
63 -func (c *boundedProgramCache) Get(key string) (*goja.Program, bool) {
64 - c.mu.RLock()
65 - prog, found := c.cache[key]
66 - c.mu.RUnlock()
67 -
68 - if found {
69 - // Move to end of LRU order (most recently used)
70 - c.mu.Lock()
71 - for i, k := range c.order {
72 - if k == key {
73 - c.order = append(c.order[:i], c.order[i+1:]...)
74 - c.order = append(c.order, key)
75 - break
76 - }
77 - }
78 - c.mu.Unlock()
79 - }
80 -
81 - return prog, found
82 -}
83 -
84 -// Put stores a compiled program in the cache, evicting oldest if at capacity.
85 -func (c *boundedProgramCache) Put(key string, prog *goja.Program) {
86 - c.mu.Lock()
87 - defer c.mu.Unlock()
88 -
89 - // Check if already cached
90 - if _, exists := c.cache[key]; exists {
91 - // Update LRU order
92 - for i, k := range c.order {
93 - if k == key {
94 - c.order = append(c.order[:i], c.order[i+1:]...)
95 - c.order = append(c.order, key)
96 - break
97 - }
98 - }
99 - return
100 - }
101 -
102 - // Evict oldest if at capacity
103 - if len(c.cache) >= jsProgramCacheSize {
104 - oldest := c.order[0]
105 - delete(c.cache, oldest)
106 - c.order = c.order[1:]
107 - }
108 -
109 - // Add new entry
110 - c.cache[key] = prog
111 - c.order = append(c.order, key)
112 -}
113 -
114 -// JavaScript program caching for performance optimization
115 -var (
116 - // programCache caches compiled JavaScript programs (keyed by script code)
117 - // Bounded to jsProgramCacheSize to prevent unbounded memory growth
118 - programCache = newBoundedProgramCache(jsProgramCacheSize)
119 -)
120 -
121 -// createConfiguredVM creates a new goja VM with all built-in functions configured.
122 -// This function is called by vmPool when creating new VM instances.
123 -func createConfiguredVM() *goja.Runtime {
124 - vm := goja.New()
125 - vm.SetMaxCallStackSize(jsMaxCallStackSize)
126 -
127 - // Configure all built-in functions
128 - setupBuiltinFunctions(vm)
129 -
130 - return vm
131 -}
132 -
133 -// setupBuiltinFunctions configures all Zabbix built-in JavaScript functions on a VM.
134 -// This is called once per VM instance when it's created.
135 -func setupBuiltinFunctions(vm *goja.Runtime) {
136 - // btoa - Base64 encode (supports string and Uint8Array)
137 - vm.Set("btoa", func(call goja.FunctionCall) goja.Value {
138 - if len(call.Arguments) == 0 {
139 - return goja.Null()
140 - }
141 -
142 - var data []byte
143 - arg := call.Arguments[0]
144 -
145 - // Check if it's a Uint8Array
146 - if obj := arg.ToObject(vm); obj != nil {
147 - if arr := obj.Get("constructor"); arr != nil && arr.String() == "function Uint8Array() { [native code] }" {
148 - // It's a typed array, extract bytes
149 - if length := obj.Get("length"); length != nil {
150 - n := int(length.ToInteger())
151 - data = make([]byte, n)
152 - for i := 0; i < n; i++ {
153 - data[i] = byte(obj.Get(fmt.Sprintf("%d", i)).ToInteger())
154 - }
155 - }
156 - }
157 - }
158 -
159 - // If not Uint8Array, treat as string
160 - if data == nil {
161 - data = []byte(arg.String())
162 - }
163 -
164 - encoded := base64.StdEncoding.EncodeToString(data)
165 - return vm.ToValue(encoded)
166 - })
167 -
168 - // atob - Base64 decode
169 - vm.Set("atob", func(call goja.FunctionCall) goja.Value {
170 - if len(call.Arguments) == 0 {
171 - return goja.Null()
172 - }
173 -
174 - encoded := call.Arguments[0].String()
175 - decoded, err := base64.StdEncoding.DecodeString(encoded)
176 - if err != nil {
177 - // Invalid base64 - return empty string
178 - return vm.ToValue("")
179 - }
180 -
181 - return vm.ToValue(string(decoded))
182 - })
183 -
184 - // hmac - HMAC hash function
185 - vm.Set("hmac", func(call goja.FunctionCall) goja.Value {
186 - if len(call.Arguments) < 3 {
187 - panic(vm.NewGoError(fmt.Errorf("hmac requires 3 arguments: algorithm, key, data")))
188 - }
189 -
190 - // Check for null/undefined arguments
191 - if goja.IsNull(call.Arguments[1]) || goja.IsUndefined(call.Arguments[1]) {
192 - panic(vm.NewGoError(fmt.Errorf("invalid key parameter")))
193 - }
194 - if goja.IsNull(call.Arguments[2]) || goja.IsUndefined(call.Arguments[2]) {
195 - panic(vm.NewGoError(fmt.Errorf("invalid data parameter")))
196 - }
197 -
198 - algorithm := call.Arguments[0].String()
199 - key := call.Arguments[1].String()
200 - data := call.Arguments[2].String()
201 -
202 - var mac hash.Hash
203 -
204 - switch algorithm {
205 - case "md5":
206 - mac = hmac.New(md5.New, []byte(key))
207 - case "sha256":
208 - mac = hmac.New(sha256.New, []byte(key))
209 - default:
210 - panic(vm.NewGoError(fmt.Errorf("unsupported hmac algorithm: %s", algorithm)))
211 - }
212 -
213 - mac.Write([]byte(data))
214 - result := hex.EncodeToString(mac.Sum(nil))
215 - return vm.ToValue(result)
216 - })
217 -
218 - // sign - RSA signature function
219 - vm.Set("sign", func(call goja.FunctionCall) goja.Value {
220 - if len(call.Arguments) < 3 {
221 - panic(vm.NewGoError(fmt.Errorf("sign requires 3 arguments: algorithm, privateKey, data")))
222 - }
223 -
224 - algorithm := call.Arguments[0].String()
225 - pemKey := call.Arguments[1].String()
226 - dataArg := call.Arguments[2]
227 -
228 - // Extract data (support string and Uint8Array)
229 - var data []byte
230 - if obj := dataArg.ToObject(vm); obj != nil {
231 - if arr := obj.Get("constructor"); arr != nil && arr.String() == "function Uint8Array() { [native code] }" {
232 - // It's a typed array, extract bytes
233 - if length := obj.Get("length"); length != nil {
234 - n := int(length.ToInteger())
235 - data = make([]byte, n)
236 - for i := 0; i < n; i++ {
237 - data[i] = byte(obj.Get(fmt.Sprintf("%d", i)).ToInteger())
238 - }
239 - }
240 - }
241 - }
242 - if data == nil {
243 - data = []byte(dataArg.String())
244 - }
245 -
246 - if algorithm != "sha256" {
247 - panic(vm.NewGoError(fmt.Errorf("unsupported signature algorithm: %s", algorithm)))
248 - }
249 -
250 - // Normalize PEM key (handle single-line keys)
251 - pemKey = normalizePEMKey(pemKey)
252 -
253 - // Parse PEM key
254 - block, _ := pem.Decode([]byte(pemKey))
255 - if block == nil {
256 - panic(vm.NewGoError(fmt.Errorf("failed to parse PEM block")))
257 - }
258 -
259 - var privateKey *rsa.PrivateKey
260 - var err error
261 -
262 - // Try PKCS#1 format first
263 - privateKey, err = x509.ParsePKCS1PrivateKey(block.Bytes)
264 - if err != nil {
265 - // Try PKCS#8 format
266 - key, err2 := x509.ParsePKCS8PrivateKey(block.Bytes)
267 - if err2 != nil {
268 - panic(vm.NewGoError(fmt.Errorf("failed to parse private key: %v", err)))
269 - }
270 - var ok bool
271 - privateKey, ok = key.(*rsa.PrivateKey)
272 - if !ok {
273 - panic(vm.NewGoError(fmt.Errorf("not an RSA private key")))
274 - }
275 - }
276 -
277 - // Validate RSA key size (minimum 2048 bits for security)
278 - keyBits := privateKey.N.BitLen()
279 - if keyBits < jsMinRSAKeyBits {
280 - panic(vm.NewGoError(fmt.Errorf("RSA key too small: %d bits (minimum %d bits required)", keyBits, jsMinRSAKeyBits)))
281 - }
282 -
283 - // Hash the data
284 - hashed := sha256.Sum256(data)
285 -
286 - // Sign the hash
287 - signature, err := rsa.SignPKCS1v15(rand.Reader, privateKey, crypto.SHA256, hashed[:])
288 - if err != nil {
289 - panic(vm.NewGoError(fmt.Errorf("signing failed: %v", err)))
290 - }
291 -
292 - // Return hex-encoded signature
293 - result := hex.EncodeToString(signature)
294 - return vm.ToValue(result)
295 - })
296 -
297 - // HttpRequest - Minimal stub for Zabbix HTTP request object
298 - // Only needed for test compatibility - actual HTTP functionality not implemented
299 - vm.RunString(`
300 - function HttpRequest() {
301 - // Minimal constructor - allows property assignment
302 - // Actual HTTP methods would be implemented here in full Zabbix
303 - }
304 - `)
305 -}
306 -
307 -// normalizePEMKey normalizes a PEM key by adding proper newlines
308 -func normalizePEMKey(pemStr string) string {
309 - // If already has newlines after BEGIN, return as is
310 - if len(pemStr) > pemMinHeaderLen && pemStr[pemNewlineCheckPos] == '\n' {
311 - return pemStr
312 - }
313 -
314 - // Find BEGIN and END markers
315 - beginIdx := 0
316 - endIdx := len(pemStr)
317 -
318 - for i := 0; i < len(pemStr)-pemBeginMarkerLen; i++ {
319 - if pemStr[i:i+pemBeginMarkerLen] == pemBeginMarker {
320 - // Find end of BEGIN line
321 - for j := i; j < len(pemStr)-pemDelimiterLen; j++ {
322 - if pemStr[j:j+pemDelimiterLen] == pemDelimiter && j > i+pemBeginMarkerLen {
323 - beginIdx = j + pemDelimiterLen
324 - break
325 - }
326 - }
327 - }
328 - if pemStr[i:i+pemEndMarkerLen] == pemEndMarker {
329 - endIdx = i
330 - break
331 - }
332 - }
333 -
334 - if beginIdx == 0 || endIdx == len(pemStr) {
335 - return pemStr // No markers found, return as is
336 - }
337 -
338 - // Extract header, body, footer
339 - header := pemStr[:beginIdx]
340 - body := pemStr[beginIdx:endIdx]
341 - footer := pemStr[endIdx:]
342 -
343 - // Remove any existing whitespace from body
344 - var bodyClean strings.Builder
345 - bodyClean.Grow(len(body))
346 - for _, ch := range body {
347 - if ch != '\n' && ch != '\r' && ch != ' ' && ch != '\t' {
348 - bodyClean.WriteRune(ch)
349 - }
350 - }
351 -
352 - // Split body into pemLineWidth-char lines (RFC 7468 standard)
353 - cleanStr := bodyClean.String()
354 - var lines []string
355 - for i := 0; i < len(cleanStr); i += pemLineWidth {
356 - end := i + pemLineWidth
357 - if end > len(cleanStr) {
358 - end = len(cleanStr)
359 - }
360 - lines = append(lines, cleanStr[i:end])
361 - }
362 -
363 - // Reconstruct PEM
364 - var result strings.Builder
365 - result.Grow(len(header) + len(cleanStr) + len(footer) + len(lines)*2)
366 - result.WriteString(header)
367 - result.WriteByte('\n')
368 - for _, line := range lines {
369 - result.WriteString(line)
370 - result.WriteByte('\n')
371 - }
372 - result.WriteString(footer)
373 -
374 - return result.String()
375 -}
376 -
377 -// javascriptExecute executes JavaScript preprocessing step with program caching.
378 -//
379 -// Performance optimizations:
380 -// - Caches compiled programs in programCache (avoids recompilation)
381 -// - Built-in functions configured once per VM (not per execution)
382 -//
383 -// Security note: VMs are NOT pooled to prevent prototype pollution attacks.
384 -// Each execution gets a fresh VM to ensure complete isolation.
385 -func javascriptExecute(value Value, paramStr string, limits JSLimits) (Value, error) {
386 - if paramStr == "" {
387 - return Value{}, fmt.Errorf("javascript code required")
388 - }
389 -
390 - // Create fresh VM for each execution (prevents prototype pollution)
391 - // VM pooling was removed for security - prototype modifications persist across reuse
392 - vm := createConfiguredVM()
393 -
394 - // No defer to return VM to pool - VM is discarded after use (GC will collect it)
395 - // This ensures no state leakage between executions
396 -
397 - // Set up execution timeout using a goroutine that will interrupt the VM
398 - // The interrupt will cause the current execution to fail; since we create a fresh
399 - // VM per execution (not pooled), the interrupted VM is simply discarded afterward
400 - timeout := limits.Timeout
401 - if timeout == 0 {
402 - timeout = 10 * time.Second // Default to Zabbix's 10s timeout
403 - }
404 - done := make(chan struct{})
405 - go func() {
406 - select {
407 - case <-time.After(timeout):
408 - vm.Interrupt("JavaScript execution timeout")
409 - case <-done:
410 - return
411 - }
412 - }()
413 - defer close(done)
414 -
415 - // Panic recovery for JavaScript errors
416 - defer func() {
417 - if r := recover(); r != nil {
418 - // Panics from goja are already handled, but catch any other panics
419 - }
420 - }()
421 -
422 - // Create the main function code
423 - code := `
424 -(function(value) {
425 - ` + paramStr + `
426 -})
427 -`
428 -
429 - // Try to get compiled program from cache
430 - var program *goja.Program
431 - if cached, ok := programCache.Get(code); ok {
432 - program = cached
433 - } else {
434 - // Compile the program (first time for this script)
435 - var err error
436 - program, err = goja.Compile("", code, false)
437 - if err != nil {
438 - return Value{}, fmt.Errorf("javascript compilation error: %w", err)
439 - }
440 - // Cache the compiled program (bounded LRU, max 1000 entries)
441 - programCache.Put(code, program)
442 - }
443 -
444 - // Run the compiled program
445 - result, err := vm.RunProgram(program)
446 - if err != nil {
447 - return Value{}, fmt.Errorf("javascript execution error: %w", err)
448 - }
449 -
450 - // Call the function with value as parameter
451 - fn := result
452 - var fnVal goja.Callable
453 - ok := false
454 - if fnVal, ok = goja.AssertFunction(fn); !ok {
455 - return Value{}, fmt.Errorf("javascript code must return a function")
456 - }
457 -
458 - // Execute the function
459 - res, err := fnVal(goja.Undefined(), vm.ToValue(value.Data))
460 - if err != nil {
461 - return Value{}, fmt.Errorf("javascript function error: %w", err)
462 - }
463 -
464 - // Get the result as string
465 - resultStr := res.String()
466 - return Value{Data: resultStr, Type: ValueTypeStr}, nil
467 -}
src/go/plugin/scripts.d/pkg/zabbixpreproc/jsonpath_step.go deleted
-203
@@ -1,203 +0,0 @@
1 -package zabbixpreproc
2 -
3 -import (
4 - "encoding/json"
5 - "fmt"
6 - "strconv"
7 - "strings"
8 -
9 - "github.com/theory/jsonpath"
10 -)
11 -
12 -// JSON Unicode escape sequence constants
13 -const (
14 - unicodeEscapeLen = 6 // Length of \uXXXX sequence
15 - surrogatePairLen = 12 // Length of surrogate pair \uXXXX\uYYYY
16 - unicodeHexDigits = 4 // Number of hex digits in \uXXXX
17 - unicodeHexBase = 16 // Base for parsing hex codes
18 - unicodeHexBitSize = 16 // Bit size for ParseUint
19 - unicodeSurrogateStart = 0xD800 // Start of surrogate range
20 - unicodeSurrogateEnd = 0xDFFF // End of surrogate range
21 - unicodeHighSurroStart = 0xD800 // Start of high surrogate range
22 - unicodeHighSurroEnd = 0xDBFF // End of high surrogate range
23 - unicodeLowSurroStart = 0xDC00 // Start of low surrogate range
24 - unicodeLowSurroEnd = 0xDFFF // End of low surrogate range
25 -)
26 -
27 -// validateSurrogatePairs checks for unpaired UTF-16 surrogates in JSON string.
28 -// Returns error if any unpaired surrogates are found.
29 -func validateSurrogatePairs(jsonStr string) error {
30 - i := 0
31 - for i < len(jsonStr) {
32 - // Find next \u escape sequence
33 - if jsonStr[i] == '\\' && i+unicodeEscapeLen-1 < len(jsonStr) && jsonStr[i+1] == 'u' {
34 - // Parse the 4-digit hex code
35 - hexCode := jsonStr[i+2 : i+2+unicodeHexDigits]
36 - code, err := strconv.ParseUint(hexCode, unicodeHexBase, unicodeHexBitSize)
37 - if err != nil {
38 - // Invalid hex code, but let json.Unmarshal handle it
39 - i++
40 - continue
41 - }
42 -
43 - // Check if this is a surrogate
44 - if code >= unicodeSurrogateStart && code <= unicodeSurrogateEnd {
45 - // This is a surrogate
46 - if code >= unicodeHighSurroStart && code <= unicodeHighSurroEnd {
47 - // High surrogate - must be followed by low surrogate
48 - if i+surrogatePairLen < len(jsonStr) && jsonStr[i+6:i+8] == "\\u" {
49 - nextHex := jsonStr[i+8 : i+8+unicodeHexDigits]
50 - nextCode, err := strconv.ParseUint(nextHex, unicodeHexBase, unicodeHexBitSize)
51 - if err == nil && nextCode >= unicodeLowSurroStart && nextCode <= unicodeLowSurroEnd {
52 - // Valid surrogate pair, skip both
53 - i += surrogatePairLen
54 - continue
55 - }
56 - }
57 - // High surrogate without low surrogate = invalid
58 - return fmt.Errorf("invalid JSON: unpaired high surrogate")
59 - } else {
60 - // Low surrogate without preceding high surrogate = invalid
61 - return fmt.Errorf("invalid JSON: unpaired low surrogate")
62 - }
63 - }
64 -
65 - i += unicodeEscapeLen // Skip this \uXXXX sequence
66 - } else {
67 - i++
68 - }
69 - }
70 - return nil
71 -}
72 -
73 -// jsonpathExtract extracts values from JSON using JSONPath expression.
74 -// jsonPathMulti extracts values using JSONPath and returns multiple metrics for arrays
75 -func jsonPathMulti(value Value, paramStr string) (Result, error) {
76 - expr := strings.TrimSpace(paramStr)
77 - if expr == "" {
78 - err := fmt.Errorf("jsonpath expression is required")
79 - return Result{Error: err}, err
80 - }
81 -
82 - // Validate surrogate pairs before parsing
83 - if err := validateSurrogatePairs(value.Data); err != nil {
84 - return Result{Error: err}, err
85 - }
86 -
87 - // Parse JSON
88 - var data interface{}
89 - err := json.Unmarshal([]byte(value.Data), &data)
90 - if err != nil {
91 - err = fmt.Errorf("invalid JSON: %w", err)
92 - return Result{Error: err}, err
93 - }
94 -
95 - // Compile and execute JSONPath expression
96 - jp, err := jsonpath.Parse(expr)
97 - if err != nil {
98 - err = fmt.Errorf("invalid jsonpath expression: %w", err)
99 - return Result{Error: err}, err
100 - }
101 -
102 - results := jp.Select(data)
103 - if len(results) == 0 {
104 - err = fmt.Errorf("jsonpath expression did not match any values")
105 - return Result{Error: err}, err
106 - }
107 -
108 - // If single result and not an array, return single metric
109 - if len(results) == 1 {
110 - // Check if the single result is an array
111 - if arr, ok := results[0].([]interface{}); ok {
112 - // It's an array - return each element as separate metric
113 - metrics := make([]Metric, 0, len(arr))
114 - for i, item := range arr {
115 - metrics = append(metrics, Metric{
116 - Name: "item",
117 - Value: marshalJSONValue(item),
118 - Type: ValueTypeStr,
119 - Labels: map[string]string{"index": fmt.Sprintf("%d", i)},
120 - })
121 - }
122 - return Result{Metrics: metrics}, nil
123 - }
124 - // Single non-array result - return as single metric
125 - return Result{
126 - Metrics: []Metric{{
127 - Name: "",
128 - Value: marshalJSONValue(results[0]),
129 - Type: ValueTypeStr,
130 - }},
131 - }, nil
132 - }
133 -
134 - // Multiple results from JSONPath - return each as separate metric
135 - metrics := make([]Metric, 0, len(results))
136 - for i, result := range results {
137 - metrics = append(metrics, Metric{
138 - Name: "item",
139 - Value: marshalJSONValue(result),
140 - Type: ValueTypeStr,
141 - Labels: map[string]string{"index": fmt.Sprintf("%d", i)},
142 - })
143 - }
144 -
145 - return Result{Metrics: metrics}, nil
146 -}
147 -
148 -// marshalJSONValue converts a value to JSON string, handling strings specially
149 -func marshalJSONValue(v interface{}) string {
150 - // If it's already a string, return it directly without JSON encoding
151 - if s, ok := v.(string); ok {
152 - return s
153 - }
154 - // For non-string values, encode as JSON
155 - b, _ := json.Marshal(v)
156 - return string(b)
157 -}
158 -
159 -func jsonpathExtract(value Value, paramStr string) (Value, error) {
160 - expr := strings.TrimSpace(paramStr)
161 - if expr == "" {
162 - return Value{}, fmt.Errorf("jsonpath expression is required")
163 - }
164 -
165 - // Validate surrogate pairs before parsing
166 - if err := validateSurrogatePairs(value.Data); err != nil {
167 - return Value{}, err
168 - }
169 -
170 - // Parse JSON
171 - var data interface{}
172 - err := json.Unmarshal([]byte(value.Data), &data)
173 - if err != nil {
174 - return Value{}, fmt.Errorf("invalid JSON: %w", err)
175 - }
176 -
177 - // Compile and execute JSONPath expression
178 - jp, err := jsonpath.Parse(expr)
179 - if err != nil {
180 - return Value{}, fmt.Errorf("invalid jsonpath expression: %w", err)
181 - }
182 -
183 - results := jp.Select(data)
184 - if len(results) == 0 {
185 - return Value{}, fmt.Errorf("jsonpath expression did not match any values")
186 - }
187 -
188 - // Return the first result
189 - result := results[0]
190 -
191 - // If result is a string, return it directly without JSON encoding
192 - if s, ok := result.(string); ok {
193 - return Value{Data: s, Type: ValueTypeStr}, nil
194 - }
195 -
196 - // For non-string results, encode as JSON
197 - resultBytes, err := json.Marshal(result)
198 - if err != nil {
199 - return Value{}, fmt.Errorf("failed to marshal result: %w", err)
200 - }
201 -
202 - return Value{Data: string(resultBytes), Type: ValueTypeStr}, nil
203 -}
src/go/plugin/scripts.d/pkg/zabbixpreproc/logger.go deleted
-49
@@ -1,49 +0,0 @@
1 -package zabbixpreproc
2 -
3 -// Logger is a minimal logging interface for preprocessing operations.
4 -// This allows users to plug in any logging library (slog, logrus, zap, etc.)
5 -// without forcing dependencies on the library.
6 -//
7 -// By default, the library uses NoopLogger (zero overhead).
8 -// Users can provide their own logger implementation via SetLogger().
9 -//
10 -// Example with slog:
11 -//
12 -// logger := slog.New(slog.NewJSONHandler(os.Stderr, nil))
13 -// preprocessor.SetLogger(NewSlogAdapter(logger))
14 -type Logger interface {
15 - // Debug logs debug-level messages with optional structured key-value pairs.
16 - // Used for: skipped SNMP lines, empty values, parsing decisions
17 - Debug(msg string, keysAndValues ...interface{})
18 -
19 - // Info logs informational messages with optional structured key-value pairs.
20 - // Used for: preprocessing statistics, cache hits
21 - Info(msg string, keysAndValues ...interface{})
22 -
23 - // Warn logs warning messages with optional structured key-value pairs.
24 - // Used for: deprecated features, performance concerns
25 - Warn(msg string, keysAndValues ...interface{})
26 -
27 - // Error logs error messages with optional structured key-value pairs.
28 - // Note: This is for logging only. Errors are still returned to caller.
29 - Error(msg string, keysAndValues ...interface{})
30 -}
31 -
32 -// NoopLogger is a no-operation logger that discards all log messages.
33 -// This is the default logger (zero performance overhead).
34 -type NoopLogger struct{}
35 -
36 -// Debug implements Logger.Debug (no-op)
37 -func (NoopLogger) Debug(msg string, keysAndValues ...interface{}) {}
38 -
39 -// Info implements Logger.Info (no-op)
40 -func (NoopLogger) Info(msg string, keysAndValues ...interface{}) {}
41 -
42 -// Warn implements Logger.Warn (no-op)
43 -func (NoopLogger) Warn(msg string, keysAndValues ...interface{}) {}
44 -
45 -// Error implements Logger.Error (no-op)
46 -func (NoopLogger) Error(msg string, keysAndValues ...interface{}) {}
47 -
48 -// Ensure NoopLogger implements Logger
49 -var _ Logger = (*NoopLogger)(nil)
src/go/plugin/scripts.d/pkg/zabbixpreproc/multi_metric_test.go deleted
-858
@@ -1,858 +0,0 @@
1 -package zabbixpreproc
2 -
3 -import (
4 - "fmt"
5 - "testing"
6 - "time"
7 -)
8 -
9 -// TestPrometheusMultiMetric tests that prometheusToJSONMulti() returns multiple metrics
10 -func TestPrometheusMultiMetric(t *testing.T) {
11 - input := `# HELP http_requests_total Total HTTP requests
12 -# TYPE http_requests_total counter
13 -http_requests_total{method="GET"} 100
14 -http_requests_total{method="POST"} 50
15 -memory_usage_bytes 1024
16 -`
17 -
18 - result, err := prometheusToJSONMulti(Value{Data: input, Type: ValueTypeStr}, "")
19 - if err != nil {
20 - t.Fatalf("prometheusToJSONMulti() error: %v", err)
21 - }
22 -
23 - // Should return 3 metrics
24 - if len(result.Metrics) != 3 {
25 - t.Fatalf("Expected 3 metrics, got %d", len(result.Metrics))
26 - }
27 -
28 - // Verify first metric
29 - m0 := result.Metrics[0]
30 - if m0.Name != "http_requests_total" {
31 - t.Errorf("Metric 0: expected name 'http_requests_total', got %q", m0.Name)
32 - }
33 - if m0.Value != "100" {
34 - t.Errorf("Metric 0: expected value '100', got %q", m0.Value)
35 - }
36 - if m0.Type != ValueTypeStr {
37 - t.Errorf("Metric 0: expected type ValueTypeStr, got %v", m0.Type)
38 - }
39 - if m0.Labels == nil {
40 - t.Fatal("Metric 0: labels should not be nil")
41 - }
42 - if m0.Labels["method"] != "GET" {
43 - t.Errorf("Metric 0: expected label method=GET, got %q", m0.Labels["method"])
44 - }
45 -
46 - // Verify second metric
47 - m1 := result.Metrics[1]
48 - if m1.Name != "http_requests_total" {
49 - t.Errorf("Metric 1: expected name 'http_requests_total', got %q", m1.Name)
50 - }
51 - if m1.Value != "50" {
52 - t.Errorf("Metric 1: expected value '50', got %q", m1.Value)
53 - }
54 - if m1.Labels["method"] != "POST" {
55 - t.Errorf("Metric 1: expected label method=POST, got %q", m1.Labels["method"])
56 - }
57 -
58 - // Verify third metric (no labels)
59 - m2 := result.Metrics[2]
60 - if m2.Name != "memory_usage_bytes" {
61 - t.Errorf("Metric 2: expected name 'memory_usage_bytes', got %q", m2.Name)
62 - }
63 - if m2.Value != "1024" {
64 - t.Errorf("Metric 2: expected value '1024', got %q", m2.Value)
65 - }
66 - if len(m2.Labels) != 0 {
67 - t.Errorf("Metric 2: expected no labels, got %v", m2.Labels)
68 - }
69 -}
70 -
71 -// TestPrometheusMultiMetric_EmptyInput tests empty input handling
72 -func TestPrometheusMultiMetric_EmptyInput(t *testing.T) {
73 - input := ""
74 -
75 - result, err := prometheusToJSONMulti(Value{Data: input, Type: ValueTypeStr}, "")
76 - if err != nil {
77 - t.Fatalf("prometheusToJSONMulti() error on empty input: %v", err)
78 - }
79 -
80 - // Empty input should return zero metrics
81 - if len(result.Metrics) != 0 {
82 - t.Errorf("Expected 0 metrics for empty input, got %d", len(result.Metrics))
83 - }
84 -}
85 -
86 -// TestPrometheusMultiMetric_InvalidInput tests error handling
87 -func TestPrometheusMultiMetric_InvalidInput(t *testing.T) {
88 - input := "invalid prometheus data here"
89 -
90 - _, err := prometheusToJSONMulti(Value{Data: input, Type: ValueTypeStr}, "")
91 - if err == nil {
92 - t.Fatal("Expected error for invalid Prometheus data, got nil")
93 - }
94 -}
95 -
96 -// TestPrometheusMultiMetric_SingleMetric tests single metric returns single-element array
97 -func TestPrometheusMultiMetric_SingleMetric(t *testing.T) {
98 - input := "cpu_usage 42.5"
99 -
100 - result, err := prometheusToJSONMulti(Value{Data: input, Type: ValueTypeStr}, "")
101 - if err != nil {
102 - t.Fatalf("prometheusToJSONMulti() error: %v", err)
103 - }
104 -
105 - if len(result.Metrics) != 1 {
106 - t.Fatalf("Expected 1 metric, got %d", len(result.Metrics))
107 - }
108 -
109 - m := result.Metrics[0]
110 - if m.Name != "cpu_usage" {
111 - t.Errorf("Expected name 'cpu_usage', got %q", m.Name)
112 - }
113 - if m.Value != "42.5" {
114 - t.Errorf("Expected value '42.5', got %q", m.Value)
115 - }
116 -}
117 -
118 -// TestPrometheusMultiMetric_ViaPreprocessor tests multi-metric through Preprocessor.Execute()
119 -func TestPrometheusMultiMetric_ViaPreprocessor(t *testing.T) {
120 - input := `http_requests_total{method="GET"} 100
121 -http_requests_total{method="POST"} 50
122 -memory_usage_bytes 1024
123 -`
124 -
125 - p := NewPreprocessor("test-shard")
126 - result, err := p.Execute("item1",
127 - Value{Data: input, Type: ValueTypeStr, Timestamp: time.Now()},
128 - Step{Type: StepTypePrometheusToJSONMulti, Params: ""})
129 -
130 - if err != nil {
131 - t.Fatalf("Execute() error: %v", err)
132 - }
133 -
134 - // Should return 3 metrics
135 - if len(result.Metrics) != 3 {
136 - t.Fatalf("Expected 3 metrics, got %d", len(result.Metrics))
137 - }
138 -
139 - // Verify metric names and values
140 - expected := []struct {
141 - name string
142 - value string
143 - }{
144 - {"http_requests_total", "100"},
145 - {"http_requests_total", "50"},
146 - {"memory_usage_bytes", "1024"},
147 - }
148 -
149 - for i, exp := range expected {
150 - if result.Metrics[i].Name != exp.name {
151 - t.Errorf("Metric %d: expected name %q, got %q", i, exp.name, result.Metrics[i].Name)
152 - }
153 - if result.Metrics[i].Value != exp.value {
154 - t.Errorf("Metric %d: expected value %q, got %q", i, exp.value, result.Metrics[i].Value)
155 - }
156 - }
157 -}
158 -
159 -// TestZabbixCompatibility_PrometheusToJSON tests that old step type still works
160 -func TestZabbixCompatibility_PrometheusToJSON(t *testing.T) {
161 - input := `http_requests_total{method="GET"} 100
162 -http_requests_total{method="POST"} 50
163 -`
164 -
165 - p := NewPreprocessor("test-shard")
166 - result, err := p.Execute("item1",
167 - Value{Data: input, Type: ValueTypeStr, Timestamp: time.Now()},
168 - Step{Type: StepTypePrometheusToJSON, Params: ""}) // OLD step type
169 -
170 - if err != nil {
171 - t.Fatalf("Execute() error: %v", err)
172 - }
173 -
174 - // OLD behavior: Should return SINGLE metric with JSON array string
175 - if len(result.Metrics) != 1 {
176 - t.Fatalf("Expected 1 metric (Zabbix compatibility), got %d", len(result.Metrics))
177 - }
178 -
179 - // Value should be a JSON array string
180 - value := result.Metrics[0].Value
181 - if value[0] != '[' || value[len(value)-1] != ']' {
182 - t.Errorf("Expected JSON array string, got: %s", value)
183 - }
184 -
185 - // Verify it contains both metrics in the JSON array
186 - if !contains(value, `"name":"http_requests_total"`) {
187 - t.Errorf("JSON array missing http_requests_total metric")
188 - }
189 - if !contains(value, `"method":"GET"`) || !contains(value, `"method":"POST"`) {
190 - t.Errorf("JSON array missing expected labels")
191 - }
192 -}
193 -
194 -func contains(s, substr string) bool {
195 - return len(s) >= len(substr) && (s == substr || len(s) > len(substr) && containsHelper(s, substr))
196 -}
197 -
198 -func containsHelper(s, substr string) bool {
199 - for i := 0; i <= len(s)-len(substr); i++ {
200 - if s[i:i+len(substr)] == substr {
201 - return true
202 - }
203 - }
204 - return false
205 -}
206 -
207 -// ============================================================================
208 -// JSONPath Multi-Metric Tests
209 -// ============================================================================
210 -
211 -// TestJSONPathMulti_Array tests JSONPath with array result
212 -func TestJSONPathMulti_Array(t *testing.T) {
213 - input := `{"items": [{"name": "a", "value": 10}, {"name": "b", "value": 20}]}`
214 - jsonpath := "$.items[*]"
215 -
216 - result, err := jsonPathMulti(Value{Data: input, Type: ValueTypeStr}, jsonpath)
217 - if err != nil {
218 - t.Fatalf("jsonPathMulti() error: %v", err)
219 - }
220 -
221 - // Should return 2 metrics (one per array element)
222 - if len(result.Metrics) != 2 {
223 - t.Fatalf("Expected 2 metrics, got %d", len(result.Metrics))
224 - }
225 -
226 - // Verify first metric
227 - m0 := result.Metrics[0]
228 - if m0.Name != "item" {
229 - t.Errorf("Metric 0: expected name 'item', got %q", m0.Name)
230 - }
231 - if m0.Labels["index"] != "0" {
232 - t.Errorf("Metric 0: expected index label '0', got %q", m0.Labels["index"])
233 - }
234 - if !contains(m0.Value, `"name":"a"`) {
235 - t.Errorf("Metric 0: expected to contain name:a, got %q", m0.Value)
236 - }
237 -
238 - // Verify second metric
239 - m1 := result.Metrics[1]
240 - if m1.Labels["index"] != "1" {
241 - t.Errorf("Metric 1: expected index label '1', got %q", m1.Labels["index"])
242 - }
243 -}
244 -
245 -// TestJSONPathMulti_SingleValue tests JSONPath with single value result
246 -func TestJSONPathMulti_SingleValue(t *testing.T) {
247 - input := `{"name": "test", "value": 42}`
248 - jsonpath := "$.value"
249 -
250 - result, err := jsonPathMulti(Value{Data: input, Type: ValueTypeStr}, jsonpath)
251 - if err != nil {
252 - t.Fatalf("jsonPathMulti() error: %v", err)
253 - }
254 -
255 - // Should return 1 metric
256 - if len(result.Metrics) != 1 {
257 - t.Fatalf("Expected 1 metric, got %d", len(result.Metrics))
258 - }
259 -
260 - m := result.Metrics[0]
261 - if m.Value != "42" {
262 - t.Errorf("Expected value '42', got %q", m.Value)
263 - }
264 - if len(m.Labels) != 0 {
265 - t.Errorf("Single value should have no labels, got %v", m.Labels)
266 - }
267 -}
268 -
269 -// TestJSONPathMulti_StringValue tests JSONPath extracting string
270 -func TestJSONPathMulti_StringValue(t *testing.T) {
271 - input := `{"message": "hello world"}`
272 - jsonpath := "$.message"
273 -
274 - result, err := jsonPathMulti(Value{Data: input, Type: ValueTypeStr}, jsonpath)
275 - if err != nil {
276 - t.Fatalf("jsonPathMulti() error: %v", err)
277 - }
278 -
279 - if len(result.Metrics) != 1 {
280 - t.Fatalf("Expected 1 metric, got %d", len(result.Metrics))
281 - }
282 -
283 - // String should be returned directly without JSON encoding
284 - m := result.Metrics[0]
285 - if m.Value != "hello world" {
286 - t.Errorf("Expected value 'hello world', got %q", m.Value)
287 - }
288 -}
289 -
290 -// TestJSONPathMulti_ArrayOfStrings tests array of primitive values
291 -func TestJSONPathMulti_ArrayOfStrings(t *testing.T) {
292 - input := `{"tags": ["red", "green", "blue"]}`
293 - jsonpath := "$.tags[*]"
294 -
295 - result, err := jsonPathMulti(Value{Data: input, Type: ValueTypeStr}, jsonpath)
296 - if err != nil {
297 - t.Fatalf("jsonPathMulti() error: %v", err)
298 - }
299 -
300 - // Should return 3 metrics
301 - if len(result.Metrics) != 3 {
302 - t.Fatalf("Expected 3 metrics, got %d", len(result.Metrics))
303 - }
304 -
305 - // Verify values
306 - expected := []string{"red", "green", "blue"}
307 - for i, exp := range expected {
308 - if result.Metrics[i].Value != exp {
309 - t.Errorf("Metric %d: expected value %q, got %q", i, exp, result.Metrics[i].Value)
310 - }
311 - if result.Metrics[i].Labels["index"] != fmt.Sprintf("%d", i) {
312 - t.Errorf("Metric %d: expected index %d, got %q", i, i, result.Metrics[i].Labels["index"])
313 - }
314 - }
315 -}
316 -
317 -// TestJSONPathMulti_ViaPreprocessor tests full integration
318 -func TestJSONPathMulti_ViaPreprocessor(t *testing.T) {
319 - input := `{"servers": [{"name": "web1", "cpu": 45}, {"name": "web2", "cpu": 60}]}`
320 - jsonpath := "$.servers[*]"
321 -
322 - p := NewPreprocessor("test-shard")
323 - result, err := p.Execute("item1",
324 - Value{Data: input, Type: ValueTypeStr, Timestamp: time.Now()},
325 - Step{Type: StepTypeJSONPathMulti, Params: jsonpath})
326 -
327 - if err != nil {
328 - t.Fatalf("Execute() error: %v", err)
329 - }
330 -
331 - if len(result.Metrics) != 2 {
332 - t.Fatalf("Expected 2 metrics, got %d", len(result.Metrics))
333 - }
334 -
335 - // Verify both metrics have index labels
336 - for i := 0; i < 2; i++ {
337 - if result.Metrics[i].Labels["index"] != fmt.Sprintf("%d", i) {
338 - t.Errorf("Metric %d missing correct index label", i)
339 - }
340 - }
341 -}
342 -
343 -// TestJSONPathMulti_EmptyResult tests error on no matches
344 -func TestJSONPathMulti_EmptyResult(t *testing.T) {
345 - input := `{"name": "test"}`
346 - jsonpath := "$.nonexistent"
347 -
348 - _, err := jsonPathMulti(Value{Data: input, Type: ValueTypeStr}, jsonpath)
349 - if err == nil {
350 - t.Fatal("Expected error for no matches, got nil")
351 - }
352 -}
353 -
354 -// TestZabbixCompatibility_JSONPath tests old step type still works
355 -func TestZabbixCompatibility_JSONPath(t *testing.T) {
356 - input := `{"items": [1, 2, 3]}`
357 - jsonpath := "$.items"
358 -
359 - p := NewPreprocessor("test-shard")
360 - result, err := p.Execute("item1",
361 - Value{Data: input, Type: ValueTypeStr, Timestamp: time.Now()},
362 - Step{Type: StepTypeJSONPath, Params: jsonpath}) // OLD step type
363 -
364 - if err != nil {
365 - t.Fatalf("Execute() error: %v", err)
366 - }
367 -
368 - // OLD behavior: Should return SINGLE metric with JSON array string
369 - if len(result.Metrics) != 1 {
370 - t.Fatalf("Expected 1 metric (Zabbix compatibility), got %d", len(result.Metrics))
371 - }
372 -
373 - // Value should be a JSON array
374 - value := result.Metrics[0].Value
375 - if value[0] != '[' {
376 - t.Errorf("Expected JSON array, got: %s", value)
377 - }
378 -}
379 -
380 -// ============================================================================
381 -// SNMP Walk to JSON Multi-Metric Tests
382 -// ============================================================================
383 -
384 -// TestSNMPWalkMulti_BasicDiscovery tests basic SNMP discovery with multiple items
385 -func TestSNMPWalkMulti_BasicDiscovery(t *testing.T) {
386 - params := `{#IFNAME}
387 -.1.3.6.1.2.1.31.1.1.1.1
388 -0
389 -{#IFDESCR}
390 -.1.3.6.1.2.1.2.2.1.2
391 -0`
392 -
393 - input := `.1.3.6.1.2.1.31.1.1.1.1.1 = STRING: "eth0"
394 -.1.3.6.1.2.1.31.1.1.1.1.2 = STRING: "eth1"
395 -.1.3.6.1.2.1.2.2.1.2.1 = STRING: "Ethernet Interface 0"
396 -.1.3.6.1.2.1.2.2.1.2.2 = STRING: "Ethernet Interface 1"
397 -`
398 -
399 - result, err := snmpWalkToJSONMulti(Value{Data: input, Type: ValueTypeStr}, params, NoopLogger{})
400 - if err != nil {
401 - t.Fatalf("snmpWalkToJSONMulti() error: %v", err)
402 - }
403 -
404 - // Should return 2 metrics (one per index: 2, 1 in descending order)
405 - if len(result.Metrics) != 2 {
406 - t.Fatalf("Expected 2 metrics, got %d", len(result.Metrics))
407 - }
408 -
409 - // Verify first metric (index 2 - descending order)
410 - m0 := result.Metrics[0]
411 - if m0.Name != "snmp_discovery" {
412 - t.Errorf("Metric 0: expected name 'snmp_discovery', got %q", m0.Name)
413 - }
414 - if m0.Labels["index"] != "2" {
415 - t.Errorf("Metric 0: expected index label '2', got %q", m0.Labels["index"])
416 - }
417 - if !contains(m0.Value, `"{#SNMPINDEX}":"2"`) {
418 - t.Errorf("Metric 0: expected SNMPINDEX=2, got %q", m0.Value)
419 - }
420 - if !contains(m0.Value, `"{#IFNAME}":"eth1"`) {
421 - t.Errorf("Metric 0: expected IFNAME=eth1, got %q", m0.Value)
422 - }
423 - if !contains(m0.Value, `"{#IFDESCR}":"Ethernet Interface 1"`) {
424 - t.Errorf("Metric 0: expected IFDESCR, got %q", m0.Value)
425 - }
426 -
427 - // Verify second metric (index 1)
428 - m1 := result.Metrics[1]
429 - if m1.Labels["index"] != "1" {
430 - t.Errorf("Metric 1: expected index label '1', got %q", m1.Labels["index"])
431 - }
432 - if !contains(m1.Value, `"{#IFNAME}":"eth0"`) {
433 - t.Errorf("Metric 1: expected IFNAME=eth0, got %q", m1.Value)
434 - }
435 -}
436 -
437 -// TestSNMPWalkMulti_EmptyInput tests empty input handling
438 -func TestSNMPWalkMulti_EmptyInput(t *testing.T) {
439 - params := `{#IFNAME}
440 -.1.3.6.1.2.1.31.1.1.1.1
441 -0`
442 -
443 - result, err := snmpWalkToJSONMulti(Value{Data: "", Type: ValueTypeStr}, params, NoopLogger{})
444 - if err != nil {
445 - t.Fatalf("snmpWalkToJSONMulti() error on empty input: %v", err)
446 - }
447 -
448 - // Empty input should return zero metrics
449 - if len(result.Metrics) != 0 {
450 - t.Errorf("Expected 0 metrics for empty input, got %d", len(result.Metrics))
451 - }
452 -}
453 -
454 -// TestSNMPWalkMulti_NoMacros tests behavior with no macros defined
455 -func TestSNMPWalkMulti_NoMacros(t *testing.T) {
456 - params := "" // No macros
457 -
458 - input := `.1.3.6.1.2.1.31.1.1.1.1.1 = STRING: "eth0"`
459 -
460 - result, err := snmpWalkToJSONMulti(Value{Data: input, Type: ValueTypeStr}, params, NoopLogger{})
461 - if err != nil {
462 - t.Fatalf("snmpWalkToJSONMulti() error: %v", err)
463 - }
464 -
465 - // No macros should return empty metrics array
466 - if len(result.Metrics) != 0 {
467 - t.Errorf("Expected 0 metrics with no macros, got %d", len(result.Metrics))
468 - }
469 -}
470 -
471 -// TestSNMPWalkMulti_SingleItem tests single discovery item
472 -func TestSNMPWalkMulti_SingleItem(t *testing.T) {
473 - params := `{#IFNAME}
474 -.1.3.6.1.2.1.31.1.1.1.1
475 -0`
476 -
477 - input := `.1.3.6.1.2.1.31.1.1.1.1.1 = STRING: "lo"`
478 -
479 - result, err := snmpWalkToJSONMulti(Value{Data: input, Type: ValueTypeStr}, params, NoopLogger{})
480 - if err != nil {
481 - t.Fatalf("snmpWalkToJSONMulti() error: %v", err)
482 - }
483 -
484 - if len(result.Metrics) != 1 {
485 - t.Fatalf("Expected 1 metric, got %d", len(result.Metrics))
486 - }
487 -
488 - m := result.Metrics[0]
489 - if m.Name != "snmp_discovery" {
490 - t.Errorf("Expected name 'snmp_discovery', got %q", m.Name)
491 - }
492 - if m.Labels["index"] != "1" {
493 - t.Errorf("Expected index label '1', got %q", m.Labels["index"])
494 - }
495 - if !contains(m.Value, `"{#IFNAME}":"lo"`) {
496 - t.Errorf("Expected IFNAME=lo, got %q", m.Value)
497 - }
498 -}
499 -
500 -// TestSNMPWalkMulti_ViaPreprocessor tests full integration
501 -func TestSNMPWalkMulti_ViaPreprocessor(t *testing.T) {
502 - params := `{#IFNAME}
503 -.1.3.6.1.2.1.31.1.1.1.1
504 -0
505 -{#IFTYPE}
506 -.1.3.6.1.2.1.2.2.1.3
507 -0`
508 -
509 - input := `.1.3.6.1.2.1.31.1.1.1.1.1 = STRING: "eth0"
510 -.1.3.6.1.2.1.31.1.1.1.1.2 = STRING: "wlan0"
511 -.1.3.6.1.2.1.2.2.1.3.1 = INTEGER: 6
512 -.1.3.6.1.2.1.2.2.1.3.2 = INTEGER: 71
513 -`
514 -
515 - p := NewPreprocessor("test-shard")
516 - result, err := p.Execute("item1",
517 - Value{Data: input, Type: ValueTypeStr, Timestamp: time.Now()},
518 - Step{Type: StepTypeSNMPWalkToJSONMulti, Params: params})
519 -
520 - if err != nil {
521 - t.Fatalf("Execute() error: %v", err)
522 - }
523 -
524 - // Should return 2 metrics (indices 2, 1 in descending order)
525 - if len(result.Metrics) != 2 {
526 - t.Fatalf("Expected 2 metrics, got %d", len(result.Metrics))
527 - }
528 -
529 - // Verify metric structure and labels
530 - for i, m := range result.Metrics {
531 - if m.Name != "snmp_discovery" {
532 - t.Errorf("Metric %d: expected name 'snmp_discovery', got %q", i, m.Name)
533 - }
534 - if m.Type != ValueTypeStr {
535 - t.Errorf("Metric %d: expected type ValueTypeStr, got %v", i, m.Type)
536 - }
537 - if m.Labels["index"] == "" {
538 - t.Errorf("Metric %d: missing index label", i)
539 - }
540 - // Verify JSON structure
541 - if !contains(m.Value, `"{#SNMPINDEX}"`) {
542 - t.Errorf("Metric %d: missing SNMPINDEX field", i)
543 - }
544 - if !contains(m.Value, `"{#IFNAME}"`) {
545 - t.Errorf("Metric %d: missing IFNAME field", i)
546 - }
547 - if !contains(m.Value, `"{#IFTYPE}"`) {
548 - t.Errorf("Metric %d: missing IFTYPE field", i)
549 - }
550 - }
551 -}
552 -
553 -// TestSNMPWalkMulti_InvalidData tests error handling
554 -func TestSNMPWalkMulti_InvalidData(t *testing.T) {
555 - params := `{#IFNAME}
556 -.1.3.6.1.2.1.31.1.1.1.1
557 -0`
558 -
559 - input := `not valid SNMP walk data`
560 -
561 - _, err := snmpWalkToJSONMulti(Value{Data: input, Type: ValueTypeStr}, params, NoopLogger{})
562 - if err == nil {
563 - t.Fatal("Expected error for invalid SNMP data, got nil")
564 - }
565 -}
566 -
567 -// TestZabbixCompatibility_SNMPWalkToJSON tests that old step type still works
568 -func TestZabbixCompatibility_SNMPWalkToJSON(t *testing.T) {
569 - params := `{#IFNAME}
570 -.1.3.6.1.2.1.31.1.1.1.1
571 -0`
572 -
573 - input := `.1.3.6.1.2.1.31.1.1.1.1.1 = STRING: "eth0"
574 -.1.3.6.1.2.1.31.1.1.1.1.2 = STRING: "eth1"
575 -`
576 -
577 - p := NewPreprocessor("test-shard")
578 - result, err := p.Execute("item1",
579 - Value{Data: input, Type: ValueTypeStr, Timestamp: time.Now()},
580 - Step{Type: StepTypeSNMPWalkToJSON, Params: params}) // OLD step type
581 -
582 - if err != nil {
583 - t.Fatalf("Execute() error: %v", err)
584 - }
585 -
586 - // OLD behavior: Should return SINGLE metric with JSON array string
587 - if len(result.Metrics) != 1 {
588 - t.Fatalf("Expected 1 metric (Zabbix compatibility), got %d", len(result.Metrics))
589 - }
590 -
591 - // Value should be a JSON array string
592 - value := result.Metrics[0].Value
593 - if value[0] != '[' || value[len(value)-1] != ']' {
594 - t.Errorf("Expected JSON array string, got: %s", value)
595 - }
596 -
597 - // Verify it contains both discovery items in the JSON array
598 - if !contains(value, `"{#SNMPINDEX}":"2"`) {
599 - t.Errorf("JSON array missing index 2")
600 - }
601 - if !contains(value, `"{#SNMPINDEX}":"1"`) {
602 - t.Errorf("JSON array missing index 1")
603 - }
604 - if !contains(value, `"{#IFNAME}":"eth0"`) {
605 - t.Errorf("JSON array missing eth0")
606 - }
607 - if !contains(value, `"{#IFNAME}":"eth1"`) {
608 - t.Errorf("JSON array missing eth1")
609 - }
610 -}
611 -
612 -// ============================================================================
613 -// CSV to JSON Multi-Metric Tests
614 -// ============================================================================
615 -
616 -// TestCSVMulti_WithHeader tests CSV with header row
617 -func TestCSVMulti_WithHeader(t *testing.T) {
618 - params := ",\n\"\n1" // comma delimiter, double quote, has header
619 -
620 - input := `name,age,city
621 -Alice,30,NYC
622 -Bob,25,LA
623 -`
624 -
625 - result, err := csvToJSONMulti(Value{Data: input, Type: ValueTypeStr}, params)
626 - if err != nil {
627 - t.Fatalf("csvToJSONMulti() error: %v", err)
628 - }
629 -
630 - // Should return 3 rows (2 data + 1 empty from trailing newline)
631 - if len(result.Metrics) != 3 {
632 - t.Fatalf("Expected 3 metrics, got %d", len(result.Metrics))
633 - }
634 -
635 - // Verify first row
636 - m0 := result.Metrics[0]
637 - if m0.Name != "csv_row" {
638 - t.Errorf("Metric 0: expected name 'csv_row', got %q", m0.Name)
639 - }
640 - if m0.Labels["row"] != "1" {
641 - t.Errorf("Metric 0: expected row label '1', got %q", m0.Labels["row"])
642 - }
643 - if !contains(m0.Value, `"name":"Alice"`) {
644 - t.Errorf("Metric 0: expected name=Alice, got %q", m0.Value)
645 - }
646 - if !contains(m0.Value, `"age":"30"`) {
647 - t.Errorf("Metric 0: expected age=30, got %q", m0.Value)
648 - }
649 - if !contains(m0.Value, `"city":"NYC"`) {
650 - t.Errorf("Metric 0: expected city=NYC, got %q", m0.Value)
651 - }
652 -
653 - // Verify second row
654 - m1 := result.Metrics[1]
655 - if m1.Labels["row"] != "2" {
656 - t.Errorf("Metric 1: expected row label '2', got %q", m1.Labels["row"])
657 - }
658 - if !contains(m1.Value, `"name":"Bob"`) {
659 - t.Errorf("Metric 1: expected name=Bob, got %q", m1.Value)
660 - }
661 -
662 - // Verify third row (empty trailing row from newline)
663 - m2 := result.Metrics[2]
664 - if m2.Labels["row"] != "3" {
665 - t.Errorf("Metric 2: expected row label '3', got %q", m2.Labels["row"])
666 - }
667 - // Empty row should have all empty fields
668 - if !contains(m2.Value, `"name":""`) {
669 - t.Errorf("Metric 2: expected empty name field, got %q", m2.Value)
670 - }
671 -}
672 -
673 -// TestCSVMulti_NoHeader tests CSV without header row
674 -func TestCSVMulti_NoHeader(t *testing.T) {
675 - params := ",\n\"\n0" // comma delimiter, double quote, no header
676 -
677 - input := `10,20,30
678 -40,50,60`
679 -
680 - result, err := csvToJSONMulti(Value{Data: input, Type: ValueTypeStr}, params)
681 - if err != nil {
682 - t.Fatalf("csvToJSONMulti() error: %v", err)
683 - }
684 -
685 - // Should return 2 rows
686 - if len(result.Metrics) != 2 {
687 - t.Fatalf("Expected 2 metrics, got %d", len(result.Metrics))
688 - }
689 -
690 - // Verify first row (1-based column indices)
691 - m0 := result.Metrics[0]
692 - if m0.Labels["row"] != "1" {
693 - t.Errorf("Metric 0: expected row label '1', got %q", m0.Labels["row"])
694 - }
695 - if !contains(m0.Value, `"1":"10"`) {
696 - t.Errorf("Metric 0: expected column 1=10, got %q", m0.Value)
697 - }
698 - if !contains(m0.Value, `"2":"20"`) {
699 - t.Errorf("Metric 0: expected column 2=20, got %q", m0.Value)
700 - }
701 - if !contains(m0.Value, `"3":"30"`) {
702 - t.Errorf("Metric 0: expected column 3=30, got %q", m0.Value)
703 - }
704 -
705 - // Verify second row
706 - m1 := result.Metrics[1]
707 - if m1.Labels["row"] != "2" {
708 - t.Errorf("Metric 1: expected row label '2', got %q", m1.Labels["row"])
709 - }
710 - if !contains(m1.Value, `"1":"40"`) {
711 - t.Errorf("Metric 1: expected column 1=40, got %q", m1.Value)
712 - }
713 -}
714 -
715 -// TestCSVMulti_EmptyInput tests empty input handling
716 -func TestCSVMulti_EmptyInput(t *testing.T) {
717 - params := ",\n\"\n1"
718 -
719 - result, err := csvToJSONMulti(Value{Data: "", Type: ValueTypeStr}, params)
720 - if err != nil {
721 - t.Fatalf("csvToJSONMulti() error on empty input: %v", err)
722 - }
723 -
724 - // Empty input should return zero metrics
725 - if len(result.Metrics) != 0 {
726 - t.Errorf("Expected 0 metrics for empty input, got %d", len(result.Metrics))
727 - }
728 -}
729 -
730 -// TestCSVMulti_SingleRow tests single data row
731 -func TestCSVMulti_SingleRow(t *testing.T) {
732 - params := ",\n\"\n1"
733 -
734 - input := `name,value
735 -test,42`
736 -
737 - result, err := csvToJSONMulti(Value{Data: input, Type: ValueTypeStr}, params)
738 - if err != nil {
739 - t.Fatalf("csvToJSONMulti() error: %v", err)
740 - }
741 -
742 - if len(result.Metrics) != 1 {
743 - t.Fatalf("Expected 1 metric, got %d", len(result.Metrics))
744 - }
745 -
746 - m := result.Metrics[0]
747 - if m.Name != "csv_row" {
748 - t.Errorf("Expected name 'csv_row', got %q", m.Name)
749 - }
750 - if m.Labels["row"] != "1" {
751 - t.Errorf("Expected row label '1', got %q", m.Labels["row"])
752 - }
753 - if !contains(m.Value, `"name":"test"`) {
754 - t.Errorf("Expected name=test, got %q", m.Value)
755 - }
756 - if !contains(m.Value, `"value":"42"`) {
757 - t.Errorf("Expected value=42, got %q", m.Value)
758 - }
759 -}
760 -
761 -// TestCSVMulti_ViaPreprocessor tests full integration
762 -func TestCSVMulti_ViaPreprocessor(t *testing.T) {
763 - params := ",\n\"\n1"
764 -
765 - input := `id,status
766 -1,active
767 -2,inactive
768 -3,pending`
769 -
770 - p := NewPreprocessor("test-shard")
771 - result, err := p.Execute("item1",
772 - Value{Data: input, Type: ValueTypeStr, Timestamp: time.Now()},
773 - Step{Type: StepTypeCSVToJSONMulti, Params: params})
774 -
775 - if err != nil {
776 - t.Fatalf("Execute() error: %v", err)
777 - }
778 -
779 - // Should return 3 metrics (one per data row)
780 - if len(result.Metrics) != 3 {
781 - t.Fatalf("Expected 3 metrics, got %d", len(result.Metrics))
782 - }
783 -
784 - // Verify all metrics have correct structure
785 - for i, m := range result.Metrics {
786 - if m.Name != "csv_row" {
787 - t.Errorf("Metric %d: expected name 'csv_row', got %q", i, m.Name)
788 - }
789 - if m.Type != ValueTypeStr {
790 - t.Errorf("Metric %d: expected type ValueTypeStr, got %v", i, m.Type)
791 - }
792 - if m.Labels["row"] == "" {
793 - t.Errorf("Metric %d: missing row label", i)
794 - }
795 - // Verify JSON structure
796 - if !contains(m.Value, `"id"`) {
797 - t.Errorf("Metric %d: missing id field", i)
798 - }
799 - if !contains(m.Value, `"status"`) {
800 - t.Errorf("Metric %d: missing status field", i)
801 - }
802 - }
803 -}
804 -
805 -// TestCSVMulti_InvalidParams tests error handling
806 -func TestCSVMulti_InvalidParams(t *testing.T) {
807 - params := "," // Missing required params
808 -
809 - input := `a,b,c`
810 -
811 - _, err := csvToJSONMulti(Value{Data: input, Type: ValueTypeStr}, params)
812 - if err == nil {
813 - t.Fatal("Expected error for invalid params, got nil")
814 - }
815 -}
816 -
817 -// TestZabbixCompatibility_CSVToJSON tests that old step type still works
818 -func TestZabbixCompatibility_CSVToJSON(t *testing.T) {
819 - params := ",\n\"\n1"
820 -
821 - input := `name,value
822 -foo,10
823 -bar,20`
824 -
825 - p := NewPreprocessor("test-shard")
826 - result, err := p.Execute("item1",
827 - Value{Data: input, Type: ValueTypeStr, Timestamp: time.Now()},
828 - Step{Type: StepTypeCSVToJSON, Params: params}) // OLD step type
829 -
830 - if err != nil {
831 - t.Fatalf("Execute() error: %v", err)
832 - }
833 -
834 - // OLD behavior: Should return SINGLE metric with JSON array string
835 - if len(result.Metrics) != 1 {
836 - t.Fatalf("Expected 1 metric (Zabbix compatibility), got %d", len(result.Metrics))
837 - }
838 -
839 - // Value should be a JSON array string
840 - value := result.Metrics[0].Value
841 - if value[0] != '[' || value[len(value)-1] != ']' {
842 - t.Errorf("Expected JSON array string, got: %s", value)
843 - }
844 -
845 - // Verify it contains both rows in the JSON array
846 - if !contains(value, `"name":"foo"`) {
847 - t.Errorf("JSON array missing foo row")
848 - }
849 - if !contains(value, `"name":"bar"`) {
850 - t.Errorf("JSON array missing bar row")
851 - }
852 - if !contains(value, `"value":"10"`) {
853 - t.Errorf("JSON array missing value 10")
854 - }
855 - if !contains(value, `"value":"20"`) {
856 - t.Errorf("JSON array missing value 20")
857 - }
858 -}
src/go/plugin/scripts.d/pkg/zabbixpreproc/pipeline_test.go deleted
-467
@@ -1,467 +0,0 @@
1 -package zabbixpreproc
2 -
3 -import (
4 - "strings"
5 - "testing"
6 - "time"
7 -)
8 -
9 -// TestPipeline_BasicChaining tests that values flow correctly between steps
10 -func TestPipeline_BasicChaining(t *testing.T) {
11 - p := NewPreprocessor("test-shard")
12 -
13 - tests := []struct {
14 - name string
15 - input string
16 - steps []Step
17 - expected string
18 - }{
19 - {
20 - name: "JSONPath → Multiplier → Range",
21 - input: `{"cpu": 0.45}`,
22 - steps: []Step{
23 - {Type: StepTypeJSONPath, Params: "$.cpu"}, // Extract 0.45
24 - {Type: StepTypeMultiplier, Params: "100"}, // Multiply by 100 → 45
25 - {Type: StepTypeValidateRange, Params: "0\n100"}, // Validate 0-100
26 - },
27 - expected: "45",
28 - },
29 - {
30 - name: "Trim → Regex → Multiplier",
31 - input: " Temperature: 25.5 C ",
32 - steps: []Step{
33 - {Type: StepTypeTrim, Params: " "}, // Remove spaces
34 - {Type: StepTypeRegexSubstitution, Params: "Temperature:\\s+([0-9.]+).*\n\\1"}, // Extract number
35 - {Type: StepTypeMultiplier, Params: "1.8"}, // Celsius to Fahrenheit delta
36 - },
37 - expected: "45.9",
38 - },
39 - {
40 - name: "XPath → Trim → Hex2Dec",
41 - input: `<data><value> 0xFF </value></data>`,
42 - steps: []Step{
43 - {Type: StepTypeXPath, Params: "//value/text()"}, // Extract " 0xFF "
44 - {Type: StepTypeTrim, Params: " "}, // Remove spaces → "0xFF"
45 - {Type: StepTypeHex2Dec}, // Convert to decimal
46 - },
47 - expected: "255",
48 - },
49 - {
50 - name: "Prometheus → Multiplier → Validate",
51 - input: "http_requests{method=\"GET\"} 100\nhttp_requests{method=\"POST\"} 50",
52 - steps: []Step{
53 - {Type: StepTypePrometheusPattern, Params: `http_requests{method="GET"}`}, // Extract 100
54 - {Type: StepTypeMultiplier, Params: "0.01"}, // Convert to percentage
55 - {Type: StepTypeValidateRange, Params: "0\n10"}, // Must be 0-10%
56 - },
57 - expected: "1",
58 - },
59 - {
60 - name: "String Replace → Trim → Bool2Dec",
61 - input: "Status: TRUE ",
62 - steps: []Step{
63 - {Type: StepTypeStringReplace, Params: "Status: \n"}, // Remove prefix
64 - {Type: StepTypeTrim, Params: " "}, // Remove spaces
65 - {Type: StepTypeBool2Dec}, // TRUE → 1
66 - },
67 - expected: "1",
68 - },
69 - }
70 -
71 - for _, tt := range tests {
72 - t.Run(tt.name, func(t *testing.T) {
73 - value := Value{Data: tt.input, Type: ValueTypeStr}
74 - result, err := p.ExecutePipeline("test-item", value, tt.steps)
75 -
76 - if err != nil {
77 - t.Errorf("Pipeline failed: %v", err)
78 - return
79 - }
80 -
81 - if len(result.Metrics) == 0 {
82 - t.Error("Pipeline produced no metrics")
83 - return
84 - }
85 -
86 - got := result.Metrics[0].Value
87 - if got != tt.expected {
88 - t.Errorf("Expected %q, got %q", tt.expected, got)
89 - }
90 - })
91 - }
92 -}
93 -
94 -// TestPipeline_ErrorPropagation tests that errors stop pipeline execution
95 -func TestPipeline_ErrorPropagation(t *testing.T) {
96 - p := NewPreprocessor("test-shard")
97 -
98 - tests := []struct {
99 - name string
100 - input string
101 - steps []Step
102 - expectError bool
103 - errorMatch string
104 - }{
105 - {
106 - name: "Invalid JSONPath stops pipeline",
107 - input: `{"value": 100}`,
108 - steps: []Step{
109 - {Type: StepTypeJSONPath, Params: "$.nonexistent"}, // Error: path not found
110 - {Type: StepTypeMultiplier, Params: "2"}, // Should NOT execute
111 - },
112 - expectError: true,
113 - errorMatch: "step 0",
114 - },
115 - {
116 - name: "Invalid multiplier stops pipeline",
117 - input: "abc",
118 - steps: []Step{
119 - {Type: StepTypeMultiplier, Params: "10"}, // Error: not a number
120 - {Type: StepTypeValidateRange, Params: "0\n100"}, // Should NOT execute
121 - },
122 - expectError: true,
123 - errorMatch: "step 0",
124 - },
125 - {
126 - name: "Failed validation stops pipeline",
127 - input: "150",
128 - steps: []Step{
129 - {Type: StepTypeValidateRange, Params: "0\n100"}, // Error: out of range
130 - {Type: StepTypeMultiplier, Params: "2"}, // Should NOT execute
131 - },
132 - expectError: true,
133 - errorMatch: "step 0",
134 - },
135 - {
136 - name: "Invalid regex stops pipeline",
137 - input: "test",
138 - steps: []Step{
139 - {Type: StepTypeRegexSubstitution, Params: "([0-9]+\n\\1"}, // Error: unclosed group
140 - {Type: StepTypeTrim, Params: " "}, // Should NOT execute
141 - },
142 - expectError: true,
143 - errorMatch: "step 0",
144 - },
145 - {
146 - name: "Error in middle step stops pipeline",
147 - input: `{"value": "abc"}`,
148 - steps: []Step{
149 - {Type: StepTypeJSONPath, Params: "$.value"}, // Extract "abc"
150 - {Type: StepTypeMultiplier, Params: "10"}, // Error: not a number
151 - {Type: StepTypeTrim, Params: " "}, // Should NOT execute
152 - },
153 - expectError: true,
154 - errorMatch: "step 1",
155 - },
156 - }
157 -
158 - for _, tt := range tests {
159 - t.Run(tt.name, func(t *testing.T) {
160 - value := Value{Data: tt.input, Type: ValueTypeStr}
161 - result, err := p.ExecutePipeline("test-item", value, tt.steps)
162 -
163 - if tt.expectError {
164 - if err == nil {
165 - t.Error("Expected error, got nil")
166 - return
167 - }
168 - if !strings.Contains(err.Error(), tt.errorMatch) {
169 - t.Errorf("Expected error containing %q, got: %v", tt.errorMatch, err)
170 - }
171 - if result.Error == nil {
172 - t.Error("Expected Result.Error to be set")
173 - }
174 - } else {
175 - if err != nil {
176 - t.Errorf("Unexpected error: %v", err)
177 - }
178 - }
179 - })
180 - }
181 -}
182 -
183 -// TestPipeline_ErrorHandlers tests error handler behavior in pipelines
184 -func TestPipeline_ErrorHandlers(t *testing.T) {
185 - p := NewPreprocessor("test-shard")
186 -
187 - tests := []struct {
188 - name string
189 - input string
190 - steps []Step
191 - expected string
192 - }{
193 - {
194 - name: "DISCARD error allows pipeline to continue with empty value",
195 - input: `{"value": 100}`,
196 - steps: []Step{
197 - {
198 - Type: StepTypeJSONPath,
199 - Params: "$.nonexistent",
200 - ErrorHandler: ErrorHandler{
201 - Action: ErrorActionDiscard, // Return empty string on error
202 - },
203 - },
204 - {Type: StepTypeTrim, Params: " "}, // Processes empty string → empty string
205 - },
206 - expected: "",
207 - },
208 - {
209 - name: "SET_VALUE error allows pipeline to continue with fallback",
210 - input: "not-a-number",
211 - steps: []Step{
212 - {
213 - Type: StepTypeMultiplier,
214 - Params: "10",
215 - ErrorHandler: ErrorHandler{
216 - Action: ErrorActionSetValue,
217 - Params: "0", // Fallback value
218 - },
219 - },
220 - {Type: StepTypeMultiplier, Params: "2"}, // Processes "0" → "0"
221 - },
222 - expected: "0",
223 - },
224 - {
225 - name: "Multiple error handlers in sequence",
226 - input: `{"data": "invalid"}`,
227 - steps: []Step{
228 - {
229 - Type: StepTypeJSONPath,
230 - Params: "$.missing",
231 - ErrorHandler: ErrorHandler{
232 - Action: ErrorActionSetValue,
233 - Params: "100",
234 - },
235 - },
236 - {
237 - Type: StepTypeMultiplier,
238 - Params: "abc", // Invalid multiplier
239 - ErrorHandler: ErrorHandler{
240 - Action: ErrorActionSetValue,
241 - Params: "50",
242 - },
243 - },
244 - {Type: StepTypeMultiplier, Params: "2"}, // Processes "50" → "100"
245 - },
246 - expected: "100",
247 - },
248 - {
249 - name: "Error handler only catches specific step errors",
250 - input: "150",
251 - steps: []Step{
252 - {
253 - Type: StepTypeValidateRange,
254 - Params: "0\n100",
255 - ErrorHandler: ErrorHandler{
256 - Action: ErrorActionSetValue,
257 - Params: "100", // Clamp to max
258 - },
259 - },
260 - {Type: StepTypeMultiplier, Params: "0.5"}, // Processes "100" → "50"
261 - },
262 - expected: "50",
263 - },
264 - }
265 -
266 - for _, tt := range tests {
267 - t.Run(tt.name, func(t *testing.T) {
268 - value := Value{Data: tt.input, Type: ValueTypeStr}
269 - result, err := p.ExecutePipeline("test-item", value, tt.steps)
270 -
271 - if err != nil {
272 - t.Errorf("Pipeline failed: %v", err)
273 - return
274 - }
275 -
276 - if len(result.Metrics) == 0 {
277 - t.Error("Pipeline produced no metrics")
278 - return
279 - }
280 -
281 - got := result.Metrics[0].Value
282 - if got != tt.expected {
283 - t.Errorf("Expected %q, got %q", tt.expected, got)
284 - }
285 - })
286 - }
287 -}
288 -
289 -// TestPipeline_ErrorHandlerSetError tests that SET_ERROR stops pipeline
290 -func TestPipeline_ErrorHandlerSetError(t *testing.T) {
291 - p := NewPreprocessor("test-shard")
292 -
293 - steps := []Step{
294 - {
295 - Type: StepTypeMultiplier,
296 - Params: "abc", // Invalid
297 - ErrorHandler: ErrorHandler{
298 - Action: ErrorActionSetError,
299 - Params: "Custom error message",
300 - },
301 - },
302 - {Type: StepTypeMultiplier, Params: "2"}, // Should NOT execute
303 - }
304 -
305 - value := Value{Data: "100", Type: ValueTypeStr}
306 - result, err := p.ExecutePipeline("test-item", value, steps)
307 -
308 - if err == nil {
309 - t.Error("Expected error, got nil")
310 - return
311 - }
312 -
313 - if !strings.Contains(err.Error(), "Custom error message") {
314 - t.Errorf("Expected custom error message, got: %v", err)
315 - }
316 -
317 - if result.Error == nil {
318 - t.Error("Expected Result.Error to be set")
319 - }
320 -}
321 -
322 -// TestPipeline_StatefulOperations tests that stateful steps work in pipelines
323 -func TestPipeline_StatefulOperations(t *testing.T) {
324 - p := NewPreprocessor("test-shard")
325 -
326 - steps := []Step{
327 - {Type: StepTypeJSONPath, Params: "$.counter"},
328 - {Type: StepTypeDeltaValue},
329 - {Type: StepTypeMultiplier, Params: "10"}, // Scale delta
330 - }
331 -
332 - // First call: baseline (delta = 0)
333 - value1 := Value{
334 - Data: `{"counter": 100}`,
335 - Type: ValueTypeStr,
336 - Timestamp: time.Now(),
337 - }
338 - result1, err := p.ExecutePipeline("item1", value1, steps)
339 - if err != nil {
340 - t.Fatalf("First call failed: %v", err)
341 - }
342 - if result1.Metrics[0].Value != "0" {
343 - t.Errorf("First delta should be 0, got %s", result1.Metrics[0].Value)
344 - }
345 -
346 - // Second call: delta = 50, scaled = 500
347 - value2 := Value{
348 - Data: `{"counter": 150}`,
349 - Type: ValueTypeStr,
350 - Timestamp: time.Now(),
351 - }
352 - result2, err := p.ExecutePipeline("item1", value2, steps)
353 - if err != nil {
354 - t.Fatalf("Second call failed: %v", err)
355 - }
356 - if result2.Metrics[0].Value != "500" {
357 - t.Errorf("Expected 500, got %s", result2.Metrics[0].Value)
358 - }
359 -}
360 -
361 -// TestPipeline_EmptyPipeline tests edge case of zero steps
362 -func TestPipeline_EmptyPipeline(t *testing.T) {
363 - p := NewPreprocessor("test-shard")
364 -
365 - value := Value{Data: "test", Type: ValueTypeStr}
366 - result, err := p.ExecutePipeline("test-item", value, []Step{})
367 -
368 - if err != nil {
369 - t.Errorf("Empty pipeline should succeed, got error: %v", err)
370 - }
371 -
372 - if len(result.Metrics) == 0 {
373 - t.Error("Empty pipeline should return input as metric")
374 - return
375 - }
376 -
377 - if result.Metrics[0].Value != "test" {
378 - t.Errorf("Expected input unchanged, got %q", result.Metrics[0].Value)
379 - }
380 -}
381 -
382 -// TestPipeline_SingleStep tests edge case of single step
383 -func TestPipeline_SingleStep(t *testing.T) {
384 - p := NewPreprocessor("test-shard")
385 -
386 - steps := []Step{
387 - {Type: StepTypeMultiplier, Params: "10"},
388 - }
389 -
390 - value := Value{Data: "5", Type: ValueTypeFloat}
391 - result, err := p.ExecutePipeline("test-item", value, steps)
392 -
393 - if err != nil {
394 - t.Errorf("Single step pipeline failed: %v", err)
395 - }
396 -
397 - if len(result.Metrics) == 0 {
398 - t.Error("Pipeline produced no metrics")
399 - return
400 - }
401 -
402 - if result.Metrics[0].Value != "50" {
403 - t.Errorf("Expected 50, got %s", result.Metrics[0].Value)
404 - }
405 -}
406 -
407 -// TestPipeline_ValueTypePreservation tests that type flows through pipeline
408 -func TestPipeline_ValueTypePreservation(t *testing.T) {
409 - p := NewPreprocessor("test-shard")
410 -
411 - tests := []struct {
412 - name string
413 - inputType ValueType
414 - steps []Step
415 - expectedType ValueType
416 - }{
417 - {
418 - name: "Float through multiplier stays float",
419 - inputType: ValueTypeFloat,
420 - steps: []Step{
421 - {Type: StepTypeMultiplier, Params: "2"},
422 - },
423 - expectedType: ValueTypeFloat,
424 - },
425 - {
426 - name: "String through trim stays string",
427 - inputType: ValueTypeStr,
428 - steps: []Step{
429 - {Type: StepTypeTrim, Params: " "},
430 - },
431 - expectedType: ValueTypeStr,
432 - },
433 - {
434 - name: "String through regex substitution stays string",
435 - inputType: ValueTypeStr,
436 - steps: []Step{
437 - {Type: StepTypeRegexSubstitution, Params: "([0-9]+)\n\\1"},
438 - },
439 - expectedType: ValueTypeStr,
440 - },
441 - }
442 -
443 - for _, tt := range tests {
444 - t.Run(tt.name, func(t *testing.T) {
445 - value := Value{
446 - Data: "100",
447 - Type: tt.inputType,
448 - Timestamp: time.Now(),
449 - }
450 -
451 - result, err := p.ExecutePipeline("test-item", value, tt.steps)
452 - if err != nil {
453 - t.Errorf("Pipeline failed: %v", err)
454 - return
455 - }
456 -
457 - if len(result.Metrics) == 0 {
458 - t.Error("Pipeline produced no metrics")
459 - return
460 - }
461 -
462 - if result.Metrics[0].Type != tt.expectedType {
463 - t.Errorf("Expected type %v, got %v", tt.expectedType, result.Metrics[0].Type)
464 - }
465 - })
466 - }
467 -}
src/go/plugin/scripts.d/pkg/zabbixpreproc/preproc.go deleted
-582
@@ -1,582 +0,0 @@
1 -// Package zabbixpreproc provides Zabbix preprocessing functionality in pure Go.
2 -package zabbixpreproc
3 -
4 -import (
5 - "fmt"
6 - "strings"
7 - "sync"
8 - "time"
9 -)
10 -
11 -// ValueType represents Zabbix item value types.
12 -type ValueType int
13 -
14 -const (
15 - ValueTypeStr ValueType = 0 // ITEM_VALUE_TYPE_STR
16 - ValueTypeUint64 ValueType = 3 // ITEM_VALUE_TYPE_UINT64
17 - ValueTypeFloat ValueType = 1 // ITEM_VALUE_TYPE_FLOAT
18 -)
19 -
20 -// Value represents a preprocessing input/output value.
21 -type Value struct {
22 - Data string // Raw string data
23 - Type ValueType // Value type
24 - Timestamp time.Time // Timestamp for stateful operations
25 - IsError bool // True if this value represents an error from a previous step
26 -}
27 -
28 -// StepType represents the type of preprocessing step.
29 -// Type IDs match Zabbix preprocessing types exactly (ZBX_PREPROC_* from zbxcommon.h)
30 -type StepType int
31 -
32 -const (
33 - // Zabbix preprocessing types (1-30) - MUST match zbxcommon.h exactly
34 - StepTypeMultiplier StepType = 1 // ZBX_PREPROC_MULTIPLIER
35 - StepTypeRTrim StepType = 2 // ZBX_PREPROC_RTRIM
36 - StepTypeLTrim StepType = 3 // ZBX_PREPROC_LTRIM
37 - StepTypeTrim StepType = 4 // ZBX_PREPROC_TRIM
38 - StepTypeRegexSubstitution StepType = 5 // ZBX_PREPROC_REGSUB
39 - StepTypeBool2Dec StepType = 6 // ZBX_PREPROC_BOOL2DEC
40 - StepTypeOct2Dec StepType = 7 // ZBX_PREPROC_OCT2DEC
41 - StepTypeHex2Dec StepType = 8 // ZBX_PREPROC_HEX2DEC
42 - StepTypeDeltaValue StepType = 9 // ZBX_PREPROC_DELTA_VALUE
43 - StepTypeDeltaSpeed StepType = 10 // ZBX_PREPROC_DELTA_SPEED
44 - StepTypeXPath StepType = 11 // ZBX_PREPROC_XPATH
45 - StepTypeJSONPath StepType = 12 // ZBX_PREPROC_JSONPATH
46 - StepTypeValidateRange StepType = 13 // ZBX_PREPROC_VALIDATE_RANGE
47 - StepTypeValidateRegex StepType = 14 // ZBX_PREPROC_VALIDATE_REGEX
48 - StepTypeValidateNotRegex StepType = 15 // ZBX_PREPROC_VALIDATE_NOT_REGEX
49 - StepTypeErrorFieldJSON StepType = 16 // ZBX_PREPROC_ERROR_FIELD_JSON
50 - StepTypeErrorFieldXML StepType = 17 // ZBX_PREPROC_ERROR_FIELD_XML
51 - StepTypeErrorFieldRegex StepType = 18 // ZBX_PREPROC_ERROR_FIELD_REGEX
52 - StepTypeThrottleValue StepType = 19 // ZBX_PREPROC_THROTTLE_VALUE
53 - StepTypeThrottleTimedValue StepType = 20 // ZBX_PREPROC_THROTTLE_TIMED_VALUE
54 - StepTypeJavaScript StepType = 21 // ZBX_PREPROC_SCRIPT
55 - StepTypePrometheusPattern StepType = 22 // ZBX_PREPROC_PROMETHEUS_PATTERN
56 - StepTypePrometheusToJSON StepType = 23 // ZBX_PREPROC_PROMETHEUS_TO_JSON
57 - StepTypeCSVToJSON StepType = 24 // ZBX_PREPROC_CSV_TO_JSON
58 - StepTypeStringReplace StepType = 25 // ZBX_PREPROC_STR_REPLACE
59 - StepTypeValidateNotSupported StepType = 26 // ZBX_PREPROC_VALIDATE_NOT_SUPPORTED
60 - StepTypeXMLToJSON StepType = 27 // ZBX_PREPROC_XML_TO_JSON
61 - StepTypeSNMPWalkValue StepType = 28 // ZBX_PREPROC_SNMP_WALK_VALUE
62 - StepTypeSNMPWalkToJSON StepType = 29 // ZBX_PREPROC_SNMP_WALK_TO_JSON
63 - StepTypeSNMPGetValue StepType = 30 // ZBX_PREPROC_SNMP_GET_VALUE
64 -
65 - // Multi-metric step types (60+) - return multiple Metric objects instead of single JSON string
66 - // These are EXTENSIONS to Zabbix, not part of official spec
67 - StepTypePrometheusToJSONMulti StepType = 60 // Extended: Prometheus to multi-metric array
68 - StepTypeJSONPathMulti StepType = 61 // Extended: JSONPath array extraction
69 - StepTypeSNMPWalkToJSONMulti StepType = 62 // Extended: SNMP walk to multi-metric array
70 - StepTypeCSVToJSONMulti StepType = 63 // Extended: CSV to multi-metric array
71 -)
72 -
73 -// ErrorAction defines how preprocessing errors are handled.
74 -type ErrorAction int
75 -
76 -const (
77 - ErrorActionDefault ErrorAction = 0 // Return error
78 - ErrorActionDiscard ErrorAction = 1 // Discard value (return empty)
79 - ErrorActionSetValue ErrorAction = 2 // Set custom value
80 - ErrorActionSetError ErrorAction = 3 // Set custom error message
81 -)
82 -
83 -// ErrorHandler defines error handling behavior for a preprocessing step.
84 -type ErrorHandler struct {
85 - Action ErrorAction
86 - Params string
87 -}
88 -
89 -// Step represents a single preprocessing step.
90 -type Step struct {
91 - Type StepType
92 - Params string
93 - ErrorHandler ErrorHandler
94 -}
95 -
96 -// Metric represents a single metric output from preprocessing.
97 -type Metric struct {
98 - Name string // Metric name
99 - Value string // Metric value
100 - Type ValueType // Value type
101 - Labels map[string]string // Optional labels
102 -}
103 -
104 -// Result represents the result of preprocessing a value.
105 -type Result struct {
106 - Metrics []Metric // Array of extracted metrics
107 - Logs []string // Optional log entries (future use)
108 - Error error // Overall error if preprocessing failed
109 - Discarded bool // True if value was intentionally discarded (e.g., throttling)
110 -}
111 -
112 -// Preprocessor executes preprocessing steps.
113 -// It maintains state for stateful operations (per shard instance).
114 -// Thread-safe for concurrent use.
115 -type Preprocessor struct {
116 - shardID string
117 - logger Logger // Logger for debugging (defaults to NoopLogger)
118 - limits Limits // Configurable limits for preprocessing operations
119 - mu sync.RWMutex // Protects state map access
120 - state map[string]*OperationState // Per-operation state keyed by operation ID
121 - cleanupTicker *time.Ticker // Ticker for periodic state cleanup (can be nil if disabled)
122 - cleanupDone chan struct{} // Signal to stop cleanup goroutine
123 - stateTTL time.Duration // TTL for inactive state entries (0 = no cleanup)
124 -}
125 -
126 -// OperationState tracks state for stateful operations like Delta and Throttle.
127 -type OperationState struct {
128 - LastValue string
129 - LastTimestamp time.Time
130 - LastValueTime time.Time
131 - LastAccess time.Time // Track last access for TTL-based cleanup
132 -}
133 -
134 -// Config holds configuration options for a Preprocessor instance.
135 -// All fields are optional - zero values use sensible defaults.
136 -type Config struct {
137 - // Logger for debugging (default: NoopLogger - zero overhead)
138 - Logger Logger
139 -
140 - // Limits for preprocessing operations (default: Zabbix-compatible limits)
141 - Limits Limits
142 -
143 - // StateTTL is the time-to-live for inactive state entries.
144 - // Default: 0 (no automatic cleanup)
145 - // Set to positive duration to enable automatic eviction.
146 - StateTTL time.Duration
147 -
148 - // StateCleanupInterval is how often to run the cleanup process.
149 - // Default: 0 (no automatic cleanup)
150 - // Only used if StateTTL > 0.
151 - StateCleanupInterval time.Duration
152 -}
153 -
154 -// NewPreprocessor creates a new preprocessor instance for a specific shard.
155 -// By default, no state cleanup is performed (stateTTL = 0).
156 -// Call EnableStateCleanup() to enable TTL-based eviction.
157 -func NewPreprocessor(shardID string) *Preprocessor {
158 - return &Preprocessor{
159 - shardID: shardID,
160 - logger: NoopLogger{}, // Default: no logging overhead
161 - limits: DefaultLimits(),
162 - state: make(map[string]*OperationState),
163 - stateTTL: 0, // Default: no cleanup
164 - }
165 -}
166 -
167 -// NewPreprocessorWithConfig creates a new preprocessor with custom configuration.
168 -// This allows per-shard customization of logging, timeouts, and cleanup policies.
169 -func NewPreprocessorWithConfig(shardID string, cfg Config) *Preprocessor {
170 - // Use default limits if not provided (zero value check)
171 - limits := cfg.Limits
172 - if limits.JavaScript.Timeout == 0 {
173 - limits = DefaultLimits()
174 - }
175 -
176 - p := &Preprocessor{
177 - shardID: shardID,
178 - limits: limits,
179 - state: make(map[string]*OperationState),
180 - stateTTL: cfg.StateTTL,
181 - }
182 -
183 - // Set logger (default to NoopLogger if not provided)
184 - if cfg.Logger != nil {
185 - p.logger = cfg.Logger
186 - } else {
187 - p.logger = NoopLogger{}
188 - }
189 -
190 - // Enable automatic state cleanup if configured
191 - if cfg.StateTTL > 0 && cfg.StateCleanupInterval > 0 {
192 - p.EnableStateCleanup(cfg.StateTTL, cfg.StateCleanupInterval)
193 - }
194 -
195 - return p
196 -}
197 -
198 -// EnableStateCleanup enables periodic cleanup of inactive state entries.
199 -// ttl: How long an entry can be inactive before being removed
200 -// cleanupInterval: How often to run the cleanup process
201 -// Returns a function to stop the cleanup goroutine (call on shutdown).
202 -func (p *Preprocessor) EnableStateCleanup(ttl, cleanupInterval time.Duration) func() {
203 - p.mu.Lock()
204 - defer p.mu.Unlock()
205 -
206 - // Stop existing cleanup if any
207 - if p.cleanupTicker != nil {
208 - p.cleanupTicker.Stop()
209 - if p.cleanupDone != nil {
210 - close(p.cleanupDone)
211 - }
212 - }
213 -
214 - p.stateTTL = ttl
215 - p.cleanupTicker = time.NewTicker(cleanupInterval)
216 - p.cleanupDone = make(chan struct{})
217 -
218 - // Start cleanup goroutine
219 - go p.runStateCleanup()
220 -
221 - // Return stop function
222 - return func() {
223 - p.mu.Lock()
224 - defer p.mu.Unlock()
225 - if p.cleanupTicker != nil {
226 - p.cleanupTicker.Stop()
227 - close(p.cleanupDone)
228 - p.cleanupTicker = nil
229 - p.cleanupDone = nil
230 - }
231 - }
232 -}
233 -
234 -// runStateCleanup periodically removes inactive state entries.
235 -func (p *Preprocessor) runStateCleanup() {
236 - for {
237 - select {
238 - case <-p.cleanupTicker.C:
239 - p.cleanupInactiveState()
240 - case <-p.cleanupDone:
241 - return
242 - }
243 - }
244 -}
245 -
246 -// cleanupInactiveState removes state entries that haven't been accessed within TTL.
247 -func (p *Preprocessor) cleanupInactiveState() {
248 - if p.stateTTL == 0 {
249 - return // Cleanup disabled
250 - }
251 -
252 - p.mu.Lock()
253 - defer p.mu.Unlock()
254 -
255 - now := time.Now()
256 - removed := 0
257 -
258 - for key, state := range p.state {
259 - if now.Sub(state.LastAccess) > p.stateTTL {
260 - delete(p.state, key)
261 - removed++
262 - }
263 - }
264 -
265 - if removed > 0 {
266 - p.logger.Debug("state cleanup completed",
267 - "shard", p.shardID,
268 - "removed", removed,
269 - "remaining", len(p.state))
270 - }
271 -}
272 -
273 -// ClearState removes all stored state entries for the given itemID within this shard.
274 -// Used when an item/instance is obsoleted so historical state does not leak to new entities.
275 -func (p *Preprocessor) ClearState(itemID string) {
276 - if itemID == "" {
277 - return
278 - }
279 - prefix := fmt.Sprintf("%s:%s:", p.shardID, itemID)
280 - p.mu.Lock()
281 - defer p.mu.Unlock()
282 - for key := range p.state {
283 - if strings.HasPrefix(key, prefix) {
284 - delete(p.state, key)
285 - }
286 - }
287 -}
288 -
289 -// SetLogger sets the logger for this preprocessor instance.
290 -// By default, NoopLogger is used (zero overhead).
291 -// This can be called to enable debug logging for troubleshooting.
292 -func (p *Preprocessor) SetLogger(logger Logger) {
293 - p.logger = logger
294 -}
295 -
296 -// PreloadState sets the history state for a stateful operation.
297 -// This is primarily for testing - allows proper setup of delta/throttle state.
298 -// itemID: The item identifier
299 -// stepType: The step type (must be stateful: Delta, Throttle)
300 -// lastValue: The previous value
301 -// lastTimestamp: The timestamp of the previous value
302 -func (p *Preprocessor) PreloadState(itemID string, stepType StepType, lastValue string, lastTimestamp time.Time) error {
303 - var stateKey string
304 - switch stepType {
305 - case StepTypeDeltaValue:
306 - stateKey = fmt.Sprintf("%s:%s:delta_value", p.shardID, itemID)
307 - case StepTypeDeltaSpeed:
308 - stateKey = fmt.Sprintf("%s:%s:delta_speed", p.shardID, itemID)
309 - case StepTypeThrottleValue:
310 - stateKey = fmt.Sprintf("%s:%s:throttle_value", p.shardID, itemID)
311 - case StepTypeThrottleTimedValue:
312 - stateKey = fmt.Sprintf("%s:%s:throttle_timed", p.shardID, itemID)
313 - default:
314 - return fmt.Errorf("step type %d is not stateful", stepType)
315 - }
316 -
317 - p.mu.Lock()
318 - p.state[stateKey] = &OperationState{
319 - LastValue: lastValue,
320 - LastValueTime: lastTimestamp,
321 - LastTimestamp: lastTimestamp,
322 - LastAccess: time.Now(),
323 - }
324 - p.mu.Unlock()
325 -
326 - return nil
327 -}
328 -
329 -// Execute applies a single preprocessing step to a value for a specific item.
330 -// itemID: Unique identifier for the item within this shard (used for state isolation)
331 -// value: The input value to process
332 -// step: The preprocessing step to apply
333 -// Returns: Result containing metrics array, optional logs, and error if failed
334 -func (p *Preprocessor) Execute(itemID string, value Value, step Step) (Result, error) {
335 - if err := validateStep(step); err != nil {
336 - return Result{Error: err}, err
337 - }
338 -
339 - result, err := p.executeStep(itemID, value, step)
340 - if err != nil {
341 - handledValue, handledErr := p.handleError(err, step.ErrorHandler)
342 - return valueToResult(handledValue, handledErr), handledErr
343 - }
344 - return result, nil
345 -}
346 -
347 -// ExecutePipeline applies multiple preprocessing steps in sequence for a specific item.
348 -// itemID: Unique identifier for the item within this shard (used for state isolation)
349 -// value: The input value to process
350 -// steps: The preprocessing steps to apply in order
351 -// Returns: Result containing metrics array, optional logs, and error if failed
352 -//
353 -// IsError Semantics:
354 -// The Value.IsError flag indicates whether the INPUT value originated from an error condition
355 -// (e.g., failed data collection in Zabbix). This flag is preserved through the entire pipeline
356 -// because it's a property of the original data source, not the preprocessing results.
357 -// - IsError=true: Original data came from an error (used by validateNotSupported)
358 -// - IsError=false: Original data was successfully collected
359 -// This flag does NOT change based on step success/failure - those are separate concerns.
360 -// Step failures return errors; IsError tracks input data provenance.
361 -type pipelineValue struct {
362 - value Value
363 - meta Metric
364 -}
365 -
366 -func (p *Preprocessor) ExecutePipeline(itemID string, value Value, steps []Step) (Result, error) {
367 - values := []pipelineValue{{value: value}}
368 - for i, step := range steps {
369 - next := make([]pipelineValue, 0, len(values))
370 - for _, pv := range values {
371 - result, err := p.Execute(itemID, pv.value, step)
372 - if err != nil {
373 - pipelineErr := fmt.Errorf("step %d failed: %w", i, err)
374 - return Result{Error: pipelineErr}, pipelineErr
375 - }
376 - if result.Discarded || len(result.Metrics) == 0 {
377 - return Result{Discarded: true}, nil
378 - }
379 - if isMultiStepType(step.Type) {
380 - for _, metric := range result.Metrics {
381 - next = append(next, pipelineValue{
382 - value: Value{
383 - Data: metric.Value,
384 - Type: metric.Type,
385 - Timestamp: pv.value.Timestamp,
386 - IsError: pv.value.IsError,
387 - },
388 - meta: metric,
389 - })
390 - }
391 - } else {
392 - metric := result.Metrics[0]
393 - next = append(next, pipelineValue{
394 - value: Value{
395 - Data: metric.Value,
396 - Type: metric.Type,
397 - Timestamp: pv.value.Timestamp,
398 - IsError: pv.value.IsError,
399 - },
400 - })
401 - }
402 - }
403 - if len(next) == 0 {
404 - return Result{Discarded: true}, nil
405 - }
406 - values = next
407 - }
408 - metrics := make([]Metric, len(values))
409 - for i, pv := range values {
410 - meta := pv.meta
411 - metrics[i] = Metric{
412 - Name: meta.Name,
413 - Value: pv.value.Data,
414 - Type: pv.value.Type,
415 - Labels: meta.Labels,
416 - }
417 - }
418 - return Result{Metrics: metrics}, nil
419 -}
420 -
421 -func isMultiStepType(t StepType) bool {
422 - switch t {
423 - case StepTypePrometheusToJSONMulti,
424 - StepTypeJSONPathMulti,
425 - StepTypeSNMPWalkToJSONMulti,
426 - StepTypeCSVToJSONMulti:
427 - return true
428 - default:
429 - return false
430 - }
431 -}
432 -
433 -// valueToResult converts a Value to a Result with a single metric
434 -func valueToResult(value Value, err error) Result {
435 - if err != nil {
436 - return Result{Error: err}
437 - }
438 - return Result{
439 - Metrics: []Metric{{
440 - Name: "",
441 - Value: value.Data,
442 - Type: value.Type,
443 - }},
444 - Error: nil,
445 - }
446 -}
447 -
448 -func (p *Preprocessor) executeStep(itemID string, value Value, step Step) (Result, error) {
449 - // Multi-metric steps - return Result with multiple Metric objects directly
450 - // These are NEW step types (60+) that don't break Zabbix compatibility
451 - switch step.Type {
452 - case StepTypePrometheusToJSONMulti:
453 - return prometheusToJSONMulti(value, step.Params)
454 - case StepTypeJSONPathMulti:
455 - return jsonPathMulti(value, step.Params)
456 - case StepTypeSNMPWalkToJSONMulti:
457 - return snmpWalkToJSONMulti(value, step.Params, p.logger)
458 - case StepTypeCSVToJSONMulti:
459 - return csvToJSONMulti(value, step.Params)
460 - }
461 -
462 - // Single-metric steps - execute and wrap with valueToResult()
463 - // These maintain Zabbix compatibility (original step types 1-29)
464 - var result Value
465 - var err error
466 -
467 - switch step.Type {
468 - case StepTypeMultiplier:
469 - result, err = multiplyValue(value, step.Params)
470 - case StepTypeTrim:
471 - result, err = trimValue(value, step.Params, "both")
472 - case StepTypeRTrim:
473 - result, err = trimValue(value, step.Params, "right")
474 - case StepTypeLTrim:
475 - result, err = trimValue(value, step.Params, "left")
476 - case StepTypeRegexSubstitution:
477 - result, err = regexSubstitute(value, step.Params)
478 - case StepTypeBool2Dec:
479 - result, err = bool2Dec(value)
480 - case StepTypeOct2Dec:
481 - result, err = oct2Dec(value)
482 - case StepTypeHex2Dec:
483 - result, err = hex2Dec(value)
484 - case StepTypeDeltaValue:
485 - result, err = p.deltaValue(itemID, value, step.Params)
486 - case StepTypeDeltaSpeed:
487 - result, err = p.deltaSpeed(itemID, value, step.Params)
488 - case StepTypeStringReplace:
489 - result, err = stringReplace(value, step.Params)
490 - case StepTypeValidateRange:
491 - result, err = validateRange(value, step.Params)
492 - case StepTypeValidateRegex:
493 - result, err = validateRegex(value, step.Params)
494 - case StepTypeValidateNotRegex:
495 - result, err = validateNotRegex(value, step.Params)
496 - case StepTypeValidateNotSupported:
497 - result, err = validateNotSupported(value, step.Params)
498 - case StepTypeJSONPath:
499 - result, err = jsonpathExtract(value, step.Params)
500 - case StepTypeXPath:
501 - result, err = xpathExtract(value, step.Params)
502 - case StepTypePrometheusPattern:
503 - result, err = prometheusPattern(value, step.Params)
504 - case StepTypePrometheusToJSON:
505 - result, err = prometheusToJSON(value, step.Params)
506 - case StepTypeCSVToJSON:
507 - result, err = csvToJSON(value, step.Params)
508 - case StepTypeXMLToJSON:
509 - result, err = xmlToJSON(value, step.Params)
510 - case StepTypeErrorFieldJSON:
511 - result, err = errorFieldJSON(value, step.Params)
512 - case StepTypeErrorFieldXML:
513 - result, err = errorFieldXML(value, step.Params)
514 - case StepTypeErrorFieldRegex:
515 - result, err = errorFieldRegex(value, step.Params)
516 - case StepTypeThrottleValue:
517 - result, err = p.throttleValue(itemID, value, step.Params)
518 - case StepTypeThrottleTimedValue:
519 - result, err = p.throttleTimedValue(itemID, value, step.Params)
520 - case StepTypeJavaScript:
521 - result, err = javascriptExecute(value, step.Params, p.limits.JavaScript)
522 - case StepTypeSNMPWalkValue:
523 - result, err = snmpWalkToValue(value, step.Params)
524 - case StepTypeSNMPGetValue:
525 - result, err = snmpGetValue(value, step.Params)
526 - case StepTypeSNMPWalkToJSON:
527 - result, err = snmpWalkToJSON(value, step.Params, p.logger)
528 - default:
529 - return Result{Error: fmt.Errorf("unsupported preprocessing type: %d", step.Type)}, fmt.Errorf("unsupported preprocessing type: %d", step.Type)
530 - }
531 -
532 - return valueToResult(result, err), err
533 -}
534 -
535 -func (p *Preprocessor) handleError(err error, handler ErrorHandler) (Value, error) {
536 - switch handler.Action {
537 - case ErrorActionDefault:
538 - return Value{}, err
539 - case ErrorActionDiscard:
540 - // Zabbix behavior: discard means replace error with empty string value
541 - return Value{Data: "", Type: ValueTypeStr}, nil
542 - case ErrorActionSetValue:
543 - return Value{Data: handler.Params, Type: ValueTypeStr}, nil
544 - case ErrorActionSetError:
545 - return Value{}, fmt.Errorf("%s", handler.Params)
546 - default:
547 - return Value{}, err
548 - }
549 -}
550 -
551 -func validateStep(step Step) error {
552 - if step.Type < 0 {
553 - return fmt.Errorf("invalid step type: %d", step.Type)
554 - }
555 - return nil
556 -}
557 -
558 -func parseValueType(s string) (ValueType, error) {
559 - switch s {
560 - case "ITEM_VALUE_TYPE_STR":
561 - return ValueTypeStr, nil
562 - case "ITEM_VALUE_TYPE_UINT64":
563 - return ValueTypeUint64, nil
564 - case "ITEM_VALUE_TYPE_FLOAT":
565 - return ValueTypeFloat, nil
566 - default:
567 - return ValueTypeStr, fmt.Errorf("unknown value type: %s", s)
568 - }
569 -}
570 -
571 -func valueTypeToString(vt ValueType) string {
572 - switch vt {
573 - case ValueTypeStr:
574 - return "ITEM_VALUE_TYPE_STR"
575 - case ValueTypeUint64:
576 - return "ITEM_VALUE_TYPE_UINT64"
577 - case ValueTypeFloat:
578 - return "ITEM_VALUE_TYPE_FLOAT"
579 - default:
580 - return "UNKNOWN"
581 - }
582 -}
src/go/plugin/scripts.d/pkg/zabbixpreproc/preproc_pipeline_test.go deleted
-22
@@ -1,22 +0,0 @@
1 -package zabbixpreproc
2 -
3 -import "testing"
4 -
5 -func TestExecutePipelineCSVToJSONMultiChained(t *testing.T) {
6 - proc := NewPreprocessor("csv-multi-test")
7 - steps := []Step{
8 - {Type: StepTypeCSVToJSONMulti, Params: ",\n\"\n1"},
9 - {Type: StepTypeJavaScript, Params: `var row = JSON.parse(value); value = row["value"]; return value;`},
10 - }
11 - payload := "name,value\nrow1,10\nrow2,20"
12 - res, err := proc.ExecutePipeline("item1", Value{Data: payload, Type: ValueTypeStr}, steps)
13 - if err != nil {
14 - t.Fatalf("ExecutePipeline error: %v", err)
15 - }
16 - if len(res.Metrics) != 2 {
17 - t.Fatalf("expected 2 metrics, got %d", len(res.Metrics))
18 - }
19 - if res.Metrics[0].Value != "10" || res.Metrics[1].Value != "20" {
20 - t.Fatalf("unexpected values: %+v", res.Metrics)
21 - }
22 -}
src/go/plugin/scripts.d/pkg/zabbixpreproc/preproc_race_test.go deleted
-266
@@ -1,266 +0,0 @@
1 -package zabbixpreproc
2 -
3 -import (
4 - "fmt"
5 - "sync"
6 - "testing"
7 - "time"
8 -)
9 -
10 -// TestConcurrentDeltaValue tests concurrent access to deltaValue operation
11 -func TestConcurrentDeltaValue(t *testing.T) {
12 - p := NewPreprocessor("test-shard")
13 - step := Step{
14 - Type: StepTypeDeltaValue,
15 - Params: "",
16 - }
17 -
18 - var wg sync.WaitGroup
19 - goroutines := 100
20 - iterations := 10
21 -
22 - for i := 0; i < goroutines; i++ {
23 - wg.Add(1)
24 - go func(id int) {
25 - defer wg.Done()
26 - itemID := fmt.Sprintf("item-%d", id) // Each goroutine uses different itemID
27 - for j := 0; j < iterations; j++ {
28 - value := Value{
29 - Data: fmt.Sprintf("%d", id*iterations+j),
30 - Type: ValueTypeFloat,
31 - Timestamp: time.Now(),
32 - }
33 - _, err := p.Execute(itemID, value, step)
34 - // First call returns error (no previous value), subsequent calls should succeed
35 - if err != nil && j > 0 {
36 - t.Errorf("goroutine %d iteration %d: unexpected error: %v", id, j, err)
37 - }
38 - }
39 - }(i)
40 - }
41 -
42 - wg.Wait()
43 -}
44 -
45 -// TestConcurrentDeltaSpeed tests concurrent access to deltaSpeed operation
46 -func TestConcurrentDeltaSpeed(t *testing.T) {
47 - p := NewPreprocessor("test-shard")
48 - step := Step{
49 - Type: StepTypeDeltaSpeed,
50 - Params: "",
51 - }
52 -
53 - var wg sync.WaitGroup
54 - goroutines := 100
55 - iterations := 10
56 -
57 - baseTime := time.Now()
58 -
59 - for i := 0; i < goroutines; i++ {
60 - wg.Add(1)
61 - go func(id int) {
62 - itemID := fmt.Sprintf("item-%d", id)
63 - defer wg.Done()
64 - for j := 0; j < iterations; j++ {
65 - value := Value{
66 - Data: fmt.Sprintf("%d", id*iterations+j),
67 - Type: ValueTypeFloat,
68 - Timestamp: baseTime.Add(time.Duration(id*iterations+j) * time.Second),
69 - }
70 - _, err := p.Execute(itemID, value, step)
71 - // First call returns error (no previous value), subsequent calls should succeed
72 - if err != nil && j > 0 {
73 - t.Errorf("goroutine %d iteration %d: unexpected error: %v", id, j, err)
74 - }
75 - }
76 - }(i)
77 - }
78 -
79 - wg.Wait()
80 -}
81 -
82 -// TestConcurrentThrottleValue tests concurrent access to throttleValue operation
83 -func TestConcurrentThrottleValue(t *testing.T) {
84 - p := NewPreprocessor("test-shard")
85 - step := Step{
86 - Type: StepTypeThrottleValue,
87 - Params: "",
88 - }
89 -
90 - var wg sync.WaitGroup
91 - goroutines := 100
92 - iterations := 10
93 -
94 - for i := 0; i < goroutines; i++ {
95 - wg.Add(1)
96 - go func(id int) {
97 - itemID := fmt.Sprintf("item-%d", id)
98 - defer wg.Done()
99 - for j := 0; j < iterations; j++ {
100 - value := Value{
101 - Data: fmt.Sprintf("value-%d-%d", id, j),
102 - Type: ValueTypeStr,
103 - Timestamp: time.Now(),
104 - }
105 - _, err := p.Execute(itemID, value, step)
106 - if err != nil {
107 - t.Errorf("goroutine %d iteration %d: unexpected error: %v", id, j, err)
108 - }
109 - }
110 - }(i)
111 - }
112 -
113 - wg.Wait()
114 -}
115 -
116 -// TestConcurrentThrottleTimedValue tests concurrent access to throttleTimedValue operation
117 -func TestConcurrentThrottleTimedValue(t *testing.T) {
118 - p := NewPreprocessor("test-shard")
119 - step := Step{
120 - Type: StepTypeThrottleTimedValue,
121 - Params: "1s",
122 - }
123 -
124 - var wg sync.WaitGroup
125 - goroutines := 100
126 - iterations := 10
127 -
128 - baseTime := time.Now()
129 -
130 - for i := 0; i < goroutines; i++ {
131 - wg.Add(1)
132 - go func(id int) {
133 - itemID := fmt.Sprintf("item-%d", id)
134 - defer wg.Done()
135 - for j := 0; j < iterations; j++ {
136 - value := Value{
137 - Data: fmt.Sprintf("value-%d-%d", id, j),
138 - Type: ValueTypeStr,
139 - Timestamp: baseTime.Add(time.Duration(id*iterations+j) * time.Millisecond * 100),
140 - }
141 - _, err := p.Execute(itemID, value, step)
142 - if err != nil {
143 - t.Errorf("goroutine %d iteration %d: unexpected error: %v", id, j, err)
144 - }
145 - }
146 - }(i)
147 - }
148 -
149 - wg.Wait()
150 -}
151 -
152 -// TestConcurrentMixedOperations tests concurrent access to mixed stateful operations
153 -func TestConcurrentMixedOperations(t *testing.T) {
154 - p := NewPreprocessor("test-shard")
155 -
156 - steps := []Step{
157 - {Type: StepTypeDeltaValue, Params: ""},
158 - {Type: StepTypeDeltaSpeed, Params: ""},
159 - {Type: StepTypeThrottleValue, Params: ""},
160 - {Type: StepTypeThrottleTimedValue, Params: "1s"},
161 - }
162 -
163 - var wg sync.WaitGroup
164 - goroutines := 50
165 - iterations := 10
166 -
167 - baseTime := time.Now()
168 -
169 - for i := 0; i < goroutines; i++ {
170 - wg.Add(1)
171 - go func(id int) {
172 - itemID := fmt.Sprintf("item-%d", id)
173 - defer wg.Done()
174 - step := steps[id%len(steps)]
175 - for j := 0; j < iterations; j++ {
176 - value := Value{
177 - Data: fmt.Sprintf("%d", id*iterations+j),
178 - Type: ValueTypeFloat,
179 - Timestamp: baseTime.Add(time.Duration(id*iterations+j) * time.Millisecond * 50),
180 - }
181 - _, err := p.Execute(itemID, value, step)
182 - // Delta operations fail on first call (no previous value)
183 - if err != nil && (step.Type == StepTypeDeltaValue || step.Type == StepTypeDeltaSpeed) && j > 0 {
184 - t.Errorf("goroutine %d iteration %d: unexpected error: %v", id, j, err)
185 - }
186 - }
187 - }(i)
188 - }
189 -
190 - wg.Wait()
191 -}
192 -
193 -// TestConcurrentPipelines tests concurrent execution of preprocessing pipelines
194 -func TestConcurrentPipelines(t *testing.T) {
195 - p := NewPreprocessor("test-shard")
196 -
197 - // Pipeline with multiple stateful operations
198 - pipeline := []Step{
199 - {Type: StepTypeTrim, Params: " "},
200 - {Type: StepTypeDeltaValue, Params: ""},
201 - {Type: StepTypeThrottleValue, Params: ""},
202 - }
203 -
204 - var wg sync.WaitGroup
205 - goroutines := 50
206 - iterations := 10
207 -
208 - for i := 0; i < goroutines; i++ {
209 - wg.Add(1)
210 - go func(id int) {
211 - itemID := fmt.Sprintf("item-%d", id)
212 - defer wg.Done()
213 - for j := 0; j < iterations; j++ {
214 - value := Value{
215 - Data: fmt.Sprintf(" %d ", id*iterations+j),
216 - Type: ValueTypeFloat,
217 - Timestamp: time.Now(),
218 - }
219 - _, err := p.ExecutePipeline(itemID, value, pipeline)
220 - // First call will fail at deltaValue (no previous value)
221 - if err != nil && j > 0 {
222 - t.Errorf("goroutine %d iteration %d: unexpected error: %v", id, j, err)
223 - }
224 - }
225 - }(i)
226 - }
227 -
228 - wg.Wait()
229 -}
230 -
231 -// TestConcurrentMultiplePreprocessors tests that separate preprocessor instances are isolated
232 -func TestConcurrentMultiplePreprocessors(t *testing.T) {
233 - preprocessors := make([]*Preprocessor, 10)
234 - for i := range preprocessors {
235 - preprocessors[i] = NewPreprocessor(fmt.Sprintf("shard-%d", i))
236 - }
237 -
238 - step := Step{
239 - Type: StepTypeDeltaValue,
240 - Params: "",
241 - }
242 -
243 - var wg sync.WaitGroup
244 -
245 - for i, p := range preprocessors {
246 - wg.Add(1)
247 - go func(id int, preprocessor *Preprocessor) {
248 - itemID := fmt.Sprintf("item-%d", id)
249 - defer wg.Done()
250 - for j := 0; j < 100; j++ {
251 - value := Value{
252 - Data: fmt.Sprintf("%d", j),
253 - Type: ValueTypeFloat,
254 - Timestamp: time.Now(),
255 - }
256 - _, err := preprocessor.Execute(itemID, value, step)
257 - // First call returns error (no previous value)
258 - if err != nil && j > 0 {
259 - t.Errorf("preprocessor %d iteration %d: unexpected error: %v", id, j, err)
260 - }
261 - }
262 - }(i, p)
263 - }
264 -
265 - wg.Wait()
266 -}
src/go/plugin/scripts.d/pkg/zabbixpreproc/preproc_test.go deleted
-117
@@ -1,117 +0,0 @@
1 -package zabbixpreproc
2 -
3 -import (
4 - "fmt"
5 - "testing"
6 -)
7 -
8 -func TestZabbixTestSuite(t *testing.T) {
9 - // Load ALL official Zabbix test suites
10 - testCases, err := LoadAllTestCases()
11 - if err != nil {
12 - t.Fatalf("Failed to load test cases: %v", err)
13 - }
14 -
15 - passed := 0
16 - failed := 0
17 - skipped := 0
18 -
19 - for _, tc := range testCases {
20 - // Capture tc in closure for t.Run
21 - tc := tc
22 - t.Run(tc.Name, func(t *testing.T) {
23 - // Create a fresh preprocessor for each test to isolate state
24 - preprocessor := NewPreprocessor("test-shard")
25 - // Parse test input
26 - value, err := ValueFromTestInput(tc)
27 - if err != nil {
28 - t.Skipf("Failed to parse input: %v", err)
29 - skipped++
30 - return
31 - }
32 -
33 - step, err := StepFromTestInput(tc)
34 - if err != nil {
35 - t.Skipf("Failed to parse step: %v", err)
36 - skipped++
37 - return
38 - }
39 -
40 - // Load history value if provided (for stateful operations like delta, throttle)
41 - historyVal := HistoryValueFromTestInput(tc)
42 - itemID := "test-item" // Use consistent itemID for tests
43 - if historyVal != nil {
44 - // Pre-populate state through public API (no direct state manipulation)
45 - if err := preprocessor.PreloadState(itemID, step.Type, historyVal.Data, historyVal.Timestamp); err != nil {
46 - // Not a stateful operation, ignore
47 - t.Logf("Note: history value provided but step type %d is not stateful", step.Type)
48 - }
49 - }
50 -
51 - // Execute preprocessing with itemID
52 - result, err := preprocessor.Execute(itemID, value, step)
53 -
54 - // Check expected outcome
55 - if tc.Out.Return == "SUCCEED" {
56 - if err != nil {
57 - t.Errorf("Expected success but got error: %v", err)
58 - failed++
59 - return
60 - }
61 -
62 - // Extract result value from first metric
63 - if len(result.Metrics) == 0 {
64 - t.Errorf("No metrics returned")
65 - failed++
66 - return
67 - }
68 - resultData := result.Metrics[0].Value
69 -
70 - expected, _ := ExpectedOutputFromTestCase(tc)
71 - if resultData != expected {
72 - // Debug: print input details for trim tests
73 - if step.Type == StepTypeRTrim || step.Type == StepTypeLTrim || step.Type == StepTypeTrim {
74 - paramsDesc := ""
75 - for i, ch := range step.Params {
76 - if i > 0 {
77 - paramsDesc += ", "
78 - }
79 - paramsDesc += fmt.Sprintf("%c(0x%x)", ch, ch)
80 - }
81 - t.Logf("DEBUG: input='%s' params='%s'[%s] (len=%d) expected='%s' (len=%d) got='%s' (len=%d)", value.Data, step.Params, paramsDesc, len(step.Params), expected, len(expected), resultData, len(resultData))
82 - }
83 - t.Errorf("Expected '%s' but got '%s'", expected, resultData)
84 - failed++
85 - return
86 - }
87 -
88 - passed++
89 - } else { // FAIL expected
90 - if err == nil {
91 - resultData := ""
92 - if len(result.Metrics) > 0 {
93 - resultData = result.Metrics[0].Value
94 - }
95 - t.Errorf("Expected error but succeeded with '%s'", resultData)
96 - failed++
97 - return
98 - }
99 -
100 - passed++
101 - }
102 - })
103 - }
104 -
105 - fmt.Printf("\n=== Test Results ===\n")
106 - fmt.Printf("Passed: %d\n", passed)
107 - fmt.Printf("Failed: %d\n", failed)
108 - fmt.Printf("Skipped: %d\n", skipped)
109 - fmt.Printf("Total: %d\n", passed+failed+skipped)
110 - if passed+failed > 0 {
111 - fmt.Printf("Pass Rate: %.1f%%\n", float64(passed)*100/float64(passed+failed))
112 - }
113 -
114 - if failed > 0 {
115 - t.Fatalf("%d tests failed", failed)
116 - }
117 -}
src/go/plugin/scripts.d/pkg/zabbixpreproc/prometheus_step.go deleted
-676
@@ -1,676 +0,0 @@
1 -package zabbixpreproc
2 -
3 -import (
4 - "encoding/json"
5 - "fmt"
6 - "regexp"
7 - "strconv"
8 - "strings"
9 -)
10 -
11 -// Prometheus regex patterns compiled once at package init
12 -var (
13 - // Format: metric_name{label="value"} number
14 - prometheusMetricWithLabelsRegex = regexp.MustCompile(`^([a-zA-Z_:][a-zA-Z0-9_:]*)\{([^}]*)\}\s+([-+]?[\d.eE+-]+)`)
15 - // Format: metric_name number
16 - prometheusMetricWithoutLabelsRegex = regexp.MustCompile(`^([a-zA-Z_:][a-zA-Z0-9_:]*)\s+([-+]?[\d.eE+-]+)`)
17 - // Format: label_name="label_value" (NOTE: this regex doesn't handle escaped quotes)
18 - // Use parsePrometheusLabels() for proper escape handling
19 - prometheusLabelRegex = regexp.MustCompile(`([a-zA-Z_][a-zA-Z0-9_]*)="([^"]*)"`)
20 -)
21 -
22 -// parsePrometheusLabels parses Prometheus label string with proper escape handling.
23 -// Handles \", \\, and \n escape sequences per Prometheus spec.
24 -// Input: `method="GET",path="/foo\"bar",code="200"`
25 -// Returns map and order of labels.
26 -func parsePrometheusLabels(labelsStr string) (map[string]string, []string) {
27 - labels := make(map[string]string)
28 - var labelOrder []string
29 -
30 - if strings.TrimSpace(labelsStr) == "" {
31 - return labels, labelOrder
32 - }
33 -
34 - i := 0
35 - n := len(labelsStr)
36 -
37 - for i < n {
38 - // Skip whitespace and commas
39 - for i < n && (labelsStr[i] == ' ' || labelsStr[i] == ',' || labelsStr[i] == '\t') {
40 - i++
41 - }
42 - if i >= n {
43 - break
44 - }
45 -
46 - // Parse label name (alphanumeric + underscore, starts with letter or underscore)
47 - nameStart := i
48 - for i < n && (labelsStr[i] == '_' || (labelsStr[i] >= 'a' && labelsStr[i] <= 'z') ||
49 - (labelsStr[i] >= 'A' && labelsStr[i] <= 'Z') || (labelsStr[i] >= '0' && labelsStr[i] <= '9')) {
50 - i++
51 - }
52 - if i == nameStart {
53 - // No valid label name found, skip rest
54 - break
55 - }
56 - labelName := labelsStr[nameStart:i]
57 -
58 - // Skip whitespace
59 - for i < n && (labelsStr[i] == ' ' || labelsStr[i] == '\t') {
60 - i++
61 - }
62 -
63 - // Expect '='
64 - if i >= n || labelsStr[i] != '=' {
65 - break
66 - }
67 - i++
68 -
69 - // Skip whitespace
70 - for i < n && (labelsStr[i] == ' ' || labelsStr[i] == '\t') {
71 - i++
72 - }
73 -
74 - // Expect opening quote
75 - if i >= n || labelsStr[i] != '"' {
76 - break
77 - }
78 - i++
79 -
80 - // Parse label value with escape handling
81 - var valueBuilder strings.Builder
82 - for i < n {
83 - if labelsStr[i] == '\\' && i+1 < n {
84 - // Escape sequence
85 - nextChar := labelsStr[i+1]
86 - switch nextChar {
87 - case '"':
88 - valueBuilder.WriteByte('"')
89 - i += 2
90 - case '\\':
91 - valueBuilder.WriteByte('\\')
92 - i += 2
93 - case 'n':
94 - valueBuilder.WriteByte('\n')
95 - i += 2
96 - default:
97 - // Unknown escape, keep as-is
98 - valueBuilder.WriteByte(labelsStr[i])
99 - i++
100 - }
101 - } else if labelsStr[i] == '"' {
102 - // End of value
103 - i++
104 - break
105 - } else {
106 - valueBuilder.WriteByte(labelsStr[i])
107 - i++
108 - }
109 - }
110 -
111 - labels[labelName] = valueBuilder.String()
112 - labelOrder = append(labelOrder, labelName)
113 - }
114 -
115 - return labels, labelOrder
116 -}
117 -
118 -// prometheusMetric represents a parsed Prometheus metric
119 -type prometheusMetric struct {
120 - Name string
121 - Labels map[string]string
122 - LabelOrder []string // Order of label keys as they appear in input
123 - Value float64
124 - ValueStr string // Original string representation of value
125 - LineRaw string
126 -}
127 -
128 -// parsePrometheusText parses Prometheus text exposition format.
129 -//
130 -// Parses metrics in Prometheus text format as defined by the Prometheus documentation.
131 -// Supports:
132 -// - Metrics with labels: http_requests_total{method="GET",code="200"} 1027
133 -// - Metrics without labels: process_cpu_seconds 4.2
134 -// - Scientific notation: very_large_value 1.23e+10
135 -// - Comment lines (ignored): # HELP and # TYPE comments
136 -// - Label validation: ensures all labels match pattern label_name="value"
137 -//
138 -// Example Prometheus format:
139 -//
140 -// # HELP http_requests_total Total HTTP requests
141 -// # TYPE http_requests_total counter
142 -// http_requests_total{method="GET"} 100
143 -// http_requests_total{method="POST"} 50
144 -// memory_usage_bytes 1024
145 -//
146 -// Parameters:
147 -// - data: Prometheus text format data (one metric per line)
148 -//
149 -// Returns:
150 -// - []prometheusMetric: Parsed metrics with names, labels, and values
151 -// - error: Returns nil on success (invalid lines are silently skipped per Prometheus spec)
152 -//
153 -// Note: This parser uses pre-compiled package-level regexes for performance.
154 -func parsePrometheusText(data string) ([]prometheusMetric, error) {
155 - var metrics []prometheusMetric
156 - lines := strings.Split(data, "\n")
157 -
158 - for _, line := range lines {
159 - originalLine := line
160 - line = strings.TrimSpace(line)
161 - if line == "" || strings.HasPrefix(line, "#") {
162 - continue
163 - }
164 -
165 - var metricName, labelsStr, valueStr string
166 - var labels map[string]string
167 -
168 - var labelOrder []string
169 -
170 - // Try with labels first
171 - matches := prometheusMetricWithLabelsRegex.FindStringSubmatch(line)
172 - if len(matches) >= 4 {
173 - metricName = matches[1]
174 - labelsStr = matches[2]
175 - valueStr = matches[3]
176 -
177 - labels = make(map[string]string)
178 -
179 - // Parse labels with proper escape handling (\", \\, \n)
180 - if strings.TrimSpace(labelsStr) != "" {
181 - labels, labelOrder = parsePrometheusLabels(labelsStr)
182 - if len(labels) == 0 {
183 - // Labels present but none valid - invalid syntax
184 - continue
185 - }
186 - }
187 - } else {
188 - // Try without labels
189 - matches = prometheusMetricWithoutLabelsRegex.FindStringSubmatch(line)
190 - if len(matches) < 3 {
191 - continue
192 - }
193 - metricName = matches[1]
194 - valueStr = matches[2]
195 - labels = make(map[string]string)
196 - }
197 -
198 - value, err := strconv.ParseFloat(valueStr, 64)
199 - if err != nil {
200 - continue
201 - }
202 -
203 - metrics = append(metrics, prometheusMetric{
204 - Name: metricName,
205 - Labels: labels,
206 - LabelOrder: labelOrder,
207 - Value: value,
208 - ValueStr: valueStr,
209 - LineRaw: strings.TrimSpace(originalLine),
210 - })
211 - }
212 -
213 - return metrics, nil
214 -}
215 -
216 -// parsePrometheusMetadata extracts HELP and TYPE comments from Prometheus text
217 -func parsePrometheusMetadata(data string) (map[string]string, map[string]string) {
218 - helpMap := make(map[string]string)
219 - typeMap := make(map[string]string)
220 -
221 - lines := strings.Split(data, "\n")
222 - for _, line := range lines {
223 - line = strings.TrimSpace(line)
224 - if !strings.HasPrefix(line, "#") {
225 - continue
226 - }
227 -
228 - // Parse # HELP metric_name description
229 - if strings.HasPrefix(line, "# HELP ") {
230 - parts := strings.SplitN(line[7:], " ", 2)
231 - if len(parts) == 2 {
232 - helpMap[parts[0]] = parts[1]
233 - }
234 - }
235 -
236 - // Parse # TYPE metric_name type
237 - if strings.HasPrefix(line, "# TYPE ") {
238 - parts := strings.SplitN(line[7:], " ", 2)
239 - if len(parts) == 2 {
240 - typeMap[parts[0]] = parts[1]
241 - }
242 - }
243 - }
244 -
245 - return helpMap, typeMap
246 -}
247 -
248 -// prometheusPattern extracts metric values matching a label pattern.
249 -func prometheusPattern(value Value, paramStr string) (Value, error) {
250 - // paramStr format: "metric_name{label_selectors}\noutput\nlabel_name"
251 - // output can be "value", "label"
252 - // If output is "label" and label_name is provided, return that specific label
253 - parts := strings.SplitN(paramStr, "\n", 3)
254 - if len(parts) < 1 {
255 - return Value{}, fmt.Errorf("prometheus pattern requires metric pattern")
256 - }
257 -
258 - // Parse metric pattern (e.g., "cpu_usage_system{cpu=\"cpu-total\",host=~\".*\"}")
259 - metricPattern := strings.TrimSpace(parts[0])
260 - outputType := "value" // Default
261 - labelName := ""
262 - if len(parts) > 1 {
263 - outputType = strings.TrimSpace(parts[1])
264 - }
265 - if len(parts) > 2 {
266 - labelName = strings.TrimSpace(parts[2])
267 - }
268 -
269 - // Extract metric name, label selectors, and value comparison from pattern
270 - metricName, labelSelectors, valueComp := parseMetricPattern(metricPattern)
271 -
272 - // Parse Prometheus text format
273 - metrics, err := parsePrometheusText(value.Data)
274 - if err != nil {
275 - return Value{}, fmt.Errorf("failed to parse prometheus data: %w", err)
276 - }
277 -
278 - // Find matching metric
279 - for _, m := range metrics {
280 - // Check if metric name matches
281 - // Priority: __name__ selector > explicit metric name > no filter
282 - nameSelector, hasNameSelector := labelSelectors["__name__"]
283 - if hasNameSelector {
284 - // Use __name__ selector to match metric name
285 - matched := false
286 - switch nameSelector.operator {
287 - case "=":
288 - matched = m.Name == nameSelector.value
289 - case "=~":
290 - re, err := compileRegexSafe(nameSelector.value, 0)
291 - if err != nil {
292 - continue
293 - }
294 - matched, err = matchWithTimeout(re, m.Name, defaultRegexTimeout)
295 - if err != nil {
296 - continue
297 - }
298 - case "!=":
299 - matched = m.Name != nameSelector.value
300 - }
301 - if !matched {
302 - continue
303 - }
304 - } else if metricName != "" {
305 - // Use metric name from pattern
306 - if !matchesPattern(m.Name, metricName) {
307 - continue
308 - }
309 - }
310 -
311 - // Filter out __name__ from label selectors (already checked above)
312 - actualLabelSelectors := make(map[string]labelSelector)
313 - for k, v := range labelSelectors {
314 - if k != "__name__" {
315 - actualLabelSelectors[k] = v
316 - }
317 - }
318 -
319 - if !matchesLabelSelectors(m.Labels, actualLabelSelectors) {
320 - continue
321 - }
322 -
323 - // Check value comparison if present
324 - if valueComp != nil {
325 - if !matchesValueComparison(m.Value, valueComp) {
326 - continue
327 - }
328 - }
329 -
330 - // Return based on output type
331 - switch outputType {
332 - case "value":
333 - return Value{Data: fmt.Sprintf("%g", m.Value), Type: ValueTypeStr}, nil
334 - case "label":
335 - if labelName != "" {
336 - // Return specific label value
337 - if val, ok := m.Labels[labelName]; ok {
338 - return Value{Data: val, Type: ValueTypeStr}, nil
339 - }
340 - return Value{}, fmt.Errorf("label %s not found", labelName)
341 - }
342 - // Return all labels as comma-separated key=value pairs
343 - var labelPairs []string
344 - for k, v := range m.Labels {
345 - labelPairs = append(labelPairs, fmt.Sprintf("%s=%s", k, v))
346 - }
347 - return Value{Data: strings.Join(labelPairs, ","), Type: ValueTypeStr}, nil
348 - default:
349 - // Unknown output type
350 - return Value{}, fmt.Errorf("unknown output type: %s", outputType)
351 - }
352 - }
353 -
354 - return Value{}, fmt.Errorf("prometheus pattern did not match any metrics")
355 -}
356 -
357 -// prometheusToJSON converts Prometheus text format to JSON.
358 -// prometheusToJSONMulti converts Prometheus text format to multiple Result metrics
359 -// Each Prometheus metric becomes a separate Result.Metric with labels
360 -func prometheusToJSONMulti(value Value, paramStr string) (Result, error) {
361 - // Parse Prometheus text format
362 - metrics, err := parsePrometheusText(value.Data)
363 - if err != nil {
364 - return Result{Error: err}, err
365 - }
366 -
367 - // If input is non-empty but no metrics were parsed, it's invalid data
368 - if len(metrics) == 0 && strings.TrimSpace(value.Data) != "" {
369 - err := fmt.Errorf("failed to parse prometheus data: no valid metrics found")
370 - return Result{Error: err}, err
371 - }
372 -
373 - // Convert each Prometheus metric to Result.Metric
374 - resultMetrics := make([]Metric, 0, len(metrics))
375 - for _, pm := range metrics {
376 - resultMetrics = append(resultMetrics, Metric{
377 - Name: pm.Name,
378 - Value: pm.ValueStr,
379 - Type: ValueTypeStr,
380 - Labels: pm.Labels, // Prometheus labels preserved
381 - })
382 - }
383 -
384 - return Result{Metrics: resultMetrics}, nil
385 -}
386 -
387 -func prometheusToJSON(value Value, paramStr string) (Value, error) {
388 - // Parse Prometheus text format
389 - metrics, err := parsePrometheusText(value.Data)
390 - if err != nil {
391 - return Value{}, fmt.Errorf("failed to parse prometheus data: %w", err)
392 - }
393 -
394 - // If input is non-empty but no metrics were parsed, it's invalid data
395 - if len(metrics) == 0 && strings.TrimSpace(value.Data) != "" {
396 - return Value{}, fmt.Errorf("failed to parse Prometheus data: no valid metrics found")
397 - }
398 -
399 - // Parse metadata (HELP and TYPE comments)
400 - helpMap, typeMap := parsePrometheusMetadata(value.Data)
401 -
402 - // Build result array with ordered fields
403 - var jsonParts []string
404 -
405 - for _, m := range metrics {
406 - // Build JSON manually to maintain field order: name, value, line_raw, labels, type, help
407 - var parts []string
408 -
409 - parts = append(parts, fmt.Sprintf(`"name":%s`, jsonString(m.Name)))
410 - parts = append(parts, fmt.Sprintf(`"value":%s`, jsonString(m.ValueStr)))
411 - parts = append(parts, fmt.Sprintf(`"line_raw":%s`, jsonString(m.LineRaw)))
412 -
413 - if len(m.Labels) > 0 {
414 - // Build labels JSON with preserved order
415 - var labelPairs []string
416 - for _, key := range m.LabelOrder {
417 - labelPairs = append(labelPairs, fmt.Sprintf(`%s:%s`, jsonString(key), jsonString(m.Labels[key])))
418 - }
419 - parts = append(parts, fmt.Sprintf(`"labels":{%s}`, strings.Join(labelPairs, ",")))
420 - }
421 -
422 - // Add type (default to "untyped")
423 - metricType := "untyped"
424 - if t, ok := typeMap[m.Name]; ok {
425 - metricType = t
426 - }
427 - parts = append(parts, fmt.Sprintf(`"type":%s`, jsonString(metricType)))
428 -
429 - // Add help if available
430 - if helpText, ok := helpMap[m.Name]; ok {
431 - parts = append(parts, fmt.Sprintf(`"help":%s`, jsonString(helpText)))
432 - }
433 -
434 - jsonParts = append(jsonParts, "{"+strings.Join(parts, ",")+"}")
435 - }
436 -
437 - resultJSON := "[" + strings.Join(jsonParts, ",") + "]"
438 - return Value{Data: resultJSON, Type: ValueTypeStr}, nil
439 -}
440 -
441 -// jsonString escapes a string for JSON
442 -func jsonString(s string) string {
443 - b, _ := json.Marshal(s)
444 - return string(b)
445 -}
446 -
447 -// matchesPattern checks if a string matches a simple wildcard pattern
448 -func matchesPattern(s, pattern string) bool {
449 - // Zabbix uses simple glob patterns, convert to regex
450 - regexPattern := regexp.QuoteMeta(pattern)
451 - regexPattern = strings.ReplaceAll(regexPattern, `\*`, ".*")
452 - regexPattern = strings.ReplaceAll(regexPattern, `\?`, ".")
453 - regexPattern = "^" + regexPattern + "$"
454 - re, err := compileRegexSafe(regexPattern, 0)
455 - if err != nil {
456 - return false
457 - }
458 - matched, err := matchWithTimeout(re, s, defaultRegexTimeout)
459 - if err != nil {
460 - return false
461 - }
462 - return matched
463 -}
464 -
465 -// parseMetricPattern parses a metric pattern like "metric_name{label=\"value\",label2=~\"regex\"} == 123.45"
466 -// Returns the metric name, label selectors, and optional value comparison
467 -func parseMetricPattern(pattern string) (string, map[string]labelSelector, *valueComparison) {
468 - // Check for value comparison operators (==, !=, >, <, >=, <=)
469 - var valueComp *valueComparison
470 -
471 - // Find value comparison operator outside of braces
472 - braceDepth := 0
473 - var compIdx int = -1
474 - var compOp string
475 -
476 - for i := 0; i < len(pattern); i++ {
477 - if pattern[i] == '{' {
478 - braceDepth++
479 - } else if pattern[i] == '}' {
480 - braceDepth--
481 - } else if braceDepth == 0 {
482 - // Check for comparison operators outside braces
483 - if i+2 <= len(pattern) {
484 - twoChar := pattern[i:min(i+2, len(pattern))]
485 - if twoChar == "==" || twoChar == "!=" || twoChar == ">=" || twoChar == "<=" {
486 - compOp = twoChar
487 - compIdx = i
488 - break
489 - }
490 - }
491 - if pattern[i] == '>' || pattern[i] == '<' {
492 - compOp = string(pattern[i])
493 - compIdx = i
494 - break
495 - }
496 - }
497 - }
498 -
499 - // Extract value comparison if present
500 - if compIdx != -1 {
501 - valueStr := strings.TrimSpace(pattern[compIdx+len(compOp):])
502 - pattern = strings.TrimSpace(pattern[:compIdx])
503 - if valueStr != "" {
504 - val, err := strconv.ParseFloat(valueStr, 64)
505 - if err == nil {
506 - valueComp = &valueComparison{
507 - operator: compOp,
508 - value: val,
509 - }
510 - }
511 - }
512 - }
513 -
514 - // Find the opening brace
515 - braceIdx := strings.Index(pattern, "{")
516 - if braceIdx == -1 {
517 - // No labels specified
518 - return pattern, nil, valueComp
519 - }
520 -
521 - metricName := pattern[:braceIdx]
522 - labelsStr := pattern[braceIdx+1:]
523 -
524 - // Remove trailing }
525 - if strings.HasSuffix(labelsStr, "}") {
526 - labelsStr = labelsStr[:len(labelsStr)-1]
527 - }
528 -
529 - // Parse label selectors
530 - selectors := make(map[string]labelSelector)
531 - if labelsStr == "" {
532 - return metricName, selectors, valueComp
533 - }
534 -
535 - // Split by comma, but respect quoted strings
536 - labelParts := splitLabelSelectors(labelsStr)
537 - for _, part := range labelParts {
538 - part = strings.TrimSpace(part)
539 - if part == "" {
540 - continue
541 - }
542 -
543 - // Check for regex operator =~ or !=
544 - var op string
545 - var splitIdx int
546 - if idx := strings.Index(part, "=~"); idx != -1 {
547 - op = "=~"
548 - splitIdx = idx
549 - } else if idx := strings.Index(part, "!="); idx != -1 {
550 - op = "!="
551 - splitIdx = idx
552 - } else if idx := strings.Index(part, "="); idx != -1 {
553 - op = "="
554 - splitIdx = idx
555 - } else {
556 - continue
557 - }
558 -
559 - labelName := strings.TrimSpace(part[:splitIdx])
560 - labelValue := strings.TrimSpace(part[splitIdx+len(op):])
561 -
562 - // Remove quotes from value
563 - labelValue = strings.Trim(labelValue, "\"")
564 -
565 - selectors[labelName] = labelSelector{
566 - operator: op,
567 - value: labelValue,
568 - }
569 - }
570 -
571 - return metricName, selectors, valueComp
572 -}
573 -
574 -func min(a, b int) int {
575 - if a < b {
576 - return a
577 - }
578 - return b
579 -}
580 -
581 -// labelSelector represents a label matching condition
582 -type labelSelector struct {
583 - operator string // "=", "=~", "!="
584 - value string
585 -}
586 -
587 -// valueComparison represents a metric value comparison
588 -type valueComparison struct {
589 - operator string // "==", "!=", ">", "<", ">=", "<="
590 - value float64
591 -}
592 -
593 -// splitLabelSelectors splits label selectors by comma, respecting quotes
594 -func splitLabelSelectors(s string) []string {
595 - var parts []string
596 - var current strings.Builder
597 - inQuotes := false
598 -
599 - for i := 0; i < len(s); i++ {
600 - ch := s[i]
601 -
602 - if ch == '"' {
603 - inQuotes = !inQuotes
604 - current.WriteByte(ch)
605 - } else if ch == ',' && !inQuotes {
606 - parts = append(parts, current.String())
607 - current.Reset()
608 - } else {
609 - current.WriteByte(ch)
610 - }
611 - }
612 -
613 - if current.Len() > 0 {
614 - parts = append(parts, current.String())
615 - }
616 -
617 - return parts
618 -}
619 -
620 -// matchesLabelSelectors checks if labels match all selectors
621 -// Also checks metric name if __name__ selector is present
622 -func matchesLabelSelectors(labels map[string]string, selectors map[string]labelSelector) bool {
623 - if len(selectors) == 0 {
624 - return true
625 - }
626 -
627 - for labelName, selector := range selectors {
628 - labelValue, exists := labels[labelName]
629 -
630 - switch selector.operator {
631 - case "=":
632 - if !exists || labelValue != selector.value {
633 - return false
634 - }
635 - case "=~":
636 - if !exists {
637 - return false
638 - }
639 - // Treat as regex
640 - re, err := compileRegexSafe(selector.value, 0)
641 - if err != nil {
642 - return false
643 - }
644 - matched, err := matchWithTimeout(re, labelValue, defaultRegexTimeout)
645 - if err != nil || !matched {
646 - return false
647 - }
648 - case "!=":
649 - if exists && labelValue == selector.value {
650 - return false
651 - }
652 - }
653 - }
654 -
655 - return true
656 -}
657 -
658 -// matchesValueComparison checks if a metric value matches a comparison
659 -func matchesValueComparison(metricValue float64, comp *valueComparison) bool {
660 - switch comp.operator {
661 - case "==":
662 - return metricValue == comp.value
663 - case "!=":
664 - return metricValue != comp.value
665 - case ">":
666 - return metricValue > comp.value
667 - case "<":
668 - return metricValue < comp.value
669 - case ">=":
670 - return metricValue >= comp.value
671 - case "<=":
672 - return metricValue <= comp.value
673 - default:
674 - return false
675 - }
676 -}
src/go/plugin/scripts.d/pkg/zabbixpreproc/regex_safe.go deleted
-254
@@ -1,254 +0,0 @@
1 -package zabbixpreproc
2 -
3 -import (
4 - "context"
5 - "fmt"
6 - "regexp"
7 - "sync"
8 - "time"
9 -)
10 -
11 -// Regex safety constants
12 -const (
13 - maxRegexNestingDepth = 10 // Maximum nesting depth for groups
14 - defaultRegexTimeout = 5 * time.Second // Default timeout for regex operations
15 - regexCacheSize = 100 // Maximum cached patterns
16 -)
17 -
18 -// regexCache caches compiled regular expressions for performance
19 -type regexCache struct {
20 - mu sync.RWMutex
21 - cache map[string]*regexp.Regexp
22 - order []string // LRU tracking
23 -}
24 -
25 -var globalRegexCache = &regexCache{
26 - cache: make(map[string]*regexp.Regexp),
27 - order: make([]string, 0, regexCacheSize),
28 -}
29 -
30 -// validateRegexComplexity performs basic complexity checks on regex patterns
31 -// maxPatternLength of 0 means no limit
32 -func validateRegexComplexity(pattern string, maxPatternLength int) error {
33 - if maxPatternLength > 0 && len(pattern) > maxPatternLength {
34 - return fmt.Errorf("regex pattern too long: %d chars (max %d)",
35 - len(pattern), maxPatternLength)
36 - }
37 -
38 - // Count nesting depth of parentheses
39 - depth := 0
40 - maxDepth := 0
41 - escaped := false
42 -
43 - for _, ch := range pattern {
44 - if escaped {
45 - escaped = false
46 - continue
47 - }
48 -
49 - if ch == '\\' {
50 - escaped = true
51 - continue
52 - }
53 -
54 - if ch == '(' {
55 - depth++
56 - if depth > maxDepth {
57 - maxDepth = depth
58 - }
59 - } else if ch == ')' {
60 - depth--
61 - if depth < 0 {
62 - return fmt.Errorf("unbalanced parentheses in regex pattern")
63 - }
64 - }
65 - }
66 -
67 - if depth != 0 {
68 - return fmt.Errorf("unbalanced parentheses in regex pattern")
69 - }
70 -
71 - if maxDepth > maxRegexNestingDepth {
72 - return fmt.Errorf("regex nesting too deep: %d levels (max %d)",
73 - maxDepth, maxRegexNestingDepth)
74 - }
75 -
76 - return nil
77 -}
78 -
79 -// compileRegexSafe compiles a regex with validation and caching
80 -// maxPatternLength of 0 means no limit (Zabbix default)
81 -func compileRegexSafe(pattern string, maxPatternLength int) (*regexp.Regexp, error) {
82 - // Check cache first (read lock)
83 - globalRegexCache.mu.RLock()
84 - if re, found := globalRegexCache.cache[pattern]; found {
85 - globalRegexCache.mu.RUnlock()
86 - return re, nil
87 - }
88 - globalRegexCache.mu.RUnlock()
89 -
90 - // Validate complexity before compiling
91 - if err := validateRegexComplexity(pattern, maxPatternLength); err != nil {
92 - return nil, err
93 - }
94 -
95 - // Compile pattern
96 - re, err := regexp.Compile(pattern)
97 - if err != nil {
98 - return nil, err
99 - }
100 -
101 - // Add to cache (write lock)
102 - globalRegexCache.mu.Lock()
103 - defer globalRegexCache.mu.Unlock()
104 -
105 - // Double-check cache after acquiring write lock (another goroutine might have added it)
106 - if cached, found := globalRegexCache.cache[pattern]; found {
107 - return cached, nil
108 - }
109 -
110 - // Evict oldest entry if cache is full (simple FIFO)
111 - if len(globalRegexCache.cache) >= regexCacheSize {
112 - oldest := globalRegexCache.order[0]
113 - delete(globalRegexCache.cache, oldest)
114 - globalRegexCache.order = globalRegexCache.order[1:]
115 - }
116 -
117 - // Add to cache
118 - globalRegexCache.cache[pattern] = re
119 - globalRegexCache.order = append(globalRegexCache.order, pattern)
120 -
121 - return re, nil
122 -}
123 -
124 -// matchWithTimeout runs a regex match with timeout protection
125 -func matchWithTimeout(re *regexp.Regexp, input string, timeout time.Duration) (bool, error) {
126 - if timeout == 0 {
127 - timeout = defaultRegexTimeout
128 - }
129 -
130 - ctx, cancel := context.WithTimeout(context.Background(), timeout)
131 - defer cancel()
132 -
133 - resultChan := make(chan bool, 1)
134 - errChan := make(chan error, 1)
135 -
136 - go func() {
137 - defer func() {
138 - if r := recover(); r != nil {
139 - select {
140 - case errChan <- fmt.Errorf("regex match panicked: %v", r):
141 - case <-ctx.Done():
142 - return // Context cancelled, exit immediately
143 - }
144 - }
145 - }()
146 -
147 - matched := re.MatchString(input)
148 - select {
149 - case resultChan <- matched:
150 - case <-ctx.Done():
151 - return // Context cancelled, exit immediately
152 - }
153 - }()
154 -
155 - select {
156 - case matched := <-resultChan:
157 - return matched, nil
158 - case err := <-errChan:
159 - return false, err
160 - case <-ctx.Done():
161 - return false, fmt.Errorf("regex match timeout after %v", timeout)
162 - }
163 -}
164 -
165 -// findStringSubmatchWithTimeout runs FindStringSubmatch with timeout protection
166 -func findStringSubmatchWithTimeout(re *regexp.Regexp, input string, timeout time.Duration) ([]string, error) {
167 - if timeout == 0 {
168 - timeout = defaultRegexTimeout
169 - }
170 -
171 - ctx, cancel := context.WithTimeout(context.Background(), timeout)
172 - defer cancel()
173 -
174 - resultChan := make(chan []string, 1)
175 - errChan := make(chan error, 1)
176 -
177 - go func() {
178 - defer func() {
179 - if r := recover(); r != nil {
180 - select {
181 - case errChan <- fmt.Errorf("regex find panicked: %v", r):
182 - case <-ctx.Done():
183 - return // Context cancelled, exit immediately
184 - }
185 - }
186 - }()
187 -
188 - matches := re.FindStringSubmatch(input)
189 - select {
190 - case resultChan <- matches:
191 - case <-ctx.Done():
192 - return // Context cancelled, exit immediately
193 - }
194 - }()
195 -
196 - select {
197 - case matches := <-resultChan:
198 - return matches, nil
199 - case err := <-errChan:
200 - return nil, err
201 - case <-ctx.Done():
202 - return nil, fmt.Errorf("regex find timeout after %v", timeout)
203 - }
204 -}
205 -
206 -// findStringSubmatchIndexWithTimeout runs FindStringSubmatchIndex with timeout protection
207 -func findStringSubmatchIndexWithTimeout(re *regexp.Regexp, input string, timeout time.Duration) ([]int, error) {
208 - if timeout == 0 {
209 - timeout = defaultRegexTimeout
210 - }
211 -
212 - ctx, cancel := context.WithTimeout(context.Background(), timeout)
213 - defer cancel()
214 -
215 - resultChan := make(chan []int, 1)
216 - errChan := make(chan error, 1)
217 -
218 - go func() {
219 - defer func() {
220 - if r := recover(); r != nil {
221 - select {
222 - case errChan <- fmt.Errorf("regex find index panicked: %v", r):
223 - case <-ctx.Done():
224 - return // Context cancelled, exit immediately
225 - }
226 - }
227 - }()
228 -
229 - matches := re.FindStringSubmatchIndex(input)
230 - select {
231 - case resultChan <- matches:
232 - case <-ctx.Done():
233 - return // Context cancelled, exit immediately
234 - }
235 - }()
236 -
237 - select {
238 - case matches := <-resultChan:
239 - return matches, nil
240 - case err := <-errChan:
241 - return nil, err
242 - case <-ctx.Done():
243 - return nil, fmt.Errorf("regex find index timeout after %v", timeout)
244 - }
245 -}
246 -
247 -// ClearRegexCache clears the global regex cache (useful for testing)
248 -func ClearRegexCache() {
249 - globalRegexCache.mu.Lock()
250 - defer globalRegexCache.mu.Unlock()
251 -
252 - globalRegexCache.cache = make(map[string]*regexp.Regexp)
253 - globalRegexCache.order = make([]string, 0, regexCacheSize)
254 -}
src/go/plugin/scripts.d/pkg/zabbixpreproc/regex_safe_test.go deleted
-316
@@ -1,316 +0,0 @@
1 -package zabbixpreproc
2 -
3 -import (
4 - "regexp"
5 - "strings"
6 - "testing"
7 - "time"
8 -)
9 -
10 -// Test constants for regex pattern limits (used only in tests)
11 -const testMaxRegexPatternLength = 1000
12 -
13 -// TestRegexComplexityValidation tests that overly complex patterns are rejected
14 -func TestRegexComplexityValidation(t *testing.T) {
15 - tests := []struct {
16 - name string
17 - pattern string
18 - maxPatternLength int
19 - wantErr bool
20 - }{
21 - {
22 - name: "simple pattern",
23 - pattern: "hello.*world",
24 - maxPatternLength: 0, // no limit
25 - wantErr: false,
26 - },
27 - {
28 - name: "pattern too long with limit",
29 - pattern: strings.Repeat("a", testMaxRegexPatternLength+1),
30 - maxPatternLength: testMaxRegexPatternLength,
31 - wantErr: true,
32 - },
33 - {
34 - name: "pattern ok when no limit",
35 - pattern: strings.Repeat("a", testMaxRegexPatternLength+1),
36 - maxPatternLength: 0, // no limit
37 - wantErr: false,
38 - },
39 - {
40 - name: "excessive nesting",
41 - pattern: strings.Repeat("(", maxRegexNestingDepth+1) + strings.Repeat(")", maxRegexNestingDepth+1),
42 - maxPatternLength: 0,
43 - wantErr: true,
44 - },
45 - {
46 - name: "unbalanced parentheses - too many open",
47 - pattern: "((hello",
48 - maxPatternLength: 0,
49 - wantErr: true,
50 - },
51 - {
52 - name: "unbalanced parentheses - too many close",
53 - pattern: "hello))",
54 - maxPatternLength: 0,
55 - wantErr: true,
56 - },
57 - {
58 - name: "acceptable nesting",
59 - pattern: strings.Repeat("(", maxRegexNestingDepth) + strings.Repeat(")", maxRegexNestingDepth),
60 - maxPatternLength: 0,
61 - wantErr: false,
62 - },
63 - }
64 -
65 - for _, tt := range tests {
66 - t.Run(tt.name, func(t *testing.T) {
67 - err := validateRegexComplexity(tt.pattern, tt.maxPatternLength)
68 - if (err != nil) != tt.wantErr {
69 - t.Errorf("validateRegexComplexity() error = %v, wantErr %v", err, tt.wantErr)
70 - }
71 - })
72 - }
73 -}
74 -
75 -// TestRegexCaching tests that compiled regexes are cached
76 -func TestRegexCaching(t *testing.T) {
77 - ClearRegexCache()
78 -
79 - pattern := "test.*pattern"
80 -
81 - // First compilation
82 - re1, err := compileRegexSafe(pattern, 0)
83 - if err != nil {
84 - t.Fatalf("compileRegexSafe() error = %v", err)
85 - }
86 -
87 - // Second compilation should return cached instance
88 - re2, err := compileRegexSafe(pattern, 0)
89 - if err != nil {
90 - t.Fatalf("compileRegexSafe() error = %v", err)
91 - }
92 -
93 - // Should be the exact same pointer (cached)
94 - if re1 != re2 {
95 - t.Errorf("Expected cached regex to be same instance, got different instances")
96 - }
97 -}
98 -
99 -// TestRegexCacheEviction tests LRU cache eviction
100 -func TestRegexCacheEviction(t *testing.T) {
101 - ClearRegexCache()
102 -
103 - // Fill cache to capacity
104 - for i := 0; i < regexCacheSize; i++ {
105 - pattern := regexp.QuoteMeta(strings.Repeat("a", i+1))
106 - _, err := compileRegexSafe(pattern, 0)
107 - if err != nil {
108 - t.Fatalf("compileRegexSafe() error = %v", err)
109 - }
110 - }
111 -
112 - // Cache should be at capacity
113 - if len(globalRegexCache.cache) != regexCacheSize {
114 - t.Errorf("Expected cache size %d, got %d", regexCacheSize, len(globalRegexCache.cache))
115 - }
116 -
117 - // Add one more - should evict oldest
118 - newPattern := regexp.QuoteMeta(strings.Repeat("z", 200))
119 - _, err := compileRegexSafe(newPattern, 0)
120 - if err != nil {
121 - t.Fatalf("compileRegexSafe() error = %v", err)
122 - }
123 -
124 - // Cache should still be at capacity
125 - if len(globalRegexCache.cache) != regexCacheSize {
126 - t.Errorf("Expected cache size %d after eviction, got %d", regexCacheSize, len(globalRegexCache.cache))
127 - }
128 -
129 - // First pattern should be evicted
130 - firstPattern := regexp.QuoteMeta("a")
131 - _, exists := globalRegexCache.cache[firstPattern]
132 - if exists {
133 - t.Errorf("Expected oldest pattern to be evicted, but it's still in cache")
134 - }
135 -}
136 -
137 -// TestMatchWithTimeout tests that slow regex operations timeout
138 -func TestMatchWithTimeout(t *testing.T) {
139 - // Simple pattern that should complete quickly
140 - re := regexp.MustCompile("hello")
141 -
142 - // Should complete successfully
143 - matched, err := matchWithTimeout(re, "hello world", 100*time.Millisecond)
144 - if err != nil {
145 - t.Errorf("matchWithTimeout() error = %v", err)
146 - }
147 - if !matched {
148 - t.Errorf("Expected match, got no match")
149 - }
150 -}
151 -
152 -// TestRegexSubstituteWithSafeRegex tests regexSubstitute uses safe regex
153 -func TestRegexSubstituteWithSafeRegex(t *testing.T) {
154 - ClearRegexCache()
155 -
156 - value := Value{Data: "hello world", Type: ValueTypeStr}
157 - params := "world\nuniverse"
158 -
159 - result, err := regexSubstitute(value, params)
160 - if err != nil {
161 - t.Fatalf("regexSubstitute() error = %v", err)
162 - }
163 -
164 - if result.Data != "universe" {
165 - t.Errorf("Expected 'universe', got '%s'", result.Data)
166 - }
167 -
168 - // Verify pattern was cached
169 - pattern := "world"
170 - _, exists := globalRegexCache.cache[pattern]
171 - if !exists {
172 - t.Errorf("Expected pattern to be cached")
173 - }
174 -}
175 -
176 -// TestValidateRegexWithSafeRegex tests validateRegex uses safe regex
177 -func TestValidateRegexWithSafeRegex(t *testing.T) {
178 - ClearRegexCache()
179 -
180 - value := Value{Data: "test123", Type: ValueTypeStr}
181 - pattern := "^test[0-9]+$"
182 -
183 - result, err := validateRegex(value, pattern)
184 - if err != nil {
185 - t.Fatalf("validateRegex() error = %v", err)
186 - }
187 -
188 - if result.Data != value.Data {
189 - t.Errorf("Expected value unchanged, got '%s'", result.Data)
190 - }
191 -
192 - // Verify pattern was cached
193 - _, exists := globalRegexCache.cache[pattern]
194 - if !exists {
195 - t.Errorf("Expected pattern to be cached")
196 - }
197 -}
198 -
199 -// TestValidateNotRegexWithSafeRegex tests validateNotRegex uses safe regex
200 -func TestValidateNotRegexWithSafeRegex(t *testing.T) {
201 - ClearRegexCache()
202 -
203 - value := Value{Data: "hello", Type: ValueTypeStr}
204 - pattern := "^[0-9]+$"
205 -
206 - result, err := validateNotRegex(value, pattern)
207 - if err != nil {
208 - t.Fatalf("validateNotRegex() error = %v", err)
209 - }
210 -
211 - if result.Data != value.Data {
212 - t.Errorf("Expected value unchanged, got '%s'", result.Data)
213 - }
214 -
215 - // Verify pattern was cached
216 - _, exists := globalRegexCache.cache[pattern]
217 - if !exists {
218 - t.Errorf("Expected pattern to be cached")
219 - }
220 -}
221 -
222 -// TestErrorFieldRegexWithSafeRegex tests errorFieldRegex uses safe regex
223 -func TestErrorFieldRegexWithSafeRegex(t *testing.T) {
224 - ClearRegexCache()
225 -
226 - value := Value{Data: "ERROR: test failed", Type: ValueTypeStr}
227 - params := "ERROR: (.+)\n$1"
228 -
229 - _, err := errorFieldRegex(value, params)
230 - if err == nil {
231 - t.Fatal("Expected error, got nil")
232 - }
233 -
234 - if err.Error() != "test failed" {
235 - t.Errorf("Expected 'test failed', got '%s'", err.Error())
236 - }
237 -
238 - // Verify pattern was cached
239 - pattern := "ERROR: (.+)"
240 - _, exists := globalRegexCache.cache[pattern]
241 - if !exists {
242 - t.Errorf("Expected pattern to be cached")
243 - }
244 -}
245 -
246 -// TestRegexProtectionAgainstComplexPatterns tests that complex patterns are rejected
247 -func TestRegexProtectionAgainstComplexPatterns(t *testing.T) {
248 - tests := []struct {
249 - name string
250 - pattern string
251 - input string
252 - wantErr bool
253 - }{
254 - {
255 - name: "simple valid pattern",
256 - pattern: "^[a-z]+$",
257 - input: "hello",
258 - wantErr: false,
259 - },
260 - {
261 - name: "long pattern ok with no limit",
262 - pattern: ".*" + strings.Repeat("a", testMaxRegexPatternLength) + ".*", // Valid long pattern
263 - input: strings.Repeat("a", testMaxRegexPatternLength), // Input that matches
264 - wantErr: false, // No limit by default
265 - },
266 - {
267 - name: "excessive nesting depth",
268 - pattern: strings.Repeat("(a", maxRegexNestingDepth+1) + strings.Repeat(")", maxRegexNestingDepth+1),
269 - input: "aaaa",
270 - wantErr: true,
271 - },
272 - }
273 -
274 - for _, tt := range tests {
275 - t.Run(tt.name, func(t *testing.T) {
276 - value := Value{Data: tt.input, Type: ValueTypeStr}
277 - _, err := validateRegex(value, tt.pattern)
278 -
279 - if (err != nil) != tt.wantErr {
280 - t.Errorf("validateRegex() error = %v, wantErr %v", err, tt.wantErr)
281 - }
282 - })
283 - }
284 -}
285 -
286 -// TestConcurrentRegexCacheAccess tests thread safety of regex cache
287 -func TestConcurrentRegexCacheAccess(t *testing.T) {
288 - ClearRegexCache()
289 -
290 - // Launch multiple goroutines that compile the same pattern
291 - pattern := "test.*concurrent"
292 - done := make(chan bool, 10)
293 -
294 - for i := 0; i < 10; i++ {
295 - go func() {
296 - for j := 0; j < 100; j++ {
297 - _, err := compileRegexSafe(pattern, 0)
298 - if err != nil {
299 - t.Errorf("compileRegexSafe() error = %v", err)
300 - }
301 - }
302 - done <- true
303 - }()
304 - }
305 -
306 - // Wait for all goroutines to complete
307 - for i := 0; i < 10; i++ {
308 - <-done
309 - }
310 -
311 - // Pattern should be in cache exactly once
312 - _, exists := globalRegexCache.cache[pattern]
313 - if !exists {
314 - t.Errorf("Expected pattern to be cached")
315 - }
316 -}
src/go/plugin/scripts.d/pkg/zabbixpreproc/regression_test.go deleted
-509
@@ -1,509 +0,0 @@
1 -package zabbixpreproc
2 -
3 -import (
4 - "strings"
5 - "testing"
6 - "time"
7 -)
8 -
9 -// TestRegressionIsErrorPropagation tests that IsError flag is preserved through pipeline.
10 -// Issue: ExecutePipeline was resetting IsError to false, breaking validateNotSupported.
11 -func TestRegressionIsErrorPropagation(t *testing.T) {
12 - p := NewPreprocessor("test-shard")
13 -
14 - tests := []struct {
15 - name string
16 - inputData string
17 - inputError bool // IsError flag on input
18 - steps []Step
19 - expectError bool // Should return error?
20 - expectData string // Expected output data (if no error)
21 - }{
22 - {
23 - name: "Error input passes through unchanged when no error-handling step",
24 - inputData: "previous error message",
25 - inputError: true,
26 - steps: []Step{
27 - {Type: StepTypeTrim, Params: " "},
28 - },
29 - expectError: false,
30 - expectData: "previous error message", // Trim works on error message string
31 - },
32 - {
33 - name: "ValidateNotSupported sees IsError flag",
34 - inputData: "ZBX_NOTSUPPORTED: Agent is not available",
35 - inputError: true,
36 - steps: []Step{
37 - {Type: StepTypeValidateNotSupported, Params: "\nAgent is not available"},
38 - },
39 - expectError: true, // Pattern matches, so error is "supported" -> fail
40 - },
41 - {
42 - name: "IsError preserved through multiple successful steps",
43 - inputData: "error data",
44 - inputError: true,
45 - steps: []Step{
46 - {Type: StepTypeTrim, Params: " "},
47 - {Type: StepTypeStringReplace, Params: "data\ninfo"},
48 - {Type: StepTypeTrim, Params: " "},
49 - },
50 - expectError: false,
51 - expectData: "error info", // Processed but IsError flag should be preserved
52 - },
53 - {
54 - name: "Normal value not treated as error",
55 - inputData: "normal value",
56 - inputError: false,
57 - steps: []Step{
58 - {Type: StepTypeTrim, Params: " "},
59 - },
60 - expectError: false,
61 - expectData: "normal value",
62 - },
63 - }
64 -
65 - for _, tt := range tests {
66 - t.Run(tt.name, func(t *testing.T) {
67 - input := Value{
68 - Data: tt.inputData,
69 - Type: ValueTypeStr,
70 - Timestamp: time.Now(),
71 - IsError: tt.inputError,
72 - }
73 -
74 - result, err := p.ExecutePipeline("item1", input, tt.steps)
75 -
76 - if tt.expectError {
77 - if err == nil {
78 - t.Error("Expected error, got nil")
79 - }
80 - } else {
81 - if err != nil {
82 - t.Errorf("Expected no error, got: %v", err)
83 - }
84 - if len(result.Metrics) > 0 && result.Metrics[0].Value != tt.expectData {
85 - t.Errorf("Expected data %q, got %q", tt.expectData, result.Metrics[0].Value)
86 - }
87 - }
88 - })
89 - }
90 -}
91 -
92 -// TestRegressionPrometheusEscapedQuotes tests proper handling of escaped characters in labels.
93 -// Issue: Regex-based parsing failed on escaped quotes like path="/foo\"bar".
94 -func TestRegressionPrometheusEscapedQuotes(t *testing.T) {
95 - tests := []struct {
96 - name string
97 - input string
98 - labelKey string
99 - labelValue string
100 - }{
101 - {
102 - name: "Escaped double quote in label value",
103 - input: `http_requests{path="/foo\"bar"} 100`,
104 - labelKey: "path",
105 - labelValue: `/foo"bar`,
106 - },
107 - {
108 - name: "Escaped backslash in label value",
109 - input: `file_size{path="C:\\Users\\Admin"} 200`,
110 - labelKey: "path",
111 - labelValue: `C:\Users\Admin`,
112 - },
113 - {
114 - name: "Escaped newline in label value",
115 - input: `message{text="line1\nline2"} 50`,
116 - labelKey: "text",
117 - labelValue: "line1\nline2",
118 - },
119 - {
120 - name: "Multiple escapes in single label",
121 - input: `complex{data="quote\"slash\\newline\n"} 75`,
122 - labelKey: "data",
123 - labelValue: "quote\"slash\\newline\n",
124 - },
125 - {
126 - name: "Mixed normal and escaped labels",
127 - input: `metric{method="GET",path="/test\"path",code="200"} 300`,
128 - labelKey: "path",
129 - labelValue: `/test"path`,
130 - },
131 - }
132 -
133 - for _, tt := range tests {
134 - t.Run(tt.name, func(t *testing.T) {
135 - metrics, err := parsePrometheusText(tt.input)
136 - if err != nil {
137 - t.Fatalf("Failed to parse: %v", err)
138 - }
139 - if len(metrics) == 0 {
140 - t.Fatal("No metrics parsed")
141 - }
142 -
143 - metric := metrics[0]
144 - if val, ok := metric.Labels[tt.labelKey]; !ok {
145 - t.Errorf("Label %q not found in parsed metric", tt.labelKey)
146 - } else if val != tt.labelValue {
147 - t.Errorf("Label %q value mismatch:\nExpected: %q\nGot: %q", tt.labelKey, tt.labelValue, val)
148 - }
149 - })
150 - }
151 -}
152 -
153 -// TestRegressionPrometheusLabelOrder tests that label order is preserved.
154 -func TestRegressionPrometheusLabelOrder(t *testing.T) {
155 - input := `metric{first="1",second="2",third="3"} 100`
156 - metrics, err := parsePrometheusText(input)
157 - if err != nil {
158 - t.Fatalf("Failed to parse: %v", err)
159 - }
160 - if len(metrics) == 0 {
161 - t.Fatal("No metrics parsed")
162 - }
163 -
164 - expected := []string{"first", "second", "third"}
165 - if len(metrics[0].LabelOrder) != len(expected) {
166 - t.Fatalf("Label order length mismatch: expected %d, got %d", len(expected), len(metrics[0].LabelOrder))
167 - }
168 -
169 - for i, key := range expected {
170 - if metrics[0].LabelOrder[i] != key {
171 - t.Errorf("Label order[%d] mismatch: expected %q, got %q", i, key, metrics[0].LabelOrder[i])
172 - }
173 - }
174 -}
175 -
176 -// TestRegressionSNMPTextualOIDSupport tests that textual OIDs are properly translated.
177 -// Issue: snmpWalkToJSON only did direct map lookup, not full translateMIB.
178 -func TestRegressionSNMPTextualOIDSupport(t *testing.T) {
179 - tests := []struct {
180 - name string
181 - oid string
182 - expectedOK bool
183 - expected string // Expected numeric OID (if OK)
184 - }{
185 - {
186 - name: "IF-MIB::ifDescr translates correctly",
187 - oid: "IF-MIB::ifDescr",
188 - expectedOK: true,
189 - expected: ".1.3.6.1.2.1.2.2.1.2",
190 - },
191 - {
192 - name: "IF-MIB::ifIndex translates correctly",
193 - oid: "IF-MIB::ifIndex",
194 - expectedOK: true,
195 - expected: ".1.3.6.1.2.1.2.2.1.1",
196 - },
197 - {
198 - name: "SNMPv2-MIB::sysDescr translates correctly",
199 - oid: "SNMPv2-MIB::sysDescr",
200 - expectedOK: true,
201 - expected: ".1.3.6.1.2.1.1.1",
202 - },
203 - {
204 - name: "HOST-RESOURCES-MIB::hrStorageDescr translates correctly",
205 - oid: "HOST-RESOURCES-MIB::hrStorageDescr",
206 - expectedOK: true,
207 - expected: ".1.3.6.1.2.1.25.2.3.1.3",
208 - },
209 - {
210 - name: "Numeric OID passthrough",
211 - oid: ".1.3.6.1.2.1.2.2.1.2",
212 - expectedOK: true,
213 - expected: ".1.3.6.1.2.1.2.2.1.2",
214 - },
215 - {
216 - name: "Numeric OID without leading dot gets normalized",
217 - oid: "1.3.6.1.2.1.2.2.1.2",
218 - expectedOK: true,
219 - expected: "1.3.6.1.2.1.2.2.1.2", // translateMIB returns passthrough for numeric
220 - },
221 - {
222 - name: "Unknown MIB returns error",
223 - oid: "UNKNOWN-MIB::unknownOid",
224 - expectedOK: false,
225 - },
226 - }
227 -
228 - for _, tt := range tests {
229 - t.Run(tt.name, func(t *testing.T) {
230 - result, err := translateMIB(tt.oid)
231 - if tt.expectedOK {
232 - if err != nil {
233 - t.Errorf("Expected successful translation, got error: %v", err)
234 - }
235 - if result != tt.expected {
236 - t.Errorf("OID translation mismatch:\nExpected: %s\nGot: %s", tt.expected, result)
237 - }
238 - } else {
239 - if err == nil {
240 - t.Error("Expected error for unknown MIB, got nil")
241 - }
242 - }
243 - })
244 - }
245 -}
246 -
247 -// TestRegressionSNMPWalkToJSONUsesTranslateMIB tests that snmpWalkToJSON uses full translateMIB.
248 -func TestRegressionSNMPWalkToJSONUsesTranslateMIB(t *testing.T) {
249 - p := NewPreprocessor("test-shard")
250 -
251 - // SNMP walk data using numeric OIDs
252 - snmpData := `.1.3.6.1.2.1.2.2.1.1.1 = INTEGER: 1
253 -.1.3.6.1.2.1.2.2.1.1.2 = INTEGER: 2
254 -.1.3.6.1.2.1.2.2.1.2.1 = STRING: "eth0"
255 -.1.3.6.1.2.1.2.2.1.2.2 = STRING: "eth1"`
256 -
257 - // Use textual OID in params - this should work now
258 - params := `{#IFINDEX}
259 -IF-MIB::ifIndex
260 -0
261 -{#IFDESCR}
262 -IF-MIB::ifDescr
263 -0`
264 -
265 - input := Value{
266 - Data: snmpData,
267 - Type: ValueTypeStr,
268 - }
269 -
270 - step := Step{
271 - Type: StepTypeSNMPWalkToJSON,
272 - Params: params,
273 - }
274 -
275 - result, err := p.Execute("item1", input, step)
276 - if err != nil {
277 - t.Fatalf("SNMP walk to JSON failed: %v", err)
278 - }
279 -
280 - if len(result.Metrics) == 0 {
281 - t.Fatal("No metrics returned")
282 - }
283 -
284 - // The result should contain JSON with both indices
285 - output := result.Metrics[0].Value
286 - if !strings.Contains(output, `"eth0"`) || !strings.Contains(output, `"eth1"`) {
287 - t.Errorf("Expected output to contain interface descriptions, got: %s", output)
288 - }
289 - if !strings.Contains(output, `"{#IFINDEX}"`) || !strings.Contains(output, `"{#IFDESCR}"`) {
290 - t.Errorf("Expected output to contain macro names, got: %s", output)
291 - }
292 -}
293 -
294 -// TestRegressionJavaScriptVMIsolation tests that JS VMs are isolated between executions.
295 -// Issue: VM pooling with resetVM didn't clear prototype modifications.
296 -func TestRegressionJavaScriptVMIsolation(t *testing.T) {
297 - // First execution: modify Object prototype
298 - script1 := `
299 - Object.prototype.polluted = "malicious";
300 - return "done";
301 - `
302 - result1, err := javascriptExecute(Value{Data: "test"}, script1, DefaultLimits().JavaScript)
303 - if err != nil {
304 - t.Fatalf("First JS execution failed: %v", err)
305 - }
306 - if result1.Data != "done" {
307 - t.Errorf("Expected 'done', got %q", result1.Data)
308 - }
309 -
310 - // Second execution: check if prototype pollution persists
311 - script2 := `
312 - // If VMs were pooled without proper isolation, this would return "malicious"
313 - var obj = {};
314 - if (obj.polluted !== undefined) {
315 - return "POLLUTION DETECTED: " + obj.polluted;
316 - }
317 - return "clean";
318 - `
319 - result2, err := javascriptExecute(Value{Data: "test"}, script2, DefaultLimits().JavaScript)
320 - if err != nil {
321 - t.Fatalf("Second JS execution failed: %v", err)
322 - }
323 - if result2.Data != "clean" {
324 - t.Errorf("VM isolation failed! Got: %q", result2.Data)
325 - }
326 -}
327 -
328 -// TestRegressionJavaScriptGlobalVariableIsolation tests that global variables don't leak.
329 -func TestRegressionJavaScriptGlobalVariableIsolation(t *testing.T) {
330 - // First execution: define global variable
331 - script1 := `
332 - globalVar = "secret";
333 - return globalVar;
334 - `
335 - result1, err := javascriptExecute(Value{Data: "test"}, script1, DefaultLimits().JavaScript)
336 - if err != nil {
337 - t.Fatalf("First JS execution failed: %v", err)
338 - }
339 - if result1.Data != "secret" {
340 - t.Errorf("Expected 'secret', got %q", result1.Data)
341 - }
342 -
343 - // Second execution: check if global variable persists
344 - script2 := `
345 - if (typeof globalVar !== 'undefined') {
346 - return "LEAK DETECTED: " + globalVar;
347 - }
348 - return "isolated";
349 - `
350 - result2, err := javascriptExecute(Value{Data: "test"}, script2, DefaultLimits().JavaScript)
351 - if err != nil {
352 - t.Fatalf("Second JS execution failed: %v", err)
353 - }
354 - if result2.Data != "isolated" {
355 - t.Errorf("Global variable leaked! Got: %q", result2.Data)
356 - }
357 -}
358 -
359 -// TestRegressionPipelineDiscardedFlag tests that pipeline explicitly flags discarded values.
360 -// Issue: Pipeline returned Result{} with nil error, hiding discard vs misconfiguration.
361 -func TestRegressionPipelineDiscardedFlag(t *testing.T) {
362 - p := NewPreprocessor("test-shard")
363 -
364 - // Test 1: Normal successful pipeline - not discarded
365 - input1 := Value{
366 - Data: "100",
367 - Type: ValueTypeStr,
368 - Timestamp: time.Now(),
369 - }
370 -
371 - steps := []Step{
372 - {Type: StepTypeTrim, Params: " "},
373 - }
374 -
375 - result1, err := p.ExecutePipeline("item1", input1, steps)
376 - if err != nil {
377 - t.Fatalf("Normal execution failed: %v", err)
378 - }
379 - if result1.Discarded {
380 - t.Error("Normal result should not be discarded")
381 - }
382 - if len(result1.Metrics) == 0 {
383 - t.Error("Normal result should have metrics")
384 - }
385 -
386 - // Test 2: Error with ErrorActionDiscard - returns empty string, not discarded
387 - // Zabbix behavior: discard means replace error with empty string value
388 - input2 := Value{
389 - Data: "invalid",
390 - Type: ValueTypeStr,
391 - Timestamp: time.Now(),
392 - }
393 -
394 - stepsDiscard := []Step{
395 - {
396 - Type: StepTypeMultiplier,
397 - Params: "2",
398 - ErrorHandler: ErrorHandler{
399 - Action: ErrorActionDiscard,
400 - },
401 - },
402 - }
403 -
404 - result2, err := p.ExecutePipeline("item2", input2, stepsDiscard)
405 - if err != nil {
406 - t.Fatalf("Discard execution failed: %v", err)
407 - }
408 - // ErrorActionDiscard returns empty string, not zero metrics
409 - if result2.Discarded {
410 - t.Error("ErrorActionDiscard should return empty string, not discard flag")
411 - }
412 - if len(result2.Metrics) == 0 {
413 - t.Error("ErrorActionDiscard should return metrics (with empty value)")
414 - }
415 - if len(result2.Metrics) > 0 && result2.Metrics[0].Value != "" {
416 - t.Errorf("ErrorActionDiscard should return empty string, got %q", result2.Metrics[0].Value)
417 - }
418 -
419 - // Test 3: Empty Result detection - Result.Discarded flag is for when step returns 0 metrics
420 - // This can happen with multi-metric steps or certain edge cases
421 - // The flag allows callers to distinguish empty result from error
422 - if result1.Discarded || result2.Discarded {
423 - t.Error("Neither normal nor discard results should have Discarded=true")
424 - }
425 -}
426 -
427 -// TestRegressionSNMPTextualWalkOutput tests that SNMP walk output with textual OIDs is parsed.
428 -// Issue: parsers skipped lines not starting with '.', rejecting IF-MIB::ifDescr.1 format.
429 -func TestRegressionSNMPTextualWalkOutput(t *testing.T) {
430 - p := NewPreprocessor("test-shard")
431 -
432 - // SNMP walk data using textual OIDs (real snmpwalk output without -On flag)
433 - snmpData := `IF-MIB::ifIndex.1 = INTEGER: 1
434 -IF-MIB::ifIndex.2 = INTEGER: 2
435 -IF-MIB::ifDescr.1 = STRING: "eth0"
436 -IF-MIB::ifDescr.2 = STRING: "eth1"
437 -IF-MIB::ifSpeed.1 = Gauge32: 1000000000
438 -IF-MIB::ifSpeed.2 = Gauge32: 10000000000`
439 -
440 - // Use textual OIDs in params
441 - params := `{#IFINDEX}
442 -IF-MIB::ifIndex
443 -0
444 -{#IFDESCR}
445 -IF-MIB::ifDescr
446 -0`
447 -
448 - input := Value{
449 - Data: snmpData,
450 - Type: ValueTypeStr,
451 - }
452 -
453 - step := Step{
454 - Type: StepTypeSNMPWalkToJSON,
455 - Params: params,
456 - }
457 -
458 - result, err := p.Execute("item1", input, step)
459 - if err != nil {
460 - t.Fatalf("SNMP walk to JSON failed with textual OID output: %v", err)
461 - }
462 -
463 - if len(result.Metrics) == 0 {
464 - t.Fatal("No metrics returned from textual SNMP walk output")
465 - }
466 -
467 - output := result.Metrics[0].Value
468 - // Verify both interfaces were parsed
469 - if !strings.Contains(output, `"eth0"`) {
470 - t.Errorf("Expected 'eth0' in output, got: %s", output)
471 - }
472 - if !strings.Contains(output, `"eth1"`) {
473 - t.Errorf("Expected 'eth1' in output, got: %s", output)
474 - }
475 - if !strings.Contains(output, `"{#IFINDEX}"`) {
476 - t.Errorf("Expected macro {#IFINDEX} in output, got: %s", output)
477 - }
478 -}
479 -
480 -// TestRegressionJavaScriptArrayPrototypeIsolation tests Array.prototype isolation.
481 -func TestRegressionJavaScriptArrayPrototypeIsolation(t *testing.T) {
482 - // First execution: modify Array prototype
483 - script1 := `
484 - Array.prototype.myMethod = function() { return "injected"; };
485 - return [].myMethod();
486 - `
487 - result1, err := javascriptExecute(Value{Data: "test"}, script1, DefaultLimits().JavaScript)
488 - if err != nil {
489 - t.Fatalf("First JS execution failed: %v", err)
490 - }
491 - if result1.Data != "injected" {
492 - t.Errorf("Expected 'injected', got %q", result1.Data)
493 - }
494 -
495 - // Second execution: check if Array prototype modification persists
496 - script2 := `
497 - if (typeof [].myMethod === 'function') {
498 - return "ARRAY POLLUTION: " + [].myMethod();
499 - }
500 - return "array_clean";
501 - `
502 - result2, err := javascriptExecute(Value{Data: "test"}, script2, DefaultLimits().JavaScript)
503 - if err != nil {
504 - t.Fatalf("Second JS execution failed: %v", err)
505 - }
506 - if result2.Data != "array_clean" {
507 - t.Errorf("Array.prototype leaked! Got: %q", result2.Data)
508 - }
509 -}
src/go/plugin/scripts.d/pkg/zabbixpreproc/snmp_mib_test.go deleted
-256
@@ -1,256 +0,0 @@
1 -package zabbixpreproc
2 -
3 -import (
4 - "strings"
5 - "sync"
6 - "testing"
7 -)
8 -
9 -// TestTranslateMIB_Hardcoded tests translation using hardcoded map
10 -func TestTranslateMIB_Hardcoded(t *testing.T) {
11 - tests := []struct {
12 - name string
13 - mibName string
14 - expected string
15 - }{
16 - {"SNMPv2-MIB sysDescr", "SNMPv2-MIB::sysDescr", ".1.3.6.1.2.1.1.1"},
17 - {"SNMPv2-MIB sysUpTime", "SNMPv2-MIB::sysUpTime", ".1.3.6.1.2.1.1.3"},
18 - {"IF-MIB ifDescr", "IF-MIB::ifDescr", ".1.3.6.1.2.1.2.2.1.2"},
19 - {"IF-MIB ifOperStatus", "IF-MIB::ifOperStatus", ".1.3.6.1.2.1.2.2.1.8"},
20 - {"IF-MIB ifHCInOctets", "IF-MIB::ifHCInOctets", ".1.3.6.1.2.1.31.1.1.1.6"},
21 - {"HOST-RESOURCES hrSystemUptime", "HOST-RESOURCES-MIB::hrSystemUptime", ".1.3.6.1.2.1.25.1.1"},
22 - {"HOST-RESOURCES hrStorageDescr", "HOST-RESOURCES-MIB::hrStorageDescr", ".1.3.6.1.2.1.25.2.3.1.3"},
23 - {"HOST-RESOURCES hrProcessorLoad", "HOST-RESOURCES-MIB::hrProcessorLoad", ".1.3.6.1.2.1.25.3.3.1.2"},
24 - {"IP-MIB ipForwarding", "IP-MIB::ipForwarding", ".1.3.6.1.2.1.4.1"},
25 - {"TCP-MIB tcpCurrEstab", "TCP-MIB::tcpCurrEstab", ".1.3.6.1.2.1.6.9"},
26 - {"UDP-MIB udpInDatagrams", "UDP-MIB::udpInDatagrams", ".1.3.6.1.2.1.7.1"},
27 - }
28 -
29 - for _, tt := range tests {
30 - t.Run(tt.name, func(t *testing.T) {
31 - result, err := translateMIB(tt.mibName)
32 - if err != nil {
33 - t.Fatalf("translateMIB(%q) error: %v", tt.mibName, err)
34 - }
35 - if result != tt.expected {
36 - t.Errorf("translateMIB(%q) = %q, expected %q", tt.mibName, result, tt.expected)
37 - }
38 - })
39 - }
40 -}
41 -
42 -// TestTranslateMIB_NumericOID tests that numeric OIDs pass through unchanged
43 -func TestTranslateMIB_NumericOID(t *testing.T) {
44 - tests := []struct {
45 - name string
46 - oid string
47 - expected string
48 - }{
49 - {"With leading dot", ".1.3.6.1.2.1.1.1", ".1.3.6.1.2.1.1.1"},
50 - {"Without leading dot", "1.3.6.1.2.1.1.1", "1.3.6.1.2.1.1.1"},
51 - {"Short OID", ".1.3.6.1", ".1.3.6.1"},
52 - {"Single digit", ".1", ".1"},
53 - {"Complex OID", ".1.3.6.1.2.1.2.2.1.10.1", ".1.3.6.1.2.1.2.2.1.10.1"},
54 - }
55 -
56 - for _, tt := range tests {
57 - t.Run(tt.name, func(t *testing.T) {
58 - result, err := translateMIB(tt.oid)
59 - if err != nil {
60 - t.Fatalf("translateMIB(%q) error: %v", tt.oid, err)
61 - }
62 - if result != tt.expected {
63 - t.Errorf("translateMIB(%q) = %q, expected %q (should pass through)", tt.oid, result, tt.expected)
64 - }
65 - })
66 - }
67 -}
68 -
69 -// TestTranslateMIB_UnknownMIB tests error handling for unknown MIBs
70 -func TestTranslateMIB_UnknownMIB(t *testing.T) {
71 - // Clear cache to ensure fresh test
72 - mibTranslationCacheMu.Lock()
73 - mibTranslationCache = make(map[string]string)
74 - mibTranslationCacheMu.Unlock()
75 -
76 - // Unknown MIB that's not in hardcoded map
77 - _, err := translateMIB("UNKNOWN-MIB::unknownObject")
78 - if err == nil {
79 - t.Error("Expected error for unknown MIB, got nil")
80 - }
81 -
82 - expectedErrMsg := "unknown MIB: UNKNOWN-MIB::unknownObject"
83 - if err != nil && err.Error()[:len(expectedErrMsg)] != expectedErrMsg {
84 - t.Errorf("Error message = %q, expected to start with %q", err.Error(), expectedErrMsg)
85 - }
86 -}
87 -
88 -// TestTranslateMIB_Cache tests that translations are cached
89 -func TestTranslateMIB_Cache(t *testing.T) {
90 - // Clear cache
91 - mibTranslationCacheMu.Lock()
92 - mibTranslationCache = make(map[string]string)
93 - mibTranslationCacheMu.Unlock()
94 -
95 - // First call - should use hardcoded map
96 - result1, err := translateMIB("SNMPv2-MIB::sysDescr")
97 - if err != nil {
98 - t.Fatalf("First call failed: %v", err)
99 - }
100 -
101 - // Second call - should still work (from hardcoded map, not cache)
102 - result2, err := translateMIB("SNMPv2-MIB::sysDescr")
103 - if err != nil {
104 - t.Fatalf("Second call failed: %v", err)
105 - }
106 -
107 - if result1 != result2 {
108 - t.Errorf("Results differ: %q vs %q", result1, result2)
109 - }
110 -
111 - // Verify cache is not polluted with hardcoded entries
112 - mibTranslationCacheMu.RLock()
113 - cacheSize := len(mibTranslationCache)
114 - mibTranslationCacheMu.RUnlock()
115 -
116 - if cacheSize > 0 {
117 - t.Errorf("Cache should be empty (hardcoded entries shouldn't be cached), got %d entries", cacheSize)
118 - }
119 -}
120 -
121 -// TestTranslateMIB_ConcurrentAccess tests thread safety
122 -func TestTranslateMIB_ConcurrentAccess(t *testing.T) {
123 - // Clear cache
124 - mibTranslationCacheMu.Lock()
125 - mibTranslationCache = make(map[string]string)
126 - mibTranslationCacheMu.Unlock()
127 -
128 - mibs := []string{
129 - "SNMPv2-MIB::sysDescr",
130 - "IF-MIB::ifDescr",
131 - "HOST-RESOURCES-MIB::hrSystemUptime",
132 - "IP-MIB::ipForwarding",
133 - "TCP-MIB::tcpCurrEstab",
134 - ".1.3.6.1.2.1.1.1",
135 - ".1.3.6.1.2.1.2.2.1.2",
136 - }
137 -
138 - var wg sync.WaitGroup
139 - goroutines := 50
140 - iterations := 20
141 -
142 - for i := 0; i < goroutines; i++ {
143 - wg.Add(1)
144 - go func(id int) {
145 - defer wg.Done()
146 - for j := 0; j < iterations; j++ {
147 - mib := mibs[j%len(mibs)]
148 - _, err := translateMIB(mib)
149 - // Only numeric OIDs and hardcoded MIBs should succeed
150 - if err != nil && !strings.HasPrefix(mib, ".") && mibToOID[mib] == "" {
151 - // Unknown MIB - error expected
152 - continue
153 - }
154 - if err != nil {
155 - t.Errorf("goroutine %d iteration %d: translateMIB(%q) failed: %v", id, j, mib, err)
156 - }
157 - }
158 - }(i)
159 - }
160 -
161 - wg.Wait()
162 -}
163 -
164 -// TestTranslateMIB_CacheSizeLimit tests that cache respects size limit
165 -func TestTranslateMIB_CacheSizeLimit(t *testing.T) {
166 - // Save original limit
167 - originalLimit := mibCacheMaxSize
168 - defer func() { mibCacheMaxSize = originalLimit }()
169 -
170 - // Set small limit for testing
171 - mibCacheMaxSize = 5
172 -
173 - // Clear cache
174 - mibTranslationCacheMu.Lock()
175 - mibTranslationCache = make(map[string]string)
176 - mibTranslationCacheMu.Unlock()
177 -
178 - // Note: This test doesn't actually populate the cache since all tested MIBs
179 - // are in the hardcoded map. To test cache limit properly, we would need
180 - // snmptranslate available and translate unknown MIBs. For now, we just
181 - // verify the cache limit constant is respected in the code.
182 -
183 - // Verify cache size limit is applied in code (checked in translateMIB function)
184 - if mibCacheMaxSize != 5 {
185 - t.Errorf("mibCacheMaxSize should be 5, got %d", mibCacheMaxSize)
186 - }
187 -}
188 -
189 -// TestParseSNMPWalk_MIBTranslation tests that parseSNMPWalk uses MIB translation
190 -//func TestParseSNMPWalk_MIBTranslation(t *testing.T) {
191 -// tests := []struct {
192 -// name string
193 -// data string
194 -// targetMIB string
195 -// expectedOID string
196 -// shouldError bool
197 -// }{
198 -// {
199 -// name: "SNMPv2-MIB sysDescr",
200 -// data: ".1.3.6.1.2.1.1.1.0 = STRING: \"Linux server 5.4.0\"",
201 -// targetMIB: "SNMPv2-MIB::sysDescr.0",
202 -// expectedOID: ".1.3.6.1.2.1.1.1.0",
203 -// shouldError: false,
204 -// },
205 -// {
206 -// name: "IF-MIB ifDescr with index",
207 -// data: ".1.3.6.1.2.1.2.2.1.2.1 = STRING: \"eth0\"",
208 -// targetMIB: "IF-MIB::ifDescr.1",
209 -// expectedOID: ".1.3.6.1.2.1.2.2.1.2.1",
210 -// shouldError: false,
211 -// },
212 -// {
213 -// name: "Numeric OID",
214 -// data: ".1.3.6.1.2.1.1.1.0 = STRING: \"Test\"",
215 -// targetMIB: ".1.3.6.1.2.1.1.1.0",
216 -// expectedOID: ".1.3.6.1.2.1.1.1.0",
217 -// shouldError: false,
218 -// },
219 -// }
220 -//
221 -// for _, tt := range tests {
222 -// t.Run(tt.name, func(t *testing.T) {
223 -// result, err := parseSNMPWalk(tt.data, tt.targetMIB)
224 -// if tt.shouldError {
225 -// if err == nil {
226 -// t.Error("Expected error, got nil")
227 -// }
228 -// return
229 -// }
230 -// if err != nil {
231 -// t.Fatalf("parseSNMPWalk() error: %v", err)
232 -// }
233 -// if result.oid != tt.expectedOID {
234 -// t.Errorf("parseSNMPWalk() OID = %q, expected %q", result.oid, tt.expectedOID)
235 -// }
236 -// })
237 -// }
238 -//}
239 -
240 -// TestTranslateMIB_AllHardcodedMIBs verifies all hardcoded MIBs translate correctly
241 -func TestTranslateMIB_AllHardcodedMIBs(t *testing.T) {
242 - // Verify every entry in mibToOID map works
243 - for mibName, expectedOID := range mibToOID {
244 - result, err := translateMIB(mibName)
245 - if err != nil {
246 - t.Errorf("translateMIB(%q) error: %v", mibName, err)
247 - continue
248 - }
249 - if result != expectedOID {
250 - t.Errorf("translateMIB(%q) = %q, expected %q", mibName, result, expectedOID)
251 - }
252 - }
253 -
254 - // Report statistics
255 - t.Logf("Verified %d hardcoded MIB translations", len(mibToOID))
256 -}
src/go/plugin/scripts.d/pkg/zabbixpreproc/snmp_step.go deleted
-1618
@@ -1,1618 +0,0 @@
1 -package zabbixpreproc
2 -
3 -import (
4 - "context"
5 - "encoding/hex"
6 - "fmt"
7 - "os/exec"
8 - "sort"
9 - "strconv"
10 - "strings"
11 - "sync"
12 - "time"
13 - "unicode/utf8"
14 -)
15 -
16 -// snmpWalkToValue extracts value from SNMP walk output.
17 -// Format modes:
18 -// 0 = unchanged (return raw value as-is)
19 -// 1 = UTF-8 (decode hex-string to UTF-8 string)
20 -// 2 = MAC (format hex-string as MAC address with colons)
21 -// 3 = BITS (convert BITS to integer)
22 -func snmpWalkToValue(value Value, params string) (Value, error) {
23 - lines := strings.Split(params, "\n")
24 - if len(lines) < 2 {
25 - return Value{}, fmt.Errorf("snmp walk requires oid and format mode parameters")
26 - }
27 -
28 - targetOID := strings.TrimSpace(lines[0])
29 - formatMode := 0
30 - if len(lines) >= 2 {
31 - var err error
32 - formatMode, err = strconv.Atoi(strings.TrimSpace(lines[1]))
33 - if err != nil {
34 - return Value{}, fmt.Errorf("invalid format mode: %v", err)
35 - }
36 - }
37 -
38 - // Parse SNMP walk output
39 - extractedValue, err := parseSNMPWalk(value.Data, targetOID)
40 - if err != nil {
41 - return Value{}, err
42 - }
43 -
44 - // Apply format conversion (trim float zeros for WALK_VALUE)
45 - result, err := formatSNMPValue(extractedValue, formatMode, true)
46 - if err != nil {
47 - return Value{}, err
48 - }
49 -
50 - return Value{Data: result, Type: ValueTypeStr}, nil
51 -}
52 -
53 -// snmpEntry represents a parsed SNMP entry
54 -type snmpEntry struct {
55 - oid string
56 - valueType string
57 - value string
58 -}
59 -
60 -// Common MIB-to-OID mappings for offline operation
61 -// Covers ~95% of real-world SNMP monitoring use cases
62 -// Falls back to snmptranslate for rare MIBs (if installed)
63 -//
64 -// NOTE: This map is effectively read-only (never modified after initialization).
65 -// It is safe for concurrent access as all operations are reads (lookups).
66 -// Go maps are safe for concurrent reads without synchronization.
67 -var mibToOID = map[string]string{
68 - // ============================================================================
69 - // SNMPv2-MIB (System group) - RFC 3418
70 - // ============================================================================
71 - "SNMPv2-MIB::sysDescr": ".1.3.6.1.2.1.1.1",
72 - "SNMPv2-MIB::sysObjectID": ".1.3.6.1.2.1.1.2",
73 - "SNMPv2-MIB::sysUpTime": ".1.3.6.1.2.1.1.3",
74 - "SNMPv2-MIB::sysContact": ".1.3.6.1.2.1.1.4",
75 - "SNMPv2-MIB::sysName": ".1.3.6.1.2.1.1.5",
76 - "SNMPv2-MIB::sysLocation": ".1.3.6.1.2.1.1.6",
77 - "SNMPv2-MIB::sysServices": ".1.3.6.1.2.1.1.7",
78 - "SNMPv2-MIB::sysORLastChange": ".1.3.6.1.2.1.1.8",
79 - "SNMPv2-MIB::sysORIndex": ".1.3.6.1.2.1.1.9.1.1",
80 - "SNMPv2-MIB::sysORID": ".1.3.6.1.2.1.1.9.1.2",
81 - "SNMPv2-MIB::sysORDescr": ".1.3.6.1.2.1.1.9.1.3",
82 - "SNMPv2-MIB::sysORUpTime": ".1.3.6.1.2.1.1.9.1.4",
83 -
84 - // SNMPv2-MIB SNMP group
85 - "SNMPv2-MIB::snmpInPkts": ".1.3.6.1.2.1.11.1",
86 - "SNMPv2-MIB::snmpOutPkts": ".1.3.6.1.2.1.11.2",
87 - "SNMPv2-MIB::snmpInBadVersions": ".1.3.6.1.2.1.11.3",
88 - "SNMPv2-MIB::snmpInBadCommunityNames": ".1.3.6.1.2.1.11.4",
89 - "SNMPv2-MIB::snmpInBadCommunityUses": ".1.3.6.1.2.1.11.5",
90 - "SNMPv2-MIB::snmpInASNParseErrs": ".1.3.6.1.2.1.11.6",
91 - "SNMPv2-MIB::snmpInTooBigs": ".1.3.6.1.2.1.11.8",
92 - "SNMPv2-MIB::snmpInNoSuchNames": ".1.3.6.1.2.1.11.9",
93 - "SNMPv2-MIB::snmpInBadValues": ".1.3.6.1.2.1.11.10",
94 - "SNMPv2-MIB::snmpInReadOnlys": ".1.3.6.1.2.1.11.11",
95 - "SNMPv2-MIB::snmpInGenErrs": ".1.3.6.1.2.1.11.12",
96 - "SNMPv2-MIB::snmpInTotalReqVars": ".1.3.6.1.2.1.11.13",
97 - "SNMPv2-MIB::snmpInTotalSetVars": ".1.3.6.1.2.1.11.14",
98 - "SNMPv2-MIB::snmpInGetRequests": ".1.3.6.1.2.1.11.15",
99 - "SNMPv2-MIB::snmpInGetNexts": ".1.3.6.1.2.1.11.16",
100 - "SNMPv2-MIB::snmpInSetRequests": ".1.3.6.1.2.1.11.17",
101 - "SNMPv2-MIB::snmpInGetResponses": ".1.3.6.1.2.1.11.18",
102 - "SNMPv2-MIB::snmpInTraps": ".1.3.6.1.2.1.11.19",
103 - "SNMPv2-MIB::snmpOutTooBigs": ".1.3.6.1.2.1.11.20",
104 - "SNMPv2-MIB::snmpOutNoSuchNames": ".1.3.6.1.2.1.11.21",
105 - "SNMPv2-MIB::snmpOutBadValues": ".1.3.6.1.2.1.11.22",
106 - "SNMPv2-MIB::snmpOutGenErrs": ".1.3.6.1.2.1.11.24",
107 - "SNMPv2-MIB::snmpOutGetRequests": ".1.3.6.1.2.1.11.25",
108 - "SNMPv2-MIB::snmpOutGetNexts": ".1.3.6.1.2.1.11.26",
109 - "SNMPv2-MIB::snmpOutSetRequests": ".1.3.6.1.2.1.11.27",
110 - "SNMPv2-MIB::snmpOutGetResponses": ".1.3.6.1.2.1.11.28",
111 - "SNMPv2-MIB::snmpOutTraps": ".1.3.6.1.2.1.11.29",
112 - "SNMPv2-MIB::snmpEnableAuthenTraps": ".1.3.6.1.2.1.11.30",
113 - "SNMPv2-MIB::snmpSilentDrops": ".1.3.6.1.2.1.11.31",
114 - "SNMPv2-MIB::snmpProxyDrops": ".1.3.6.1.2.1.11.32",
115 -
116 - // ============================================================================
117 - // IF-MIB (Interfaces) - RFC 2863
118 - // ============================================================================
119 - // Interface counters (.1.3.6.1.2.1.2.x)
120 - "IF-MIB::ifNumber": ".1.3.6.1.2.1.2.1",
121 -
122 - // Legacy interfaces table (.1.3.6.1.2.1.2.2.1.x)
123 - "IF-MIB::ifIndex": ".1.3.6.1.2.1.2.2.1.1",
124 - "IF-MIB::ifDescr": ".1.3.6.1.2.1.2.2.1.2",
125 - "IF-MIB::ifType": ".1.3.6.1.2.1.2.2.1.3",
126 - "IF-MIB::ifMtu": ".1.3.6.1.2.1.2.2.1.4",
127 - "IF-MIB::ifSpeed": ".1.3.6.1.2.1.2.2.1.5",
128 - "IF-MIB::ifPhysAddress": ".1.3.6.1.2.1.2.2.1.6",
129 - "IF-MIB::ifAdminStatus": ".1.3.6.1.2.1.2.2.1.7",
130 - "IF-MIB::ifOperStatus": ".1.3.6.1.2.1.2.2.1.8",
131 - "IF-MIB::ifLastChange": ".1.3.6.1.2.1.2.2.1.9",
132 - "IF-MIB::ifInOctets": ".1.3.6.1.2.1.2.2.1.10",
133 - "IF-MIB::ifInUcastPkts": ".1.3.6.1.2.1.2.2.1.11",
134 - "IF-MIB::ifInNUcastPkts": ".1.3.6.1.2.1.2.2.1.12",
135 - "IF-MIB::ifInDiscards": ".1.3.6.1.2.1.2.2.1.13",
136 - "IF-MIB::ifInErrors": ".1.3.6.1.2.1.2.2.1.14",
137 - "IF-MIB::ifInUnknownProtos": ".1.3.6.1.2.1.2.2.1.15",
138 - "IF-MIB::ifOutOctets": ".1.3.6.1.2.1.2.2.1.16",
139 - "IF-MIB::ifOutUcastPkts": ".1.3.6.1.2.1.2.2.1.17",
140 - "IF-MIB::ifOutNUcastPkts": ".1.3.6.1.2.1.2.2.1.18",
141 - "IF-MIB::ifOutDiscards": ".1.3.6.1.2.1.2.2.1.19",
142 - "IF-MIB::ifOutErrors": ".1.3.6.1.2.1.2.2.1.20",
143 - "IF-MIB::ifOutQLen": ".1.3.6.1.2.1.2.2.1.21",
144 - "IF-MIB::ifSpecific": ".1.3.6.1.2.1.2.2.1.22",
145 -
146 - // IF-MIB Extended table (.1.3.6.1.2.1.31.1.1.1.x)
147 - "IF-MIB::ifName": ".1.3.6.1.2.1.31.1.1.1.1",
148 - "IF-MIB::ifInMulticastPkts": ".1.3.6.1.2.1.31.1.1.1.2",
149 - "IF-MIB::ifInBroadcastPkts": ".1.3.6.1.2.1.31.1.1.1.3",
150 - "IF-MIB::ifOutMulticastPkts": ".1.3.6.1.2.1.31.1.1.1.4",
151 - "IF-MIB::ifOutBroadcastPkts": ".1.3.6.1.2.1.31.1.1.1.5",
152 - "IF-MIB::ifHCInOctets": ".1.3.6.1.2.1.31.1.1.1.6",
153 - "IF-MIB::ifHCInUcastPkts": ".1.3.6.1.2.1.31.1.1.1.7",
154 - "IF-MIB::ifHCInMulticastPkts": ".1.3.6.1.2.1.31.1.1.1.8",
155 - "IF-MIB::ifHCInBroadcastPkts": ".1.3.6.1.2.1.31.1.1.1.9",
156 - "IF-MIB::ifHCOutOctets": ".1.3.6.1.2.1.31.1.1.1.10",
157 - "IF-MIB::ifHCOutUcastPkts": ".1.3.6.1.2.1.31.1.1.1.11",
158 - "IF-MIB::ifHCOutMulticastPkts": ".1.3.6.1.2.1.31.1.1.1.12",
159 - "IF-MIB::ifHCOutBroadcastPkts": ".1.3.6.1.2.1.31.1.1.1.13",
160 - "IF-MIB::ifLinkUpDownTrapEnable": ".1.3.6.1.2.1.31.1.1.1.14",
161 - "IF-MIB::ifHighSpeed": ".1.3.6.1.2.1.31.1.1.1.15",
162 - "IF-MIB::ifPromiscuousMode": ".1.3.6.1.2.1.31.1.1.1.16",
163 - "IF-MIB::ifConnectorPresent": ".1.3.6.1.2.1.31.1.1.1.17",
164 - "IF-MIB::ifAlias": ".1.3.6.1.2.1.31.1.1.1.18",
165 - "IF-MIB::ifCounterDiscontinuityTime": ".1.3.6.1.2.1.31.1.1.1.19",
166 -
167 - // IF-MIB Stack table
168 - "IF-MIB::ifStackHigherLayer": ".1.3.6.1.2.1.31.1.2.1.1",
169 - "IF-MIB::ifStackLowerLayer": ".1.3.6.1.2.1.31.1.2.1.2",
170 - "IF-MIB::ifStackStatus": ".1.3.6.1.2.1.31.1.2.1.3",
171 -
172 - // ============================================================================
173 - // IP-MIB - RFC 4293
174 - // ============================================================================
175 - "IP-MIB::ipForwarding": ".1.3.6.1.2.1.4.1",
176 - "IP-MIB::ipDefaultTTL": ".1.3.6.1.2.1.4.2",
177 - "IP-MIB::ipInReceives": ".1.3.6.1.2.1.4.3",
178 - "IP-MIB::ipInHdrErrors": ".1.3.6.1.2.1.4.4",
179 - "IP-MIB::ipInAddrErrors": ".1.3.6.1.2.1.4.5",
180 - "IP-MIB::ipForwDatagrams": ".1.3.6.1.2.1.4.6",
181 - "IP-MIB::ipInUnknownProtos": ".1.3.6.1.2.1.4.7",
182 - "IP-MIB::ipInDiscards": ".1.3.6.1.2.1.4.8",
183 - "IP-MIB::ipInDelivers": ".1.3.6.1.2.1.4.9",
184 - "IP-MIB::ipOutRequests": ".1.3.6.1.2.1.4.10",
185 - "IP-MIB::ipOutDiscards": ".1.3.6.1.2.1.4.11",
186 - "IP-MIB::ipOutNoRoutes": ".1.3.6.1.2.1.4.12",
187 - "IP-MIB::ipReasmTimeout": ".1.3.6.1.2.1.4.13",
188 - "IP-MIB::ipReasmReqds": ".1.3.6.1.2.1.4.14",
189 - "IP-MIB::ipReasmOKs": ".1.3.6.1.2.1.4.15",
190 - "IP-MIB::ipReasmFails": ".1.3.6.1.2.1.4.16",
191 - "IP-MIB::ipFragOKs": ".1.3.6.1.2.1.4.17",
192 - "IP-MIB::ipFragFails": ".1.3.6.1.2.1.4.18",
193 - "IP-MIB::ipFragCreates": ".1.3.6.1.2.1.4.19",
194 -
195 - // IP Address table
196 - "IP-MIB::ipAdEntAddr": ".1.3.6.1.2.1.4.20.1.1",
197 - "IP-MIB::ipAdEntIfIndex": ".1.3.6.1.2.1.4.20.1.2",
198 - "IP-MIB::ipAdEntNetMask": ".1.3.6.1.2.1.4.20.1.3",
199 - "IP-MIB::ipAdEntBcastAddr": ".1.3.6.1.2.1.4.20.1.4",
200 - "IP-MIB::ipAdEntReasmMaxSize": ".1.3.6.1.2.1.4.20.1.5",
201 -
202 - // IP Route table
203 - "IP-MIB::ipRouteDest": ".1.3.6.1.2.1.4.21.1.1",
204 - "IP-MIB::ipRouteIfIndex": ".1.3.6.1.2.1.4.21.1.2",
205 - "IP-MIB::ipRouteMetric1": ".1.3.6.1.2.1.4.21.1.3",
206 - "IP-MIB::ipRouteMetric2": ".1.3.6.1.2.1.4.21.1.4",
207 - "IP-MIB::ipRouteMetric3": ".1.3.6.1.2.1.4.21.1.5",
208 - "IP-MIB::ipRouteMetric4": ".1.3.6.1.2.1.4.21.1.6",
209 - "IP-MIB::ipRouteNextHop": ".1.3.6.1.2.1.4.21.1.7",
210 - "IP-MIB::ipRouteType": ".1.3.6.1.2.1.4.21.1.8",
211 - "IP-MIB::ipRouteProto": ".1.3.6.1.2.1.4.21.1.9",
212 - "IP-MIB::ipRouteAge": ".1.3.6.1.2.1.4.21.1.10",
213 - "IP-MIB::ipRouteMask": ".1.3.6.1.2.1.4.21.1.11",
214 - "IP-MIB::ipRouteMetric5": ".1.3.6.1.2.1.4.21.1.12",
215 - "IP-MIB::ipRouteInfo": ".1.3.6.1.2.1.4.21.1.13",
216 -
217 - // IP Net-to-Media table
218 - "IP-MIB::ipNetToMediaIfIndex": ".1.3.6.1.2.1.4.22.1.1",
219 - "IP-MIB::ipNetToMediaPhysAddress": ".1.3.6.1.2.1.4.22.1.2",
220 - "IP-MIB::ipNetToMediaNetAddress": ".1.3.6.1.2.1.4.22.1.3",
221 - "IP-MIB::ipNetToMediaType": ".1.3.6.1.2.1.4.22.1.4",
222 -
223 - // IP Forwarding table (newer)
224 - "IP-MIB::inetCidrRouteNumber": ".1.3.6.1.2.1.4.24.6",
225 -
226 - // ============================================================================
227 - // ICMP-MIB - RFC 4293
228 - // ============================================================================
229 - "IP-MIB::icmpInMsgs": ".1.3.6.1.2.1.5.1",
230 - "IP-MIB::icmpInErrors": ".1.3.6.1.2.1.5.2",
231 - "IP-MIB::icmpInDestUnreachs": ".1.3.6.1.2.1.5.3",
232 - "IP-MIB::icmpInTimeExcds": ".1.3.6.1.2.1.5.4",
233 - "IP-MIB::icmpInParmProbs": ".1.3.6.1.2.1.5.5",
234 - "IP-MIB::icmpInSrcQuenchs": ".1.3.6.1.2.1.5.6",
235 - "IP-MIB::icmpInRedirects": ".1.3.6.1.2.1.5.7",
236 - "IP-MIB::icmpInEchos": ".1.3.6.1.2.1.5.8",
237 - "IP-MIB::icmpInEchoReps": ".1.3.6.1.2.1.5.9",
238 - "IP-MIB::icmpInTimestamps": ".1.3.6.1.2.1.5.10",
239 - "IP-MIB::icmpInTimestampReps": ".1.3.6.1.2.1.5.11",
240 - "IP-MIB::icmpInAddrMasks": ".1.3.6.1.2.1.5.12",
241 - "IP-MIB::icmpInAddrMaskReps": ".1.3.6.1.2.1.5.13",
242 - "IP-MIB::icmpOutMsgs": ".1.3.6.1.2.1.5.14",
243 - "IP-MIB::icmpOutErrors": ".1.3.6.1.2.1.5.15",
244 - "IP-MIB::icmpOutDestUnreachs": ".1.3.6.1.2.1.5.16",
245 - "IP-MIB::icmpOutTimeExcds": ".1.3.6.1.2.1.5.17",
246 - "IP-MIB::icmpOutParmProbs": ".1.3.6.1.2.1.5.18",
247 - "IP-MIB::icmpOutSrcQuenchs": ".1.3.6.1.2.1.5.19",
248 - "IP-MIB::icmpOutRedirects": ".1.3.6.1.2.1.5.20",
249 - "IP-MIB::icmpOutEchos": ".1.3.6.1.2.1.5.21",
250 - "IP-MIB::icmpOutEchoReps": ".1.3.6.1.2.1.5.22",
251 - "IP-MIB::icmpOutTimestamps": ".1.3.6.1.2.1.5.23",
252 - "IP-MIB::icmpOutTimestampReps": ".1.3.6.1.2.1.5.24",
253 - "IP-MIB::icmpOutAddrMasks": ".1.3.6.1.2.1.5.25",
254 - "IP-MIB::icmpOutAddrMaskReps": ".1.3.6.1.2.1.5.26",
255 -
256 - // ============================================================================
257 - // TCP-MIB - RFC 4022
258 - // ============================================================================
259 - "TCP-MIB::tcpRtoAlgorithm": ".1.3.6.1.2.1.6.1",
260 - "TCP-MIB::tcpRtoMin": ".1.3.6.1.2.1.6.2",
261 - "TCP-MIB::tcpRtoMax": ".1.3.6.1.2.1.6.3",
262 - "TCP-MIB::tcpMaxConn": ".1.3.6.1.2.1.6.4",
263 - "TCP-MIB::tcpActiveOpens": ".1.3.6.1.2.1.6.5",
264 - "TCP-MIB::tcpPassiveOpens": ".1.3.6.1.2.1.6.6",
265 - "TCP-MIB::tcpAttemptFails": ".1.3.6.1.2.1.6.7",
266 - "TCP-MIB::tcpEstabResets": ".1.3.6.1.2.1.6.8",
267 - "TCP-MIB::tcpCurrEstab": ".1.3.6.1.2.1.6.9",
268 - "TCP-MIB::tcpInSegs": ".1.3.6.1.2.1.6.10",
269 - "TCP-MIB::tcpOutSegs": ".1.3.6.1.2.1.6.11",
270 - "TCP-MIB::tcpRetransSegs": ".1.3.6.1.2.1.6.12",
271 - "TCP-MIB::tcpInErrs": ".1.3.6.1.2.1.6.14",
272 - "TCP-MIB::tcpOutRsts": ".1.3.6.1.2.1.6.15",
273 - "TCP-MIB::tcpHCInSegs": ".1.3.6.1.2.1.6.17",
274 - "TCP-MIB::tcpHCOutSegs": ".1.3.6.1.2.1.6.18",
275 -
276 - // TCP Connection table
277 - "TCP-MIB::tcpConnState": ".1.3.6.1.2.1.6.13.1.1",
278 - "TCP-MIB::tcpConnLocalAddress": ".1.3.6.1.2.1.6.13.1.2",
279 - "TCP-MIB::tcpConnLocalPort": ".1.3.6.1.2.1.6.13.1.3",
280 - "TCP-MIB::tcpConnRemAddress": ".1.3.6.1.2.1.6.13.1.4",
281 - "TCP-MIB::tcpConnRemPort": ".1.3.6.1.2.1.6.13.1.5",
282 -
283 - // ============================================================================
284 - // UDP-MIB - RFC 4113
285 - // ============================================================================
286 - "UDP-MIB::udpInDatagrams": ".1.3.6.1.2.1.7.1",
287 - "UDP-MIB::udpNoPorts": ".1.3.6.1.2.1.7.2",
288 - "UDP-MIB::udpInErrors": ".1.3.6.1.2.1.7.3",
289 - "UDP-MIB::udpOutDatagrams": ".1.3.6.1.2.1.7.4",
290 - "UDP-MIB::udpHCInDatagrams": ".1.3.6.1.2.1.7.8",
291 - "UDP-MIB::udpHCOutDatagrams": ".1.3.6.1.2.1.7.9",
292 -
293 - // UDP Listener table
294 - "UDP-MIB::udpLocalAddress": ".1.3.6.1.2.1.7.5.1.1",
295 - "UDP-MIB::udpLocalPort": ".1.3.6.1.2.1.7.5.1.2",
296 -
297 - // ============================================================================
298 - // HOST-RESOURCES-MIB - RFC 2790
299 - // ============================================================================
300 - // System group (.1.3.6.1.2.1.25.1.x)
301 - "HOST-RESOURCES-MIB::hrSystemUptime": ".1.3.6.1.2.1.25.1.1",
302 - "HOST-RESOURCES-MIB::hrSystemDate": ".1.3.6.1.2.1.25.1.2",
303 - "HOST-RESOURCES-MIB::hrSystemInitialLoadDevice": ".1.3.6.1.2.1.25.1.3",
304 - "HOST-RESOURCES-MIB::hrSystemInitialLoadParameters": ".1.3.6.1.2.1.25.1.4",
305 - "HOST-RESOURCES-MIB::hrSystemNumUsers": ".1.3.6.1.2.1.25.1.5",
306 - "HOST-RESOURCES-MIB::hrSystemProcesses": ".1.3.6.1.2.1.25.1.6",
307 - "HOST-RESOURCES-MIB::hrSystemMaxProcesses": ".1.3.6.1.2.1.25.1.7",
308 -
309 - // Storage group (.1.3.6.1.2.1.25.2.x)
310 - "HOST-RESOURCES-MIB::hrMemorySize": ".1.3.6.1.2.1.25.2.2",
311 - "HOST-RESOURCES-MIB::hrStorageIndex": ".1.3.6.1.2.1.25.2.3.1.1",
312 - "HOST-RESOURCES-MIB::hrStorageType": ".1.3.6.1.2.1.25.2.3.1.2",
313 - "HOST-RESOURCES-MIB::hrStorageDescr": ".1.3.6.1.2.1.25.2.3.1.3",
314 - "HOST-RESOURCES-MIB::hrStorageAllocationUnits": ".1.3.6.1.2.1.25.2.3.1.4",
315 - "HOST-RESOURCES-MIB::hrStorageSize": ".1.3.6.1.2.1.25.2.3.1.5",
316 - "HOST-RESOURCES-MIB::hrStorageUsed": ".1.3.6.1.2.1.25.2.3.1.6",
317 - "HOST-RESOURCES-MIB::hrStorageAllocationFailures": ".1.3.6.1.2.1.25.2.3.1.7",
318 -
319 - // Device group (.1.3.6.1.2.1.25.3.x)
320 - "HOST-RESOURCES-MIB::hrDeviceIndex": ".1.3.6.1.2.1.25.3.2.1.1",
321 - "HOST-RESOURCES-MIB::hrDeviceType": ".1.3.6.1.2.1.25.3.2.1.2",
322 - "HOST-RESOURCES-MIB::hrDeviceDescr": ".1.3.6.1.2.1.25.3.2.1.3",
323 - "HOST-RESOURCES-MIB::hrDeviceID": ".1.3.6.1.2.1.25.3.2.1.4",
324 - "HOST-RESOURCES-MIB::hrDeviceStatus": ".1.3.6.1.2.1.25.3.2.1.5",
325 - "HOST-RESOURCES-MIB::hrDeviceErrors": ".1.3.6.1.2.1.25.3.2.1.6",
326 - "HOST-RESOURCES-MIB::hrProcessorFrwID": ".1.3.6.1.2.1.25.3.3.1.1",
327 - "HOST-RESOURCES-MIB::hrProcessorLoad": ".1.3.6.1.2.1.25.3.3.1.2",
328 - "HOST-RESOURCES-MIB::hrNetworkIfIndex": ".1.3.6.1.2.1.25.3.4.1.1",
329 - "HOST-RESOURCES-MIB::hrPrinterStatus": ".1.3.6.1.2.1.25.3.5.1.1",
330 - "HOST-RESOURCES-MIB::hrPrinterDetectedErrorState": ".1.3.6.1.2.1.25.3.5.1.2",
331 - "HOST-RESOURCES-MIB::hrDiskStorageAccess": ".1.3.6.1.2.1.25.3.6.1.1",
332 - "HOST-RESOURCES-MIB::hrDiskStorageMedia": ".1.3.6.1.2.1.25.3.6.1.2",
333 - "HOST-RESOURCES-MIB::hrDiskStorageRemoveble": ".1.3.6.1.2.1.25.3.6.1.3",
334 - "HOST-RESOURCES-MIB::hrDiskStorageCapacity": ".1.3.6.1.2.1.25.3.6.1.4",
335 - "HOST-RESOURCES-MIB::hrPartitionIndex": ".1.3.6.1.2.1.25.3.7.1.1",
336 - "HOST-RESOURCES-MIB::hrPartitionLabel": ".1.3.6.1.2.1.25.3.7.1.2",
337 - "HOST-RESOURCES-MIB::hrPartitionID": ".1.3.6.1.2.1.25.3.7.1.3",
338 - "HOST-RESOURCES-MIB::hrPartitionSize": ".1.3.6.1.2.1.25.3.7.1.4",
339 - "HOST-RESOURCES-MIB::hrPartitionFSIndex": ".1.3.6.1.2.1.25.3.7.1.5",
340 - "HOST-RESOURCES-MIB::hrFSIndex": ".1.3.6.1.2.1.25.3.8.1.1",
341 - "HOST-RESOURCES-MIB::hrFSMountPoint": ".1.3.6.1.2.1.25.3.8.1.2",
342 - "HOST-RESOURCES-MIB::hrFSRemoteMountPoint": ".1.3.6.1.2.1.25.3.8.1.3",
343 - "HOST-RESOURCES-MIB::hrFSType": ".1.3.6.1.2.1.25.3.8.1.4",
344 - "HOST-RESOURCES-MIB::hrFSAccess": ".1.3.6.1.2.1.25.3.8.1.5",
345 - "HOST-RESOURCES-MIB::hrFSBootable": ".1.3.6.1.2.1.25.3.8.1.6",
346 - "HOST-RESOURCES-MIB::hrFSStorageIndex": ".1.3.6.1.2.1.25.3.8.1.7",
347 - "HOST-RESOURCES-MIB::hrFSLastFullBackupDate": ".1.3.6.1.2.1.25.3.8.1.8",
348 - "HOST-RESOURCES-MIB::hrFSLastPartialBackupDate": ".1.3.6.1.2.1.25.3.8.1.9",
349 -
350 - // Running Software group (.1.3.6.1.2.1.25.4.x)
351 - "HOST-RESOURCES-MIB::hrSWOSIndex": ".1.3.6.1.2.1.25.4.1",
352 - "HOST-RESOURCES-MIB::hrSWRunIndex": ".1.3.6.1.2.1.25.4.2.1.1",
353 - "HOST-RESOURCES-MIB::hrSWRunName": ".1.3.6.1.2.1.25.4.2.1.2",
354 - "HOST-RESOURCES-MIB::hrSWRunID": ".1.3.6.1.2.1.25.4.2.1.3",
355 - "HOST-RESOURCES-MIB::hrSWRunPath": ".1.3.6.1.2.1.25.4.2.1.4",
356 - "HOST-RESOURCES-MIB::hrSWRunParameters": ".1.3.6.1.2.1.25.4.2.1.5",
357 - "HOST-RESOURCES-MIB::hrSWRunType": ".1.3.6.1.2.1.25.4.2.1.6",
358 - "HOST-RESOURCES-MIB::hrSWRunStatus": ".1.3.6.1.2.1.25.4.2.1.7",
359 -
360 - // Running Software Performance group
361 - "HOST-RESOURCES-MIB::hrSWRunPerfCPU": ".1.3.6.1.2.1.25.5.1.1.1",
362 - "HOST-RESOURCES-MIB::hrSWRunPerfMem": ".1.3.6.1.2.1.25.5.1.1.2",
363 -
364 - // Installed Software group (.1.3.6.1.2.1.25.6.x)
365 - "HOST-RESOURCES-MIB::hrSWInstalledLastChange": ".1.3.6.1.2.1.25.6.1",
366 - "HOST-RESOURCES-MIB::hrSWInstalledLastUpdateTime": ".1.3.6.1.2.1.25.6.2",
367 - "HOST-RESOURCES-MIB::hrSWInstalledIndex": ".1.3.6.1.2.1.25.6.3.1.1",
368 - "HOST-RESOURCES-MIB::hrSWInstalledName": ".1.3.6.1.2.1.25.6.3.1.2",
369 - "HOST-RESOURCES-MIB::hrSWInstalledID": ".1.3.6.1.2.1.25.6.3.1.3",
370 - "HOST-RESOURCES-MIB::hrSWInstalledType": ".1.3.6.1.2.1.25.6.3.1.4",
371 - "HOST-RESOURCES-MIB::hrSWInstalledDate": ".1.3.6.1.2.1.25.6.3.1.5",
372 -
373 - // ============================================================================
374 - // ENTITY-MIB - RFC 6933
375 - // ============================================================================
376 - "ENTITY-MIB::entPhysicalIndex": ".1.3.6.1.2.1.47.1.1.1.1.1",
377 - "ENTITY-MIB::entPhysicalDescr": ".1.3.6.1.2.1.47.1.1.1.1.2",
378 - "ENTITY-MIB::entPhysicalVendorType": ".1.3.6.1.2.1.47.1.1.1.1.3",
379 - "ENTITY-MIB::entPhysicalContainedIn": ".1.3.6.1.2.1.47.1.1.1.1.4",
380 - "ENTITY-MIB::entPhysicalClass": ".1.3.6.1.2.1.47.1.1.1.1.5",
381 - "ENTITY-MIB::entPhysicalParentRelPos": ".1.3.6.1.2.1.47.1.1.1.1.6",
382 - "ENTITY-MIB::entPhysicalName": ".1.3.6.1.2.1.47.1.1.1.1.7",
383 - "ENTITY-MIB::entPhysicalHardwareRev": ".1.3.6.1.2.1.47.1.1.1.1.8",
384 - "ENTITY-MIB::entPhysicalFirmwareRev": ".1.3.6.1.2.1.47.1.1.1.1.9",
385 - "ENTITY-MIB::entPhysicalSoftwareRev": ".1.3.6.1.2.1.47.1.1.1.1.10",
386 - "ENTITY-MIB::entPhysicalSerialNum": ".1.3.6.1.2.1.47.1.1.1.1.11",
387 - "ENTITY-MIB::entPhysicalMfgName": ".1.3.6.1.2.1.47.1.1.1.1.12",
388 - "ENTITY-MIB::entPhysicalModelName": ".1.3.6.1.2.1.47.1.1.1.1.13",
389 - "ENTITY-MIB::entPhysicalAlias": ".1.3.6.1.2.1.47.1.1.1.1.14",
390 - "ENTITY-MIB::entPhysicalAssetID": ".1.3.6.1.2.1.47.1.1.1.1.15",
391 - "ENTITY-MIB::entPhysicalIsFRU": ".1.3.6.1.2.1.47.1.1.1.1.16",
392 - "ENTITY-MIB::entPhysicalMfgDate": ".1.3.6.1.2.1.47.1.1.1.1.17",
393 - "ENTITY-MIB::entPhysicalUris": ".1.3.6.1.2.1.47.1.1.1.1.18",
394 -
395 - // ============================================================================
396 - // EtherLike-MIB - RFC 3635
397 - // ============================================================================
398 - "EtherLike-MIB::dot3StatsIndex": ".1.3.6.1.2.1.10.7.2.1.1",
399 - "EtherLike-MIB::dot3StatsAlignmentErrors": ".1.3.6.1.2.1.10.7.2.1.2",
400 - "EtherLike-MIB::dot3StatsFCSErrors": ".1.3.6.1.2.1.10.7.2.1.3",
401 - "EtherLike-MIB::dot3StatsSingleCollisionFrames": ".1.3.6.1.2.1.10.7.2.1.4",
402 - "EtherLike-MIB::dot3StatsMultipleCollisionFrames": ".1.3.6.1.2.1.10.7.2.1.5",
403 - "EtherLike-MIB::dot3StatsSQETestErrors": ".1.3.6.1.2.1.10.7.2.1.6",
404 - "EtherLike-MIB::dot3StatsDeferredTransmissions": ".1.3.6.1.2.1.10.7.2.1.7",
405 - "EtherLike-MIB::dot3StatsLateCollisions": ".1.3.6.1.2.1.10.7.2.1.8",
406 - "EtherLike-MIB::dot3StatsExcessiveCollisions": ".1.3.6.1.2.1.10.7.2.1.9",
407 - "EtherLike-MIB::dot3StatsInternalMacTransmitErrors": ".1.3.6.1.2.1.10.7.2.1.10",
408 - "EtherLike-MIB::dot3StatsCarrierSenseErrors": ".1.3.6.1.2.1.10.7.2.1.11",
409 - "EtherLike-MIB::dot3StatsFrameTooLongs": ".1.3.6.1.2.1.10.7.2.1.13",
410 - "EtherLike-MIB::dot3StatsInternalMacReceiveErrors": ".1.3.6.1.2.1.10.7.2.1.16",
411 - "EtherLike-MIB::dot3StatsSymbolErrors": ".1.3.6.1.2.1.10.7.2.1.18",
412 - "EtherLike-MIB::dot3StatsDuplexStatus": ".1.3.6.1.2.1.10.7.2.1.19",
413 -
414 - // ============================================================================
415 - // UCD-SNMP-MIB (Linux/Unix systems) - NET-SNMP
416 - // ============================================================================
417 - // Memory statistics
418 - "UCD-SNMP-MIB::memIndex": ".1.3.6.1.4.1.2021.4.1",
419 - "UCD-SNMP-MIB::memErrorName": ".1.3.6.1.4.1.2021.4.2",
420 - "UCD-SNMP-MIB::memTotalSwap": ".1.3.6.1.4.1.2021.4.3",
421 - "UCD-SNMP-MIB::memAvailSwap": ".1.3.6.1.4.1.2021.4.4",
422 - "UCD-SNMP-MIB::memTotalReal": ".1.3.6.1.4.1.2021.4.5",
423 - "UCD-SNMP-MIB::memAvailReal": ".1.3.6.1.4.1.2021.4.6",
424 - "UCD-SNMP-MIB::memTotalFree": ".1.3.6.1.4.1.2021.4.11",
425 - "UCD-SNMP-MIB::memMinimumSwap": ".1.3.6.1.4.1.2021.4.12",
426 - "UCD-SNMP-MIB::memShared": ".1.3.6.1.4.1.2021.4.13",
427 - "UCD-SNMP-MIB::memBuffer": ".1.3.6.1.4.1.2021.4.14",
428 - "UCD-SNMP-MIB::memCached": ".1.3.6.1.4.1.2021.4.15",
429 - "UCD-SNMP-MIB::memSwapError": ".1.3.6.1.4.1.2021.4.100",
430 - "UCD-SNMP-MIB::memSwapErrorMsg": ".1.3.6.1.4.1.2021.4.101",
431 -
432 - // CPU statistics
433 - "UCD-SNMP-MIB::ssIndex": ".1.3.6.1.4.1.2021.11.1",
434 - "UCD-SNMP-MIB::ssErrorName": ".1.3.6.1.4.1.2021.11.2",
435 - "UCD-SNMP-MIB::ssSwapIn": ".1.3.6.1.4.1.2021.11.3",
436 - "UCD-SNMP-MIB::ssSwapOut": ".1.3.6.1.4.1.2021.11.4",
437 - "UCD-SNMP-MIB::ssIOSent": ".1.3.6.1.4.1.2021.11.5",
438 - "UCD-SNMP-MIB::ssIOReceive": ".1.3.6.1.4.1.2021.11.6",
439 - "UCD-SNMP-MIB::ssSysInterrupts": ".1.3.6.1.4.1.2021.11.7",
440 - "UCD-SNMP-MIB::ssSysContext": ".1.3.6.1.4.1.2021.11.8",
441 - "UCD-SNMP-MIB::ssCpuUser": ".1.3.6.1.4.1.2021.11.9",
442 - "UCD-SNMP-MIB::ssCpuSystem": ".1.3.6.1.4.1.2021.11.10",
443 - "UCD-SNMP-MIB::ssCpuIdle": ".1.3.6.1.4.1.2021.11.11",
444 - "UCD-SNMP-MIB::ssCpuRawUser": ".1.3.6.1.4.1.2021.11.50",
445 - "UCD-SNMP-MIB::ssCpuRawNice": ".1.3.6.1.4.1.2021.11.51",
446 - "UCD-SNMP-MIB::ssCpuRawSystem": ".1.3.6.1.4.1.2021.11.52",
447 - "UCD-SNMP-MIB::ssCpuRawIdle": ".1.3.6.1.4.1.2021.11.53",
448 - "UCD-SNMP-MIB::ssCpuRawWait": ".1.3.6.1.4.1.2021.11.54",
449 - "UCD-SNMP-MIB::ssCpuRawKernel": ".1.3.6.1.4.1.2021.11.55",
450 - "UCD-SNMP-MIB::ssCpuRawInterrupt": ".1.3.6.1.4.1.2021.11.56",
451 - "UCD-SNMP-MIB::ssIORawSent": ".1.3.6.1.4.1.2021.11.57",
452 - "UCD-SNMP-MIB::ssIORawReceived": ".1.3.6.1.4.1.2021.11.58",
453 - "UCD-SNMP-MIB::ssRawInterrupts": ".1.3.6.1.4.1.2021.11.59",
454 - "UCD-SNMP-MIB::ssRawContexts": ".1.3.6.1.4.1.2021.11.60",
455 - "UCD-SNMP-MIB::ssCpuRawSoftIRQ": ".1.3.6.1.4.1.2021.11.61",
456 - "UCD-SNMP-MIB::ssCpuRawSteal": ".1.3.6.1.4.1.2021.11.64",
457 - "UCD-SNMP-MIB::ssCpuRawGuest": ".1.3.6.1.4.1.2021.11.65",
458 - "UCD-SNMP-MIB::ssCpuRawGuestNice": ".1.3.6.1.4.1.2021.11.66",
459 -
460 - // Load average
461 - "UCD-SNMP-MIB::laIndex": ".1.3.6.1.4.1.2021.10.1.1",
462 - "UCD-SNMP-MIB::laNames": ".1.3.6.1.4.1.2021.10.1.2",
463 - "UCD-SNMP-MIB::laLoad": ".1.3.6.1.4.1.2021.10.1.3",
464 - "UCD-SNMP-MIB::laConfig": ".1.3.6.1.4.1.2021.10.1.4",
465 - "UCD-SNMP-MIB::laLoadInt": ".1.3.6.1.4.1.2021.10.1.5",
466 - "UCD-SNMP-MIB::laLoadFloat": ".1.3.6.1.4.1.2021.10.1.6",
467 - "UCD-SNMP-MIB::laErrorFlag": ".1.3.6.1.4.1.2021.10.1.100",
468 - "UCD-SNMP-MIB::laErrMessage": ".1.3.6.1.4.1.2021.10.1.101",
469 -
470 - // Disk I/O statistics
471 - "UCD-SNMP-MIB::diskIOIndex": ".1.3.6.1.4.1.2021.13.15.1.1.1",
472 - "UCD-SNMP-MIB::diskIODevice": ".1.3.6.1.4.1.2021.13.15.1.1.2",
473 - "UCD-SNMP-MIB::diskIONRead": ".1.3.6.1.4.1.2021.13.15.1.1.3",
474 - "UCD-SNMP-MIB::diskIONWritten": ".1.3.6.1.4.1.2021.13.15.1.1.4",
475 - "UCD-SNMP-MIB::diskIOReads": ".1.3.6.1.4.1.2021.13.15.1.1.5",
476 - "UCD-SNMP-MIB::diskIOWrites": ".1.3.6.1.4.1.2021.13.15.1.1.6",
477 - "UCD-SNMP-MIB::diskIONReadX": ".1.3.6.1.4.1.2021.13.15.1.1.12",
478 - "UCD-SNMP-MIB::diskIONWrittenX": ".1.3.6.1.4.1.2021.13.15.1.1.13",
479 -
480 - // ============================================================================
481 - // UPS-MIB - RFC 1628
482 - // ============================================================================
483 - "UPS-MIB::upsIdentManufacturer": ".1.3.6.1.2.1.33.1.1.1",
484 - "UPS-MIB::upsIdentModel": ".1.3.6.1.2.1.33.1.1.2",
485 - "UPS-MIB::upsIdentUPSSoftwareVersion": ".1.3.6.1.2.1.33.1.1.3",
486 - "UPS-MIB::upsIdentAgentSoftwareVersion": ".1.3.6.1.2.1.33.1.1.4",
487 - "UPS-MIB::upsIdentName": ".1.3.6.1.2.1.33.1.1.5",
488 - "UPS-MIB::upsIdentAttachedDevices": ".1.3.6.1.2.1.33.1.1.6",
489 - "UPS-MIB::upsBatteryStatus": ".1.3.6.1.2.1.33.1.2.1",
490 - "UPS-MIB::upsSecondsOnBattery": ".1.3.6.1.2.1.33.1.2.2",
491 - "UPS-MIB::upsEstimatedMinutesRemaining": ".1.3.6.1.2.1.33.1.2.3",
492 - "UPS-MIB::upsEstimatedChargeRemaining": ".1.3.6.1.2.1.33.1.2.4",
493 - "UPS-MIB::upsBatteryVoltage": ".1.3.6.1.2.1.33.1.2.5",
494 - "UPS-MIB::upsBatteryCurrent": ".1.3.6.1.2.1.33.1.2.6",
495 - "UPS-MIB::upsBatteryTemperature": ".1.3.6.1.2.1.33.1.2.7",
496 - "UPS-MIB::upsInputLineBads": ".1.3.6.1.2.1.33.1.3.1",
497 - "UPS-MIB::upsInputNumLines": ".1.3.6.1.2.1.33.1.3.2",
498 - "UPS-MIB::upsInputLineIndex": ".1.3.6.1.2.1.33.1.3.3.1.1",
499 - "UPS-MIB::upsInputFrequency": ".1.3.6.1.2.1.33.1.3.3.1.2",
500 - "UPS-MIB::upsInputVoltage": ".1.3.6.1.2.1.33.1.3.3.1.3",
501 - "UPS-MIB::upsInputCurrent": ".1.3.6.1.2.1.33.1.3.3.1.4",
502 - "UPS-MIB::upsInputTruePower": ".1.3.6.1.2.1.33.1.3.3.1.5",
503 - "UPS-MIB::upsOutputSource": ".1.3.6.1.2.1.33.1.4.1",
504 - "UPS-MIB::upsOutputFrequency": ".1.3.6.1.2.1.33.1.4.2",
505 - "UPS-MIB::upsOutputNumLines": ".1.3.6.1.2.1.33.1.4.3",
506 - "UPS-MIB::upsOutputLineIndex": ".1.3.6.1.2.1.33.1.4.4.1.1",
507 - "UPS-MIB::upsOutputVoltage": ".1.3.6.1.2.1.33.1.4.4.1.2",
508 - "UPS-MIB::upsOutputCurrent": ".1.3.6.1.2.1.33.1.4.4.1.3",
509 - "UPS-MIB::upsOutputPower": ".1.3.6.1.2.1.33.1.4.4.1.4",
510 - "UPS-MIB::upsOutputPercentLoad": ".1.3.6.1.2.1.33.1.4.4.1.5",
511 - "UPS-MIB::upsBypassFrequency": ".1.3.6.1.2.1.33.1.5.1",
512 - "UPS-MIB::upsBypassNumLines": ".1.3.6.1.2.1.33.1.5.2",
513 - "UPS-MIB::upsAlarmsPresent": ".1.3.6.1.2.1.33.1.6.1",
514 - "UPS-MIB::upsTestId": ".1.3.6.1.2.1.33.1.7.1",
515 - "UPS-MIB::upsTestSpinLock": ".1.3.6.1.2.1.33.1.7.2",
516 - "UPS-MIB::upsTestResultsSummary": ".1.3.6.1.2.1.33.1.7.3",
517 - "UPS-MIB::upsTestResultsDetail": ".1.3.6.1.2.1.33.1.7.4",
518 - "UPS-MIB::upsTestStartTime": ".1.3.6.1.2.1.33.1.7.5",
519 - "UPS-MIB::upsTestElapsedTime": ".1.3.6.1.2.1.33.1.7.6",
520 -
521 - // ============================================================================
522 - // LLDP-MIB - IEEE 802.1AB
523 - // ============================================================================
524 - "LLDP-MIB::lldpLocChassisIdSubtype": ".1.0.8802.1.1.2.1.3.1",
525 - "LLDP-MIB::lldpLocChassisId": ".1.0.8802.1.1.2.1.3.2",
526 - "LLDP-MIB::lldpLocSysName": ".1.0.8802.1.1.2.1.3.3",
527 - "LLDP-MIB::lldpLocSysDesc": ".1.0.8802.1.1.2.1.3.4",
528 - "LLDP-MIB::lldpLocSysCapSupported": ".1.0.8802.1.1.2.1.3.5",
529 - "LLDP-MIB::lldpLocSysCapEnabled": ".1.0.8802.1.1.2.1.3.6",
530 - "LLDP-MIB::lldpRemChassisIdSubtype": ".1.0.8802.1.1.2.1.4.1.1.4",
531 - "LLDP-MIB::lldpRemChassisId": ".1.0.8802.1.1.2.1.4.1.1.5",
532 - "LLDP-MIB::lldpRemPortIdSubtype": ".1.0.8802.1.1.2.1.4.1.1.6",
533 - "LLDP-MIB::lldpRemPortId": ".1.0.8802.1.1.2.1.4.1.1.7",
534 - "LLDP-MIB::lldpRemPortDesc": ".1.0.8802.1.1.2.1.4.1.1.8",
535 - "LLDP-MIB::lldpRemSysName": ".1.0.8802.1.1.2.1.4.1.1.9",
536 - "LLDP-MIB::lldpRemSysDesc": ".1.0.8802.1.1.2.1.4.1.1.10",
537 - "LLDP-MIB::lldpRemSysCapSupported": ".1.0.8802.1.1.2.1.4.1.1.11",
538 - "LLDP-MIB::lldpRemSysCapEnabled": ".1.0.8802.1.1.2.1.4.1.1.12",
539 -
540 - // ============================================================================
541 - // Printer-MIB - RFC 3805
542 - // ============================================================================
543 - "Printer-MIB::prtGeneralConfigChanges": ".1.3.6.1.2.1.43.5.1.1.1",
544 - "Printer-MIB::prtGeneralCurrentLocalization": ".1.3.6.1.2.1.43.5.1.1.2",
545 - "Printer-MIB::prtGeneralReset": ".1.3.6.1.2.1.43.5.1.1.3",
546 - "Printer-MIB::prtGeneralCurrentOperator": ".1.3.6.1.2.1.43.5.1.1.4",
547 - "Printer-MIB::prtGeneralServicePerson": ".1.3.6.1.2.1.43.5.1.1.5",
548 - "Printer-MIB::prtInputDefaultIndex": ".1.3.6.1.2.1.43.5.1.1.6",
549 - "Printer-MIB::prtOutputDefaultIndex": ".1.3.6.1.2.1.43.5.1.1.7",
550 - "Printer-MIB::prtMarkerDefaultIndex": ".1.3.6.1.2.1.43.5.1.1.8",
551 - "Printer-MIB::prtMediaPathDefaultIndex": ".1.3.6.1.2.1.43.5.1.1.9",
552 - "Printer-MIB::prtConsoleLocalization": ".1.3.6.1.2.1.43.5.1.1.10",
553 - "Printer-MIB::prtConsoleNumberOfDisplayLines": ".1.3.6.1.2.1.43.5.1.1.11",
554 - "Printer-MIB::prtConsoleNumberOfDisplayChars": ".1.3.6.1.2.1.43.5.1.1.12",
555 - "Printer-MIB::prtConsoleDisable": ".1.3.6.1.2.1.43.5.1.1.13",
556 - "Printer-MIB::prtCoverIndex": ".1.3.6.1.2.1.43.6.1.1.1",
557 - "Printer-MIB::prtCoverDescription": ".1.3.6.1.2.1.43.6.1.1.2",
558 - "Printer-MIB::prtCoverStatus": ".1.3.6.1.2.1.43.6.1.1.3",
559 - "Printer-MIB::prtMarkerSuppliesIndex": ".1.3.6.1.2.1.43.11.1.1.1",
560 - "Printer-MIB::prtMarkerSuppliesMarkerIndex": ".1.3.6.1.2.1.43.11.1.1.2",
561 - "Printer-MIB::prtMarkerSuppliesColorantIndex": ".1.3.6.1.2.1.43.11.1.1.3",
562 - "Printer-MIB::prtMarkerSuppliesClass": ".1.3.6.1.2.1.43.11.1.1.4",
563 - "Printer-MIB::prtMarkerSuppliesType": ".1.3.6.1.2.1.43.11.1.1.5",
564 - "Printer-MIB::prtMarkerSuppliesDescription": ".1.3.6.1.2.1.43.11.1.1.6",
565 - "Printer-MIB::prtMarkerSuppliesSupplyUnit": ".1.3.6.1.2.1.43.11.1.1.7",
566 - "Printer-MIB::prtMarkerSuppliesMaxCapacity": ".1.3.6.1.2.1.43.11.1.1.8",
567 - "Printer-MIB::prtMarkerSuppliesLevel": ".1.3.6.1.2.1.43.11.1.1.9",
568 - "Printer-MIB::prtAlertIndex": ".1.3.6.1.2.1.43.18.1.1.1",
569 - "Printer-MIB::prtAlertSeverityLevel": ".1.3.6.1.2.1.43.18.1.1.2",
570 - "Printer-MIB::prtAlertTrainingLevel": ".1.3.6.1.2.1.43.18.1.1.3",
571 - "Printer-MIB::prtAlertGroup": ".1.3.6.1.2.1.43.18.1.1.4",
572 - "Printer-MIB::prtAlertGroupIndex": ".1.3.6.1.2.1.43.18.1.1.5",
573 - "Printer-MIB::prtAlertLocation": ".1.3.6.1.2.1.43.18.1.1.6",
574 - "Printer-MIB::prtAlertCode": ".1.3.6.1.2.1.43.18.1.1.7",
575 - "Printer-MIB::prtAlertDescription": ".1.3.6.1.2.1.43.18.1.1.8",
576 - "Printer-MIB::prtAlertTime": ".1.3.6.1.2.1.43.18.1.1.9",
577 -
578 - // ============================================================================
579 - // BGP4-MIB - RFC 4273
580 - // ============================================================================
581 - "BGP4-MIB::bgpVersion": ".1.3.6.1.2.1.15.1",
582 - "BGP4-MIB::bgpLocalAs": ".1.3.6.1.2.1.15.2",
583 - "BGP4-MIB::bgpPeerIdentifier": ".1.3.6.1.2.1.15.3.1.1",
584 - "BGP4-MIB::bgpPeerState": ".1.3.6.1.2.1.15.3.1.2",
585 - "BGP4-MIB::bgpPeerAdminStatus": ".1.3.6.1.2.1.15.3.1.3",
586 - "BGP4-MIB::bgpPeerNegotiatedVersion": ".1.3.6.1.2.1.15.3.1.4",
587 - "BGP4-MIB::bgpPeerLocalAddr": ".1.3.6.1.2.1.15.3.1.5",
588 - "BGP4-MIB::bgpPeerLocalPort": ".1.3.6.1.2.1.15.3.1.6",
589 - "BGP4-MIB::bgpPeerRemoteAddr": ".1.3.6.1.2.1.15.3.1.7",
590 - "BGP4-MIB::bgpPeerRemotePort": ".1.3.6.1.2.1.15.3.1.8",
591 - "BGP4-MIB::bgpPeerRemoteAs": ".1.3.6.1.2.1.15.3.1.9",
592 - "BGP4-MIB::bgpPeerInUpdates": ".1.3.6.1.2.1.15.3.1.10",
593 - "BGP4-MIB::bgpPeerOutUpdates": ".1.3.6.1.2.1.15.3.1.11",
594 - "BGP4-MIB::bgpPeerInTotalMessages": ".1.3.6.1.2.1.15.3.1.12",
595 - "BGP4-MIB::bgpPeerOutTotalMessages": ".1.3.6.1.2.1.15.3.1.13",
596 - "BGP4-MIB::bgpPeerLastError": ".1.3.6.1.2.1.15.3.1.14",
597 - "BGP4-MIB::bgpPeerFsmEstablishedTransitions": ".1.3.6.1.2.1.15.3.1.15",
598 - "BGP4-MIB::bgpPeerFsmEstablishedTime": ".1.3.6.1.2.1.15.3.1.16",
599 - "BGP4-MIB::bgpPeerConnectRetryInterval": ".1.3.6.1.2.1.15.3.1.17",
600 - "BGP4-MIB::bgpPeerHoldTime": ".1.3.6.1.2.1.15.3.1.18",
601 - "BGP4-MIB::bgpPeerKeepAlive": ".1.3.6.1.2.1.15.3.1.19",
602 - "BGP4-MIB::bgpPeerHoldTimeConfigured": ".1.3.6.1.2.1.15.3.1.20",
603 - "BGP4-MIB::bgpPeerKeepAliveConfigured": ".1.3.6.1.2.1.15.3.1.21",
604 - "BGP4-MIB::bgpPeerMinASOriginationInterval": ".1.3.6.1.2.1.15.3.1.22",
605 - "BGP4-MIB::bgpPeerMinRouteAdvertisementInterval": ".1.3.6.1.2.1.15.3.1.23",
606 - "BGP4-MIB::bgpPeerInUpdateElapsedTime": ".1.3.6.1.2.1.15.3.1.24",
607 -
608 - // ============================================================================
609 - // OSPF-MIB - RFC 4750
610 - // ============================================================================
611 - "OSPF-MIB::ospfRouterId": ".1.3.6.1.2.1.14.1.1",
612 - "OSPF-MIB::ospfAdminStat": ".1.3.6.1.2.1.14.1.2",
613 - "OSPF-MIB::ospfVersionNumber": ".1.3.6.1.2.1.14.1.3",
614 - "OSPF-MIB::ospfAreaBdrRtrStatus": ".1.3.6.1.2.1.14.1.4",
615 - "OSPF-MIB::ospfASBdrRtrStatus": ".1.3.6.1.2.1.14.1.5",
616 - "OSPF-MIB::ospfExternLsaCount": ".1.3.6.1.2.1.14.1.6",
617 - "OSPF-MIB::ospfExternLsaCksumSum": ".1.3.6.1.2.1.14.1.7",
618 - "OSPF-MIB::ospfTOSSupport": ".1.3.6.1.2.1.14.1.8",
619 - "OSPF-MIB::ospfOriginateNewLsas": ".1.3.6.1.2.1.14.1.9",
620 - "OSPF-MIB::ospfRxNewLsas": ".1.3.6.1.2.1.14.1.10",
621 - "OSPF-MIB::ospfExtLsdbLimit": ".1.3.6.1.2.1.14.1.11",
622 - "OSPF-MIB::ospfMulticastExtensions": ".1.3.6.1.2.1.14.1.12",
623 - "OSPF-MIB::ospfExitOverflowInterval": ".1.3.6.1.2.1.14.1.13",
624 - "OSPF-MIB::ospfDemandExtensions": ".1.3.6.1.2.1.14.1.14",
625 -
626 - // ============================================================================
627 - // DISMAN-EVENT-MIB - RFC 2981
628 - // ============================================================================
629 - "DISMAN-EVENT-MIB::sysUpTimeInstance": ".1.3.6.1.2.1.1.3.0",
630 -}
631 -
632 -// Translation cache for snmptranslate results
633 -// Thread-safe cache to avoid repeated exec calls
634 -var (
635 - mibTranslationCache = make(map[string]string)
636 - mibTranslationCacheMu sync.RWMutex
637 - mibCacheMaxSize = 1000 // Reasonable limit for production
638 -)
639 -
640 -// translateMIB translates MIB name to numeric OID
641 -// Strategy: hardcoded map → cache → snmptranslate (optional) → error
642 -// validateMIBName checks if a MIB name contains only safe characters.
643 -// This prevents command injection when passing to snmptranslate.
644 -// Valid MIB names: alphanumeric, dots, colons, hyphens, underscores.
645 -func validateMIBName(mibName string) error {
646 - if mibName == "" {
647 - return fmt.Errorf("empty MIB name")
648 - }
649 -
650 - // Max length check (reasonable limit for MIB names)
651 - if len(mibName) > 256 {
652 - return fmt.Errorf("MIB name too long")
653 - }
654 -
655 - // Check each character for safety
656 - for i, ch := range mibName {
657 - if (ch >= 'a' && ch <= 'z') ||
658 - (ch >= 'A' && ch <= 'Z') ||
659 - (ch >= '0' && ch <= '9') ||
660 - ch == '.' || ch == ':' || ch == '-' || ch == '_' {
661 - continue
662 - }
663 - return fmt.Errorf("invalid character '%c' at position %d in MIB name", ch, i)
664 - }
665 -
666 - return nil
667 -}
668 -
669 -func translateMIB(mibName string) (string, error) {
670 - // 1. Check if already numeric OID (passthrough)
671 - if strings.HasPrefix(mibName, ".") || (len(mibName) > 0 && mibName[0] >= '0' && mibName[0] <= '9') {
672 - return mibName, nil
673 - }
674 -
675 - // 2. Validate MIB name BEFORE any cache operations (defense in depth)
676 - if err := validateMIBName(mibName); err != nil {
677 - return "", fmt.Errorf("invalid MIB name: %w", err)
678 - }
679 -
680 - // 3. Check hardcoded map (covers ~90% of use cases)
681 - if oid, found := mibToOID[mibName]; found {
682 - return oid, nil
683 - }
684 -
685 - // 4. Check translation cache (from previous snmptranslate calls)
686 - mibTranslationCacheMu.RLock()
687 - if oid, found := mibTranslationCache[mibName]; found {
688 - mibTranslationCacheMu.RUnlock()
689 - return oid, nil
690 - }
691 - mibTranslationCacheMu.RUnlock()
692 -
693 - // 5. Try snmptranslate (optional - fails gracefully if not installed)
694 - // Use timeout to prevent hanging on slow/stuck snmptranslate
695 - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
696 - defer cancel()
697 - cmd := exec.CommandContext(ctx, "snmptranslate", "-On", mibName)
698 - output, err := cmd.Output()
699 - if err == nil {
700 - oid := strings.TrimSpace(string(output))
701 - // Validate OID format before caching
702 - if strings.HasPrefix(oid, ".") {
703 - // Cache the result (with size limit)
704 - mibTranslationCacheMu.Lock()
705 - if len(mibTranslationCache) < mibCacheMaxSize {
706 - mibTranslationCache[mibName] = oid
707 - }
708 - mibTranslationCacheMu.Unlock()
709 - return oid, nil
710 - }
711 - }
712 -
713 - // 5. Unknown MIB - fail with helpful error message
714 - return "", fmt.Errorf("unknown MIB: %s (install net-snmp or use numeric OIDs)", mibName)
715 -}
716 -
717 -// parseSNMPWalkAll parses all SNMP entries from snmpwalk output.
718 -// Returns a slice of all parsed entries, preserving order.
719 -// This handles the same complex formats as parseSNMPWalk but returns all entries.
720 -func parseSNMPWalkAll(data string, logger Logger) ([]snmpEntry, error) {
721 - lines := strings.Split(data, "\n")
722 - if len(lines) > 0 && lines[len(lines)-1] == "" {
723 - lines = lines[:len(lines)-1]
724 - }
725 -
726 - var entries []snmpEntry
727 -
728 - for i := 0; i < len(lines); {
729 - line := lines[i]
730 -
731 - // Normalize textual OIDs to numeric format (e.g., IF-MIB::ifDescr.1 -> .1.3.6.1.2.1.2.2.1.2.1)
732 - line = normalizeTextualOIDLine(line)
733 -
734 - // Check if this is a new entry (starts with OID)
735 - if !strings.HasPrefix(strings.TrimSpace(line), ".") {
736 - if strings.TrimSpace(line) != "" {
737 - logger.Debug("skipping non-OID line in snmp walk output", "line", line, "lineNumber", i+1)
738 - }
739 - i++
740 - continue
741 - }
742 -
743 - // Parse line using helper
744 - oid, valueType, valueStr, ok := parseSNMPLine(line)
745 - if !ok {
746 - logger.Debug("skipping malformed snmp line", "line", line, "lineNumber", i+1)
747 - i++
748 - continue
749 - }
750 -
751 - // Handle "Wrong Type" warnings - extract actual type and value
752 - if strings.HasPrefix(valueType, "Wrong Type") {
753 - actualTypeParts := strings.SplitN(valueStr, ":", 2)
754 - if len(actualTypeParts) == 2 {
755 - valueType = strings.TrimSpace(actualTypeParts[0])
756 - valueStr = strings.TrimLeft(actualTypeParts[1], " ")
757 - } else {
758 - return nil, fmt.Errorf("cannot parse snmp walk output")
759 - }
760 - }
761 -
762 - // Collect multiline values using helper
763 - finalValue, consumed := collectMultilineValue(valueType, valueStr, lines, i)
764 -
765 - entries = append(entries, snmpEntry{
766 - oid: oid,
767 - valueType: valueType,
768 - value: finalValue,
769 - })
770 -
771 - i += consumed + 1
772 - }
773 -
774 - return entries, nil
775 -}
776 -
777 -// parseSNMPWalk parses SNMP walk text output and extracts value for target OID.
778 -//
779 -// This custom parser processes text output from the snmpwalk command-line tool
780 -// (not binary SNMP protocol data). It handles complex SNMP data formats including:
781 -// - Multiple value types: STRING, INTEGER, Hex-STRING, Timeticks, Counter32, etc.
782 -// - Multiline quoted strings with proper quote tracking
783 -// - Multiline hex strings with continuation lines
784 -// - MIB name to numeric OID translation
785 -// - "Wrong Type" warnings with type/value extraction
786 -// - NULL and empty string handling
787 -//
788 -// Example SNMP walk format:
789 -//
790 -// .1.3.6.1.2.1.1.1.0 = STRING: "Linux server 5.4.0"
791 -// .1.3.6.1.2.1.1.3.0 = Timeticks: (12345) 0:02:03.45
792 -//
793 -// Parameters:
794 -// - data: Text output from snmpwalk command (one or more OID=value lines)
795 -// - targetOID: OID to extract (supports MIB names like "SNMPv2-MIB::sysDescr.0")
796 -//
797 -// Returns:
798 -// - snmpEntry: Parsed entry containing OID, value type, and value
799 -// - error: Parsing error if format is invalid or target OID not found
800 -//
801 -// This function cannot use gosnmp library because it parses command-line tool output,
802 -// not binary SNMP protocol data. No standard library exists for this text format.
803 -// normalizeOID adds a leading dot to numeric OIDs if missing
804 -func normalizeOID(oid string) string {
805 - if !strings.HasPrefix(oid, ".") && len(oid) > 0 && oid[0] >= '0' && oid[0] <= '9' {
806 - return "." + oid
807 - }
808 - return oid
809 -}
810 -
811 -// normalizeTextualOIDLine converts textual OID lines to numeric OID format.
812 -// Input: "IF-MIB::ifDescr.1 = STRING: \"eth0\""
813 -// Output: ".1.3.6.1.2.1.2.2.1.2.1 = STRING: \"eth0\""
814 -//
815 -// Returns original line unchanged if:
816 -// - Not a textual OID format (no "::" before "=")
817 -// - MIB name is not in the built-in translation table (translateMIB fails)
818 -//
819 -// IMPORTANT: Unknown MIBs (vendor-specific, custom, or not in hardcoded map) are
820 -// returned as-is. Since the parser expects lines starting with ".", these untranslated
821 -// textual OID lines will be silently skipped. For full textual OID support, either:
822 -// - Use numeric OIDs in snmpwalk output (snmpwalk -On)
823 -// - Extend translateMIB() with additional MIB mappings
824 -// - Use snmptranslate externally before passing to this library
825 -func normalizeTextualOIDLine(line string) string {
826 - trimmed := strings.TrimSpace(line)
827 - if trimmed == "" || strings.HasPrefix(trimmed, "#") {
828 - return line
829 - }
830 -
831 - // Check for textual OID format (contains :: before =)
832 - eqIdx := strings.Index(trimmed, "=")
833 - if eqIdx == -1 {
834 - return line
835 - }
836 -
837 - oidPart := strings.TrimSpace(trimmed[:eqIdx])
838 -
839 - // Check if OID contains :: (textual format)
840 - if !strings.Contains(oidPart, "::") {
841 - return line
842 - }
843 -
844 - // Extract MIB name and instance suffix
845 - // Format: MIB-NAME::objectName.instance or MIB-NAME::objectName
846 - // Split on :: to get MIB::object
847 - parts := strings.SplitN(oidPart, "::", 2)
848 - if len(parts) != 2 {
849 - return line
850 - }
851 -
852 - mibPrefix := parts[0]
853 - objectAndInstance := parts[1]
854 -
855 - // Split object name from instance (e.g., "ifDescr.1" -> "ifDescr", "1")
856 - var objectName, instanceSuffix string
857 - dotIdx := strings.Index(objectAndInstance, ".")
858 - if dotIdx == -1 {
859 - objectName = objectAndInstance
860 - instanceSuffix = ""
861 - } else {
862 - objectName = objectAndInstance[:dotIdx]
863 - instanceSuffix = objectAndInstance[dotIdx:] // Includes the dot
864 - }
865 -
866 - // Construct full MIB name for translation
867 - fullMIBName := mibPrefix + "::" + objectName
868 -
869 - // Try to translate
870 - numericOID, err := translateMIB(fullMIBName)
871 - if err != nil {
872 - // Can't translate - return original line (will be skipped later)
873 - return line
874 - }
875 -
876 - // Reconstruct line with numeric OID
877 - newOID := numericOID + instanceSuffix
878 - rest := trimmed[eqIdx:]
879 - return newOID + " " + rest
880 -}
881 -
882 -// parseSNMPLine parses a single SNMP walk output line into OID, type, and value
883 -func parseSNMPLine(line string) (oid, valueType, value string, ok bool) {
884 - // Split on = to get OID and rest
885 - parts := strings.SplitN(line, "=", 2)
886 - if len(parts) != 2 {
887 - return "", "", "", false
888 - }
889 -
890 - oid = strings.TrimSpace(parts[0])
891 - rest := strings.TrimSpace(parts[1])
892 -
893 - // Extract type and value (format: "TYPE: value")
894 - typeParts := strings.SplitN(rest, ":", 2)
895 -
896 - if len(typeParts) == 2 {
897 - // Format: "TYPE: value"
898 - valueType = strings.TrimSpace(typeParts[0])
899 - value = strings.TrimLeft(typeParts[1], " ")
900 - return oid, valueType, value, true
901 - }
902 -
903 - // No colon - could be "NULL", empty quotes "", or raw value
904 - trimmed := strings.TrimSpace(typeParts[0])
905 - if trimmed == "NULL" {
906 - return oid, "NULL", "", true
907 - }
908 - if trimmed == "\"\"" {
909 - return oid, "EMPTY", "", true
910 - }
911 - // Arbitrary value without type
912 - return oid, "", trimmed, true
913 -}
914 -
915 -// isMultilineValue determines if an SNMP value requires multiline processing
916 -func isMultilineValue(valueType, value string) bool {
917 - if valueType == "Hex-STRING" {
918 - return true
919 - }
920 - if valueType == "STRING" {
921 - // Quoted string without closing quote, or unquoted string
922 - if strings.HasPrefix(value, "\"") && !strings.HasSuffix(strings.TrimRight(value, " \t"), "\"") {
923 - return true
924 - }
925 - if !strings.HasPrefix(value, "\"") {
926 - return true
927 - }
928 - }
929 - return false
930 -}
931 -
932 -// appendContinuationLine adds a continuation line to a multiline SNMP value
933 -func appendContinuationLine(entry *snmpEntry, line string) {
934 - if entry.valueType == "Hex-STRING" {
935 - // For Hex-STRING, append with space separator (trimmed)
936 - entry.value += " " + strings.TrimSpace(line)
937 - } else {
938 - // For STRING, append with newline preserved
939 - entry.value += "\n" + line
940 - }
941 -}
942 -
943 -// isStringClosed checks if a multiline STRING value is now complete
944 -func isStringClosed(valueType, line string) bool {
945 - if valueType != "STRING" {
946 - return false
947 - }
948 - trimmed := strings.TrimRight(line, " \t")
949 - // Closed if ends with unescaped quote
950 - return strings.HasSuffix(trimmed, "\"") && !strings.HasSuffix(trimmed, "\\\"")
951 -}
952 -
953 -// collectMultilineValue collects continuation lines for multiline SNMP values
954 -// Returns the complete value and the number of continuation lines consumed
955 -func collectMultilineValue(valueType, initialValue string, lines []string, startIdx int) (string, int) {
956 - value := initialValue
957 - consumed := 0
958 -
959 - if valueType == "STRING" && strings.HasPrefix(initialValue, "\"") {
960 - trimmed := strings.TrimRight(initialValue, " \t")
961 - isMultiline := (trimmed == "\"") || (!strings.HasSuffix(trimmed, "\""))
962 -
963 - if isMultiline {
964 - // Multiline quoted string
965 - for startIdx+consumed+1 < len(lines) && !strings.HasPrefix(strings.TrimSpace(lines[startIdx+consumed+1]), ".") {
966 - consumed++
967 - value += "\n" + lines[startIdx+consumed]
968 - if isStringClosed(valueType, lines[startIdx+consumed]) {
969 - break
970 - }
971 - }
972 - }
973 - } else if valueType == "Hex-STRING" {
974 - // Multiline hex-string - continuation lines are indented hex bytes
975 - for startIdx+consumed+1 < len(lines) && !strings.HasPrefix(strings.TrimSpace(lines[startIdx+consumed+1]), ".") {
976 - consumed++
977 - value += " " + strings.TrimSpace(lines[startIdx+consumed])
978 - }
979 - }
980 -
981 - return value, consumed
982 -}
983 -
984 -func parseSNMPWalk(data string, targetOID string) (snmpEntry, error) {
985 - // Translate MIB name to numeric OID (if needed)
986 - translatedOID, err := translateMIB(targetOID)
987 - if err != nil {
988 - return snmpEntry{}, err
989 - }
990 - targetOID = normalizeOID(translatedOID)
991 -
992 - // Split lines and remove trailing empty line (from YAML | formatting)
993 - lines := strings.Split(data, "\n")
994 - if len(lines) > 0 && lines[len(lines)-1] == "" {
995 - lines = lines[:len(lines)-1]
996 - }
997 -
998 - var current snmpEntry
999 - inMultiline := false
1000 - wentMultiline := false // Track if we actually appended continuation lines
1001 -
1002 - for i := 0; i < len(lines); i++ {
1003 - line := lines[i]
1004 -
1005 - // Normalize textual OIDs to numeric format (e.g., IF-MIB::ifDescr.1 -> .1.3.6.1.2.1.2.2.1.2.1)
1006 - line = normalizeTextualOIDLine(line)
1007 -
1008 - // Check if this is a new entry (starts with OID)
1009 - if strings.HasPrefix(strings.TrimSpace(line), ".") {
1010 - // If we were in multiline and found our target, check if it's complete
1011 - if inMultiline && current.oid == targetOID {
1012 - // Check for broken quoting - if we started with a quote but never closed it
1013 - if current.valueType == "STRING" && strings.HasPrefix(strings.TrimSpace(current.value), "\"") {
1014 - // Still in quoted mode but hit new OID - broken quoting
1015 - return snmpEntry{}, fmt.Errorf("malformed quoted string: missing closing quote")
1016 - }
1017 - // Only preserve whitespace for STRING if we actually went multiline
1018 - if current.valueType != "STRING" || !wentMultiline {
1019 - current.value = strings.TrimSpace(current.value)
1020 - }
1021 - return current, nil
1022 - }
1023 -
1024 - // Parse new entry using helper
1025 - oid, valueType, valueStr, ok := parseSNMPLine(line)
1026 - if !ok {
1027 - continue
1028 - }
1029 -
1030 - current = snmpEntry{
1031 - oid: oid,
1032 - valueType: valueType,
1033 - value: valueStr,
1034 - }
1035 -
1036 - // Check if this is a multiline value using helper
1037 - inMultiline = isMultilineValue(valueType, valueStr)
1038 -
1039 - // If not multiline and this is our target, we're done
1040 - if !inMultiline && oid == targetOID {
1041 - return current, nil
1042 - }
1043 - } else if inMultiline {
1044 - // Continuation line
1045 - wentMultiline = true
1046 - appendContinuationLine(&current, line)
1047 -
1048 - // Check if STRING is now closed using helper
1049 - if isStringClosed(current.valueType, line) {
1050 - inMultiline = false
1051 - if current.oid == targetOID {
1052 - // Don't trim - preserve exact formatting
1053 - return current, nil
1054 - }
1055 - }
1056 - }
1057 - }
1058 -
1059 - // Check if last entry matches (for cases where file ends without newline)
1060 - if current.oid == targetOID {
1061 - // Only preserve whitespace for STRING if we actually went multiline
1062 - if current.valueType != "STRING" || !wentMultiline {
1063 - current.value = strings.TrimSpace(current.value)
1064 - }
1065 - return current, nil
1066 - }
1067 -
1068 - return snmpEntry{}, fmt.Errorf("OID not found: %s", targetOID)
1069 -}
1070 -
1071 -// formatSNMPValue applies format conversion to SNMP value
1072 -// trimFloatZeros: whether to trim trailing zeros from Opaque floats (true for WALK_VALUE, false for WALK_TO_JSON)
1073 -func formatSNMPValue(entry snmpEntry, formatMode int, trimFloatZeros bool) (string, error) {
1074 - switch entry.valueType {
1075 - case "STRING":
1076 - return formatSTRING(entry.value, formatMode)
1077 - case "Hex-STRING":
1078 - return formatHexSTRING(entry.value, formatMode)
1079 - case "INTEGER":
1080 - return strings.TrimSpace(entry.value), nil
1081 - case "BITS":
1082 - if formatMode == 3 {
1083 - return convertBITSToInteger(entry.value)
1084 - }
1085 - return strings.TrimSpace(entry.value), nil
1086 - case "IpAddress", "Counter32", "Gauge32", "Counter64", "Timeticks":
1087 - // Return value as-is for these types
1088 - return strings.TrimSpace(entry.value), nil
1089 - case "Opaque":
1090 - // Opaque can have wrapped types - parse them
1091 - return formatOpaque(entry.value, trimFloatZeros), nil
1092 - case "OID":
1093 - return strings.TrimSpace(entry.value), nil
1094 - case "NULL":
1095 - // NULL type returns "NULL" as a string
1096 - return "NULL", nil
1097 - case "EMPTY":
1098 - // Empty quotes - return empty string
1099 - return "", nil
1100 - case "":
1101 - // No type specified - return value as-is (arbitrary number)
1102 - return strings.TrimSpace(entry.value), nil
1103 - default:
1104 - // Unknown type - return value as-is
1105 - return strings.TrimSpace(entry.value), nil
1106 - }
1107 -}
1108 -
1109 -// formatSTRING handles STRING formatting
1110 -func formatSTRING(value string, formatMode int) (string, error) {
1111 - // Check if it's a quoted string (trim only for quote detection)
1112 - trimmed := strings.TrimSpace(value)
1113 - if strings.HasPrefix(trimmed, "\"") && strings.HasSuffix(trimmed, "\"") && len(trimmed) >= 2 {
1114 - // Unescape the quoted string
1115 - unquoted := trimmed[1 : len(trimmed)-1]
1116 - // Replace escape sequences
1117 - unquoted = strings.ReplaceAll(unquoted, "\\\"", "\"")
1118 - unquoted = strings.ReplaceAll(unquoted, "\\\\", "\\")
1119 - return unquoted, nil
1120 - }
1121 - // Unquoted string - return as-is (preserve whitespace)
1122 - return value, nil
1123 -}
1124 -
1125 -// formatHexSTRING handles Hex-STRING formatting
1126 -func formatHexSTRING(value string, formatMode int) (string, error) {
1127 - // Clean up the hex string (remove extra spaces)
1128 - value = strings.TrimSpace(value)
1129 -
1130 - switch formatMode {
1131 - case 0:
1132 - // Unchanged - normalize spaces
1133 - parts := strings.Fields(value)
1134 - return strings.Join(parts, " "), nil
1135 -
1136 - case 1:
1137 - // UTF-8 conversion
1138 - bytes, err := parseHexBytes(value)
1139 - if err != nil {
1140 - return "", err
1141 - }
1142 - // Remove null terminator if present
1143 - if len(bytes) > 0 && bytes[len(bytes)-1] == 0 {
1144 - bytes = bytes[:len(bytes)-1]
1145 - }
1146 - // Convert to string and replace invalid UTF-8 bytes with '?'
1147 - result := string(bytes)
1148 - if !utf8.ValidString(result) {
1149 - // Iterate over bytes and decode runes manually to properly handle
1150 - // both invalid bytes and legitimate U+FFFD characters
1151 - var fixed strings.Builder
1152 - for i := 0; i < len(bytes); {
1153 - r, size := utf8.DecodeRune(bytes[i:])
1154 - // If DecodeRune returns (RuneError, 1), it's an invalid byte
1155 - // If it returns (RuneError, 3), it's a valid encoding of U+FFFD
1156 - if r == utf8.RuneError && size == 1 {
1157 - fixed.WriteByte('?')
1158 - i++
1159 - } else {
1160 - fixed.WriteRune(r)
1161 - i += size
1162 - }
1163 - }
1164 - return fixed.String(), nil
1165 - }
1166 - return result, nil
1167 -
1168 - case 2:
1169 - // MAC address format
1170 - bytes, err := parseHexBytes(value)
1171 - if err != nil {
1172 - return "", err
1173 - }
1174 - // Format as colon-separated hex
1175 - var parts []string
1176 - for _, b := range bytes {
1177 - parts = append(parts, fmt.Sprintf("%02X", b))
1178 - }
1179 - return strings.Join(parts, ":"), nil
1180 -
1181 - default:
1182 - // Unknown format mode - return unchanged
1183 - parts := strings.Fields(value)
1184 - return strings.Join(parts, " "), nil
1185 - }
1186 -}
1187 -
1188 -// parseHexBytes parses hex string like "74 65 73 74" into bytes
1189 -func parseHexBytes(hexStr string) ([]byte, error) {
1190 - parts := strings.Fields(hexStr)
1191 - var bytes []byte
1192 - for _, part := range parts {
1193 - b, err := hex.DecodeString(part)
1194 - if err != nil {
1195 - return nil, fmt.Errorf("invalid hex string: %v", err)
1196 - }
1197 - bytes = append(bytes, b...)
1198 - }
1199 - return bytes, nil
1200 -}
1201 -
1202 -// convertBITSToInteger converts BITS hex string to integer (little-endian)
1203 -func convertBITSToInteger(value string) (string, error) {
1204 - bytes, err := parseHexBytes(value)
1205 - if err != nil {
1206 - return "", err
1207 - }
1208 -
1209 - // BITS uses little-endian byte order
1210 - // Reverse the bytes
1211 - for i, j := 0, len(bytes)-1; i < j; i, j = i+1, j-1 {
1212 - bytes[i], bytes[j] = bytes[j], bytes[i]
1213 - }
1214 -
1215 - // Convert bytes to integer (big-endian after reversal)
1216 - var result uint64
1217 - for _, b := range bytes {
1218 - result = (result << 8) | uint64(b)
1219 - }
1220 -
1221 - return fmt.Sprintf("%d", result), nil
1222 -}
1223 -
1224 -// formatOpaque handles Opaque wrapped types
1225 -func formatOpaque(value string, trimFloatZeros bool) string {
1226 - // Opaque can have wrapped types like "Float: 0.460000" or "STRING: \"hello\""
1227 - // Extract the wrapped type and value
1228 - parts := strings.SplitN(value, ":", 2)
1229 - if len(parts) == 2 {
1230 - wrappedType := strings.TrimSpace(parts[0])
1231 - val := strings.TrimSpace(parts[1])
1232 -
1233 - // For STRING wrapped type, strip quotes
1234 - if wrappedType == "STRING" && len(val) >= 2 && strings.HasPrefix(val, "\"") && strings.HasSuffix(val, "\"") {
1235 - val = val[1 : len(val)-1]
1236 - }
1237 -
1238 - // For Float wrapped type, optionally trim trailing zeros
1239 - if wrappedType == "Float" && trimFloatZeros && strings.Contains(val, ".") {
1240 - val = strings.TrimRight(val, "0")
1241 - val = strings.TrimRight(val, ".")
1242 - }
1243 -
1244 - // For other types (Unsigned32, etc.), return value as-is
1245 - return val
1246 - }
1247 - return strings.TrimSpace(value)
1248 -}
1249 -
1250 -// snmpGetValue formats raw SNMP hex-string value.
1251 -// Format modes:
1252 -// 0 = unchanged (return raw hex as-is with normalized spaces)
1253 -// 1 = UTF-8 (decode hex-string to UTF-8 string)
1254 -// 2 = MAC (format hex-string as MAC address with colons)
1255 -// 3 = BITS (convert BITS to integer)
1256 -func snmpGetValue(value Value, params string) (Value, error) {
1257 - lines := strings.Split(params, "\n")
1258 - if len(lines) < 1 {
1259 - return Value{}, fmt.Errorf("snmp get value requires format mode parameter")
1260 - }
1261 -
1262 - formatMode := 0
1263 - if len(lines) >= 1 && strings.TrimSpace(lines[0]) != "" {
1264 - var err error
1265 - formatMode, err = strconv.Atoi(strings.TrimSpace(lines[0]))
1266 - if err != nil {
1267 - return Value{}, fmt.Errorf("invalid format mode: %v", err)
1268 - }
1269 - }
1270 -
1271 - // For BITS mode, convert directly
1272 - if formatMode == 3 {
1273 - result, err := convertBITSToInteger(value.Data)
1274 - if err != nil {
1275 - return Value{}, err
1276 - }
1277 - return Value{Data: result, Type: ValueTypeStr}, nil
1278 - }
1279 -
1280 - // Apply format conversion to hex-string input
1281 - result, err := formatHexSTRING(value.Data, formatMode)
1282 - if err != nil {
1283 - return Value{}, err
1284 - }
1285 -
1286 - return Value{Data: result, Type: ValueTypeStr}, nil
1287 -}
1288 -
1289 -// snmpWalkToJSON converts SNMP walk output to JSON discovery format.
1290 -// Params format (one entry per 3 lines):
1291 -//
1292 -// MACRO_NAME
1293 -// OID_PREFIX
1294 -// FORMAT_MODE
1295 -func snmpWalkToJSON(value Value, params string, logger Logger) (Value, error) {
1296 - // Parse params into macro definitions
1297 - type macroDef struct {
1298 - name string
1299 - oidPrefix string
1300 - formatMode int
1301 - }
1302 -
1303 - var macros []macroDef
1304 - lines := strings.Split(params, "\n")
1305 -
1306 - // Validate params format: must be triplets (macro, oid, format)
1307 - // Exception: empty params (all whitespace) is valid and returns []
1308 - trimmedParams := strings.TrimSpace(params)
1309 - if trimmedParams != "" && len(lines)%3 != 0 {
1310 - return Value{}, fmt.Errorf("snmp walk to json requires parameters in triplets (macro, oid, format)")
1311 - }
1312 -
1313 - for i := 0; i+2 < len(lines); i += 3 {
1314 - name := strings.TrimSpace(lines[i])
1315 - oidPrefix := strings.TrimSpace(lines[i+1])
1316 - formatModeStr := strings.TrimSpace(lines[i+2])
1317 -
1318 - if name == "" || oidPrefix == "" {
1319 - continue
1320 - }
1321 -
1322 - formatMode := 0
1323 - if formatModeStr != "" {
1324 - var err error
1325 - formatMode, err = strconv.Atoi(formatModeStr)
1326 - if err != nil {
1327 - return Value{}, fmt.Errorf("invalid format mode: %v", err)
1328 - }
1329 - }
1330 -
1331 - // Translate MIB name to numeric OID (supports textual OIDs like IF-MIB::ifDescr)
1332 - translatedOID, err := translateMIB(oidPrefix)
1333 - if err != nil {
1334 - return Value{}, err
1335 - }
1336 - oidPrefix = normalizeOID(translatedOID)
1337 -
1338 - macros = append(macros, macroDef{
1339 - name: name,
1340 - oidPrefix: oidPrefix,
1341 - formatMode: formatMode,
1342 - })
1343 - }
1344 -
1345 - // If no macros defined, return empty array
1346 - if len(macros) == 0 {
1347 - return Value{Data: "[]", Type: ValueTypeStr}, nil
1348 - }
1349 -
1350 - // Parse SNMP walk data using shared parser
1351 - entries, err := parseSNMPWalkAll(value.Data, logger)
1352 - if err != nil {
1353 - return Value{}, err
1354 - }
1355 -
1356 - // Group entries by index (last OID component)
1357 - type indexData struct {
1358 - index string
1359 - values map[string]string
1360 - }
1361 - indices := make(map[string]*indexData)
1362 -
1363 - // Process each parsed entry
1364 - for _, entry := range entries {
1365 - // Check which macro this OID matches
1366 - for _, macro := range macros {
1367 - // Build match prefix - add dot only if not already present
1368 - matchPrefix := macro.oidPrefix
1369 - if !strings.HasSuffix(matchPrefix, ".") {
1370 - matchPrefix += "."
1371 - }
1372 -
1373 - if strings.HasPrefix(entry.oid, matchPrefix) {
1374 - // Extract index (everything after the prefix)
1375 - index := strings.TrimPrefix(entry.oid, matchPrefix)
1376 -
1377 - // Get or create index data
1378 - if indices[index] == nil {
1379 - indices[index] = &indexData{
1380 - index: index,
1381 - values: make(map[string]string),
1382 - }
1383 - }
1384 -
1385 - // Format the value (preserve float zeros for WALK_TO_JSON)
1386 - formatted, err := formatSNMPValue(entry, macro.formatMode, false)
1387 - if err != nil {
1388 - return Value{}, err
1389 - }
1390 -
1391 - indices[index].values[macro.name] = formatted
1392 - break
1393 - }
1394 - }
1395 - }
1396 -
1397 - // If no data found for any macro, check if it's due to bad data or just empty input
1398 - if len(indices) == 0 {
1399 - // If we found valid SNMP entries but none matched our macros, that's an error
1400 - // If we found no valid entries at all (empty or bad data), return error only for bad data
1401 - if len(entries) > 0 {
1402 - // Found valid SNMP entries but none matched - this is an error
1403 - return Value{}, fmt.Errorf("cannot convert snmp walk to json")
1404 - }
1405 - // No valid entries found - could be empty input (ok) or bad data (error)
1406 - // Distinguish by checking if input has non-empty, non-whitespace content
1407 - trimmedData := strings.TrimSpace(value.Data)
1408 - if trimmedData != "" {
1409 - // Non-empty input but no valid SNMP entries - bad data
1410 - return Value{}, fmt.Errorf("cannot convert snmp walk to json")
1411 - }
1412 - // Empty input - return empty array
1413 - return Value{Data: "[]", Type: ValueTypeStr}, nil
1414 - }
1415 -
1416 - // Convert to JSON array, sorted by index in descending order
1417 - var indexList []string
1418 - for idx := range indices {
1419 - indexList = append(indexList, idx)
1420 - }
1421 - sort.Sort(sort.Reverse(sort.StringSlice(indexList)))
1422 -
1423 - // Build JSON manually to preserve field order: {#SNMPINDEX} first, then macros in order
1424 - var jsonParts []string
1425 - for _, idx := range indexList {
1426 - data := indices[idx]
1427 -
1428 - // Start with {#SNMPINDEX}
1429 - var fields []string
1430 - fields = append(fields, fmt.Sprintf("\"{#SNMPINDEX}\":%q", data.index))
1431 -
1432 - // Add macros in definition order
1433 - for _, macro := range macros {
1434 - if val, ok := data.values[macro.name]; ok {
1435 - // Handle NULL values as JSON null
1436 - if val == "NULL" {
1437 - fields = append(fields, fmt.Sprintf("%q:null", macro.name))
1438 - } else {
1439 - fields = append(fields, fmt.Sprintf("%q:%q", macro.name, val))
1440 - }
1441 - }
1442 - }
1443 -
1444 - jsonParts = append(jsonParts, "{"+strings.Join(fields, ",")+"}")
1445 - }
1446 -
1447 - return Value{Data: "[" + strings.Join(jsonParts, ",") + "]", Type: ValueTypeStr}, nil
1448 -}
1449 -
1450 -// snmpWalkToJSONMulti converts SNMP walk data to multiple discovery metrics
1451 -// Each discovery item becomes a separate Result.Metric with index label
1452 -func snmpWalkToJSONMulti(value Value, params string, logger Logger) (Result, error) {
1453 - // Parse params into macro definitions
1454 - type macroDef struct {
1455 - name string
1456 - oidPrefix string
1457 - formatMode int
1458 - }
1459 -
1460 - var macros []macroDef
1461 - lines := strings.Split(params, "\n")
1462 -
1463 - // Validate params format: must be triplets (macro, oid, format)
1464 - // Exception: empty params (all whitespace) is valid and returns []
1465 - trimmedParams := strings.TrimSpace(params)
1466 - if trimmedParams != "" && len(lines)%3 != 0 {
1467 - err := fmt.Errorf("snmp walk to json requires parameters in triplets (macro, oid, format)")
1468 - return Result{Error: err}, err
1469 - }
1470 -
1471 - for i := 0; i+2 < len(lines); i += 3 {
1472 - name := strings.TrimSpace(lines[i])
1473 - oidPrefix := strings.TrimSpace(lines[i+1])
1474 - formatModeStr := strings.TrimSpace(lines[i+2])
1475 -
1476 - if name == "" || oidPrefix == "" {
1477 - continue
1478 - }
1479 -
1480 - formatMode := 0
1481 - if formatModeStr != "" {
1482 - var err error
1483 - formatMode, err = strconv.Atoi(formatModeStr)
1484 - if err != nil {
1485 - err = fmt.Errorf("invalid format mode: %v", err)
1486 - return Result{Error: err}, err
1487 - }
1488 - }
1489 -
1490 - // Translate MIB name to numeric OID (supports textual OIDs like IF-MIB::ifDescr)
1491 - translatedOID, err := translateMIB(oidPrefix)
1492 - if err != nil {
1493 - return Result{Error: err}, err
1494 - }
1495 - oidPrefix = normalizeOID(translatedOID)
1496 -
1497 - macros = append(macros, macroDef{
1498 - name: name,
1499 - oidPrefix: oidPrefix,
1500 - formatMode: formatMode,
1501 - })
1502 - }
1503 -
1504 - // If no macros defined, return empty metrics array
1505 - if len(macros) == 0 {
1506 - return Result{Metrics: []Metric{}}, nil
1507 - }
1508 -
1509 - // Parse SNMP walk data using shared parser
1510 - entries, err := parseSNMPWalkAll(value.Data, logger)
1511 - if err != nil {
1512 - return Result{Error: err}, err
1513 - }
1514 -
1515 - // Group entries by index (last OID component)
1516 - type indexData struct {
1517 - index string
1518 - values map[string]string
1519 - }
1520 - indices := make(map[string]*indexData)
1521 -
1522 - // Process each parsed entry
1523 - for _, entry := range entries {
1524 - // Check which macro this OID matches
1525 - for _, macro := range macros {
1526 - // Build match prefix - add dot only if not already present
1527 - matchPrefix := macro.oidPrefix
1528 - if !strings.HasSuffix(matchPrefix, ".") {
1529 - matchPrefix += "."
1530 - }
1531 -
1532 - if strings.HasPrefix(entry.oid, matchPrefix) {
1533 - // Extract index (everything after the prefix)
1534 - index := strings.TrimPrefix(entry.oid, matchPrefix)
1535 -
1536 - // Get or create index data
1537 - if indices[index] == nil {
1538 - indices[index] = &indexData{
1539 - index: index,
1540 - values: make(map[string]string),
1541 - }
1542 - }
1543 -
1544 - // Format the value (preserve float zeros for WALK_TO_JSON)
1545 - formatted, err := formatSNMPValue(entry, macro.formatMode, false)
1546 - if err != nil {
1547 - return Result{Error: err}, err
1548 - }
1549 -
1550 - indices[index].values[macro.name] = formatted
1551 - break
1552 - }
1553 - }
1554 - }
1555 -
1556 - // If no data found for any macro, check if it's due to bad data or just empty input
1557 - if len(indices) == 0 {
1558 - // If we found valid SNMP entries but none matched our macros, that's an error
1559 - // If we found no valid entries at all (empty or bad data), return error only for bad data
1560 - if len(entries) > 0 {
1561 - // Found valid SNMP entries but none matched - this is an error
1562 - err := fmt.Errorf("cannot convert snmp walk to json")
1563 - return Result{Error: err}, err
1564 - }
1565 - // No valid entries found - could be empty input (ok) or bad data (error)
1566 - // Distinguish by checking if input has non-empty, non-whitespace content
1567 - trimmedData := strings.TrimSpace(value.Data)
1568 - if trimmedData != "" {
1569 - // Non-empty input but no valid SNMP entries - bad data
1570 - err := fmt.Errorf("cannot convert snmp walk to json")
1571 - return Result{Error: err}, err
1572 - }
1573 - // Empty input - return empty metrics array
1574 - return Result{Metrics: []Metric{}}, nil
1575 - }
1576 -
1577 - // Convert to multiple metrics, sorted by index in descending order
1578 - var indexList []string
1579 - for idx := range indices {
1580 - indexList = append(indexList, idx)
1581 - }
1582 - sort.Sort(sort.Reverse(sort.StringSlice(indexList)))
1583 -
1584 - // Build Result with multiple Metric objects
1585 - metrics := make([]Metric, 0, len(indexList))
1586 - for _, idx := range indexList {
1587 - data := indices[idx]
1588 -
1589 - // Build JSON object for this discovery item
1590 - // Start with {#SNMPINDEX}
1591 - var fields []string
1592 - fields = append(fields, fmt.Sprintf("\"{#SNMPINDEX}\":%q", data.index))
1593 -
1594 - // Add macros in definition order
1595 - for _, macro := range macros {
1596 - if val, ok := data.values[macro.name]; ok {
1597 - // Handle NULL values as JSON null
1598 - if val == "NULL" {
1599 - fields = append(fields, fmt.Sprintf("%q:null", macro.name))
1600 - } else {
1601 - fields = append(fields, fmt.Sprintf("%q:%q", macro.name, val))
1602 - }
1603 - }
1604 - }
1605 -
1606 - jsonObj := "{" + strings.Join(fields, ",") + "}"
1607 -
1608 - // Create metric with index label
1609 - metrics = append(metrics, Metric{
1610 - Name: "snmp_discovery",
1611 - Value: jsonObj,
1612 - Type: ValueTypeStr,
1613 - Labels: map[string]string{"index": data.index},
1614 - })
1615 - }
1616 -
1617 - return Result{Metrics: metrics}, nil
1618 -}
src/go/plugin/scripts.d/pkg/zabbixpreproc/state_isolation_test.go deleted
-203
@@ -1,203 +0,0 @@
1 -package zabbixpreproc
2 -
3 -import (
4 - "testing"
5 - "time"
6 -)
7 -
8 -// TestPerItemStateIsolation tests that different items have isolated state
9 -func TestPerItemStateIsolation(t *testing.T) {
10 - p := NewPreprocessor("test-shard")
11 -
12 - // Delta value test - two items should have independent state
13 - step := Step{Type: StepTypeDeltaValue, Params: ""}
14 -
15 - // Item 1: Send values 10, 20, 30
16 - value1_1 := Value{Data: "10", Type: ValueTypeFloat, Timestamp: time.Now()}
17 - value1_2 := Value{Data: "20", Type: ValueTypeFloat, Timestamp: time.Now()}
18 - value1_3 := Value{Data: "30", Type: ValueTypeFloat, Timestamp: time.Now()}
19 -
20 - // Item 2: Send values 100, 200, 300
21 - value2_1 := Value{Data: "100", Type: ValueTypeFloat, Timestamp: time.Now()}
22 - value2_2 := Value{Data: "200", Type: ValueTypeFloat, Timestamp: time.Now()}
23 - value2_3 := Value{Data: "300", Type: ValueTypeFloat, Timestamp: time.Now()}
24 -
25 - // First calls should return 0 (no previous value to delta from)
26 - result1_1, err1_1 := p.Execute("item1", value1_1, step)
27 - if err1_1 != nil {
28 - t.Fatalf("Unexpected error on first call for item1: %v", err1_1)
29 - }
30 - if len(result1_1.Metrics) == 0 || result1_1.Metrics[0].Value != "0" {
31 - t.Errorf("Expected 0 on first call for item1, got %v", result1_1.Metrics)
32 - }
33 -
34 - result2_1, err2_1 := p.Execute("item2", value2_1, step)
35 - if err2_1 != nil {
36 - t.Fatalf("Unexpected error on first call for item2: %v", err2_1)
37 - }
38 - if len(result2_1.Metrics) == 0 || result2_1.Metrics[0].Value != "0" {
39 - t.Errorf("Expected 0 on first call for item2, got %v", result2_1.Metrics)
40 - }
41 -
42 - // Second calls should succeed with delta
43 - result1_2, err1_2 := p.Execute("item1", value1_2, step)
44 - if err1_2 != nil {
45 - t.Fatalf("Unexpected error for item1 second call: %v", err1_2)
46 - }
47 - if len(result1_2.Metrics) == 0 || result1_2.Metrics[0].Value != "10" {
48 - t.Errorf("Expected delta of 10 for item1, got %v", result1_2.Metrics)
49 - }
50 -
51 - result2_2, err2_2 := p.Execute("item2", value2_2, step)
52 - if err2_2 != nil {
53 - t.Fatalf("Unexpected error for item2 second call: %v", err2_2)
54 - }
55 - if len(result2_2.Metrics) == 0 || result2_2.Metrics[0].Value != "100" {
56 - t.Errorf("Expected delta of 100 for item2, got %v", result2_2.Metrics)
57 - }
58 -
59 - // Third calls should also succeed with correct deltas
60 - result1_3, err1_3 := p.Execute("item1", value1_3, step)
61 - if err1_3 != nil {
62 - t.Fatalf("Unexpected error for item1 third call: %v", err1_3)
63 - }
64 - if len(result1_3.Metrics) == 0 || result1_3.Metrics[0].Value != "10" {
65 - t.Errorf("Expected delta of 10 for item1, got %v", result1_3.Metrics)
66 - }
67 -
68 - result2_3, err2_3 := p.Execute("item2", value2_3, step)
69 - if err2_3 != nil {
70 - t.Fatalf("Unexpected error for item2 third call: %v", err2_3)
71 - }
72 - if len(result2_3.Metrics) == 0 || result2_3.Metrics[0].Value != "100" {
73 - t.Errorf("Expected delta of 100 for item2, got %v", result2_3.Metrics)
74 - }
75 -}
76 -
77 -// TestPerItemStateIsolation_Throttle tests throttle state isolation
78 -func TestPerItemStateIsolation_Throttle(t *testing.T) {
79 - p := NewPreprocessor("test-shard")
80 - step := Step{Type: StepTypeThrottleValue, Params: ""}
81 -
82 - // Item 1 sends "A", "A", "B"
83 - result1_1, _ := p.Execute("item1", Value{Data: "A", Type: ValueTypeStr, Timestamp: time.Now()}, step)
84 - result1_2, _ := p.Execute("item1", Value{Data: "A", Type: ValueTypeStr, Timestamp: time.Now()}, step)
85 - result1_3, _ := p.Execute("item1", Value{Data: "B", Type: ValueTypeStr, Timestamp: time.Now()}, step)
86 -
87 - // Item 2 sends "X", "X", "Y"
88 - result2_1, _ := p.Execute("item2", Value{Data: "X", Type: ValueTypeStr, Timestamp: time.Now()}, step)
89 - result2_2, _ := p.Execute("item2", Value{Data: "X", Type: ValueTypeStr, Timestamp: time.Now()}, step)
90 - result2_3, _ := p.Execute("item2", Value{Data: "Y", Type: ValueTypeStr, Timestamp: time.Now()}, step)
91 -
92 - // Item 1: First value should pass, second should be throttled, third should pass
93 - if len(result1_1.Metrics) == 0 || result1_1.Metrics[0].Value != "A" {
94 - t.Errorf("Item1 first call: expected 'A', got %v", result1_1.Metrics)
95 - }
96 - if len(result1_2.Metrics) == 0 || result1_2.Metrics[0].Value != "" {
97 - t.Errorf("Item1 second call: expected empty (throttled), got %v", result1_2.Metrics)
98 - }
99 - if len(result1_3.Metrics) == 0 || result1_3.Metrics[0].Value != "B" {
100 - t.Errorf("Item1 third call: expected 'B', got %v", result1_3.Metrics)
101 - }
102 -
103 - // Item 2: Same pattern (independent of item1)
104 - if len(result2_1.Metrics) == 0 || result2_1.Metrics[0].Value != "X" {
105 - t.Errorf("Item2 first call: expected 'X', got %v", result2_1.Metrics)
106 - }
107 - if len(result2_2.Metrics) == 0 || result2_2.Metrics[0].Value != "" {
108 - t.Errorf("Item2 second call: expected empty (throttled), got %v", result2_2.Metrics)
109 - }
110 - if len(result2_3.Metrics) == 0 || result2_3.Metrics[0].Value != "Y" {
111 - t.Errorf("Item2 third call: expected 'Y', got %v", result2_3.Metrics)
112 - }
113 -}
114 -
115 -// TestShardIDInStateKey tests that shardID is part of state key
116 -func TestShardIDInStateKey(t *testing.T) {
117 - // Two preprocessors with different shards
118 - p1 := NewPreprocessor("shard1")
119 - p2 := NewPreprocessor("shard2")
120 -
121 - step := Step{Type: StepTypeDeltaValue, Params: ""}
122 -
123 - // Both use same itemID but different shards
124 - itemID := "same-item"
125 -
126 - // Shard 1: Send 10, 20
127 - _, _ = p1.Execute(itemID, Value{Data: "10", Type: ValueTypeFloat, Timestamp: time.Now()}, step)
128 - result1, err1 := p1.Execute(itemID, Value{Data: "20", Type: ValueTypeFloat, Timestamp: time.Now()}, step)
129 -
130 - // Shard 2: Send 100, 200
131 - _, _ = p2.Execute(itemID, Value{Data: "100", Type: ValueTypeFloat, Timestamp: time.Now()}, step)
132 - result2, err2 := p2.Execute(itemID, Value{Data: "200", Type: ValueTypeFloat, Timestamp: time.Now()}, step)
133 -
134 - // Both should succeed with correct deltas (proves state isolation)
135 - if err1 != nil {
136 - t.Fatalf("Shard1 error: %v", err1)
137 - }
138 - if len(result1.Metrics) == 0 || result1.Metrics[0].Value != "10" {
139 - t.Errorf("Shard1: expected delta of 10, got %v", result1.Metrics)
140 - }
141 -
142 - if err2 != nil {
143 - t.Fatalf("Shard2 error: %v", err2)
144 - }
145 - if len(result2.Metrics) == 0 || result2.Metrics[0].Value != "100" {
146 - t.Errorf("Shard2: expected delta of 100, got %v", result2.Metrics)
147 - }
148 -}
149 -
150 -// TestStateKeyFormat tests that state keys follow shardID:itemID:operation format
151 -func TestStateKeyFormat(t *testing.T) {
152 - p := NewPreprocessor("my-shard")
153 - step := Step{Type: StepTypeDeltaValue, Params: ""}
154 -
155 - // Execute to populate state
156 - _, _ = p.Execute("my-item", Value{Data: "10", Type: ValueTypeFloat, Timestamp: time.Now()}, step)
157 -
158 - // Check that state key exists with correct format
159 - expectedKey := "my-shard:my-item:delta_value"
160 - if _, exists := p.state[expectedKey]; !exists {
161 - t.Errorf("Expected state key '%s' not found. Existing keys: %v", expectedKey, getStateKeys(p))
162 - }
163 -}
164 -
165 -func TestClearStateRemovesItemEntries(t *testing.T) {
166 - p := NewPreprocessor("clear-shard")
167 - step := Step{Type: StepTypeDeltaValue, Params: ""}
168 -
169 - // Populate state for two different items
170 - _, _ = p.Execute("item1", Value{Data: "10", Type: ValueTypeFloat, Timestamp: time.Now()}, step)
171 - _, _ = p.Execute("item1", Value{Data: "20", Type: ValueTypeFloat, Timestamp: time.Now()}, step)
172 - _, _ = p.Execute("item2", Value{Data: "100", Type: ValueTypeFloat, Timestamp: time.Now()}, step)
173 - _, _ = p.Execute("item2", Value{Data: "200", Type: ValueTypeFloat, Timestamp: time.Now()}, step)
174 -
175 - if len(getStateKeys(p)) != 2 {
176 - t.Fatalf("expected state for two items, got keys %v", getStateKeys(p))
177 - }
178 -
179 - // Clear item1 state and verify only item2 remains
180 - p.ClearState("item1")
181 - keys := getStateKeys(p)
182 - if len(keys) != 1 || keys[0] != "clear-shard:item2:delta_value" {
183 - t.Fatalf("expected only item2 state to remain, got %v", keys)
184 - }
185 -
186 - // Ensure clearing non-existing item is no-op
187 - p.ClearState("missing")
188 - keys = getStateKeys(p)
189 - if len(keys) != 1 || keys[0] != "clear-shard:item2:delta_value" {
190 - t.Fatalf("clear of missing item altered state: %v", keys)
191 - }
192 -}
193 -
194 -// Helper to get state keys for debugging
195 -func getStateKeys(p *Preprocessor) []string {
196 - p.mu.RLock()
197 - defer p.mu.RUnlock()
198 - keys := make([]string, 0, len(p.state))
199 - for k := range p.state {
200 - keys = append(keys, k)
201 - }
202 - return keys
203 -}
src/go/plugin/scripts.d/pkg/zabbixpreproc/steps.go deleted
-637
@@ -1,637 +0,0 @@
1 -package zabbixpreproc
2 -
3 -import (
4 - "fmt"
5 - "strconv"
6 - "strings"
7 - "time"
8 -)
9 -
10 -// Numeric parsing constants
11 -const (
12 - parseBase10 = 10 // Decimal base for integer parsing
13 - parseBase8 = 8 // Octal base for oct2dec
14 - parseBase16 = 16 // Hexadecimal base for hex2dec
15 - parseBitSize64 = 64 // 64-bit precision for float/int parsing
16 -)
17 -
18 -// formatFloat formats a float for output, avoiding scientific notation
19 -func formatFloat(f float64) string {
20 - // If it's a whole number, format as integer
21 - if f == float64(int64(f)) {
22 - return fmt.Sprintf("%d", int64(f))
23 - }
24 - // Otherwise use %g which will choose reasonable formatting
25 - return fmt.Sprintf("%g", f)
26 -}
27 -
28 -// multiplyValue multiplies a numeric value by a multiplier.
29 -func multiplyValue(value Value, paramStr string) (Value, error) {
30 - multiplier, err := strconv.ParseFloat(paramStr, parseBitSize64)
31 - if err != nil {
32 - return Value{}, fmt.Errorf("invalid multiplier: %w", err)
33 - }
34 -
35 - var numValue float64
36 - // For UINT64 type, parse as integer first (truncate any decimals)
37 - if value.Type == ValueTypeUint64 {
38 - intVal, err := strconv.ParseInt(value.Data, 10, 64)
39 - if err != nil {
40 - // Try float, then truncate to uint64
41 - floatVal, err := strconv.ParseFloat(value.Data, 64)
42 - if err != nil {
43 - return Value{}, fmt.Errorf("invalid numeric value: %w", err)
44 - }
45 - intVal = int64(floatVal)
46 - }
47 - numValue = float64(intVal)
48 - } else {
49 - var err error
50 - numValue, err = strconv.ParseFloat(value.Data, 64)
51 - if err != nil {
52 - return Value{}, fmt.Errorf("invalid numeric value: %w", err)
53 - }
54 - }
55 -
56 - result := numValue * multiplier
57 -
58 - // Convert back based on value type
59 - switch value.Type {
60 - case ValueTypeUint64:
61 - // Banker's rounding (round half to even) like Zabbix
62 - floor := int64(result)
63 - frac := result - float64(floor)
64 - var rounded int64
65 - if frac < 0.5 {
66 - rounded = floor
67 - } else if frac > 0.5 {
68 - rounded = floor + 1
69 - } else {
70 - // Exactly 0.5 - round to even
71 - if floor%2 == 0 {
72 - rounded = floor
73 - } else {
74 - rounded = floor + 1
75 - }
76 - }
77 - return Value{Data: fmt.Sprintf("%d", rounded), Type: ValueTypeUint64}, nil
78 - case ValueTypeFloat:
79 - return Value{Data: formatFloat(result), Type: ValueTypeFloat}, nil
80 - default:
81 - // String type - return result as string
82 - if result == float64(int64(result)) {
83 - return Value{Data: fmt.Sprintf("%d", int64(result)), Type: ValueTypeStr}, nil
84 - }
85 - return Value{Data: formatFloat(result), Type: ValueTypeStr}, nil
86 - }
87 -}
88 -
89 -// trimValue trims specified characters from a value.
90 -func trimValue(value Value, paramStr string, direction string) (Value, error) {
91 - chars := paramStr
92 - if chars == "" {
93 - chars = " \t\r\n" // Default whitespace
94 - } else {
95 - // Handle Zabbix escape sequences in params
96 - chars = interpretEscapeSequences(chars)
97 - }
98 -
99 - // Trim the value data
100 - result := value.Data
101 - switch direction {
102 - case "left":
103 - result = strings.TrimLeft(result, chars)
104 - case "right":
105 - result = strings.TrimRight(result, chars)
106 - case "both":
107 - result = strings.Trim(result, chars)
108 - }
109 -
110 - return Value{Data: result, Type: value.Type}, nil
111 -}
112 -
113 -// interpretEscapeSequences handles Zabbix escape sequences in trim/ltrim/rtrim params
114 -// Handles both single backslash (\s) and double backslash (\\s) formats
115 -func interpretEscapeSequences(s string) string {
116 - var result strings.Builder
117 - result.Grow(len(s)) // Pre-allocate capacity for efficiency
118 -
119 - for i := 0; i < len(s); i++ {
120 - if s[i] == '\\' && i+1 < len(s) {
121 - next := s[i+1]
122 -
123 - // Check for double-backslash escape sequences (\\s, \\n, etc.)
124 - // This is only for trim operations where \\s means full whitespace class
125 - if next == '\\' && i+2 < len(s) {
126 - third := s[i+2]
127 - switch third {
128 - case 's':
129 - // \\s means whitespace class: space, tab, newline, carriage return, and backslash
130 - result.WriteString(" \t\r\n\\")
131 - i += 2
132 - continue
133 - }
134 - }
135 -
136 - // Single backslash escape sequences
137 - switch next {
138 - case '\\':
139 - // Backslash-backslash is a literal backslash (consume both backslashes)
140 - result.WriteByte('\\')
141 - i++
142 - continue
143 - case 'n':
144 - result.WriteByte('\n')
145 - i++
146 - continue
147 - case 'r':
148 - result.WriteByte('\r')
149 - i++
150 - continue
151 - case 't':
152 - result.WriteByte('\t')
153 - i++
154 - continue
155 - case 's':
156 - result.WriteByte(' ')
157 - i++
158 - continue
159 - case ' ':
160 - // Backslash-space is an escape for space
161 - result.WriteByte(' ')
162 - i++
163 - continue
164 - default:
165 - // Not an escape sequence, just add the backslash
166 - result.WriteByte(s[i])
167 - }
168 - } else {
169 - result.WriteByte(s[i])
170 - }
171 - }
172 - return result.String()
173 -}
174 -
175 -// regexSubstitute performs regex find and replace.
176 -func regexSubstitute(value Value, paramStr string) (Value, error) {
177 - // paramStr format: "pattern\noutput"
178 - parts := strings.SplitN(paramStr, "\n", 2)
179 - if len(parts) != 2 {
180 - return Value{}, fmt.Errorf("regex substitution requires pattern and output")
181 - }
182 -
183 - pattern := parts[0]
184 - output := parts[1]
185 -
186 - // Debug: log the parsed parts
187 - //fmt.Printf("DEBUG regsub: pattern='%s' output='%s'\n", pattern, output)
188 -
189 - // Compile regex with safety checks and caching
190 - re, err := compileRegexSafe(pattern, 0)
191 - if err != nil {
192 - return Value{}, fmt.Errorf("invalid regex pattern: %w", err)
193 - }
194 -
195 - // Check if there's a match - if no match, return error (with timeout protection)
196 - matched, err := matchWithTimeout(re, value.Data, defaultRegexTimeout)
197 - if err != nil {
198 - return Value{}, fmt.Errorf("regex match failed: %w", err)
199 - }
200 - if !matched {
201 - return Value{}, fmt.Errorf("regex pattern does not match")
202 - }
203 -
204 - // Find match with capture groups (with timeout protection)
205 - // Zabbix regsub returns only the replacement string (not modified original)
206 - matches, err := findStringSubmatchIndexWithTimeout(re, value.Data, defaultRegexTimeout)
207 - if err != nil {
208 - return Value{}, fmt.Errorf("regex substitution failed: %w", err)
209 - }
210 - if matches == nil {
211 - return Value{}, fmt.Errorf("regex pattern does not match")
212 - }
213 -
214 - // Convert Zabbix-style backreferences (\1, \2) to Go-style ($1, $2)
215 - // Zabbix uses \N syntax, but Go's regexp.Expand() uses $N syntax
216 - // Must preserve escaped backslashes (\\1 should become literal \1, not $1)
217 - goOutput := convertBackreferences(output)
218 -
219 - // Use regexp.Expand() for proper capture group expansion
220 - // This correctly handles:
221 - // - Multi-digit backreferences (${10}, ${11}, etc.)
222 - // - Escaped backslashes
223 - // - All edge cases (unmatched groups, nested references, etc.)
224 - var replacement []byte
225 - replacement = re.Expand(replacement, []byte(goOutput), []byte(value.Data), matches)
226 -
227 - // Return the replacement string (not the modified original string)
228 - return Value{Data: string(replacement), Type: value.Type}, nil
229 -}
230 -
231 -// convertBackreferences converts Zabbix-style \N to Go-style ${N}
232 -// Preserves escaped backslashes: \\1 remains \1 (literal)
233 -// Uses ${N} instead of $N to avoid ambiguity (e.g., $1y vs ${1}y)
234 -func convertBackreferences(s string) string {
235 - var result strings.Builder
236 - i := 0
237 - for i < len(s) {
238 - if i+1 < len(s) && s[i] == '\\' {
239 - if s[i+1] == '\\' {
240 - // Escaped backslash: \\1 → \1 (literal)
241 - result.WriteByte('\\')
242 - i += 2
243 - continue
244 - } else if s[i+1] >= '0' && s[i+1] <= '9' {
245 - // Backreference: \1 → ${1}, \10 → ${10}, etc.
246 - result.WriteString("${")
247 - // Handle multi-digit backreferences (\10, \11, etc.)
248 - j := i + 1
249 - for j < len(s) && s[j] >= '0' && s[j] <= '9' {
250 - result.WriteByte(s[j])
251 - j++
252 - }
253 - result.WriteByte('}')
254 - i = j
255 - continue
256 - }
257 - }
258 - result.WriteByte(s[i])
259 - i++
260 - }
261 - return result.String()
262 -}
263 -
264 -// bool2Dec converts boolean values to decimal.
265 -func bool2Dec(value Value) (Value, error) {
266 - // Empty input is an error
267 - if value.Data == "" {
268 - return Value{}, fmt.Errorf("empty input for bool2dec")
269 - }
270 -
271 - v := strings.ToLower(strings.TrimSpace(value.Data))
272 -
273 - // Zabbix considers these as true (full words and single letters)
274 - trueValues := map[string]bool{
275 - "1": true, "true": true, "yes": true, "on": true,
276 - "t": true, "y": true, "ok": true,
277 - }
278 -
279 - // Zabbix considers these as false (full words and single letters)
280 - falseValues := map[string]bool{
281 - "0": true, "false": true, "no": true, "off": true,
282 - "f": true, "n": true, "err": true, "error": true,
283 - }
284 -
285 - var result int64
286 - if trueValues[v] {
287 - result = 1
288 - } else if falseValues[v] {
289 - result = 0
290 - } else {
291 - // Try to parse as number
292 - num, err := strconv.ParseInt(v, 10, 64)
293 - if err != nil {
294 - // If not a recognized value and not a number, return error
295 - return Value{}, fmt.Errorf("unrecognized boolean value: %s", v)
296 - }
297 - if num != 0 {
298 - result = 1
299 - }
300 - }
301 -
302 - return Value{Data: fmt.Sprintf("%d", result), Type: ValueTypeUint64}, nil
303 -}
304 -
305 -// oct2Dec converts octal string to decimal.
306 -func oct2Dec(value Value) (Value, error) {
307 - // Parse as octal
308 - num, err := strconv.ParseInt(strings.TrimSpace(value.Data), parseBase8, parseBitSize64)
309 - if err != nil {
310 - return Value{}, fmt.Errorf("invalid octal value: %w", err)
311 - }
312 -
313 - return Value{Data: fmt.Sprintf("%d", num), Type: ValueTypeUint64}, nil
314 -}
315 -
316 -// hex2Dec converts hexadecimal string to decimal.
317 -func hex2Dec(value Value) (Value, error) {
318 - // Parse as hexadecimal (with or without 0x prefix)
319 - data := strings.TrimSpace(value.Data)
320 - if strings.HasPrefix(data, "0x") || strings.HasPrefix(data, "0X") {
321 - data = data[2:]
322 - }
323 -
324 - // Remove spaces for parsing
325 - data = strings.ReplaceAll(data, " ", "")
326 -
327 - num, err := strconv.ParseInt(data, parseBase16, parseBitSize64)
328 - if err != nil {
329 - return Value{}, fmt.Errorf("invalid hexadecimal value: %w", err)
330 - }
331 -
332 - return Value{Data: fmt.Sprintf("%d", num), Type: ValueTypeUint64}, nil
333 -}
334 -
335 -// deltaValue calculates the difference between current and previous value.
336 -// Thread-safe for concurrent use. State isolated per shard and item.
337 -func (p *Preprocessor) deltaValue(itemID string, value Value, paramStr string) (Value, error) {
338 - // State key: shardID:itemID:operation
339 - stateKey := fmt.Sprintf("%s:%s:delta_value", p.shardID, itemID)
340 -
341 - // Parse numeric value before acquiring lock
342 - numValue, err := strconv.ParseFloat(value.Data, 64)
343 - if err != nil {
344 - return Value{}, fmt.Errorf("invalid numeric value: %w", err)
345 - }
346 -
347 - // Acquire exclusive lock for read-modify-write operation
348 - p.mu.Lock()
349 - defer p.mu.Unlock()
350 -
351 - state, hasState := p.state[stateKey]
352 -
353 - if !hasState {
354 - // First call - no previous value
355 - p.state[stateKey] = &OperationState{
356 - LastValue: value.Data,
357 - LastTimestamp: value.Timestamp,
358 - LastAccess: time.Now(),
359 - }
360 - return Value{Data: "0", Type: ValueTypeStr}, nil
361 - }
362 -
363 - // Update access time
364 - state.LastAccess = time.Now()
365 -
366 - // Calculate delta
367 - prevValue, err := strconv.ParseFloat(state.LastValue, 64)
368 - if err != nil {
369 - // If we can't parse previous, reset and return 0
370 - p.state[stateKey] = &OperationState{
371 - LastValue: value.Data,
372 - LastTimestamp: value.Timestamp,
373 - LastAccess: time.Now(),
374 - }
375 - return Value{Data: "0", Type: ValueTypeStr}, nil
376 - }
377 -
378 - delta := numValue - prevValue
379 -
380 - // Update state
381 - p.state[stateKey] = &OperationState{
382 - LastValue: value.Data,
383 - LastTimestamp: value.Timestamp,
384 - LastAccess: time.Now(),
385 - }
386 -
387 - // Discard negative deltas (value decreased)
388 - if delta < 0 {
389 - return Value{Data: "", Type: ValueTypeStr}, nil
390 - }
391 -
392 - // Return delta
393 - if delta == float64(int64(delta)) {
394 - return Value{Data: fmt.Sprintf("%d", int64(delta)), Type: ValueTypeStr}, nil
395 - }
396 - return Value{Data: fmt.Sprintf("%g", delta), Type: ValueTypeStr}, nil
397 -}
398 -
399 -// deltaSpeed calculates the speed (delta per second).
400 -// Thread-safe for concurrent use. State isolated per shard and item.
401 -func (p *Preprocessor) deltaSpeed(itemID string, value Value, paramStr string) (Value, error) {
402 - // State key: shardID:itemID:operation
403 - stateKey := fmt.Sprintf("%s:%s:delta_speed", p.shardID, itemID)
404 -
405 - // Parse numeric value before acquiring lock
406 - numValue, err := strconv.ParseFloat(value.Data, 64)
407 - if err != nil {
408 - return Value{}, fmt.Errorf("invalid numeric value: %w", err)
409 - }
410 -
411 - // Acquire exclusive lock for read-modify-write operation
412 - p.mu.Lock()
413 - defer p.mu.Unlock()
414 -
415 - state, hasState := p.state[stateKey]
416 -
417 - if !hasState {
418 - // First call - no previous value
419 - p.state[stateKey] = &OperationState{
420 - LastValue: value.Data,
421 - LastValueTime: value.Timestamp,
422 - LastTimestamp: value.Timestamp,
423 - LastAccess: time.Now(),
424 - }
425 - return Value{Data: "0", Type: ValueTypeStr}, nil
426 - }
427 -
428 - // Update access time
429 - state.LastAccess = time.Now()
430 -
431 - // Calculate delta
432 - prevValue, err := strconv.ParseFloat(state.LastValue, 64)
433 - if err != nil {
434 - // If we can't parse previous, reset and return 0
435 - p.state[stateKey] = &OperationState{
436 - LastValue: value.Data,
437 - LastValueTime: value.Timestamp,
438 - LastTimestamp: value.Timestamp,
439 - LastAccess: time.Now(),
440 - }
441 - return Value{Data: "0", Type: ValueTypeStr}, nil
442 - }
443 -
444 - delta := numValue - prevValue
445 - timeDiff := value.Timestamp.Sub(state.LastValueTime).Seconds()
446 -
447 - // Update state
448 - p.state[stateKey] = &OperationState{
449 - LastValue: value.Data,
450 - LastValueTime: value.Timestamp,
451 - LastTimestamp: value.Timestamp,
452 - LastAccess: time.Now(),
453 - }
454 -
455 - // Discard if time went backwards or stayed the same
456 - if timeDiff <= 0 {
457 - return Value{Data: "", Type: ValueTypeStr}, nil
458 - }
459 -
460 - // Discard if value decreased (negative delta)
461 - if delta < 0 {
462 - return Value{Data: "", Type: ValueTypeStr}, nil
463 - }
464 -
465 - speed := delta / timeDiff
466 -
467 - // Format based on input value type
468 - if value.Type == ValueTypeUint64 {
469 - // For UINT64, truncate to integer
470 - return Value{Data: fmt.Sprintf("%d", int64(speed)), Type: ValueTypeStr}, nil
471 - }
472 -
473 - // For other types, use float formatting
474 - if speed == float64(int64(speed)) {
475 - return Value{Data: fmt.Sprintf("%d", int64(speed)), Type: ValueTypeStr}, nil
476 - }
477 - return Value{Data: fmt.Sprintf("%g", speed), Type: ValueTypeStr}, nil
478 -}
479 -
480 -// stringReplace replaces one string with another.
481 -func stringReplace(value Value, paramStr string) (Value, error) {
482 - // paramStr format: "search\nreplace"
483 - parts := strings.SplitN(paramStr, "\n", 2)
484 - if len(parts) != 2 {
485 - return Value{}, fmt.Errorf("string replace requires search and replace parameters")
486 - }
487 -
488 - // Interpret escape sequences in search and replace strings
489 - search := interpretEscapeSequences(parts[0])
490 - replace := interpretEscapeSequences(parts[1])
491 -
492 - // Empty search string is not allowed
493 - if search == "" {
494 - return Value{}, fmt.Errorf("search string cannot be empty")
495 - }
496 -
497 - result := strings.ReplaceAll(value.Data, search, replace)
498 - return Value{Data: result, Type: value.Type}, nil
499 -}
500 -
501 -// validateRange validates that a value is within a numeric range.
502 -func validateRange(value Value, paramStr string) (Value, error) {
503 - // paramStr format: "min\nmax"
504 - parts := strings.SplitN(paramStr, "\n", 2)
505 - if len(parts) != 2 {
506 - return Value{}, fmt.Errorf("validate range requires min and max parameters")
507 - }
508 -
509 - minStr := strings.TrimSpace(parts[0])
510 - maxStr := strings.TrimSpace(parts[1])
511 -
512 - numValue, err := strconv.ParseFloat(value.Data, 64)
513 - if err != nil {
514 - return Value{}, fmt.Errorf("invalid numeric value: %w", err)
515 - }
516 -
517 - min, err := strconv.ParseFloat(minStr, 64)
518 - if err != nil {
519 - return Value{}, fmt.Errorf("invalid minimum value: %w", err)
520 - }
521 -
522 - max, err := strconv.ParseFloat(maxStr, 64)
523 - if err != nil {
524 - return Value{}, fmt.Errorf("invalid maximum value: %w", err)
525 - }
526 -
527 - if numValue < min || numValue > max {
528 - return Value{}, fmt.Errorf("value %g is out of range [%g, %g]", numValue, min, max)
529 - }
530 -
531 - return value, nil
532 -}
533 -
534 -// validateRegex validates that a value matches a regex pattern.
535 -func validateRegex(value Value, paramStr string) (Value, error) {
536 - pattern := strings.TrimSpace(paramStr)
537 - if pattern == "" {
538 - return Value{}, fmt.Errorf("validate regex requires a pattern")
539 - }
540 -
541 - re, err := compileRegexSafe(pattern, 0)
542 - if err != nil {
543 - return Value{}, fmt.Errorf("invalid regex pattern: %w", err)
544 - }
545 -
546 - matched, err := matchWithTimeout(re, value.Data, defaultRegexTimeout)
547 - if err != nil {
548 - return Value{}, fmt.Errorf("regex validation failed: %w", err)
549 - }
550 - if !matched {
551 - return Value{}, fmt.Errorf("value does not match regex pattern: %s", pattern)
552 - }
553 -
554 - return value, nil
555 -}
556 -
557 -// validateNotRegex validates that a value does NOT match a regex pattern.
558 -func validateNotRegex(value Value, paramStr string) (Value, error) {
559 - pattern := strings.TrimSpace(paramStr)
560 - if pattern == "" {
561 - return Value{}, fmt.Errorf("validate not regex requires a pattern")
562 - }
563 -
564 - re, err := compileRegexSafe(pattern, 0)
565 - if err != nil {
566 - return Value{}, fmt.Errorf("invalid regex pattern: %w", err)
567 - }
568 -
569 - matched, err := matchWithTimeout(re, value.Data, defaultRegexTimeout)
570 - if err != nil {
571 - return Value{}, fmt.Errorf("regex validation failed: %w", err)
572 - }
573 - if matched {
574 - return Value{}, fmt.Errorf("value matches regex pattern but should not: %s", pattern)
575 - }
576 -
577 - return value, nil
578 -}
579 -
580 -// validateNotSupported checks if an error from a previous step is "not supported".
581 -// - If input is a VALUE (not an error): pass through unchanged (succeed)
582 -// - If input is an ERROR:
583 -// - If params is empty: fail (any error is considered "supported")
584 -// - If params has pattern: check if error matches pattern
585 -// - Matches: fail (error is "supported"/expected)
586 -// - Doesn't match: succeed (error is "not supported"/unexpected)
587 -func validateNotSupported(value Value, paramStr string) (Value, error) {
588 - // If input is not an error, pass through unchanged
589 - if !value.IsError {
590 - return value, nil
591 - }
592 -
593 - // Input is an error - validate it
594 - paramStr = strings.TrimSpace(paramStr)
595 -
596 - // If no params provided, any error is considered "supported" - fail
597 - if paramStr == "" {
598 - return Value{}, fmt.Errorf("value is not supported")
599 - }
600 -
601 - // Parse params: "mode\npattern"
602 - parts := strings.SplitN(paramStr, "\n", 2)
603 - if len(parts) != 2 {
604 - // Invalid params format, treat as "any error is supported"
605 - return Value{}, fmt.Errorf("value is not supported")
606 - }
607 -
608 - // mode := strings.TrimSpace(parts[0]) // Mode not currently used
609 - pattern := strings.TrimSpace(parts[1])
610 -
611 - // Empty pattern means any error is supported
612 - if pattern == "" {
613 - return Value{}, fmt.Errorf("value is not supported")
614 - }
615 -
616 - // Check if error message matches pattern (regex match)
617 - re, err := compileRegexSafe(pattern, 0)
618 - if err != nil {
619 - // Invalid regex pattern - treat as validation failure
620 - return Value{}, fmt.Errorf("invalid pattern in validation: %w", err)
621 - }
622 -
623 - matched, err := matchWithTimeout(re, value.Data, defaultRegexTimeout)
624 - if err != nil {
625 - // Regex timeout or error
626 - return Value{}, fmt.Errorf("pattern matching failed: %w", err)
627 - }
628 -
629 - if matched {
630 - // Error matches pattern - it's "supported"/expected, so fail
631 - return Value{}, fmt.Errorf("value is not supported")
632 - }
633 -
634 - // Error doesn't match pattern - it's "not supported"/unexpected, so succeed
635 - // Pass through the error data unchanged
636 - return value, nil
637 -}
src/go/plugin/scripts.d/pkg/zabbixpreproc/testdata/item_preproc_csv_to_json.yaml deleted
-517
@@ -1,517 +0,0 @@
1 ----
2 -test case: 'missing second parameter'
3 -in:
4 - csv: ''
5 - params: ''
6 -out:
7 - result: ''
8 - return: 'FAIL'
9 ----
10 -test case: 'missing third parameter'
11 -in:
12 - csv: ''
13 - params: "\n"
14 -out:
15 - result: ''
16 - return: 'FAIL'
17 ----
18 -test case: 'invalid first parameter'
19 -in:
20 - csv: ''
21 - params: ",.\n\n0"
22 -out:
23 - result: ''
24 - return: 'FAIL'
25 ----
26 -test case: 'invalid second parameter'
27 -in:
28 - csv: ''
29 - params: "\n,.\n0"
30 -out:
31 - result: ''
32 - return: 'FAIL'
33 ----
34 -test case: 'empty input (1)'
35 -in:
36 - csv: ''
37 - params: "\n\n1"
38 -out:
39 - result: '[]'
40 - return: 'SUCCEED'
41 ----
42 -test case: 'empty input (2)'
43 -in:
44 - csv: ''
45 - params: "\n\n0"
46 -out:
47 - result: '[]'
48 - return: 'SUCCEED'
49 ----
50 -test case: 'single char (1)'
51 -in:
52 - csv: 'A'
53 - params: ",\n\n1"
54 -out:
55 - result: '[]'
56 - return: 'SUCCEED'
57 ----
58 -test case: 'single char (2)'
59 -in:
60 - csv: 'A'
61 - params: ",\n\n0"
62 -out:
63 - result: '[{"1":"A"}]'
64 - return: 'SUCCEED'
65 ----
66 -test case: 'single UTF8 4-bytes character (1)'
67 -in:
68 - csv: '😂'
69 - params: ",\n\n1"
70 -out:
71 - result: '[]'
72 - return: 'SUCCEED'
73 ----
74 -test case: 'single UTF8 4-bytes character (2)'
75 -in:
76 - csv: '😂'
77 - params: ",\n\n0"
78 -out:
79 - result: '[{"1":"😂"}]'
80 - return: 'SUCCEED'
81 ----
82 -test case: 'empty line (1)'
83 -in:
84 - csv: "\n"
85 - params: ",\n\n1"
86 -out:
87 - result: '[{"":""}]'
88 - return: 'SUCCEED'
89 ----
90 -test case: 'empty line (2)'
91 -in:
92 - csv: "\n"
93 - params: ",\n\n0"
94 -out:
95 - result: '[{},{}]'
96 - return: 'SUCCEED'
97 ----
98 -test case: 'multiple empty lines (1)'
99 -in:
100 - csv: "\n\n\n"
101 - params: ",\n\n1"
102 -out:
103 - result: '[{"":""},{"":""},{"":""}]'
104 - return: 'SUCCEED'
105 ----
106 -test case: 'multiple empty lines (2)'
107 -in:
108 - csv: "\n\n\n"
109 - params: ",\n\n0"
110 -out:
111 - result: '[{},{},{},{}]'
112 - return: 'SUCCEED'
113 ----
114 -test case: 'single column (1)'
115 -in:
116 - csv: |
117 - nr
118 - 1
119 - 2
120 - params: "\n\n1"
121 -out:
122 - result: '[{"nr":"1"},{"nr":"2"},{"nr":""}]'
123 - return: 'SUCCEED'
124 ----
125 -test case: 'single column (2)'
126 -in:
127 - csv: |
128 - nr
129 - 1
130 - 2
131 - params: "\n\n0"
132 -out:
133 - result: '[{"1":"nr"},{"1":"1"},{"1":"2"},{}]'
134 - return: 'SUCCEED'
135 ----
136 -test case: 'single line (1)'
137 -in:
138 - csv: "a,b,c"
139 - params: "\n\n1"
140 -out:
141 - result: '[]'
142 - return: 'SUCCEED'
143 ----
144 -test case: 'single line (2)'
145 -in:
146 - csv: "a,b,c"
147 - params: "\n\n0"
148 -out:
149 - result: '[{"1":"a","2":"b","3":"c"}]'
150 - return: 'SUCCEED'
151 ----
152 -test case: 'no trailing line break (1)'
153 -in:
154 - csv: "nr,\"name\",addr\n\"1\",abc,xyz\n\"2\",\"cba\",zyx"
155 - params: "\n\"\n1"
156 -out:
157 - result: '[{"nr":"1","name":"abc","addr":"xyz"},{"nr":"2","name":"cba","addr":"zyx"}]'
158 - return: 'SUCCEED'
159 ----
160 -test case: 'no trailing line break (2)'
161 -in:
162 - csv: "nr,\"name\",addr\n\"1\",abc,xyz\n\"2\",\"cba\","
163 - params: "\n\"\n1"
164 -out:
165 - result: '[{"nr":"1","name":"abc","addr":"xyz"},{"nr":"2","name":"cba","addr":""}]'
166 - return: 'SUCCEED'
167 ----
168 -test case: 'no trailing line break (3)'
169 -in:
170 - csv: "nr,\"name\",addr\n\"1\",abc,xyz\n\"2\",\"cba\",\"zyx\""
171 - params: "\n\"\n1"
172 -out:
173 - result: '[{"nr":"1","name":"abc","addr":"xyz"},{"nr":"2","name":"cba","addr":"zyx"}]'
174 - return: 'SUCCEED'
175 ----
176 -test case: 'empty fields (1)'
177 -in:
178 - csv: ",,"
179 - params: "\n\n1"
180 -out:
181 - result: ''
182 - return: 'FAIL'
183 ----
184 -test case: 'empty fields (2)'
185 -in:
186 - csv: ",,"
187 - params: "\n\n0"
188 -out:
189 - result: '[{"1":"","2":"","3":""}]'
190 - return: 'SUCCEED'
191 ----
192 -test case: 'empty fields (3)'
193 -in:
194 - csv: "a,b,c\n,,"
195 - params: "\n\n1"
196 -out:
197 - result: '[{"a":"","b":"","c":""}]'
198 - return: 'SUCCEED'
199 ----
200 -test case: 'delimiter in quoted field'
201 -in:
202 - csv: '`fld,1`,`fld,2`'
203 - params: "\n`\n0"
204 -out:
205 - result: '[{"1":"fld,1","2":"fld,2"}]'
206 - return: 'SUCCEED'
207 ----
208 -test case: 'quotation character in unquoted field'
209 -in:
210 - csv: 'fld`1,fld`2'
211 - params: "\n`\n0"
212 -out:
213 - result: '[{"1":"fld`1","2":"fld`2"}]'
214 - return: 'SUCCEED'
215 ----
216 -test case: 'quotation character in unquoted field (2)'
217 -in:
218 - csv: ' `fld`1` , `fld`2` '
219 - params: "\n`\n0"
220 -out:
221 - result: '[{"1":" `fld`1` ","2":" `fld`2` "}]'
222 - return: 'SUCCEED'
223 ----
224 -test case: 'escaped quotation character'
225 -in:
226 - csv: '`fld``1`,`fld``2`'
227 - params: "\n`\n0"
228 -out:
229 - result: '[{"1":"fld`1","2":"fld`2"}]'
230 - return: 'SUCCEED'
231 ----
232 -test case: 'delimiter set in sep line'
233 -in:
234 - csv: |-
235 - sEp=.
236 - col1,.;col2,.;
237 - fld1,.;fld2,.;
238 - params: "\n\n1"
239 -out:
240 - result: '[{"col1,":"fld1,",";col2,":";fld2,",";":";"}]'
241 - return: 'SUCCEED'
242 ----
243 -test case: 'cr/nl line breaks'
244 -in:
245 - csv: "Sep=.\r\nname.addr\r\nabc.xyz\r\n\r\nfld1.fld2\r\n\r\n\r\nfld3.fld4\r\n"
246 - params: "\n\n1"
247 -out:
248 - result: '[{"name":"abc","addr":"xyz"},{"name":"","addr":""},{"name":"fld1","addr":"fld2"},{"name":"","addr":""},{"name":"","addr":""},{"name":"fld3","addr":"fld4"},{"name":"","addr":""}]'
249 - return: 'SUCCEED'
250 ----
251 -test case: 'various length rows'
252 -in:
253 - csv: |-
254 - fld1,fld2,fld3
255 - fld1,fld2,fld3,fld4
256 - fld1,fld2
257 - fld1,fld2,fld3,fld4,fld5
258 - params: "\n\n0"
259 -out:
260 - result: '[{"1":"fld1","2":"fld2","3":"fld3"},{"1":"fld1","2":"fld2","3":"fld3","4":"fld4"},{"1":"fld1","2":"fld2"},{"1":"fld1","2":"fld2","3":"fld3","4":"fld4","5":"fld5"}]'
261 - return: 'SUCCEED'
262 ----
263 -test case: 'equal delimiter and quotation characters'
264 -in:
265 - csv: |-
266 - ,fld1,,fld2
267 - params: ",\n,\n0"
268 -out:
269 - result: '[{"1":"","2":"fld1","3":"","4":"fld2"}]'
270 - return: 'SUCCEED'
271 ----
272 -test case: 'UTF8 2-byte delimiter'
273 -in:
274 - csv: |-
275 - col1фcol2
276 - fld1фfld2
277 - params: "ф\n\n1"
278 -out:
279 - result: '[{"col1":"fld1","col2":"fld2"}]'
280 - return: 'SUCCEED'
281 ----
282 -test case: 'UTF8 3-byte delimiter set in sep line'
283 -in:
284 - csv: |-
285 - sep=ꢂ
286 - col1ꢂcol2
287 - fld1ꢂfld2
288 - params: "\n\n1"
289 -out:
290 - result: '[{"col1":"fld1","col2":"fld2"}]'
291 - return: 'SUCCEED'
292 ----
293 -test case: 'UTF8 4-byte delimiter'
294 -in:
295 - csv: |-
296 - col1😀col2
297 - fld1😀fld2
298 - params: "😀\n\n1"
299 -out:
300 - result: '[{"col1":"fld1","col2":"fld2"}]'
301 - return: 'SUCCEED'
302 ----
303 -test case: 'UTF8 2-byte quotation character'
304 -in:
305 - csv: |-
306 - ыcol1ы,ыcol,2ы
307 - ыfld1ы,ыfld,2ы
308 - params: "\nы\n1"
309 -out:
310 - result: '[{"col1":"fld1","col,2":"fld,2"}]'
311 - return: 'SUCCEED'
312 ----
313 -test case: 'UTF8 3-byte quotation character'
314 -in:
315 - csv: |-
316 - ꢂcol1ꢂ,ꢂcol,2ꢂ
317 - ꢂfld1ꢂ,ꢂfld,2ꢂ
318 - params: "\nꢂ\n1"
319 -out:
320 - result: '[{"col1":"fld1","col,2":"fld,2"}]'
321 - return: 'SUCCEED'
322 ----
323 -test case: 'UTF8 multi-byte delimiter and quotation characters'
324 -in:
325 - csv: |-
326 - 😀col1😀ꢂ😀col2😀ꢂ😀😀
327 - fld1ꢂ😀fld2😀ꢂ
328 - params: "ꢂ\n😀\n1"
329 -out:
330 - result: '[{"col1":"fld1","col2":"fld2","":""}]'
331 - return: 'SUCCEED'
332 ----
333 -test case: 'delimiter set to space'
334 -in:
335 - csv: |-
336 - col1 col2 `col 3`
337 - fld1 fld2 `fld 3`
338 - params: " \n`\n1"
339 -out:
340 - result: '[{"col1":"fld1","col2":"fld2","col 3":"fld 3","":""}]'
341 - return: 'SUCCEED'
342 ----
343 -test case: 'quotation character set to space'
344 -in:
345 - csv: |-
346 - col1, col2 ,col 3
347 - fld1 ,fld2, fld 3
348 - params: "\n \n1"
349 -out:
350 - result: '[{"col1":"fld1","col2":"fld2","col 3":"fld 3"}]'
351 - return: 'SUCCEED'
352 ----
353 -test case: 'sep line only (1)'
354 -in:
355 - csv: 'sep=.'
356 - params: "\n\n1"
357 -out:
358 - result: '[]'
359 - return: 'SUCCEED'
360 ----
361 -test case: 'sep line only (2)'
362 -in:
363 - csv: 'sep=.'
364 - params: "\n\n0"
365 -out:
366 - result: '[]'
367 - return: 'SUCCEED'
368 ----
369 -test case: 'sep line only (3)'
370 -in:
371 - csv: "sep=.\n"
372 - params: "\n\n1"
373 -out:
374 - result: '[]'
375 - return: 'SUCCEED'
376 ----
377 -test case: 'sep line only (4)'
378 -in:
379 - csv: 'sep=.'
380 - params: "\n\n0"
381 -out:
382 - result: '[]'
383 - return: 'SUCCEED'
384 ----
385 -test case: 'unsupported sep line (1)'
386 -in:
387 - csv: |-
388 - sep=.;
389 - fld1,fld2
390 - params: "\n\n0"
391 -out:
392 - result: '[{"1":"sep=.;"},{"1":"fld1","2":"fld2"}]'
393 - return: 'SUCCEED'
394 ----
395 -test case: 'unsupported sep line (2)'
396 -in:
397 - csv: |-
398 - sep=
399 - fld1,fld2
400 - params: "\n\n0"
401 -out:
402 - result: '[{"1":"sep="},{"1":"fld1","2":"fld2"}]'
403 - return: 'SUCCEED'
404 ----
405 -test case: 'duplicated column names (1)'
406 -in:
407 - csv: |-
408 - col1,col1
409 - fld1,fld2
410 - params: "\n\n1"
411 -out:
412 - result: ''
413 - return: 'FAIL'
414 ----
415 -test case: 'duplicated column names (2)'
416 -in:
417 - csv: |-
418 - ,
419 - fld1,fld2
420 - params: "\n\n1"
421 -out:
422 - result: ''
423 - return: 'FAIL'
424 ----
425 -test case: 'more fields in data row than in header'
426 -in:
427 - csv: |-
428 - col1,col1
429 - fld1,fld2,fld3
430 - params: "\n\n1"
431 -out:
432 - result: ''
433 - return: 'FAIL'
434 ----
435 -test case: 'unclosed quoted field'
436 -in:
437 - csv: 'col1,"col1'
438 - params: "\n\"\n0"
439 -out:
440 - result: ''
441 - return: 'FAIL'
442 ----
443 -test case: 'unclosed quoted field (UTF8-2)'
444 -in:
445 - csv: 'col1,ыcol1'
446 - params: "\nы\n0"
447 -out:
448 - result: ''
449 - return: 'FAIL'
450 ----
451 -test case: 'unclosed quoted field (UTF8-3)'
452 -in:
453 - csv: 'col1,ꢂcol1'
454 - params: "\nꢂ\n0"
455 -out:
456 - result: ''
457 - return: 'FAIL'
458 ----
459 -test case: 'unclosed quoted field (UTF8-4)'
460 -in:
461 - csv: 'col1,😀col1'
462 - params: "\n😀\n0"
463 -out:
464 - result: ''
465 - return: 'FAIL'
466 ----
467 -test case: 'escaped quotation character in unclosed quoted field (UTF8-3)'
468 -in:
469 - csv: 'col1,ꢂcol1ꢂꢂ'
470 - params: "\nꢂ\n0"
471 -out:
472 - result: ''
473 - return: 'FAIL'
474 ----
475 -test case: 'missing delimiter or line break after quoted field'
476 -in:
477 - csv: 'col1,"col1" '
478 - params: "\n\"\n0"
479 -out:
480 - result: ''
481 - return: 'FAIL'
482 ----
483 -test case: 'unsupported line break'
484 -in:
485 - csv: "col1,col1\n\rfld1,fld2"
486 - params: "\n\n0"
487 -out:
488 - result: ''
489 - return: 'FAIL'
490 ----
491 -test case: 'CSV sample input (1)'
492 -in:
493 - csv: |
494 - sEp=.
495 - ;c`2;`;```;😀ꢂыa;col.5;`col6`;col7
496 - fld.1;`fld.2`;f`3;`;```;;``;aыꢂ😀
497 -
498 - ``;fld2`ы;fld3ꢂ`;`😀`;;``
499 -
500 -
501 - ;c`2;`;```;😀ꢂыa;col.5;`col6`;col7
502 - params: ";\n`\n1"
503 -out:
504 - result: '[{"":"fld.1","c`2":"fld.2",";`":"f`3","😀ꢂыa":";`","col.5":"","col6":"","col7":"aыꢂ😀"},{"":"","c`2":"",";`":"","😀ꢂыa":"","col.5":"","col6":"","col7":""},{"":"","c`2":"fld2`ы",";`":"fld3ꢂ`","😀ꢂыa":"😀","col.5":"","col6":"","col7":""},{"":"","c`2":"",";`":"","😀ꢂыa":"","col.5":"","col6":"","col7":""},{"":"","c`2":"",";`":"","😀ꢂыa":"","col.5":"","col6":"","col7":""},{"":"","c`2":"c`2",";`":";`","😀ꢂыa":"😀ꢂыa","col.5":"col.5","col6":"col6","col7":"col7"},{"":"","c`2":"",";`":"","😀ꢂыa":"","col.5":"","col6":"","col7":""}]'
505 - return: 'SUCCEED'
506 ----
507 -test case: 'CSV sample input (2)'
508 -in:
509 - csv: |-
510 - sEp=.
511 - col1,.;col2,.;
512 - fld1,.;fld2,.;
513 - params: ";\n\n1"
514 -out:
515 - result: '[{"col1,.":"fld1,.","col2,.":"fld2,.","":""}]'
516 - return: 'SUCCEED'
517 -...
src/go/plugin/scripts.d/pkg/zabbixpreproc/testdata/item_preproc_xpath.yaml deleted
-105
@@ -1,105 +0,0 @@
1 ----
2 -test case: 'empty input parameters'
3 -in:
4 - xml: ''
5 - xpath: ''
6 -out:
7 - result: ''
8 - return: 'FAIL'
9 ----
10 -test case: 'single start tag'
11 -in:
12 - xml: '<a>'
13 - xpath: ''
14 -out:
15 - result: ''
16 - return: 'FAIL'
17 ----
18 -test case: 'single end tag'
19 -in:
20 - xml: '<a/>'
21 - xpath: ''
22 -out:
23 - result: ''
24 - return: 'FAIL'
25 ----
26 -test case: 'wrong operation format'
27 -in:
28 - xml: '<a/>'
29 - xpath: '/a[\'
30 -out:
31 - result: ''
32 - return: 'FAIL'
33 ----
34 -test case: 'wrong operation expression'
35 -in:
36 - xml: '<a/>'
37 - xpath: '1 div 0'
38 -out:
39 - result: ''
40 - return: 'FAIL'
41 ----
42 -test case: 'wrong operation format 2'
43 -in:
44 - xml: '<a/>'
45 - xpath: '-a'
46 -out:
47 - result: ''
48 - return: 'FAIL'
49 ----
50 -test case: 'empty output'
51 -in:
52 - xml: '<a/>'
53 - xpath: '/b'
54 -out:
55 - result: ''
56 - return: 'SUCCEED'
57 ----
58 -test case: 'successful expression'
59 -in:
60 - xml: '<a/>'
61 - xpath: '3 div 2'
62 -out:
63 - result: '1.5'
64 - return: 'SUCCEED'
65 ----
66 -test case: 'return end tag'
67 -in:
68 - xml: '<a/>'
69 - xpath: '/a'
70 -out:
71 - result: '<a/>'
72 - return: 'SUCCEED'
73 ----
74 -test case: 'return text'
75 -in:
76 - xml: '<a>1</a>'
77 - xpath: '/a/text()'
78 -out:
79 - result: '1'
80 - return: 'SUCCEED'
81 ----
82 -test case: 'return string'
83 -in:
84 - xml: '<a>1</a>'
85 - xpath: 'string(/a)'
86 -out:
87 - result: '1'
88 - return: 'SUCCEED'
89 ----
90 -test case: 'return attribute'
91 -in:
92 - xml: '<a b="10">1</a>'
93 - xpath: 'string(/a/@b)'
94 -out:
95 - result: '10'
96 - return: 'SUCCEED'
97 ----
98 -test case: 'return pattern'
99 -in:
100 - xml: '<a><b x="1"/><c x="2"/><d x="1"/></a>'
101 - xpath: '//*[@x="1"]'
102 -out:
103 - result: '<b x="1"/><d x="1"/>'
104 - return: 'SUCCEED'
105 -...
src/go/plugin/scripts.d/pkg/zabbixpreproc/testdata/zbx_item_preproc.yaml deleted
-4630
@@ -1,4630 +0,0 @@
1 ----
2 -test case: string(10) * 10
3 -in:
4 - value:
5 - value_type: ITEM_VALUE_TYPE_STR
6 - time: 2017-10-29 03:15:00 +03:00
7 - data: 10
8 - step:
9 - type: ZBX_PREPROC_MULTIPLIER
10 - params: 10
11 -out:
12 - return: SUCCEED
13 - value: 100
14 ----
15 -test case: string(10x) * 10
16 -in:
17 - value:
18 - value_type: ITEM_VALUE_TYPE_STR
19 - time: 2017-10-29 03:15:00 +03:00
20 - data: 10x
21 - step:
22 - type: ZBX_PREPROC_MULTIPLIER
23 - params: 10
24 -out:
25 - return: FAIL
26 ----
27 -test case: string(10) * abc
28 -in:
29 - value:
30 - value_type: ITEM_VALUE_TYPE_STR
31 - time: 2017-10-29 03:15:00 +03:00
32 - data: 10
33 - step:
34 - type: ZBX_PREPROC_MULTIPLIER
35 - params: abc
36 -out:
37 - return: FAIL
38 ----
39 -test case: string(1.5) * 3
40 -in:
41 - value:
42 - value_type: ITEM_VALUE_TYPE_STR
43 - time: 2017-10-29 03:15:00 +03:00
44 - data: 1.5
45 - step:
46 - type: ZBX_PREPROC_MULTIPLIER
47 - params: 3
48 -out:
49 - return: SUCCEED
50 - value: 4.5
51 ----
52 -test case: uint64(1.5) * 3
53 -in:
54 - value:
55 - value_type: ITEM_VALUE_TYPE_UINT64
56 - time: 2017-10-29 03:15:00 +03:00
57 - data: 1.5
58 - step:
59 - type: ZBX_PREPROC_MULTIPLIER
60 - params: 3
61 -out:
62 - return: SUCCEED
63 - value: 3
64 ----
65 -test case: uint64(3) * 1.5
66 -in:
67 - value:
68 - value_type: ITEM_VALUE_TYPE_UINT64
69 - time: 2017-10-29 03:15:00 +03:00
70 - data: 3
71 - step:
72 - type: ZBX_PREPROC_MULTIPLIER
73 - params: 1.5
74 -out:
75 - return: SUCCEED
76 - value: 4
77 ----
78 -test case: float(1.5) * 3
79 -in:
80 - value:
81 - value_type: ITEM_VALUE_TYPE_FLOAT
82 - time: 2017-10-29 03:15:00 +03:00
83 - data: 1.5
84 - step:
85 - type: ZBX_PREPROC_MULTIPLIER
86 - params: 3
87 -out:
88 - return: SUCCEED
89 - value: 4.5
90 ----
91 -test case: float(3) * 1.5
92 -in:
93 - value:
94 - value_type: ITEM_VALUE_TYPE_FLOAT
95 - time: 2017-10-29 03:15:00 +03:00
96 - data: 3
97 - step:
98 - type: ZBX_PREPROC_MULTIPLIER
99 - params: 1.5
100 -out:
101 - return: SUCCEED
102 - value: 4.5
103 ----
104 -test case: rtrim(01abc01, 01)
105 -in:
106 - value:
107 - value_type: ITEM_VALUE_TYPE_STR
108 - time: 2017-10-29 03:15:00 +03:00
109 - data: 01abc01
110 - step:
111 - type: ZBX_PREPROC_RTRIM
112 - params: 01
113 -out:
114 - return: SUCCEED
115 - value: 01abc
116 ----
117 -test case: rtrim(01abc01, abc)
118 -in:
119 - value:
120 - value_type: ITEM_VALUE_TYPE_STR
121 - time: 2017-10-29 03:15:00 +03:00
122 - data: 01abc01
123 - step:
124 - type: ZBX_PREPROC_RTRIM
125 - params: abc
126 -out:
127 - return: SUCCEED
128 - value: 01abc01
129 ----
130 -test case: ltrim(01abc01, 01)
131 -in:
132 - value:
133 - value_type: ITEM_VALUE_TYPE_STR
134 - time: 2017-10-29 03:15:00 +03:00
135 - data: 01abc01
136 - step:
137 - type: ZBX_PREPROC_LTRIM
138 - params: 01
139 -out:
140 - return: SUCCEED
141 - value: abc01
142 ----
143 -test case: ltrim(01abc01, abc)
144 -in:
145 - value:
146 - value_type: ITEM_VALUE_TYPE_STR
147 - time: 2017-10-29 03:15:00 +03:00
148 - data: 01abc01
149 - step:
150 - type: ZBX_PREPROC_LTRIM
151 - params: abc
152 -out:
153 - return: SUCCEED
154 - value: 01abc01
155 ----
156 -test case: trim(01abc01, 01)
157 -in:
158 - value:
159 - value_type: ITEM_VALUE_TYPE_STR
160 - time: 2017-10-29 03:15:00 +03:00
161 - data: 01abc01
162 - step:
163 - type: ZBX_PREPROC_TRIM
164 - params: 01
165 -out:
166 - return: SUCCEED
167 - value: abc
168 ----
169 -test case: trim(01abc01, abc)
170 -in:
171 - value:
172 - value_type: ITEM_VALUE_TYPE_STR
173 - time: 2017-10-29 03:15:00 +03:00
174 - data: 01abc01
175 - step:
176 - type: ZBX_PREPROC_TRIM
177 - params: abc
178 -out:
179 - return: SUCCEED
180 - value: 01abc01
181 ----
182 -test case: trim(\\s, abc)
183 -in:
184 - value:
185 - value_type: ITEM_VALUE_TYPE_STR
186 - time: 2017-10-29 03:15:00 +03:00
187 - data: "\\ abc \\"
188 - step:
189 - type: ZBX_PREPROC_TRIM
190 - params: "\\\\s"
191 -out:
192 - return: SUCCEED
193 - value: "abc"
194 ----
195 -test case: regsub("test 123 number", "([0-9]+", 1)
196 -in:
197 - value:
198 - value_type: ITEM_VALUE_TYPE_STR
199 - time: 2017-10-29 03:15:00 +03:00
200 - data: test 123 number
201 - step:
202 - type: ZBX_PREPROC_REGSUB
203 - params: "([0-9]+\n\\1"
204 -out:
205 - return: FAIL
206 ----
207 -test case: regsub("test 123 number", "([0-9]+)", \1)
208 -in:
209 - value:
210 - value_type: ITEM_VALUE_TYPE_STR
211 - time: 2017-10-29 03:15:00 +03:00
212 - data: test 123 number
213 - step:
214 - type: ZBX_PREPROC_REGSUB
215 - params: "([0-9]+)\n\\1"
216 -out:
217 - return: SUCCEED
218 - value: 123
219 ----
220 -test case: regsub("test 123 number", "([0-9]+)", x\1y)
221 -in:
222 - value:
223 - value_type: ITEM_VALUE_TYPE_STR
224 - time: 2017-10-29 03:15:00 +03:00
225 - data: test 123 number
226 - step:
227 - type: ZBX_PREPROC_REGSUB
228 - params: "([0-9]+)\nx\\1y"
229 -out:
230 - return: SUCCEED
231 - value: x123y
232 ----
233 -test case: regsub("test 123 number", "([0-9]+)", )
234 -in:
235 - value:
236 - value_type: ITEM_VALUE_TYPE_STR
237 - time: 2017-10-29 03:15:00 +03:00
238 - data: test 123 number
239 - step:
240 - type: ZBX_PREPROC_REGSUB
241 - params: "([0-9]+)"
242 -out:
243 - return: FAIL
244 ----
245 -test case: regsub("test abc number", "([0-9]+)", \1)
246 -in:
247 - value:
248 - value_type: ITEM_VALUE_TYPE_STR
249 - time: 2017-10-29 03:15:00 +03:00
250 - data: test abc number
251 - step:
252 - type: ZBX_PREPROC_REGSUB
253 - params: "([0-9]+)\n\\1"
254 -out:
255 - return: FAIL
256 ----
257 -test case: regsub("", "([0-9]+)", \1)
258 -in:
259 - value:
260 - value_type: ITEM_VALUE_TYPE_STR
261 - time: 2017-10-29 03:15:00 +03:00
262 - data:
263 - step:
264 - type: ZBX_PREPROC_REGSUB
265 - params: "([0-9]+)\n\\1"
266 -out:
267 - return: FAIL
268 ----
269 -test case: regsub("", "^$", ok)
270 -in:
271 - value:
272 - value_type: ITEM_VALUE_TYPE_STR
273 - time: 2017-10-29 03:15:00 +03:00
274 - data: ""
275 - step:
276 - type: ZBX_PREPROC_REGSUB
277 - params: "^$\nok"
278 -out:
279 - return: SUCCEED
280 - value: ok
281 ----
282 -test case: bool2dec()
283 -in:
284 - value:
285 - value_type: ITEM_VALUE_TYPE_STR
286 - time: 2017-10-29 03:15:00 +03:00
287 - data: ""
288 - step:
289 - type: ZBX_PREPROC_BOOL2DEC
290 -out:
291 - return: FAIL
292 ----
293 -test case: bool2dec(abc)
294 -in:
295 - value:
296 - value_type: ITEM_VALUE_TYPE_STR
297 - time: 2017-10-29 03:15:00 +03:00
298 - data: "abc"
299 - step:
300 - type: ZBX_PREPROC_BOOL2DEC
301 -out:
302 - return: FAIL
303 ----
304 -test case: bool2dec(1)
305 -in:
306 - value:
307 - value_type: ITEM_VALUE_TYPE_STR
308 - time: 2017-10-29 03:15:00 +03:00
309 - data: "1"
310 - step:
311 - type: ZBX_PREPROC_BOOL2DEC
312 -out:
313 - return: SUCCEED
314 - value: 1
315 ----
316 -test case: bool2dec(0)
317 -in:
318 - value:
319 - value_type: ITEM_VALUE_TYPE_STR
320 - time: 2017-10-29 03:15:00 +03:00
321 - data: "0"
322 - step:
323 - type: ZBX_PREPROC_BOOL2DEC
324 -out:
325 - return: SUCCEED
326 - value: 0
327 ----
328 -test case: bool2dec(true)
329 -in:
330 - value:
331 - value_type: ITEM_VALUE_TYPE_STR
332 - time: 2017-10-29 03:15:00 +03:00
333 - data: "true"
334 - step:
335 - type: ZBX_PREPROC_BOOL2DEC
336 -out:
337 - return: SUCCEED
338 - value: 1
339 ----
340 -test case: bool2dec(t)
341 -in:
342 - value:
343 - value_type: ITEM_VALUE_TYPE_STR
344 - time: 2017-10-29 03:15:00 +03:00
345 - data: "t"
346 - step:
347 - type: ZBX_PREPROC_BOOL2DEC
348 -out:
349 - return: SUCCEED
350 - value: 1
351 ----
352 -test case: bool2dec(yes)
353 -in:
354 - value:
355 - value_type: ITEM_VALUE_TYPE_STR
356 - time: 2017-10-29 03:15:00 +03:00
357 - data: "yes"
358 - step:
359 - type: ZBX_PREPROC_BOOL2DEC
360 -out:
361 - return: SUCCEED
362 - value: 1
363 ----
364 -test case: bool2dec(y)
365 -in:
366 - value:
367 - value_type: ITEM_VALUE_TYPE_STR
368 - time: 2017-10-29 03:15:00 +03:00
369 - data: "y"
370 - step:
371 - type: ZBX_PREPROC_BOOL2DEC
372 -out:
373 - return: SUCCEED
374 - value: 1
375 ----
376 -test case: bool2dec(ok)
377 -in:
378 - value:
379 - value_type: ITEM_VALUE_TYPE_STR
380 - time: 2017-10-29 03:15:00 +03:00
381 - data: "ok"
382 - step:
383 - type: ZBX_PREPROC_BOOL2DEC
384 -out:
385 - return: SUCCEED
386 - value: 1
387 ----
388 -test case: bool2dec(on)
389 -in:
390 - value:
391 - value_type: ITEM_VALUE_TYPE_STR
392 - time: 2017-10-29 03:15:00 +03:00
393 - data: "on"
394 - step:
395 - type: ZBX_PREPROC_BOOL2DEC
396 -out:
397 - return: SUCCEED
398 - value: 1
399 ----
400 -test case: bool2dec(false)
401 -in:
402 - value:
403 - value_type: ITEM_VALUE_TYPE_STR
404 - time: 2017-10-29 03:15:00 +03:00
405 - data: "false"
406 - step:
407 - type: ZBX_PREPROC_BOOL2DEC
408 -out:
409 - return: SUCCEED
410 - value: 0
411 ----
412 -test case: bool2dec(f)
413 -in:
414 - value:
415 - value_type: ITEM_VALUE_TYPE_STR
416 - time: 2017-10-29 03:15:00 +03:00
417 - data: "f"
418 - step:
419 - type: ZBX_PREPROC_BOOL2DEC
420 -out:
421 - return: SUCCEED
422 - value: 0
423 ----
424 -test case: bool2dec(no)
425 -in:
426 - value:
427 - value_type: ITEM_VALUE_TYPE_STR
428 - time: 2017-10-29 03:15:00 +03:00
429 - data: "no"
430 - step:
431 - type: ZBX_PREPROC_BOOL2DEC
432 -out:
433 - return: SUCCEED
434 - value: 0
435 ----
436 -test case: bool2dec(n)
437 -in:
438 - value:
439 - value_type: ITEM_VALUE_TYPE_STR
440 - time: 2017-10-29 03:15:00 +03:00
441 - data: "n"
442 - step:
443 - type: ZBX_PREPROC_BOOL2DEC
444 -out:
445 - return: SUCCEED
446 - value: 0
447 ----
448 -test case: bool2dec(err)
449 -in:
450 - value:
451 - value_type: ITEM_VALUE_TYPE_STR
452 - time: 2017-10-29 03:15:00 +03:00
453 - data: "err"
454 - step:
455 - type: ZBX_PREPROC_BOOL2DEC
456 -out:
457 - return: SUCCEED
458 - value: 0
459 ----
460 -test case: bool2dec(off)
461 -in:
462 - value:
463 - value_type: ITEM_VALUE_TYPE_STR
464 - time: 2017-10-29 03:15:00 +03:00
465 - data: "off"
466 - step:
467 - type: ZBX_PREPROC_BOOL2DEC
468 -out:
469 - return: SUCCEED
470 - value: 0
471 ----
472 -test case: oct2dec(0)
473 -in:
474 - value:
475 - value_type: ITEM_VALUE_TYPE_STR
476 - time: 2017-10-29 03:15:00 +03:00
477 - data: 0
478 - step:
479 - type: ZBX_PREPROC_OCT2DEC
480 -out:
481 - return: SUCCEED
482 - value: 0
483 ----
484 -test case: oct2dec(7)
485 -in:
486 - value:
487 - value_type: ITEM_VALUE_TYPE_STR
488 - time: 2017-10-29 03:15:00 +03:00
489 - data: 7
490 - step:
491 - type: ZBX_PREPROC_OCT2DEC
492 -out:
493 - return: SUCCEED
494 - value: 7
495 ----
496 -test case: oct2dec(10)
497 -in:
498 - value:
499 - value_type: ITEM_VALUE_TYPE_STR
500 - time: 2017-10-29 03:15:00 +03:00
501 - data: 10
502 - step:
503 - type: ZBX_PREPROC_OCT2DEC
504 -out:
505 - return: SUCCEED
506 - value: 8
507 ----
508 -test case: oct2dec(8)
509 -in:
510 - value:
511 - value_type: ITEM_VALUE_TYPE_STR
512 - time: 2017-10-29 03:15:00 +03:00
513 - data: 8
514 - step:
515 - type: ZBX_PREPROC_OCT2DEC
516 -out:
517 - return: FAIL
518 ----
519 -test case: hex2dec(F)
520 -in:
521 - value:
522 - value_type: ITEM_VALUE_TYPE_STR
523 - time: 2017-10-29 03:15:00 +03:00
524 - data: F
525 - step:
526 - type: ZBX_PREPROC_HEX2DEC
527 -out:
528 - return: SUCCEED
529 - value: 15
530 ----
531 -test case: hex2dec(a)
532 -in:
533 - value:
534 - value_type: ITEM_VALUE_TYPE_STR
535 - time: 2017-10-29 03:15:00 +03:00
536 - data: a
537 - step:
538 - type: ZBX_PREPROC_HEX2DEC
539 -out:
540 - return: SUCCEED
541 - value: 10
542 ----
543 -test case: hex2dec(10)
544 -in:
545 - value:
546 - value_type: ITEM_VALUE_TYPE_STR
547 - time: 2017-10-29 03:15:00 +03:00
548 - data: 10
549 - step:
550 - type: ZBX_PREPROC_HEX2DEC
551 -out:
552 - return: SUCCEED
553 - value: 16
554 ----
555 -test case: hex2dec(g)
556 -in:
557 - value:
558 - value_type: ITEM_VALUE_TYPE_STR
559 - time: 2017-10-29 03:15:00 +03:00
560 - data: g
561 - step:
562 - type: ZBX_PREPROC_HEX2DEC
563 -out:
564 - return: FAIL
565 ----
566 -test case: hex2dec(ff ff)
567 -in:
568 - value:
569 - value_type: ITEM_VALUE_TYPE_STR
570 - time: 2017-10-29 03:15:00 +03:00
571 - data: ff ff
572 - step:
573 - type: ZBX_PREPROC_HEX2DEC
574 -out:
575 - return: SUCCEED
576 - value: 65535
577 ----
578 -test case: deltavalue(5, 10)
579 -in:
580 - value:
581 - value_type: ITEM_VALUE_TYPE_STR
582 - time: 2017-10-29 03:15:00 +03:00
583 - data: 10
584 - history:
585 - variant: ZBX_VARIANT_UI64
586 - time: 2017-10-29 03:14:00 +03:00
587 - data: 5
588 - step:
589 - type: ZBX_PREPROC_DELTA_VALUE
590 -out:
591 - return: SUCCEED
592 - value: 5
593 - history:
594 - data: 10
595 - time: 2017-10-29 03:15:00 +03:00
596 ----
597 -test case: deltavalue(-4.5, 5.5)
598 -in:
599 - value:
600 - value_type: ITEM_VALUE_TYPE_STR
601 - time: 2017-10-29 03:15:00 +03:00
602 - data: 5.5
603 - history:
604 - variant: ZBX_VARIANT_DBL
605 - time: 2017-10-29 03:14:00 +03:00
606 - data: -4.5
607 - step:
608 - type: ZBX_PREPROC_DELTA_VALUE
609 -out:
610 - return: SUCCEED
611 - value: 10
612 - history:
613 - time: 2017-10-29 03:15:00 +03:00
614 - data: 5.5
615 ----
616 -test case: deltavalue(10, 10)
617 -in:
618 - value:
619 - value_type: ITEM_VALUE_TYPE_STR
620 - time: 2017-10-29 03:15:00 +03:00
621 - data: 10
622 - history:
623 - variant: ZBX_VARIANT_UI64
624 - time: 2017-10-29 03:14:00 +03:00
625 - data: 10
626 - step:
627 - type: ZBX_PREPROC_DELTA_VALUE
628 -out:
629 - return: SUCCEED
630 - value: 0
631 - history:
632 - time: 2017-10-29 03:15:00 +03:00
633 - data: 10
634 ----
635 -test case: deltavalue(10, 9)
636 -in:
637 - value:
638 - value_type: ITEM_VALUE_TYPE_STR
639 - time: 2017-10-29 03:15:00 +03:00
640 - data: 0
641 - history:
642 - variant: ZBX_VARIANT_UI64
643 - time: 2017-10-29 03:14:00 +03:00
644 - data: 10
645 - step:
646 - type: ZBX_PREPROC_DELTA_VALUE
647 -out:
648 - return: SUCCEED
649 - history:
650 - time: 2017-10-29 03:15:00 +03:00
651 - data: 0
652 ----
653 -test case: deltavalue(0, 1.5)
654 -in:
655 - value:
656 - value_type: ITEM_VALUE_TYPE_STR
657 - time: 2017-10-29 03:15:00 +03:00
658 - data: 1.5
659 - history:
660 - variant: ZBX_VARIANT_DBL
661 - time: 2017-10-29 03:14:00 +03:00
662 - data: 0
663 - step:
664 - type: ZBX_PREPROC_DELTA_VALUE
665 -out:
666 - return: SUCCEED
667 - value: 1.5
668 - history:
669 - time: 2017-10-29 03:15:00 +03:00
670 - data: 1.5
671 ----
672 -test case: deltaspeed(2, 1, 10s)
673 -in:
674 - value:
675 - value_type: ITEM_VALUE_TYPE_STR
676 - time: 2017-10-29 03:15:00 +03:00
677 - data: 1
678 - history:
679 - variant: ZBX_VARIANT_UI64
680 - time: 2017-10-29 03:14:50 +03:00
681 - data: 2
682 - step:
683 - type: ZBX_PREPROC_DELTA_SPEED
684 -out:
685 - return: SUCCEED
686 - history:
687 - time: 2017-10-29 03:15:00 +03:00
688 - data: 1
689 ----
690 -test case: deltaspeed(1, 2, -10s)
691 -in:
692 - value:
693 - value_type: ITEM_VALUE_TYPE_STR
694 - time: 2017-10-29 03:15:00 +03:00
695 - data: 2
696 - history:
697 - variant: ZBX_VARIANT_UI64
698 - time: 2017-10-29 03:15:10 +03:00
699 - data: 1
700 - step:
701 - type: ZBX_PREPROC_DELTA_SPEED
702 -out:
703 - return: SUCCEED
704 - history:
705 - time: 2017-10-29 03:15:00 +03:00
706 - data: 2
707 ----
708 -test case: deltaspeed(1, 2, 10s)
709 -in:
710 - value:
711 - value_type: ITEM_VALUE_TYPE_FLOAT
712 - time: 2017-10-29 03:15:00 +03:00
713 - data: 2
714 - history:
715 - variant: ZBX_VARIANT_UI64
716 - time: 2017-10-29 03:14:50 +03:00
717 - data: 1
718 - step:
719 - type: ZBX_PREPROC_DELTA_SPEED
720 -out:
721 - return: SUCCEED
722 - value: 0.1
723 - history:
724 - time: 2017-10-29 03:15:00 +03:00
725 - data: 2
726 ----
727 -test case: deltaspeed(2, 3, 10s)
728 -in:
729 - value:
730 - value_type: ITEM_VALUE_TYPE_UINT64
731 - time: 2017-10-29 03:15:00 +03:00
732 - data: 3
733 - history:
734 - variant: ZBX_VARIANT_UI64
735 - time: 2017-10-29 03:14:50 +03:00
736 - data: 2
737 - step:
738 - type: ZBX_PREPROC_DELTA_SPEED
739 -out:
740 - return: SUCCEED
741 - value: 0
742 - history:
743 - time: 2017-10-29 03:15:00 +03:00
744 - data: 3
745 ----
746 -test case: deltaspeed(2, 3, 1s)
747 -in:
748 - value:
749 - value_type: ITEM_VALUE_TYPE_UINT64
750 - time: 2017-10-29 03:15:00 +03:00
751 - data: 3
752 - history:
753 - variant: ZBX_VARIANT_UI64
754 - time: 2017-10-29 03:14:59 +03:00
755 - data: 2
756 - step:
757 - type: ZBX_PREPROC_DELTA_SPEED
758 -out:
759 - return: SUCCEED
760 - value: 1
761 - history:
762 - time: 2017-10-29 03:15:00 +03:00
763 - data: 3
764 ----
765 -test case: xpath1
766 -in:
767 - value:
768 - value_type: ITEM_VALUE_TYPE_STR
769 - time: 2017-10-29 03:15:00 +03:00
770 - data: ""
771 - step:
772 - type: ZBX_PREPROC_XPATH
773 - params: ""
774 -out:
775 - return: FAIL
776 ----
777 -test case: xpath2
778 -in:
779 - value:
780 - value_type: ITEM_VALUE_TYPE_STR
781 - time: 2017-10-29 03:15:00 +03:00
782 - data: <a/>
783 - step:
784 - type: ZBX_PREPROC_XPATH
785 - params: ""
786 -out:
787 - return: FAIL
788 ----
789 -test case: xpath3
790 -in:
791 - value:
792 - value_type: ITEM_VALUE_TYPE_STR
793 - time: 2017-10-29 03:15:00 +03:00
794 - data: <a/>
795 - step:
796 - type: ZBX_PREPROC_XPATH
797 - params: |-
798 - /a["]
799 -out:
800 - return: FAIL
801 ----
802 -test case: xpath4
803 -in:
804 - value:
805 - value_type: ITEM_VALUE_TYPE_STR
806 - time: 2017-10-29 03:15:00 +03:00
807 - data: <a/>
808 - step:
809 - type: ZBX_PREPROC_XPATH
810 - params: 1 div 0
811 -out:
812 - return: FAIL
813 ----
814 -test case: xpath5
815 -in:
816 - value:
817 - value_type: ITEM_VALUE_TYPE_STR
818 - time: 2017-10-29 03:15:00 +03:00
819 - data: <a/>
820 - step:
821 - type: ZBX_PREPROC_XPATH
822 - params: -a
823 -out:
824 - return: FAIL
825 ----
826 -test case: xpath6
827 -in:
828 - value:
829 - value_type: ITEM_VALUE_TYPE_STR
830 - time: 2017-10-29 03:15:00 +03:00
831 - data: <a/>
832 - step:
833 - type: ZBX_PREPROC_XPATH
834 - params: /b
835 -out:
836 - return: SUCCEED
837 - value: ""
838 ----
839 -test case: xpath7
840 -in:
841 - value:
842 - value_type: ITEM_VALUE_TYPE_STR
843 - time: 2017-10-29 03:15:00 +03:00
844 - data: <a/>
845 - step:
846 - type: ZBX_PREPROC_XPATH
847 - params: 3 div 2
848 -out:
849 - return: SUCCEED
850 - value: 1.5
851 ----
852 -test case: xpath8
853 -in:
854 - value:
855 - value_type: ITEM_VALUE_TYPE_STR
856 - time: 2017-10-29 03:15:00 +03:00
857 - data: <a/>
858 - step:
859 - type: ZBX_PREPROC_XPATH
860 - params: /a
861 -out:
862 - return: SUCCEED
863 - value: <a/>
864 ----
865 -test case: xpath9
866 -in:
867 - value:
868 - value_type: ITEM_VALUE_TYPE_STR
869 - time: 2017-10-29 03:15:00 +03:00
870 - data: <a>1</a>
871 - step:
872 - type: ZBX_PREPROC_XPATH
873 - params: /a/text()
874 -out:
875 - return: SUCCEED
876 - value: 1
877 ----
878 -test case: xpath10
879 -in:
880 - value:
881 - value_type: ITEM_VALUE_TYPE_STR
882 - time: 2017-10-29 03:15:00 +03:00
883 - data: <a>1</a>
884 - step:
885 - type: ZBX_PREPROC_XPATH
886 - params: string(/a)
887 -out:
888 - return: SUCCEED
889 - value: 1
890 ----
891 -test case: xpath11
892 -in:
893 - value:
894 - value_type: ITEM_VALUE_TYPE_STR
895 - time: 2017-10-29 03:15:00 +03:00
896 - data: <a b="10">1</a>
897 - step:
898 - type: ZBX_PREPROC_XPATH
899 - params: string(/a/@b)
900 -out:
901 - return: SUCCEED
902 - value: 10
903 ----
904 -test case: xpath12
905 -in:
906 - value:
907 - value_type: ITEM_VALUE_TYPE_STR
908 - time: 2017-10-29 03:15:00 +03:00
909 - data: <a><b x="1"/><c x="2"/><d x="1"/></a>
910 - step:
911 - type: ZBX_PREPROC_XPATH
912 - params: //*[@x="1"]
913 -out:
914 - return: SUCCEED
915 - value: <b x="1"/><d x="1"/>
916 ----
917 -test case: jsonpath1
918 -in:
919 - value:
920 - value_type: ITEM_VALUE_TYPE_STR
921 - time: 2017-10-29 03:15:00 +03:00
922 - data: abc
923 - step:
924 - type: ZBX_PREPROC_JSONPATH
925 - params:
926 -out:
927 - return: FAIL
928 ----
929 -test case: jsonpath2
930 -in:
931 - value:
932 - value_type: ITEM_VALUE_TYPE_STR
933 - time: 2017-10-29 03:15:00 +03:00
934 - data: |-
935 - {"a":{"b":[1, 2, 3]}}
936 - step:
937 - type: ZBX_PREPROC_JSONPATH
938 - params: $abc
939 -out:
940 - return: FAIL
941 ----
942 -test case: jsonpath3
943 -in:
944 - value:
945 - value_type: ITEM_VALUE_TYPE_STR
946 - time: 2017-10-29 03:15:00 +03:00
947 - data: |-
948 - {"a":{"b":[1, 2, 3]}}
949 - step:
950 - type: ZBX_PREPROC_JSONPATH
951 - params: $.abc
952 -out:
953 - return: FAIL
954 ----
955 -test case: jsonpath4
956 -in:
957 - value:
958 - value_type: ITEM_VALUE_TYPE_STR
959 - time: 2017-10-29 03:15:00 +03:00
960 - data: |-
961 - {"a":{"b":[1, 2, 3]}}
962 - step:
963 - type: ZBX_PREPROC_JSONPATH
964 - params: $.a
965 -out:
966 - return: SUCCEED
967 - value: |-
968 - {"b":[1,2,3]}
969 ----
970 -test case: jsonpath5
971 -in:
972 - value:
973 - value_type: ITEM_VALUE_TYPE_STR
974 - time: 2017-10-29 03:15:00 +03:00
975 - data: |-
976 - {"a":{"b":[1, 2, 3]}}
977 - step:
978 - type: ZBX_PREPROC_JSONPATH
979 - params: $.a['b']
980 -out:
981 - return: SUCCEED
982 - value: |-
983 - [1,2,3]
984 ----
985 -test case: jsonpath6
986 -in:
987 - value:
988 - value_type: ITEM_VALUE_TYPE_STR
989 - time: 2017-10-29 03:15:00 +03:00
990 - data: |-
991 - {"a":{"b":[1, 2, 3]}}
992 - step:
993 - type: ZBX_PREPROC_JSONPATH
994 - params: $.a['b'][1]
995 -out:
996 - return: SUCCEED
997 - value: 2
998 ----
999 -test case: jsonpath7
1000 -in:
1001 - value:
1002 - value_type: ITEM_VALUE_TYPE_STR
1003 - time: 2017-10-29 03:15:00 +03:00
1004 - data: |-
1005 - {"a":{"b c":["one", "two", "three"]}}
1006 - step:
1007 - type: ZBX_PREPROC_JSONPATH
1008 - params: $.a['b c']
1009 -out:
1010 - return: SUCCEED
1011 - value: '["one","two","three"]'
1012 ----
1013 -test case: jsonpath8
1014 -in:
1015 - value:
1016 - value_type: ITEM_VALUE_TYPE_STR
1017 - time: 2017-10-29 03:15:00 +03:00
1018 - data: |-
1019 - {"a":{"b c":["one", "two \"2\"", 3]}}
1020 - step:
1021 - type: ZBX_PREPROC_JSONPATH
1022 - params: $.a['b c'][1]
1023 -out:
1024 - return: SUCCEED
1025 - value: two "2"
1026 ----
1027 -test case: jsonpath9
1028 -in:
1029 - value:
1030 - value_type: ITEM_VALUE_TYPE_STR
1031 - time: 2017-10-29 03:15:00 +03:00
1032 - data: |-
1033 - {"a":{"b c":["one", "two \"2\"", 3]}}
1034 - step:
1035 - type: ZBX_PREPROC_JSONPATH
1036 - params: $.a['b c'][2]
1037 -out:
1038 - return: SUCCEED
1039 - value: 3
1040 ----
1041 -test case: jsonpath10
1042 -in:
1043 - value:
1044 - value_type: ITEM_VALUE_TYPE_STR
1045 - time: 2017-10-29 03:15:00 +03:00
1046 - data: |-
1047 - {"a":{"b":[1, 2, 3]}}
1048 - step:
1049 - type: ZBX_PREPROC_JSONPATH
1050 - params: $.a['b'][3]
1051 -out:
1052 - return: FAIL
1053 ----
1054 -test case: jsonpath11
1055 -in:
1056 - value:
1057 - value_type: ITEM_VALUE_TYPE_STR
1058 - time: 2017-10-29 03:15:00 +03:00
1059 - data: |-
1060 - {"a":{"b":[1, 2, 3]}}
1061 - step:
1062 - type: ZBX_PREPROC_JSONPATH
1063 - params: $.a['b'][
1064 -out:
1065 - return: FAIL
1066 ----
1067 -test case: jsonpath12
1068 -in:
1069 - value:
1070 - value_type: ITEM_VALUE_TYPE_STR
1071 - time: 2017-10-29 03:15:00 +03:00
1072 - data: |-
1073 - {"a":{"b":[1, 2, 3]}}
1074 - step:
1075 - type: ZBX_PREPROC_JSONPATH
1076 - params: $.a['b][3]
1077 -out:
1078 - return: FAIL
1079 ----
1080 -test case: jsonpath13
1081 -in:
1082 - value:
1083 - value_type: ITEM_VALUE_TYPE_STR
1084 - time: 2017-10-29 03:15:00 +03:00
1085 - data: |-
1086 - {"a":{"\ud800": 1}}
1087 - step:
1088 - type: ZBX_PREPROC_JSONPATH
1089 - params: $.a
1090 -out:
1091 - return: FAIL
1092 ----
1093 -test case: validate_range(1, 5, 10)
1094 -in:
1095 - value:
1096 - value_type: ITEM_VALUE_TYPE_STR
1097 - time: 2017-10-29 03:15:00 +03:00
1098 - data: 1
1099 - step:
1100 - type: ZBX_PREPROC_VALIDATE_RANGE
1101 - params: |-
1102 - 5
1103 - 10
1104 -out:
1105 - return: FAIL
1106 ----
1107 -test case: validate_range(5, 5, 10)
1108 -in:
1109 - value:
1110 - value_type: ITEM_VALUE_TYPE_STR
1111 - time: 2017-10-29 03:15:00 +03:00
1112 - data: 5
1113 - step:
1114 - type: ZBX_PREPROC_VALIDATE_RANGE
1115 - params: |-
1116 - 5
1117 - 10
1118 -out:
1119 - return: SUCCEED
1120 - value: 5
1121 ----
1122 -test case: validate_range(10, 5, 10)
1123 -in:
1124 - value:
1125 - value_type: ITEM_VALUE_TYPE_STR
1126 - time: 2017-10-29 03:15:00 +03:00
1127 - data: 10
1128 - step:
1129 - type: ZBX_PREPROC_VALIDATE_RANGE
1130 - params: |-
1131 - 5
1132 - 10
1133 -out:
1134 - return: SUCCEED
1135 - value: 10
1136 ----
1137 -test case: validate_range(10.1, 5, 10)
1138 -in:
1139 - value:
1140 - value_type: ITEM_VALUE_TYPE_STR
1141 - time: 2017-10-29 03:15:00 +03:00
1142 - data: 10.1
1143 - step:
1144 - type: ZBX_PREPROC_VALIDATE_RANGE
1145 - params: |-
1146 - 5
1147 - 10
1148 -out:
1149 - return: FAIL
1150 ----
1151 -test case: validate_regex(abc 123 xyz, ([0-9+))
1152 -in:
1153 - value:
1154 - value_type: ITEM_VALUE_TYPE_STR
1155 - time: 2017-10-29 03:15:00 +03:00
1156 - data: abc 123 xyz
1157 - step:
1158 - type: ZBX_PREPROC_VALIDATE_REGEX
1159 - params: ([0-9+)
1160 -out:
1161 - return: FAIL
1162 ----
1163 -test case: validate_regex(abc opq xyz, ([0-9]+))
1164 -in:
1165 - value:
1166 - value_type: ITEM_VALUE_TYPE_STR
1167 - time: 2017-10-29 03:15:00 +03:00
1168 - data: abc opq xyz
1169 - step:
1170 - type: ZBX_PREPROC_VALIDATE_REGEX
1171 - params: ([0-9]+)
1172 -out:
1173 - return: FAIL
1174 ----
1175 -test case: validate_regex(abc 123 xyz, ([0-9]+))
1176 -in:
1177 - value:
1178 - value_type: ITEM_VALUE_TYPE_STR
1179 - time: 2017-10-29 03:15:00 +03:00
1180 - data: abc 123 xyz
1181 - step:
1182 - type: ZBX_PREPROC_VALIDATE_REGEX
1183 - params: ([0-9]+)
1184 -out:
1185 - return: SUCCEED
1186 - value: abc 123 xyz
1187 ----
1188 -test case: validate_regex(abc 123 xyz, ([0-9+))
1189 -in:
1190 - value:
1191 - value_type: ITEM_VALUE_TYPE_STR
1192 - time: 2017-10-29 03:15:00 +03:00
1193 - data: abc 123 xyz
1194 - step:
1195 - type: ZBX_PREPROC_VALIDATE_NOT_REGEX
1196 - params: ([0-9+)
1197 -out:
1198 - return: FAIL
1199 ----
1200 -test case: validate_regex(abc opq xyz, ([0-9]+))
1201 -in:
1202 - value:
1203 - value_type: ITEM_VALUE_TYPE_STR
1204 - time: 2017-10-29 03:15:00 +03:00
1205 - data: abc opq xyz
1206 - step:
1207 - type: ZBX_PREPROC_VALIDATE_NOT_REGEX
1208 - params: ([0-9]+)
1209 -out:
1210 - return: SUCCEED
1211 - value: abc opq xyz
1212 ----
1213 -test case: validate_regex(abc 123 xyz, ([0-9]+))
1214 -in:
1215 - value:
1216 - value_type: ITEM_VALUE_TYPE_STR
1217 - time: 2017-10-29 03:15:00 +03:00
1218 - data: abc 123 xyz
1219 - step:
1220 - type: ZBX_PREPROC_VALIDATE_NOT_REGEX
1221 - params: ([0-9]+)
1222 -out:
1223 - return: FAIL
1224 ----
1225 -test case: string(10) * 10 (discard)
1226 -in:
1227 - value:
1228 - value_type: ITEM_VALUE_TYPE_STR
1229 - time: 2017-10-29 03:15:00 +03:00
1230 - data: x
1231 - step:
1232 - type: ZBX_PREPROC_MULTIPLIER
1233 - params: 10
1234 - error_handler: ZBX_PREPROC_FAIL_DISCARD_VALUE
1235 -out:
1236 - return: SUCCEED
1237 ----
1238 -test case: string(10) * 10 (set value)
1239 -in:
1240 - value:
1241 - value_type: ITEM_VALUE_TYPE_STR
1242 - time: 2017-10-29 03:15:00 +03:00
1243 - data: x
1244 - step:
1245 - type: ZBX_PREPROC_MULTIPLIER
1246 - params: 10
1247 - error_handler: ZBX_PREPROC_FAIL_SET_VALUE
1248 - error_handler_params: invalid
1249 -out:
1250 - return: SUCCEED
1251 - value: invalid
1252 ----
1253 -test case: string(10) * 10 (set error)
1254 -in:
1255 - value:
1256 - value_type: ITEM_VALUE_TYPE_STR
1257 - time: 2017-10-29 03:15:00 +03:00
1258 - data: x
1259 - step:
1260 - type: ZBX_PREPROC_MULTIPLIER
1261 - params: 10
1262 - error_handler: ZBX_PREPROC_FAIL_SET_ERROR
1263 - error_handler_params: custom error
1264 -out:
1265 - return: FAIL
1266 - error: custom error
1267 ----
1268 -test case: jsonerror(x, $.error)
1269 -in:
1270 - value:
1271 - value_type: ITEM_VALUE_TYPE_STR
1272 - time: 2017-10-29 03:15:00 +03:00
1273 - data: x
1274 - step:
1275 - type: ZBX_PREPROC_ERROR_FIELD_JSON
1276 - params: $.error
1277 -out:
1278 - return: SUCCEED
1279 - value: x
1280 ----
1281 -test case: jsonerror({"error":"error message"}, $error)
1282 -in:
1283 - value:
1284 - value_type: ITEM_VALUE_TYPE_STR
1285 - time: 2017-10-29 03:15:00 +03:00
1286 - data: |-
1287 - {"error":"error message"}
1288 - step:
1289 - type: ZBX_PREPROC_ERROR_FIELD_JSON
1290 - params: $error
1291 -out:
1292 - return: FAIL
1293 ----
1294 -test case: jsonerror({"data":"123"}, $.error)
1295 -in:
1296 - value:
1297 - value_type: ITEM_VALUE_TYPE_STR
1298 - time: 2017-10-29 03:15:00 +03:00
1299 - data: |-
1300 - {"data":"123"}
1301 - step:
1302 - type: ZBX_PREPROC_ERROR_FIELD_JSON
1303 - params: $.error
1304 -out:
1305 - return: SUCCEED
1306 - value: |-
1307 - {"data":"123"}
1308 ----
1309 -test case: jsonerror({"error":"error message"}, $.error)
1310 -in:
1311 - value:
1312 - value_type: ITEM_VALUE_TYPE_STR
1313 - time: 2017-10-29 03:15:00 +03:00
1314 - data: |-
1315 - {"error":"error message"}
1316 - step:
1317 - type: ZBX_PREPROC_ERROR_FIELD_JSON
1318 - params: $.error
1319 -out:
1320 - return: FAIL
1321 - error: error message
1322 ----
1323 -test case: xmlerror(x, //error)
1324 -in:
1325 - value:
1326 - value_type: ITEM_VALUE_TYPE_STR
1327 - time: 2017-10-29 03:15:00 +03:00
1328 - data: x
1329 - step:
1330 - type: ZBX_PREPROC_ERROR_FIELD_XML
1331 - params: //error
1332 -out:
1333 - return: SUCCEED
1334 - value: x
1335 ----
1336 -test case: xmlerror(<a><error>custom error</error></a>, "//text(")
1337 -in:
1338 - value:
1339 - value_type: ITEM_VALUE_TYPE_STR
1340 - time: 2017-10-29 03:15:00 +03:00
1341 - data: <a><error>custom error</error></a>
1342 - step:
1343 - type: ZBX_PREPROC_ERROR_FIELD_XML
1344 - params: //text(
1345 -out:
1346 - return: FAIL
1347 ----
1348 -test case: xmlerror(<a><error>custom error</error></a>, "//error/text()")
1349 -in:
1350 - value:
1351 - value_type: ITEM_VALUE_TYPE_STR
1352 - time: 2017-10-29 03:15:00 +03:00
1353 - data: <a><error>custom error</error></a>
1354 - step:
1355 - type: ZBX_PREPROC_ERROR_FIELD_XML
1356 - params: //error/text()
1357 -out:
1358 - return: FAIL
1359 - error: custom error
1360 ----
1361 -test case: xmlerror(<a><value>1</value></a>, "//error/text()")
1362 -in:
1363 - value:
1364 - value_type: ITEM_VALUE_TYPE_STR
1365 - time: 2017-10-29 03:15:00 +03:00
1366 - data: <a><value>1</value></a>
1367 - step:
1368 - type: ZBX_PREPROC_ERROR_FIELD_XML
1369 - params: //error/text()
1370 -out:
1371 - return: SUCCEED
1372 - value: <a><value>1</value></a>
1373 ----
1374 -test case: regexerror("error:123, "error:([0-9+)")
1375 -in:
1376 - value:
1377 - value_type: ITEM_VALUE_TYPE_STR
1378 - time: 2017-10-29 03:15:00 +03:00
1379 - data: error:123
1380 - step:
1381 - type: ZBX_PREPROC_ERROR_FIELD_REGEX
1382 - params: ([0-9+)
1383 -out:
1384 - return: FAIL
1385 ----
1386 -test case: regexerror("error:123, "error:([0-9]+)")
1387 -in:
1388 - value:
1389 - value_type: ITEM_VALUE_TYPE_STR
1390 - time: 2017-10-29 03:15:00 +03:00
1391 - data: error:123
1392 - step:
1393 - type: ZBX_PREPROC_ERROR_FIELD_REGEX
1394 - params: |-
1395 - error:([0-9]+)
1396 - \1
1397 -out:
1398 - return: FAIL
1399 - error: 123
1400 ----
1401 -test case: regexerror("value:123, "error:([0-9]+)")
1402 -in:
1403 - value:
1404 - value_type: ITEM_VALUE_TYPE_STR
1405 - time: 2017-10-29 03:15:00 +03:00
1406 - data: value:123
1407 - step:
1408 - type: ZBX_PREPROC_ERROR_FIELD_REGEX
1409 - params: |-
1410 - error:([0-9]+)
1411 - \1
1412 -out:
1413 - return: SUCCEED
1414 - value: value:123
1415 ----
1416 -test case: throttle(1, 123)
1417 -in:
1418 - value:
1419 - value_type: ITEM_VALUE_TYPE_STR
1420 - time: 2017-10-29 03:15:00 +03:00
1421 - data: 123
1422 - history:
1423 - variant: ZBX_VARIANT_STR
1424 - time: 2017-10-29 03:14:00 +03:00
1425 - data: 1
1426 - step:
1427 - type: ZBX_PREPROC_THROTTLE_VALUE
1428 -out:
1429 - return: SUCCEED
1430 - value: 123
1431 - history:
1432 - time: 2017-10-29 03:15:00 +03:00
1433 - data: 123
1434 ----
1435 -test case: throttle(, 123)
1436 -in:
1437 - value:
1438 - value_type: ITEM_VALUE_TYPE_STR
1439 - time: 2017-10-29 03:15:00 +03:00
1440 - data: 123
1441 - step:
1442 - type: ZBX_PREPROC_THROTTLE_VALUE
1443 -out:
1444 - return: SUCCEED
1445 - value: 123
1446 - history:
1447 - time: 2017-10-29 03:15:00 +03:00
1448 - data: 123
1449 ----
1450 -test case: throttle(123, 123)
1451 -in:
1452 - value:
1453 - value_type: ITEM_VALUE_TYPE_STR
1454 - time: 2017-10-29 03:15:00 +03:00
1455 - data: 123
1456 - history:
1457 - variant: ZBX_VARIANT_STR
1458 - time: 2017-10-29 03:14:00 +03:00
1459 - data: 123
1460 - step:
1461 - type: ZBX_PREPROC_THROTTLE_VALUE
1462 -out:
1463 - return: SUCCEED
1464 - history:
1465 - time: 2017-10-29 03:14:00 +03:00
1466 - data: 123
1467 ----
1468 -test case: throttle_timed(, abc)
1469 -in:
1470 - value:
1471 - value_type: ITEM_VALUE_TYPE_STR
1472 - time: 2017-10-29 03:15:00 +03:00
1473 - data: abc
1474 - step:
1475 - type: ZBX_PREPROC_THROTTLE_TIMED_VALUE
1476 - params: 1m
1477 -out:
1478 - return: SUCCEED
1479 - value: abc
1480 - history:
1481 - time: 2017-10-29 03:15:00 +03:00
1482 - data: abc
1483 ----
1484 -test case: throttle_timed(xyz, abc)
1485 -in:
1486 - value:
1487 - value_type: ITEM_VALUE_TYPE_STR
1488 - time: 2017-10-29 03:15:00 +03:00
1489 - data: abc
1490 - history:
1491 - variant: ZBX_VARIANT_STR
1492 - time: 2017-10-29 03:14:30 +03:00
1493 - data: xyz
1494 - step:
1495 - type: ZBX_PREPROC_THROTTLE_TIMED_VALUE
1496 - params: 1m
1497 -out:
1498 - return: SUCCEED
1499 - value: abc
1500 - history:
1501 - time: 2017-10-29 03:15:00 +03:00
1502 - data: abc
1503 ----
1504 -test case: throttle_timed(abc, abc, 30s)
1505 -in:
1506 - value:
1507 - value_type: ITEM_VALUE_TYPE_STR
1508 - time: 2017-10-29 03:15:00 +03:00
1509 - data: abc
1510 - history:
1511 - variant: ZBX_VARIANT_STR
1512 - time: 2017-10-29 03:14:30 +03:00
1513 - data: abc
1514 - step:
1515 - type: ZBX_PREPROC_THROTTLE_TIMED_VALUE
1516 - params: 1m
1517 -out:
1518 - return: SUCCEED
1519 - history:
1520 - time: 2017-10-29 03:14:30 +03:00
1521 - data: abc
1522 ----
1523 -test case: throttle_timed(abc, abc, 1m)
1524 -in:
1525 - value:
1526 - value_type: ITEM_VALUE_TYPE_STR
1527 - time: 2017-10-29 03:15:00 +03:00
1528 - data: abc
1529 - history:
1530 - variant: ZBX_VARIANT_STR
1531 - time: 2017-10-29 03:14:00 +03:00
1532 - data: abc
1533 - step:
1534 - type: ZBX_PREPROC_THROTTLE_TIMED_VALUE
1535 - params: 1m
1536 -out:
1537 - return: SUCCEED
1538 - value: abc
1539 - history:
1540 - time: 2017-10-29 03:15:00 +03:00
1541 - data: abc
1542 ----
1543 -test case: float(1.5e0) * 1
1544 -in:
1545 - value:
1546 - value_type: ITEM_VALUE_TYPE_FLOAT
1547 - time: 2017-10-29 03:15:00 +03:00
1548 - data: 1.5e0
1549 - step:
1550 - type: ZBX_PREPROC_MULTIPLIER
1551 - params: 1
1552 -out:
1553 - return: SUCCEED
1554 - value: 1.5
1555 ----
1556 -test case: float(1) * 1.5e0
1557 -in:
1558 - value:
1559 - value_type: ITEM_VALUE_TYPE_FLOAT
1560 - time: 2017-10-29 03:15:00 +03:00
1561 - data: 1
1562 - step:
1563 - type: ZBX_PREPROC_MULTIPLIER
1564 - params: 1.5e0
1565 -out:
1566 - return: SUCCEED
1567 - value: 1.5
1568 ----
1569 -test case: float(1.5e0) * 1.5e0
1570 -in:
1571 - value:
1572 - value_type: ITEM_VALUE_TYPE_FLOAT
1573 - time: 2017-10-29 03:15:00 +03:00
1574 - data: 1.5e0
1575 - step:
1576 - type: ZBX_PREPROC_MULTIPLIER
1577 - params: 1.5e0
1578 -out:
1579 - return: SUCCEED
1580 - value: 2.25
1581 ----
1582 -test case: float(1.5e1) * 1
1583 -in:
1584 - value:
1585 - value_type: ITEM_VALUE_TYPE_FLOAT
1586 - time: 2017-10-29 03:15:00 +03:00
1587 - data: 1.5e1
1588 - step:
1589 - type: ZBX_PREPROC_MULTIPLIER
1590 - params: 1
1591 -out:
1592 - return: SUCCEED
1593 - value: 15
1594 ----
1595 -test case: float(1) * 1.5e1
1596 -in:
1597 - value:
1598 - value_type: ITEM_VALUE_TYPE_FLOAT
1599 - time: 2017-10-29 03:15:00 +03:00
1600 - data: 1
1601 - step:
1602 - type: ZBX_PREPROC_MULTIPLIER
1603 - params: 1.5e1
1604 -out:
1605 - return: SUCCEED
1606 - value: 15
1607 ----
1608 -test case: float(1.5e1) * 1.5e1
1609 -in:
1610 - value:
1611 - value_type: ITEM_VALUE_TYPE_FLOAT
1612 - time: 2017-10-29 03:15:00 +03:00
1613 - data: 1.5e1
1614 - step:
1615 - type: ZBX_PREPROC_MULTIPLIER
1616 - params: 1.5e1
1617 -out:
1618 - return: SUCCEED
1619 - value: 225
1620 ----
1621 -test case: float(1.5e10) * 1
1622 -in:
1623 - value:
1624 - value_type: ITEM_VALUE_TYPE_FLOAT
1625 - time: 2017-10-29 03:15:00 +03:00
1626 - data: 1.5e10
1627 - step:
1628 - type: ZBX_PREPROC_MULTIPLIER
1629 - params: 1
1630 -out:
1631 - return: SUCCEED
1632 - value: 15000000000
1633 ----
1634 -test case: float(1) * 1.5e10
1635 -in:
1636 - value:
1637 - value_type: ITEM_VALUE_TYPE_FLOAT
1638 - time: 2017-10-29 03:15:00 +03:00
1639 - data: 1
1640 - step:
1641 - type: ZBX_PREPROC_MULTIPLIER
1642 - params: 1.5e10
1643 -out:
1644 - return: SUCCEED
1645 - value: 15000000000
1646 ----
1647 -test case: float(1.5e10) * 1.5e10
1648 -in:
1649 - value:
1650 - value_type: ITEM_VALUE_TYPE_FLOAT
1651 - time: 2017-10-29 03:15:00 +03:00
1652 - data: 1.5e10
1653 - step:
1654 - type: ZBX_PREPROC_MULTIPLIER
1655 - params: 1.5e10
1656 -out:
1657 - return: SUCCEED
1658 - value: 225000000000000000000
1659 ----
1660 -test case: prometheus_getmetric1
1661 -in:
1662 - value:
1663 - value_type: ITEM_VALUE_TYPE_STR
1664 - time: 2017-10-29 03:15:00 +03:00
1665 - data: |
1666 - # HELP cpu_usage_system Telegraf collected metric
1667 - # TYPE cpu_usage_system gauge
1668 - cpu_usage_system{cpu="cpu-total",host="host1"} 1.1940298507220641
1669 - cpu_usage_system{cpu="cpu0",host="host1"} 1.1940298507220641
1670 - cpu_usage_system{cpu="cpu1",host="host1"} 1.1340298507220641
1671 - step:
1672 - type: ZBX_PREPROC_PROMETHEUS_PATTERN
1673 - params: "cpu_usage_system{cpu=\"cpu-total\",host=~\".*\"}\nvalue\n"
1674 -out:
1675 - return: SUCCEED
1676 - value: 1.1940298507220641
1677 ----
1678 -test case: prometheus_getmetric2
1679 -in:
1680 - value:
1681 - value_type: ITEM_VALUE_TYPE_STR
1682 - time: 2017-10-29 03:15:00 +03:00
1683 - data: metric_without_timestamp_and_labels 12.47
1684 - step:
1685 - type: ZBX_PREPROC_PROMETHEUS_PATTERN
1686 - params: "metric_without_timestamp_and_labels\nvalue\n"
1687 -out:
1688 - return: SUCCEED
1689 - value: 12.47
1690 ----
1691 -test case: prometheus_getmetric3
1692 -in:
1693 - value:
1694 - value_type: ITEM_VALUE_TYPE_STR
1695 - time: 2017-10-29 03:15:00 +03:00
1696 - data: |
1697 - # HELP cpu_usage_system Telegraf collected metric
1698 - # TYPE cpu_usage_system gauge
1699 - cpu_usage_system{cpu="cpu-total",host="host1"} 1.1940298507220641
1700 - cpu_usage_system{cpu="cpu0",host="host1"} 1.1940298507220641
1701 - cpu_usage_system{cpu="cpu1",host="host1"} 1.1340298507220641
1702 - step:
1703 - type: ZBX_PREPROC_PROMETHEUS_PATTERN
1704 - params: "{cpu=\"cpu0\",__name__=\"cpu_usage_system\"}\nvalue\n"
1705 -out:
1706 - return: SUCCEED
1707 - value: 1.1940298507220641
1708 ----
1709 -test case: prometheus_getmetric4
1710 -in:
1711 - value:
1712 - value_type: ITEM_VALUE_TYPE_STR
1713 - time: 2017-10-29 03:15:00 +03:00
1714 - data: |
1715 - # HELP http_requests_total The total number of HTTP requests.
1716 - # TYPE http_requests_total counter
1717 - http_requests_total{method="post",code="200"} 1027 1395066363000
1718 - http_requests_total{method="post",code="400"} 3 1395066363000
1719 - step:
1720 - type: ZBX_PREPROC_PROMETHEUS_PATTERN
1721 - params: "http_requests_total{code=\"200\"}\nvalue\n"
1722 -out:
1723 - return: SUCCEED
1724 - value: 1027
1725 ----
1726 -test case: prometheus_getmetric5
1727 -in:
1728 - value:
1729 - value_type: ITEM_VALUE_TYPE_STR
1730 - time: 2017-10-29 03:15:00 +03:00
1731 - data: |
1732 - # HELP cpu_usage_system Telegraf collected metric
1733 - # TYPE cpu_usage_system gauge
1734 - cpu_usage_system{cpu="cpu1",host="host1"} 1.1340298507220641
1735 - step:
1736 - type: ZBX_PREPROC_PROMETHEUS_PATTERN
1737 - params: "\nvalue\n"
1738 -out:
1739 - return: SUCCEED
1740 - value: 1.1340298507220641
1741 ----
1742 -test case: prometheus_getmetric6
1743 -in:
1744 - value:
1745 - value_type: ITEM_VALUE_TYPE_STR
1746 - time: 2017-10-29 03:15:00 +03:00
1747 - data: |
1748 - # HELP cpu_usage_system Telegraf collected metric
1749 - # TYPE cpu_usage_system gauge
1750 - cpu_usage_system{cpu="cpu1",host="host1"} 1.1340298507220641
1751 - step:
1752 - type: ZBX_PREPROC_PROMETHEUS_PATTERN
1753 - params: "cpu_usage_system\nvalue\n"
1754 -out:
1755 - return: SUCCEED
1756 - value: 1.1340298507220641
1757 ----
1758 -test case: prometheus_getmetric7
1759 -in:
1760 - value:
1761 - value_type: ITEM_VALUE_TYPE_STR
1762 - time: 2017-10-29 03:15:00 +03:00
1763 - data: |
1764 - # HELP cpu_usage_system Telegraf collected metric
1765 - # TYPE cpu_usage_system gauge
1766 - cpu_usage_system{cpu="cpu-total",host="host1"} 1.1940298507220641
1767 - cpu_usage_system{cpu="cpu0",host="host2"} 1.1940298507220641
1768 - cpu_usage_system{cpu="cpu1",host="host3"} 1.1340298507220641
1769 - step:
1770 - type: ZBX_PREPROC_PROMETHEUS_PATTERN
1771 - params: "cpu_usage_system{cpu=\"cpu0\"} == 1.1940298507220641\nlabel\nhost"
1772 -out:
1773 - return: SUCCEED
1774 - value: host2
1775 ----
1776 -test case: prometheus_getmetric8
1777 -in:
1778 - value:
1779 - value_type: ITEM_VALUE_TYPE_STR
1780 - time: 2017-10-29 03:15:00 +03:00
1781 - data: >
1782 - AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz:_0123456789{
1783 - AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz_0123456789="anything"} 123123123
1784 - step:
1785 - type: ZBX_PREPROC_PROMETHEUS_PATTERN
1786 - params: |-
1787 - AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz:_0123456789 == 123123123
1788 - label
1789 - AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz_0123456789
1790 -out:
1791 - return: SUCCEED
1792 - value: anything
1793 ----
1794 -test case: prometheus_getmetric9
1795 -in:
1796 - value:
1797 - value_type: ITEM_VALUE_TYPE_STR
1798 - time: 2017-10-29 03:15:00 +03:00
1799 - data: |
1800 - # HELP cpu_usage_system Telegraf collected metric
1801 - # TYPE cpu_usage_system gauge
1802 - cpu_usage_system{cpu="cpu-total",host="host1"} 1.1940298507220641
1803 - cpu_usage_system{cpu="cpu0",host="host2"} 1.1940298507220641
1804 - cpu_usage_system{cpu="cpu1",host="host3"} 1.1340298507220641
1805 - step:
1806 - type: ZBX_PREPROC_PROMETHEUS_PATTERN
1807 - params: |-
1808 - cpu_usage_system{cpu="cpu0"} == 1.1940298507220641
1809 - label
1810 - host
1811 -out:
1812 - return: SUCCEED
1813 - value: host2
1814 ----
1815 -test case: prometheus_getmetric10
1816 -in:
1817 - value:
1818 - value_type: ITEM_VALUE_TYPE_STR
1819 - time: 2017-10-29 03:15:00 +03:00
1820 - data: |
1821 - # HELP cpu_usage_system Telegraf collected metric
1822 - # TYPE cpu_usage_system gauge
1823 - cpu_usage_system{cpu="cpu-total",host="host1"} 1.1940298507220641
1824 - cpu_usage_system{cpu="cpu0",host="host2"} 1.1940298507220641
1825 - cpu_usage_system{cpu="cpu1",host="host,3"} 1.1340298507220641
1826 - step:
1827 - type: ZBX_PREPROC_PROMETHEUS_PATTERN
1828 - params: |-
1829 - cpu_usage_system{cpu="cpu1"}
1830 - label
1831 - host
1832 -out:
1833 - return: SUCCEED
1834 - value: host,3
1835 ----
1836 -test case: prometheus_getmetric11
1837 -in:
1838 - value:
1839 - value_type: ITEM_VALUE_TYPE_STR
1840 - time: 2017-10-29 03:15:00 +03:00
1841 - data: |
1842 - # HELP cpu_usage_system Telegraf collected metric
1843 - # TYPE cpu_usage_system gauge
1844 - cpu_usage_system{cpu="cpu-total"} 1.1940298507220641
1845 - cpu_usage_system{cpu="cpu0"} 1.1940298507220641
1846 - cpu_usage_system{cpu="cpu1"} 1.1340298507220641
1847 - step:
1848 - type: ZBX_PREPROC_PROMETHEUS_PATTERN
1849 - params: |-
1850 - cpu_usage_system{cpu="cpu-total"}
1851 - gpu
1852 - error_handler: ZBX_PREPROC_FAIL_SET_ERROR
1853 - error_handler_params: custom error
1854 -out:
1855 - return: FAIL
1856 - error: custom error
1857 ----
1858 -test case: prometheus_getmetric12
1859 -in:
1860 - value:
1861 - value_type: ITEM_VALUE_TYPE_STR
1862 - time: 2017-10-29 03:15:00 +03:00
1863 - data: |
1864 - # HELP cpu_usage_system Telegraf collected metric
1865 - # TYPE cpu_usage_system gauge
1866 - cpu_usage_system{cpu="cpu-total"} 1.1940298507220641
1867 - cpu_usage_system{cpu="cpu0"} 1.1940298507220641
1868 - cpu_usage_system{cpu="cpu1"} 1.1340298507220641
1869 - step:
1870 - type: ZBX_PREPROC_PROMETHEUS_PATTERN
1871 - params: |-
1872 - cpu_usage_system{cpu="cpu"}
1873 - \value
1874 - error_handler: ZBX_PREPROC_FAIL_SET_ERROR
1875 - error_handler_params: custom error
1876 -out:
1877 - return: FAIL
1878 - error: custom error
1879 ----
1880 -test case: prometheus_getmetric13
1881 -in:
1882 - value:
1883 - value_type: ITEM_VALUE_TYPE_STR
1884 - time: 2017-10-29 03:15:00 +03:00
1885 - data: |
1886 - wmi_service_state{name="dhcp",state="continue pending"} 0
1887 - wmi_service_state{name="dhcp",state="continue pending"} 1
1888 - wmi_service_state{name="dhcp",state="pause pending"} 1
1889 - wmi_service_state{name="dhcp",state="paused"} 1
1890 - wmi_service_state{name="dhcp",state="running"} 1
1891 - wmi_service_state{name="dhcp",state="start pending"} 1
1892 - wmi_service_state{name="dhcp",state="stop pending"} 1
1893 - wmi_service_state{name="dhcp",state="stopped"} 1
1894 - wmi_service_state{name="dhcp",state="unknown"} 1
1895 - wmi_service_state{name="dhcp",state="continue pending"} 1
1896 - wmi_service_state{name="dhcp",state="pause pending"} 1
1897 - wmi_service_state{name="dhcp",state="paused"} 1
1898 - wmi_service_state{name="dhcp",state="running"} 1
1899 - wmi_service_state{name="dhcp",state="start pending"} 1
1900 - wmi_service_state{name="dhcp",state="stop pending"} 1
1901 - wmi_service_state{name="dhcp",state="stopped"} 1
1902 - wmi_service_state{name="dhcp",state="unknown"} 1
1903 - wmi_service_state{name="dhcp",state="unknown"} 0
1904 - step:
1905 - type: ZBX_PREPROC_PROMETHEUS_PATTERN
1906 - params: |-
1907 - wmi_service_state == 1
1908 - \value
1909 - error_handler: ZBX_PREPROC_FAIL_SET_ERROR
1910 - error_handler_params: custom error
1911 -out:
1912 - return: FAIL
1913 - error: custom error
1914 ----
1915 -test case: prometheus_to_json1
1916 -in:
1917 - value:
1918 - value_type: ITEM_VALUE_TYPE_STR
1919 - time: 2017-10-29 03:15:00 +03:00
1920 - data: |
1921 - # HELP cpu_usage_system Telegraf collected metric
1922 - # TYPE cpu_usage_system gauge
1923 - cpu_usage_system{cpu="cpu-total"} 1.1940298507220641
1924 - cpu_usage_system{cpu="cpu0"} 1.1940298507220641
1925 - cpu_usage_system{cpu="cpu1"} 1.1340298507220641
1926 - step:
1927 - type: ZBX_PREPROC_PROMETHEUS_TO_JSON
1928 - params: cpu_usage_system
1929 -out:
1930 - return: SUCCEED
1931 - value: '[{"name":"cpu_usage_system","value":"1.1940298507220641","line_raw":"cpu_usage_system{cpu=\"cpu-total\"} 1.1940298507220641","labels":{"cpu":"cpu-total"},"type":"gauge","help":"Telegraf collected metric"},{"name":"cpu_usage_system","value":"1.1940298507220641","line_raw":"cpu_usage_system{cpu=\"cpu0\"} 1.1940298507220641","labels":{"cpu":"cpu0"},"type":"gauge","help":"Telegraf collected metric"},{"name":"cpu_usage_system","value":"1.1340298507220641","line_raw":"cpu_usage_system{cpu=\"cpu1\"} 1.1340298507220641","labels":{"cpu":"cpu1"},"type":"gauge","help":"Telegraf collected metric"}]'
1932 ----
1933 -test case: prometheus_to_json2
1934 -in:
1935 - value:
1936 - value_type: ITEM_VALUE_TYPE_STR
1937 - time: 2017-10-29 03:15:00 +03:00
1938 - data: |
1939 - # HELP wmi_os_timezone OperatingSystem.LocalDateTime
1940 - # TYPE wmi_os_timezone gauge
1941 - wmi_os_timezone{timezone="MSK"} 1
1942 - step:
1943 - type: ZBX_PREPROC_PROMETHEUS_TO_JSON
1944 - params: wmi_os_timezone
1945 -out:
1946 - return: SUCCEED
1947 - value: '[{"name":"wmi_os_timezone","value":"1","line_raw":"wmi_os_timezone{timezone=\"MSK\"} 1","labels":{"timezone":"MSK"},"type":"gauge","help":"OperatingSystem.LocalDateTime"}]'
1948 ----
1949 -test case: prometheus_to_json3
1950 -in:
1951 - value:
1952 - value_type: ITEM_VALUE_TYPE_STR
1953 - time: 2017-10-29 03:15:00 +03:00
1954 - data: |
1955 - random_date{year="2019",month="february",day="02/12/2019"} 1
1956 - random_date{year="2019",month="march",day="03/07/2019"} 2
1957 - step:
1958 - type: ZBX_PREPROC_PROMETHEUS_TO_JSON
1959 - params: random_date{year="2019",day=~"^([0-2][0-9]|(3)[0-1])(\\/)(((0)[0-9])|((1)[0-2]))(\\/)\\d{4}$"}
1960 -out:
1961 - return: SUCCEED
1962 - value: '[{"name":"random_date","value":"1","line_raw":"random_date{year=\"2019\",month=\"february\",day=\"02/12/2019\"} 1","labels":{"year":"2019","month":"february","day":"02/12/2019"},"type":"untyped"},{"name":"random_date","value":"2","line_raw":"random_date{year=\"2019\",month=\"march\",day=\"03/07/2019\"} 2","labels":{"year":"2019","month":"march","day":"03/07/2019"},"type":"untyped"}]'
1963 ----
1964 -test case: prometheus_to_json4
1965 -in:
1966 - value:
1967 - value_type: ITEM_VALUE_TYPE_STR
1968 - time: 2017-10-29 03:15:00 +03:00
1969 - data: metric_without_timestamp_and_labels 12.47
1970 - step:
1971 - type: ZBX_PREPROC_PROMETHEUS_TO_JSON
1972 - params: metric_without_timestamp_and_labels
1973 -out:
1974 - return: SUCCEED
1975 - value: '[{"name":"metric_without_timestamp_and_labels","value":"12.47","line_raw":"metric_without_timestamp_and_labels 12.47","type":"untyped"}]'
1976 ----
1977 -test case: prometheus_to_json5
1978 -in:
1979 - value:
1980 - value_type: ITEM_VALUE_TYPE_STR
1981 - time: 2017-10-29 03:15:00 +03:00
1982 - data: wmi_os_timezone{timezone} 1
1983 - step:
1984 - type: ZBX_PREPROC_PROMETHEUS_TO_JSON
1985 - params: wmi_os_timezone
1986 - error_handler: ZBX_PREPROC_FAIL_SET_ERROR
1987 - error_handler_params: custom error
1988 -out:
1989 - return: FAIL
1990 - error: custom error
1991 ----
1992 -test case: csv_to_json1
1993 -in:
1994 - value:
1995 - value_type: ITEM_VALUE_TYPE_STR
1996 - time: 2017-10-29 03:15:00 +03:00
1997 - data: |
1998 - sep=.
1999 - A.B.C
2000 - 1.2.3
2001 - step:
2002 - type: ZBX_PREPROC_CSV_TO_JSON
2003 - params: "\n\n1"
2004 -out:
2005 - return: SUCCEED
2006 - value: '[{"A":"1","B":"2","C":"3"},{"A":"","B":"","C":""}]'
2007 ----
2008 -test case: csv_to_json2
2009 -in:
2010 - value:
2011 - value_type: ITEM_VALUE_TYPE_STR
2012 - time: 2017-10-29 03:15:00 +03:00
2013 - data: |
2014 - sep=.
2015 - A.`B.``B`.C
2016 - 1.`2`.`3.`
2017 - step:
2018 - type: ZBX_PREPROC_CSV_TO_JSON
2019 - params: "\n`\n1"
2020 -out:
2021 - return: SUCCEED
2022 - value: '[{"A":"1","B.`B":"2","C":"3."},{"A":"","B.`B":"","C":""}]'
2023 ----
2024 -test case: csv_to_json3
2025 -in:
2026 - value:
2027 - value_type: ITEM_VALUE_TYPE_STR
2028 - time: 2017-10-29 03:15:00 +03:00
2029 - data: 1,2,3
2030 - step:
2031 - type: ZBX_PREPROC_CSV_TO_JSON
2032 - params: "\n\n0"
2033 -out:
2034 - return: SUCCEED
2035 - value: '[{"1":"1","2":"2","3":"3"}]'
2036 ----
2037 -test case: csv_to_json4
2038 -in:
2039 - value:
2040 - value_type: ITEM_VALUE_TYPE_STR
2041 - time: 2017-10-29 03:15:00 +03:00
2042 - data: |
2043 - A,B,C
2044 - 1,2,3,4
2045 - step:
2046 - type: ZBX_PREPROC_PROMETHEUS_TO_JSON
2047 - params: "\n\n1"
2048 - error_handler: ZBX_PREPROC_FAIL_SET_ERROR
2049 - error_handler_params: custom error
2050 -out:
2051 - return: FAIL
2052 - error: custom error
2053 ----
2054 -test case: replace a to b
2055 -in:
2056 - value:
2057 - value_type: ITEM_VALUE_TYPE_STR
2058 - time: 2017-10-29 03:15:00 +03:00
2059 - data: a
2060 - step:
2061 - type: ZBX_PREPROC_STR_REPLACE
2062 - params: "a\nb"
2063 -out:
2064 - return: SUCCEED
2065 - value: b
2066 ----
2067 -test case: replace a to bbb
2068 -in:
2069 - value:
2070 - value_type: ITEM_VALUE_TYPE_STR
2071 - time: 2017-10-29 03:15:00 +03:00
2072 - data: a1a2a3a
2073 - step:
2074 - type: ZBX_PREPROC_STR_REPLACE
2075 - params: "a\nbbb"
2076 -out:
2077 - return: SUCCEED
2078 - value: bbb1bbb2bbb3bbb
2079 ----
2080 -test case: replace a to a
2081 -in:
2082 - value:
2083 - value_type: ITEM_VALUE_TYPE_STR
2084 - time: 2017-10-29 03:15:00 +03:00
2085 - data: a
2086 - step:
2087 - type: ZBX_PREPROC_STR_REPLACE
2088 - params: "a\na"
2089 -out:
2090 - return: SUCCEED
2091 - value: a
2092 ----
2093 -test case: replace a to nothing
2094 -in:
2095 - value:
2096 - value_type: ITEM_VALUE_TYPE_STR
2097 - time: 2017-10-29 03:15:00 +03:00
2098 - data: a
2099 - step:
2100 - type: ZBX_PREPROC_STR_REPLACE
2101 - params: "a\n"
2102 -out:
2103 - return: SUCCEED
2104 - value: ""
2105 ----
2106 -test case: replace a to nothing in between
2107 -in:
2108 - value:
2109 - value_type: ITEM_VALUE_TYPE_STR
2110 - time: 2017-10-29 03:15:00 +03:00
2111 - data: abac
2112 - step:
2113 - type: ZBX_PREPROC_STR_REPLACE
2114 - params: "a\n"
2115 -out:
2116 - return: SUCCEED
2117 - value: "bc"
2118 ----
2119 -test case: replace non printable characters
2120 -in:
2121 - value:
2122 - value_type: ITEM_VALUE_TYPE_STR
2123 - time: 2017-10-29 03:15:00 +03:00
2124 - data: "\n\r "
2125 - step:
2126 - type: ZBX_PREPROC_STR_REPLACE
2127 - params: "\\n\\r\\t\\s\nOK"
2128 -out:
2129 - return: SUCCEED
2130 - value: "OK"
2131 ----
2132 -test case: replace non printable characters mixed
2133 -in:
2134 - value:
2135 - value_type: ITEM_VALUE_TYPE_STR
2136 - time: 2017-10-29 03:15:00 +03:00
2137 - data: "\n1\r2 3 4"
2138 - step:
2139 - type: ZBX_PREPROC_STR_REPLACE
2140 - params: "\\n1\\r2\\t3\\s4\nOK"
2141 -out:
2142 - return: SUCCEED
2143 - value: "OK"
2144 ----
2145 -test case: replace non printable characters 2
2146 -in:
2147 - value:
2148 - value_type: ITEM_VALUE_TYPE_STR
2149 - time: 2017-10-29 03:15:00 +03:00
2150 - data: "\n\r "
2151 - step:
2152 - type: ZBX_PREPROC_STR_REPLACE
2153 - params: "\\n\\r \nOK"
2154 -out:
2155 - return: SUCCEED
2156 - value: "OK"
2157 ----
2158 -test case: replace non printable characters in both search and replace
2159 -in:
2160 - value:
2161 - value_type: ITEM_VALUE_TYPE_STR
2162 - time: 2017-10-29 03:15:00 +03:00
2163 - data: "\n\r "
2164 - step:
2165 - type: ZBX_PREPROC_STR_REPLACE
2166 - params: "\\n\\r\\t\\s\n\\n\\r\\tOK\\s"
2167 -out:
2168 - return: SUCCEED
2169 - value: "\n\r\tOK "
2170 ----
2171 -test case: replace \
2172 -in:
2173 - value:
2174 - value_type: ITEM_VALUE_TYPE_STR
2175 - time: 2017-10-29 03:15:00 +03:00
2176 - data: "\\"
2177 - step:
2178 - type: ZBX_PREPROC_STR_REPLACE
2179 - params: "\\\nOK"
2180 -out:
2181 - return: SUCCEED
2182 - value: "OK"
2183 ----
2184 -test case: replace 2 characters "\t" to OK
2185 -in:
2186 - value:
2187 - value_type: ITEM_VALUE_TYPE_STR
2188 - time: 2017-10-29 03:15:00 +03:00
2189 - data: "\\t"
2190 - step:
2191 - type: ZBX_PREPROC_STR_REPLACE
2192 - params: "\\\\t\nOK"
2193 -out:
2194 - return: SUCCEED
2195 - value: "OK"
2196 ----
2197 -test case: Missing second parameter
2198 -in:
2199 - value:
2200 - value_type: ITEM_VALUE_TYPE_STR
2201 - time: 2017-10-29 03:15:00 +03:00
2202 - data: "a"
2203 - step:
2204 - type: ZBX_PREPROC_STR_REPLACE
2205 - params: "OK"
2206 -out:
2207 - return: FAIL
2208 - value: "a"
2209 ----
2210 -test case: Missing first parameter
2211 -in:
2212 - value:
2213 - value_type: ITEM_VALUE_TYPE_STR
2214 - time: 2017-10-29 03:15:00 +03:00
2215 - data: "a"
2216 - step:
2217 - type: ZBX_PREPROC_STR_REPLACE
2218 - params: "\nOK"
2219 -out:
2220 - return: FAIL
2221 - value: "a"
2222 ----
2223 -test case: Single slash in search
2224 -in:
2225 - value:
2226 - value_type: ITEM_VALUE_TYPE_STR
2227 - time: 2017-10-29 03:15:00 +03:00
2228 - data: "\\"
2229 - step:
2230 - type: ZBX_PREPROC_STR_REPLACE
2231 - params: "\\\nOK"
2232 -out:
2233 - return: SUCCEED
2234 - value: "OK"
2235 ----
2236 -test case: Single slash in replace
2237 -in:
2238 - value:
2239 - value_type: ITEM_VALUE_TYPE_STR
2240 - time: 2017-10-29 03:15:00 +03:00
2241 - data: "OK"
2242 - step:
2243 - type: ZBX_PREPROC_STR_REPLACE
2244 - params: "OK\n\\"
2245 -out:
2246 - return: SUCCEED
2247 - value: "\\"
2248 ----
2249 -test case: No match
2250 -in:
2251 - value:
2252 - value_type: ITEM_VALUE_TYPE_STR
2253 - time: 2017-10-29 03:15:00 +03:00
2254 - data: "O K"
2255 - step:
2256 - type: ZBX_PREPROC_STR_REPLACE
2257 - params: "OK\ntest"
2258 -out:
2259 - return: SUCCEED
2260 - value: "O K"
2261 ----
2262 -test case: SNMP walk to value - bad data
2263 -in:
2264 - value:
2265 - value_type: ITEM_VALUE_TYPE_STR
2266 - time: 2017-10-29 03:15:00 +03:00
2267 - data: |
2268 - aaa
2269 - bbb
2270 - step:
2271 - type: ZBX_PREPROC_SNMP_WALK_VALUE
2272 - params: |-
2273 - .1.3.6.1.1
2274 - 0
2275 -out:
2276 - return: FAIL
2277 ----
2278 -test case: SNMP walk to value - bad param
2279 -in:
2280 - value:
2281 - value_type: ITEM_VALUE_TYPE_STR
2282 - time: 2017-10-29 03:15:00 +03:00
2283 - data: |
2284 - .1.3.6.1.1 = Hex-STRING: 71 71
2285 - step:
2286 - type: ZBX_PREPROC_SNMP_WALK_VALUE
2287 -out:
2288 - return: FAIL
2289 ----
2290 -test case: SNMP walk to value - string
2291 -in:
2292 - value:
2293 - value_type: ITEM_VALUE_TYPE_STR
2294 - time: 2017-10-29 03:15:00 +03:00
2295 - data: |
2296 - .1.3.6.1.1 = STRING: "AAA"
2297 - step:
2298 - type: ZBX_PREPROC_SNMP_WALK_VALUE
2299 - params: |-
2300 - .1.3.6.1.1
2301 - 0
2302 -out:
2303 - return: SUCCEED
2304 - value: 'AAA'
2305 ----
2306 -test case: SNMP walk to value - NULL
2307 -in:
2308 - value:
2309 - value_type: ITEM_VALUE_TYPE_STR
2310 - time: 2017-10-29 03:15:00 +03:00
2311 - data: |
2312 - .1.3.6.1.1 = NULL
2313 - step:
2314 - type: ZBX_PREPROC_SNMP_WALK_VALUE
2315 - params: |-
2316 - .1.3.6.1.1
2317 - 0
2318 -out:
2319 - return: SUCCEED
2320 - value: 'NULL'
2321 ----
2322 -test case: SNMP walk to value - Arbitrary number
2323 -in:
2324 - value:
2325 - value_type: ITEM_VALUE_TYPE_STR
2326 - time: 2017-10-29 03:15:00 +03:00
2327 - data: |
2328 - .1.3.6.1.1 = 123
2329 - step:
2330 - type: ZBX_PREPROC_SNMP_WALK_VALUE
2331 - params: |-
2332 - .1.3.6.1.1
2333 - 0
2334 -out:
2335 - return: SUCCEED
2336 - value: 123
2337 ----
2338 -test case: SNMP walk to value - INTEGER
2339 -in:
2340 - value:
2341 - value_type: ITEM_VALUE_TYPE_STR
2342 - time: 2017-10-29 03:15:00 +03:00
2343 - data: |
2344 - .1.3.6.1.1 = INTEGER: 123
2345 - step:
2346 - type: ZBX_PREPROC_SNMP_WALK_VALUE
2347 - params: |-
2348 - .1.3.6.1.1
2349 - 0
2350 -out:
2351 - return: SUCCEED
2352 - value: 123
2353 ----
2354 -test case: SNMP walk to value - IpAddress
2355 -in:
2356 - value:
2357 - value_type: ITEM_VALUE_TYPE_STR
2358 - time: 2017-10-29 03:15:00 +03:00
2359 - data: |
2360 - .1.3.6.1.1 = IpAddress: 127.0.0.1
2361 - step:
2362 - type: ZBX_PREPROC_SNMP_WALK_VALUE
2363 - params: |-
2364 - .1.3.6.1.1
2365 - 0
2366 -out:
2367 - return: SUCCEED
2368 - value: '127.0.0.1'
2369 ----
2370 -test case: SNMP walk to value - Hex-STRING
2371 -in:
2372 - value:
2373 - value_type: ITEM_VALUE_TYPE_STR
2374 - time: 2017-10-29 03:15:00 +03:00
2375 - data: |
2376 - .1.3.6.1.1 = Hex-STRING: 71 71 AA
2377 - step:
2378 - type: ZBX_PREPROC_SNMP_WALK_VALUE
2379 - params: |-
2380 - .1.3.6.1.1
2381 - 0
2382 -out:
2383 - return: SUCCEED
2384 - value: '71 71 AA'
2385 ----
2386 -test case: SNMP walk to value - Hex-STRING (multiline)
2387 -in:
2388 - value:
2389 - value_type: ITEM_VALUE_TYPE_STR
2390 - time: 2017-10-29 03:15:00 +03:00
2391 - data: |
2392 - .1.3.6.1.1 = Hex-STRING: 71 71
2393 - AA BB CC
2394 - DD
2395 - step:
2396 - type: ZBX_PREPROC_SNMP_WALK_VALUE
2397 - params: |-
2398 - .1.3.6.1.1
2399 - 0
2400 -out:
2401 - return: SUCCEED
2402 - value: '71 71 AA BB CC DD'
2403 ----
2404 -test case: SNMP walk to value - Hex-STRING (multiline) - space on the last line
2405 -in:
2406 - value:
2407 - value_type: ITEM_VALUE_TYPE_STR
2408 - time: 2017-10-29 03:15:00 +03:00
2409 - data: |
2410 - .1.3.6.1.1 = Hex-STRING: 71 71
2411 - AA BB CC
2412 - DD
2413 - step:
2414 - type: ZBX_PREPROC_SNMP_WALK_VALUE
2415 - params: |-
2416 - .1.3.6.1.1
2417 - 0
2418 -out:
2419 - return: SUCCEED
2420 - value: '71 71 AA BB CC DD'
2421 ----
2422 -test case: SNMP walk to value - Hex-STRING (multiline) - with succeeding string
2423 -in:
2424 - value:
2425 - value_type: ITEM_VALUE_TYPE_STR
2426 - time: 2017-10-29 03:15:00 +03:00
2427 - data: |
2428 - .1.3.6.1.1 = Hex-STRING: 71 71
2429 - AA BB CC
2430 - DD
2431 - .1.3.6.1.2 = Hex-STRING: 74 65 73 74 20 D1 91
2432 - step:
2433 - type: ZBX_PREPROC_SNMP_WALK_VALUE
2434 - params: |-
2435 - .1.3.6.1.1
2436 - 0
2437 -out:
2438 - return: SUCCEED
2439 - value: '71 71 AA BB CC DD'
2440 ----
2441 -test case: SNMP walk to value - Hex-STRING - to UTF8
2442 -in:
2443 - value:
2444 - value_type: ITEM_VALUE_TYPE_STR
2445 - time: 2017-10-29 03:15:00 +03:00
2446 - data: |
2447 - .1.3.6.1.1 = Hex-STRING: 74 65 73 74 20 D1 91
2448 - step:
2449 - type: ZBX_PREPROC_SNMP_WALK_VALUE
2450 - params: |-
2451 - .1.3.6.1.1
2452 - 1
2453 -out:
2454 - return: SUCCEED
2455 - value: 'test ё'
2456 ----
2457 -test case: SNMP walk to value - Hex-STRING - to UTF8 (null terminated)
2458 -in:
2459 - value:
2460 - value_type: ITEM_VALUE_TYPE_STR
2461 - time: 2017-10-29 03:15:00 +03:00
2462 - data: |
2463 - .1.3.6.1.1 = Hex-STRING: 74 65 73 74 20 D1 91 00
2464 - step:
2465 - type: ZBX_PREPROC_SNMP_WALK_VALUE
2466 - params: |-
2467 - .1.3.6.1.1
2468 - 1
2469 -out:
2470 - return: SUCCEED
2471 - value: 'test ё'
2472 ----
2473 -test case: SNMP walk to value - Hex-STRING - to UTF8 - invalid hex string
2474 -in:
2475 - value:
2476 - value_type: ITEM_VALUE_TYPE_STR
2477 - time: 2017-10-29 03:15:00 +03:00
2478 - data: |
2479 - .1.3.6.1.1 = Hex-STRING: 74 65 X3 74 20 D1 91
2480 - step:
2481 - type: ZBX_PREPROC_SNMP_WALK_VALUE
2482 - params: |-
2483 - .1.3.6.1.1
2484 - 1
2485 -out:
2486 - return: FAIL
2487 ----
2488 -test case: SNMP walk to value - Hex-STRING - to UTF8 - invalid hex string (2)
2489 -in:
2490 - value:
2491 - value_type: ITEM_VALUE_TYPE_STR
2492 - time: 2017-10-29 03:15:00 +03:00
2493 - data: |
2494 - .1.3.6.1.1 = Hex-STRING: 74 65 74 20 D1 9
2495 - step:
2496 - type: ZBX_PREPROC_SNMP_WALK_VALUE
2497 - params: |-
2498 - .1.3.6.1.1
2499 - 1
2500 -out:
2501 - return: FAIL
2502 ----
2503 -test case: SNMP walk to value - Hex-STRING - to UTF8 - invalid sequence
2504 -in:
2505 - value:
2506 - value_type: ITEM_VALUE_TYPE_STR
2507 - time: 2017-10-29 03:15:00 +03:00
2508 - data: |
2509 - .1.3.6.1.1 = Hex-STRING: 74 65 73 74 20 D1 91 FF
2510 - step:
2511 - type: ZBX_PREPROC_SNMP_WALK_VALUE
2512 - params: |-
2513 - .1.3.6.1.1
2514 - 1
2515 -out:
2516 - return: SUCCEED
2517 - value: 'test ё?'
2518 ----
2519 -test case: SNMP walk to value - Hex-STRING - to MAC
2520 -in:
2521 - value:
2522 - value_type: ITEM_VALUE_TYPE_STR
2523 - time: 2017-10-29 03:15:00 +03:00
2524 - data: |
2525 - .1.3.6.1.1 = Hex-STRING: 74 65 73 74 20 D1
2526 - step:
2527 - type: ZBX_PREPROC_SNMP_WALK_VALUE
2528 - params: |-
2529 - .1.3.6.1.1
2530 - 2
2531 -out:
2532 - return: SUCCEED
2533 - value: '74:65:73:74:20:D1'
2534 ----
2535 -test case: SNMP walk to value - Hex-STRING - to MAC - invalid hex string
2536 -in:
2537 - value:
2538 - value_type: ITEM_VALUE_TYPE_STR
2539 - time: 2017-10-29 03:15:00 +03:00
2540 - data: |
2541 - .1.3.6.1.1 = Hex-STRING: 74 65 73 74 XX
2542 - step:
2543 - type: ZBX_PREPROC_SNMP_WALK_VALUE
2544 - params: |-
2545 - .1.3.6.1.1
2546 - 2
2547 -out:
2548 - return: FAIL
2549 ----
2550 -test case: SNMP walk to value - BITS to integer - 1
2551 -in:
2552 - value:
2553 - value_type: ITEM_VALUE_TYPE_STR
2554 - time: 2017-10-29 03:15:00 +03:00
2555 - data: |
2556 - .1.3.6.1.1 = BITS: FE EE 15 15
2557 - step:
2558 - type: ZBX_PREPROC_SNMP_WALK_VALUE
2559 - params: |-
2560 - .1.3.6.1.1
2561 - 3
2562 -out:
2563 - return: SUCCEED
2564 - value: '353758974'
2565 ----
2566 -test case: SNMP walk to value - BITS to integer - 2
2567 -in:
2568 - value:
2569 - value_type: ITEM_VALUE_TYPE_STR
2570 - time: 2017-10-29 03:15:00 +03:00
2571 - data: |
2572 - .1.3.6.1.1 = BITS: 01
2573 - step:
2574 - type: ZBX_PREPROC_SNMP_WALK_VALUE
2575 - params: |-
2576 - .1.3.6.1.1
2577 - 3
2578 -out:
2579 - return: SUCCEED
2580 - value: '1'
2581 ----
2582 -test case: SNMP walk to value - BITS to integer - 3
2583 -in:
2584 - value:
2585 - value_type: ITEM_VALUE_TYPE_STR
2586 - time: 2017-10-29 03:15:00 +03:00
2587 - data: |
2588 - .1.3.6.1.1 = BITS: 00 00 00
2589 - step:
2590 - type: ZBX_PREPROC_SNMP_WALK_VALUE
2591 - params: |-
2592 - .1.3.6.1.1
2593 - 3
2594 -out:
2595 - return: SUCCEED
2596 - value: '0'
2597 ----
2598 -test case: SNMP walk to value - BITS to integer - 4
2599 -in:
2600 - value:
2601 - value_type: ITEM_VALUE_TYPE_STR
2602 - time: 2017-10-29 03:15:00 +03:00
2603 - data: |
2604 - .1.3.6.1.1 = BITS: 01 02 03 04 05 06 07 08 09 10
2605 - step:
2606 - type: ZBX_PREPROC_SNMP_WALK_VALUE
2607 - params: |-
2608 - .1.3.6.1.1
2609 - 3
2610 -out:
2611 - return: SUCCEED
2612 - value: '578437695752307201'
2613 ----
2614 -test case: SNMP walk to value - BITS to integer - 5
2615 -in:
2616 - value:
2617 - value_type: ITEM_VALUE_TYPE_STR
2618 - time: 2017-10-29 03:15:00 +03:00
2619 - data: |
2620 - .1.3.6.1.1 = BITS: 01 02 03 04 05 06 07 08
2621 - step:
2622 - type: ZBX_PREPROC_SNMP_WALK_VALUE
2623 - params: |-
2624 - .1.3.6.1.1
2625 - 3
2626 -out:
2627 - return: SUCCEED
2628 - value: '578437695752307201'
2629 ----
2630 -test case: SNMP walk to value - BITS to integer - 6
2631 -in:
2632 - value:
2633 - value_type: ITEM_VALUE_TYPE_STR
2634 - time: 2017-10-29 03:15:00 +03:00
2635 - data: |
2636 - .1.3.6.1.1 = BITS: 01 02 03 04 05 06 07 0
2637 - step:
2638 - type: ZBX_PREPROC_SNMP_WALK_VALUE
2639 - params: |-
2640 - .1.3.6.1.1
2641 - 3
2642 -out:
2643 - return: FAIL
2644 ----
2645 -test case: SNMP walk to value - BITS to integer - 7
2646 -in:
2647 - value:
2648 - value_type: ITEM_VALUE_TYPE_STR
2649 - time: 2017-10-29 03:15:00 +03:00
2650 - data: |
2651 - .1.3.6.1.1 = BITS: QW 11
2652 - step:
2653 - type: ZBX_PREPROC_SNMP_WALK_VALUE
2654 - params: |-
2655 - .1.3.6.1.1
2656 - 3
2657 -out:
2658 - return: FAIL
2659 ----
2660 -test case: SNMP walk to value - Opaque wrapped type
2661 -in:
2662 - value:
2663 - value_type: ITEM_VALUE_TYPE_STR
2664 - time: 2017-10-29 03:15:00 +03:00
2665 - data: |
2666 - .1.3.6.1.1 = Opaque: Float: 0.460000
2667 - step:
2668 - type: ZBX_PREPROC_SNMP_WALK_VALUE
2669 - params: |-
2670 - .1.3.6.1.1
2671 - 0
2672 -out:
2673 - return: SUCCEED
2674 - value: 0.460000
2675 ----
2676 -test case: SNMP walk to value - Counter32
2677 -in:
2678 - value:
2679 - value_type: ITEM_VALUE_TYPE_STR
2680 - time: 2017-10-29 03:15:00 +03:00
2681 - data: |
2682 - .1.3.6.1.1 = Counter32: 12345678
2683 - step:
2684 - type: ZBX_PREPROC_SNMP_WALK_VALUE
2685 - params: |-
2686 - .1.3.6.1.1
2687 - 0
2688 -out:
2689 - return: SUCCEED
2690 - value: 12345678
2691 ----
2692 -test case: SNMP walk to value - STRING with newline
2693 -in:
2694 - value:
2695 - value_type: ITEM_VALUE_TYPE_STR
2696 - time: 2017-10-29 03:15:00 +03:00
2697 - data: |
2698 - .1.3.6.1.1 = STRING: "line1
2699 - line2"
2700 - .1.3.6.2.1 = STRING: "line2
2701 - line3"
2702 - step:
2703 - type: ZBX_PREPROC_SNMP_WALK_VALUE
2704 - params: |-
2705 - .1.3.6.1.1
2706 - 0
2707 -out:
2708 - return: SUCCEED
2709 - value: |-
2710 - line1
2711 - line2
2712 ----
2713 -test case: SNMP walk to value - unquoted STRING with newline
2714 -in:
2715 - value:
2716 - value_type: ITEM_VALUE_TYPE_STR
2717 - time: 2017-10-29 03:15:00 +03:00
2718 - data: |
2719 - .1.3.6.1.1 = STRING: line1
2720 - line2
2721 - .1.3.6.2.1 = STRING: line2
2722 - line3
2723 - step:
2724 - type: ZBX_PREPROC_SNMP_WALK_VALUE
2725 - params: |-
2726 - .1.3.6.1.1
2727 - 0
2728 -out:
2729 - return: SUCCEED
2730 - value: |-
2731 - line1
2732 - line2
2733 ----
2734 -test case: SNMP walk to value - unquoted STRING with newline and empty string
2735 -in:
2736 - value:
2737 - value_type: ITEM_VALUE_TYPE_STR
2738 - time: 2017-10-29 03:15:00 +03:00
2739 - data: |
2740 - .1.3.6.1.1 = STRING: line1
2741 -
2742 - .1.3.6.2.1 = STRING: line2
2743 - line3
2744 - step:
2745 - type: ZBX_PREPROC_SNMP_WALK_VALUE
2746 - params: |-
2747 - .1.3.6.1.1
2748 - 0
2749 -out:
2750 - return: SUCCEED
2751 - value: "line1\n"
2752 ----
2753 -test case: SNMP walk to value - quoted STRING with quoted substring
2754 -in:
2755 - value:
2756 - value_type: ITEM_VALUE_TYPE_STR
2757 - time: 2017-10-29 03:15:00 +03:00
2758 - data: |
2759 - .1.3.6.1.1 = STRING: "foo
2760 - \"bar\"
2761 - baz"
2762 - .1.3.6.2.1 = STRING: line2
2763 - line3
2764 - step:
2765 - type: ZBX_PREPROC_SNMP_WALK_VALUE
2766 - params: |-
2767 - .1.3.6.1.1
2768 - 0
2769 -out:
2770 - return: SUCCEED
2771 - value: |-
2772 - foo
2773 - "bar"
2774 - baz
2775 ----
2776 -test case: SNMP walk to value - quoted STRING with broken quoting
2777 -in:
2778 - value:
2779 - value_type: ITEM_VALUE_TYPE_STR
2780 - time: 2017-10-29 03:15:00 +03:00
2781 - data: |
2782 - .1.3.6.1.1 = STRING: "foo
2783 - \
2784 - .1.3.6.2.1 = STRING: line2
2785 - line3
2786 - step:
2787 - type: ZBX_PREPROC_SNMP_WALK_VALUE
2788 - params: |-
2789 - .1.3.6.1.1
2790 - 0
2791 -out:
2792 - return: FAIL
2793 ----
2794 -test case: SNMP walk to value - backslashes in quoted string
2795 -in:
2796 - value:
2797 - value_type: ITEM_VALUE_TYPE_STR
2798 - time: 2017-10-29 03:15:00 +03:00
2799 - data: |
2800 - .1.3.6.1.1 = STRING: "foo\\bar\\baz"
2801 - step:
2802 - type: ZBX_PREPROC_SNMP_WALK_VALUE
2803 - params: |-
2804 - .1.3.6.1.1
2805 - 0
2806 -out:
2807 - return: SUCCEED
2808 - value: foo\bar\baz
2809 ----
2810 -test case: SNMP walk to value - backslashes in unquoted string
2811 -in:
2812 - value:
2813 - value_type: ITEM_VALUE_TYPE_STR
2814 - time: 2017-10-29 03:15:00 +03:00
2815 - data: |
2816 - .1.3.6.1.1 = STRING: foo\bar\baz
2817 - step:
2818 - type: ZBX_PREPROC_SNMP_WALK_VALUE
2819 - params: |-
2820 - .1.3.6.1.1
2821 - 0
2822 -out:
2823 - return: SUCCEED
2824 - value: foo\bar\baz
2825 ----
2826 -test case: SNMP walk to value - Empty type
2827 -in:
2828 - value:
2829 - value_type: ITEM_VALUE_TYPE_STR
2830 - time: 2017-10-29 03:15:00 +03:00
2831 - data: |
2832 - .1.3.6.1.1 = ""
2833 - step:
2834 - type: ZBX_PREPROC_SNMP_WALK_VALUE
2835 - params: |-
2836 - .1.3.6.1.1
2837 - 0
2838 -out:
2839 - return: SUCCEED
2840 - value: ''
2841 ----
2842 -test case: SNMP walk to value - OID without prepending dot
2843 -in:
2844 - value:
2845 - value_type: ITEM_VALUE_TYPE_STR
2846 - time: 2017-10-29 03:15:00 +03:00
2847 - data: |
2848 - .1.3.6.1.1 = 123
2849 - step:
2850 - type: ZBX_PREPROC_SNMP_WALK_VALUE
2851 - params: |-
2852 - 1.3.6.1.1
2853 - 0
2854 -out:
2855 - return: SUCCEED
2856 - value: 123
2857 ----
2858 -test case: SNMP walk to value - MIB translation 1
2859 -in:
2860 - value:
2861 - value_type: ITEM_VALUE_TYPE_STR
2862 - time: 2017-10-29 03:15:00 +03:00
2863 - data: |
2864 - .1.3.6.1.2.1.2.2.1.2 = 20
2865 - step:
2866 - type: ZBX_PREPROC_SNMP_WALK_VALUE
2867 - params: |-
2868 - IF-MIB::ifDescr
2869 - 1
2870 - netsnmp_required: y
2871 -out:
2872 - return: SUCCEED
2873 - value: 20
2874 ----
2875 -test case: SNMP walk to value - MIB translation 2
2876 -in:
2877 - value:
2878 - value_type: ITEM_VALUE_TYPE_STR
2879 - time: 2017-10-29 03:15:00 +03:00
2880 - data: |
2881 - .1.3.6.1.2.1.1.1 = 20
2882 - step:
2883 - type: ZBX_PREPROC_SNMP_WALK_VALUE
2884 - params: |-
2885 - SNMPv2-MIB::sysDescr
2886 - 0
2887 - netsnmp_required: y
2888 -out:
2889 - return: SUCCEED
2890 - value: 20
2891 ----
2892 -test case: SNMP walk to value - bad param
2893 -in:
2894 - value:
2895 - value_type: ITEM_VALUE_TYPE_STR
2896 - time: 2017-10-29 03:15:00 +03:00
2897 - data: |
2898 - 71 71
2899 - step:
2900 - type: ZBX_PREPROC_SNMP_WALK_VALUE
2901 -out:
2902 - return: FAIL
2903 ----
2904 -test case: SNMP get to value - Hex-STRING - to UTF8
2905 -in:
2906 - value:
2907 - value_type: ITEM_VALUE_TYPE_STR
2908 - time: 2017-10-29 03:15:00 +03:00
2909 - data: |
2910 - 74 65 73 74 20 D1 91
2911 - step:
2912 - type: ZBX_PREPROC_SNMP_GET_VALUE
2913 - params: '1'
2914 -out:
2915 - return: SUCCEED
2916 - value: 'test ё'
2917 ----
2918 -test case: SNMP get to value - Hex-STRING - to UTF8 (null terminated)
2919 -in:
2920 - value:
2921 - value_type: ITEM_VALUE_TYPE_STR
2922 - time: 2017-10-29 03:15:00 +03:00
2923 - data: |
2924 - 74 65 73 74 20 D1 91 00
2925 - step:
2926 - type: ZBX_PREPROC_SNMP_GET_VALUE
2927 - params: '1'
2928 -out:
2929 - return: SUCCEED
2930 - value: 'test ё'
2931 ----
2932 -test case: SNMP get to value - Hex-STRING - to UTF8 - invalid hex string
2933 -in:
2934 - value:
2935 - value_type: ITEM_VALUE_TYPE_STR
2936 - time: 2017-10-29 03:15:00 +03:00
2937 - data: |
2938 - 74 65 X3 74 20 D1 91
2939 - step:
2940 - type: ZBX_PREPROC_SNMP_GET_VALUE
2941 - params: '1'
2942 -out:
2943 - return: FAIL
2944 ----
2945 -test case: SNMP get to value - Hex-STRING - to UTF8 - invalid hex string (2)
2946 -in:
2947 - value:
2948 - value_type: ITEM_VALUE_TYPE_STR
2949 - time: 2017-10-29 03:15:00 +03:00
2950 - data: |
2951 - 74 65 74 20 D1 9
2952 - step:
2953 - type: ZBX_PREPROC_SNMP_GET_VALUE
2954 - params: '1'
2955 -out:
2956 - return: FAIL
2957 ----
2958 -test case: SNMP get to value - Hex-STRING - to UTF8 - invalid sequence
2959 -in:
2960 - value:
2961 - value_type: ITEM_VALUE_TYPE_STR
2962 - time: 2017-10-29 03:15:00 +03:00
2963 - data: |
2964 - 74 65 73 74 20 D1 91 FF
2965 - step:
2966 - type: ZBX_PREPROC_SNMP_GET_VALUE
2967 - params: '1'
2968 -out:
2969 - return: SUCCEED
2970 - value: 'test ё?'
2971 ----
2972 -test case: SNMP get to value - Hex-STRING - to MAC
2973 -in:
2974 - value:
2975 - value_type: ITEM_VALUE_TYPE_STR
2976 - time: 2017-10-29 03:15:00 +03:00
2977 - data: |
2978 - 74 65 73 74 20 D1
2979 - step:
2980 - type: ZBX_PREPROC_SNMP_GET_VALUE
2981 - params: '2'
2982 -out:
2983 - return: SUCCEED
2984 - value: '74:65:73:74:20:D1'
2985 ----
2986 -test case: SNMP get to value - Hex-STRING - to MAC - invalid hex string
2987 -in:
2988 - value:
2989 - value_type: ITEM_VALUE_TYPE_STR
2990 - time: 2017-10-29 03:15:00 +03:00
2991 - data: |
2992 - 74 65 73 74 XX
2993 - step:
2994 - type: ZBX_PREPROC_SNMP_GET_VALUE
2995 - params: '2'
2996 -out:
2997 - return: FAIL
2998 ----
2999 -test case: SNMP get to value - BITS to integer - 1
3000 -in:
3001 - value:
3002 - value_type: ITEM_VALUE_TYPE_STR
3003 - time: 2017-10-29 03:15:00 +03:00
3004 - data: |
3005 - FE EE 15 15
3006 - step:
3007 - type: ZBX_PREPROC_SNMP_GET_VALUE
3008 - params: '3'
3009 -out:
3010 - return: SUCCEED
3011 - value: '353758974'
3012 ----
3013 -test case: SNMP get to value - BITS to integer - 2
3014 -in:
3015 - value:
3016 - value_type: ITEM_VALUE_TYPE_STR
3017 - time: 2017-10-29 03:15:00 +03:00
3018 - data: |
3019 - 01
3020 - step:
3021 - type: ZBX_PREPROC_SNMP_GET_VALUE
3022 - params: '3'
3023 -out:
3024 - return: SUCCEED
3025 - value: '1'
3026 ----
3027 -test case: SNMP get to value - BITS to integer - 3
3028 -in:
3029 - value:
3030 - value_type: ITEM_VALUE_TYPE_STR
3031 - time: 2017-10-29 03:15:00 +03:00
3032 - data: |
3033 - 00 00 00
3034 - step:
3035 - type: ZBX_PREPROC_SNMP_GET_VALUE
3036 - params: '3'
3037 -out:
3038 - return: SUCCEED
3039 - value: '0'
3040 ----
3041 -test case: SNMP get to value - BITS to integer - 4
3042 -in:
3043 - value:
3044 - value_type: ITEM_VALUE_TYPE_STR
3045 - time: 2017-10-29 03:15:00 +03:00
3046 - data: |
3047 - 01 02 03 04 05 06 07 08 09 10
3048 - step:
3049 - type: ZBX_PREPROC_SNMP_GET_VALUE
3050 - params: '3'
3051 -out:
3052 - return: SUCCEED
3053 - value: '578437695752307201'
3054 ----
3055 -test case: SNMP get to value - BITS to integer - 5
3056 -in:
3057 - value:
3058 - value_type: ITEM_VALUE_TYPE_STR
3059 - time: 2017-10-29 03:15:00 +03:00
3060 - data: |
3061 - 01 02 03 04 05 06 07 08
3062 - step:
3063 - type: ZBX_PREPROC_SNMP_GET_VALUE
3064 - params: '3'
3065 -out:
3066 - return: SUCCEED
3067 - value: '578437695752307201'
3068 ----
3069 -test case: SNMP get to value - BITS to integer - 6
3070 -in:
3071 - value:
3072 - value_type: ITEM_VALUE_TYPE_STR
3073 - time: 2017-10-29 03:15:00 +03:00
3074 - data: |
3075 - 01 02 03 04 05 06 07 0
3076 - step:
3077 - type: ZBX_PREPROC_SNMP_GET_VALUE
3078 - params: '3'
3079 -out:
3080 - return: FAIL
3081 ----
3082 -test case: SNMP get to value - BITS to integer - 7
3083 -in:
3084 - value:
3085 - value_type: ITEM_VALUE_TYPE_STR
3086 - time: 2017-10-29 03:15:00 +03:00
3087 - data: |
3088 - .QW 11
3089 - step:
3090 - type: ZBX_PREPROC_SNMP_GET_VALUE
3091 - params: '3'
3092 -out:
3093 - return: FAIL
3094 ----
3095 -test case: SNMP walk to JSON - bad data
3096 -in:
3097 - value:
3098 - value_type: ITEM_VALUE_TYPE_STR
3099 - time: 2017-10-29 03:15:00 +03:00
3100 - data: |
3101 - bad data
3102 - step:
3103 - type: ZBX_PREPROC_SNMP_WALK_TO_JSON
3104 - params: |-
3105 - AAA
3106 - .1.3.6.1
3107 - 0
3108 - BBB
3109 - .1.3.6.2
3110 - 0
3111 -out:
3112 - return: FAIL
3113 ----
3114 -test case: SNMP walk to JSON - no data
3115 -in:
3116 - value:
3117 - value_type: ITEM_VALUE_TYPE_STR
3118 - time: 2017-10-29 03:15:00 +03:00
3119 - data: |
3120 - .1.3.6.1.1 = 123
3121 - step:
3122 - type: ZBX_PREPROC_SNMP_WALK_TO_JSON
3123 - params: |-
3124 - BBB
3125 - .1.3.6.2
3126 - 0
3127 -out:
3128 - return: FAIL
3129 ----
3130 -test case: SNMP walk to JSON - Empty snmp data
3131 -in:
3132 - value:
3133 - value_type: ITEM_VALUE_TYPE_STR
3134 - time: 2017-10-29 03:15:00 +03:00
3135 - data: |
3136 - step:
3137 - type: ZBX_PREPROC_SNMP_WALK_TO_JSON
3138 - params: |-
3139 - MACRO1
3140 - .1.3.6.1
3141 - 0
3142 -out:
3143 - return: SUCCEED
3144 - value: '[]'
3145 ----
3146 -test case: SNMP walk to JSON - Empty snmp data and empty params
3147 -in:
3148 - value:
3149 - value_type: ITEM_VALUE_TYPE_STR
3150 - time: 2017-10-29 03:15:00 +03:00
3151 - data: |
3152 - step:
3153 - type: ZBX_PREPROC_SNMP_WALK_TO_JSON
3154 - params: |-
3155 -out:
3156 - return: SUCCEED
3157 - value: '[]'
3158 ----
3159 -test case: SNMP walk to JSON - STRING type
3160 -in:
3161 - value:
3162 - value_type: ITEM_VALUE_TYPE_STR
3163 - time: 2017-10-29 03:15:00 +03:00
3164 - data: |
3165 - .1.3.6.1.1 = STRING: "xxx"
3166 - .1.3.6.1.2 = STRING: "yyy"
3167 - .1.3.6.2.1 = STRING: "aaa"
3168 - .1.3.6.2.2 = STRING: "bbb"
3169 - step:
3170 - type: ZBX_PREPROC_SNMP_WALK_TO_JSON
3171 - params: |-
3172 - MACRO1
3173 - .1.3.6.1
3174 - 0
3175 - MACRO2
3176 - .1.3.6.2
3177 - 0
3178 -out:
3179 - return: SUCCEED
3180 - value: '[{"{#SNMPINDEX}":"2","MACRO1":"yyy","MACRO2":"bbb"},{"{#SNMPINDEX}":"1","MACRO1":"xxx","MACRO2":"aaa"}]'
3181 ----
3182 -test case: SNMP walk to JSON - Counter/Gauge types
3183 -in:
3184 - value:
3185 - value_type: ITEM_VALUE_TYPE_STR
3186 - time: 2017-10-29 03:15:00 +03:00
3187 - data: |
3188 - .1.3.6.1.1 = Counter32: 32
3189 - .1.3.6.1.2 = Counter64: 64
3190 - .1.3.6.2.1 = Gauge32: 32
3191 - .1.3.6.2.2 = Counter64: 64
3192 - step:
3193 - type: ZBX_PREPROC_SNMP_WALK_TO_JSON
3194 - params: |-
3195 - MACRO1
3196 - .1.3.6.1
3197 - 0
3198 - MACRO2
3199 - .1.3.6.2
3200 - 0
3201 -out:
3202 - return: SUCCEED
3203 - value: '[{"{#SNMPINDEX}":"2","MACRO1":"64","MACRO2":"64"},{"{#SNMPINDEX}":"1","MACRO1":"32","MACRO2":"32"}]'
3204 ----
3205 -test case: SNMP walk to JSON - NULL
3206 -in:
3207 - value:
3208 - value_type: ITEM_VALUE_TYPE_STR
3209 - time: 2017-10-29 03:15:00 +03:00
3210 - data: |
3211 - .1.3.6.1.1 = NULL
3212 - .1.3.6.1.2 = Counter64: 64
3213 - .1.3.6.2.1 = Gauge32: 32
3214 - .1.3.6.2.2 = NULL
3215 - step:
3216 - type: ZBX_PREPROC_SNMP_WALK_TO_JSON
3217 - params: |-
3218 - MACRO1
3219 - .1.3.6.1
3220 - 0
3221 - MACRO2
3222 - .1.3.6.2
3223 - 0
3224 -out:
3225 - return: SUCCEED
3226 - value: '[{"{#SNMPINDEX}":"2","MACRO1":"64","MACRO2":null},{"{#SNMPINDEX}":"1","MACRO1":null,"MACRO2":"32"}]'
3227 ----
3228 -test case: SNMP walk to JSON - Empty type
3229 -in:
3230 - value:
3231 - value_type: ITEM_VALUE_TYPE_STR
3232 - time: 2017-10-29 03:15:00 +03:00
3233 - data: |
3234 - .1.3.6.1.1 = STRING: "aaa"
3235 - .1.3.6.1.2 = Counter64: 64
3236 - .1.3.6.2.1 = ""
3237 - .1.3.6.2.2 = Counter64: 64
3238 - step:
3239 - type: ZBX_PREPROC_SNMP_WALK_TO_JSON
3240 - params: |-
3241 - MACRO1
3242 - .1.3.6.1
3243 - 0
3244 - MACRO2
3245 - .1.3.6.2
3246 - 0
3247 -out:
3248 - return: SUCCEED
3249 - value: '[{"{#SNMPINDEX}":"2","MACRO1":"64","MACRO2":"64"},{"{#SNMPINDEX}":"1","MACRO1":"aaa","MACRO2":""}]'
3250 ----
3251 -test case: SNMP walk to JSON - Empty type
3252 -in:
3253 - value:
3254 - value_type: ITEM_VALUE_TYPE_STR
3255 - time: 2017-10-29 03:15:00 +03:00
3256 - data: |
3257 - .1.3.6.1.1 = 123
3258 - .1.3.6.1.2 = 456
3259 - .1.3.6.2.1 = ""
3260 - .1.3.6.2.2 = Counter64: 64
3261 - step:
3262 - type: ZBX_PREPROC_SNMP_WALK_TO_JSON
3263 - params: |-
3264 - MACRO1
3265 - .1.3.6.1
3266 - 0
3267 - MACRO2
3268 - .1.3.6.2
3269 - 0
3270 -out:
3271 - return: SUCCEED
3272 - value: '[{"{#SNMPINDEX}":"2","MACRO1":"456","MACRO2":"64"},{"{#SNMPINDEX}":"1","MACRO1":"123","MACRO2":""}]'
3273 ----
3274 -test case: SNMP walk to JSON - Integer/Timetick
3275 -in:
3276 - value:
3277 - value_type: ITEM_VALUE_TYPE_STR
3278 - time: 2017-10-29 03:15:00 +03:00
3279 - data: |
3280 - .1.3.6.1.1 = 1000
3281 - .1.3.6.1.2 = 2000
3282 - .1.3.6.2.1 = 3000
3283 - .1.3.6.2.2 = 4000
3284 - step:
3285 - type: ZBX_PREPROC_SNMP_WALK_TO_JSON
3286 - params: |-
3287 - MACRO1
3288 - .1.3.6.1
3289 - 0
3290 - MACRO2
3291 - .1.3.6.2
3292 - 0
3293 -out:
3294 - return: SUCCEED
3295 - value: '[{"{#SNMPINDEX}":"2","MACRO1":"2000","MACRO2":"4000"},{"{#SNMPINDEX}":"1","MACRO1":"1000","MACRO2":"3000"}]'
3296 ----
3297 -test case: SNMP walk to JSON - IpAddress
3298 -in:
3299 - value:
3300 - value_type: ITEM_VALUE_TYPE_STR
3301 - time: 2017-10-29 03:15:00 +03:00
3302 - data: |
3303 - .1.3.6.1.1 = IpAddress: 10.0.0.0
3304 - .1.3.6.1.2 = IpAddress: 172.16.0.0
3305 - .1.3.6.2.1 = IpAddress: 10.255.255.255
3306 - .1.3.6.2.2 = IpAddress: 172.31.255.255
3307 - step:
3308 - type: ZBX_PREPROC_SNMP_WALK_TO_JSON
3309 - params: |-
3310 - MACRO1
3311 - .1.3.6.1
3312 - 0
3313 - MACRO2
3314 - .1.3.6.2
3315 - 0
3316 -out:
3317 - return: SUCCEED
3318 - value: '[{"{#SNMPINDEX}":"2","MACRO1":"172.16.0.0","MACRO2":"172.31.255.255"},{"{#SNMPINDEX}":"1","MACRO1":"10.0.0.0","MACRO2":"10.255.255.255"}]'
3319 ----
3320 -test case: SNMP walk to JSON - STRING type with newline
3321 -in:
3322 - value:
3323 - value_type: ITEM_VALUE_TYPE_STR
3324 - time: 2017-10-29 03:15:00 +03:00
3325 - data: |
3326 - .1.3.6.1.1 = STRING: "x
3327 - xx
3328 - "
3329 - .1.3.6.1.2 = STRING: "y
3330 - yy"
3331 - .1.3.6.2.1 = STRING: "
3332 - aaa"
3333 - .1.3.6.2.2 = STRING: "bbb
3334 - "
3335 - step:
3336 - type: ZBX_PREPROC_SNMP_WALK_TO_JSON
3337 - params: |-
3338 - MACRO1
3339 - .1.3.6.1
3340 - 0
3341 - MACRO2
3342 - .1.3.6.2
3343 - 0
3344 -out:
3345 - return: SUCCEED
3346 - value: '[{"{#SNMPINDEX}":"2","MACRO1":"y\nyy","MACRO2":"bbb\n"},{"{#SNMPINDEX}":"1","MACRO1":"x\nxx\n","MACRO2":"\naaa"}]'
3347 ----
3348 -test case: SNMP walk to JSON - Opaque wrapped type
3349 -in:
3350 - value:
3351 - value_type: ITEM_VALUE_TYPE_STR
3352 - time: 2017-10-29 03:15:00 +03:00
3353 - data: |
3354 - .1.3.6.1.1 = Opaque: Float: 0.110000
3355 - .1.3.6.1.2 = Opaque: STRING: "hello1.2"
3356 - .1.3.6.2.1 = Opaque: Float: 0.210000
3357 - .1.3.6.2.2 = Opaque: Unsigned32: 220000
3358 - step:
3359 - type: ZBX_PREPROC_SNMP_WALK_TO_JSON
3360 - params: |-
3361 - MACRO1
3362 - .1.3.6.1
3363 - 0
3364 - MACRO2
3365 - .1.3.6.2
3366 - 0
3367 -out:
3368 - return: SUCCEED
3369 - value: '[{"{#SNMPINDEX}":"2","MACRO1":"hello1.2","MACRO2":"220000"},{"{#SNMPINDEX}":"1","MACRO1":"0.110000","MACRO2":"0.210000"}]'
3370 ----
3371 -test case: SNMP walk to JSON - OID is wanted without prepending dot - 1
3372 -in:
3373 - value:
3374 - value_type: ITEM_VALUE_TYPE_STR
3375 - time: 2017-10-29 03:15:00 +03:00
3376 - data: |
3377 - .1.3.6.1.1 = 1000
3378 - .1.3.6.1.2 = 2000
3379 - .1.3.6.2.1 = 3000
3380 - .1.3.6.2.2 = 4000
3381 - step:
3382 - type: ZBX_PREPROC_SNMP_WALK_TO_JSON
3383 - params: |-
3384 - MACRO1
3385 - 1.3.6.1
3386 - 0
3387 - MACRO2
3388 - 1.3.6.2
3389 - 0
3390 -out:
3391 - return: SUCCEED
3392 - value: '[{"{#SNMPINDEX}":"2","MACRO1":"2000","MACRO2":"4000"},{"{#SNMPINDEX}":"1","MACRO1":"1000","MACRO2":"3000"}]'
3393 ----
3394 -test case: SNMP walk to JSON - OID is wanted without prepending dot - 2
3395 -in:
3396 - value:
3397 - value_type: ITEM_VALUE_TYPE_STR
3398 - time: 2017-10-29 03:15:00 +03:00
3399 - data: |
3400 - .1.3.6.1.1 = 1000
3401 - .1.3.6.1.2 = 2000
3402 - .1.3.6.2.1 = 3000
3403 - .1.3.6.2.2 = 4000
3404 - step:
3405 - type: ZBX_PREPROC_SNMP_WALK_TO_JSON
3406 - params: |-
3407 - MACRO1
3408 - 1.3.6.1
3409 - 0
3410 - MACRO2
3411 - .1.3.6.2
3412 - 0
3413 -out:
3414 - return: SUCCEED
3415 - value: '[{"{#SNMPINDEX}":"2","MACRO1":"2000","MACRO2":"4000"},{"{#SNMPINDEX}":"1","MACRO1":"1000","MACRO2":"3000"}]'
3416 ----
3417 -test case: SNMP walk to JSON - MIB translation 1
3418 -in:
3419 - value:
3420 - value_type: ITEM_VALUE_TYPE_STR
3421 - time: 2017-10-29 03:15:00 +03:00
3422 - data: |
3423 - .1.3.6.1.2.1.2.2.1.2.1 = 20
3424 - .1.3.6.1.2.1.2.2.1.2.2 = 30
3425 - step:
3426 - type: ZBX_PREPROC_SNMP_WALK_TO_JSON
3427 - params: |-
3428 - IFDESCR
3429 - IF-MIB::ifDescr
3430 - 0
3431 - netsnmp_required: y
3432 -out:
3433 - return: SUCCEED
3434 - value: '[{"{#SNMPINDEX}":"2","IFDESCR":"30"},{"{#SNMPINDEX}":"1","IFDESCR":"20"}]'
3435 ----
3436 -test case: SNMP walk to JSON - No more variables 1
3437 -in:
3438 - value:
3439 - value_type: ITEM_VALUE_TYPE_STR
3440 - time: 2017-10-29 03:15:00 +03:00
3441 - data: |
3442 - .1.3.6.1.1 = 123
3443 - .1.3.6.1.2 = 456
3444 - .1.3.6.2.1 = STRING: "TEST"
3445 - .1.3.6.2.2 = Counter64: 64
3446 - .1.3.6.3.1 = No more variables left in this MIB View (It is past the end of the MIB tree)
3447 - step:
3448 - type: ZBX_PREPROC_SNMP_WALK_TO_JSON
3449 - params: |-
3450 - MACRO1
3451 - .1.3.6.1
3452 - 0
3453 - MACRO2
3454 - .1.3.6.2
3455 - 0
3456 -out:
3457 - return: SUCCEED
3458 - value: '[{"{#SNMPINDEX}":"2","MACRO1":"456","MACRO2":"64"},{"{#SNMPINDEX}":"1","MACRO1":"123","MACRO2":"TEST"}]'
3459 ----
3460 -test case: SNMP walk to JSON - No more variables 2
3461 -in:
3462 - value:
3463 - value_type: ITEM_VALUE_TYPE_STR
3464 - time: 2017-10-29 03:15:00 +03:00
3465 - data: |
3466 - .1.3.6.1.1 = 123
3467 - .1.3.6.1.2 = 456
3468 - .1.3.6.2.1 = STRING: "TEST"
3469 - .1.3.6.2.2 = Counter64: 64
3470 - .1.3.6.3.1 = No more variables left in this MIB View (It is past the end of the MIB tree)
3471 - .1.3.7.1.1 = 123
3472 - .1.3.7.1.2 = 456
3473 - .1.3.7.2.1 = STRING: "TEST"
3474 - .1.3.7.2.2 = Counter64: 64
3475 - step:
3476 - type: ZBX_PREPROC_SNMP_WALK_TO_JSON
3477 - params: |-
3478 - MACRO1
3479 - .1.3.6.1
3480 - 0
3481 - MACRO2
3482 - .1.3.6.2
3483 - 0
3484 -out:
3485 - return: SUCCEED
3486 - value: '[{"{#SNMPINDEX}":"2","MACRO1":"456","MACRO2":"64"},{"{#SNMPINDEX}":"1","MACRO1":"123","MACRO2":"TEST"}]'
3487 ----
3488 -test case: SNMP walk to JSON - Duplicate OIDs
3489 -in:
3490 - value:
3491 - value_type: ITEM_VALUE_TYPE_STR
3492 - time: 2017-10-29 03:15:00 +03:00
3493 - data: |
3494 - .1.3.6.1.1 = 11
3495 - .1.3.6.1.2 = 12
3496 - .1.3.6.2.1 = 21
3497 - .1.3.6.2.2 = 22
3498 - .1.3.6.1.1 = 11
3499 - .1.3.6.1.2 = 12
3500 - step:
3501 - type: ZBX_PREPROC_SNMP_WALK_TO_JSON
3502 - params: |-
3503 - MACRO1
3504 - .1.3.6.1
3505 - 0
3506 - MACRO2
3507 - .1.3.6.2
3508 - 0
3509 -out:
3510 - return: SUCCEED
3511 - value: '[{"{#SNMPINDEX}":"2","MACRO1":"12","MACRO2":"22"},{"{#SNMPINDEX}":"1","MACRO1":"11","MACRO2":"21"}]'
3512 ----
3513 -test case: SNMP walk to JSON - Hex-STRING type - unchanged
3514 -in:
3515 - value:
3516 - value_type: ITEM_VALUE_TYPE_STR
3517 - time: 2017-10-29 03:15:00 +03:00
3518 - data: |
3519 - .1.3.6.1.1 = Hex-STRING: AA BE FF
3520 - .1.3.6.1.2 = Hex-STRING: FF FF
3521 - .1.3.6.2.1 = Hex-STRING: DD CD
3522 - .1.3.6.2.2 = Hex-STRING: 99 A5 F1
3523 - step:
3524 - type: ZBX_PREPROC_SNMP_WALK_TO_JSON
3525 - params: |-
3526 - MACRO1
3527 - .1.3.6.1
3528 - 0
3529 - MACRO2
3530 - .1.3.6.2
3531 - 0
3532 -out:
3533 - return: SUCCEED
3534 - value: '[{"{#SNMPINDEX}":"2","MACRO1":"FF FF","MACRO2":"99 A5 F1"},{"{#SNMPINDEX}":"1","MACRO1":"AA BE FF","MACRO2":"DD CD"}]'
3535 ----
3536 -test case: SNMP walk to JSON - Hex-STRING type - to UTF8
3537 -in:
3538 - value:
3539 - value_type: ITEM_VALUE_TYPE_STR
3540 - time: 2017-10-29 03:15:00 +03:00
3541 - data: |
3542 - .1.3.6.1.1 = Hex-STRING: 74 65 73 74 20 D1 8B
3543 - .1.3.6.1.2 = Hex-STRING: 74 65 73 74 20 D1 84
3544 - .1.3.6.2.1 = Hex-STRING: D1 91
3545 - .1.3.6.2.2 = Hex-STRING: EA 9A 85
3546 - step:
3547 - type: ZBX_PREPROC_SNMP_WALK_TO_JSON
3548 - params: |-
3549 - MACRO1
3550 - .1.3.6.1
3551 - 1
3552 - MACRO2
3553 - .1.3.6.2
3554 - 1
3555 -out:
3556 - return: SUCCEED
3557 - value: '[{"{#SNMPINDEX}":"2","MACRO1":"test ф","MACRO2":"ꚅ"},{"{#SNMPINDEX}":"1","MACRO1":"test ы","MACRO2":"ё"}]'
3558 ----
3559 -test case: SNMP walk to JSON - Hex-STRING type - to UTF8 - invalid sequence
3560 -in:
3561 - value:
3562 - value_type: ITEM_VALUE_TYPE_STR
3563 - time: 2017-10-29 03:15:00 +03:00
3564 - data: |
3565 - .1.3.6.1.1 = Hex-STRING: 74 65 73 74 20 FF FF
3566 - step:
3567 - type: ZBX_PREPROC_SNMP_WALK_TO_JSON
3568 - params: |-
3569 - MACRO1
3570 - .1.3.6.1
3571 - 1
3572 -out:
3573 - return: SUCCEED
3574 - value: '[{"{#SNMPINDEX}":"1","MACRO1":"test ??"}]'
3575 ----
3576 -test case: SNMP walk to JSON - Hex-STRING type - to UTF8 - invalid hex string
3577 -in:
3578 - value:
3579 - value_type: ITEM_VALUE_TYPE_STR
3580 - time: 2017-10-29 03:15:00 +03:00
3581 - data: |
3582 - .1.3.6.1.1 = Hex-STRING: 74 65 73 74 20 D1 8B XX
3583 - step:
3584 - type: ZBX_PREPROC_SNMP_WALK_TO_JSON
3585 - params: |-
3586 - MACRO1
3587 - .1.3.6.1
3588 - 1
3589 -out:
3590 - return: FAIL
3591 ----
3592 -test case: SNMP walk to JSON - Hex-STRING type - to UTF8 - null terminated
3593 -in:
3594 - value:
3595 - value_type: ITEM_VALUE_TYPE_STR
3596 - time: 2017-10-29 03:15:00 +03:00
3597 - data: |
3598 - .1.3.6.1.1 = Hex-STRING: 74 65 73 74 00
3599 - step:
3600 - type: ZBX_PREPROC_SNMP_WALK_TO_JSON
3601 - params: |-
3602 - MACRO1
3603 - .1.3.6.1
3604 - 1
3605 -out:
3606 - return: SUCCEED
3607 - value: '[{"{#SNMPINDEX}":"1","MACRO1":"test"}]'
3608 ----
3609 -test case: SNMP walk to JSON - Hex-STRING type - to MAC
3610 -in:
3611 - value:
3612 - value_type: ITEM_VALUE_TYPE_STR
3613 - time: 2017-10-29 03:15:00 +03:00
3614 - data: |
3615 - .1.3.6.1.1 = Hex-STRING: 74 65 73 74 20 D1
3616 - .1.3.6.1.2 = Hex-STRING: FF FF FF FF FF FF
3617 - .1.3.6.2.1 = Hex-STRING: 00 00 00 00 00 00
3618 - .1.3.6.2.2 = Hex-STRING: EA 9A 85 11 22 33
3619 - step:
3620 - type: ZBX_PREPROC_SNMP_WALK_TO_JSON
3621 - params: |-
3622 - MACRO1
3623 - .1.3.6.1
3624 - 2
3625 - MACRO2
3626 - .1.3.6.2
3627 - 2
3628 -out:
3629 - return: SUCCEED
3630 - value: '[{"{#SNMPINDEX}":"2","MACRO1":"FF:FF:FF:FF:FF:FF","MACRO2":"EA:9A:85:11:22:33"},{"{#SNMPINDEX}":"1","MACRO1":"74:65:73:74:20:D1","MACRO2":"00:00:00:00:00:00"}]'
3631 ----
3632 -test case: SNMP walk to JSON - Hex-STRING type - to MAC - invalid MAC, invalid hex string
3633 -in:
3634 - value:
3635 - value_type: ITEM_VALUE_TYPE_STR
3636 - time: 2017-10-29 03:15:00 +03:00
3637 - data: |
3638 - .1.3.6.1.1 = Hex-STRING: 74 65 73 74 20 XX
3639 - step:
3640 - type: ZBX_PREPROC_SNMP_WALK_TO_JSON
3641 - params: |-
3642 - MACRO1
3643 - .1.3.6.1
3644 - 2
3645 -out:
3646 - return: FAIL
3647 ----
3648 -test case: SNMP walk to JSON - Hex-STRING type - mixed 'treat-as' values
3649 -in:
3650 - value:
3651 - value_type: ITEM_VALUE_TYPE_STR
3652 - time: 2017-10-29 03:15:00 +03:00
3653 - data: |
3654 - .1.3.6.1.1 = Hex-STRING: D1 8B
3655 - .1.3.6.1.2 = Hex-STRING: D1 91
3656 - .1.3.6.2.1 = Hex-STRING: 00 00 00 00 00 00
3657 - .1.3.6.2.2 = Hex-STRING: EA 9A 85 11 22 33
3658 - step:
3659 - type: ZBX_PREPROC_SNMP_WALK_TO_JSON
3660 - params: |-
3661 - MACRO1
3662 - .1.3.6.1
3663 - 1
3664 - MACRO2
3665 - .1.3.6.2
3666 - 0
3667 -out:
3668 - return: SUCCEED
3669 - value: '[{"{#SNMPINDEX}":"2","MACRO1":"ё","MACRO2":"EA 9A 85 11 22 33"},{"{#SNMPINDEX}":"1","MACRO1":"ы","MACRO2":"00 00 00 00 00 00"}]'
3670 ----
3671 -test case: SNMP walk to value - Hex-STRING (multiline)
3672 -in:
3673 - value:
3674 - value_type: ITEM_VALUE_TYPE_STR
3675 - time: 2017-10-29 03:15:00 +03:00
3676 - data: |
3677 - .1.3.6.1.1 = Hex-STRING: 71 71
3678 - AA BB CC
3679 - DD
3680 - .1.3.6.1.2 = Hex-STRING: D1 91
3681 - .1.3.6.2.1 = Hex-STRING: 00 00 00 00 00 00
3682 - .1.3.6.2.2 = Hex-STRING: EA 9A 85 11 22 33
3683 - step:
3684 - type: ZBX_PREPROC_SNMP_WALK_TO_JSON
3685 - params: |-
3686 - MACRO1
3687 - .1.3.6.1
3688 - 0
3689 - MACRO2
3690 - .1.3.6.2
3691 - 0
3692 -out:
3693 - return: SUCCEED
3694 - value: '[{"{#SNMPINDEX}":"2","MACRO1":"D1 91","MACRO2":"EA 9A 85 11 22 33"},{"{#SNMPINDEX}":"1","MACRO1":"71 71 AA BB CC DD","MACRO2":"00 00 00 00 00 00"}]'
3695 ----
3696 -test case: SNMP walk to value - Hex-STRING (multiline) - to MAC
3697 -in:
3698 - value:
3699 - value_type: ITEM_VALUE_TYPE_STR
3700 - time: 2017-10-29 03:15:00 +03:00
3701 - data: |
3702 - .1.3.6.1.1 = Hex-STRING: 71 71
3703 - AA BB CC
3704 - DD
3705 - .1.3.6.1.2 = Hex-STRING: D1 91
3706 - .1.3.6.2.1 = Hex-STRING: 00 00 00 00 00 00
3707 - .1.3.6.2.2 = Hex-STRING: EA 9A 85 11 22 33
3708 - step:
3709 - type: ZBX_PREPROC_SNMP_WALK_TO_JSON
3710 - params: |-
3711 - MACRO1
3712 - .1.3.6.1
3713 - 2
3714 - MACRO2
3715 - .1.3.6.2
3716 - 0
3717 -out:
3718 - return: SUCCEED
3719 - value: '[{"{#SNMPINDEX}":"2","MACRO1":"D1:91","MACRO2":"EA 9A 85 11 22 33"},{"{#SNMPINDEX}":"1","MACRO1":"71:71:AA:BB:CC:DD","MACRO2":"00 00 00 00 00 00"}]'
3720 ----
3721 -test case: SNMP walk to value - Hex-STRING (multiline) - space on the last line
3722 -in:
3723 - value:
3724 - value_type: ITEM_VALUE_TYPE_STR
3725 - time: 2017-10-29 03:15:00 +03:00
3726 - data: |
3727 - .1.3.6.1.1 = Hex-STRING: 71 71
3728 - AA BB CC
3729 - DD
3730 - .1.3.6.1.2 = Hex-STRING: D1 91
3731 - .1.3.6.2.1 = Hex-STRING: 00 00 00 00 00 00
3732 - .1.3.6.2.2 = Hex-STRING: EA 9A 85 11 22 33
3733 - step:
3734 - type: ZBX_PREPROC_SNMP_WALK_TO_JSON
3735 - params: |-
3736 - MACRO1
3737 - .1.3.6.1
3738 - 0
3739 - MACRO2
3740 - .1.3.6.2
3741 - 0
3742 -out:
3743 - return: SUCCEED
3744 - value: '[{"{#SNMPINDEX}":"2","MACRO1":"D1 91","MACRO2":"EA 9A 85 11 22 33"},{"{#SNMPINDEX}":"1","MACRO1":"71 71 AA BB CC DD","MACRO2":"00 00 00 00 00 00"}]'
3745 ----
3746 -test case: SNMP walk to JSON - BITS - unchanged
3747 -in:
3748 - value:
3749 - value_type: ITEM_VALUE_TYPE_STR
3750 - time: 2017-10-29 03:15:00 +03:00
3751 - data: |
3752 - .1.3.6.1.1 = BITS: 09 08 07
3753 - .1.3.6.1.2 = BITS: 55 66
3754 - .1.3.6.2.1 = BITS: 01 02
3755 - .1.3.6.2.2 = BITS: 99 10 20
3756 - step:
3757 - type: ZBX_PREPROC_SNMP_WALK_TO_JSON
3758 - params: |-
3759 - MACRO1
3760 - .1.3.6.1
3761 - 0
3762 - MACRO2
3763 - .1.3.6.2
3764 - 0
3765 -out:
3766 - return: SUCCEED
3767 - value: '[{"{#SNMPINDEX}":"2","MACRO1":"55 66","MACRO2":"99 10 20"},{"{#SNMPINDEX}":"1","MACRO1":"09 08 07","MACRO2":"01 02"}]'
3768 ----
3769 -test case: SNMP walk to JSON - BITS - unchanged
3770 -in:
3771 - value:
3772 - value_type: ITEM_VALUE_TYPE_STR
3773 - time: 2017-10-29 03:15:00 +03:00
3774 - data: |
3775 - .1.3.6.1.1 = BITS: 09 08 07
3776 - .1.3.6.1.2 = BITS: 55 66
3777 - .1.3.6.2.1 = BITS: 01 02
3778 - .1.3.6.2.2 = BITS: 99 10 20
3779 - step:
3780 - type: ZBX_PREPROC_SNMP_WALK_TO_JSON
3781 - params: |-
3782 - MACRO1
3783 - .1.3.6.1
3784 - 3
3785 - MACRO2
3786 - .1.3.6.2
3787 - 3
3788 -out:
3789 - return: SUCCEED
3790 - value: '[{"{#SNMPINDEX}":"2","MACRO1":"26197","MACRO2":"2101401"},{"{#SNMPINDEX}":"1","MACRO1":"460809","MACRO2":"513"}]'
3791 ----
3792 -test case: SNMP walk to JSON - suppressed output of chosen oids that are longer than prefix 1 - prefix without dot
3793 -in:
3794 - value:
3795 - value_type: ITEM_VALUE_TYPE_STR
3796 - time: 2017-10-29 03:15:00 +03:00
3797 - data: |
3798 - .1.3.6.1.1 = STRING: "xxx"
3799 - .1.3.6.1.2 = STRING: "yyy"
3800 - .1.3.6.10.1 = STRING: "no"
3801 - .1.3.6.11.2 = STRING: "no"
3802 - .1.3.6.2.1 = STRING: "aaa"
3803 - .1.3.6.2.2 = STRING: "bbb"
3804 - .1.3.6.20.1 = STRING: "no"
3805 - .1.3.6.20.2 = STRING: "no"
3806 - step:
3807 - type: ZBX_PREPROC_SNMP_WALK_TO_JSON
3808 - params: |-
3809 - MACRO1
3810 - 1.3.6.1
3811 - 0
3812 - MACRO2
3813 - 1.3.6.2
3814 - 0
3815 -out:
3816 - return: SUCCEED
3817 - value: '[{"{#SNMPINDEX}":"2","MACRO1":"yyy","MACRO2":"bbb"},{"{#SNMPINDEX}":"1","MACRO1":"xxx","MACRO2":"aaa"}]'
3818 ----
3819 -test case: SNMP walk to JSON - suppressed output of chosen oids that are longer than prefix 1
3820 -in:
3821 - value:
3822 - value_type: ITEM_VALUE_TYPE_STR
3823 - time: 2017-10-29 03:15:00 +03:00
3824 - data: |
3825 - .1.3.6.1.1 = STRING: "xxx"
3826 - .1.3.6.1.2 = STRING: "yyy"
3827 - .1.3.6.10.1 = STRING: "no"
3828 - .1.3.6.11.2 = STRING: "no"
3829 - .1.3.6.2.1 = STRING: "aaa"
3830 - .1.3.6.2.2 = STRING: "bbb"
3831 - .1.3.6.20.1 = STRING: "no"
3832 - .1.3.6.20.2 = STRING: "no"
3833 - step:
3834 - type: ZBX_PREPROC_SNMP_WALK_TO_JSON
3835 - params: |-
3836 - MACRO1
3837 - .1.3.6.1
3838 - 0
3839 - MACRO2
3840 - .1.3.6.2
3841 - 0
3842 -out:
3843 - return: SUCCEED
3844 - value: '[{"{#SNMPINDEX}":"2","MACRO1":"yyy","MACRO2":"bbb"},{"{#SNMPINDEX}":"1","MACRO1":"xxx","MACRO2":"aaa"}]'
3845 ----
3846 -test case: SNMP walk to JSON - suppressed output of chosen oids that are longer than prefix 2 - prefix without dot
3847 -in:
3848 - value:
3849 - value_type: ITEM_VALUE_TYPE_STR
3850 - time: 2017-10-29 03:15:00 +03:00
3851 - data: |
3852 - .1.3.6.1.1 = STRING: "xxx"
3853 - .1.3.6.1.2 = STRING: "yyy"
3854 - .1.3.6.101.1 = STRING: "no"
3855 - .1.3.6.12123.2 = STRING: "no"
3856 - .1.3.6.2.1 = STRING: "aaa"
3857 - .1.3.6.2.2 = STRING: "bbb"
3858 - .1.3.6.20123.1 = STRING: "no"
3859 - .1.3.6.21123.2 = STRING: "no"
3860 - .1.3.6.3.1 = STRING: "no"
3861 - .1.3.6.333.1 = STRING: "no"
3862 - step:
3863 - type: ZBX_PREPROC_SNMP_WALK_TO_JSON
3864 - params: |-
3865 - MACRO1
3866 - 1.3.6.1
3867 - 0
3868 - MACRO2
3869 - 1.3.6.2
3870 - 0
3871 -out:
3872 - return: SUCCEED
3873 - value: '[{"{#SNMPINDEX}":"2","MACRO1":"yyy","MACRO2":"bbb"},{"{#SNMPINDEX}":"1","MACRO1":"xxx","MACRO2":"aaa"}]'
3874 ----
3875 -test case: SNMP walk to JSON - suppressed output of chosen oids that are longer than prefix 2
3876 -in:
3877 - value:
3878 - value_type: ITEM_VALUE_TYPE_STR
3879 - time: 2017-10-29 03:15:00 +03:00
3880 - data: |
3881 - .1.3.6.1.1 = STRING: "xxx"
3882 - .1.3.6.1.2 = STRING: "yyy"
3883 - .1.3.6.101.1 = STRING: "no"
3884 - .1.3.6.12123.2 = STRING: "no"
3885 - .1.3.6.2.1 = STRING: "aaa"
3886 - .1.3.6.2.2 = STRING: "bbb"
3887 - .1.3.6.20123.1 = STRING: "no"
3888 - .1.3.6.21123.2 = STRING: "no"
3889 - .1.3.6.3.1 = STRING: "no"
3890 - .1.3.6.333.1 = STRING: "no"
3891 - step:
3892 - type: ZBX_PREPROC_SNMP_WALK_TO_JSON
3893 - params: |-
3894 - MACRO1
3895 - .1.3.6.1
3896 - 0
3897 - MACRO2
3898 - .1.3.6.2
3899 - 0
3900 -out:
3901 - return: SUCCEED
3902 - value: '[{"{#SNMPINDEX}":"2","MACRO1":"yyy","MACRO2":"bbb"},{"{#SNMPINDEX}":"1","MACRO1":"xxx","MACRO2":"aaa"}]'
3903 ----
3904 -test case: SNMP walk to JSON - suppressed output of chosen oids that are longer than prefix 3 (dot at the end of param)
3905 -in:
3906 - value:
3907 - value_type: ITEM_VALUE_TYPE_STR
3908 - time: 2017-10-29 03:15:00 +03:00
3909 - data: |
3910 - .1.3.6.1.1 = STRING: "xxx"
3911 - .1.3.6.1.2 = STRING: "yyy"
3912 - .1.3.6.101.1 = STRING: "no"
3913 - .1.3.6.12123.2 = STRING: "no"
3914 - .1.3.6.2.1 = STRING: "aaa"
3915 - .1.3.6.2.2 = STRING: "bbb"
3916 - .1.3.6.20123.1 = STRING: "no"
3917 - .1.3.6.21123.2 = STRING: "no"
3918 - .1.3.6.3.1 = STRING: "no"
3919 - .1.3.6.333.1 = STRING: "no"
3920 - step:
3921 - type: ZBX_PREPROC_SNMP_WALK_TO_JSON
3922 - params: |-
3923 - MACRO1
3924 - .1.3.6.1
3925 - 0
3926 - MACRO2
3927 - .1.3.6.2.
3928 - 0
3929 -out:
3930 - return: SUCCEED
3931 - value: '[{"{#SNMPINDEX}":"2","MACRO1":"yyy","MACRO2":"bbb"},{"{#SNMPINDEX}":"1","MACRO1":"xxx","MACRO2":"aaa"}]'
3932 ----
3933 -test case: SNMP walk to JSON - suppressed output of chosen oids that are longer than prefix 4 (dot at the end of param)
3934 -in:
3935 - value:
3936 - value_type: ITEM_VALUE_TYPE_STR
3937 - time: 2017-10-29 03:15:00 +03:00
3938 - data: |
3939 - .1.3.6.1.1 = STRING: "xxx"
3940 - .1.3.6.1.2 = STRING: "yyy"
3941 - .1.3.6.101.1 = STRING: "no"
3942 - .1.3.6.12123.2 = STRING: "no"
3943 - .1.3.6.2.1 = STRING: "aaa"
3944 - .1.3.6.2.2 = STRING: "bbb"
3945 - .1.3.6.20123.1 = STRING: "no"
3946 - .1.3.6.21123.2 = STRING: "no"
3947 - .1.3.6.3.1 = STRING: "no"
3948 - .1.3.6.333.1 = STRING: "no"
3949 - step:
3950 - type: ZBX_PREPROC_SNMP_WALK_TO_JSON
3951 - params: |-
3952 - MACRO1
3953 - .1.3.6.1
3954 - 0
3955 - MACRO2
3956 - 1.3.6.2.
3957 - 0
3958 -out:
3959 - return: SUCCEED
3960 - value: '[{"{#SNMPINDEX}":"2","MACRO1":"yyy","MACRO2":"bbb"},{"{#SNMPINDEX}":"1","MACRO1":"xxx","MACRO2":"aaa"}]'
3961 ----
3962 -test case: SNMP walk to JSON - oid to be walked is a leaf and index cannot be chosen
3963 -in:
3964 - value:
3965 - value_type: ITEM_VALUE_TYPE_STR
3966 - time: 2017-10-29 03:15:00 +03:00
3967 - data: |
3968 - .1.3.6.1 = STRING: "xxx"
3969 - .1.3.6.2.1 = STRING: "aaa"
3970 - .1.3.6.2.2 = STRING: "bbb"
3971 - step:
3972 - type: ZBX_PREPROC_SNMP_WALK_TO_JSON
3973 - params: |-
3974 - MACRO1
3975 - .1.3.6.1
3976 - 0
3977 - MACRO2
3978 - .1.3.6.2
3979 - 0
3980 -out:
3981 - return: SUCCEED
3982 - value: '[{"{#SNMPINDEX}":"2","MACRO2":"bbb"},{"{#SNMPINDEX}":"1","MACRO2":"aaa"}]'
3983 ----
3984 -test case: SNMP walk to JSON - param with dot at the end - 1
3985 -in:
3986 - value:
3987 - value_type: ITEM_VALUE_TYPE_STR
3988 - time: 2017-10-29 03:15:00 +03:00
3989 - data: |
3990 - .1.3.6.2.1 = STRING: "123"
3991 - .1.3.6.2.2 = STRING: "234"
3992 - step:
3993 - type: ZBX_PREPROC_SNMP_WALK_TO_JSON
3994 - params: |-
3995 - MACRO1
3996 - .1.3.6.2.
3997 - 0
3998 -out:
3999 - return: SUCCEED
4000 - value: '[{"{#SNMPINDEX}":"2","MACRO1":"234"},{"{#SNMPINDEX}":"1","MACRO1":"123"}]'
4001 ----
4002 -test case: SNMP walk to JSON - param with dot at the end - 2
4003 -in:
4004 - value:
4005 - value_type: ITEM_VALUE_TYPE_STR
4006 - time: 2017-10-29 03:15:00 +03:00
4007 - data: |
4008 - .1.3.6.1.1 = STRING: "xxx"
4009 - .1.3.6.1.2 = STRING: "yyy"
4010 - .1.3.6.2.1 = STRING: "aaa"
4011 - .1.3.6.2.2 = STRING: "bbb"
4012 - step:
4013 - type: ZBX_PREPROC_SNMP_WALK_TO_JSON
4014 - params: |-
4015 - MACRO1
4016 - .1.3.6.1.
4017 - 0
4018 - MACRO2
4019 - .1.3.6.2
4020 - 0
4021 -out:
4022 - return: SUCCEED
4023 - value: '[{"{#SNMPINDEX}":"2","MACRO1":"yyy","MACRO2":"bbb"},{"{#SNMPINDEX}":"1","MACRO1":"xxx","MACRO2":"aaa"}]'
4024 ----
4025 -test case: SNMP walk to JSON - param with dot at the end - 3
4026 -in:
4027 - value:
4028 - value_type: ITEM_VALUE_TYPE_STR
4029 - time: 2017-10-29 03:15:00 +03:00
4030 - data: |
4031 - .1.3.6.1.1 = STRING: "xxx"
4032 - .1.3.6.1.2 = STRING: "yyy"
4033 - .1.3.6.2.1 = STRING: "aaa"
4034 - .1.3.6.2.2 = STRING: "bbb"
4035 - step:
4036 - type: ZBX_PREPROC_SNMP_WALK_TO_JSON
4037 - params: |-
4038 - MACRO1
4039 - .1.3.6.1.
4040 - 0
4041 - MACRO2
4042 - 1.3.6.2
4043 - 0
4044 -out:
4045 - return: SUCCEED
4046 - value: '[{"{#SNMPINDEX}":"2","MACRO1":"yyy","MACRO2":"bbb"},{"{#SNMPINDEX}":"1","MACRO1":"xxx","MACRO2":"aaa"}]'
4047 ----
4048 -test case: SNMP walk to JSON - param with dot at the end - 4
4049 -in:
4050 - value:
4051 - value_type: ITEM_VALUE_TYPE_STR
4052 - time: 2017-10-29 03:15:00 +03:00
4053 - data: |
4054 - .1.3.6.1.1 = STRING: "xxx"
4055 - .1.3.6.1.2 = STRING: "yyy"
4056 - .1.3.6.2.1 = STRING: "aaa"
4057 - .1.3.6.2.2 = STRING: "bbb"
4058 - step:
4059 - type: ZBX_PREPROC_SNMP_WALK_TO_JSON
4060 - params: |-
4061 - MACRO1
4062 - 1.3.6.1.
4063 - 0
4064 - MACRO2
4065 - 1.3.6.2
4066 - 0
4067 -out:
4068 - return: SUCCEED
4069 - value: '[{"{#SNMPINDEX}":"2","MACRO1":"yyy","MACRO2":"bbb"},{"{#SNMPINDEX}":"1","MACRO1":"xxx","MACRO2":"aaa"}]'
4070 ----
4071 -test case: SNMP walk to JSON - invalid params - 1
4072 -in:
4073 - value:
4074 - value_type: ITEM_VALUE_TYPE_STR
4075 - time: 2017-10-29 03:15:00 +03:00
4076 - data: |
4077 - .1.3.6.1.1 = STRING: "xxx"
4078 - .1.3.6.1.2 = STRING: "yyy"
4079 - .1.3.6.2.1 = STRING: "aaa"
4080 - .1.3.6.2.2 = STRING: "bbb"
4081 - step:
4082 - type: ZBX_PREPROC_SNMP_WALK_TO_JSON
4083 - params: |-
4084 - MACRO1
4085 -out:
4086 - return: FAIL
4087 ----
4088 -test case: SNMP walk to JSON - invalid params - 2
4089 -in:
4090 - value:
4091 - value_type: ITEM_VALUE_TYPE_STR
4092 - time: 2017-10-29 03:15:00 +03:00
4093 - data: |
4094 - .1.3.6.1.1 = STRING: "xxx"
4095 - .1.3.6.1.2 = STRING: "yyy"
4096 - .1.3.6.2.1 = STRING: "aaa"
4097 - .1.3.6.2.2 = STRING: "bbb"
4098 - step:
4099 - type: ZBX_PREPROC_SNMP_WALK_TO_JSON
4100 - params: |-
4101 - MACRO1
4102 - 1.3.6.1.
4103 -out:
4104 - return: FAIL
4105 ----
4106 -test case: SNMP walk to JSON - invalid params - 3
4107 -in:
4108 - value:
4109 - value_type: ITEM_VALUE_TYPE_STR
4110 - time: 2017-10-29 03:15:00 +03:00
4111 - data: |
4112 - .1.3.6.1.1 = STRING: "xxx"
4113 - .1.3.6.1.2 = STRING: "yyy"
4114 - .1.3.6.2.1 = STRING: "aaa"
4115 - .1.3.6.2.2 = STRING: "bbb"
4116 - step:
4117 - type: ZBX_PREPROC_SNMP_WALK_TO_JSON
4118 - params: |-
4119 - MACRO1
4120 - 1.3.6.1.
4121 - 0
4122 - MACRO2
4123 -out:
4124 - return: FAIL
4125 ----
4126 -test case: SNMP walk to JSON - invalid params - 4
4127 -in:
4128 - value:
4129 - value_type: ITEM_VALUE_TYPE_STR
4130 - time: 2017-10-29 03:15:00 +03:00
4131 - data: |
4132 - .1.3.6.1.1 = STRING: "xxx"
4133 - .1.3.6.1.2 = STRING: "yyy"
4134 - .1.3.6.2.1 = STRING: "aaa"
4135 - .1.3.6.2.2 = STRING: "bbb"
4136 - step:
4137 - type: ZBX_PREPROC_SNMP_WALK_TO_JSON
4138 - params: |-
4139 - MACRO1
4140 - 1.3.6.1.
4141 - 0
4142 - MACRO2
4143 - 1.3.6.2
4144 -out:
4145 - return: FAIL
4146 ----
4147 -test case: SNMP walk to JSON - invalid params - 5
4148 -in:
4149 - value:
4150 - value_type: ITEM_VALUE_TYPE_STR
4151 - time: 2017-10-29 03:15:00 +03:00
4152 - data: |
4153 - .1.3.6.1.1 = STRING: "xxx"
4154 - .1.3.6.1.2 = STRING: "yyy"
4155 - .1.3.6.2.1 = STRING: "aaa"
4156 - .1.3.6.2.2 = STRING: "bbb"
4157 - step:
4158 - type: ZBX_PREPROC_SNMP_WALK_TO_JSON
4159 - params: |-
4160 - MACRO1
4161 - 1.3.6.1.
4162 - 0
4163 - MACRO2
4164 - 1.3.6.2
4165 - 1
4166 - X
4167 -out:
4168 - return: FAIL
4169 ----
4170 -test case: SNMP walk to JSON - wrong type warning
4171 -in:
4172 - value:
4173 - value_type: ITEM_VALUE_TYPE_STR
4174 - time: 2017-10-29 03:15:00 +03:00
4175 - data: |
4176 - .1.3.6.1.1 = STRING: "xxx"
4177 - .1.3.6.1.2 = STRING: "yyy"
4178 - .1.3.6.2.1 = STRING: "aaa"
4179 - .1.3.6.2.2 = Wrong Type (should be Gauge32 or Unsigned32): Counter32: 123
4180 - step:
4181 - type: ZBX_PREPROC_SNMP_WALK_TO_JSON
4182 - params: |-
4183 - MACRO1
4184 - 1.3.6.1.
4185 - 0
4186 - MACRO2
4187 - 1.3.6.2
4188 - 0
4189 -out:
4190 - return: SUCCEED
4191 - value: '[{"{#SNMPINDEX}":"2","MACRO1":"yyy","MACRO2":"123"},{"{#SNMPINDEX}":"1","MACRO1":"xxx","MACRO2":"aaa"}]'
4192 ----
4193 -test case: SNMP walk to JSON - wrong type warning invalid format
4194 -in:
4195 - value:
4196 - value_type: ITEM_VALUE_TYPE_STR
4197 - time: 2017-10-29 03:15:00 +03:00
4198 - data: |
4199 - .1.3.6.1.1 = STRING: "xxx"
4200 - .1.3.6.1.2 = STRING: "yyy"
4201 - .1.3.6.2.1 = STRING: "aaa"
4202 - .1.3.6.2.2 = Wrong Type (should be Gauge32 or Unsigned32): Counter32 123
4203 - .1.3.6.2.3 = STRING: "bbb"
4204 - step:
4205 - type: ZBX_PREPROC_SNMP_WALK_TO_JSON
4206 - params: |-
4207 - MACRO1
4208 - 1.3.6.1.
4209 - 0
4210 - MACRO2
4211 - 1.3.6.2
4212 - 0
4213 -out:
4214 - return: FAIL
4215 ----
4216 -test case: HMAC in JavaScript - Invalid algorithm
4217 -in:
4218 - value:
4219 - value_type: ITEM_VALUE_TYPE_STR
4220 - time: 2017-10-29 03:15:00 +03:00
4221 - data: "k"
4222 - step:
4223 - type: ZBX_PREPROC_SCRIPT
4224 - params: "return hmac('xxx', 'xxx', 'yyy');"
4225 -out:
4226 - return: FAIL
4227 ----
4228 -test case: HMAC in JavaScript - MD5
4229 -in:
4230 - value:
4231 - value_type: ITEM_VALUE_TYPE_STR
4232 - time: 2017-10-29 03:15:00 +03:00
4233 - data: "k"
4234 - step:
4235 - type: ZBX_PREPROC_SCRIPT
4236 - params: "return hmac('md5', 'xxx', 'yyy');"
4237 -out:
4238 - return: SUCCEED
4239 - value: 'd8e8efd954dabfdbfd69398d92ad62d1'
4240 ----
4241 -test case: HMAC in JavaScript - SHA-256
4242 -in:
4243 - value:
4244 - value_type: ITEM_VALUE_TYPE_STR
4245 - time: 2017-10-29 03:15:00 +03:00
4246 - data: "k"
4247 - step:
4248 - type: ZBX_PREPROC_SCRIPT
4249 - params: "return hmac('sha256', 'xxx', 'yyy');"
4250 -out:
4251 - return: SUCCEED
4252 - value: '97f50eed1079c9460f2bb9ddf7dc2e4258471f301ad2c1f14ad50144c4de4303'
4253 ----
4254 -test case: HMAC in JavaScript - Invalid key
4255 -in:
4256 - value:
4257 - value_type: ITEM_VALUE_TYPE_STR
4258 - time: 2017-10-29 03:15:00 +03:00
4259 - data: "k"
4260 - step:
4261 - type: ZBX_PREPROC_SCRIPT
4262 - params: "return hmac('sha256', null, 'yyy');"
4263 -out:
4264 - return: FAIL
4265 ----
4266 -test case: HMAC in JavaScript - Missing key
4267 -in:
4268 - value:
4269 - value_type: ITEM_VALUE_TYPE_STR
4270 - time: 2017-10-29 03:15:00 +03:00
4271 - data: "k"
4272 - step:
4273 - type: ZBX_PREPROC_SCRIPT
4274 - params: "return hmac('sha256');"
4275 -out:
4276 - return: FAIL
4277 ----
4278 -test case: HMAC in JavaScript - Invalid data
4279 -in:
4280 - value:
4281 - value_type: ITEM_VALUE_TYPE_STR
4282 - time: 2017-10-29 03:15:00 +03:00
4283 - data: "k"
4284 - step:
4285 - type: ZBX_PREPROC_SCRIPT
4286 - params: "return hmac('sha256', 'xxx', null);"
4287 -out:
4288 - return: FAIL
4289 ----
4290 -test case: HMAC in JavaScript - Missing data
4291 -in:
4292 - value:
4293 - value_type: ITEM_VALUE_TYPE_STR
4294 - time: 2017-10-29 03:15:00 +03:00
4295 - data: "k"
4296 - step:
4297 - type: ZBX_PREPROC_SCRIPT
4298 - params: "return hmac('sha256', 'xxx');"
4299 -out:
4300 - return: FAIL
4301 ----
4302 -test case: RS256 in JavaScript - single line pkey w/o newlines
4303 -in:
4304 - encryption_required: 1
4305 - value:
4306 - value_type: ITEM_VALUE_TYPE_STR
4307 - time: 2017-10-29 03:15:00 +03:00
4308 - data: "k"
4309 - step:
4310 - type: ZBX_PREPROC_SCRIPT
4311 - params: "return sign('sha256', '-----BEGIN RSA PRIVATE KEY-----MIICXgIBAAKBgQCsDnSry3s269UFKpYvptshcmKZ4ZXxqj6771qr/oEOtRa/o+8JmFtucELQg7HrzRUwxG9htmCuSZ+eOcMQ7vJOrgS+1Ol01P+Ol/PdFFeT5utqzG2NaYSIs/oLAaTkiDUkDBhnG6Ns0xWgJlRomR2W4RI8Os/zKh9eccRaZorjQwIDAQABAoGBAKT300vo9QHqyruCYq/bxx1hpEKw9ejZd/8P9xsUEb/9R4uF7iqAB6JzRszQiKZzY473uWexyy8w7jdyYKL6yB2vCMLdy68nxoIYcjSrk3F/kC47ga7NKCS8T21OFarxhRzxqgF6rg/mfrgeT7nxD1EM0vk6mCSmftbaVZE7M/KRAkEA4FDqvtjen9RrROe2Al23h/1gDqgL9rJ5SzSbwwYmzRA/XYlfH7k9RPGQahzq15IHrdxN04Ov2dVRyybbR0A1awJBAMRb3+P1yKzIkkTGYE2ZWctBcDCnzwwaKKZuWX7OfA+ZPNUm+L6Owg4gztHIAA6DyizXxaHhh8qcL0A6nx7hJ4kCQQCjrces3LiNrcVcMSuZTGMYYuVNrIeWSqLBIrEpvHphlaJ3ET8M+fDgNtgm5dTi2daqoZ9UYLnXXJXjhAXrVsnjAkEAnn8ahcNLLEzp5CHMclaqKGq1yFBry/UBgvXnv3ekpGKou9UtS4OoGAdbafHqOc0fUHsKx9Rn65+OdGXdMuvDIQJAJ+L5i2iRo4TuNpHxMCPN/3bamMlDktIfPlVLbuEcinG1XZ9zs9Y1J738iCF2d56wlApbYGqw4vp9+CdX62LoMw==-----END RSA PRIVATE KEY-----', 'test');"
4312 -out:
4313 - return: SUCCEED
4314 - value: '272f1ef9012b81365bd29f929cfd63dc8a0bafbfaf1fb0bb1a6982d04260ee888573e7591e0e8076a3d8eb9da7a85bd57e61b4504abcfd62815f88cb0c6955fe4bab6d0b4361182182e316a96c83d5ab9ac8e314b5f11eabcefbeb520ec41b04b52323aedcf53b630a232f0a63c66a4cc4411241160ccb30409525c9c51a06d5'
4315 ----
4316 -test case: RS256 in JavaScript - single line pkey with newlines
4317 -in:
4318 - encryption_required: 1
4319 - value:
4320 - value_type: ITEM_VALUE_TYPE_STR
4321 - time: 2017-10-29 03:15:00 +03:00
4322 - data: "a"
4323 - step:
4324 - type: ZBX_PREPROC_SCRIPT
4325 - params: |
4326 - var pkey = '-----BEGIN RSA PRIVATE KEY-----\nMIICXgIBAAKBgQCsDnSry3s269UFKpYvptshcmKZ4ZXxqj6771qr/oEOtRa/o+8J\nmFtucELQg7HrzRUwxG9htmCuSZ+eOcMQ7vJOrgS+1Ol01P+Ol/PdFFeT5utqzG2N\naYSIs/oLAaTkiDUkDBhnG6Ns0xWgJlRomR2W4RI8Os/zKh9eccRaZorjQwIDAQAB\nAoGBAKT300vo9QHqyruCYq/bxx1hpEKw9ejZd/8P9xsUEb/9R4uF7iqAB6JzRszQ\niKZzY473uWexyy8w7jdyYKL6yB2vCMLdy68nxoIYcjSrk3F/kC47ga7NKCS8T21O\nFarxhRzxqgF6rg/mfrgeT7nxD1EM0vk6mCSmftbaVZE7M/KRAkEA4FDqvtjen9Rr\nROe2Al23h/1gDqgL9rJ5SzSbwwYmzRA/XYlfH7k9RPGQahzq15IHrdxN04Ov2dVR\nyybbR0A1awJBAMRb3+P1yKzIkkTGYE2ZWctBcDCnzwwaKKZuWX7OfA+ZPNUm+L6O\nwg4gztHIAA6DyizXxaHhh8qcL0A6nx7hJ4kCQQCjrces3LiNrcVcMSuZTGMYYuVN\nrIeWSqLBIrEpvHphlaJ3ET8M+fDgNtgm5dTi2daqoZ9UYLnXXJXjhAXrVsnjAkEA\nnn8ahcNLLEzp5CHMclaqKGq1yFBry/UBgvXnv3ekpGKou9UtS4OoGAdbafHqOc0f\nUHsKx9Rn65+OdGXdMuvDIQJAJ+L5i2iRo4TuNpHxMCPN/3bamMlDktIfPlVLbuEc\ninG1XZ9zs9Y1J738iCF2d56wlApbYGqw4vp9+CdX62LoMw==\n-----END RSA PRIVATE KEY-----';
4327 - return sign('sha256', pkey, 'test');
4328 -out:
4329 - return: SUCCEED
4330 - value: "272f1ef9012b81365bd29f929cfd63dc8a0bafbfaf1fb0bb1a6982d04260ee888573e7591e0e8076a3d8eb9da7a85bd57e61b4504abcfd62815f88cb0c6955fe4bab6d0b4361182182e316a96c83d5ab9ac8e314b5f11eabcefbeb520ec41b04b52323aedcf53b630a232f0a63c66a4cc4411241160ccb30409525c9c51a06d5"
4331 ----
4332 -test case: RS256 in JavaScript - single line pkey with missing newline before END block
4333 -in:
4334 - encryption_required: 1
4335 - value:
4336 - value_type: ITEM_VALUE_TYPE_STR
4337 - time: 2017-10-29 03:15:00 +03:00
4338 - data: "a"
4339 - step:
4340 - type: ZBX_PREPROC_SCRIPT
4341 - params: |
4342 - var pkey = '-----BEGIN RSA PRIVATE KEY-----\nMIICXgIBAAKBgQCsDnSry3s269UFKpYvptshcmKZ4ZXxqj6771qr/oEOtRa/o+8J\nmFtucELQg7HrzRUwxG9htmCuSZ+eOcMQ7vJOrgS+1Ol01P+Ol/PdFFeT5utqzG2N\naYSIs/oLAaTkiDUkDBhnG6Ns0xWgJlRomR2W4RI8Os/zKh9eccRaZorjQwIDAQAB\nAoGBAKT300vo9QHqyruCYq/bxx1hpEKw9ejZd/8P9xsUEb/9R4uF7iqAB6JzRszQ\niKZzY473uWexyy8w7jdyYKL6yB2vCMLdy68nxoIYcjSrk3F/kC47ga7NKCS8T21O\nFarxhRzxqgF6rg/mfrgeT7nxD1EM0vk6mCSmftbaVZE7M/KRAkEA4FDqvtjen9Rr\nROe2Al23h/1gDqgL9rJ5SzSbwwYmzRA/XYlfH7k9RPGQahzq15IHrdxN04Ov2dVR\nyybbR0A1awJBAMRb3+P1yKzIkkTGYE2ZWctBcDCnzwwaKKZuWX7OfA+ZPNUm+L6O\nwg4gztHIAA6DyizXxaHhh8qcL0A6nx7hJ4kCQQCjrces3LiNrcVcMSuZTGMYYuVN\nrIeWSqLBIrEpvHphlaJ3ET8M+fDgNtgm5dTi2daqoZ9UYLnXXJXjhAXrVsnjAkEA\nnn8ahcNLLEzp5CHMclaqKGq1yFBry/UBgvXnv3ekpGKou9UtS4OoGAdbafHqOc0f\nUHsKx9Rn65+OdGXdMuvDIQJAJ+L5i2iRo4TuNpHxMCPN/3bamMlDktIfPlVLbuEc\ninG1XZ9zs9Y1J738iCF2d56wlApbYGqw4vp9+CdX62LoMw==-----END RSA PRIVATE KEY-----';
4343 - return sign('sha256', pkey, 'test');
4344 -out:
4345 - return: SUCCEED
4346 - value: "272f1ef9012b81365bd29f929cfd63dc8a0bafbfaf1fb0bb1a6982d04260ee888573e7591e0e8076a3d8eb9da7a85bd57e61b4504abcfd62815f88cb0c6955fe4bab6d0b4361182182e316a96c83d5ab9ac8e314b5f11eabcefbeb520ec41b04b52323aedcf53b630a232f0a63c66a4cc4411241160ccb30409525c9c51a06d5"
4347 ----
4348 -test case: RS256 in JavaScript - single line pkey with missing newline before BEGIN block
4349 -in:
4350 - encryption_required: 1
4351 - value:
4352 - value_type: ITEM_VALUE_TYPE_STR
4353 - time: 2017-10-29 03:15:00 +03:00
4354 - data: "a"
4355 - step:
4356 - type: ZBX_PREPROC_SCRIPT
4357 - params: |
4358 - var pkey = '-----BEGIN RSA PRIVATE KEY-----MIICXgIBAAKBgQCsDnSry3s269UFKpYvptshcmKZ4ZXxqj6771qr/oEOtRa/o+8J\nmFtucELQg7HrzRUwxG9htmCuSZ+eOcMQ7vJOrgS+1Ol01P+Ol/PdFFeT5utqzG2N\naYSIs/oLAaTkiDUkDBhnG6Ns0xWgJlRomR2W4RI8Os/zKh9eccRaZorjQwIDAQAB\nAoGBAKT300vo9QHqyruCYq/bxx1hpEKw9ejZd/8P9xsUEb/9R4uF7iqAB6JzRszQ\niKZzY473uWexyy8w7jdyYKL6yB2vCMLdy68nxoIYcjSrk3F/kC47ga7NKCS8T21O\nFarxhRzxqgF6rg/mfrgeT7nxD1EM0vk6mCSmftbaVZE7M/KRAkEA4FDqvtjen9Rr\nROe2Al23h/1gDqgL9rJ5SzSbwwYmzRA/XYlfH7k9RPGQahzq15IHrdxN04Ov2dVR\nyybbR0A1awJBAMRb3+P1yKzIkkTGYE2ZWctBcDCnzwwaKKZuWX7OfA+ZPNUm+L6O\nwg4gztHIAA6DyizXxaHhh8qcL0A6nx7hJ4kCQQCjrces3LiNrcVcMSuZTGMYYuVN\nrIeWSqLBIrEpvHphlaJ3ET8M+fDgNtgm5dTi2daqoZ9UYLnXXJXjhAXrVsnjAkEA\nnn8ahcNLLEzp5CHMclaqKGq1yFBry/UBgvXnv3ekpGKou9UtS4OoGAdbafHqOc0f\nUHsKx9Rn65+OdGXdMuvDIQJAJ+L5i2iRo4TuNpHxMCPN/3bamMlDktIfPlVLbuEc\ninG1XZ9zs9Y1J738iCF2d56wlApbYGqw4vp9+CdX62LoMw==\n-----END RSA PRIVATE KEY-----';
4359 - return sign('sha256', pkey, 'test');
4360 -out:
4361 - return: SUCCEED
4362 - value: "272f1ef9012b81365bd29f929cfd63dc8a0bafbfaf1fb0bb1a6982d04260ee888573e7591e0e8076a3d8eb9da7a85bd57e61b4504abcfd62815f88cb0c6955fe4bab6d0b4361182182e316a96c83d5ab9ac8e314b5f11eabcefbeb520ec41b04b52323aedcf53b630a232f0a63c66a4cc4411241160ccb30409525c9c51a06d5"
4363 ----
4364 -test case: RS256 in JavaScript - spaces instead of newlines
4365 -in:
4366 - encryption_required: 1
4367 - value:
4368 - value_type: ITEM_VALUE_TYPE_STR
4369 - time: 2017-10-29 03:15:00 +03:00
4370 - data: "a"
4371 - step:
4372 - type: ZBX_PREPROC_SCRIPT
4373 - params: |
4374 - var pkey = '-----BEGIN RSA PRIVATE KEY----- MIICXgIBAAKBgQCsDnSry3s269UFKpYvptshcmKZ4ZXxqj6771qr/oEOtRa/o+8J mFtucELQg7HrzRUwxG9htmCuSZ+eOcMQ7vJOrgS+1Ol01P+Ol/PdFFeT5utqzG2N aYSIs/oLAaTkiDUkDBhnG6Ns0xWgJlRomR2W4RI8Os/zKh9eccRaZorjQwIDAQAB AoGBAKT300vo9QHqyruCYq/bxx1hpEKw9ejZd/8P9xsUEb/9R4uF7iqAB6JzRszQ iKZzY473uWexyy8w7jdyYKL6yB2vCMLdy68nxoIYcjSrk3F/kC47ga7NKCS8T21O FarxhRzxqgF6rg/mfrgeT7nxD1EM0vk6mCSmftbaVZE7M/KRAkEA4FDqvtjen9Rr ROe2Al23h/1gDqgL9rJ5SzSbwwYmzRA/XYlfH7k9RPGQahzq15IHrdxN04Ov2dVR yybbR0A1awJBAMRb3+P1yKzIkkTGYE2ZWctBcDCnzwwaKKZuWX7OfA+ZPNUm+L6O wg4gztHIAA6DyizXxaHhh8qcL0A6nx7hJ4kCQQCjrces3LiNrcVcMSuZTGMYYuVN rIeWSqLBIrEpvHphlaJ3ET8M+fDgNtgm5dTi2daqoZ9UYLnXXJXjhAXrVsnjAkEA nn8ahcNLLEzp5CHMclaqKGq1yFBry/UBgvXnv3ekpGKou9UtS4OoGAdbafHqOc0f UHsKx9Rn65+OdGXdMuvDIQJAJ+L5i2iRo4TuNpHxMCPN/3bamMlDktIfPlVLbuEc inG1XZ9zs9Y1J738iCF2d56wlApbYGqw4vp9+CdX62LoMw== -----END RSA PRIVATE KEY-----';
4375 - return sign('sha256', pkey, 'test');
4376 -out:
4377 - return: SUCCEED
4378 - value: "272f1ef9012b81365bd29f929cfd63dc8a0bafbfaf1fb0bb1a6982d04260ee888573e7591e0e8076a3d8eb9da7a85bd57e61b4504abcfd62815f88cb0c6955fe4bab6d0b4361182182e316a96c83d5ab9ac8e314b5f11eabcefbeb520ec41b04b52323aedcf53b630a232f0a63c66a4cc4411241160ccb30409525c9c51a06d5"
4379 ----
4380 -test case: RS256 in JavaScript - PKCS#8
4381 -in:
4382 - encryption_required: 1
4383 - value:
4384 - value_type: ITEM_VALUE_TYPE_STR
4385 - time: 2017-10-29 03:15:00 +03:00
4386 - data: "a"
4387 - step:
4388 - type: ZBX_PREPROC_SCRIPT
4389 - params: |
4390 - var pkey = '-----BEGIN PRIVATE KEY-----MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBAKwOdKvLezbr1QUqli+m2yFyYpnhlfGqPrvvWqv+gQ61Fr+j7wmYW25wQtCDsevNFTDEb2G2YK5Jn545wxDu8k6uBL7U6XTU/46X890UV5Pm62rMbY1phIiz+gsBpOSINSQMGGcbo2zTFaAmVGiZHZbhEjw6z/MqH15xxFpmiuNDAgMBAAECgYEApPfTS+j1AerKu4Jir9vHHWGkQrD16Nl3/w/3GxQRv/1Hi4XuKoAHonNGzNCIpnNjjve5Z7HLLzDuN3JgovrIHa8Iwt3LryfGghhyNKuTcX+QLjuBrs0oJLxPbU4VqvGFHPGqAXquD+Z+uB5PufEPUQzS+TqYJKZ+1tpVkTsz8pECQQDgUOq+2N6f1GtE57YCXbeH/WAOqAv2snlLNJvDBibNED9diV8fuT1E8ZBqHOrXkget3E3Tg6/Z1VHLJttHQDVrAkEAxFvf4/XIrMiSRMZgTZlZy0FwMKfPDBoopm5Zfs58D5k81Sb4vo7CDiDO0cgADoPKLNfFoeGHypwvQDqfHuEniQJBAKOtx6zcuI2txVwxK5lMYxhi5U2sh5ZKosEisSm8emGVoncRPwz58OA22Cbl1OLZ1qqhn1RguddcleOEBetWyeMCQQCefxqFw0ssTOnkIcxyVqooarXIUGvL9QGC9ee/d6SkYqi71S1Lg6gYB1tp8eo5zR9QewrH1Gfrn450Zd0y68MhAkAn4vmLaJGjhO42kfEwI83/dtqYyUOS0h8+VUtu4RyKcbVdn3Oz1jUnvfyIIXZ3nrCUCltgarDi+n34J1frYugz-----END PRIVATE KEY-----';
4391 - return sign('sha256', pkey, 'test');
4392 -out:
4393 - return: SUCCEED
4394 - value: "272f1ef9012b81365bd29f929cfd63dc8a0bafbfaf1fb0bb1a6982d04260ee888573e7591e0e8076a3d8eb9da7a85bd57e61b4504abcfd62815f88cb0c6955fe4bab6d0b4361182182e316a96c83d5ab9ac8e314b5f11eabcefbeb520ec41b04b52323aedcf53b630a232f0a63c66a4cc4411241160ccb30409525c9c51a06d5"
4395 ----
4396 -test case: RS256 in JavaScript - data as Uint8Array
4397 -in:
4398 - encryption_required: 1
4399 - value:
4400 - value_type: ITEM_VALUE_TYPE_STR
4401 - time: 2017-10-29 03:15:00 +03:00
4402 - data: "a"
4403 - step:
4404 - type: ZBX_PREPROC_SCRIPT
4405 - params: |
4406 - var pkey = '-----BEGIN PRIVATE KEY-----MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBAKwOdKvLezbr1QUqli+m2yFyYpnhlfGqPrvvWqv+gQ61Fr+j7wmYW25wQtCDsevNFTDEb2G2YK5Jn545wxDu8k6uBL7U6XTU/46X890UV5Pm62rMbY1phIiz+gsBpOSINSQMGGcbo2zTFaAmVGiZHZbhEjw6z/MqH15xxFpmiuNDAgMBAAECgYEApPfTS+j1AerKu4Jir9vHHWGkQrD16Nl3/w/3GxQRv/1Hi4XuKoAHonNGzNCIpnNjjve5Z7HLLzDuN3JgovrIHa8Iwt3LryfGghhyNKuTcX+QLjuBrs0oJLxPbU4VqvGFHPGqAXquD+Z+uB5PufEPUQzS+TqYJKZ+1tpVkTsz8pECQQDgUOq+2N6f1GtE57YCXbeH/WAOqAv2snlLNJvDBibNED9diV8fuT1E8ZBqHOrXkget3E3Tg6/Z1VHLJttHQDVrAkEAxFvf4/XIrMiSRMZgTZlZy0FwMKfPDBoopm5Zfs58D5k81Sb4vo7CDiDO0cgADoPKLNfFoeGHypwvQDqfHuEniQJBAKOtx6zcuI2txVwxK5lMYxhi5U2sh5ZKosEisSm8emGVoncRPwz58OA22Cbl1OLZ1qqhn1RguddcleOEBetWyeMCQQCefxqFw0ssTOnkIcxyVqooarXIUGvL9QGC9ee/d6SkYqi71S1Lg6gYB1tp8eo5zR9QewrH1Gfrn450Zd0y68MhAkAn4vmLaJGjhO42kfEwI83/dtqYyUOS0h8+VUtu4RyKcbVdn3Oz1jUnvfyIIXZ3nrCUCltgarDi+n34J1frYugz-----END PRIVATE KEY-----';
4407 - return sign('sha256', pkey, new Uint8Array([0x74, 0x65, 0x73, 0x74]));
4408 -out:
4409 - return: SUCCEED
4410 - value: "272f1ef9012b81365bd29f929cfd63dc8a0bafbfaf1fb0bb1a6982d04260ee888573e7591e0e8076a3d8eb9da7a85bd57e61b4504abcfd62815f88cb0c6955fe4bab6d0b4361182182e316a96c83d5ab9ac8e314b5f11eabcefbeb520ec41b04b52323aedcf53b630a232f0a63c66a4cc4411241160ccb30409525c9c51a06d5"
4411 ----
4412 -test case: RS256 in JavaScript - Invalid algorithm
4413 -in:
4414 - encryption_required: 1
4415 - value:
4416 - value_type: ITEM_VALUE_TYPE_STR
4417 - time: 2017-10-29 03:15:00 +03:00
4418 - data: "a"
4419 - step:
4420 - type: ZBX_PREPROC_SCRIPT
4421 - params: "return sign('q', pkey, 'test');"
4422 -out:
4423 - return: FAIL
4424 ----
4425 -test case: RS256 in JavaScript - Invalid pkey
4426 -in:
4427 - encryption_required: 1
4428 - value:
4429 - value_type: ITEM_VALUE_TYPE_STR
4430 - time: 2017-10-29 03:15:00 +03:00
4431 - data: "a"
4432 - step:
4433 - type: ZBX_PREPROC_SCRIPT
4434 - params: "return sign('q', 'invalid', 'test');"
4435 -out:
4436 - return: FAIL
4437 ----
4438 -test case: RS256 in JavaScript - Missing pkey
4439 -in:
4440 - encryption_required: 1
4441 - value:
4442 - value_type: ITEM_VALUE_TYPE_STR
4443 - time: 2017-10-29 03:15:00 +03:00
4444 - data: "a"
4445 - step:
4446 - type: ZBX_PREPROC_SCRIPT
4447 - params: "return sign('q');"
4448 -out:
4449 - return: FAIL
4450 ----
4451 -test case: RS256 in JavaScript - Invalid data
4452 -in:
4453 - encryption_required: 1
4454 - value:
4455 - value_type: ITEM_VALUE_TYPE_STR
4456 - time: 2017-10-29 03:15:00 +03:00
4457 - data: "a"
4458 - step:
4459 - type: ZBX_PREPROC_SCRIPT
4460 - params: "return sign('q', 'invalid', '');"
4461 -out:
4462 - return: FAIL
4463 ----
4464 -test case: RS256 in JavaScript - Missing data
4465 -in:
4466 - encryption_required: 1
4467 - value:
4468 - value_type: ITEM_VALUE_TYPE_STR
4469 - time: 2017-10-29 03:15:00 +03:00
4470 - data: "a"
4471 - step:
4472 - type: ZBX_PREPROC_SCRIPT
4473 - params: "return sign('q', 'pkey');"
4474 -out:
4475 - return: FAIL
4476 ----
4477 -test case: Check for not supported without parameter (false)
4478 -in:
4479 - value:
4480 - value_type: ITEM_VALUE_TYPE_STR
4481 - time: 2017-10-29 03:15:00 +03:00
4482 - data: "no error"
4483 - step:
4484 - type: ZBX_PREPROC_VALIDATE_NOT_SUPPORTED
4485 - params: ""
4486 - error_handler: ZBX_PREPROC_FAIL_SET_VALUE
4487 - error_handler_params: error_value
4488 -out:
4489 - return: SUCCEED
4490 - value: "no error"
4491 ----
4492 -test case: Check for not supported without parameter
4493 -in:
4494 - error:
4495 - time: 2017-10-29 03:15:00 +03:00
4496 - data: "error any"
4497 - step:
4498 - type: ZBX_PREPROC_VALIDATE_NOT_SUPPORTED
4499 - params: ""
4500 - error_handler: ZBX_PREPROC_FAIL_SET_VALUE
4501 - error_handler_params: error_value
4502 -out:
4503 - return: SUCCEED
4504 - value: error_value
4505 ----
4506 -test case: Check for not supported with parameter (false)
4507 -in:
4508 - error:
4509 - time: 2017-10-29 03:15:00 +03:00
4510 - data: "error any"
4511 - step:
4512 - type: ZBX_PREPROC_VALIDATE_NOT_SUPPORTED
4513 - params: "0\n123"
4514 - error_handler: ZBX_PREPROC_FAIL_SET_ERROR
4515 - error_handler_params: custom error
4516 -out:
4517 - return: SUCCEED
4518 - value: "error any"
4519 ----
4520 -test case: Check for not supported with parameter
4521 -in:
4522 - error:
4523 - time: 2017-10-29 03:15:00 +03:00
4524 - data: "error 123"
4525 - step:
4526 - type: ZBX_PREPROC_VALIDATE_NOT_SUPPORTED
4527 - params: "0\n123"
4528 - error_handler: ZBX_PREPROC_FAIL_SET_ERROR
4529 - error_handler_params: custom error
4530 -out:
4531 - return: FAIL
4532 - error: custom error
4533 ----
4534 -test case: Check for not supported with parameter and capturing group
4535 -in:
4536 - error:
4537 - time: 2017-10-29 03:15:00 +03:00
4538 - data: "error 123"
4539 - step:
4540 - type: ZBX_PREPROC_VALIDATE_NOT_SUPPORTED
4541 - params: "0\n123"
4542 - error_handler: ZBX_PREPROC_FAIL_SET_ERROR
4543 - error_handler_params: custom error \0
4544 -out:
4545 - return: FAIL
4546 - error: custom error 123
4547 ----
4548 -test case: btoa in JavaScript - binary
4549 -in:
4550 - value:
4551 - value_type: ITEM_VALUE_TYPE_STR
4552 - time: 2017-10-29 03:15:00 +03:00
4553 - data: "k"
4554 - step:
4555 - type: ZBX_PREPROC_SCRIPT
4556 - params: "return btoa(new Uint8Array([49, 0, 51]));"
4557 -out:
4558 - return: SUCCEED
4559 - value: 'MQAz'
4560 ----
4561 -test case: Crash by accessing internal object pointer in JavaScript
4562 -in:
4563 - value:
4564 - value_type: ITEM_VALUE_TYPE_STR
4565 - time: 2017-10-29 03:15:00 +03:00
4566 - data: "a"
4567 - step:
4568 - type: ZBX_PREPROC_SCRIPT
4569 - params: |
4570 - var http = new HttpRequest();
4571 - var fake = {};
4572 - fake[atob('//9k')] = 'xyz';
4573 - fake.get = http.get;
4574 - fake.get('http://localhost');
4575 - return 0;
4576 - script_uses_curl: 1
4577 -out:
4578 - return: FAIL
4579 ----
4580 -test case: Memory leak with global variable and exception
4581 -in:
4582 - value:
4583 - value_type: ITEM_VALUE_TYPE_STR
4584 - time: 2017-10-29 03:15:00 +03:00
4585 - data: "a"
4586 - step:
4587 - type: ZBX_PREPROC_SCRIPT
4588 - params: |
4589 - req = new HttpRequest();
4590 - throw 0;
4591 - script_uses_curl: 1
4592 -out:
4593 - return: FAIL
4594 ----
4595 -test case: Memory leak with finalizer
4596 -in:
4597 - value:
4598 - value_type: ITEM_VALUE_TYPE_STR
4599 - time: 2017-10-29 03:15:00 +03:00
4600 - data: "a"
4601 - step:
4602 - type: ZBX_PREPROC_SCRIPT
4603 - params: |
4604 - var req = new HttpRequest();
4605 -
4606 - req[atob('gkZpbmFsaXplcg==')] = null;
4607 -
4608 - return 0;
4609 - script_uses_curl: 1
4610 -out:
4611 - value: "0"
4612 - return: SUCCEED
4613 ----
4614 - test case: SNMP walk to value - duplicate oids
4615 - in:
4616 - value:
4617 - value_type: ITEM_VALUE_TYPE_STR
4618 - time: 2017-10-29 03:15:00 +03:00
4619 - data: |
4620 - .1.3.6.1.1 = STRING: "AAA"
4621 - .1.3.6.1.1 = STRING: "AAA"
4622 - step:
4623 - type: ZBX_PREPROC_SNMP_WALK_VALUE
4624 - params: |-
4625 - .1.3.6.1.1
4626 - 0
4627 - out:
4628 - return: SUCCEED
4629 - value: "AAA"
4630 -...
src/go/plugin/scripts.d/pkg/zabbixpreproc/testloader_test.go deleted
-460
@@ -1,460 +0,0 @@
1 -package zabbixpreproc
2 -
3 -import (
4 - "fmt"
5 - "os"
6 - "strings"
7 - "time"
8 -
9 - "gopkg.in/yaml.v3"
10 -)
11 -
12 -// valueInput represents the input value in a test case
13 -type valueInput struct {
14 - ValueType string `yaml:"value_type"`
15 - Time string `yaml:"time"`
16 - Data interface{} `yaml:"data"`
17 -}
18 -
19 -// stepInput represents the preprocessing step in a test case
20 -type stepInput struct {
21 - Type string `yaml:"type"`
22 - Params string `yaml:"params"`
23 - ErrorHandler string `yaml:"error_handler"`
24 - ErrorHandlerParam string `yaml:"error_handler_params"`
25 -}
26 -
27 -// testOutput represents the expected output in a test case
28 -type testOutput struct {
29 - Return string `yaml:"return"`
30 - Value interface{} `yaml:"value"`
31 - Error string `yaml:"error"`
32 -}
33 -
34 -// testInput represents the input section of a test case
35 -type testInputSection struct {
36 - Value valueInput `yaml:"value"`
37 - Error *valueInput `yaml:"error"` // Input is an error instead of a value
38 - HistoryValue *valueInput `yaml:"history_value"` // Deprecated field
39 - History *valueInput `yaml:"history"` // Actual field used in tests
40 - Step stepInput `yaml:"step"`
41 -}
42 -
43 -// TestCase represents a single Zabbix preprocessing test case.
44 -type TestCase struct {
45 - Name string
46 - In testInputSection
47 - Out testOutput
48 -}
49 -
50 -// LoadTestCases loads Zabbix YAML test cases from file.
51 -func LoadTestCases(filename string) ([]TestCase, error) {
52 - data, err := os.ReadFile(filename)
53 - if err != nil {
54 - return nil, fmt.Errorf("failed to read test file: %w", err)
55 - }
56 -
57 - // Split YAML documents - split on "---" at start of line
58 - docStr := string(data)
59 - // Handle both with and without leading ---
60 - if strings.HasPrefix(docStr, "---") {
61 - docStr = docStr[3:] // Remove leading ---
62 - }
63 -
64 - docs := strings.Split(docStr, "\n---")
65 - var testCases []TestCase
66 -
67 - for _, doc := range docs {
68 - doc = strings.TrimSpace(doc)
69 - if doc == "" {
70 - continue
71 - }
72 -
73 - var rawTC struct {
74 - TestCase string `yaml:"test case"`
75 - In testInputSection `yaml:"in"`
76 - Out testOutput `yaml:"out"`
77 - }
78 -
79 - if err := yaml.Unmarshal([]byte(doc), &rawTC); err != nil {
80 - // Log warning for unparseable YAML (don't silently skip)
81 - fmt.Fprintf(os.Stderr, "WARNING: Failed to parse YAML document in test file: %v\n", err)
82 - continue
83 - }
84 -
85 - if rawTC.TestCase == "" {
86 - continue
87 - }
88 -
89 - testCases = append(testCases, TestCase{
90 - Name: rawTC.TestCase,
91 - In: rawTC.In,
92 - Out: rawTC.Out,
93 - })
94 - }
95 -
96 - return testCases, nil
97 -}
98 -
99 -// LoadXPathTestCases loads XPath-specific test cases and converts to standard format
100 -func LoadXPathTestCases(filename string) ([]TestCase, error) {
101 - data, err := os.ReadFile(filename)
102 - if err != nil {
103 - return nil, fmt.Errorf("failed to read XPath test file: %w", err)
104 - }
105 -
106 - // Split YAML documents
107 - docStr := string(data)
108 - if strings.HasPrefix(docStr, "---") {
109 - docStr = docStr[3:]
110 - }
111 -
112 - docs := strings.Split(docStr, "\n---")
113 - var testCases []TestCase
114 -
115 - for _, doc := range docs {
116 - doc = strings.TrimSpace(doc)
117 - if doc == "" {
118 - continue
119 - }
120 -
121 - var rawTC struct {
122 - TestCase string `yaml:"test case"`
123 - In struct {
124 - XML string `yaml:"xml"`
125 - XPath string `yaml:"xpath"`
126 - } `yaml:"in"`
127 - Out struct {
128 - Result string `yaml:"result"`
129 - Return string `yaml:"return"`
130 - } `yaml:"out"`
131 - }
132 -
133 - if err := yaml.Unmarshal([]byte(doc), &rawTC); err != nil {
134 - // Log warning for unparseable YAML (don't silently skip)
135 - fmt.Fprintf(os.Stderr, "WARNING: Failed to parse XPath test YAML: %v\n", err)
136 - continue
137 - }
138 -
139 - if rawTC.TestCase == "" {
140 - continue
141 - }
142 -
143 - // Convert to standard format
144 - tc := TestCase{
145 - Name: rawTC.TestCase,
146 - In: testInputSection{
147 - Value: valueInput{
148 - ValueType: "ITEM_VALUE_TYPE_STR",
149 - Data: rawTC.In.XML,
150 - },
151 - Step: stepInput{
152 - Type: "ZBX_PREPROC_XPATH",
153 - Params: rawTC.In.XPath,
154 - },
155 - },
156 - Out: testOutput{
157 - Return: rawTC.Out.Return,
158 - Value: rawTC.Out.Result,
159 - },
160 - }
161 -
162 - testCases = append(testCases, tc)
163 - }
164 -
165 - return testCases, nil
166 -}
167 -
168 -// LoadCSVTestCases loads CSV-specific test cases and converts to standard format
169 -func LoadCSVTestCases(filename string) ([]TestCase, error) {
170 - data, err := os.ReadFile(filename)
171 - if err != nil {
172 - return nil, fmt.Errorf("failed to read CSV test file: %w", err)
173 - }
174 -
175 - // Split YAML documents
176 - docStr := string(data)
177 - if strings.HasPrefix(docStr, "---") {
178 - docStr = docStr[3:]
179 - }
180 -
181 - docs := strings.Split(docStr, "\n---")
182 - var testCases []TestCase
183 -
184 - for _, doc := range docs {
185 - doc = strings.TrimSpace(doc)
186 - if doc == "" {
187 - continue
188 - }
189 -
190 - var rawTC struct {
191 - TestCase string `yaml:"test case"`
192 - In struct {
193 - CSV string `yaml:"csv"`
194 - Params string `yaml:"params"`
195 - } `yaml:"in"`
196 - Out struct {
197 - Result string `yaml:"result"`
198 - Return string `yaml:"return"`
199 - } `yaml:"out"`
200 - }
201 -
202 - if err := yaml.Unmarshal([]byte(doc), &rawTC); err != nil {
203 - // Log warning for unparseable YAML (don't silently skip)
204 - fmt.Fprintf(os.Stderr, "WARNING: Failed to parse CSV test YAML: %v\n", err)
205 - continue
206 - }
207 -
208 - if rawTC.TestCase == "" {
209 - continue
210 - }
211 -
212 - // Convert to standard format
213 - tc := TestCase{
214 - Name: rawTC.TestCase,
215 - In: testInputSection{
216 - Value: valueInput{
217 - ValueType: "ITEM_VALUE_TYPE_STR",
218 - Data: rawTC.In.CSV,
219 - },
220 - Step: stepInput{
221 - Type: "ZBX_PREPROC_CSV_TO_JSON",
222 - Params: rawTC.In.Params,
223 - },
224 - },
225 - Out: testOutput{
226 - Return: rawTC.Out.Return,
227 - Value: rawTC.Out.Result,
228 - },
229 - }
230 -
231 - testCases = append(testCases, tc)
232 - }
233 -
234 - return testCases, nil
235 -}
236 -
237 -// LoadAllTestCases loads all official Zabbix test suites
238 -func LoadAllTestCases() ([]TestCase, error) {
239 - var allTests []TestCase
240 -
241 - // Load main test suite
242 - mainTests, err := LoadTestCases("testdata/zbx_item_preproc.yaml")
243 - if err != nil {
244 - return nil, fmt.Errorf("failed to load main test suite: %w", err)
245 - }
246 - allTests = append(allTests, mainTests...)
247 -
248 - // Load XPath test suite
249 - xpathTests, err := LoadXPathTestCases("testdata/item_preproc_xpath.yaml")
250 - if err != nil {
251 - return nil, fmt.Errorf("failed to load XPath test suite: %w", err)
252 - }
253 - allTests = append(allTests, xpathTests...)
254 -
255 - // Load CSV test suite
256 - csvTests, err := LoadCSVTestCases("testdata/item_preproc_csv_to_json.yaml")
257 - if err != nil {
258 - return nil, fmt.Errorf("failed to load CSV test suite: %w", err)
259 - }
260 - allTests = append(allTests, csvTests...)
261 -
262 - return allTests, nil
263 -}
264 -
265 -// StepTypeFromString converts Zabbix step type string to StepType.
266 -func StepTypeFromString(s string) (StepType, error) {
267 - switch s {
268 - case "ZBX_PREPROC_MULTIPLIER":
269 - return StepTypeMultiplier, nil
270 - case "ZBX_PREPROC_TRIM":
271 - return StepTypeTrim, nil
272 - case "ZBX_PREPROC_RTRIM":
273 - return StepTypeRTrim, nil
274 - case "ZBX_PREPROC_LTRIM":
275 - return StepTypeLTrim, nil
276 - case "ZBX_PREPROC_REGSUB":
277 - return StepTypeRegexSubstitution, nil
278 - case "ZBX_PREPROC_BOOL2DEC":
279 - return StepTypeBool2Dec, nil
280 - case "ZBX_PREPROC_OCT2DEC":
281 - return StepTypeOct2Dec, nil
282 - case "ZBX_PREPROC_HEX2DEC":
283 - return StepTypeHex2Dec, nil
284 - case "ZBX_PREPROC_DELTA_VALUE":
285 - return StepTypeDeltaValue, nil
286 - case "ZBX_PREPROC_DELTA_SPEED":
287 - return StepTypeDeltaSpeed, nil
288 - case "ZBX_PREPROC_XPATH":
289 - return StepTypeXPath, nil
290 - case "ZBX_PREPROC_JSONPATH":
291 - return StepTypeJSONPath, nil
292 - case "ZBX_PREPROC_VALIDATE_RANGE":
293 - return StepTypeValidateRange, nil
294 - case "ZBX_PREPROC_VALIDATE_REGEX":
295 - return StepTypeValidateRegex, nil
296 - case "ZBX_PREPROC_VALIDATE_NOT_REGEX":
297 - return StepTypeValidateNotRegex, nil
298 - case "ZBX_PREPROC_ERROR_FIELD_JSON":
299 - return StepTypeErrorFieldJSON, nil
300 - case "ZBX_PREPROC_ERROR_FIELD_XML":
301 - return StepTypeErrorFieldXML, nil
302 - case "ZBX_PREPROC_ERROR_FIELD_REGEX":
303 - return StepTypeErrorFieldRegex, nil
304 - case "ZBX_PREPROC_THROTTLE_VALUE":
305 - return StepTypeThrottleValue, nil
306 - case "ZBX_PREPROC_THROTTLE_TIMED_VALUE":
307 - return StepTypeThrottleTimedValue, nil
308 - case "ZBX_PREPROC_PROMETHEUS_PATTERN":
309 - return StepTypePrometheusPattern, nil
310 - case "ZBX_PREPROC_PROMETHEUS_TO_JSON":
311 - return StepTypePrometheusToJSON, nil
312 - case "ZBX_PREPROC_CSV_TO_JSON":
313 - return StepTypeCSVToJSON, nil
314 - case "ZBX_PREPROC_STR_REPLACE":
315 - return StepTypeStringReplace, nil
316 - case "ZBX_PREPROC_SNMP_WALK_VALUE":
317 - return StepTypeSNMPWalkValue, nil
318 - case "ZBX_PREPROC_SCRIPT":
319 - return StepTypeJavaScript, nil
320 - case "ZBX_PREPROC_VALIDATE_NOT_SUPPORTED":
321 - return StepTypeValidateNotSupported, nil
322 - case "ZBX_PREPROC_XML_TO_JSON":
323 - return StepTypeXMLToJSON, nil
324 - case "ZBX_PREPROC_SNMP_GET_VALUE":
325 - return StepTypeSNMPGetValue, nil
326 - case "ZBX_PREPROC_SNMP_WALK_TO_JSON":
327 - return StepTypeSNMPWalkToJSON, nil
328 - default:
329 - return -1, fmt.Errorf("unknown step type: %s", s)
330 - }
331 -}
332 -
333 -// ValueFromTestInput converts test input to Value.
334 -func ValueFromTestInput(tc TestCase) (Value, error) {
335 - // Check if this is an error input (from a previous step failure)
336 - if tc.In.Error != nil {
337 - data := ""
338 - if tc.In.Error.Data != nil {
339 - data = fmt.Sprint(tc.In.Error.Data)
340 - }
341 -
342 - timestamp := time.Now()
343 - if tc.In.Error.Time != "" {
344 - t, err := time.Parse("2006-01-02 15:04:05 -07:00", tc.In.Error.Time)
345 - if err == nil {
346 - timestamp = t
347 - }
348 - }
349 -
350 - return Value{
351 - Data: data,
352 - Type: ValueTypeStr, // Errors are always strings
353 - Timestamp: timestamp,
354 - IsError: true, // Mark as error input
355 - }, nil
356 - }
357 -
358 - // Normal value input
359 - vt, err := parseValueType(tc.In.Value.ValueType)
360 - if err != nil {
361 - return Value{}, err
362 - }
363 -
364 - data := ""
365 - if tc.In.Value.Data != nil {
366 - data = fmt.Sprint(tc.In.Value.Data)
367 - }
368 -
369 - timestamp := time.Now()
370 - if tc.In.Value.Time != "" {
371 - // Parse time in format: 2017-10-29 03:15:00 +03:00
372 - t, err := time.Parse("2006-01-02 15:04:05 -07:00", tc.In.Value.Time)
373 - if err == nil {
374 - timestamp = t
375 - }
376 - }
377 -
378 - return Value{
379 - Data: data,
380 - Type: vt,
381 - Timestamp: timestamp,
382 - IsError: false, // Normal value, not an error
383 - }, nil
384 -}
385 -
386 -// HistoryValueFromTestInput converts history value from test input to Value
387 -func HistoryValueFromTestInput(tc TestCase) *Value {
388 - // Check both possible field names
389 - histValue := tc.In.HistoryValue
390 - if histValue == nil {
391 - histValue = tc.In.History
392 - }
393 - if histValue == nil {
394 - return nil
395 - }
396 -
397 - data := ""
398 - if histValue.Data != nil {
399 - data = fmt.Sprint(histValue.Data)
400 - }
401 -
402 - timestamp := time.Now()
403 - if histValue.Time != "" {
404 - t, err := time.Parse("2006-01-02 15:04:05 -07:00", histValue.Time)
405 - if err == nil {
406 - timestamp = t
407 - }
408 - }
409 -
410 - return &Value{
411 - Data: data,
412 - Type: ValueTypeStr,
413 - Timestamp: timestamp,
414 - }
415 -}
416 -
417 -// StepFromTestInput converts test input to Step.
418 -func StepFromTestInput(tc TestCase) (Step, error) {
419 - stepType, err := StepTypeFromString(tc.In.Step.Type)
420 - if err != nil {
421 - return Step{}, err
422 - }
423 -
424 - // Parse error handler if provided
425 - errorHandler := ErrorHandler{Action: ErrorActionDefault}
426 - if tc.In.Step.ErrorHandler != "" {
427 - errorHandler = parseErrorHandler(tc.In.Step.ErrorHandler, tc.In.Step.ErrorHandlerParam)
428 - }
429 -
430 - return Step{
431 - Type: stepType,
432 - Params: tc.In.Step.Params,
433 - ErrorHandler: errorHandler,
434 - }, nil
435 -}
436 -
437 -// parseErrorHandler converts Zabbix error handler string to ErrorHandler
438 -func parseErrorHandler(handlerStr, paramStr string) ErrorHandler {
439 - switch handlerStr {
440 - case "ZBX_PREPROC_FAIL_DEFAULT":
441 - return ErrorHandler{Action: ErrorActionDefault}
442 - case "ZBX_PREPROC_FAIL_DISCARD_VALUE":
443 - return ErrorHandler{Action: ErrorActionDiscard}
444 - case "ZBX_PREPROC_FAIL_SET_VALUE":
445 - return ErrorHandler{Action: ErrorActionSetValue, Params: paramStr}
446 - case "ZBX_PREPROC_FAIL_SET_ERROR":
447 - return ErrorHandler{Action: ErrorActionSetError, Params: paramStr}
448 - default:
449 - return ErrorHandler{Action: ErrorActionDefault}
450 - }
451 -}
452 -
453 -// ExpectedOutputFromTestCase gets expected output from test case.
454 -func ExpectedOutputFromTestCase(tc TestCase) (string, error) {
455 - output := ""
456 - if tc.Out.Value != nil {
457 - output = fmt.Sprint(tc.Out.Value)
458 - }
459 - return output, nil
460 -}
src/go/plugin/scripts.d/pkg/zabbixpreproc/validation.go deleted
-186
@@ -1,186 +0,0 @@
1 -package zabbixpreproc
2 -
3 -import (
4 - "fmt"
5 - "strings"
6 -)
7 -
8 -// ValidateStep performs early validation of step parameters.
9 -// This catches common errors before execution, useful for pipeline validation.
10 -// Note: Does not perform deep validation (regex compilation, JSONPath parsing, etc.)
11 -// as those are checked during execution.
12 -func ValidateStep(step Step) error {
13 - // Check step type is in valid range
14 - if step.Type < 0 {
15 - return fmt.Errorf("invalid step type: %d (must be >= 0)", step.Type)
16 - }
17 -
18 - // Validate parameters based on step type
19 - switch step.Type {
20 - // Steps that require non-empty parameters
21 - case StepTypeMultiplier:
22 - if step.Params == "" {
23 - return fmt.Errorf("multiplier step requires a numeric parameter")
24 - }
25 -
26 - case StepTypeRegexSubstitution:
27 - if step.Params == "" {
28 - return fmt.Errorf("regex substitution requires pattern and replacement (pattern\\nreplacement)")
29 - }
30 - if !strings.Contains(step.Params, "\n") {
31 - return fmt.Errorf("regex substitution requires newline-separated pattern and replacement")
32 - }
33 -
34 - case StepTypeStringReplace:
35 - if step.Params == "" {
36 - return fmt.Errorf("string replace requires search and replacement (search\\nreplacement)")
37 - }
38 - if !strings.Contains(step.Params, "\n") {
39 - return fmt.Errorf("string replace requires newline-separated search and replacement")
40 - }
41 -
42 - case StepTypeJSONPath, StepTypeJSONPathMulti:
43 - if step.Params == "" {
44 - return fmt.Errorf("jsonpath step requires a jsonpath expression parameter")
45 - }
46 -
47 - case StepTypeXPath:
48 - if step.Params == "" {
49 - return fmt.Errorf("xpath step requires an xpath expression parameter")
50 - }
51 -
52 - case StepTypePrometheusPattern:
53 - if step.Params == "" {
54 - return fmt.Errorf("prometheus pattern requires a pattern parameter")
55 - }
56 -
57 - case StepTypePrometheusToJSON, StepTypePrometheusToJSONMulti:
58 - if step.Params == "" {
59 - return fmt.Errorf("prometheus to json requires a pattern parameter")
60 - }
61 -
62 - case StepTypeCSVToJSON, StepTypeCSVToJSONMulti:
63 - parts := strings.Split(step.Params, "\n")
64 - if len(parts) < 3 {
65 - return fmt.Errorf("csv to json requires 3 parameters: delimiter, quote, header (got %d)", len(parts))
66 - }
67 -
68 - case StepTypeSNMPWalkValue:
69 - parts := strings.Split(step.Params, "\n")
70 - if len(parts) < 2 {
71 - return fmt.Errorf("snmp walk value requires 2 parameters: oid, format (got %d)", len(parts))
72 - }
73 -
74 - case StepTypeSNMPGetValue:
75 - if step.Params == "" {
76 - return fmt.Errorf("snmp get value requires format mode parameter")
77 - }
78 -
79 - case StepTypeSNMPWalkToJSON, StepTypeSNMPWalkToJSONMulti:
80 - if step.Params == "" {
81 - return fmt.Errorf("snmp walk to json requires at least one triplet (macro, oid, format)")
82 - }
83 - parts := strings.Split(step.Params, "\n")
84 - if len(parts)%3 != 0 {
85 - return fmt.Errorf("snmp walk to json requires parameters in triplets of (macro, oid, format), got %d parameters", len(parts))
86 - }
87 -
88 - case StepTypeValidateRange:
89 - if step.Params == "" {
90 - return fmt.Errorf("validate range requires min and max parameters")
91 - }
92 -
93 - case StepTypeValidateRegex:
94 - if step.Params == "" {
95 - return fmt.Errorf("validate regex requires a pattern parameter")
96 - }
97 -
98 - case StepTypeValidateNotRegex:
99 - if step.Params == "" {
100 - return fmt.Errorf("validate not regex requires a pattern parameter")
101 - }
102 -
103 - case StepTypeValidateNotSupported:
104 - if step.Params == "" {
105 - return fmt.Errorf("validate not supported requires an error message parameter")
106 - }
107 -
108 - case StepTypeErrorFieldJSON:
109 - if step.Params == "" {
110 - return fmt.Errorf("error field json requires a jsonpath expression parameter")
111 - }
112 -
113 - case StepTypeErrorFieldXML:
114 - if step.Params == "" {
115 - return fmt.Errorf("error field xml requires an xpath expression parameter")
116 - }
117 -
118 - case StepTypeErrorFieldRegex:
119 - if step.Params == "" {
120 - return fmt.Errorf("error field regex requires a pattern parameter")
121 - }
122 -
123 - case StepTypeThrottleValue:
124 - if step.Params == "" {
125 - return fmt.Errorf("throttle value requires a time period parameter")
126 - }
127 -
128 - case StepTypeThrottleTimedValue:
129 - if step.Params == "" {
130 - return fmt.Errorf("throttle timed value requires a time period parameter")
131 - }
132 -
133 - case StepTypeJavaScript:
134 - if step.Params == "" {
135 - return fmt.Errorf("javascript step requires code parameter")
136 - }
137 -
138 - case StepTypeDeltaValue, StepTypeDeltaSpeed:
139 - // Params optional for delta steps (defaults handled in execution)
140 -
141 - case StepTypeTrim, StepTypeRTrim, StepTypeLTrim:
142 - // Params optional for trim (defaults to whitespace)
143 -
144 - case StepTypeBool2Dec, StepTypeOct2Dec, StepTypeHex2Dec:
145 - // No parameters required for conversion steps
146 -
147 - default:
148 - // Unknown step type - not necessarily an error (future extensibility)
149 - // The actual execution will handle this
150 - }
151 -
152 - // Validate error handler
153 - switch step.ErrorHandler.Action {
154 - case ErrorActionDefault, ErrorActionDiscard:
155 - // No params required
156 -
157 - case ErrorActionSetValue:
158 - // Params should contain the value to set (can be empty string)
159 -
160 - case ErrorActionSetError:
161 - if step.ErrorHandler.Params == "" {
162 - return fmt.Errorf("error handler set_error requires an error message parameter")
163 - }
164 -
165 - default:
166 - return fmt.Errorf("invalid error handler action: %d", step.ErrorHandler.Action)
167 - }
168 -
169 - return nil
170 -}
171 -
172 -// ValidatePipeline validates all steps in a pipeline before execution.
173 -// This is useful for catching configuration errors early, especially in multi-step pipelines.
174 -func ValidatePipeline(steps []Step) error {
175 - if len(steps) == 0 {
176 - return fmt.Errorf("pipeline cannot be empty")
177 - }
178 -
179 - for i, step := range steps {
180 - if err := ValidateStep(step); err != nil {
181 - return fmt.Errorf("step %d: %w", i, err)
182 - }
183 - }
184 -
185 - return nil
186 -}
src/go/plugin/scripts.d/pkg/zabbixpreproc/validation_test.go deleted
-292
@@ -1,292 +0,0 @@
1 -package zabbixpreproc
2 -
3 -import (
4 - "strings"
5 - "testing"
6 -)
7 -
8 -func TestValidateStep(t *testing.T) {
9 - tests := []struct {
10 - name string
11 - step Step
12 - wantErr bool
13 - errMsg string
14 - }{
15 - // Valid steps
16 - {
17 - name: "valid multiplier",
18 - step: Step{Type: StepTypeMultiplier, Params: "2.5"},
19 - wantErr: false,
20 - },
21 - {
22 - name: "valid jsonpath",
23 - step: Step{Type: StepTypeJSONPath, Params: "$.cpu"},
24 - wantErr: false,
25 - },
26 - {
27 - name: "valid csv to json",
28 - step: Step{Type: StepTypeCSVToJSON, Params: ",\n\"\n1"},
29 - wantErr: false,
30 - },
31 - {
32 - name: "valid snmp walk to json",
33 - step: Step{Type: StepTypeSNMPWalkToJSON, Params: "{#NAME}\n.1.3.6.1.2.1.1\n1"},
34 - wantErr: false,
35 - },
36 - {
37 - name: "valid trim with params",
38 - step: Step{Type: StepTypeTrim, Params: " "},
39 - wantErr: false,
40 - },
41 - {
42 - name: "valid trim without params",
43 - step: Step{Type: StepTypeTrim, Params: ""},
44 - wantErr: false,
45 - },
46 - {
47 - name: "valid bool2dec",
48 - step: Step{Type: StepTypeBool2Dec, Params: ""},
49 - wantErr: false,
50 - },
51 -
52 - // Invalid steps - negative type
53 - {
54 - name: "negative step type",
55 - step: Step{Type: StepType(-1), Params: ""},
56 - wantErr: true,
57 - errMsg: "invalid step type",
58 - },
59 -
60 - // Invalid steps - missing required params
61 - {
62 - name: "multiplier missing params",
63 - step: Step{Type: StepTypeMultiplier, Params: ""},
64 - wantErr: true,
65 - errMsg: "multiplier step requires a numeric parameter",
66 - },
67 - {
68 - name: "jsonpath missing params",
69 - step: Step{Type: StepTypeJSONPath, Params: ""},
70 - wantErr: true,
71 - errMsg: "jsonpath step requires",
72 - },
73 - {
74 - name: "xpath missing params",
75 - step: Step{Type: StepTypeXPath, Params: ""},
76 - wantErr: true,
77 - errMsg: "xpath step requires",
78 - },
79 - {
80 - name: "prometheus pattern missing params",
81 - step: Step{Type: StepTypePrometheusPattern, Params: ""},
82 - wantErr: true,
83 - errMsg: "prometheus pattern requires",
84 - },
85 - {
86 - name: "javascript missing params",
87 - step: Step{Type: StepTypeJavaScript, Params: ""},
88 - wantErr: true,
89 - errMsg: "javascript step requires",
90 - },
91 -
92 - // Invalid steps - malformed params
93 - {
94 - name: "regex substitution missing newline",
95 - step: Step{Type: StepTypeRegexSubstitution, Params: "pattern"},
96 - wantErr: true,
97 - errMsg: "requires newline-separated",
98 - },
99 - {
100 - name: "string replace missing newline",
101 - step: Step{Type: StepTypeStringReplace, Params: "search"},
102 - wantErr: true,
103 - errMsg: "requires newline-separated",
104 - },
105 - {
106 - name: "csv to json insufficient params",
107 - step: Step{Type: StepTypeCSVToJSON, Params: ",\n\""},
108 - wantErr: true,
109 - errMsg: "requires 3 parameters",
110 - },
111 - {
112 - name: "snmp walk value insufficient params",
113 - step: Step{Type: StepTypeSNMPWalkValue, Params: ".1.3.6.1"},
114 - wantErr: true,
115 - errMsg: "requires 2 parameters",
116 - },
117 - {
118 - name: "snmp walk to json non-triplet params",
119 - step: Step{Type: StepTypeSNMPWalkToJSON, Params: "{#NAME}\n.1.3.6.1"},
120 - wantErr: true,
121 - errMsg: "requires parameters in triplets",
122 - },
123 - {
124 - name: "snmp walk to json empty params",
125 - step: Step{Type: StepTypeSNMPWalkToJSON, Params: ""},
126 - wantErr: true,
127 - errMsg: "requires at least one triplet",
128 - },
129 -
130 - // Error handler validation
131 - {
132 - name: "valid error handler - default",
133 - step: Step{
134 - Type: StepTypeJSONPath,
135 - Params: "$.value",
136 - ErrorHandler: ErrorHandler{Action: ErrorActionDefault},
137 - },
138 - wantErr: false,
139 - },
140 - {
141 - name: "valid error handler - discard",
142 - step: Step{
143 - Type: StepTypeJSONPath,
144 - Params: "$.value",
145 - ErrorHandler: ErrorHandler{Action: ErrorActionDiscard},
146 - },
147 - wantErr: false,
148 - },
149 - {
150 - name: "valid error handler - set value",
151 - step: Step{
152 - Type: StepTypeJSONPath,
153 - Params: "$.value",
154 - ErrorHandler: ErrorHandler{Action: ErrorActionSetValue, Params: "0"},
155 - },
156 - wantErr: false,
157 - },
158 - {
159 - name: "valid error handler - set value empty",
160 - step: Step{
161 - Type: StepTypeJSONPath,
162 - Params: "$.value",
163 - ErrorHandler: ErrorHandler{Action: ErrorActionSetValue, Params: ""},
164 - },
165 - wantErr: false,
166 - },
167 - {
168 - name: "valid error handler - set error",
169 - step: Step{
170 - Type: StepTypeJSONPath,
171 - Params: "$.value",
172 - ErrorHandler: ErrorHandler{Action: ErrorActionSetError, Params: "Custom error"},
173 - },
174 - wantErr: false,
175 - },
176 - {
177 - name: "invalid error handler - set error missing message",
178 - step: Step{
179 - Type: StepTypeJSONPath,
180 - Params: "$.value",
181 - ErrorHandler: ErrorHandler{Action: ErrorActionSetError, Params: ""},
182 - },
183 - wantErr: true,
184 - errMsg: "requires an error message parameter",
185 - },
186 - {
187 - name: "invalid error handler - unknown action",
188 - step: Step{
189 - Type: StepTypeJSONPath,
190 - Params: "$.value",
191 - ErrorHandler: ErrorHandler{Action: ErrorAction(99)},
192 - },
193 - wantErr: true,
194 - errMsg: "invalid error handler action",
195 - },
196 - }
197 -
198 - for _, tt := range tests {
199 - t.Run(tt.name, func(t *testing.T) {
200 - err := ValidateStep(tt.step)
201 - if tt.wantErr {
202 - if err == nil {
203 - t.Errorf("ValidateStep() expected error containing %q, got nil", tt.errMsg)
204 - return
205 - }
206 - if !strings.Contains(err.Error(), tt.errMsg) {
207 - t.Errorf("ValidateStep() error = %v, want error containing %q", err, tt.errMsg)
208 - }
209 - } else {
210 - if err != nil {
211 - t.Errorf("ValidateStep() unexpected error = %v", err)
212 - }
213 - }
214 - })
215 - }
216 -}
217 -
218 -func TestValidatePipeline(t *testing.T) {
219 - tests := []struct {
220 - name string
221 - steps []Step
222 - wantErr bool
223 - errMsg string
224 - }{
225 - {
226 - name: "valid pipeline",
227 - steps: []Step{
228 - {Type: StepTypeJSONPath, Params: "$.cpu"},
229 - {Type: StepTypeMultiplier, Params: "100"},
230 - {Type: StepTypeTrim, Params: ""},
231 - },
232 - wantErr: false,
233 - },
234 - {
235 - name: "empty pipeline",
236 - steps: []Step{},
237 - wantErr: true,
238 - errMsg: "pipeline cannot be empty",
239 - },
240 - {
241 - name: "pipeline with invalid step at index 1",
242 - steps: []Step{
243 - {Type: StepTypeJSONPath, Params: "$.cpu"},
244 - {Type: StepTypeMultiplier, Params: ""}, // Missing params
245 - {Type: StepTypeTrim, Params: ""},
246 - },
247 - wantErr: true,
248 - errMsg: "step 1:",
249 - },
250 - {
251 - name: "pipeline with invalid step at index 0",
252 - steps: []Step{
253 - {Type: StepTypeJSONPath, Params: ""}, // Missing params
254 - {Type: StepTypeMultiplier, Params: "100"},
255 - },
256 - wantErr: true,
257 - errMsg: "step 0:",
258 - },
259 - {
260 - name: "pipeline with invalid error handler",
261 - steps: []Step{
262 - {Type: StepTypeJSONPath, Params: "$.cpu"},
263 - {
264 - Type: StepTypeMultiplier,
265 - Params: "100",
266 - ErrorHandler: ErrorHandler{Action: ErrorActionSetError, Params: ""},
267 - },
268 - },
269 - wantErr: true,
270 - errMsg: "step 1:",
271 - },
272 - }
273 -
274 - for _, tt := range tests {
275 - t.Run(tt.name, func(t *testing.T) {
276 - err := ValidatePipeline(tt.steps)
277 - if tt.wantErr {
278 - if err == nil {
279 - t.Errorf("ValidatePipeline() expected error containing %q, got nil", tt.errMsg)
280 - return
281 - }
282 - if !strings.Contains(err.Error(), tt.errMsg) {
283 - t.Errorf("ValidatePipeline() error = %v, want error containing %q", err, tt.errMsg)
284 - }
285 - } else {
286 - if err != nil {
287 - t.Errorf("ValidatePipeline() unexpected error = %v", err)
288 - }
289 - }
290 - })
291 - }
292 -}
src/go/plugin/scripts.d/pkg/zabbixpreproc/xml_step.go deleted
-144
@@ -1,144 +0,0 @@
1 -package zabbixpreproc
2 -
3 -import (
4 - "encoding/json"
5 - "fmt"
6 - "strings"
7 -
8 - "github.com/antchfx/xmlquery"
9 -)
10 -
11 -// xmlToJSON converts XML to JSON following Zabbix serialization rules.
12 -//
13 -// Zabbix XML to JSON Serialization Rules:
14 -// 1. Attributes: Prepended with '@'
15 -// <xml foo="FOO"> → {"xml": {"@foo": "FOO"}}
16 -// 2. Self-closing elements: Become null
17 -// <foo/> → {"foo": null}
18 -// 3. Empty attributes: Preserved as empty strings
19 -// bar="" → "@bar": ""
20 -// 4. Repeated elements: Consolidated into arrays
21 -// <foo>A</foo><foo>B</foo> → "foo": ["A", "B"]
22 -// 5. Simple text elements: Direct string values
23 -// <foo>BAZ</foo> → {"foo": "BAZ"}
24 -// 6. Text with attributes: Uses #text key
25 -// <foo bar="BAR">BAZ</foo> → {"foo": {"@bar": "BAR", "#text": "BAZ"}}
26 -func xmlToJSON(value Value, paramStr string) (Value, error) {
27 - // Parse XML
28 - doc, err := xmlquery.Parse(strings.NewReader(value.Data))
29 - if err != nil {
30 - return Value{}, fmt.Errorf("invalid XML: %w", err)
31 - }
32 -
33 - // Convert to JSON structure
34 - result := convertNodeToJSON(doc)
35 -
36 - // Serialize to JSON string
37 - jsonBytes, err := json.Marshal(result)
38 - if err != nil {
39 - return Value{}, fmt.Errorf("JSON serialization failed: %w", err)
40 - }
41 -
42 - return Value{Data: string(jsonBytes), Type: ValueTypeStr}, nil
43 -}
44 -
45 -// convertNodeToJSON converts an XML node to a JSON-compatible structure
46 -func convertNodeToJSON(node *xmlquery.Node) interface{} {
47 - if node == nil {
48 - return nil
49 - }
50 -
51 - // Handle document node - process root element
52 - if node.Type == xmlquery.DocumentNode {
53 - for child := node.FirstChild; child != nil; child = child.NextSibling {
54 - if child.Type == xmlquery.ElementNode {
55 - // Return the root element as a map with element name as key
56 - return map[string]interface{}{
57 - child.Data: convertElementToJSON(child),
58 - }
59 - }
60 - }
61 - return nil
62 - }
63 -
64 - // For element nodes, wrap in a map with element name
65 - if node.Type == xmlquery.ElementNode {
66 - return map[string]interface{}{
67 - node.Data: convertElementToJSON(node),
68 - }
69 - }
70 -
71 - return nil
72 -}
73 -
74 -// convertElementToJSON converts an XML element to JSON following Zabbix rules
75 -func convertElementToJSON(node *xmlquery.Node) interface{} {
76 - // Collect attributes (Rule 1: prepend with '@')
77 - attrs := make(map[string]interface{})
78 - for _, attr := range node.Attr {
79 - // Rule 3: Empty attributes preserved as empty strings
80 - attrs["@"+attr.Name.Local] = attr.Value
81 - }
82 -
83 - // Collect child elements grouped by name
84 - children := make(map[string][]interface{})
85 - var textContent strings.Builder
86 - hasChildren := false
87 -
88 - for child := node.FirstChild; child != nil; child = child.NextSibling {
89 - switch child.Type {
90 - case xmlquery.ElementNode:
91 - hasChildren = true
92 - childValue := convertElementToJSON(child)
93 - children[child.Data] = append(children[child.Data], childValue)
94 -
95 - case xmlquery.TextNode, xmlquery.CharDataNode:
96 - // Collect text content (trim each segment but preserve structure)
97 - text := strings.TrimSpace(child.Data)
98 - if text != "" {
99 - if textContent.Len() > 0 {
100 - textContent.WriteString(" ")
101 - }
102 - textContent.WriteString(text)
103 - }
104 - }
105 - }
106 -
107 - text := textContent.String()
108 -
109 - // Rule 2: Self-closing elements (no attributes, no children, no text) → null
110 - if len(attrs) == 0 && !hasChildren && text == "" {
111 - return nil
112 - }
113 -
114 - // Rule 5: Simple text elements (no attributes, no children, only text) → direct string
115 - if len(attrs) == 0 && !hasChildren && text != "" {
116 - return text
117 - }
118 -
119 - // Build result object
120 - result := make(map[string]interface{})
121 -
122 - // Add attributes first
123 - for k, v := range attrs {
124 - result[k] = v
125 - }
126 -
127 - // Rule 6: Text with attributes → use #text key
128 - if text != "" {
129 - result["#text"] = text
130 - }
131 -
132 - // Rule 4: Repeated elements → arrays
133 - for name, values := range children {
134 - if len(values) == 1 {
135 - // Single child - add directly
136 - result[name] = values[0]
137 - } else {
138 - // Multiple children with same name - create array
139 - result[name] = values
140 - }
141 - }
142 -
143 - return result
144 -}
src/go/plugin/scripts.d/pkg/zabbixpreproc/xml_step_test.go deleted
-502
@@ -1,502 +0,0 @@
1 -package zabbixpreproc
2 -
3 -import (
4 - "encoding/json"
5 - "testing"
6 - "time"
7 -)
8 -
9 -// TestXMLToJSON_Rule1_Attributes tests Rule 1: Attributes prepended with '@'
10 -func TestXMLToJSON_Rule1_Attributes(t *testing.T) {
11 - tests := []struct {
12 - name string
13 - xml string
14 - expected string
15 - }{
16 - {
17 - name: "Single attribute",
18 - xml: `<xml foo="FOO"></xml>`,
19 - expected: `{"xml":{"@foo":"FOO"}}`,
20 - },
21 - {
22 - name: "Multiple attributes",
23 - xml: `<root id="123" name="test" active="true"></root>`,
24 - expected: `{"root":{"@active":"true","@id":"123","@name":"test"}}`,
25 - },
26 - {
27 - name: "Nested element with attributes",
28 - xml: `<root><child attr="value"/></root>`,
29 - expected: `{"root":{"child":{"@attr":"value"}}}`,
30 - },
31 - }
32 -
33 - for _, tt := range tests {
34 - t.Run(tt.name, func(t *testing.T) {
35 - value := Value{
36 - Data: tt.xml,
37 - Type: ValueTypeStr,
38 - Timestamp: time.Now(),
39 - }
40 -
41 - result, err := xmlToJSON(value, "")
42 - if err != nil {
43 - t.Fatalf("xmlToJSON() error = %v", err)
44 - }
45 -
46 - // Compare JSON (normalize by parsing and re-marshaling)
47 - var got, want interface{}
48 - if err := json.Unmarshal([]byte(result.Data), &got); err != nil {
49 - t.Fatalf("Failed to parse result JSON: %v", err)
50 - }
51 - if err := json.Unmarshal([]byte(tt.expected), &want); err != nil {
52 - t.Fatalf("Failed to parse expected JSON: %v", err)
53 - }
54 -
55 - gotJSON, _ := json.Marshal(got)
56 - wantJSON, _ := json.Marshal(want)
57 -
58 - if string(gotJSON) != string(wantJSON) {
59 - t.Errorf("xmlToJSON() = %s, want %s", gotJSON, wantJSON)
60 - }
61 - })
62 - }
63 -}
64 -
65 -// TestXMLToJSON_Rule2_SelfClosingElements tests Rule 2: Self-closing elements become null
66 -func TestXMLToJSON_Rule2_SelfClosingElements(t *testing.T) {
67 - tests := []struct {
68 - name string
69 - xml string
70 - expected string
71 - }{
72 - {
73 - name: "Single self-closing element",
74 - xml: `<root><foo/></root>`,
75 - expected: `{"root":{"foo":null}}`,
76 - },
77 - {
78 - name: "Multiple self-closing elements",
79 - xml: `<root><foo/><bar/><baz/></root>`,
80 - expected: `{"root":{"bar":null,"baz":null,"foo":null}}`,
81 - },
82 - {
83 - name: "Root self-closing",
84 - xml: `<empty/>`,
85 - expected: `{"empty":null}`,
86 - },
87 - }
88 -
89 - for _, tt := range tests {
90 - t.Run(tt.name, func(t *testing.T) {
91 - value := Value{
92 - Data: tt.xml,
93 - Type: ValueTypeStr,
94 - Timestamp: time.Now(),
95 - }
96 -
97 - result, err := xmlToJSON(value, "")
98 - if err != nil {
99 - t.Fatalf("xmlToJSON() error = %v", err)
100 - }
101 -
102 - var got, want interface{}
103 - json.Unmarshal([]byte(result.Data), &got)
104 - json.Unmarshal([]byte(tt.expected), &want)
105 -
106 - gotJSON, _ := json.Marshal(got)
107 - wantJSON, _ := json.Marshal(want)
108 -
109 - if string(gotJSON) != string(wantJSON) {
110 - t.Errorf("xmlToJSON() = %s, want %s", gotJSON, wantJSON)
111 - }
112 - })
113 - }
114 -}
115 -
116 -// TestXMLToJSON_Rule3_EmptyAttributes tests Rule 3: Empty attributes preserved as empty strings
117 -func TestXMLToJSON_Rule3_EmptyAttributes(t *testing.T) {
118 - tests := []struct {
119 - name string
120 - xml string
121 - expected string
122 - }{
123 - {
124 - name: "Empty attribute value",
125 - xml: `<root bar=""></root>`,
126 - expected: `{"root":{"@bar":""}}`,
127 - },
128 - {
129 - name: "Mix of empty and non-empty attributes",
130 - xml: `<root foo="FOO" bar="" baz="BAZ"></root>`,
131 - expected: `{"root":{"@bar":"","@baz":"BAZ","@foo":"FOO"}}`,
132 - },
133 - }
134 -
135 - for _, tt := range tests {
136 - t.Run(tt.name, func(t *testing.T) {
137 - value := Value{
138 - Data: tt.xml,
139 - Type: ValueTypeStr,
140 - Timestamp: time.Now(),
141 - }
142 -
143 - result, err := xmlToJSON(value, "")
144 - if err != nil {
145 - t.Fatalf("xmlToJSON() error = %v", err)
146 - }
147 -
148 - var got, want interface{}
149 - json.Unmarshal([]byte(result.Data), &got)
150 - json.Unmarshal([]byte(tt.expected), &want)
151 -
152 - gotJSON, _ := json.Marshal(got)
153 - wantJSON, _ := json.Marshal(want)
154 -
155 - if string(gotJSON) != string(wantJSON) {
156 - t.Errorf("xmlToJSON() = %s, want %s", gotJSON, wantJSON)
157 - }
158 - })
159 - }
160 -}
161 -
162 -// TestXMLToJSON_Rule4_RepeatedElements tests Rule 4: Repeated elements consolidated into arrays
163 -func TestXMLToJSON_Rule4_RepeatedElements(t *testing.T) {
164 - tests := []struct {
165 - name string
166 - xml string
167 - expected string
168 - }{
169 - {
170 - name: "Three identical elements",
171 - xml: `<root><foo>BAR</foo><foo>BAZ</foo><foo>QUX</foo></root>`,
172 - expected: `{"root":{"foo":["BAR","BAZ","QUX"]}}`,
173 - },
174 - {
175 - name: "Two identical elements",
176 - xml: `<root><item>A</item><item>B</item></root>`,
177 - expected: `{"root":{"item":["A","B"]}}`,
178 - },
179 - {
180 - name: "Mixed single and repeated elements",
181 - xml: `<root><foo>A</foo><bar>B</bar><foo>C</foo></root>`,
182 - expected: `{"root":{"bar":"B","foo":["A","C"]}}`,
183 - },
184 - {
185 - name: "Repeated complex elements",
186 - xml: `<root><item id="1">A</item><item id="2">B</item></root>`,
187 - expected: `{"root":{"item":[{"#text":"A","@id":"1"},{"#text":"B","@id":"2"}]}}`,
188 - },
189 - }
190 -
191 - for _, tt := range tests {
192 - t.Run(tt.name, func(t *testing.T) {
193 - value := Value{
194 - Data: tt.xml,
195 - Type: ValueTypeStr,
196 - Timestamp: time.Now(),
197 - }
198 -
199 - result, err := xmlToJSON(value, "")
200 - if err != nil {
201 - t.Fatalf("xmlToJSON() error = %v", err)
202 - }
203 -
204 - var got, want interface{}
205 - json.Unmarshal([]byte(result.Data), &got)
206 - json.Unmarshal([]byte(tt.expected), &want)
207 -
208 - gotJSON, _ := json.Marshal(got)
209 - wantJSON, _ := json.Marshal(want)
210 -
211 - if string(gotJSON) != string(wantJSON) {
212 - t.Errorf("xmlToJSON() = %s, want %s", gotJSON, wantJSON)
213 - }
214 - })
215 - }
216 -}
217 -
218 -// TestXMLToJSON_Rule5_SimpleTextElements tests Rule 5: Simple text elements become direct strings
219 -func TestXMLToJSON_Rule5_SimpleTextElements(t *testing.T) {
220 - tests := []struct {
221 - name string
222 - xml string
223 - expected string
224 - }{
225 - {
226 - name: "Simple text element",
227 - xml: `<root><foo>BAZ</foo></root>`,
228 - expected: `{"root":{"foo":"BAZ"}}`,
229 - },
230 - {
231 - name: "Multiple simple text elements",
232 - xml: `<root><name>John</name><age>30</age><city>NYC</city></root>`,
233 - expected: `{"root":{"age":"30","city":"NYC","name":"John"}}`,
234 - },
235 - {
236 - name: "Nested simple text",
237 - xml: `<root><outer><inner>value</inner></outer></root>`,
238 - expected: `{"root":{"outer":{"inner":"value"}}}`,
239 - },
240 - {
241 - name: "Text with whitespace trimming",
242 - xml: `<root><foo> BAR </foo></root>`,
243 - expected: `{"root":{"foo":"BAR"}}`,
244 - },
245 - }
246 -
247 - for _, tt := range tests {
248 - t.Run(tt.name, func(t *testing.T) {
249 - value := Value{
250 - Data: tt.xml,
251 - Type: ValueTypeStr,
252 - Timestamp: time.Now(),
253 - }
254 -
255 - result, err := xmlToJSON(value, "")
256 - if err != nil {
257 - t.Fatalf("xmlToJSON() error = %v", err)
258 - }
259 -
260 - var got, want interface{}
261 - json.Unmarshal([]byte(result.Data), &got)
262 - json.Unmarshal([]byte(tt.expected), &want)
263 -
264 - gotJSON, _ := json.Marshal(got)
265 - wantJSON, _ := json.Marshal(want)
266 -
267 - if string(gotJSON) != string(wantJSON) {
268 - t.Errorf("xmlToJSON() = %s, want %s", gotJSON, wantJSON)
269 - }
270 - })
271 - }
272 -}
273 -
274 -// TestXMLToJSON_Rule6_TextWithAttributes tests Rule 6: Text with attributes uses #text key
275 -func TestXMLToJSON_Rule6_TextWithAttributes(t *testing.T) {
276 - tests := []struct {
277 - name string
278 - xml string
279 - expected string
280 - }{
281 - {
282 - name: "Text with single attribute",
283 - xml: `<root><foo bar="BAR">BAZ</foo></root>`,
284 - expected: `{"root":{"foo":{"#text":"BAZ","@bar":"BAR"}}}`,
285 - },
286 - {
287 - name: "Text with multiple attributes",
288 - xml: `<root><item id="1" type="text">Value</item></root>`,
289 - expected: `{"root":{"item":{"#text":"Value","@id":"1","@type":"text"}}}`,
290 - },
291 - {
292 - name: "Root element with text and attributes",
293 - xml: `<root version="1.0">Content</root>`,
294 - expected: `{"root":{"#text":"Content","@version":"1.0"}}`,
295 - },
296 - }
297 -
298 - for _, tt := range tests {
299 - t.Run(tt.name, func(t *testing.T) {
300 - value := Value{
301 - Data: tt.xml,
302 - Type: ValueTypeStr,
303 - Timestamp: time.Now(),
304 - }
305 -
306 - result, err := xmlToJSON(value, "")
307 - if err != nil {
308 - t.Fatalf("xmlToJSON() error = %v", err)
309 - }
310 -
311 - var got, want interface{}
312 - json.Unmarshal([]byte(result.Data), &got)
313 - json.Unmarshal([]byte(tt.expected), &want)
314 -
315 - gotJSON, _ := json.Marshal(got)
316 - wantJSON, _ := json.Marshal(want)
317 -
318 - if string(gotJSON) != string(wantJSON) {
319 - t.Errorf("xmlToJSON() = %s, want %s", gotJSON, wantJSON)
320 - }
321 - })
322 - }
323 -}
324 -
325 -// TestXMLToJSON_ComplexCombinations tests complex combinations of all rules
326 -func TestXMLToJSON_ComplexCombinations(t *testing.T) {
327 - tests := []struct {
328 - name string
329 - xml string
330 - expected string
331 - }{
332 - {
333 - name: "Complex nested structure",
334 - xml: `<root>
335 - <user id="123" active="true">
336 - <name>John Doe</name>
337 - <email/>
338 - <tags>
339 - <tag>admin</tag>
340 - <tag>user</tag>
341 - </tags>
342 - </user>
343 - </root>`,
344 - expected: `{"root":{"user":{"@active":"true","@id":"123","email":null,"name":"John Doe","tags":{"tag":["admin","user"]}}}}`,
345 - },
346 - {
347 - name: "Mixed attributes, text, and children",
348 - xml: `<config version="2.0">
349 - <setting name="timeout">30</setting>
350 - <setting name="retries">3</setting>
351 - <debug enabled="false"/>
352 - </config>`,
353 - expected: `{"config":{"@version":"2.0","debug":{"@enabled":"false"},"setting":[{"#text":"30","@name":"timeout"},{"#text":"3","@name":"retries"}]}}`,
354 - },
355 - {
356 - name: "Real-world API response example",
357 - xml: `<response status="success">
358 - <data>
359 - <item id="1" type="product">Widget</item>
360 - <item id="2" type="product">Gadget</item>
361 - <metadata>
362 - <total>2</total>
363 - <page>1</page>
364 - </metadata>
365 - </data>
366 - </response>`,
367 - expected: `{"response":{"@status":"success","data":{"item":[{"#text":"Widget","@id":"1","@type":"product"},{"#text":"Gadget","@id":"2","@type":"product"}],"metadata":{"page":"1","total":"2"}}}}`,
368 - },
369 - }
370 -
371 - for _, tt := range tests {
372 - t.Run(tt.name, func(t *testing.T) {
373 - value := Value{
374 - Data: tt.xml,
375 - Type: ValueTypeStr,
376 - Timestamp: time.Now(),
377 - }
378 -
379 - result, err := xmlToJSON(value, "")
380 - if err != nil {
381 - t.Fatalf("xmlToJSON() error = %v", err)
382 - }
383 -
384 - var got, want interface{}
385 - json.Unmarshal([]byte(result.Data), &got)
386 - json.Unmarshal([]byte(tt.expected), &want)
387 -
388 - gotJSON, _ := json.Marshal(got)
389 - wantJSON, _ := json.Marshal(want)
390 -
391 - if string(gotJSON) != string(wantJSON) {
392 - t.Errorf("xmlToJSON() = %s, want %s", gotJSON, wantJSON)
393 - }
394 - })
395 - }
396 -}
397 -
398 -// TestXMLToJSON_ErrorCases tests error handling
399 -func TestXMLToJSON_ErrorCases(t *testing.T) {
400 - tests := []struct {
401 - name string
402 - xml string
403 - wantErr bool
404 - }{
405 - {
406 - name: "Invalid XML - unclosed tag",
407 - xml: `<root><foo>`,
408 - wantErr: true,
409 - },
410 - {
411 - name: "Invalid XML - mismatched tags",
412 - xml: `<root><foo></bar></root>`,
413 - wantErr: true,
414 - },
415 - {
416 - name: "Empty input",
417 - xml: ``,
418 - wantErr: true,
419 - },
420 - {
421 - name: "Not XML",
422 - xml: `{"json": "data"}`,
423 - wantErr: true,
424 - },
425 - }
426 -
427 - for _, tt := range tests {
428 - t.Run(tt.name, func(t *testing.T) {
429 - value := Value{
430 - Data: tt.xml,
431 - Type: ValueTypeStr,
432 - Timestamp: time.Now(),
433 - }
434 -
435 - _, err := xmlToJSON(value, "")
436 - if (err != nil) != tt.wantErr {
437 - t.Errorf("xmlToJSON() error = %v, wantErr %v", err, tt.wantErr)
438 - }
439 - })
440 - }
441 -}
442 -
443 -// TestXMLToJSON_EdgeCases tests edge cases and special scenarios
444 -func TestXMLToJSON_EdgeCases(t *testing.T) {
445 - tests := []struct {
446 - name string
447 - xml string
448 - expected string
449 - }{
450 - {
451 - name: "Empty root element with attributes",
452 - xml: `<root id="1"/>`,
453 - expected: `{"root":{"@id":"1"}}`,
454 - },
455 - {
456 - name: "Only whitespace text",
457 - xml: `<root> </root>`,
458 - expected: `{"root":null}`,
459 - },
460 - {
461 - name: "CDATA section",
462 - xml: `<root><![CDATA[Special <chars> & stuff]]></root>`,
463 - expected: `{"root":"Special <chars> & stuff"}`,
464 - },
465 - {
466 - name: "XML with namespaces",
467 - xml: `<root xmlns:foo="http://example.com"><bar>value</bar></root>`,
468 - expected: `{"root":{"@foo":"http://example.com","bar":"value"}}`,
469 - },
470 - {
471 - name: "Numeric-looking values",
472 - xml: `<root><num>123</num><float>45.67</float></root>`,
473 - expected: `{"root":{"float":"45.67","num":"123"}}`,
474 - },
475 - }
476 -
477 - for _, tt := range tests {
478 - t.Run(tt.name, func(t *testing.T) {
479 - value := Value{
480 - Data: tt.xml,
481 - Type: ValueTypeStr,
482 - Timestamp: time.Now(),
483 - }
484 -
485 - result, err := xmlToJSON(value, "")
486 - if err != nil {
487 - t.Fatalf("xmlToJSON() error = %v", err)
488 - }
489 -
490 - var got, want interface{}
491 - json.Unmarshal([]byte(result.Data), &got)
492 - json.Unmarshal([]byte(tt.expected), &want)
493 -
494 - gotJSON, _ := json.Marshal(got)
495 - wantJSON, _ := json.Marshal(want)
496 -
497 - if string(gotJSON) != string(wantJSON) {
498 - t.Errorf("xmlToJSON() = %s, want %s", gotJSON, wantJSON)
499 - }
500 - })
501 - }
502 -}
src/go/plugin/scripts.d/pkg/zabbixpreproc/xml_to_json_test.go deleted
-267
@@ -1,267 +0,0 @@
1 -package zabbixpreproc
2 -
3 -import (
4 - "encoding/json"
5 - "testing"
6 -)
7 -
8 -// TestXMLToJSONZabbixDocumentation tests all examples from official Zabbix docs.
9 -// Source: https://www.zabbix.com/documentation/6.0/en/manual/config/items/preprocessing/javascript/javascript_objects
10 -func TestXMLToJSONZabbixDocumentation(t *testing.T) {
11 - tests := []struct {
12 - name string
13 - input string
14 - expected string
15 - rule string
16 - }{
17 - {
18 - name: "Rule 1: Attributes prepended with @",
19 - input: `<xml foo="FOO"><bar><baz>BAZ</baz></bar></xml>`,
20 - expected: `{"xml":{"@foo":"FOO","bar":{"baz":"BAZ"}}}`,
21 - rule: "XML attributes will be converted to keys that have their names prepended with '@'",
22 - },
23 - {
24 - name: "Rule 2: Self-closing elements become null",
25 - input: `<xml><foo/></xml>`,
26 - expected: `{"xml":{"foo":null}}`,
27 - rule: "Self-closing elements (<foo/>) will be converted as having 'null' value",
28 - },
29 - {
30 - name: "Rule 3: Empty attributes preserved as empty strings",
31 - input: `<xml><foo bar="" /></xml>`,
32 - expected: `{"xml":{"foo":{"@bar":""}}}`,
33 - rule: "Empty attributes (with \"\" value) will be converted as having empty string ('') value",
34 - },
35 - {
36 - name: "Rule 4: Multiple same-named children become arrays",
37 - input: `<xml><foo>BAR</foo><foo>BAZ</foo><foo>QUX</foo></xml>`,
38 - expected: `{"xml":{"foo":["BAR","BAZ","QUX"]}}`,
39 - rule: "Multiple child nodes with the same element name will be converted to a single key that has an array of values",
40 - },
41 - {
42 - name: "Rule 5: Simple text elements become strings",
43 - input: `<xml><foo>BAZ</foo></xml>`,
44 - expected: `{"xml":{"foo":"BAZ"}}`,
45 - rule: "If a text element has no attributes and no children, it will be converted as a string",
46 - },
47 - {
48 - name: "Rule 6: Text with attributes uses #text",
49 - input: `<xml><foo bar="BAR">BAZ</foo></xml>`,
50 - expected: `{"xml":{"foo":{"@bar":"BAR","#text":"BAZ"}}}`,
51 - rule: "If a text element has no children but has attributes: text content will be converted to an element with the key '#text'",
52 - },
53 - }
54 -
55 - p := NewPreprocessor("test-shard")
56 -
57 - for _, tt := range tests {
58 - t.Run(tt.name, func(t *testing.T) {
59 - input := Value{Data: tt.input, Type: ValueTypeStr}
60 - step := Step{Type: StepTypeXMLToJSON}
61 -
62 - result, err := p.Execute("item1", input, step)
63 - if err != nil {
64 - t.Fatalf("XML to JSON failed: %v", err)
65 - }
66 -
67 - if len(result.Metrics) == 0 {
68 - t.Fatal("Expected metrics in result")
69 - }
70 -
71 - // Compare as JSON structures (order-independent)
72 - var expected, actual interface{}
73 - if err := json.Unmarshal([]byte(tt.expected), &expected); err != nil {
74 - t.Fatalf("Failed to parse expected JSON: %v", err)
75 - }
76 - if err := json.Unmarshal([]byte(result.Metrics[0].Value), &actual); err != nil {
77 - t.Fatalf("Failed to parse actual JSON: %v\nGot: %s", err, result.Metrics[0].Value)
78 - }
79 -
80 - // Marshal back to normalize for comparison
81 - expectedNorm, _ := json.Marshal(expected)
82 - actualNorm, _ := json.Marshal(actual)
83 -
84 - if string(expectedNorm) != string(actualNorm) {
85 - t.Errorf("Rule: %s\nExpected: %s\nGot: %s", tt.rule, tt.expected, result.Metrics[0].Value)
86 - }
87 - })
88 - }
89 -}
90 -
91 -// TestXMLToJSONEdgeCases tests edge cases not in official docs
92 -func TestXMLToJSONEdgeCases(t *testing.T) {
93 - p := NewPreprocessor("test-shard")
94 -
95 - tests := []struct {
96 - name string
97 - input string
98 - expected string
99 - }{
100 - {
101 - name: "Nested elements with attributes",
102 - input: `<root><parent id="1"><child name="test">value</child></parent></root>`,
103 - expected: `{"root":{"parent":{"@id":"1","child":{"@name":"test","#text":"value"}}}}`,
104 - },
105 - {
106 - name: "Multiple different children",
107 - input: `<data><a>1</a><b>2</b><c>3</c></data>`,
108 - expected: `{"data":{"a":"1","b":"2","c":"3"}}`,
109 - },
110 - {
111 - name: "Empty root element",
112 - input: `<empty/>`,
113 - expected: `{"empty":null}`,
114 - },
115 - {
116 - name: "Root with only attributes",
117 - input: `<config version="1.0" debug="true"/>`,
118 - expected: `{"config":{"@version":"1.0","@debug":"true"}}`,
119 - },
120 - {
121 - name: "Mixed repeated and unique children",
122 - input: `<list><item>A</item><item>B</item><unique>C</unique></list>`,
123 - expected: `{"list":{"item":["A","B"],"unique":"C"}}`,
124 - },
125 - {
126 - name: "Deeply nested structure",
127 - input: `<a><b><c><d>deep</d></c></b></a>`,
128 - expected: `{"a":{"b":{"c":{"d":"deep"}}}}`,
129 - },
130 - {
131 - name: "Numeric text content",
132 - input: `<metric><value>42</value><timestamp>1234567890</timestamp></metric>`,
133 - expected: `{"metric":{"value":"42","timestamp":"1234567890"}}`,
134 - },
135 - {
136 - name: "Special characters in text",
137 - input: `<data><msg>Hello &amp; World</msg></data>`,
138 - expected: `{"data":{"msg":"Hello & World"}}`,
139 - },
140 - {
141 - name: "Boolean-like text values",
142 - input: `<flags><enabled>true</enabled><disabled>false</disabled></flags>`,
143 - expected: `{"flags":{"enabled":"true","disabled":"false"}}`,
144 - },
145 - }
146 -
147 - for _, tt := range tests {
148 - t.Run(tt.name, func(t *testing.T) {
149 - input := Value{Data: tt.input, Type: ValueTypeStr}
150 - step := Step{Type: StepTypeXMLToJSON}
151 -
152 - result, err := p.Execute("item1", input, step)
153 - if err != nil {
154 - t.Fatalf("XML to JSON failed: %v", err)
155 - }
156 -
157 - if len(result.Metrics) == 0 {
158 - t.Fatal("Expected metrics in result")
159 - }
160 -
161 - // Compare as JSON structures
162 - var expected, actual interface{}
163 - if err := json.Unmarshal([]byte(tt.expected), &expected); err != nil {
164 - t.Fatalf("Failed to parse expected JSON: %v", err)
165 - }
166 - if err := json.Unmarshal([]byte(result.Metrics[0].Value), &actual); err != nil {
167 - t.Fatalf("Failed to parse actual JSON: %v\nGot: %s", err, result.Metrics[0].Value)
168 - }
169 -
170 - expectedNorm, _ := json.Marshal(expected)
171 - actualNorm, _ := json.Marshal(actual)
172 -
173 - if string(expectedNorm) != string(actualNorm) {
174 - t.Errorf("Expected: %s\nGot: %s", tt.expected, result.Metrics[0].Value)
175 - }
176 - })
177 - }
178 -}
179 -
180 -// TestXMLToJSONErrorHandling tests invalid XML inputs
181 -func TestXMLToJSONErrorHandling(t *testing.T) {
182 - p := NewPreprocessor("test-shard")
183 -
184 - tests := []struct {
185 - name string
186 - input string
187 - }{
188 - {
189 - name: "Malformed XML - unclosed tag",
190 - input: `<root><unclosed>`,
191 - },
192 - {
193 - name: "Malformed XML - mismatched tags",
194 - input: `<root><child></wrong></root>`,
195 - },
196 - {
197 - name: "Empty input",
198 - input: ``,
199 - },
200 - {
201 - name: "Not XML at all",
202 - input: `This is just plain text`,
203 - },
204 - }
205 -
206 - for _, tt := range tests {
207 - t.Run(tt.name, func(t *testing.T) {
208 - input := Value{Data: tt.input, Type: ValueTypeStr}
209 - step := Step{Type: StepTypeXMLToJSON}
210 -
211 - _, err := p.Execute("item1", input, step)
212 - if err == nil {
213 - t.Errorf("Expected error for invalid XML input: %s", tt.input)
214 - }
215 - })
216 - }
217 -}
218 -
219 -// TestXMLToJSONLLDCompatible tests that output is compatible with Zabbix LLD
220 -func TestXMLToJSONLLDCompatible(t *testing.T) {
221 - p := NewPreprocessor("test-shard")
222 -
223 - // Example: XML that could be used for LLD discovery
224 - input := `<discovery>
225 - <host name="server1" ip="192.168.1.1"/>
226 - <host name="server2" ip="192.168.1.2"/>
227 - <host name="server3" ip="192.168.1.3"/>
228 - </discovery>`
229 -
230 - step := Step{Type: StepTypeXMLToJSON}
231 - result, err := p.Execute("item1", Value{Data: input, Type: ValueTypeStr}, step)
232 - if err != nil {
233 - t.Fatalf("XML to JSON failed: %v", err)
234 - }
235 -
236 - // Parse result
237 - var data map[string]interface{}
238 - if err := json.Unmarshal([]byte(result.Metrics[0].Value), &data); err != nil {
239 - t.Fatalf("Failed to parse JSON: %v", err)
240 - }
241 -
242 - // Verify structure is compatible with LLD
243 - discovery, ok := data["discovery"].(map[string]interface{})
244 - if !ok {
245 - t.Fatal("Expected 'discovery' key in output")
246 - }
247 -
248 - hosts, ok := discovery["host"].([]interface{})
249 - if !ok {
250 - t.Fatal("Expected 'host' to be an array (multiple hosts)")
251 - }
252 -
253 - if len(hosts) != 3 {
254 - t.Errorf("Expected 3 hosts, got %d", len(hosts))
255 - }
256 -
257 - // Each host should have @name and @ip attributes
258 - for i, h := range hosts {
259 - host := h.(map[string]interface{})
260 - if _, ok := host["@name"]; !ok {
261 - t.Errorf("Host %d missing @name attribute", i)
262 - }
263 - if _, ok := host["@ip"]; !ok {
264 - t.Errorf("Host %d missing @ip attribute", i)
265 - }
266 - }
267 -}
src/go/plugin/scripts.d/pkg/zabbixpreproc/xpath_step.go deleted
-107
@@ -1,107 +0,0 @@
1 -package zabbixpreproc
2 -
3 -import (
4 - "fmt"
5 - "math"
6 - "regexp"
7 - "strings"
8 -
9 - "github.com/antchfx/xmlquery"
10 - "github.com/antchfx/xpath"
11 -)
12 -
13 -// XPath regex patterns compiled once at package init
14 -var (
15 - // Matches empty XML tags like <tag></tag> or <tag attr="value"></tag>
16 - xpathEmptyTagRegex = regexp.MustCompile(`<([a-zA-Z][a-zA-Z0-9]*)(\s+[^>]*)?></([a-zA-Z][a-zA-Z0-9]*)>`)
17 -)
18 -
19 -// xpathExtract extracts values from XML using XPath expression.
20 -func xpathExtract(value Value, paramStr string) (Value, error) {
21 - expr := strings.TrimSpace(paramStr)
22 - if expr == "" {
23 - return Value{}, fmt.Errorf("xpath expression is required")
24 - }
25 -
26 - // Parse XML
27 - doc, err := xmlquery.Parse(strings.NewReader(value.Data))
28 - if err != nil {
29 - return Value{}, fmt.Errorf("invalid XML: %w", err)
30 - }
31 -
32 - // Compile and evaluate XPath expression
33 - xpathExpr, err := xpath.Compile(expr)
34 - if err != nil {
35 - return Value{}, fmt.Errorf("invalid xpath expression: %w", err)
36 - }
37 -
38 - // Evaluate the expression
39 - result := xpathExpr.Evaluate(xmlquery.CreateXPathNavigator(doc))
40 -
41 - // Handle different result types
42 - switch v := result.(type) {
43 - case *xpath.NodeIterator:
44 - // Node selection - collect nodes
45 - var output strings.Builder
46 - for v.MoveNext() {
47 - node := v.Current().(*xmlquery.NodeNavigator).Current()
48 -
49 - // For text and attribute nodes, return the data
50 - // For element nodes, return the XML representation
51 - if node.Type == xmlquery.TextNode || node.Type == xmlquery.AttributeNode {
52 - output.WriteString(node.Data)
53 - } else {
54 - xml := node.OutputXML(true)
55 - // Convert empty tags to self-closing format (e.g., <a></a> to <a/>)
56 - xml = convertToSelfClosingTags(xml)
57 - output.WriteString(xml)
58 - }
59 - }
60 - return Value{Data: output.String(), Type: ValueTypeStr}, nil
61 -
62 - case string:
63 - // String result from string() function
64 - return Value{Data: v, Type: ValueTypeStr}, nil
65 -
66 - case float64:
67 - // Numeric result from arithmetic expressions
68 - // Check for invalid numeric results
69 - if math.IsInf(v, 0) || math.IsNaN(v) {
70 - return Value{}, fmt.Errorf("invalid numeric result from XPath expression")
71 - }
72 - return Value{Data: fmt.Sprintf("%g", v), Type: ValueTypeStr}, nil
73 -
74 - case bool:
75 - // Boolean result from boolean expressions
76 - if v {
77 - return Value{Data: "true", Type: ValueTypeStr}, nil
78 - }
79 - return Value{Data: "false", Type: ValueTypeStr}, nil
80 -
81 - default:
82 - // Unknown type - convert to string
83 - return Value{Data: fmt.Sprint(v), Type: ValueTypeStr}, nil
84 - }
85 -}
86 -
87 -// convertToSelfClosingTags converts empty XML tags to self-closing format
88 -func convertToSelfClosingTags(xml string) string {
89 - // Match patterns like <tag></tag> or <tag attr="value"></tag>
90 - // Note: Go regex doesn't support backreferences in pattern, so we match and check manually
91 - return xpathEmptyTagRegex.ReplaceAllStringFunc(xml, func(match string) string {
92 - // Extract the tag names from opening and closing tags
93 - submatches := xpathEmptyTagRegex.FindStringSubmatch(match)
94 - if len(submatches) >= 4 {
95 - openTag := submatches[1]
96 - attrs := submatches[2]
97 - closeTag := submatches[3]
98 -
99 - // Only convert if opening and closing tags match
100 - if openTag == closeTag {
101 - return "<" + openTag + attrs + "/>"
102 - }
103 - }
104 - // Return unchanged if tags don't match
105 - return match
106 - })
107 -}