mirror of
https://github.com/hanzoai/o11y.git
synced 2026-10-15 12:58:33 +00:00
main
12
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
4679327ddc |
Sentinel reads the errors /v1/event lands, and takes none itself
Hanzo CI/CD / test (linux-amd64) (push) Skipped
Hanzo CI/CD / test (linux-arm64) (push) Skipped
Hanzo CI/CD / test-advisory (push) Failing after 5m15s
Hanzo CI/CD / test-amd64 (push) Failing after 17m40s
Hanzo CI/CD / cicd (push) Skipped
CI/CD / cicd (push) Failing after 17m53s
The event plane folded every occurrence into event.fact, and the Sentry module still read and wrote event.error. Its reads now select the error signal of event.fact, mapping issue, env, origin and the severity band. Errors enter through /v1/event only. The Sentry envelope and store routes, DSN keys and their rotation leave the module; the consumer of event.error files each fact through Ingest(org, product), which keeps the issue lifecycle and returns the issues a batch created. |
||
|
|
f0ad9f2321 |
refactor: use, not mount — one verb for composition
cloud renamed its composition verb to zip own word, and this module publishes the other half of that seam. |
||
|
|
f4b78d6371 |
docs: name the thing instead of calling it a door
One word stood for several: the MCP server, one address that accepts requests, the single way in, and the boundary a credential is validated at. Doc comments here are lifted into OpenAPI descriptions and republished on docs.hanzo.ai, so the ambiguity was reaching customers. Each site now names its own sense — endpoint, entry point, the MCP server, the edge, address — or takes the word the surrounding code already uses. Comments and prose strings only. Identifiers keep their names, including the doc comments that must open with the identifier they document. go build ./... passes. |
||
|
|
c10b49bbe8 |
feat(o11y): nothing answers outside /v1/o11y — sentinel folds, /ws merges
Two addresses were still spelled at a root that is not this capability's, and HIP-0139 §3 gives every route one home: /v1/<name>. Both close by fold, and neither leaves an alias behind. /v1/sentinel/* -> /v1/o11y/sentinel/*. The error-tracking face is twelve concrete paths the runtime registers literally, plus the declaration table's group over the same root, so one constant and the registrations under it move together. It is one capability's second face, not a second capability: same store, same runtime, same tenant rule. The ingest door is unaffected — a minted DSN names /v1/event and always did. /ws/query_progress merges onto /v1/o11y/query_progress. It was never a second read: one handler answered both mount points, upgrading when the caller asked and long-polling when it did not. The Upgrade is a property of the request, not of the address, so two paths bought a second spelling and nothing else. /ws was also the last address this service answered outside /v1, and the console's api-prefix list drops it with the route. The hatch census goes eleven to ten and the surface 367 to 366 — the numbers are tests here, not prose, so they move in the same commit as the route. |
||
|
|
c42aa37d71 |
feat(sentinel): the ingest door is /v1/event, off the product face
One wire had two public addresses. The Sentry-wire hatch was registered under
sentinelRoot, so /v1/sentinel/{project}/envelope|store answered alongside the
real door — and a face is not a door. A face is read by a signed-in person and
answers about what is already stored; a door is knocked on by a keyed beacon
that holds no session and carries something in. Different callers, different
credentials, so they no longer share a root.
eventRoot ("/v1/event") is that door, and it is the SAME string mintDSN spells,
so the address a client is told about and the address this service opens are one
value. IngestWire matches on it, and still by METHOD + PREFIX + SUFFIX rather
than a bare prefix, so nothing else on either root is reachable through the
exemption — the faces stay gated, and so does /v1/event itself, the product
event door beside this one.
The /api/ segment stays where it is. A stock SDK appends its own fixed
/api/<project>/envelope/ to whatever DSN path it is given, so that spelling is
the protocol's and not ours to rename.
|
||
|
|
2e23268ee3 |
feat(sentinel): /v1/sentry becomes /v1/sentinel; the DSN names /v1/event
/v1/sentry named our product after somebody else's. The face is /v1/sentinel now — issues, projects, stats, logs, traces, discover: the fifteen addresses a signed-in person reads. One const carries it (relay.go's sentinelRoot), which is why the whole surface moved from one line; the groups in telemetry.go and sentryerrors.go derive from it and nothing restates a path. The WIRE is untouched and stays untouched. sentry_key, X-Sentry-Auth and application/x-sentry-envelope are a third party's protocol that we RECEIVE, not a name we chose, so renaming them would break every SDK in the field and buy nothing. Sentinel is what we call the product; the envelope is what arrives. mintDSN now addresses /v1/event. A DSN is the INGEST door and the face is not — @hanzo/event 0.3.29 derives its ingest URL from the DSN's own path, so this line decides where every reporting client posts. It said /v1/sentry, which after the rename would have pointed clients at a read surface. Ingest and face are different nouns and no longer share a root. zipdoc_gen.go's fifteen route keys were moved by hand, and that is not a licence to hand-edit generated files: `go generate -run zipdoc ./...` FAILS on this tree before any change of mine — claim.go:146's generic opGet wrapper makes the path non-constant, so zipdoc refuses the root package. Verified against a stashed pristine tree. The keys are literal strings and this is what a working generator would emit; the generator is the real repair and is owed. Every count the repo pins still holds: 353 typed ops, 353 published operations, 353 MCP tools, 367 routes. 155 packages green. BREAKING CHANGE: the error-tracking read surface moves from /v1/sentry to /v1/sentinel, and minted DSNs address /v1/event. |
||
|
|
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]>
|
||
|
|
0411dad1e3 |
chore(zip): v1.24.1 — tests stop reaching through fiber (#44)
App.Test calls prepare, which installs the deferred projections — /mcp, the OpenAPI document, the op-call plane, the plugin route. Reaching through Fiber() skips that, so a test written against the escape hatch cannot see a surface production exposes, and the closer it gets to the real program the more confidently it reports the wrong thing. 12 call sites across 7 files. Full suite green on this host. The `unreachable code` vet notices in pkg/parser/filterquery/grammar are pre-existing and in generated parser output. Co-authored-by: zeekay <[email protected]> Co-authored-by: Hanzo Dev <[email protected]> |
||
|
|
55b7918b7f |
mount: the route table takes the router, and the binary links it
Mount(app *zip.App, deps cloud.Deps) fused two separate things — WHAT the 367
routes are, and WHICH host's dependencies serve them. It read exactly one field
of that second argument, deps.Logger, on exactly one line, to announce itself.
That one line cost three things, each worse than the last:
- github.com/hanzoai/o11y required github.com/hanzoai/cloud, which requires
github.com/hanzoai/o11y. A module CYCLE, for a log line.
- Because the cycle made a whole-module resolve impossible in an image with no
cloud checkout, the Dockerfile scoped itself to one package to route around
it — and wrote the workaround down as if it were a design decision.
- So cmd/community, the binary the image runs, could not import the route
declarations without dragging the embedding host in. It never imported them.
353 typed ops, 353 published operations and 353 MCP tools were true of the
source and absent from the process.
The last one is the P0 and it was invisible to every proof the conversion had:
the tests mount the table directly inside the test process, which is exactly the
thing that cannot fail. An uncalled package-level func is legal Go — the package
built, the route arithmetic added up, and the shipped server published nothing.
Status codes could not see it either. /version answers 200 either way, because
the service's own registration answers it. The tell was the body of an UNKNOWN
path: text/plain "404 page not found", which is pkg/web/noopweb's http.NotFound
behind the console's terminal catch-all.
A ROUTE TABLE IS A VALUE. It takes the router it registers into, and nothing
else. The router already carries the host's logger — app.Logger() IS the value
Deps.Logger was — so the second argument never carried information the first
did not already have.
Mount(app *zip.App) error. cloud is gone from go.mod, and ~20 transitive
requirements go with it (hanzoai/{iam,commerce,authz,tasks,vfs,orm,ha,s3-go,…}).
Standing proof, and it must stay this way:
go list -deps ./cmd/community | grep hanzoai/cloud # EMPTY
go list -deps ./cmd/community | grep -x github.com/hanzoai/o11y # PRESENT
The second line is the one that matters. pkg/query-service/app's new publish()
mounts the ONE table onto the router the server serves, then calls app.Prepare()
— which is what turns the registry into doors: /.well-known/openapi.json, /docs,
/mcp, the by-name call plane. This server serves through Fiber().Listener rather
than zip's own Listen, so without that call the document would exist in the
process and answer on no port.
Order is the contract, twice. AFTER the service's own routes: Fiber matches in
registration order, so every path is still answered by the handler that has
always answered it — no relay hop, no buffered round-trip, and therefore
livetail, the long-poll and the chunked export still stream on this listener. The
table NAMES the surface here; it does not stand in front of it. BEFORE the
console's terminal catch-all, or the SPA would answer for Prepare's routes.
Three tests read this out of the router instead of asserting it about the source:
the served document publishes 353 operations as application/json, every path in
it is o11y's own surface, and an earlier registration outranks the declaration.
hanzoai/cloud keeps working with a mechanical call-site change —
o11y.Mount(a, deps) -> o11y.Mount(a) at apps/o11y/o11y.go — and cmd/cloud builds
clean against this tree. That change lands in cloud with its next o11y pin bump;
cloud's current pin (v1.5.41) is unaffected.
Co-authored-by: Hanzo Dev <[email protected]>
|
||
|
|
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]>
|
||
|
|
ed6b7cad76 |
mount: three slices were DARK — 83 typed ops reached no router
sentryerrors.go (18 ops), rulesalerts.go (36) and integrations.go (29) landed
with the rest of the conversion, but nothing in Mount ever called them. An
uncalled package-level func is legal Go, so the package built, the whole suite
passed, and every one of those routes still fell through the /v1/o11y/* wildcard.
The conversion was true of the SOURCE and false of the BINARY — the exact
zero-value defect class this migration keeps producing: a green surface over a
mechanism that never ran.
Route-table arithmetic is what caught it, not the compiler and not a test:
Mount registered 250 routes against 321 zip.* registrations across the slice
files, and the missing 83 were exactly these three. After wiring: 331.
THE TESTS WERE ENCODING THE DEFECT. Five assertions passed BECAUSE the slices
were dark and flipped red the moment Mount was fixed:
- mount_test.go pinned /v1/o11y/errortracking/issues as wildcard-delegated
- telemetry_test.go sampled /v1/sentry/{projects,issues,issues/:id/events,
events/:id} as wildcarded and counted the whole /v1/sentry prefix as its own
- access_test.go sampled /v1/o11y/alerts as wildcarded
- apm_test.go's census counted cloud_integrations' /…/services as APM doors
Each is now scoped or removed, with the reason in place. The tell is general and
worth remembering: a wildcard-delegation assertion that PASSES for a route
someone converted is proof the conversion did not reach the router.
This also dissolves the 'cloud_integrations is an unclaimed family' finding —
those eleven paths were never unclaimed. They were in the dark integrations
slice, which is why they read as runtime-only when the surface was measured from
the outside.
Full suite green; the only wildcard sample left is the Sentry SDK envelope wire,
which is a genuine hatch.
Co-authored-by: Hanzo Dev <[email protected]>
|
||
|
|
c4acee269a |
telemetry: the five error-read routes answer as typed ops
discover, logs, traces, traces/{id} and stats reached traffic only through the
delegation wildcard, so they were in no document: no SDK method, no command, no
agent tool, no reference page for a customer's own error telemetry. They are
typed ops now — real In, real Out, and the prose zipdoc lifts out of the handler
doc comments — which is the only construct that reaches all of those projections.
OURS, all five: pkg/apiserver/o11yapiserver/sentry.go, pkg/modules/sentry and
pkg/types/sentrytypes were added whole by
|