Skip to content

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
· 12 commits to main since this release

📋 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 widget ranks the whole fleet by any metric name. The
    curated Top RTT / CPU / memory cards each rank one thing the catalog chose; this one ranks whatever
    you type, current value or trailing-hour peak, so a vendor metric no card covers can still answer
    "which of my devices is worst". Three behaviours worth knowing:

    • The metric is typed, not picked from a list. There is deliberately no fleet-wide metric
      catalogue to choose from — enumerating every series across every node is the one query that does
      not survive a large fleet. The field suggests the metrics this browser has already seen on nodes
      you opened, and accepts anything else.
    • A counter is refused rather than ranked. Ranking a counter's stored value ranks how long each
      node has been up, not how busy it is. Use the interface Top-N cards for traffic and errors, or
      chart the counter as a rate on a node.
    • An empty ranking says why it might be empty. Nothing reporting the metric and a name that
      does not match are indistinguishable from the browser, so the message admits both rather than
      implying the fleet is idle.
  • Adding an SSO provider now starts by picking which identity provider it is — Microsoft Entra
    ID, Okta, Google Workspace, or "Other" for anything else — and then asks only for what that
    product needs. The form used to present eight free-text fields and assume the operator already
    knew what their IdP wanted, which was not a safe assumption: the scopes it pre-filled included
    groups, and Entra ID rejects any non-standard scope outright, so an Entra deployment never
    reached a sign-in page at all
    . Dropping groups everywhere was not the fix either — it is
    exactly how Okta delivers group membership. What each product now contributes:

    • Entra ID asks for the directory (tenant) ID and builds the issuer URL from it. It requests
      only the standard OIDC scopes, and says that the groups claim is turned on in the app
      registration's token configuration and arrives as group object IDs.
    • Okta asks for the org domain and builds the issuer URL from it, requesting the groups
      scope its org authorization server serves. A custom authorization server has a different issuer
      and belongs under "Other", which the form says.
    • Google Workspace has one issuer, so there is no URL to enter. Its group→role mapping is
      gone, deliberately
      : Google does not put group membership in the ID token, so a mapping
      configured against it could never match. The form explains this and requires a default role,
      since without one every sign-in would be denied.
    • "Other" is the previous form, unchanged, for any other OIDC provider.
    • Existing providers are untouched. They list and reopen as "Other", which is what they are —
      they were defined field by field. Nothing about how a configured provider signs users in has
      changed, and reopening one whose issuer this form does not build leaves it exactly as stored.
    • The API gains an optional kind on POST/PUT /api/v1/settings/oidc and reports it on the
      listing (and through the MCP get_config surface). Omitting it means generic.
    • ⚠️ This has not been exercised against a live Entra, Okta or Google tenant. What each
      product accepts is taken from its published documentation.

Improvements

  • Device health and the metric list on a node's Overview no longer require admin rights. They
    were read from the collection-set endpoint, which requires ManageConfig, so a Viewer saw a single
    built-in metric and no health gauges at all. They now come from the metric inventory, which is a
    read.
  • The Japanese UI copy was swept end to end. It had accumulated two spellings for the same
    terms, English sentence structure carried through the translation, and notation that drifted
    between screens. Wording changed on most screens; no behaviour did. The conventions are now
    written down so new strings stay consistent.
  • The scheduler no longer queries url_checks once per node per sweep. URL-monitor ids are now
    preloaded once a round, the way DNS monitors already were, so a fleet of tens of thousands of
    ordinary devices stops paying one database round trip each, every polling round, to discover it
    has no URL check.
  • The WebUI loads the Topology, Troubleshoot and Settings sections on demand. Everything on an
    operator's daily path — dashboard, nodes, alerts — still arrives in the first download, but the
    ~25 screens behind those three sections no longer do, so opening the dashboard stops fetching the
    world-map outline, the report registry and seventeen settings pages nobody asked for. Moving
    between screens inside a section is unaffected: a section loads once, then behaves as before.
  • Two fleet-scale paths stopped doing work proportional to the fleet on every round. Neither is
    visible on a small deployment; both were the difference between a steady state that costs nothing
    and one that does not. The scheduler no longer deep-copies every node's resolved check set — with
    its decrypted credentials, OID column lists and route-probe plans — once per pool per sweep just
    to discover nothing changed, and the coordinator now holds its registry lock only while reading
    membership, so poller heartbeats and the Pollers view no longer queue behind a working-set diff.
    Alert evaluation resolves each metric's threshold once per poll result rather than once per
    sample, which matters most for a wide SNMP table where one result carries a hundred samples of
    the same metric. The dwell window still sees every sample individually — only the repeated
    lookup went away.
  • The Nodes tree no longer rebuilds on every live status update. The status dots arrive over a
    stream that flushes for any node in the fleet; the tree was being reconstructed on each flush
    even when nothing on screen had moved.

Bug Fixes

  • The dashboard summary, the per-group tallies, the network map and the inventory report no longer
    disagree with the Nodes page about a node's state.
    All five surfaces are supposed to apply the
    same rule — the alert engine's opinion when it has one, otherwise a recent ICMP sample means ok
    — but the rule had been written out by hand in five places and two of them had dropped the
    fallback. The visible symptom was a core restart: for the minutes before the first sweep the
    dashboard reported unknown for the same nodes the Nodes page beside it was showing as ok, and
    a PDF inventory report generated in that window printed unknown down the whole column. The rule
    now lives in one place. Deliberately unchanged: the fleet health timeline still records the
    raw engine view, so the post-restart gap remains visible in history — a historical record should
    say what was actually being monitored at that moment.
  • A Cisco Meraki organization collecting the inventory tier showed the raw key
    meraki.tier.inventory instead of a label.
    The API accepts all four tiers and the org list
    prints what is stored, but only three of them had ever been given a name. The cadence dialog
    still offers three checkboxes — inventory is a reconciliation triggered from "Import devices",
    not a recurring collection, so a checkbox for it would promise polling that never happens.

Security

  • CSV exports can no longer carry a spreadsheet formula that executes when the file is opened.
    The audit log records the username submitted to a failed login, so anyone who can reach the
    sign-in page could plant a cell reading =HYPERLINK("http://…"&A1,"Click") and have it evaluated
    by the administrator who later exports and opens the log; the Troubleshoot exports carry
    device-supplied strings with the same problem. RFC 4180 quoting does not help — a spreadsheet
    strips the quotes and then evaluates the text. Values beginning with =, +, -, @, TAB or CR
    are now prefixed with an apostrophe so the cell is read as text. A value that is entirely a
    negative number (-5, -0.31) is exempt and still exports as a number: the Troubleshoot reports
    export correlation coefficients, and neutralizing those would leave a column that is text for half
    its rows and numeric for the other half — which sorts wrong rather than merely looking wrong.
  • PDF report rendering no longer has access to the container's filesystem. Reports are rendered
    by wkhtmltopdf, which was being invoked with --enable-local-file-access for no reason: the
    generated document is self-contained (inline styles, inline SVG charts, no images, no links, no
    url()), so nothing in a report ever needed to read a file. The flag is what would turn an
    escaping mistake in the device-supplied text of a report into a local-file read, so it is now
    passed as --disable-local-file-access rather than left to a default that a future version could
    flip back.
  • The test transport that reports every device reachable is no longer compiled into the shipped
    binaries.
    It is now behind a feature that only test builds enable.