feat(rust-plugins): rate/delta DSL primitive with persistent state - #6419
Open
julienmathis wants to merge 6 commits into
Open
julienmathis wants to merge 6 commits into
julienmathis wants to merge 6 commits into
Conversation
…, bound walks, timeouts & retries The engine blindly trusted the agent it queried, which is precisely the untrusted party of the dialogue: - Agent errors are now detected: a response with error-status != 0 produces a typed error naming the RFC 3416 status (e.g. "noSuchName"). - Request/response correlation: every request carries a fresh request-id, and received datagrams are validated (request-id, community echo, PDU type) before being accepted — unrelated datagrams (e.g. a late retransmission of a previous request) are discarded instead of being consumed as the current answer. - Bounded walks: OIDs must be strictly increasing during a walk (the classic snmpwalk "OID not increasing" guard) and a single walk may collect at most 100 000 values — both protect against a buggy or malicious agent looping the walk or streaming endless data. - Configurable timeouts and retries via three new CLI options: --timeout (per-attempt receive timeout, default 1s), --snmp-retries (default 2), and --collect-timeout (global budget for the whole collection, default 50s, so the plugin exits with a clean UNKNOWN before centengine's own kill timeout). - The UDP receive buffer was 1024 bytes, silently truncating large bulk responses; raised to 65535. Connection parameters (target, community, timeouts, retries) are now threaded through a single SnmpConfig instead of loose string arguments, and the two walk loops share one implementation. Test plan: - cargo build --release succeeds - cargo test: 75 passed, 0 failed (10 new: RFC 3416 names, response validation, non-increasing OID, varbind cap, expired deadline) - Manual check against an unroutable address: default settings exit UNKNOWN after retries*timeout with the attempt count in the message; --collect-timeout correctly preempts a longer per-attempt timeout
2 tasks
…-failure message The protocol-hardening commit changed the no-connection error message from "Could not connect to X is the hostname..." to "No valid SNMP response from X after N attempts (timeout Ts per attempt)" (clearer: it names the retry budget actually exhausted), but never updated the Robot fixture that pins it. Both cgs-no-connection cases have been failing CI since this branch was opened.
julienmathis
force-pushed
the
feat/dsl-rate-delta
branch
from
September 8, 2026 12:14
c02fdf1 to
4b8367c
Compare
snmp_bulk_get built a GetBulkRequest PDU for every "get" query, relying on a workaround that stripped a trailing .0 and depended on GetNext landing exactly one leaf ahead. GetBulk (even with max-repetitions=1) can never perform an exact match, so this silently returned the wrong value for any non-.0-suffixed OID, such as a specific row of a multi-row table. Every currently shipped definition only queries .0-suffixed scalars, so the bug was latent. Switch to a real GetRequest and drop the trailing-zero workaround, which is no longer needed.
…ips on table walks Every change here is backed by a measurement (synthetic 10 000-row ifTable, 50 000 varbinds, snmpsim), not assumed: - GetBulk max-repetitions raised from a hardcoded 10 to a default of 50 (Perl parity), configurable globally via --maxrepetitions and per-query via "max-repetitions" in the JSON collect entry. On the synthetic table this cuts requests from 5001 to 1001 (a walk of N rows now costs ceil(N/max_repetitions) round-trips instead of ceil(N/10)). - Nagios thresholds are now parsed once per metric (parse_threshold) instead of once per vector element — a 10 000-row table with warning+critical thresholds no longer reparses the same two strings 10 000 times, and threshold syntax errors now name the metric and field. - The UDP socket and receive buffer are allocated once per walk instead of once per request (open_socket / shared buffer threaded through send_request), removing a bind+connect+alloc per round trip on large walks. - eval_str's macro-matching regex is compiled once (OnceLock) instead of on every template evaluation. Test plan: - cargo build --release succeeds - cargo test: 75 passed, 0 failed - Manual check against an unroutable address: retry/timeout behavior unchanged (UNKNOWN + exit 3 after the configured attempts)
…Perfetto export
Replaces log/env_logger with tracing/tracing-subscriber across the
crate, in three activation modes:
- default (production): silent on stderr, zero measurable overhead.
- PLUGIN_LOG=debug (or any level, mirroring the historical env var):
structured logs plus **span durations on close**
(`walk{oid=...} close time.busy=5.65ms`) — which stage was slow on
this host, from one env var. PLUGIN_LOG's default moves from `info`
to `warn`: a plugin must be silent on stderr nominally, and per-value
info-level formatting was hot-path waste (deliberate behavior change).
- --trace-file <path>: full Chrome-trace recording, loadable in
Perfetto — validated manually (JSON parses, span hierarchy present).
Spans: check -> collect -> walk{oid}/get -> request{id} per attempt,
metric{name}, aggregation{name}, output.
No OTLP/collector export: a process running at high check-per-minute
rates must not ship spans over the network; out of scope by design.
snmp_plugin() now returns the exit code instead of calling
process::exit mid-flight: destructors run on every path, so the trace
flush guard (held for the whole function) always writes the file
before the process exits. main() calls process::exit(code) once, at
the top level. The CLI argument loop is flattened to
`while let Some(arg) = parser.next()? { ... }`, so a lexopt parse
error now goes through the same UNKNOWN + exit(3) path as every other
error instead of a bare `Error: {err}` + exit(1).
Test plan:
- cargo build --release / cargo test: 75 passed, 0 failed
- Manual: nominal run has 0 bytes on stderr; PLUGIN_LOG=debug shows
nested check/metric/aggregation spans with time.busy/time.idle on
close; --trace-file produces a valid Chrome-trace JSON; unknown-flag,
missing-JSON, --help and unroutable-target retry behavior unchanged
SNMP counters (ifInOctets, ...) are monotonically increasing values;
turning them into per-second rates requires the value and timestamp of
the previous run. Adds a "rate": true field on a collect entry:
{ "name": "if", "oid": "1.3.6.1.2.1.2.2.1", "query": "Walk",
"labels": { "1.2": "descr", "1.10": "in", "1.16": "out" },
"rate": true }
Every numeric value of the entry becomes a per-second rate computed
against the previous run; string columns (labels) are left untouched
and vectors stay aligned.
Design:
- Identity = full OID, never the table position — a new interface
appearing between runs cannot shift its neighbors' rates.
- State files (new src/state.rs module): mode 0600 + atomic write
(temp file + rename + fsync) — never world-readable, never
half-written. A read failure (missing/corrupt file) is never fatal
(fresh start); a write failure IS fatal (a stale reference would
silently produce wrong rates forever).
- 32-bit counter wraparound corrected; a 64-bit decrease or a missing
previous instance is treated as a reset and yields one aligned 0.0
sample rather than desynchronizing the vector.
- First run: "OK: Buffer creation", exit 0 (Perl parity).
- New --statefile-dir CLI option (Perl parity, default
/var/lib/centreon/centplugins).
- Instrumented with state_read/state_write spans (visible via
PLUGIN_LOG=debug or --trace-file, see the tracing PR).
SnmpResult gains a `samples: Vec<(item_key, full_oid, value)>` field,
populated only when a collect entry asks for rates (capture_samples
threaded through the walk/get functions and process_response), so
apply_rate can rebuild each numeric vector in original push order
while keying state persistence by OID.
Test plan:
- cargo build --release / cargo test: 84 passed, 0 failed (9 new:
wraparound, reset, dt<=0, state roundtrip + 0600 assert + corrupt-file
recovery, two-run rate integration, new-instance alignment, sample
capture)
- --check-format validates the new example (examples/new-traffic-rate.json)
- Manual: state files created under a temp statefile-dir are mode 0600
and survive a corrupt-content injection without crashing (exercised
by the state.rs unit tests directly against the real filesystem)
julienmathis
force-pushed
the
feat/dsl-rate-delta
branch
from
September 13, 2026 20:19
4b8367c to
dff4638
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Stacked on #6418 (tracing — state_read/state_write spans, reuses the SnmpConfig chain). SNMP counters (
ifInOctets, ...) are monotonically increasing values; turning them into per-second rates requires the value and timestamp of the previous run. Adds a"rate": truefield on a collect entry:{ "name": "if", "oid": "1.3.6.1.2.1.2.2.1", "query": "Walk", "labels": { "1.2": "descr", "1.10": "in", "1.16": "out" }, "rate": true }Every numeric value of the entry becomes a per-second rate computed against the previous run; string columns (labels) are left untouched and vectors stay aligned. See
examples/new-traffic-rate.json.Design
src/state.rsmodule): mode0600+ atomic write (temp file + rename + fsync) — never world-readable, never half-written. A read failure (missing/corrupt file) is never fatal (fresh start); a write failure IS fatal (a stale reference would silently produce wrong rates forever).0.0sample rather than desynchronizing the vector.OK: Buffer creation, exit 0 (Perl parity).--statefile-dirCLI option (Perl parity, default/var/lib/centreon/centplugins).state_read/state_writespans (visible viaPLUGIN_LOG=debugor--trace-file, see feat(rust-plugins): tracing instrumentation — spans, span durations, Perfetto export #6418).SnmpResultgains asamples: Vec<(item_key, full_oid, value)>field, populated only when a collect entry asks for rates (capture_samplesthreaded through the walk/get functions andprocess_response), soapply_ratecan rebuild each numeric vector in original push order while keying state persistence by OID.Test plan
cargo build --release/cargo test: 84 passed, 0 failed (9 new: wraparound, reset, dt≤0, state roundtrip + 0600 assert + corrupt-file recovery, two-run rate integration, new-instance alignment, sample capture)--check-formatvalidates the new example0600and survive a corrupt-content injection without crashing (exercised by thestate.rsunit tests directly against the real filesystem)