Release
Announcing Toise 0.14.0 — reachability, and the first deprecation
Two things in this release, and they pull in opposite directions. One adds a type, because a question an incident actually asks had no answer in the graph. The other announces the first thing we have promised to take away. Both are the same exercise: deciding what we are willing to be bound by.
The boundary nobody could query
"Why can't A reach B" is the first question a deployment incident asks. Two workloads on the same virtual network segment can address each other; two on different segments cannot, whatever the firewall says. That boundary is the answer, and it was nowhere in the graph.
Not because nobody had the data. A Docker Swarm probe sees every overlay. But none of the registered network types named a segment — a device is a box, an interface is a port, an address is an IP — so the information rode as metric labels: queryable, and not traversable. Answering "what else is on this segment" meant scanning every service and comparing strings.
That is the same shape that kept IP addresses untraversable until
network.address became a node, and the same one that settled
the pod two releases ago: an attribute you have to string-match on
is not a join.
service.instance --has_segment--> network.segment (the cluster declares it)
container --attached_to--> network.segment (the workload joins it)
Verified on a real four-node Swarm before shipping, not asserted from the code:
api attached_to -> ingress, frontend
cache attached_to -> backend
find_path(api -> cache, attached_to) -> reachable: false
One hop. That is the whole point of the type.
The part we refused to freeze
Identity is a subtype-prefixed value, and only
swarm:<network-id> is frozen — the cluster
assigns it, so it is unique and not derived from an address.
k8s: and vlan: are deliberately left open, and
that is the decision rather than an omission. Kubernetes has no object to
identify in the default flat-network case: a NetworkPolicy is a restriction
that composes with others, a Namespace is an administrative boundary the
network ignores. And a VLAN id is not globally unique — VLAN 10 exists at
every site — while a VLAN trunked across five switches is one
segment rather than five, so scoping it by device would fragment exactly
what it is meant to unify.
The first draft of the design froze k8s: anyway, by analogy
with Swarm. The producer team pointed out the analogy did not hold, and they
were right. An identity engraved wrongly costs a migration; an open
one costs a sentence — and the sentence says what would have to be
true for it to close, which is the useful part.
What the edges do not promise
The impact direction says a segment failing takes what is attached to it. Correct as a statement of dependency — and it will never fire.
An overlay is a control-plane construct: it does not fail on its own. What fails is a node or the underlying transport, and no producer can report "this segment is down". Attached-task counts and subnet saturation are metrics, not health.
So both readings are written down, because leaving one out makes the
semantics look richer than they are: impact_of stays meaningful
over a hypothetically failing segment — if this overlay broke,
what goes with it is a real question during a fabric change — while
nothing propagates at runtime, so do not build an alert on it.
The alertable event exists and sits on the edge instead: a container losing
its attachment is observable, and the runtime reports it.
Membership is likewise necessary and not sufficient for reachability. Policies restrict on top of it, and on a flat Kubernetes network shared membership says almost nothing. A graph that overstates what it knows is worse than one that admits a gap.
The first deprecation
After 1.0, the stability policy binds us to whatever the surfaces look like on that day. So the freeze audit asks of each item a different question from "is it documented": would we defend this in three years?
Three relation types answer no. routes_via,
forwards_to and adjacent_to were superseded years
of releases ago and have been documented as "do not emit" since. Frozen at
1.0 they would be supported for the life of 1.x. They are now deprecated for
removal — checked first against production, where all three have zero use.
This release is the notice: our own rule requires a deprecation in a release preceding a removal, so without shipping it now we could not have removed them at 1.0 without breaking the policy on the day we publish it.
The interesting part of the audit was that the four first findings did not
all get the same treatment. ENTITY_IDENTITY_CHANGED cannot be
deprecated at all: it is a protobuf enum value retained to replay logs
written before exact identity matching, so it stays forever. What was
actually wrong is that the change taxonomy listed it with no hint that
nothing emits it — inviting a consumer to write a handler for an event that
will never arrive. Now stated.
And recentChanges gained the 1h default its MCP
twin already had. Backward compatible, so it could have waited — but
freezing two surfaces that answer the same question differently is precisely
what the audit exists to prevent.
Two contract decisions, from a producer's adoption
The reference producer adopted the wire vocabulary and explicitly declined
the SDK's client, for a good reason: their agent already owns a single OTLP
rail carrying its tenant header and enrichment, and a second OTLP client
would have added a connection, an auth surface, and a seam — reintroducing
the drift a shared contract exists to remove. They import
pkg/emit/wire for the vocabulary and run differential tests
against our encoder, so a divergence fails their build.
Their feedback was to keep that separation at 1.0. It turned out the promise
— wire is stdlib-only, so taking the vocabulary never drags a
protocol stack into your module graph — lived in a doc comment and was
enforced by nothing. A convenience import added tomorrow
would have closed that door silently. It is now a test, and the promise was
measured rather than repeated: a module importing only wire
requires one thing after go mod tidy, and grpc is not in it.
They also asked whether producer delete reasons should mirror our
delete_source. No — the two are distinct axes:
delete_source says who authored the disappearance and
we write it; the reason says why the producer decided and they
write it. The enum stays open, but it gains a recommended core, because an
open enum without one is how every producer invents its own spelling.
Six of those values say the resource ended. One says something else, and it is the one that earns the section:
unmonitored — the observation ended; the resource may well still be running
A probe removed from a configuration, or a target that left the scope, says nothing about the resource. A database entity that disappears because somebody edited a probe list must not read as "the database is gone" — that is the reading that causes an incident. It is the producer-side mirror of our own liveness expiry: there we say I stopped hearing, here the producer says I stopped looking.
Get it
go install github.com/toise-dev/toise/cmd/toise-server@v0.14.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.14.0 docs and the
changelog.
Producers get the segment vocabulary with pkg/emit/v0.9.0.