collectors-snmp-trap-profiles
GitHub用于编写或审查 Netdata SNMP Trap 配置文件、指标规则及生成器,确保符合 MIB 和分类契约。
Trigger Scenarios
Install
npx skills add netdata/netdata --skill collectors-snmp-trap-profiles -g -y
SKILL.md
Frontmatter
{
"name": "collectors-snmp-trap-profiles",
"description": "Author or review Netdata SNMP trap profiles, metrics\/charts rules, loader schema, snmp-trap-profile-gen, stock catalogue\/compression and category\/severity changes. Preserve MIB, varbind, taxonomy and cardinality contracts. Trap-log queries and SNMP polling profiles use separate skills."
}
SNMP Trap Profile Authoring
Use this skill for authoring or reviewing the affected contracts under:
src/go/plugin/go.d/config/go.d/snmp.trap-profiles/(stock packdefault/,catalogue.json,profile-format.md)src/go/cmd/snmptrapprofilegen/(the generator; installed assnmp-trap-profile-gen)src/go/plugin/go.d/collector/snmp_traps/internal/catalog/when the change is about what a profile may contain
Select The Task
Apply AGENTS.md#skill-selection. Review checks affected rules against source and existing validation evidence;
implementation procedures below do not require a reviewer to create artifacts, regenerate the pack, call a classifier
or install profiles. Select additional sections when the changed behavior reaches them.
| Task | Read |
|---|---|
| Profile contents, templates or metric rules | Profile checks and the profile-format owner |
| Generator or loader behavior | Applicable generator/profile checks and their source owners; follow emitted-schema effects |
| Stock pack regeneration or compression | Regeneration, file-size/compression and catalogue contracts; inspect affected generator changes |
| Category or severity set changes | Taxonomy section and every coupled surface it lists |
| Trap log investigation or polling profiles | query-snmp-traps or collectors-snmp-profiles |
Authoritative sources
This skill holds only what the documents below lack. Point at them; do not restate them here.
src/go/plugin/go.d/config/go.d/snmp.trap-profiles/profile-format.md: the profile format (file layout, the varbinds table, trap entries,.0.tolerance, description templates,metrics:/charts:rules and their validation list, categories, severities, cardinality, operator overrides, generated stock profiles). It ships with the pack underusr/lib/netdata/conf.d/go.d/snmp.trap-profiles/, so it is operator-facing and must stay true to the loader.src/go/plugin/go.d/collector/snmp_traps/ARCHITECTURE.md: how the collector works (life of a trap, journal field contract, package map, "Where To Change Things", and the validation commands; the sections a rule below relies on are cited assrc/go/plugin/go.d/collector/snmp_traps/ARCHITECTURE.md#where-to-change-thingsandsrc/go/plugin/go.d/collector/snmp_traps/ARCHITECTURE.md#validation). Collector code changes start there, not here.docs/npm/snmp-traps/(published operator docs):trap-profiles.md(override versus new profile),configuration.md(every job option),field-reference.md(everyTRAP_*field),metrics.md(built-in charts and dimensions).- Sibling skills:
collectors-snmp-profilesfor polling profiles;query-snmp-trapsfor reading trap logs. - Design history (rejected alternatives, the comparison with other trap systems, the phase plans) is not maintained in the tree; the pre-implementation design records that used to live in this directory are in git history.
Required checks before changing a profile
-
Trap
name:is MIB-qualified<MIB-MODULE>::<symbol>(IF-MIB::linkDown). Bare symbols are reused across vendor MIB modules and are not unique; the qualified form is whatsnmptranslateproduces and what lands inTRAP_NAME. If the OID changes, the name changes. Different OIDs must have different names (profile-format.md, "Trap entries"). -
Check
MAX-ACCESSof the source MIB object for every varbind. Anot-accessibleindex object still belongs in the varbinds table, so that the rare device that does include it gets it named inTRAP_JSONand, when non-sensitive and non-redundant, as an indexedTRAP_VAR_*field. Never use it as adescription:template variable on its own: an SNMP entity normally does not send it in the trap PDU, so the placeholder renders empty. No in-tree artifact can check this; it needs the MIB. -
Every varbind reference resolves. A name in a trap's
varbinds:list must exist in the file-scopedvarbinds:table or be an inline{name, oid, type}dict on that trap. A dangling name renders empty in the description and produces a misleading journal message. Table entries need bothoidandtype; an empty{}entry fails profile load. The generator drops extractor records with an empty name, OID, or type from the table and from every trap's reference list (buildProfile, pinned byTestBuildProfileDropsUnresolvedVarbinds); do the same by hand. -
Keep the metadata the loader validates. Trap
status:takes onlycurrent,deprecated,mandatory,obsolete,optional(validTrapStatusesin the generator,validStatusesininternal/catalog/profile.go). Varbindenum:is what renders{{value}}symbolically and whatequals/inpredicates match;constraints:documents the range. File-scopevendor:,mib_count:,trap_count:are emitted on every stock file. Do not strip any of them when editing. -
Categories: closed set of 8.
state_change,config_change,security,auth,license,mobility,diagnostic,unknown. Cross-cutting concerns (compliance scope, tenant, datacenter, change window) go inlabels:, not new slugs. Every profile entry declarescategoryandseverity(the loader rejects a missing or unknown value); only a trap with no profile match is logged withunknown/noticeat runtime, and operators reclassify it through the job'soverrides:. There is no "custom" category. Changing the set is taxonomy work. -
Severities: closed set of 8 full syslog names mapped to
PRIORITY=0..7:emerg,alert,crit,err,warning,notice,info,debug(neverwarn).emergis for true vendor catastrophe; routine events arewarning/notice/info;debugonly when the MIB itself marks the notification debug-level. -
Label cardinality. Label templates reference bounded varbinds only. Reject labels built from MAC addresses, source IPs, usernames, packet contents, RAID slot IDs, or any per-event identifier; the loader rejects unbounded label templates at profile load. High-cardinality content belongs in
description:(rendered intoMESSAGE), the indexedTRAP_VAR_*fields, andTRAP_JSON, none of which propagate to metrics. -
Label keys match
[a-z][a-z0-9_]*and emit asTRAP_TAG_<KEY_UPPERCASE>, whether they come from a profile'slabels:or from a listener job's per-OIDoverrides[].labels(there is no job-widelabels:option). The dedicated prefix makes collision with the plugin-ownedTRAP_*fields impossible, so the key syntax is the only check. Seedocs/npm/snmp-traps/field-reference.mdfor the shippedTRAP_*set (it is not closed at the profile level; new fields arrive with collector releases). -
Trap OID form and
.0.tolerance. Use the OID form the source MIB tooling produces. Lookup is exact-match first, then retries the alternate spelling with one.0.segment added or removed before the final arc (model.AlternateTrapOID, called fromEpoch.lookupLoaded). A catalogue that defines both spellings of one trap fails to load ("alternate form already defined"). The tolerance is trap-OID only: never normalize or alternate-match varbind OIDs. -
Stock versus operator. Files under
default/are generated and overwritten on regeneration (their header comment says so); never hand-edit them for site concerns. Operator profiles live in the user config directorygo.d/snmp.trap-profiles/(catalog_paths.go) in one of three forms: a complete same-identity replacement of a stock file, an independent different-identity addition, or a metric-only profile whose rules reference stock traps. Partial inheritance does not exist; anextends:key is rejected as unknown at profile load. Per-OID category, severity, and label overrides belong in the listener job'soverrides:(docs/npm/snmp-traps/trap-profiles.mdhas the decision table). -
No hand-authored journal fields. There is no
journal_fields:key:TRAP_VAR_*fields are derived from the received non-sensitive, non-redundant varbinds andTRAP_JSONkeeps the audit copy.display_hintis documented as a future varbind field but is not part of the loaded schema:varbinds:entries accept unknown keys (profileYAMLSpecininternal/catalog/load.go), so a hand-addeddisplay_hintis silently ignored and then overwritten on regeneration. When the renderer starts consuming DISPLAY-HINT metadata, the extractor,profile-format.md, andmodel.TrapEntrychange in the same cycle. -
Profile metrics only through the validated schema.
profile-format.md, "Optionalmetrics:rules andcharts:", owns the syntax, defaults, numeric source types, and the load-time rejection list; a listener job enables rules explicitly withprofile_metrics.include(rule names, never trap names or filenames). Checks the loader enforces (internal/catalog/metric_validate.go) that authors most often get wrong:- Chart IDs and contexts must not reuse the six built-in charts
events,severity,errors,dedup_suppressed,pipeline,profile_metric_diagnosticsor theirsnmp.trap.*contexts; a chart context defaults tosnmp.trap.<chart id>and must start withsnmp.trap.; metric names must not start with a reserved prefix (builtInProfileMetricChartIDs,reservedProfileMetricPrefixes). - Every
where:predicate selects exactly one string-valued source:varbind, orfieldfrom the closed setcategory,severity,trap_name,trap_oid; predicates AND; useabsent, notnotplusexists. identity.resource.key_from_varbindmust be an integer-like bounded varbind (INTEGER,Integer32,Unsigned32,Gauge32);Counter32,Counter64,TimeTicksaresamplevalues, not identity keys.missing:is one ofdrop,error,zero,unknown_dimension(zerois invalid forcounterandstate;unknown_dimensionneeds resource identity). Authoring rules the loader does NOT check, so review them by hand:- Every rule sharing a chart has the same label shape: do not mix resource and non-resource rules, or several resource classes, in one chart.
- Declare
lifecycleexplicitly on every chart that creates per-source or per-resource instances instead of relying on the loader's defaults; expired instances are removed and a returning identity starts a fresh series. - On a chart that carries a
staterule,lifecycle.expire_after_cyclesmust outlaststate.ttl: the TTL publishes the clear value once and only then expires the series, so a chart that expires first leaves the problem state as the last value. The loader validates the two fields independently and never compares them. A rule'swhere:is evaluated before itsstate.set_when/clear_whenpredicates. - Never use the community varbind or another sensitive varbind (
model.IsSensitiveVarbind) as a predicate or sample source; redaction happens downstream, not in rule validation. - Profile rules describe vendor or site semantics, never receiver health. Profile metrics update only after the trap is committed to the configured backend; dedup-suppressed and write-failed traps do not count.
- No stock profile ships
metrics:today (0 of 803). Stock rules would be a curation layer that the generator must preserve through a tested read-modify-write path from a reviewable, committed source recording rule name, trap, varbinds, type, chart, and cardinality evidence, and must validate against thevarbinds:andtraps:it just emitted before writing the file. That path does not exist; build it before adding stock rules, and check pack size and lazy-load memory when you do.
- Chart IDs and contexts must not reuse the six built-in charts
Required checks when editing the generator (src/go/cmd/snmptrapprofilegen/)
-
One Go binary, no runtime dependencies. CMake target
snmp_trap_profile_genbuildssnmp-trap-profile-genand installs it underusr/libexec/netdata/plugins.d/in theplugin-gocomponent; the pack build runs it withCGO_ENABLED=0. Do not add Python, CGO, SQLite, or a runtime MIB compiler to the shipped path. -
Subcommands are
extract,classify,emit,generate, andcompress-zstd(usage).generateis extract plus optional classify plus emit; the three stages exist separately for reruns on saved artifacts. -
Extraction stays incremental and memory-bounded. The corpus is too large for one MIB universe: keep batch-based gomib loading (
--batch-size, default 32), deterministic source priority, and the review artifacts under--out-dir:traps.jsonl,extraction-report.json,conflicts.json(duplicate trap OIDs),dot0-conflicts.json(both.0.spellings present),source-conflicts.json(one module name in several files).--baseline-profiles-diradds a stock-overlap report. If source discovery changes, rerun a representative multi-vendor corpus before touching the stock pack. -
Classification cache stays reviewable JSONL. One
Classificationrecord per trap keyed byhashTrap, withschema_versionandprompt_version; a record whose versions differ fromdefaultSchemaVerordefaultPromptVeris rejected, so bumpdefaultPromptVerwhenever the prompt or the taxonomy changes or the cache silently replays stale answers.--force-llmignores the cache. The cache path is derived from the default out-dir unless--cacheis passed explicitly, even when--out-dirdiffers. Never switch to SQLite or another opaque store. -
LLM output validation is mandatory. Every response passes the classifier response JSON Schema (
classifierResponseSchemaJSON, checked byvalidateClassifierResponseSchema), the template check (validateDescriptionTemplate: only the helpers inclassifierTemplateFuncMap, references checked against the trap record), and the style check (validateDescriptionStyle: ends withon {{hostname}}.,{{hostname}}exactly once). Off-taxonomy categories are remapped first byrepairInvalidCategory. Up tomaxLLMAttempts(5) tries, thenmechanicalClassification, or a hard failure under--require-llm. MIB text reaches the model wrapped as untrusted input (sanitizePromptText); keep that wrapping. -
Emission is deterministic and produces the file-scoped table (
buildProfile,writeProfileYAML):- trap
name:is MIB-qualified; varbind table names are bare symbols; - one table entry per varbind name; a name that recurs with a different OID or type falls back to an inline
{name, oid, type}dict on that trap (intended; do not "fix" it, and do not regress to inline everywhere); - records with an empty name, OID, or type are dropped from table and references; no
{}entries; - traps sort by OID (
compareOIDString) and the table keys and rule names are sorted, so regenerations diff cleanly; - the three-line header comment is part of the file and of its digest;
- the vendor slug (
vendorForOID:standard,ieee-lldp,ieee-802, the PEN slug orenterprise-<pen>, elseoid-<first arc>orunknown) is the output filename and therefore the identity an operator override replaces.
- trap
-
catalogue.jsonstays in sync. Each entry (profileCatalogueEntry) recordsfile,mib_count,mibs,sample_traps,trap_count,trap_oids,varbind_count,sha256, andmetric_rule_nameswhen the profile has rules (omitted otherwise, which is every stock file today; emitting stock rules also needs the curation path described in profile check 12).sha256is 64 lowercase hex over the exact bytes written, comments and final newline included (format pinned byTestStockProfileCatalogueRequiresValidSHA256); lazy hydration verifies it (TestStockProfileEpochBindsLazyHydrationToManifestContent). Catalog tests load all shipped profiles and require the manifest and the files to agree in both directions (TestStockProfileCatalogueMatchesDefaultFiles,TestStockCatalogueReconcilesPhysicalInventory,TestStockProfileDefaultFilesParse): regenerating profiles without the catalogue fails tests. -
PEN registry. The default is the bundled snapshot (
defaultPENFilePath; installed atusr/lib/netdata/conf.d/go.d/snmp.profiles/metadata/iana-enterprise-numbers.txt). With--refresh-pen, or when the file is missing or empty,loadPENsfetches--pen-urland a failed fetch aborts the run (TestLoadPENsRefreshFailureIsFatal). An air-gapped run needs the snapshot present.
Regenerating the stock pack
- Operation: this command overwrites stock profiles and the catalogue. Run it only for authorized pack regeneration; review or schema inspection does not require it.
- Classifier:
--classifysends MIB-derived text to the configured endpoint. Establish the source scope and endpoint/model within the task's authorization before running; the default local endpoint is not an offline parser. - Artifacts: from the repository root, create a fresh private run below.
- Cache: select a compatible earlier cache explicitly in
--cachefor an intentional rerun. Reuse can append and compact that file; copy it into the fresh run first when preserving the original matters.
(
set -e
umask 077
PROFILE_AUDIT_DIR="$PWD/.local/audits/snmp-trap-profiles"
mkdir -p "$PROFILE_AUDIT_DIR"
PROFILE_RUN_DIR="$(mktemp -d "$PROFILE_AUDIT_DIR/generate.XXXXXX")"
cd src/go
go run ./cmd/snmptrapprofilegen generate \
--source-dir /path/to/mibs \
--all \
--classify \
--require-llm \
--concurrency 20 \
--out-dir "$PROFILE_RUN_DIR" \
--cache "$PROFILE_RUN_DIR/classification-cache.jsonl" \
--profiles-out-dir ./plugin/go.d/config/go.d/snmp.trap-profiles/default \
--catalogue ./plugin/go.d/config/go.d/snmp.trap-profiles/catalogue.json
)
- Classification talks to an OpenAI-compatible endpoint (
--base-url, default a local server;--model); without--classifyevery trap keeps the extractor defaults: categoryunknown, severitynotice, description<qualified name> on {{hostname}}.. - Review the diff: ordering is deterministic, so unexpected churn means an extractor or prompt change.
- Counts quoted in docs (
ARCHITECTURE.md,docs/npm/snmp-traps/trap-profiles.md) describe the pack; recompute them fromcatalogue.jsonafter a regeneration rather than carrying old numbers.
The installed operator form converts site MIBs offline; the output under snmp-trap-profile-gen-output/profiles/ is
copied into the operator profile directory (profile-format.md, "Generated stock profiles").
Changing categories or severities (taxonomy work)
The sets are duplicated in code, alerts, and docs. Only the telemetry series are test-pinned
(TestJobCollectsExactRetainedMetricSet in internal/telemetry/job_test.go enumerates the per-category and
per-severity counters); nothing pins the generator's or the loader's sets. A change must touch every site:
- Generator
main.go:validCategories,validSeverities,severityPriority,repairInvalidCategory,mechanicalClassification(the no-LLM fallback hard-codes both sets), the classifier response JSON Schema (classifierResponseSchemaJSON), and the prompt text; bumpdefaultPromptVer. - Collector
internal/catalog/profile.go:validCategories,validSeverities,categoryList,severityList(the loader rejects profiles the generator would otherwise emit). - Per-slug surfaces in the collector: the
overrides[].categoryandoverrides[].severityenums inconfig_schema.json, theinternal/telemetrycounters and their test, theeventsandseveritychart dimensions incharts.yamlandmetadata.yaml, the OTLP severity mapping (otlpSeverityininternal/output/otlp), and thePRIORITYmapping ininternal/output/journal(grep -rnthe slug acrosssnmp_traps/). - Health:
src/health/health.d/snmp_traps.confhas severity-rate alert templates foremerg,alert,crit,err, andwarning(notice,info,debugdeliberately do not alert), mirrored in thealerts:list ofmetadata.yaml; a renamed or removed slug silently breaks them.grep -rnthe slug acrosssrc/health/health.d/too. - Docs:
profile-format.mdcategory and severity tables, and every operator page that enumerates the sets (grep -rlnthe slug acrossdocs/npm/snmp-traps/: todayREADME.md,trap-profiles.md,configuration.md,field-reference.md,metrics.md,alerts.md,usage-and-output.md,journal-and-querying.md,investigation-playbooks.md). - Re-run classification for the full corpus: existing cache records were produced under the old taxonomy.
Add tests that pin the generator's and the loader's sets when you touch them.
File size and compression
- Stock profile YAMLs stay raw in the repository so
git diffreviews them. - The pack build (CMake) runs the generator's
compress-zstd --rmsubcommand viago runon a copy of the pack and installs*.yaml.zstpluscatalogue.json.zst; the installed pack must stay compressed. The loader accepts profiles as.yaml.zst,.yml.zst,.yaml, or.yml(internal/catalog/load.go) and the manifest ascatalogue.jsonorcatalogue.json.zst, never gzip (internal/catalog/stock.go). - Operator profiles stay uncompressed
.yamlfor editability. - If one vendor file passes about 10 MB in the repository, cut description verbosity rather than hide generated bloat behind compression.
Validation
Use the suites for the affected generator/profile contract, including consumers when their behavior changes. Review checks relevant existing results and can run isolated local tests; it does not execute the stock-regeneration recipe.
cd src/go
go test -count=1 ./cmd/snmptrapprofilegen/
go test -count=1 ./plugin/go.d/collector/snmp_traps/internal/catalog/...
The catalog tests load all shipped profiles and verify the manifest. For collector code changes run the full
snmp_traps suite with -race as src/go/plugin/go.d/collector/snmp_traps/ARCHITECTURE.md#validation describes.
Version History
- 1e97a0f Current 2026-09-23 11:23


