---
name: project-snmp-trap-profiles-authoring
description: Use when editing Netdata SNMP trap profile YAMLs, trap profile metric rules, the trap profile-format documentation, the snmp-trap-profile-gen Go helper, or running a regeneration of the OOB trap profile pack. Enforces the closed 8-category / 8-severity taxonomy, the file-scoped varbinds-table pattern, metric cardinality discipline, and stock/operator separation.
---

# SNMP Trap Profile Authoring

Use this skill before editing files under:

- `src/go/plugin/go.d/config/go.d/snmp.trap-profiles/`
- `src/go/cmd/snmptrapprofilegen/` (shipped helper source)
- `.agents/skills/project-snmp-trap-profiles-authoring/netdata.md` (when the change touches profile schema or trap subsystem decisions that the profiles encode)

The authoritative schema reference is
`src/go/plugin/go.d/config/go.d/snmp.trap-profiles/profile-format.md`. The
authoritative subsystem design is
`.agents/skills/project-snmp-trap-profiles-authoring/netdata.md`. This skill is the working checklist
that keeps repository edits aligned with both.

## SNMP Trap Design Docs

The Netdata-owned trap design docs are committed in this skill folder:

- `netdata.md` — authoritative subsystem design
- `trap-metrics-profiles.md` — trap metric rules
- `netdata-snmp-hub-architecture.md` — hub architecture
- `pipeline-internals.md` — pipeline internals
- `decisions/` — accepted design decisions (ADRs)

Research evidence is local-only working memory (not committed) under
`.agents/sow/specs/snmp-traps/research/` (`domain/`, `playbooks/`,
`netdata-existing/`, `external-systems/`, `comparison/`). It informs the design
docs but is not itself a Netdata product contract.

When a research finding becomes a product rule, copy the accepted rule into one
of the committed design docs above (or a `decisions/` entry) and cite the
research path as evidence. Do not put research files beside the committed design
docs.

## Trap OID `.0.` tolerance

Profile authors should use the canonical trap OID form produced by the source
MIB/tooling. The receiver lookup is exact-match-first and then tolerates the
SMIv1 / SMIv2 trap-OID ambiguity by adding or removing a single `.0.` segment
immediately before the final OID arc on primary miss. This tolerance is
trap-OID-only: do not normalize or alternate-match varbind OIDs.

## Required checks before changing a profile

1. **Trap `name:` must be MIB-qualified.** Every trap entry's `name:`
   field uses the canonical SMI form `<MIB-MODULE>::<symbol>` (e.g.
   `IF-MIB::linkDown`, `CISCO-CONFIG-MAN-MIB::ccmCLIRunningConfigChanged`).
   Vendors reuse bare symbolic names across product-line MIB modules; the
   bare symbol is NOT globally unique. The qualified form matches what
   `snmptranslate` / `snmptrapd` / MIB browsers produce and is what the
   plugin writes to the `TRAP_NAME` journal field. Rule: if the OID
   changes, the `name:` slug MUST change.

2. **Resolve every varbind reference.** A name in a trap entry's `varbinds:`
   list MUST exist in the file-scoped `varbinds:` table or be an inline dict
   on the trap entry. Dangling name references are a bug — they render as empty
   values in restricted templates and produce misleading journal messages.

3. **Identify the source MIB object for every varbind.** Check the object's
   `MAX-ACCESS`. `not-accessible` index objects must still be declared in the
   table (they ship inside `TRAP_JSON` and, when non-sensitive/non-redundant,
   indexed `TRAP_VAR_*` fields), but never as a `description:` template
   variable on its own — varbinds an SNMP entity will not send in a trap PDU
   never resolve at runtime.

4. **File-scoped `varbinds:` table entries require both `oid` and `type`.**
   Varbind records with `resolved: false` (the MIB-extractor couldn't
   resolve the OBJECT-TYPE through the IMPORTS chain) MUST be dropped
   from both the table and the per-trap reference list. An empty `{}`
   entry under `varbinds:` violates the schema and is rejected by the
   plugin at profile load.

5. **Categories: closed set of 8.** `category` must be one of
   `state_change`, `config_change`, `security`, `auth`, `license`, `mobility`,
   `diagnostic`, `unknown`. Do not introduce new categories. Cross-cutting
   concerns (compliance scope, tenant, datacenter, change window…) belong in
   `labels:`, not as new category slugs. Operator-authored OIDs default to
   `unknown` and the operator overrides category/severity/labels in plugin
   config — there is no separate "custom" category slug.

6. **Severities: closed set of 8, mapped to syslog PRIORITY.** `severity`
   must be one of `emerg`, `alert`, `crit`, `err`, `warning`, `notice`,
   `info`, `debug` (full names — not `warn`). The plugin maps these to
   `PRIORITY=0..7` on the journal entry. `emerg` is reserved for true vendor
   catastrophe; default to `warning`/`notice`/`info` for routine events.
   `debug` is rare and only for traps the MIB itself marks as debug-level.

7. **Cardinality discipline on `labels:`.** Label templates must reference
   bounded-cardinality varbinds only. Reject (do not commit) labels that
   reference MAC addresses, source IPs, usernames, packet contents, RAID
   slot IDs, or any per-event identifier. High-cardinality content belongs
   in `description:` (rendered into MESSAGE), indexed `TRAP_VAR_*` journal
   fields, and `TRAP_JSON`, not in metric-propagating labels.

8. **Label keys use a structurally-safe namespace.** All labels (from
   profile `labels:` AND operator config `labels:`) emit as
   `TRAP_TAG_<KEY_UPPERCASE>` journal fields. The dedicated `TRAP_TAG_*`
   namespace removes any risk of collision with the plugin-controlled
   `TRAP_*` field set (`TRAP_OID`, `TRAP_NAME`, `TRAP_CATEGORY`, etc. — see
   spec §11). The only remaining validation is the lowercase-key syntax
   rule (`[a-z][a-z0-9_]*`). Pick label keys that read clearly.

9. **Stock vs operator separation.** Files under
   `src/go/plugin/go.d/config/go.d/snmp.trap-profiles/default/` are stock
   vendor-curated profiles, regenerated by `src/go/cmd/snmptrapprofilegen/`
   and shipped from Netdata as
   `/usr/libexec/netdata/plugins.d/snmp-trap-profile-gen`. Do not hand-edit
   them for site-specific concerns; site overrides belong under
   `/etc/netdata/go.d/snmp.trap-profiles/` and are documented in
   `profile-format.md` § "Operator overrides". Operator composition has three
   supported forms: a complete same-identity replacement, an independent
   different-identity addition, or a metric-only profile that references stock
   traps. Partial inheritance is unsupported; `extends:` is rejected.

10. **Profile metrics use the validated `metrics:` / `charts:` schema.** Trap
    profiles may define optional trap-to-metric rules only through the schema in
    `src/go/plugin/go.d/config/go.d/snmp.trap-profiles/profile-format.md`.
    Listener jobs decide enablement with `profile_metrics`. Do not add ad hoc
    metric fields, unbounded labels, or site-specific metric choices to stock
    profiles. Site-specific metric rules belong in operator profile files under
    `/etc/netdata/go.d/snmp.trap-profiles/`.

    Required profile-metric authoring checks:
    - Rule types are only `counter`, `sample`, and `state`; use canonical
      fields for every profile. Jobs enable rules explicitly by name with
      `profile_metrics.include`.
    - `where:` predicates are ANDed and may use `equals`, `in`, `exists`,
      `absent`, `greater_than`, `less_than`, `range`, and `not`; never combine
      `not` with `exists` or `absent`, and never define a predicate without a
      condition operator.
    - Every predicate MUST select exactly one string-valued source: `varbind`
      or `field`. Use separate `where` entries for additional AND constraints.
    - `sample` rules may read only numeric varbind types documented in
      `profile-format.md`; `TimeTicks` is converted to seconds before `scale`.
      `Counter32`, `Counter64`, and `TimeTicks` are valid for sample rules,
      not resource identity keys.
    - `state` rules use either separate `problem_trap` / `clear_trap` OIDs or
      same-OID `state.set_when` / `state.clear_when` predicates. `state.ttl`
      must be a positive Go duration string. TTL expiry clears the state once
      and removes the series after that successful collection.
    - `identity.resource.key_from_varbind` MUST reference an integer-like
      bounded varbind (`INTEGER`, `Integer32`, `Unsigned32`, or `Gauge32`).
      Never use strings, MACs, usernames, addresses, payloads, or event IDs as
      metric resource keys.
    - All rules sharing one chart MUST have the same label shape. Do not mix
      resource and non-resource rules in one chart, and do not mix multiple
      resource classes in one chart.
    - Charts that can create source or resource instances MUST have bounded
      lifecycle settings. Expired series are removed; if the same identity
      appears again, the next committed trap creates a fresh series.
    - `missing: unknown_dimension` is allowed only with resource identity.
      `missing: drop` increments rule-miss diagnostics; `missing: error`
      increments extraction-failure diagnostics.
    - Metric names, chart IDs, and chart contexts MUST NOT collide with
      built-in receiver charts, the built-in `profile_metric_diagnostics`
      chart, or any other loaded profile rule/chart.
      Reserved metric prefixes include `snmp_trap_events_`,
      `snmp_trap_severity_`, `snmp_trap_errors_`, `snmp_trap_dedup_`,
      `snmp_trap_pipeline_`, `snmp_trap_metric_`, and
      `snmp_trap_profile_metrics_`. Built-in receiver health is job-scoped;
      profile rules should describe vendor or site semantics, not duplicate
      receiver pipeline health.
    - Profile metrics update only after the trap is successfully committed to
      the configured journal and/or OTLP backend. Dedup-suppressed and
      write-failed traps do not update profile metrics.

11. **No `journal_fields:` list in profiles.** The plugin derives indexed
    `TRAP_VAR_*` journal fields automatically from received non-sensitive,
    non-redundant event varbinds, and keeps the structured audit copy in
    `TRAP_JSON`. There is no profile knob to hand-author per-OID journal
    field names.

12. **`display_hint` is reserved, not yet emitted.** `profile-format.md`
    documents `display_hint` (e.g. `1x:` for MAC, `1d.1d.1d.1d` for IPv4)
    as a future varbind field. The extractor keeps display hints in
    intermediate JSONL when `gomib` exposes them, but the stock profile
    emitter does not write `display_hint` today. Do not add `display_hint`
    keys by hand to stock profiles — they would be silently overwritten on
    regeneration. When the plugin's renderer needs display-hint formatting,
    the emitter and loader will be updated in the same regeneration cycle.

## Required checks when editing the generator (`src/go/cmd/snmptrapprofilegen/`)

1. **The shipped helper must remain a single Go binary.** It is built by CMake
   as `snmp-trap-profile-gen`, installed under
   `usr/libexec/netdata/plugins.d/`, and packaged in the `plugin-go`
   component. Do not add Python, CGO, SQLite, or runtime MIB compiler
   dependencies to the shipped operator path.

2. **Extraction must remain incremental.** The full corpus is too large to
   load as one global MIB universe. Keep the batch-based gomib loading path,
   deterministic source priority, duplicate-module `source-conflicts.json`,
   OID-level `conflicts.json`, and bounded memory validation. If source
   discovery changes, rerun at least a representative multi-vendor corpus
   before touching the stock pack.

3. **Classification cache stays reviewable text.** The cache is deterministic
   JSONL keyed by the classifier input hash. Do not switch to SQLite or another
   opaque cache for committed or CI-reviewed state.

4. **LLM output validation is mandatory.** Model responses must validate
   against the JSON Schema and the semantic validators: closed category,
   closed severity, exact template helper allowlist, and uniform description
   style ending with ` on {{hostname}}.`. Retry invalid responses up to five total
   attempts before mechanical fallback, or hard failure under `--require-llm`.

5. **YAML emission** is the producer of files in `default/`. It must:
   - MIB-qualify every trap `name:` as `<MIB-MODULE>::<symbol>` so the
     slug is globally unique;
   - dedup varbinds into the file-scoped table (do not regress to per-trap
     inline varbinds — see netdata.md §7 and `profile-format.md`);
   - drop varbind records with no resolvable `oid` (extractor's
     `resolved: false` cases) from both the table and the per-trap
     reference list — never emit empty `{}` table entries;
   - drop internal pipeline metadata (`enrichment_source`,
     `enrichment_attempts`) from the YAML output;
   - keep `catalogue.json` in sync: each entry must route its stock file by
     `trap_oids`, `mibs`, and `metric_rule_names` so the collector can hydrate
     only the file needed for a lookup;
   - emit each entry's required `sha256` as 64 lowercase hexadecimal
     characters computed over the exact decompressed YAML bytes written to
     disk, including comments and the final newline;
   - emit deterministic output (sorted varbind names, traps sorted by
     OID then name) so regenerations produce reviewable diffs.

6. **PEN registry handling** must use the bundled snapshot by default. CMake
   installs
   `src/go/plugin/go.d/config/go.d/snmp.profiles/metadata/iana-enterprise-numbers.txt`
   under `usr/lib/netdata/conf.d/go.d/snmp.profiles/metadata/`. `--refresh-pen`
   may fetch the current IANA registry when explicitly requested.

7. **Regenerating the stock pack** uses the Go helper:

   ```bash
   cd src/go
   go run ./cmd/snmptrapprofilegen generate \
     --source-dir /path/to/mibs \
     --all \
     --classify \
     --require-llm \
     --concurrency 20 \
     --out-dir /tmp/snmp-trap-profile-gen-output \
     --profiles-out-dir ../../src/go/plugin/go.d/config/go.d/snmp.trap-profiles/default \
     --catalogue ../../src/go/plugin/go.d/config/go.d/snmp.trap-profiles/catalogue.json
   ```

   The installed operator equivalent is:

   ```bash
   /usr/libexec/netdata/plugins.d/snmp-trap-profile-gen generate \
     --source-dir ./mibs \
     --all \
     --out-dir ./snmp-trap-profile-gen-output
   ```

   The shipped pack lives under
   `src/go/plugin/go.d/config/go.d/snmp.trap-profiles/default/`. Operator
   output should be copied from `snmp-trap-profile-gen-output/profiles/` into
   `/etc/netdata/go.d/snmp.trap-profiles/`.

## Required checks when changing categories or severities (taxonomy work)

These are closed sets enforced in three places that must stay in sync:

1. `src/go/cmd/snmptrapprofilegen/main.go` — `validCategories`,
   `validSeverities`, `severityPriority`, JSON Schema, and classifier prompt text.
2. `src/go/plugin/go.d/config/go.d/snmp.trap-profiles/profile-format.md`
   — the operator-facing category and severity tables.
3. `.agents/skills/project-snmp-trap-profiles-authoring/netdata.md` — §3 (category taxonomy) and
   §11 (PRIORITY mapping).

A taxonomy change without all three updates is incomplete and will be
rejected at review. Any taxonomy change also requires a re-run of
the Go helper's classification path against the full corpus (the existing
classifications were done under the prior taxonomy and are now stale).

## File size discipline

Stock profile YAMLs stay raw in the repository so changes are reviewable in
`git diff`. Installed/package stock vendor profiles MUST be compressed as
`.yaml.zst`; the runtime loader supports raw `.yaml`, compressed `.yaml.zst`,
and compressed `.yml.zst`. Operator/user profiles under
`/etc/netdata/go.d/snmp.trap-profiles/` SHOULD stay uncompressed `.yaml` for editability. If a single vendor file grows
past ~10 MB in the repository, revisit description verbosity rather than hiding unreviewable generated bloat behind
compression.
