mirror of
https://github.com/hanzoai/o11y.git
synced 2026-10-15 12:58:33 +00:00
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]>
175 lines
6.4 KiB
Go
175 lines
6.4 KiB
Go
package o11y_test
|
|
|
|
import (
|
|
"net/http"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/hanzoai/o11y/pkg/types"
|
|
"github.com/hanzoai/o11y/pkg/types/spantypes"
|
|
"github.com/hanzoai/o11y/pkg/valuer"
|
|
)
|
|
|
|
// THE WIRE PROOF for the span-mapper face. Same harness as every other slice.
|
|
// The four writes answer 204 with no body, so those cases assert the STATUS the
|
|
// op declared as well as the path it reached — a declared status that does not
|
|
// match the wire is the one lie a generated client cannot recover from.
|
|
|
|
func fullGroup() *spantypes.SpanMapperGroup {
|
|
at := time.Date(2026, 7, 31, 12, 0, 0, 0, time.UTC)
|
|
return &spantypes.SpanMapperGroup{
|
|
ID: valuer.MustNewUUID("6ba7b810-9dad-11d1-80b4-00c04fd430c8"),
|
|
OrgID: valuer.MustNewUUID("6ba7b811-9dad-11d1-80b4-00c04fd430c8"),
|
|
Name: "promote-k8s",
|
|
Condition: spantypes.SpanMapperGroupCondition{
|
|
Attributes: []string{"k8s.pod.name"},
|
|
Resource: []string{"service.name"},
|
|
},
|
|
Enabled: true,
|
|
TimeAuditable: types.TimeAuditable{CreatedAt: at, UpdatedAt: at},
|
|
UserAuditable: types.UserAuditable{CreatedBy: "z", UpdatedBy: "z"},
|
|
}
|
|
}
|
|
|
|
func fullMapper() *spantypes.SpanMapper {
|
|
at := time.Date(2026, 7, 31, 12, 0, 0, 0, time.UTC)
|
|
return &spantypes.SpanMapper{
|
|
ID: valuer.MustNewUUID("6ba7b812-9dad-11d1-80b4-00c04fd430c8"),
|
|
GroupID: valuer.MustNewUUID("6ba7b810-9dad-11d1-80b4-00c04fd430c8"),
|
|
Name: "pod-name",
|
|
FieldContext: spantypes.FieldContextSpanAttribute,
|
|
Enabled: true,
|
|
TimeAuditable: types.TimeAuditable{CreatedAt: at, UpdatedAt: at},
|
|
UserAuditable: types.UserAuditable{CreatedBy: "z", UpdatedBy: "z"},
|
|
}
|
|
}
|
|
|
|
func TestSpanMapperReadsAnswerTheRuntimeAnswer(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name, path string
|
|
payload any
|
|
}{
|
|
{"groups", "/v1/o11y/span_mapper_groups",
|
|
spantypes.NewGettableSpanMapperGroups([]*spantypes.SpanMapperGroup{fullGroup()})},
|
|
{"mappers", "/v1/o11y/span_mapper_groups/g-1/span_mappers",
|
|
spantypes.NewGettableSpanMappers([]*spantypes.SpanMapper{fullMapper()})},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
app := mounted(t)
|
|
want := rendered(t, http.StatusOK, tc.payload)
|
|
asked := logsRuntime(t, http.StatusOK, want)
|
|
|
|
status, got := call(t, app, member(http.MethodGet, tc.path, nil))
|
|
if status != http.StatusOK {
|
|
t.Fatalf("status=%d body=%s", status, got)
|
|
}
|
|
if string(got) != string(want) {
|
|
t.Fatalf("op answered %s, runtime wrote %s", got, want)
|
|
}
|
|
if r := *asked; r.Method != http.MethodGet || r.URL.Path != tc.path {
|
|
t.Fatalf("runtime asked %s %s", r.Method, r.URL.Path)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// The enabled filter reaches the runtime as a query parameter, and an UNSET
|
|
// filter stays absent rather than arriving as "false" — the runtime has always
|
|
// read a missing parameter as "all groups", so sending false would narrow a
|
|
// listing the caller did not narrow.
|
|
func TestSpanMapperGroupFilterStaysAbsentWhenUnset(t *testing.T) {
|
|
payload := spantypes.NewGettableSpanMapperGroups([]*spantypes.SpanMapperGroup{fullGroup()})
|
|
|
|
for _, tc := range []struct{ target, want string }{
|
|
{"/v1/o11y/span_mapper_groups", ""},
|
|
{"/v1/o11y/span_mapper_groups?enabled=true", "enabled=true"},
|
|
} {
|
|
t.Run(tc.target, func(t *testing.T) {
|
|
app := mounted(t)
|
|
asked := logsRuntime(t, http.StatusOK, rendered(t, http.StatusOK, payload))
|
|
call(t, app, member(http.MethodGet, tc.target, nil))
|
|
if got := (*asked).URL.RawQuery; got != tc.want {
|
|
t.Fatalf("runtime saw query %q, want %q", got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSpanMapperCreatesAnswer201(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name, path string
|
|
payload any
|
|
body string
|
|
}{
|
|
{"group", "/v1/o11y/span_mapper_groups", fullGroup(),
|
|
`{"name":"promote-k8s","condition":{"attributes":["k8s.pod.name"],"resource":["service.name"]},"enabled":true}`},
|
|
{"mapper", "/v1/o11y/span_mapper_groups/g-1/span_mappers", fullMapper(),
|
|
`{"name":"pod-name","fieldContext":"span","config":{},"enabled":true}`},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
app := mounted(t)
|
|
want := rendered(t, http.StatusCreated, tc.payload)
|
|
asked := logsRuntime(t, http.StatusCreated, want)
|
|
|
|
status, got := call(t, app, member(http.MethodPost, tc.path, strings.NewReader(tc.body)))
|
|
if status != http.StatusCreated {
|
|
t.Fatalf("status=%d want 201, body=%s", status, got)
|
|
}
|
|
if string(got) != string(want) {
|
|
t.Fatalf("op answered %s, runtime wrote %s", got, want)
|
|
}
|
|
if r := *asked; r.Method != http.MethodPost || r.URL.Path != tc.path {
|
|
t.Fatalf("runtime asked %s %s", r.Method, r.URL.Path)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// The four writes answer 204. Their ops declare it, and each must reach its own
|
|
// path with the two ids on VERBATIM.
|
|
func TestSpanMapperWritesAnswer204(t *testing.T) {
|
|
for _, tc := range []struct{ method, path, body string }{
|
|
{http.MethodPatch, "/v1/o11y/span_mapper_groups/g.1", `{"enabled":false}`},
|
|
{http.MethodDelete, "/v1/o11y/span_mapper_groups/g.1", ""},
|
|
{http.MethodPatch, "/v1/o11y/span_mapper_groups/g.1/span_mappers/m.1", `{"enabled":false}`},
|
|
{http.MethodDelete, "/v1/o11y/span_mapper_groups/g.1/span_mappers/m.1", ""},
|
|
} {
|
|
t.Run(tc.method+" "+tc.path, func(t *testing.T) {
|
|
app := mounted(t)
|
|
asked := logsRuntime(t, http.StatusNoContent, nil)
|
|
|
|
var req *http.Request
|
|
if tc.body == "" {
|
|
req = member(tc.method, tc.path, nil)
|
|
} else {
|
|
req = member(tc.method, tc.path, strings.NewReader(tc.body))
|
|
}
|
|
status, got := call(t, app, req)
|
|
if status != http.StatusNoContent {
|
|
t.Fatalf("status=%d want 204, body=%s", status, got)
|
|
}
|
|
if r := *asked; r.Method != tc.method || r.URL.Path != tc.path {
|
|
t.Fatalf("runtime asked %s %s", r.Method, r.URL.Path)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSpanMapperRoutesAreTheSameEight(t *testing.T) {
|
|
want := map[string]bool{
|
|
"GET /v1/o11y/span_mapper_groups": true,
|
|
"POST /v1/o11y/span_mapper_groups": true,
|
|
"PATCH /v1/o11y/span_mapper_groups/:groupId": true,
|
|
"DELETE /v1/o11y/span_mapper_groups/:groupId": true,
|
|
"GET /v1/o11y/span_mapper_groups/:groupId/span_mappers": true,
|
|
"POST /v1/o11y/span_mapper_groups/:groupId/span_mappers": true,
|
|
"PATCH /v1/o11y/span_mapper_groups/:groupId/span_mappers/:mapperId": true,
|
|
"DELETE /v1/o11y/span_mapper_groups/:groupId/span_mappers/:mapperId": true,
|
|
}
|
|
if len(want) != 8 {
|
|
t.Fatalf("the census itself is wrong: %d", len(want))
|
|
}
|
|
assertRoutes(t, want, "/v1/o11y/span_mapper_groups")
|
|
}
|