mirror of
https://github.com/hanzoai/o11y.git
synced 2026-10-15 12:58:33 +00:00
main
6
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
cae99adb44 |
The relay reads the operation zip served
An op learns its address from zip.OpOf and zip.AddressOf, so the claim table, the five declaring verbs and the address bookkeeping are gone. A host that declares one of these addresses itself now gets a Build conflict from zip. Co-authored-by: Hanzo Dev <[email protected]> |
||
|
|
2965c80547 |
An address is measured from a root this package owns
The five claim verbs need the address an operation will answer at, which is a root joined with a path. They used to ask the target for its prefix through a struct written to answer with one — and that struct existed for a second reason that has since gone: an operation with no declared id took a shape-derived one qualified by the prefix of the occurrence it answered under, so a plain group would have renamed every undeclared id. All 319 operations state their own id now, so a declared id survives composition verbatim and the group is just a group. The roots are this package's own constants, so each group carries the one it was made with and nothing can disagree about the address. The escape hatches name their whole address and register through Raw, which says which method each one answers. The test that measured the gap reached zero and said so; it now holds the property from the other side, that no operation goes undeclared. Co-authored-by: Hanzo Dev <[email protected]> |
||
|
|
599a26cedf |
the runtime answers by name, so the declaration is the only registration
o11y's surface was stated twice: once as the implementation (367 routes through
routing.Router) and once as the module's own declaration of it (353 typed ops +
11 hatches + 3 probes, the same 367 addresses with an input, an output and their
prose). The declaration had to REACH the implementation, and the only way it
could was to speak HTTP to the WHOLE service — build a request, hand it to one
http.Handler that was the entire router, and let that router match the path a
second time to find the handler the declaration had already named. An in-process
round trip, across a net/http<->fasthttp bridge, through a recorder, to answer a
question the caller had the answer to.
A registration is a map from address to handler. routing.Table.Handler reads it
as one and o11y.SetRuntime hands the whole table over, so the second match is
gone and with it the failure class the reachability census exists for: a request
cannot lose its route to a router that is never consulted.
Two things could not be said before and now can:
- "this address is not implemented." A declared address the runtime does not
serve came back as the runtime's own 404, indistinguishable from a caller's
typo. It is a nil at the seam now. The two sides HAD drifted: three addresses
the declaration names {traceId} were registered {traceID}, invisible because
a router matches by POSITION and a segment's name never reaches it.
- "the runtime is right here." The standalone server IS the runtime and could
not install itself as that one handler without an op relaying into the router
already serving it, so it installed nothing — and every op on its /mcp
surface and its by-name call plane answered 503 in the binary holding all 367
handlers. It installs its own table now.
The address is stated ONCE, at the registration, and rides the call: claim.go's
verbs wrap every op in addressed, which puts the address into the context, and
relay reads it there. relay takes no method and no path — which door was knocked
on is not something an op's body decides. That retires 706 spellings of 367
addresses; the 119 hand-built concatenations are rendered by zip.Address from the
same input zip bound the segments into, so the round trip is exact by
construction rather than by eye.
Reached BY NAME there is no router in front of the handler, so the two chain
members that need the route — Audit keys its record on the template, Resource
resolves the declared resources — would read an empty one, silently. Table.Handler
hands back the chain carrying the template it was registered at, written through
coretypes.SetRoute: the same representation bind writes for a matched request,
one read on the other side. TestByNameCarriesTheSameRouteAsMatching drives both
doors against one registration and compares them to each other.
Addresses did not move: the census — 367 fiber routes, 367 declared, 353
operation ids — is byte-identical to the parent. 155 packages ok, 0 fail, the
same set by name as the parent.
wantQueryServiceRoutes closes the equality argument the sibling address test
makes. It rested on four facts and 134 was a sentence, so a route deleted in
pkg/query-service/app left both address tests green while the declaration went on
naming an address nothing serves — which costs more now that it is a 503 rather
than a 404.
Co-authored-by: Hanzo Dev <[email protected]>
|
||
|
|
9119c4b747 |
A host may take an address, and now it can say so
Every route in this table is named. That is what made the table honest, and it is also what turned a host's overlapping route from a silent shadow into a refusal to compose: zip validates that one address is declared once, so a host declaring GET /v1/o11y/metrics alongside this table does not win, it panics. hanzoai/cloud declares three of these addresses natively — GET /v1/o11y/logs, GET /v1/o11y/metrics, POST /v1/o11y/query_range — and used to win them by registering first, back when a wildcard answered everything else. The wildcard is gone, so ordering decides nothing and the overlap is a boot failure. In-order shadowing was never a decision, only an artefact of registration order, and the two handlers it hid could drift apart forever without anyone being told. Claimed makes the host NAME what it takes, which is the same fact written down where it can be read. WHY A VERB PER DECLARATION, NOT A FILTER ON THE TARGET. The check needs the ADDRESS, and an address is the target's prefix plus the path. zip resolves an OpTarget once, without a path — OpScope carries no route — so a target cannot refuse one address and accept another. The declaration can, so the five verbs each gained the check and all 353 typed ops plus the eleven hatches go through them. Gating only the three addresses that collide today would have been a seam that was true where someone remembered it, which is the shape of defect this package already shipped once. A claim names a method AND a path, spelled the way zip spells an address in its own diagnostics — "GET /v1/o11y/metrics" — so the string that names the conflict is the string that resolves it. A claim on an address this table does not declare is inert rather than an error: the host is describing what IT serves, and a table that stopped declaring an address must not become a boot failure in every host that had claimed it. Nothing about the runtime changes. A claimed operation is still there and still reachable on the runtime's own router; this table stops PUBLISHING it, so the host's handler is the one answer at that address in the host's process. Tested: an unclaimed Mount declares everything (the control, so the absences are attributable to the claim); a claimed address is not declared while its siblings, its other methods and the rest of the table are; a claimed hatch goes too, since the eleven took a different verb; an unknown claim is inert; and a claim does not leak into the next Mount in the same process. The full root suite, including the 353-op census, is green. Co-authored-by: Hanzo Dev <[email protected]> |
||
|
|
bf6e17e0af |
chore(zip): v1.23.0 — Use is the one composition verb
Four changes zip v1.23 forced. Only the first is a compile error; the other
three are silent, and two of them would have shipped a broken service.
1. Prepare() -> Build() error. publish already returned an error, so the verdict
propagates: a composition this server cannot serve now fails at startup
naming both claimants, where before it could only be found by the routes
going missing.
2. routing.Router read its prefix back off zip (OpScope().Prefix). A group is
now an App INCLUDED at a prefix rather than a router carrying one, so
OpScope().Prefix is empty for every group — and reading it STILL COMPILES.
The Table would have silently recorded every nested route at its leaf path.
The prefix is composed at Group now, by the same join Handle records with.
3. THE OPERATION IDS. zip qualifies an op's id by the prefix of the occurrence
it answers under, so every id declared under app.Group(o11yRoot) became
"v1.o11y.CreateRole". That is 353 renamed MCP tools, OpenAPI operationIds,
CLI commands and generated SDK methods — in a patch release, with no opt-out
(an explicit WithOperationID is qualified too). The rule is right for a
composition; o11yRoot is not one. It is this service's own address, and an
occurrence at the ROOT prefix is unqualified. So the ops register on the app
with OpScope.Prefix doing what it documents — prepend to the op's path. Same
method, same full path, same middleware, and the id the declaration wrote.
One type (relay.go `under`), 18 call sites, no route literal touched. It is
also what mount.go's PATH UNTOUCHED already claimed and what mountHealth
already did by hand.
4. ONE ADDRESS, ONE DEFINITION — and the standalone server declared 367 twice.
The service's own handlers SERVE those addresses and the published table
DESCRIBES them; zip has no way to spell that difference ("the route and the
op are the same fact recorded once"), so Build refused the program and the
community server could not boot. The two facts now live on two apps: the
declaration builds on an app of its own and only its CONTROL routes — the
document, /docs, /mcp, the call plane, the plugin declaration — are served
from the listener. zip keeps control routes off the walk, and none of the
five is one of this service's 367, so nothing can shadow or be shadowed.
This also makes ORDER IS THE CONTRACT structural instead of positional: the
declaration is not on the listener at all, so there is no order to get wrong,
and livetail, the long-poll and the chunked export still stream unbuffered.
TestPublishServesTheDocument now reads the tool count from a tools/list over
the served door rather than len(app.MCPTools()) — an in-process accessor on the
app the test just built is the shape of proof that file exists to reject.
Measured: go build ./... clean; go vet ./... clean apart from pre-existing
"unreachable code" in the generated parsers; go test ./... 155 ok, 0 failed.
|
||
|
|
2d4b46d8ee |
mount: the wildcard is gone — every one of 367 routes is named
The delegation catch-all made three facts indistinguishable: a route that was
converted, a route that was not, and a route that did not exist. That is not a
theoretical cost — it is exactly how 83 typed ops shipped in files nothing
called, passing a full green suite while every one of those routes fell through
the wildcard.
So: derive the gap precisely, close it, and delete the catch-all.
THE GAP, re-derived. Extract every path the runtime registers (134 in
pkg/query-service/app/routes_*.go, 233 in pkg/apiserver/o11yapiserver/*.go — the
134 matching the count routes.go documents), normalise :params on both sides,
and diff against what Mount installs. 367 runtime routes, 43 unnamed. Not the
four hatch classes the plan expected: eleven were genuine hatches and
THIRTY-TWO were simply never converted.
CONVERTED (32 new typed ops, three new slices):
* traces.go — a trace's spans, the trace field catalog and its tuning
write, waterfall, flamegraph, span aggregations
* tracefunnels.go — funnel CRUD plus twelve analytics reads, six over a saved
funnel and six over one described inline
* spanmappers.go — the ingest-time attribute mapping groups and their mappers
THE ELEVEN HATCHES, each its own route literal with its reason beside it
(mount.go mountHatches): four streams with no single answer to name (livetail,
query_progress long-poll, its websocket, the chunked export), three 303
redirects whose answer is a Location and not a body, and four Sentry-SDK ingest
doors — a foreign protocol we RECEIVE, whose /api/ segment the SDK hardcodes and
which is not ours to rename.
TWO OF THOSE ELEVEN WERE UNREACHABLE. /ws/query_progress and the two
/v1/sentry/<project>/{envelope,store}/ doors sit outside /v1/o11y, so the old
catch-all never saw them: a Sentry SDK pointed at the clean root got a 404 in
the composed binary, and telemetry_test.go passed only because the test
registered the wildcard the deployment did not have. Naming every route is what
surfaced it.
ONE SEAM, TWO ROOTS. Five copies of the relay function (relay, relayAt,
relayO11y, relayAPM, send) differing only in a prefix and whether they took
query parameters, and NINE names for two values — identityPrefix, infraPrefix,
integrationsPrefix, metricsRoot, apmPrefix, accessPrefix, rulesAlertsPrefix and
o11yRoot were all "/v1/o11y". Ops now pass the whole public path and relay
concatenates nothing: 193 call sites, one implementation, -520 lines.
Also: the per-slice *_zipdoc.go files fold back into zipdoc_gen.go as their own
headers instructed; the health probes' fall-through names its destination
instead of relying on a catch-all being registered after it (they would have
started 404ing the moment SetHealth was late — precisely when liveness matters);
O11yLogField/O11yLogFieldsOut/O11yLogFieldSetting become
O11yTelemetryField/O11yFieldCatalogOut/O11yFieldSetting because logs and traces
tune ONE catalog through one runtime type.
PAYOFF, counted off the document rather than the router: 353 operations in the
published OpenAPI spec and 353 MCP tools, from 0 for this surface before the
conversion began. 353 typed + 11 hatches + 3 probes = 367.
RECORDED, NOT FIXED: the runtime renders with jsoniter and zip with the stdlib
encoder, and jsoniter does not implement `,omitzero` — so for the seventeen
fields across pkg/types that use it the runtime writes the zero value where a
typed op omits the key. Pre-existing on every shipped op reaching
TelemetryFieldKey. Pinned by traces_test.go rather than hidden.
gorilla/mux and gin stay: mux is the runtime's own router in 100 files, and gin
is imported only for its form binder by pkg/valuer/valuer.go
(ginbinding.BindUnmarshaler) and pkg/http/binding/query.go
(ginbinding.MapFormWithTag). go mod tidy dropped 687 lines of go.mod/go.sum.
Build and full suite green: 133 packages ok, 0 fail.
Co-authored-by: Hanzo Dev <[email protected]>
|