Release
Announcing Toise 0.10.0 — delete provenance and host-scoped endpoints
When something disappears from your infrastructure graph, the first question is always the same: did it go away, or did we stop hearing from it? Until now Toise could not tell you. 0.10.0 records the answer on every disappearance, and stops two machines' loopback services from collapsing into one node. Both changes are additive.
This release ratifies an alignment round with the reference producer, and
most of it comes from a real incident rather than a design session — the
kind of round where the interesting output is the rule you write down
afterwards. It is not a wire-contract break and needs no data migration:
newly written events carry schema version 1.1, older events
read back unchanged.
Who authored the disappearance
Before 0.10.0, an entity expired by the liveness backstop and an entity deleted by its producer were rigorously indistinguishable on every read surface, and a removed relation covered three origins with no discriminant at all. That is exactly the question a flapping incident asks: did my agent delete this, or did Toise reap it after a missed heartbeat?
Every deletion now carries its author: producer for an
explicit delete or an edge dropped by absence on re-emit,
liveness_expiry for Toise's own backstop, cascade
when an endpoint died and took the edge with it. It surfaces on
recent_changes, entity_history and
graph_diff — including the transient bucket, which turns the
flap surface into a one-call triage — and on GraphQL as
ChangeEvent.deleteSource.
Two things it deliberately does not do. It never touches
delete_reason, which stays the producer's verbatim motive and
is never used to derive the source. And events recorded before 0.10.0 read
back with an unknown source rather than being
retroactively labelled producer — a guess written into history
is worse than an admitted gap.
Two hosts, two loopbacks
An endpoint is identified by what an observer can actually see:
{server.address, server.port, network.transport}. That works
because two observers dialing the same routable address should land on the
same node. For an address whose scope is narrower than the
observation domain, the same rule tells a lie: every host's
127.0.0.1:5432 collapsed into a single node shared by the
whole fleet.
Endpoints in exactly 127.0.0.0/8, ::1,
169.254.0.0/16 and fe80::/10 now carry the
observing host's id as a fourth identity key. Everything
else — RFC1918 and CGNAT included — keeps the three-key form and its
cross-observer join, because a LAN address is shared by construction and
that sharing is the point. Resolution honors the scope: a host-scoped
endpoint resolves only against its own host's listeners, never through a
fleet-wide scan that would re-merge exactly what the fourth key separated.
Rules that came out of the incident
A producer was flapping, and the cause was a sizing rule read one level too
low. The recommended slack on entity.report.interval applies to
the effective re-emission cadence — what a consumer can
actually observe — not to the internal heartbeat tick. Sizing on the tick
gave that producer no tolerance to a single missed re-emission, which is a
mechanical flap waiting to happen. The contract now says so.
Alongside it: telemetry.relay.* as the vendor-neutral way to
say "this telemetry passed through this agent on this host" on relayed
spans, first relay winning and the set of three inserted atomically; and
address canonicalization frozen, so that a CIDR is explicit-prefix with
zeroed host bits and every IPv6 identity is RFC 5952 text.
Security
grpc v1.82.1 and the Go toolchain 1.26.5 in both
modules, clearing two advisories; govulncheck is symbol-clean.
Producers get the same grpc bump through pkg/emit/v0.5.1.
Get it
go install github.com/toise-dev/toise/cmd/toise-server@v0.10.0
Binaries for linux and darwin (amd64 / arm64) are on the release, each with a checksum, and the container image is on GHCR. See the 0.10.0 docs and the changelog.