Skip to content

Releases: horryworks/Yagra

Release list

v0.2.1 the node list reports kind instead of source, detail tabs filtered by kind, kind-shaped Overview, URL/DNS/Meraki badges in the inventory, + adds a node, Edit node on the pane

Choose a tag to compare

@horryworks horryworks released this 10 Aug 12:48

📋 Full release notes: RELEASE_NOTES.md

Container images published by CI for this release (also tagged :latest, since this version carries no pre-release suffix):

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

Breaking changes

  • The node list now reports what each node is. GET /api/v1/nodes and
    GET /api/v1/nodes/by-group replace the two-valued source field with kind — the same
    meraki / url / dns / device value GET /api/v1/nodes/{id} already returned, resolved by
    the one function the scheduler also asks, so a list row can no longer disagree with the detail
    page it opens. source is removed rather than deprecated: it was a lossy projection added for a
    single tree badge, and it would have read "device" for every monitor kind added since and every
    one added next. Mapping for API clients: source == "meraki" becomes kind == "meraki", and
    everything that was "device" is now one of device, url or dns. The MCP list_nodes and
    get_node_status tools carry the same kind, so an AI client can tell a URL monitor from a
    switch before asking which metrics it has.

Improvements

  • A node's detail page now shows only the tabs its kind can fill. A URL or DNS monitor shows
    Overview and Collection; a Meraki device adds Events. Interfaces, Neighbors and Flow are gone from
    those kinds — not because they were empty today, but because they cannot ever be filled: a URL or
    DNS monitor is dispatched one HTTP or DNS check and is never SNMP-walked or pinged, and a Meraki
    device is polled through the Dashboard API with no per-node job at all. Nothing becomes
    unreachable: passive events for any node remain at Alerts ▸ Events filtered by node, and none
    of these kinds is ever a NetFlow exporter. A bookmarked link to a tab a node does not show now
    lands on Overview and corrects the address bar, instead of drawing a pane with no tab selected.
  • The Overview of a monitor stops describing a device it is not. A DNS monitor used to lead with
    an empty "ICMP RTT · last 30 min" chart and a facts grid whose Maker, Model, SNMP credential and
    Uptime were four consecutive em dashes — all of them answers to an SNMP walk that kind never
    receives. The chart is now shown only where ICMP is actually sent, the grid lists only rows that
    can hold a value (a DNS monitor gains a Resolver row instead), and the line under the node's name
    reads wg.example.net · A rather than 0.0.0.0 · unknown device. Each kind's "last seen" is now
    read from the metric it actually produces, so monitors show one at all.
  • URL, DNS and Meraki nodes are now labelled as such in the inventory. A short URL / DNS /
    Meraki badge sits after the name in the node tree, in a group's member list and beside the title
    on the detail page, with the full kind on hover. Ordinary devices stay unmarked, so the badge means
    "read this one differently". Previously only Meraki was distinguishable and a URL monitor was
    indistinguishable from a switch.
  • The Collection tab no longer shows a 0 count pill on nodes whose profile attaches no
    collection sets — which is every URL and DNS monitor. A zero there read as a fault; it now shows
    no pill, matching the Interfaces tab's existing rule.
  • The inventory's + adds a node, not just a group. It now opens a two-item menu — add node, add
    group — and both act on whatever the tree has selected: with a group (or a node inside one)
    selected they read Add node to "Tokyo"… / Add subgroup in "Tokyo"…, and with nothing selected
    they file at top level. Adding a node used to be reachable only by right-clicking a tree row,
    which is undiscoverable on a desktop and impossible on a touch device, where contextmenu never
    fires — so on a phone the inventory could grow folders but not nodes. The + on a group row offers
    the same two, scoped to that group.
  • The Add node dialog now has a Group field. It is preselected from wherever the dialog was
    opened and can be changed before saving, instead of the previous read-only "Adding to top
    level
    " note. This also fixes a node added from a selected group's pane landing at top level
    regardless of the group.
  • "Edit node" is now on the node pane of the Nodes page, not only on the full detail page.
    Changing a node's profile, credential, maker/model or poller pool from the inventory split meant
    first pressing "Open detail" — a button that reads as navigation, not as editing, so the edit path
    was effectively hidden. It sits beside "Move…" and opens the same dialog.

Bug Fixes

  • A DNS monitor's resolution history no longer paints its status chip over the next column. The
    Resolution column was 140px wide while "No such name (NXDOMAIN)" — and its Japanese equivalent
    more so — needs half again as much, and a status chip is deliberately never squeezed, so it
    overflowed at every browser width. The column is now sized for the longest label in either locale,
    and the chip truncates with an ellipsis (full text on hover) rather than spilling if a future label
    or a wider system font still does not fit.

v0.2.0 First public beta release, every collected metric visible and chartable, URL monitors that read the body, an SSO form that knows your IdP

Choose a tag to compare

@horryworks horryworks released this 09 Aug 15:12

📋 Full release notes: RELEASE_NOTES.md

Container images — published to the GitHub Container Registry by CI from this tag:

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

Each is also tagged :latest and with the immutable commit sha fa884fca90c98db65625cf9f2e09a78c3461fce7. Start from DEPLOYMENT.md.

This is Yagra's first public release. Everything up to v0.1.23 was developed in a private
repository and never published; those notes are kept below as the record of how the system got here,
not as releases you could have been running. If you are arriving now, v0.2.0 is the beginning —
there is no upgrade to perform and no earlier published version to upgrade from. The minor bump
rather than 1.0.0 is deliberate: what changed is that the source and the images are public, not
that the API has stopped moving.

Three things worth reading before you deploy it:

  • Yagra is in open beta. It is a working stack, but most of what these notes describe has not yet
    been validated in anyone else's production network. Run it alongside your existing monitoring
    rather than in place of it, and treat the feature list as what is built, not as what has been
    proven at scale elsewhere
    .
  • Bug reports are the most useful thing you can send, and they go to
    GitHub Issues — which is also the only contact
    channel for this project
    , questions and commercial-licensing inquiries included. There is no
    contact e-mail address. The one exception is a security vulnerability, which goes through
    private vulnerability reporting
    rather than a public issue. Pull requests are not being accepted yet — see
    CONTRIBUTING.md.
  • Start from DEPLOYMENT.md, not from docker-compose.yml. The single-node
    compose file is an evaluation stack: default database credentials, an ephemeral key-encryption key
    (so stored device credentials do not survive a restart), and a self-signed certificate. What a real
    deployment changes is listed there, and the security-relevant essentials are in
    SECURITY.md.

New Features

  • URL monitors now record how long the endpoint took to answer, as the new http_response_time_ms
    gauge, shown on the node's Overview and available to thresholds like any other metric. Until now a
    URL monitor could say whether an endpoint was up but nothing about whether it was slow — the
    probe has always measured this and simply discarded it. Two things worth knowing:

    • It is time to the response headers, not to a completed body: the probe does not read the
      response body.
    • Nothing is recorded when the endpoint did not answer. A timeout would otherwise appear as a
      flat "slow response" for the whole outage, and a latency threshold would page for the same
      incident http_up already covers.
    • No default threshold is seeded — response latency varies too much between environments for one
      to be right. Set one per profile, group or node.
  • URL monitors can now check the response body for a keyword. A monitor may require that the
    body contains a keyword, or that it does not — the case that catches an endpoint answering
    200 while its body says the service is broken, which availability monitoring structurally cannot
    see. Configure it on the node's URL-monitor dialog; it also rides on PUT /api/v1/nodes/{id}/url-check as the optional body_match object (pattern, mode, max_bytes)
    and is reported by the MCP get_config surface. What it reports:

    • http_body_match1 satisfied, 0 not. A below 0.5 critical threshold is seeded on the
      built-in URL profile, so a rule alerts without a second configuration step. Existing monitors
      are unaffected: the metric is emitted only for a monitor that carries a rule.
    • http_body_truncated — diagnostic, 1 when the body outgrew the read budget.
    • A body larger than the read budget reports "not satisfied", never "satisfied". Truncating
      silently would let a must not contain rule report healthy about a page whose error text sits
      past the cut. Raise max_bytes (default 65536, up to 1048576) if a legitimate keyword is
      landing beyond it.
    • Matching is plain, case-sensitive substring matching — not a regular expression.
    • GET/POST only; a body rule on a HEAD monitor is refused (body_match_needs_body), because
      a HEAD response has no body and the rule could never be satisfied.
    • Rolling upgrades: a poller that has not been upgraded is not sent a content-checked monitor
      at all, and the withheld count is recorded on yagra_specs_withheld_total{cap="http-body"}. An
      older poller would drop the rule, never read the body, and report http_up = 1 — a green
      dashboard for the exact outage the rule was guarding against — so the check pauses rather than
      reporting a result it did not compute. The same gate http-auth already uses.
  • URL monitors can now record numbers out of a JSON response body, under metric names the
    operator chooses — a queue depth, a replication lag, a worker count. Configure up to 8 per monitor
    on the URL-monitor dialog, or as the json_extract array on PUT /api/v1/nodes/{id}/url-check (metric + path); the extracted values appear on the node's
    Overview and can carry thresholds like any other metric.

    • The path is dot-separated and names exactly one valuedata.queue.depth,
      items.0.value (items[0].value is accepted and means the same). It is deliberately not
      JSONPath: a rule that could select many values would need a reduction nobody asked for.
    • Numbers, booleans (true → 1) and quoted numbers ("42") are recorded. Anything else
      records nothing for that poll — never a zero
      , because a zero is indistinguishable from the
      value genuinely being zero. The same applies when the body is not valid JSON or was truncated.
    • A metric name must be a valid TSDB name and may not be one the monitor already reports
      (http_up, http_status_code, http_response_time_ms, ssl_cert_days_to_expiry,
      http_body_match, http_body_truncated) — that would overwrite the node's own availability
      series. Two rules may not write the same name.
    • Unlike the keyword check, extraction is not withheld from an older poller: it would record
      nothing (a visibly absent series) rather than a wrong reading, and withholding would stop the
      whole monitor including http_up.
  • body_max_bytes is a property of the monitor, not of the keyword rule — one body, one read,
    one budget, shared by the keyword check and extraction. Default 65536, range 1024–1048576.

  • http_response_time_ms still measures time to the response headers even when a body feature
    is configured, so the metric means the same thing on every monitor.

  • Every metric a node collects is now visible, not just the ones the UI was written to know
    about.
    The node's Collection tab lists them all and charts any of them; until now it showed the
    latest value of scalar metrics only, with no history, so an operator who added a vendor table
    column could watch it collect successfully and never see a number. The new
    GET /api/v1/nodes/{id}/metrics answers it, and the MCP tool list_node_metrics mirrors it.
    Each entry states three things:

    • statusok (configured and flowing), no_data (configured, nothing has arrived) or
      unconfigured (data exists with no collection item). The last is normal rather than a fault:
      reachability, the URL and DNS monitors, the neighbour count and values extracted from a
      monitored JSON response all come from checks rather than from a collection set, and so were
      invisible to every screen driven by that set. snmp_neighbor_count is chartable for the first
      time as a result.
    • dimensionnone, interface (read those per interface instead) or entity, meaning one
      series per table row. Row identity is folded away when the values are collected, so these are
      shown as a node-wide maximum and labelled as such rather than implying a per-row breakdown
      that cannot be produced.
    • metric_kind — gauge or counter, which decides how it may be charted.
  • A counter can now be charted as a rate. GET /api/v1/nodes/{id}/metrics/{metric}/range takes
    rate=true, returning the per-second rate instead of the stored values. There was previously no
    way to chart a node-level counter at all: its stored value is an odometer reading, and agg=max
    over one draws a rising line that looks like traffic and is not. rate cannot be combined with
    agg (rate_with_agg) — a per-entity counter has no node-level rate.

  • A new "Metric chart" dashboard widget charts any metric of any node. Pick a node, pick one of
    its metrics from the list the device actually reports, and the widget draws the last 6 hours.
    Every other card on the board answers a question the catalog decided in advance; this one is for
    the metric your devices have and ours do not — a vendor temperature, a value lifted out of a
    monitored JSON body. Add it as many times as you like. Two behaviours worth knowing:

    • It offers only what it can draw honestly. Per-interface metrics are not listed — the node's
      Interfaces tab charts those per port, and collapsing eight ports to one line answers a different
      question. A counter is charted as a per-second rate, never as its stored value.
    • A metric that stops being available says so. If the node no longer reports the selected
      metric, the widget names it instead of drawing an empty chart.
  • **A new "Top nodes by metric" dashboard widge...

Read more

v0.1.23 An alert for monitoring's own blind spot, a support bundle, and an RCA that investigates

Choose a tag to compare

@horryworks horryworks released this 08 Aug 11:57

📋 Full release notes: RELEASE_NOTES.md

Container images (published by CI to GHCR):

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

This release also moves :latest.


Breaking changes

  • An alert's node field is now its subject, and is no longer always a UUID. For a
    poller-pool alert it reads pool:<name>. This affects GET /api/v1/alerts, GET /api/v1/alerts/history, the /api/v1/stream/alerts frames and the MCP alert tools. Every
    response carrying an alert now also carries subject_kind (node | pool) and, for a named
    subject, subject_namebranch on subject_kind before treating node as a node id.
    • On history rows and in the MCP DTOs, node / node_id is null for a non-node subject rather
      than a made-up UUID.
    • POST /api/v1/alerts/ack now takes either node (unchanged) or subject, the alert's
      flat subject form. node is no longer required; sending neither is a 400 invalid_subject.
    • Node-oriented aggregates are unaffected and stay node-only: /api/v1/alerts/top-nodes and
      /api/v1/alerts/transitions never report a pool.
  • Four previously unbounded tables now declare a retention policy (ADR-040). Troubleshoot
    analysis runs and their findings, generated AI root-cause reports, and monitoring-gap records are
    pruned on a schedule; the interface map is declared kept-until-node-deletion, with its reason. The
    first three had grown without limit since they were introduced — migration 0026 says "no auto-trim
    yet" in as many words, and scheduled analyses have been writing to that table on a cadence since.
    • A new Diagnostic data window (default 90 days) in Settings ▸ System settings covers analysis
      runs and RCA reports. It is deliberately separate from "Report runs": a window's name must not
      silently govern a second kind of data.
    • Monitoring gaps follow the alert-linked window instead, because a gap explains an absence of
      alerts and is only readable beside the history it explains.
    • The interface map is not pruned by age, and the reason is in the policy table: an orphaned
      (node, ifIndex) row is a stale identity rather than old data, and last_seen only advances
      while interface collection is running — so an age-based sweep would erase the names and speeds
      of every interface on a node whose polling was merely paused. Those rows disappear with their
      node. The real fix belongs in the poller and is a separate change.

New Features

  • Yagra now alerts when a poller pool stops having a poller. This was monitoring's own blind
    spot. The alert engine reasons about poll results, so when a pool loses its last live poller
    there is nothing for it to reason about: the scheduler falls back to publishing jobs on a subject
    nothing is subscribed to, plain NATS discards them, and the nodes drift to unknown rather than
    down. An entire site stops being monitored and every dashboard stays calm. Until now the
    condition was visible only to someone already looking at Settings ▸ Pollers.
    • Delivered over your configured notification channels at critical severity, so an existing
      critical → PagerDuty routing rule reaches it, and closed automatically when a poller returns.
    • A pool must be uncovered for five minutes before it notifies — a poller announces its own
      departure, so an ordinary rolling restart raises the condition instantly and the debounce is
      what stops that paging anyone. Tune or disable it with
      YAGRA_POOL_COVERAGE_ALERT_AFTER_SECS (default 300, 0 = off).
    • Two new gauges regardless of that setting: yagra_pools_without_live_poller (unlabelled — the
      one to alert on, or to drive a scale-up from) and yagra_pool_nodes_without_live_poller{pool},
      which reports 0 for a healthy pool rather than disappearing.
    • Meraki-managed nodes are excluded, as they are from the Pollers page: core's org collector
      polls them, so they do not depend on a pool.
    • It is a full alert, not only a notification. It appears on Active alerts and in alert
      history, streams live, is acknowledgeable from your incident tool, and renders through your
      notification templates. Two new template variables come with it — subject_kind and an
      always-present subject_name — so a template can read correctly for both a device and a pool.
    • A group-scoped operator sees the pools their own nodes are polled by, which is exactly the
      person whose site went dark. Pools holding no node they can see stay invisible to them.
    • Two things it is deliberately not: it never rolls into a node's displayed state (it belongs
      to no node), and it cannot be muted — a mute names a node.
  • The MCP surface can now read Yagra's own configuration — get_config(kind=…). One tool over
    28 reads: thresholds, event rules and sources, notification channels and routing rules, profiles
    and collection templates, a node's collected metrics, classification rules, the MIB catalog, a
    node's URL/DNS check, discovery candidates and scans, Meraki orgs/networks/polling, forwarding
    destinations, report definitions and schedules, and the retention / adjacency / LLM / roles /
    OIDC / LDAP settings. This closes the ADR-042 read-parity backlog: every read the WebUI can reach
    is now reachable from /mcp except the four live SSE streams, which have no subscription
    transport.
    • Read-only does not mean readable-by-anyone. Each kind demands the same permission its REST
      counterpart does — manage-users for OIDC and LDAP, manage-config for fourteen of them, and
      view for the rest — so a Viewer is served the role matrix and refused the threshold ruleset
      from the same tool.
    • No stored secret is returned. A node's URL check reports whether a credential is bound
      (has_credential), never which one; the REST body is unchanged.
  • The LLM root-cause analysis can now look things up for itself. Previously it was handed a
    fixed set of facts — the alert, the dependents, the upstream chain, a signal timeline, recent
    config changes — and answered in one shot, so it could only reason about what had been decided in
    advance to include. It now gets the read-only MCP tools and asks: pull the interface series, check
    whether the poller was even up, read what syslog said, look at the threshold that fired.
    • It runs under the caller's own visibility scope, so a group-scoped operator's analysis
      cannot read a node they cannot see, and under a view-only tool allow-list — the write tools,
      run_analysis, run_rca and the audit log are all out of reach, checked per folded branch
      rather than per tool.
    • What it looked up is stored with the answer (transcript on the report body) and replayed
      on both surfaces, for the same reason the evidence always was: an explanation whose reader
      cannot check what it was based on is an assertion.
    • Bounded by turns (YAGRA_RCA_MAX_TURNS, default 6), wall clock
      (YAGRA_RCA_TASK_BUDGET_SECS, default 240) and total tool output. Hitting a bound returns the
      model's last answer rather than failing the request. Set YAGRA_RCA_MAX_TURNS=1 to get the
      previous single-shot behaviour back exactly
      — no tools are offered and the request sent to the
      provider is byte-identical to before.
    • Tool output is device-supplied text and is fenced as such: a syslog line that says "ignore your
      instructions" arrives inside the same untrusted-output markers a device's sysDescr always did.
  • Support bundle (Settings ▸ System Health). One download containing everything needed to
    diagnose a deployment from outside it: which binary is actually running (image source ref and
    build profile, not just the version), every system-health section, the allow-listed environment,
    applied migrations with their checksums, per-table sizes and connection counts, active alerts, the
    audit tail, core's Prometheus scrape, and core's own rotated log files. Also at
    GET /api/v1/system/support-bundle?since_hours=N.
    • It is built for a site where nobody can open a shell and data does not leave casually, so the
      archive is designed to be reviewed before it is released: every entry is JSON or plain text,
      and MANIFEST.json lists what is carried and what is deliberately left out, with reasons.
    • Secrets are handled two ways. The environment is carried by an allow-list — a deny-list of
      password-shaped names would miss the credential inside YAGRA_DATABASE_URL's userinfo, which is
      the one that actually ships. Then every assembled byte is scanned, and a match aborts the
      export
      rather than redacting it: the strongest rule is the set of literal secret values the
      core process can see in its own environment, so a credential arriving through an unanticipated
      path is caught too. A refusal answers 500 support_bundle_redaction_failed naming the file and
      the rule, never the value.
    • It requires ManageConfig + ManageCredentials + ViewAudit — all three, so this cannot become
      a way to read the audit log or the credential report through an endpoint whose name mentions
      neither. In practice that means Admin.
  • Core's log is now written to disk as well as stdout. Hourly JSON-lines files under
    YAGRA_LOG_DIR, YAGRA_LOG_RETAIN_HOURS of them (default 48, pruned automatically), on a named
    volume so they outlive the container. Reading docker logs needs a shell on the host, which is
    exactly what a locked-down deployment does not grant — so a panic or an OOM used to leave nothing
    retrievable. A support bundle taken after a recovery now carries the run that died.
    • On by default in docker-compose.yml and docker-compose.deploy.yml; set YAGRA_LOG_DIR
      empt...
Read more

v0.1.22 HTTPS by default, importable TLS certificates, a network map that draws itself, derived alert suppression, LDAP/AD sign-in, unmonitored-host discovery, MCP system health, half the memory

Choose a tag to compare

@horryworks horryworks released this 05 Aug 03:37

📋 Full release notes: RELEASE_NOTES.md

Images (published by CI to GHCR; :latest now points at this release):

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

⚠️ This release moves the WebUI to HTTPS on port 443 and removes the plain-HTTP listener. Read the breaking changes below before upgrading — http://<host>:3000 stops answering, everyone is signed out, and SSO needs its redirect URI updated in two places.

Breaking changes

  • The WebUI is now HTTPS, on port 443, and there is no plain-HTTP listener. Everything the UI
    carries — the login password, bearer tokens, and device credentials on their way to being
    encrypted — used to cross the network in the clear by default. v0.1.9 shipped instructions for
    fixing that and a commented-out configuration block; a year later nobody had uncommented it. So
    the secure shape is now the one you get by doing nothing.
    • http://<host>:3000 no longer answers. If your .env still sets YAGRA_WEB_PORT=3000 you
      keep port 3000 and it becomes https://<host>:3000 — the port stayed, the scheme changed.
      Delete the line to land on 443.
    • A redirect was considered and deliberately rejected. Most webhook senders do not follow
      redirects, and those that do turn a 301 on POST into a GET, so
      POST /api/v1/ingest/webhook/:source_id would have gone on returning success while events
      stopped arriving. Connection-refused is the failure you can see. Move webhook senders and any
      other machine client
      to https://<host>/api/v1/…, or to core's unchanged plaintext
      http://<host>:8080/api/v1/….
    • Everyone is signed out and loses their saved UI state. http://host:3000 and
      https://host are different origins, so the session token, dashboard layouts, theme and table
      preferences do not carry over. There is no migration for this that would be worth its
      complexity; sign in again and the layouts are rebuilt as you go.
    • SSO stops working until two things are updated. The stored OIDC redirect URI is an absolute
      URL and must now use the new scheme and port, and the same value has to be updated at the
      identity provider. Settings ▸ Auth shows a warning when the stored value no longer matches the
      address you are browsing from. Yagra will not rewrite it for you — changing where an IdP is
      permitted to send an authorization code is not something an upgrade should do on your behalf.
    • Your browser will warn on first visit. The certificate is self-signed until you import one,
      and because Yagra cannot know the hostname you will use, the name usually will not match either.
      Import a real certificate at Settings ▸ TLS, or regenerate the self-signed one with the
      correct names from the same page.
    • Set YAGRA_WEB_TLS=off to keep serving plain HTTP from the container — the supported shape
      when an external reverse proxy or load balancer already terminates HTTPS in front of it.
    • Core's own API port is unchanged: still plaintext, still published on the LAN. That is
      deliberate sequencing, not an oversight — closing it in the same upgrade that introduces an
      untrusted certificate would break every Prometheus scrape and API script at once, with two
      overlapping causes. Once those clients are on the TLS edge with a certificate they trust, set
      YAGRA_API_BIND=127.0.0.1 to take it off the network. Settings ▸ TLS shows the current state.
    • If you maintain your own compose file, core's container now has a fixed group as well as a
      fixed user — both 10001.
      The certificate bundle is written 0640 owned by that group, and
      the web container joins it with group_add: ["10001"] in order to read it. Leave the group to
      the base image and nginx traverses the directory, is refused the file itself, and the WebUI
      never comes up while every other signal stays green. The shipped compose files already do this.

New Features

  • Import your own TLS certificate from the WebUI. Settings ▸ TLS shows what is being served —
    subject, issuer, the names it covers, expiry and fingerprint — and takes a PEM certificate chain
    and private key, either pasted or from a file. The new certificate is live within seconds, with
    nothing restarted. Yagra will not accept a pair that does not go together, has already expired,
    or carries no subject alternative name, and says which of those it is rather than failing at the
    next handshake. Encrypted private keys and .pfx bundles are refused with the openssl command
    that converts them. The self-signed certificate can be regenerated with the hostnames and IP
    addresses you actually use, and renews itself before it expires.
    • The private key is envelope-encrypted at rest like every other secret and is never returned by
      the API. The certificate is downloadable, so you can hand it to a Prometheus ca_file, a
      curl --cacert, or an operating-system trust store.
    • System Health says when it is about to expire. An expired certificate takes the whole UI
      down — including the page you would use to fix it — so it is one more row there, and reaches the
      MCP get_system_health tool with it. Degraded is reserved for something somebody has to act
      on: expired, or an imported certificate inside its last 30 days. A self-signed one nearing
      expiry renews itself and is not reported as a problem. The same fact is on the Prometheus
      endpoint as yagra_web_tls_expires_in_days, for your own alerting.
    • This does not manage the NATS bus certificate, which the bus reads for itself at startup.
  • An AI client can now ask whether Yagra itself is healthy. The MCP surface had no way to see
    the monitoring system's own state, which matters more than it sounds: with no way to tell that a
    poller is offline or a store unreachable, a model reads missing data as a healthy quiet and
    reports that the fleet is fine. Six new tools close that (/mcp, off by default):
    • get_system_health(section=…) — the poller fleet and per-pool summary, poll-loop counters,
      which nodes one poller holds, which poller owns one node, recent core↔poller outages,
      per-store reachability, core/poller host resources and their trends, forwarding delivery
      status, whether stored credentials still decrypt, the running version, and which optional
      tiers are enabled.
    • get_report_runs, get_audit (who changed or acknowledged what),
      fleet_state_history, get_dns_chain, and run_rca — an LLM explanation of one
      incident, the same one the WebUI's "Explain this incident" produces.
    • get_fleet_summary(kind="coverage") answers which nodes have actually reported recently,
      with a watchlist of the ones that have not.
    • Sections require different permissions, matching the WebUI exactly: most need view,
      forwarding status needs manage-config, credential health needs manage-credentials, the audit
      log needs view-audit, and run_rca needs ack-alerts. Read-only does not mean
      readable-by-anyone. Reports and the state timeline refuse a group-scoped token rather than
      showing it the whole fleet, as the REST endpoints do.
  • The Network map now draws the network, not a list of parent links you typed in. Yagra derives
    the connectivity graph from what devices report: CDP/LLDP adjacency, and nodes that have an
    interface address in the same IP subnet. Two nodes sharing a subnet are adjacent as a matter of
    fact, so a map appears without anyone entering a single link. Each edge carries the evidence
    behind it — LLDP, CDP, or shared subnet — and the map labels and legends them. Redundant paths are
    kept rather than collapsed: a server reached through two routers shows both links. Drawing the
    map is all this does on its own
    ; alert suppression keeps following the dependency graph you
    maintain by hand until you deliberately hand it over, which is the next feature down.
  • A new read endpoint, GET /api/v1/topology/links, returns that graph in keyset pages, with a
    summary of everything the derivation observed but declined to turn into a link (unmatched
    neighbours, ambiguous management addresses, segments with no identifiable router). A group-scoped
    caller sees only links whose both endpoints are visible to them.
  • The network map now finds the links that share no subnet. Until now a link was derived from
    two devices holding an address in the same prefix, which structurally cannot see a point-to-point
    /32 (a PPPoE Dialer, a tunnel endpoint), an unnumbered OSPF link, or a peering across a
    segment whose addressing has not been collected. Yagra now also reads each device's OSPF
    neighbours and BGP peers
    , and asks its routing table about specific destinations — so those
    links appear on the map with OSPF neighbor, BGP peer or Connected route as their evidence.
    A link seen both ways is still one link carrying both.
    • On by default, at the same hourly cadence as the other two automatic walks, and switchable
      at Settings ▸ System settings ▸ Discovery walks. Unlike the ARP walk, the tables it reads are
      sized by the device's own peering mesh, not by the network.
    • The routing table is never walked. A router carrying a full table has hundreds of thousands
      of routes, so Yagra asks about one destination at a time — and only a device that holds a host
      address of its own is asked at all, capped at 64 destinations. On a fleet of ordinary devices
      this issues no route queries whatsoever.
    • A down session still draws its link. A BGP session in active is a link with a fault, and
      that is usually the thing being investigated — making the topology disappear in step with the
      outage it exists to explain would be exactly ba...
Read more

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

📋 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...
Read more

v0.1.20 Group-scoped visibility end to end, scheduled analyses, cross-run findings search, API tokens bound to an owner and usable over REST, the Geo map page, mute from the alert row

Choose a tag to compare

@horryworks horryworks released this 02 Aug 18:21

📋 Full release notes: RELEASE_NOTES.md

Images (published by CI to GHCR):

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

Group scoping went from a type nothing consulted to a working control: the read surface filters by
it, an admin can hand one out, and both API tokens and /mcp honour it. Alongside that, the
Troubleshoot tools gained a cross-run findings search and scheduled runs, and the Geo map became a
real page.

Breaking changes

  • An API token now acts as an account, and one whose owner cannot be resolved stops working.
    Tokens used to be free-standing identities: api_tokens carried its own role and had no link to
    users at all, so deleting, disabling or demoting the account that issued a token changed nothing
    about the token. Migration 0057 binds each token to an owner, matching on the created_by
    username. A token whose issuing account no longer exists cannot be matched, and no longer
    authenticates
    — it is listed as "No owner" so it can be revoked deliberately. Re-issue any such
    token, preferably owned by a service account (below).
  • The four Top-N endpoints now return an object, not a bare array.
    GET /api/v1/metrics/top, /metrics/interface-top, /metrics/interface-delta and
    /alerts/top-nodes answer {"entries": [...], "partial": false}; what used to be the whole body
    is now entries. /metrics/interface-heatmap keeps its shape and gains the same partial field.
    partial is true when the ranking covers only the groups the calling account may see and rows
    it is entitled to may be missing — a Top-N is ranked by the metric store, which knows nothing
    about groups, so a scoped account's list is filtered afterwards and can come back short. It is
    always false for an account with unrestricted visibility, which is every account today. The
    WebUI is updated; an external client reading these endpoints must read entries.
  • :latest now means the latest stable release, not the development trunk. Until now every
    push to main published ghcr.io/horryworks/yagra-*:<sha> and moved :latest, so the default
    in docker-compose.deploy.yml and docker-compose.poller.yml handed you a development build.
    Development builds are no longer published at all — the registry holds releases and nothing else.
    :latest moves when a release is tagged without a -beta/-rc suffix, and :<sha> exists only
    for commits that were released. If you were following main through :latest you are now
    following releases; pin an explicit tag if you wanted something else. Note :latest follows the
    most recently pushed stable tag rather than the highest version, so a hotfix cut after a larger
    release moves it backwards.
  • docker-compose.deploy.yml takes a new YAGRA_IMAGE_REPO, defaulting to
    ghcr.io/horryworks. Leave it unset for published releases; it exists so a development machine
    can point at a private registry holding unreleased builds.

New Features

  • Group-scoped API tokens and /mcp connections work. The two refusals that stood in for
    enforcement are lifted, and the promise made in v0.1.19 — "group scoping will be accepted again
    when the read paths actually filter by it"
    — is kept. POST /api/v1/api-tokens accepts
    {"scope": {"Groups": [...]}} instead of answering 400 unsupported_scope, and /mcp admits a
    group-scoped token instead of 403-ing it. Every MCP tool now resolves the caller's scope and
    applies the same rule its REST counterpart does: node lists and event searches filter in the
    query, rankings and histories filter after, a tool naming an out-of-scope node answers exactly
    what an unknown id answers, and run_analysis cannot be launched over a scope the caller does not
    hold. Settings ▸ API tokens gained a scope picker and a Can see column.
    Two containment rules: a token can never exceed its owner, so a token owned by a group-scoped
    account inherits that account's scope
    (narrowing the account narrows its tokens at once, with
    nothing to re-issue) and giving such a token a different scope is refused with
    400 owner_is_scoped; and a token scope must name groups that exist. To give a token a narrower
    view than its owner, own it with a service account scoped to what the token should see.
  • Accounts can be limited to a set of node groups. Settings ▸ Users ▸ Change scope
    (PUT /api/v1/users/{id}/scope) narrows what an account sees to the groups you pick and
    everything beneath them; "All" restores the whole fleet. Enforcement across the read surface
    shipped earlier in this release — this is the part that hands a scope out, so a scope is now
    something an account can actually hold rather than a value nothing could be set to. A node in no
    group stays visible only to unrestricted accounts, and a node outside the scope answers 404 (the
    same answer an unknown id gets, so the scope cannot be used to probe for what exists). Saving a
    scope signs the account out of its current sessions, the way a role change does — the scope is
    captured in the session token, so a live one would keep the old, wider view.
    Two rules worth knowing: an Admin cannot be scoped (409 admin_is_unscoped) because
    administration is fleet-wide, and promoting an account to Admin clears whatever scope it held; and
    a scope naming no groups is refused (400 empty_scope) rather than stored, since it would
    otherwise be an account that signs in successfully to an empty inventory with nothing to explain
    why. GET /api/v1/users and GET /api/v1/auth/me now carry scope, and the account menu says so
    out loud when the signed-in account is limited. SSO accounts are provisioned unrestricted and are
    narrowed here; the stored assignment survives every subsequent login (mapping IdP groups to a
    scope is a later increment).
  • Scheduled analyses. Troubleshoot ▸ Scheduled runs an analysis on a preset cadence — daily,
    weekly or monthly at a time of day (UTC), over the whole fleet, a site or one node. Until now
    every analysis had to be launched by hand, so a nightly anomaly sweep meant someone remembering.
    GET/POST /api/v1/analysis/schedules and PUT/DELETE /api/v1/analysis/schedules/{id}, all
    Operator-and-up like launching a run.
    Two behaviours worth knowing: a fire the runner's admission control refuses is deferred, not
    skipped
    — the schedule stays due and the next minute's tick retries, rather than losing a whole
    period to a busy moment — and the traffic-flow analyses cannot be scheduled on a deployment
    with no flow store
    , because each fire would write an empty run forever. A schedule defaults to
    not notifying, unlike a run you launch and wait for.
  • Saved findings — search what the analyses found, across every run. Troubleshoot ▸ Saved
    findings (GET /api/v1/analysis/findings) lists findings from every analysis, newest first,
    filterable by node or site, by analysis, by severity and by time window. Until now a finding was
    only reachable through the run that produced it, so "has anything been found about this switch
    lately" meant opening runs one at a time. Rows link back to the run's report. The endpoint is
    keyset-paged: pass the last row's at and id back as before and before_id.
  • The REST API accepts an API token. Until now a yat_… token authenticated /mcp alone and
    the REST API answered 401, so unattended automation had to store a password and log in on every
    run. A token now names the surfaces it may be presented at, and one that includes rest works
    on /api/v1 exactly like a session token: Authorization: Bearer yat_…. Existing tokens carry
    mcp alone, so upgrading cannot turn a credential minted for an AI client into one that can
    reconfigure monitoring — reaching REST is an explicit choice made when the token is issued.
    Two limits apply to a token wherever it is used: it cannot administer users (a credential that
    could mint its own successor would outlive every revocation of the original), and endpoints that
    identify the signed-in account — GET /api/v1/auth/me, the personal dashboard — answer 403.
  • API tokens have an owner and an optional expiry. A token's effective role is
    min(token role, owner's current role), so demoting an account narrows its tokens at once, and
    disabling or deleting an account revokes them. Expiry is optional: POST /api/v1/api-tokens takes
    expires_at, and omitting it still means no expiry — appropriate for a service account driving an
    integration. Settings ▸ API tokens shows the owner, the surfaces, the expiry and why a token is
    refused when it is.
  • Service accounts. POST /api/v1/users takes kind: "service" — a machine account with no
    password that cannot sign in through either the local form or SSO. It exists to own API tokens, so
    an integration keeps working when the person who set it up changes teams, and so that disabling it
    stops every credential it owns at once. password is now optional in that request body and is
    refused for a service account rather than ignored. The lock-out guard that protects the last
    admin now counts only accounts a human can sign in with, so a service account cannot become the
    only administrator.
  • YAGRA_PAT_OIDC_IDLE_DAYS (default 30) bounds how long an API token owned by an
    SSO-provisioned account survives its owner's silence. Yagra is never told when an identity
    provider disables an account — the accounts table is only refreshed by a successful SSO login —
    so an absent owner is the only signal available. Local and service accounts are unaffected.
  • URL monitors can present credentials. A new http_auth credential kind covers Basic, Bearer
    and a custom header; bind one to a URL monitor and the poller pre...
Read more

v0.1.19 AI-assisted root-cause analysis, node-to-poller visibility, generated OpenAPI contract, editable URL/DNS monitors, auth-before-availability guards, counter-threshold and IPv6 SSRF fixes

Choose a tag to compare

@horryworks horryworks released this 01 Aug 16:32

📋 Full release notes: RELEASE_NOTES.md

Ask why, and see who polls what. Two additions lead this release. AI-assisted root-cause
analysis
runs from a live alert: Yagra already assembles the incident — the cascade's root cause,
the metric anomaly, the passive events and the dominant traffic around it — and now asks a language
model for the sentence a human writes at the end. It is off until you configure a provider, and
nothing in the alert path depends on it. Alongside it, node→poller visibility: every node says
which pool it belongs to and which poller is actually polling it, and pools are assignable from the
inventory tree — including on a folder, which is the only bulk assignment the tree has. Underneath
both, the northbound API now publishes its own OpenAPI 3.1 document, generated from the handlers,
with the WebUI's types generated from that — which removed 3,340 lines of hand-transcribed
TypeScript and closed a class of contract drift for good.

Breaking changes

  • An unauthenticated request now answers 401 across the whole API, not 503. Yagra answers 503
    when a subsystem an endpoint needs is not configured ("skeleton mode"), and roughly a hundred
    handlers ran that availability check before the permission guard — so an anonymous caller could
    tell a configured deployment from an unconfigured one without holding any credential. Guard order
    is now uniform API-wide: authenticate and authorize first, check availability second. Authenticated
    callers see no change; only anonymous requests to an unconfigured subsystem move from 503 to 401.

  • VITE_API_BASE is now an origin, not a base path. It used to default to /api/v1 and be
    prepended to relative paths; it now defaults to empty and the /api/v1 prefix is part of every
    path. Only a WebUI build that overrides it is affected, and only to drop the /api/v1 suffix:
    VITE_API_BASE=https://core.example.net, not …/api/v1. This also fixes the live-update streams,
    which never appended /api/v1 themselves and so pointed at a different host than the API client
    whenever the variable was set.

  • GET /api/v1/thresholds returns an envelope and is capped. The response changed from a bare
    StoredThreshold[] to { "items": [...], "total": <n>, "truncated": <bool> }, and the server
    returns at most 500 rules per request — ?limit= can narrow that, never widen it. The WebUI
    ships in the same image and was updated with it, so this affects external automation only.
    Anonymous requests to this endpoint now answer 401 rather than 503, matching the rest of the
    API: a caller is authenticated before the server discloses whether a subsystem is configured.
    Reading the rules requires ManageConfig, not View — a threshold set describes when and whom
    Yagra will page — so it stays closed on a public dashboard.

  • POST /api/v1/api-tokens rejects a group scope. Sending {"scope": {"Groups": [...]}} now
    answers 400 unsupported_scope instead of minting the token; omit scope or send "All".
    Nothing enforced a group scope on either surface — /mcp refuses a group-scoped token outright
    and the REST endpoints never consulted the scope at all — so what was issued was a credential
    that looked least-privileged and was in fact either unusable or unrestricted, depending on where
    it was pointed. The WebUI never offered the field, so this affects API clients only. Group scoping
    will be accepted again when the read paths actually filter by it.

  • Four report and group endpoints now answer the status code the rest of the API does. The three
    report deletes — DELETE /api/v1/reports/definitions/{id}, /runs/{id} and /schedules/{id}
    returned 200 {"ok": true} where the other 24 deletes in the API return 204 No Content; they now
    return 204 with no body. POST /api/v1/node-groups was the only creator that discarded the id
    it had just generated, answering 204; it now returns 201 with {"id": "<uuid>"} like the other
    twenty creators. POST /api/v1/reports/schedules returned its {"id": …} under 200 and now
    returns 201. A client that checks for an exact status code, or reads ok off a delete
    response, needs updating; the WebUI ships in the same image and ignored both.

  • GET /api/v1/rca/{id} and POST /api/v1/rca now describe what is inside a report. The
    body field was published as an untyped JSON blob, so a generated client got unknown for the
    entire AI answer and its evidence. The document now carries real schemas for the answer
    (summary, root_cause, dependents, next_steps, confidence, raw) and for the incident
    context it was grounded in. The bytes on the wire are unchanged — only the description was
    missing. Regenerate your client to pick the types up.

  • Running a Troubleshoot analysis is now an Operator action on both surfaces. It required
    Admin over the REST API and merely Viewer over MCP, so the on-call operator was refused in
    the WebUI while the same person could run the identical analysis through an AI client. Both now
    ask for the acknowledge-alerts permission (Operator and up), which is also what cancelling a run
    takes. An analysis changes no configuration — the admin requirement was standing in for a rate
    limit, and real admission control has done that job since it was added. Reading past runs and
    their findings is unchanged and still open to Viewers. Viewer-scoped API tokens can no longer
    launch analyses over /mcp.

  • Four endpoints nothing called have been removed. GET /api/v1/rca/{id},
    GET /api/v1/reports/definitions/{id}, PUT /api/v1/node-groups/{id}/geo and
    POST /api/v1/events/alerts/close were reachable but called by neither the WebUI, the MCP tool
    surface, nor any documented automation — each answering requests, appearing in the published
    contract and carrying tests for a feature that had no way in. They now 404. The data each read
    is still served: an RCA report comes back from the POST /api/v1/rca that produced it, and a
    report definition from GET /api/v1/reports/definitions. Two are a capability loss, not a
    cleanup:
    group map coordinates can no longer be set at all — the Sites map widget reads them
    and nothing writes them any more — and an event-raised alert can no longer be closed by hand; it
    clears on its rule's TTL as before. Say so if either mattered to you and it can come back with a
    UI attached.

New Features

  • AI-assisted root-cause analysis, on demand. Yagra already assembles the evidence for an
    incident: dependency suppression attributes a cascade to its root cause, and the incident timeline
    gathers the metric anomaly, the passive events and the dominant traffic around it. What no amount
    of correlation produces is the sentence a human writes at the end. Active alerts gains a button
    that asks a language model for that sentence, grounded in exactly that evidence, and returns a
    summary, a probable root cause, the dependents it explains and suggested next steps — with a
    confidence, and the model's raw answer kept beside it.
    It is off until you configure it. With no provider row there is no client, no credential and no
    egress. Nothing in the alert path calls into it, so hysteresis, suppression, dedup and notification
    behave identically whether the provider answers, times out, or was never set up. Choose one
    provider — Vertex AI, Gemini or Claude — whose credential is sealed with the same
    envelope cipher as every other stored secret and is write-only once saved. Generating a report
    takes Operator (the people carrying the pager are the ones who need the explanation) and is
    bounded by a concurrency limit, a rate window and a context cache rather than by a narrower role;
    reading one back takes View; configuring the provider takes Admin. Device output quoted into the
    prompt is fenced, each provider's endpoint is a compiled-in constant rather than a settings field
    so a configuration screen cannot become an exfiltration channel, and Yagra still has no way to
    configure a network device
    .
  • Every node says which poller polls it, and pools are assignable from the inventory tree.
    Node→poller assignment existed but was effectively invisible: answering "which poller polls this
    node?" meant running redis-cli, and a node's pool was writable only through an API field no
    screen sent. Node detail now shows Pool and Polled by — the latter a five-state answer
    (assigned / pending / legacy fan-out / Meraki / unknown) read from the working set core actually
    published rather than re-derived from the hash ring, so a node's answer and a poller's node list
    cannot disagree. Settings ▸ Pollers drills into any poller's node set inline. Pool is now
    editable on a node and on a folder, and right-clicking either in the inventory tree offers a
    pool chip row
    — the pools that exist, plus Inherit and Custom — which is the only bulk assignment
    the tree has, since it has no multi-select. A node's effective pool resolves as its own → nearest
    ancestor folder → default, and every chip says whether that pool has a live poller: assigning
    to a pool with none publishes its jobs to a subject nothing subscribes to, and the node silently
    stops being monitored.
  • A URL or DNS monitor's configuration can be edited and removed after it is created. Until now
    the add-node dialog could create one and the node detail could display it, but changing a URL, a
    timeout, a resolver or a record type meant deleting the node and making it again — the endpoints
    existed the whole time with nothing calling them. The node's URL/DNS health card gains a ⋮ menu
    with Edit and Remove monitoring; the editor covers every field including the expected
    HTTP status (any 2xx, an explicit code ...
Read more

v0.1.18 Passive-data forwarding tee (syslog/traps/flow + BigQuery), DNS name-resolution monitoring, 11 new Troubleshoot analyses, per-tool reports for all 15, passive/flow dashboard widgets

Choose a tag to compare

@horryworks horryworks released this 26 Jul 02:18

📋 Full release notes: RELEASE_NOTES.md

Hand your passive data onward, and see it. Yagra already received syslog, SNMP traps and flow
exports; this release lets it forward them — a filtered tee to a SIEM or collector, byte-for-byte
or rebuilt, over UDP/TCP/TLS, plus BigQuery destinations that stream normalized rows for long-term
querying. Alongside it: DNS name-resolution monitoring as a first-class node kind, eleven new
Troubleshoot analyses
over the passive-event and flow stores, a tailored report screen for every
one of the 15 analyses
, and thirteen new dashboard widgets for passive events and traffic flow.

New Features

  • Forwarding ("tee") destinations — a new Settings ▸ Forwarding page defines destinations that
    say "everything matching this filter also goes there". Core does the sending from one leader-only
    egress point, so you allow one address through the firewall rather than one per poller, and you
    stop configuring a second export target on every device.
    • Syslog and SNMP traps relay byte-for-byte: the original datagram is carried alongside the
      parsed event, so the collector sees exactly what the device sent. Where no original exists, Yagra
      rebuilds a faithful RFC 5424 / SNMPv2c message instead.
    • Flow exports (NetFlow v5/v9/IPFIX, sFlow) relay verbatim, template datagrams included — the
      aggregated flow records can't stand in for them, because bucketing, top-N truncation and 5-tuple
      folding are irreversible. Flow filters are an any-record test: one matching record forwards the
      whole datagram, since records can't be removed without re-encoding it.
    • Syslog over TLS (RFC 5425) verifies the collector against the system trust store plus an
      optional per-destination CA certificate.
    • BigQuery destinations stream normalized, typed rows — one per event, one per flow record —
      via tabledata.insertAll, for querying months of history rather than mirroring a live stream.
      Because rows are independent, flow filtering here is exact per record. The table is created for
      you with day partitioning and clustering; the dataset is not, so Yagra never picks your data
      residency for you.
    • Each destination has a bounded queue, rate limit and circuit breaker, and cannot silently
      degrade
      : a destination promised byte-exact output but given none counts it, and any poller that
      can't supply original bytes is named on the page.
  • DNS name-resolution monitoring (a node kind) — monitor a name the way you monitor a URL. Bind a
    node to the built-in DNS name resolution profile and Yagra records whether the name resolves and
    the dig-like recursive CNAME chain it resolves through, with a history that appends only when
    the chain actually changes
    (TTL countdown and round-robin reordering don't count as a change).
    Numeric summaries (dns_up, dns_resolve_ms, dns_chain_length, dns_answer_count) are graphable
    and alertable, with a default dns_up threshold seeded so a new monitor alerts out of the box.
  • Eleven new Troubleshoot analyses over the passive-event and flow stores: event_storm,
    event_flap, severity_shift, rule_gap, auth_probe (passive); traffic_anomaly, talker_shift,
    new_destination, flow_scan (flow); and saturation + incident_correlate reading across metrics,
    events and flow together. As before, an analysis is a read — no device I/O — under the same
    concurrency and rate limits.
  • A tailored report for every Troubleshoot analysis. Previously only the anomaly scan had a real
    report and the other 14 tools showed a "coming soon" toast; now all 15 have their own screen built
    for their own findings — including an SVG incident timeline for incident_correlate (the order
    signals arrived in is what points at a cause), a scan-shape scatter for flow_scan, and a share
    meter with capacity context for saturation. Every report supports CSV export and ?job= deep links.
  • Passive-events and traffic-flow dashboard widgets — two new catalog sections adding 13 widgets:
    event feed, volume, kind mix, top traps, triage, noisy sources and rule coverage; plus top talkers,
    top AS, top ports, protocol mix, a conversation Sankey and a traffic trend. The flow widgets read
    fleet-wide (every exporter), not one node at a time.
  • New MCP tools for flow and eventsflow_fanout and event_stats, and top_flows gains
    protocol/port/peer/ASN/direction filters with AS-name resolution. run_analysis accepts all the new
    analysis kinds.

Improvements

  • Container images for the metrics and log stores are pinned. docker-compose.yml and
    docker-compose.deploy.yml referenced victoria-metrics:latest and victoria-logs:latest; both now
    name an explicit version, so an unrelated docker compose pull can no longer roll the storage engine
    underneath your history. Bump them deliberately, with a backup — the same policy as the pinned Rust
    base images.
  • Store-and-forward spilling is cheaper under pressure. A remote poller buffering results during a
    bus outage no longer issues a filesystem free-space syscall per spilled result on its async runtime;
    the reading is cached and debited by bytes written, so the safety floor still trips early rather
    than late.
  • Traffic-flow views read better — conversation endpoints get a minimum label slot so long
    addresses stay legible, the conversation Sankey is size-capped instead of growing without bound, and
    source/destination AS numbers are shown alongside the addresses.
  • Fleet-wide flow endpoints. The flow API is no longer node-scoped only; series, top talkers,
    conversations, top ports, protocols and top-AS all take a fleet scope. A new /events/stats endpoint
    serves categorical and time-series event aggregates over the same filter as the event log, via
    PostgreSQL or VictoriaLogs, so summaries and the log always agree.

Bug Fixes

  • A poller no longer stalls when it meets a check kind it doesn't understand. Working-set snapshot
    chunks are now decoded per element, so one unknown check can't fail a whole chunk, gap the sequence
    and spin that poller in a resync loop — which stalled all of its polling, not just the unknown
    check. This is a permanent fix for every future check type, not just DNS.
  • DNS checks no longer suppress one another. The per-target single-flight poll guard would drop
    every DNS check but one per cycle, because DNS monitors share a resolver target by design; they now
    take the global guard instead.
  • The AS drill-down no longer breaks the Conversation flow view. Filtering by AS returned a 500 for
    the conversation table and Sankey (the aggregate was aliased to the same name as the column it
    filtered on), leaving stale data on screen while the other panels updated.
  • Troubleshoot report deep links resolve correctly. A ?job= older than the recent-jobs window
    rendered as if nothing had been requested, and a ?job= belonging to a different tool rendered the
    wrong report over foreign findings; the report shell now fetches the job directly and redirects to
    the report that can actually read it.

Security

  • Forwarding is built so it can't quietly leak. TLS destinations have no option to disable
    certificate verification
    . BigQuery destinations deliberately have no raw-payload column — a
    relayed datagram passes once to a collector you chose, but a table persists, and a raw-bytes column
    would make the credentials that routinely appear in syslog bodies permanently queryable off-box; the
    API rejects verbatim mode for BigQuery rather than relying on a hint being read. Service-account
    signing uses a constant-time RSA implementation.
  • Removed an unmaintained TLS dependency. rustls-pemfile (RUSTSEC-2025-0134, archived upstream)
    is gone; certificate and key parsing now uses the same code directly from rustls-pki-types. The
    supply-chain policy check passes clean across advisories, bans, licenses and sources.
  • Resolved a client-side open-redirect advisory by updating the frontend router, and a
    path-traversal advisory in the build toolchain. The router fix is user-facing; the toolchain fix is
    build-time only and never shipped.

Container images

Published to GitHub Container Registry by CI for this tag:

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

:latest tracks main (the test-server channel) and is not moved by this tag — pin :v0.1.18 for a stable deployment.

v0.1.17 MCP server for AI assistants — read + Troubleshoot + audited write tools, API tokens, analysis rate caps

Choose a tag to compare

@horryworks horryworks released this 21 Jul 00:45

📋 Full release notes: RELEASE_NOTES.md

Talk to Yagra from an AI assistant. Yagra now ships a built-in MCP (Model Context Protocol) server — an opt-in, authenticated tool surface at /mcp that lets an AI client (Claude Code/Desktop, or any MCP-capable assistant) query live monitoring state and run diagnostics in your own words. It is off by default and, when enabled, is mostly read-only; the few write tools are permission-gated and audited, and nothing can change device configuration.

New Features

  • MCP server (AI / automation tool surface) — enable with YAGRA_ENABLE_MCP to expose a Streamable-HTTP MCP endpoint at /mcp on the API port. It offers 15 tools: read tools for fleet summary, nodes, node status, active/historical alerts, metrics, topology, traffic flows, and passive events (syslog/traps/webhooks); on-demand Troubleshoot analyses (anomaly / correlation / capacity / flap); and three write tools — acknowledge an alert, open a maintenance window, and trigger an immediate poll. Tool output is sanitized (monitoring credentials never leave the system) and there is no tool that configures or changes a network device.
  • API tokens — a new Settings ▸ API tokens page issues long-lived yat_ tokens (each with a role and group scope) for non-browser clients such as an MCP assistant. The raw token is shown once at creation and only its hash is stored; issuing and revoking are admin actions and are audited.

Improvements

  • On-demand analyses are rate- and concurrency-limited. The Troubleshoot analysis runner now caps how many analyses run at once (YAGRA_ANALYSIS_MAX_CONCURRENT, default 4) and how many may start per minute (YAGRA_ANALYSIS_RATE_PER_MIN, default 30); when saturated, the API returns 429 instead of piling on work. This bounds the cost of both the UI and the new MCP analysis tools.

Security

  • Per-tool authorization and audit for MCP write actions. Every MCP write tool re-checks the caller's role (RBAC) and records an audit entry; the surface is fail-closed — if the caller can't be authorized, the write is refused. A Viewer token stays read-only.
  • Resolved two High-severity advisories in the frontend build toolchain by updating transitive dev dependencies (brace-expansion, js-yaml). These are build-time only and never shipped in the product, but the dependency tree is now clean.

Container images

Pull the versioned images from GHCR:

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

v0.1.16 Relicense to AGPL-3.0-only + flow-ingestion hardening

Choose a tag to compare

@horryworks horryworks released this 20 Jul 10:16

📋 Full release notes: RELEASE_NOTES.md

Licensing and flow-ingestion hardening. Yagra is now released under the GNU Affero General
Public License v3.0 (AGPL-3.0-only)
, and this release hardens the traffic-flow ingestion path so a
busy or slow flow store can't disturb the rest of the system. There are no new user-facing features —
a default install behaves exactly as v0.1.15 did, now under the new license.

Licensing

  • Relicensed to AGPL-3.0-only. Every source file now carries an SPDX header and the repository
    ships under AGPL-3.0-only. Because Yagra is typically run as a network service, note AGPL §13:
    if you run a modified version and let others use it over a network, you must offer them its source.
    For terms other than the AGPL (e.g. embedding Yagra in a proprietary product), a commercial
    license
    may be available — see the README.

Improvements

  • Flow ingestion is isolated from a slow flow store. The ClickHouse flow writer now runs
    separately from the flow bus consumer, handing rows off over a bounded queue. A slow or hung
    ClickHouse can no longer back the yagra.flows subscription up into a silent message drop — under
    pressure, flow rows are dropped and counted (the store is loss-tolerant by design) instead of
    stalling ingestion.
  • Flow exporter resolution no longer reloads the node table per batch. Flow from an exporter whose
    source IP isn't a registered node (common for routers that export from a loopback) used to re-scan
    the whole node table on every batch; each unmapped exporter is now retried at most once, so flow
    ingestion stays cheap at tens of thousands of nodes.

Container images (published by CI to GHCR — pull :v0.1.16 for this release):

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