Files
zeekay 4679327ddc
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
Sentinel reads the errors /v1/event lands, and takes none itself
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.
2026-09-30 01:15:34 -07:00

188 lines
11 KiB
Go

package o11y
import (
"net/http"
"github.com/zap-proto/zip"
)
// Mount registers Hanzo o11y's HTTP surface under /v1/o11y per HIP-0106.
//
// A ROUTE TABLE IS A VALUE, AND IT TAKES THE ROUTER AND NOTHING ELSE. Mount used
// to take a second argument — the embedding host's dependency struct,
// hanzoai/cloud's Deps — and it read exactly ONE field of it, Logger, on exactly
// one line, to announce itself. That one line braided WHAT the routes are into
// WHICH host serves them, and the price was paid three levels up:
//
// - 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 `go mod download` unresolvable in an image that has
// no cloud checkout, the community Dockerfile had to build one package
// instead of the module, and that package — cmd/community — could not import
// these declarations without dragging the host in. So it did not import them
// at all, and the whole conversion (353 typed ops, 353 published operations,
// 353 MCP tools) shipped in a package the running process never linked.
//
// The router already carries a logger: the host configured it with its own when
// it built the app, so app.Logger() is the SAME value Deps.Logger was, reached
// through the argument that was already here. Nothing was carried by the second
// argument that the first did not already have.
//
// With it gone the declarations stand alone, and BOTH hosts mount this ONE
// table: the community server's own public router (pkg/query-service/app, which
// is also the handler the unified binary embeds) and hanzoai/cloud's outer
// router. One declaration, two hosts, nothing to drift.
//
// The o11y runtime (metrics, traces, logs, dashboards, alerts) is heavy:
// telemetry stores, rule manager, websocket attachments, opamp server. These
// routes do not re-implement it — each hands its call to the runtime's handler
// FOR ITS OWN ADDRESS (see [SetRuntime] and relay), and until a runtime is
// installed they 503 with a clear error rather than pretending.
//
// EVERY ROUTE IS NAMED. There is no /v1/o11y/* catch-all any more. The runtime
// registers 329 method+path pairs; 322 of them are typed ops declared in the
// slice files below, and the remaining 7 are registered one by one in
// mountHatches with the reason each cannot be typed written next to it. A
// catch-all hides the difference between "converted" and "not converted" — the
// dark-slice defect this package already shipped once was invisible precisely
// because a wildcard answered for the routes nobody had wired up. A named route
// per hatch makes the escape hatches countable, and makes any NEW un-typed route
// a 404 that someone has to come and justify rather than a silent fall-through.
//
// PATH UNTOUCHED. Delegation never rewrites r.URL: every route is registered at
// its full public path, so the route literal IS the contract — one spelling,
// nothing to translate, nothing to drift.
// One address, one declaration. Every route is named, so a host that serves one
// of these addresses itself gets a Build conflict from zip rather than a second
// handler standing behind the first.
func Use(app *zip.App) error {
app.Logger().Info("o11y: mounting routes", "prefix", o11yRoot)
// Native probe group, registered ahead of everything else so Fiber's
// in-order match serves it off the mux tree (see health.go).
mountHealth(app)
// The TYPED telemetry ops of the error-tracking face — the five reads that
// now carry a named input, a named output and their prose into the document
// instead of hiding behind a wildcard. They answer on the runtime, so the
// wire is unchanged; see telemetry.go.
mountTelemetry(app)
// The TYPED infra ops — hosts, processes, the Kubernetes fleet and the
// infra_monitoring rollups; forty-four reads that now carry named inputs,
// named outputs and their prose into the document. They answer on the
// runtime, so the wire is unchanged; see infra.go.
mountInfra(app)
// The TYPED logs ops — the record read, the field catalog and its tuning
// write, the aggregate read, pipelines and path promotion; live tail stays
// a hatch because it is a stream. Same construction: they answer on the
// runtime, so the wire is unchanged; see logs.go.
mountLogs(app)
// The TYPED identity ops — who the caller is, my-org, quick filters and
// preferences. Same construction: named In, named Out, answered on the
// runtime, wire unchanged; see identity.go. Invites, passwords, reset
// tokens, member administration, sessions, SSO domains and the /complete/*
// sign-in callbacks are GONE, not hatched: Hanzo IAM owns credentials.
mountIdentity(app)
// The TYPED access-control ops — roles, service accounts, service-account
// keys and the authorization probe. Same construction: named In, named Out,
// answered on the runtime, so the wire — including every CheckResources and
// OpenAccess gate — is unchanged; see access.go.
mountAccess(app)
// The TYPED APM ops — the service catalog, the messaging-queue views and
// the third-party API overview; twenty-one reads that now carry named
// inputs, named outputs and their prose into the document. Same
// construction: named In, named Out, answered on the runtime, wire
// unchanged; see apm.go.
mountAPM(app)
// The TYPED dashboard ops — v2 dashboard CRUD, cloning, locking, per-user
// pinning, the org-shared saved views, public-sharing config and the two
// anonymous public reads; twenty-two routes that now carry named inputs,
// named outputs and their prose into the document. Same construction: they
// answer on the runtime, so the wire is unchanged; see dashboards.go.
mountDashboards(app)
// The TYPED LLM-observability ops — the four gen_ai span views
// (observations, traces, sessions, users), eval scores, human annotations
// and the LLM pricing rules; fourteen reads and writes that now carry named
// inputs, named outputs and their prose into the document. Same
// construction: they answer on the runtime, so the wire is unchanged; see
// llmobs.go.
mountLLMObs(app)
// The TYPED metrics ops — the metrics-explorer reads and the metadata write,
// and the volume-control (metric reduction) rules; nineteen ops that now
// carry named inputs, named outputs and their prose into the document. They
// answer on the runtime, so the wire is unchanged; see metrics.go.
mountMetrics(app)
// The TYPED platform ops — instant queries, dashboard variables, product
// events, usage, the dependency graph, version/health, disks, first-user
// registration, retention and apdex settings, licenses, global config,
// feature flags, org stats, filter suggestions, k8s onboarding, metric
// metadata, span percentiles and query-filter analysis. Same construction:
// they answer on the runtime through the one shared relay seam, so the wire
// is unchanged; see platform.go. The three service probes (healthz, readyz,
// livez) stay on mountHealth above — its fall-through-when-unset is
// load-bearing.
mountPlatform(app)
// The TYPED query-core ops — the v5 composite query engine (query_range, its
// dry-run preview, variable substitution), the legacy metrics range read and
// the builder-format echo, attribute autocomplete, the field catalog and the
// saved explorer views. Same construction: named In, named Out, answered on
// the runtime, wire unchanged; see querycore.go. Query progress (long-poll
// and websocket) and raw-data export stay hatches — they are streams, not
// JSON answers.
mountQueryCore(app)
// The last three slices of the first conversion pass. They were DARK: their
// files landed with the rest of the conversion, but nothing here called them,
// so 83 typed ops existed in the source and reached no router at all. An
// uncalled package-level func is legal Go, so the package built and the whole
// suite passed while every one of those routes still fell through the
// wildcard — the conversion was true of the source and false of the binary.
// Route-table arithmetic is what caught it, and it is why the wildcard is
// gone: with every route named, a slice that is not mounted 404s loudly
// instead of being quietly answered by a catch-all.
mountSentryErrors(app)
mountRulesAlerts(app)
mountIntegrations(app)
// The TYPED traces ops — one trace's spans, the trace field catalog and its
// tuning write, and the three trace-detail views (waterfall, flamegraph,
// span aggregations); see traces.go.
mountTraces(app)
// The TYPED trace-funnel ops — funnel CRUD plus the twelve analytics reads,
// six over a saved funnel and six over one described inline; see
// tracefunnels.go.
mountTraceFunnels(app)
// The TYPED span-mapper ops — the ingest-time rules that move or copy span
// attributes into resource attributes; see spanmappers.go.
mountSpanMappers(app)
// And the three that cannot be typed, each named and justified.
mountHatches(app)
return nil
}
// mountHatches registers the THREE routes that cannot be typed ops, one route
// literal each, with the reason next to it. This list is meant to shrink and is
// meant to be hard to grow: adding to it costs a justification in review, where
// a catch-all cost nothing.
//
// A typed op declares an input type, an output type and a success status, and
// zip projects that declaration into the OpenAPI document, the MCP tool, the CLI
// command and the by-name call plane. Every route here breaks that declaration
// in a way that would make the document LIE, and lying in the document is worse
// than being absent from it — a generated client that trusts a false contract
// fails at the customer, not at review.
//
// The stream list was three, and the third is gone rather than moved: /ws/query_progress was
// the same read as o11yRoot+"/query_progress" over an Upgrade, and one read at
// two addresses is one address too many. The Upgrade is a property of the
// request, so both protocols answer at the /v1/o11y address now.
func mountHatches(app *zip.App) {
// ── 1. STREAMS: there is no one answer to name ───────────────────────────
// These never produce a single complete JSON value. relay buffers a whole
// answer through an httptest recorder before decoding it, so a typed
// livetail would hang on the first tail, and a typed progress poll would
// return only after the query it reports on had already finished — which is
// the one thing a progress endpoint must not do.
route(app, http.MethodGet, o11yRoot+"/logs/livetail") // unbounded stream of log records
route(app, http.MethodGet, o11yRoot+"/query_progress") // progress: a long poll, or a websocket when the caller upgrades
route(app, http.MethodPost, o11yRoot+"/export_raw_data") // chunked CSV/JSONL attachment, X-Response-Complete trailer
}