Release
Announcing Toise 0.18.0 — making the graph legible
A correct answer still has to be readable by whoever receives it. 0.17.0 was about saying what an answer is worth. This one is about making it easy to read.
The trigger was ordinary feedback from a consumer: a well-formed question, a correct answer, and entities that were hard to recognise at a glance. The names were in the graph; the compact answer just did not lead with them.
Legible to whom matters here. Toise's reader is an MCP client or a GraphQL caller, not a person looking at a picture; the human-facing picture lives at the edge of this project on purpose. So this release is about the strings and handles a program gets back, not about a drawing.
Read this before upgrading if you consume entity labels.
A label now contains a value that drifts: a rename changes it, where
before it did not. Anything that groups, joins or matches on a label has
to move to identity_fingerprint. There is no wire-contract
break and no data migration — a 0.17 deployment upgrades in place.
A name meant to be shown
Until 0.18.0, an entity's only human-facing string was its label, and the
label was built from its identifying attributes. That is right for identity,
and not what a screen wants: in the compact verbosity mode —
the mode that exists so an assistant can scan many entities cheaply —
twenty-one hosts rendered as host host.id=<uuid> and
ninety-four containers as sixty-four hex characters.
The names were in the graph the whole time. host.name was
present on all twenty-one hosts, container.name on all
ninety-four containers. The compact mode simply did not show them.
So entities now carry display_name (MCP) and
displayName (GraphQL) beside the identity fingerprint. It is
the value meant to be shown, on its own, with no parsing. Where a single
attribute cannot name a thing — a network endpoint — it composes one:
network.endpoint 10.0.0.5:5432
[2001:db8::5]:5432 (IPv6 bracketed, so the port stays legible)
That leaves three values with three jobs, and none invites another's use:
identity_fingerprint to match on
display_name to show
label to scan
The label changed too, and that one needs a warning rather than a note. It
now reads host dash172 host.id=<uuid>: the identity stays
in full, and the name leads because that is what a reader reaches first.
But a label now contains a value that drifts, so a rename
changes it where before it did not. Anything that grouped, joined or
matched on a label has to move to identity_fingerprint, which
is returned beside it, is identical across replicas, survives re-minting,
and does not move when a thing is renamed. Code that grouped by label
across incarnations should move to the fingerprint now.
Both spellings of the interface name
senhub-agent 0.6.0 moved its identifying key for an interface from
interface.name to network.interface.name, the
semantic-convention spelling. The two are different identities, so an
interface is re-minted once when its producer migrates — expected, and it
happens once.
Toise accepts both spellings as telemetry join keys and as display names for as long as the retention window still holds pre-migration observations. Dropping the old spelling on the day the new one lands would blind the metrics pivot for every interface observed before the migration, which is the whole point of keeping a history. A test pins that the two remain distinct: merging them would be the silent merge exact identity exists to forbid.
The same principle, applied to correctness
The third change is not about reading. Entity liveness has been reference-counted per producer since ADR 0019: an entity stays live while any producer asserts it, and dies when the last reference goes. Edges were not. One reference per edge, and any removal deleted it outright.
Wherever an entity is shared, the two halves contradicted each other.
Several producers emit the same network.address because they
reference it as their gateway, exactly as the producer contract asks; only
the one that owns the interface carries the bound_to
descriptor. Every other emission arrived with no descriptor for that
entity, the reconciler read the absence as a retraction, and the edge was
removed — then re-asserted by its owner, then removed again.
Measured on a lab graph: the edge joining the gateway to the interface that
holds it was asserted and retracted about a hundred and fifty
times a day. Each removal was reported with
delete_source=producer. Technically accurate, since a
producer's event caused it, but misleading, because that producer had
never asserted the edge in the first place.
Edges are now reference-counted per producer exactly as entities are. An edge survives while any producer asserts it. A producer that merely references a shared entity no longer disturbs anything on it. Endpoint death is unchanged — a deleted entity still takes every incident edge with it — and snapshots written by earlier builds restore unchanged.
And, at the edge: the example viewer
examples/graph-viz/ is an example, not a product surface. It
earns a mention because the measurement behind its change says something
about graphs in general.
Before touching it we measured a real lab graph: 546 entities, and 478 of them — eighty-eight per cent — are the child of a single owner. An interface belongs to a device, a service runs on a host, a route belongs to a machine. That ratio is why thinness was a symptom of density rather than a style problem: edges are drawn faint precisely so a hairball stays readable, and thickening them on 546 nodes makes it worse.
Folding the ownership chain draws 48 nodes instead of 546; on a 367-node deployment it draws 3. Nothing is hidden silently — a folded owner says how many of what it holds, and double-click opens it.
The same file also stopped hiding the links it computes, and that part is
not about the example. The engine stores facts only and never merges by
heuristic, so an IP carried as a plain attribute is not linked to the
network.address entity for that IP; Toise resolves the join at
read time and deliberately never writes it back. An operator reported
seeing no link between a gateway and a dashboard it talks to. The link was
there, resolved, but hidden by a default. Derived links are now drawn by
default.
Upgrading
No wire-contract break, no data migration: a 0.17 deployment upgrades in place. The one thing to check before upgrading is whether anything you run groups or joins on an entity label — see the warning above.
One behaviour changes on purpose: an edge asserted by two producers now survives one of them dropping it.
Known limitation
The event log is complete and reading the graph at a past instant is exact.
On a tenant ingesting thousands of events per minute, however, the
change-feed read (recentChanges, and entityHistory
bounded by time) can under-report, down to answering 0 for a
busy period. Below a few hundred events per minute it is accurate. The
behaviour predates this release and is not made worse by it.
It is tracked as #397. If you run at that volume and rely on the change feed to review incidents after the fact, follow that issue.
Saying what an answer is worth was the 0.17.0 programme. It applies to our own release notes too.
Get it
go install github.com/toise-dev/toise/cmd/toise-server@v0.18.0
Binaries for linux and darwin (amd64 / arm64) are on the release, each with a checksum, and the multi-architecture container image is on GHCR. See the 0.18.0 docs and the changelog.