Skip to content

v0.1.21 Notification templates, MCP read parity (9 new tools), CDP/LLDP neighbour history, configuration bundles, operator-configurable retention

Choose a tag to compare

@horryworks horryworks released this 03 Aug 16:26
· 61 commits to main since this release

📋 Full release notes: RELEASE_NOTES.md

Images (published by CI to GHCR):

ghcr.io/horryworks/yagra-core:v0.1.21
ghcr.io/horryworks/yagra-poller:v0.1.21
ghcr.io/horryworks/yagra-web:v0.1.21

Breaking changes

  • Core no longer starts when YAGRA_KEK_FILE points at a file it cannot read. It used to log an
    error and boot on a freshly generated random key — which meant the deployment looked healthy while
    every stored monitoring credential had become permanently undecryptable, and nothing said so until
    the next poll failed. That silent data-loss path is now a loud startup failure naming the path
    (load KEK from <path>). Unset stays unchanged: no YAGRA_KEK_FILE still means the ephemeral
    dev key, so the dev compose is byte-identical. Only a deployment that was already broken is
    affected; point the variable at the real key file, or unset it.
  • YAGRA_FLOW_RETENTION_DAYS now seeds a brand-new deployment only. On an existing one, flow
    retention comes from Settings ▸ System settings ▸ Data retention. The env var previously had no
    effect at all on an existing ClickHouse volume (see Bug Fixes), so nothing changes on upgrade —
    existing deployments keep the 30 days their tables are actually enforcing.

New Features

  • Three new MCP tools, and the biggest gap in that surface is closed (ADR-042 increment 2).
    /mcp could open a maintenance window but could not list one back, so an AI client could silence
    a node and then report the fleet healthy — having caused the silence itself.
    • list_suppressions — every maintenance window and mute the caller may see, in one answer.
      Check it before concluding a fleet is quiet; a silenced fleet looks the same as a healthy one.
    • alert_trends — how alerting has behaved over time: top_nodes (which nodes alert most
      often — chronic offenders, which the active-alert list cannot show), transitions (the latest
      fires and recoveries), and calendar (fires bucketed by weekday and hour, for spotting a
      nightly pattern).
    • search_analysis_findings — Troubleshoot findings across every run, filtered by node,
      folder, diagnostic, severity or time. Distinct from reading one run you already know about.
    • list_analyses takes kind=schedules to list recurring analyses, and list_node_groups
      takes include_state
      to return each folder's health tally alongside its name.
    • MCP remains read-only: no tool was added that changes anything.
  • Notification templates (Alerts ▸ Notification routing ▸ a channel ▸ Edit notification
    template, ADR-039). Each channel can override the subject and body it sends, written as a Jinja2
    template over a fixed set of alert variables. The immediate reason: the built-in subject named the
    node by UUIDnode 6f1c9d2a-0b3e-4a71-9c8d-2e5f7a1b4c60 is critical — so a template can now
    say {{ severity | upper }}: {{ node_name }} ({{ group }}) instead. Conditionals work, which is
    the usual second request: {% if event == 'resolve' %}Recovered{% endif %}.
    • Existing channels are unchanged, byte for byte. No template means the wording Yagra has
      always sent; upgrading changes no notification.
    • A broken template never costs you a notification. If it cannot be rendered when an alert
      fires — a bad filter, output too large, or a body that stopped being valid JSON on a channel
      that sends JSON — that field falls back to the built-in wording and the alert still goes out.
      The fallback is per field, so a mistake in the body does not discard a subject that was right.
      Each occurrence increments yagra_notification_template_errors_total{reason} and is logged.
    • Preview before you save. A template's first real execution is during an outage, so the
      editor renders it against a representative alert and shows exactly what would be sent,
      including whether the body parses as JSON. A template that does not compile is refused at save
      time rather than at 3am.
    • Variables available: node name/id/address, group, profile, severity, state, metric, value,
      threshold, direction, time, flapping, the root cause when an alert is rolled up, and the dedup
      key Yagra sends to PagerDuty/JSM. Credentials are never in scope. Interpolating into a JSON
      body wants the tojson filter, which the editor's error message tells you.
    • Editing is Admin-only and audited, and takes effect within ~30 seconds without a restart.
    • Not covered: a webhook or SMTP destination configured through YAGRA_WEBHOOK_URL /
      YAGRA_SMTP_* keeps the built-in wording — it has no channel record to attach a template to.
      Add it as a channel in the UI to template it.
  • The MCP tool surface now answers the questions the WebUI answers (ADR-042). It had drifted
    into a subset — 110 read endpoints against 17 tools — and the gaps were the ones a troubleshooting
    session hits first. Six new read-only tools, taking the surface to 23:
    • get_interface_series — one interface's in/out throughput and error rates over time.
      query_metrics is node-level only, so per-interface history had no tool at all.
    • top_metrics / top_interfaces — fleet-wide rankings. "Which nodes are worst on CPU
      right now", and "which links are busiest, most erroring, or moved the most". Previously an AI
      client could only read one node's metric at a time.
    • get_neighbors — a node's CDP/LLDP adjacency and its recent changes (ADR-038).
    • list_node_groups — the folder tree, which is also how a caller finds the group id
      run_analysis(scope="group") asks for.
    • fleet_throughput — total in/out bits per second across every exporter.
    • top_flows now takes an optional node_id: omit it for the fleet-wide aggregations that
      previously had no tool. Like the REST endpoints, the fleet-wide form is refused for a token
      limited to a group, since the rows keep no exporter attribution to narrow.
    • MCP stays read-only. The write surface is unchanged — acknowledging an alert, opening a
      maintenance window, and triggering a poll — and this release adds none. Reading Yagra's own
      configuration over MCP still requires the same permission the UI does; read-only does not mean
      readable by anyone.
  • CDP/LLDP neighbor discovery (node detail ▸ Neighbors, ADR-038). Every SNMP node's LLDP and
    CDP tables are walked on a slow cadence, and the result is recorded as what changed, and when
    the tab shows which local port faces which peer right now, plus a timeline of every time that
    moved. A history row is written only when the adjacency actually changes: the agent's own
    churn (LLDP's TimeMark, its remote-row renumbering, row ordering) is normalized away, so a rack
    nobody is repatching writes nothing at all. Both protocols are normalized onto one model, so a
    device running both shows one table and one capability legend.
    • On by default. After upgrading, each SNMP node gets one extra walk per hour. A device that
      speaks neither protocol answers in a single round trip and costs essentially nothing; a
      48-port switch returns a few dozen rows. Switch it off, or change the cadence, in
      Settings ▸ System settings ▸ Neighbor discovery. Turning it off keeps everything already
      recorded.
    • Read-only, like everything else Yagra does to a device — no configuration is written.
      Neighbor data raises no alerts and is deliberately not wired into dependency
      suppression: LLDP reports adjacency, which has no direction, and guessing the upstream wrong
      would silence a real outage rather than surface it (ADR-015).
    • New endpoints: GET /api/v1/nodes/{id}/neighbors and
      GET /api/v1/nodes/{id}/neighbors/history (View, node-scoped), plus
      GET/PUT /api/v1/settings/neighbors (View / ManageConfig).
  • Data retention is configurable from the UI (Settings ▸ System settings ▸ Data retention,
    ADR-040). Alert-linked data, unmatched events, report runs and traffic flows each get their own
    window; changes apply on the next sweep with no restart, and a flow change is applied to
    ClickHouse immediately. The card lists every retained subject, including the ones Yagra cannot
    change: VictoriaMetrics and VictoriaLogs take retention as a container start flag with no runtime
    API, so those rows are read-only and show the value read back from the store's own /flags
    endpoint — what it is really enforcing, rather than a number mirrored from configuration. The
    audit log is listed as kept indefinitely, by design.
  • GET /api/v1/settings/retention (View) and PUT (ManageConfig) expose the same policy.
  • GET /api/v1/credentials/health (ManageCredentials) reports whether every stored credential
    still decrypts under the loaded key. This is the assertion a database restore cannot make on its
    own: rows can come back whole while the key-encryption key is a different one.
  • Configuration bundles (Settings ▸ Configuration bundle, GET/POST /api/v1/config/bundle,
    ADR-040). Export a deployment's monitoring configuration — profiles, metric sets, classification
    rules, groups, nodes, thresholds, URL/DNS monitors, forwarding destinations, event sources and
    rules, report and analysis schedules — as one JSON file, and apply it to another deployment. This
    is for migration, not backup: a bundle carries no credentials, no notification-channel
    settings, no ingest tokens and no history. Import is upsert only — nothing is ever deleted and
    there is no replace mode — runs in one transaction, and ?dry_run=true performs the real import
    and rolls it back, so its report is exactly what applying would do. The report names every row it
    skipped or changed and why: a missing required reference skips the row rather than widening it, a
    destination or webhook source that needs a secret arrives disabled, and schedules are recomputed
    on the target's clock. Notification channels and routing rules are deliberately not carried —
    a channel is its sealed config and no API can attach one to an existing channel id, so an
    imported rule would notify nobody, silently. The export refuses rather than truncating when a
    table exceeds 10,000 rows; use a database dump for a deployment that size.
  • A backup procedure that ships as scripts, and a way to prove it works.
    scripts/yagra-backup.sh takes the tier-1 set (KEK first, then a full pg_dump, then a
    VictoriaMetrics snapshot) with a manifest; scripts/yagra-restore-verify.sh restores it into a
    throwaway stack and asserts /readyz, the node count, the audit-log row count, and that
    credentials actually decrypt. ADR-017 has required a backup and rollback path for destructive
    migrations since it was written, and until now the repository contained no pg_dump at all.

Bug Fixes

  • The MCP top_flows and flow_fanout tools did not clamp their row limit. The REST flow
    endpoints cap a request at 1000 rows; the MCP tools had their own copy of that query builder and
    it had lost the cap, so an AI client asking for 100000 rows got a ClickHouse query with no bound
    on it. Both surfaces now share one window-and-limit rule, so the cap applies wherever the
    query comes from. A tool call asking for more than 1000 rows now receives 1000. The default
    (100 rows) is unchanged.
  • A site pin on the Geo map counted only the nodes filed directly in that folder. Nodes
    normally live in sub-folders — racks, floors, closets — so an operator who placed their Tokyo
    site and filed the switches under Tokyo ▸ Floor 2 ▸ Rack A got a pin that was permanently
    green and empty, showing nothing about a site that was on fire. Group coordinates are now
    inherited
    : a folder with none of its own belongs to its nearest placed ancestor, and a pin
    counts everything that resolves to it. Both the Geo map page and the dashboard widget change
    together, so a site cannot read amber in one and green in the other.
    • Inheritance does not add pins. Thirty racks under one building stay one pin — thirty
      exactly-overlapping ones would only hide the building. The number of pins is still the number
      of folders carrying their own coordinates; what changed is what each one counts.
    • The group dialog now says when a folder is already on the map through its parent, so an empty
      pair of coordinate boxes no longer reads as "this site is missing from the map".
    • API: GET /api/v1/node-groups rows gain effective_latitude, effective_longitude,
      geo_source (own / inherited / unset) and geo_group — the folder whose pin this one
      belongs to. latitude/longitude still mean the folder's own coordinates and are unchanged.
  • YAGRA_FLOW_RETENTION_DAYS did nothing on an existing deployment. The ClickHouse TTL was only
    ever emitted inside CREATE TABLE IF NOT EXISTS, which is a no-op once the tables exist, so the
    retention an existing volume ran with was whatever it was created with — while DEPLOYMENT.md
    documented the variable as live. Retention changes are now applied with ALTER TABLE … MODIFY TTL,
    and only when the declared TTL actually differs (issuing it unconditionally would re-mutate every
    part on each restart). Lowering the window deletes flow rows older than it, so the change is
    logged at warn with the old and new values.
  • The five envelope-encrypted stores each loaded their own key. Credentials, notification
    channels, forwarding destinations, OIDC and LLM config called the key loader independently, so on
    the ephemeral (unset) path each got a different random key — despite the code's own comment
    saying they shared one. They now share a single loaded key.