Merge pull request #387 from hanzo-inc/fix/o11y-fleet-telemetry

o11y: the product scope is the fleet, and a sub-cent debit is not an outage
This commit is contained in:
z
2026-08-06 01:23:56 -07:00
committed by GitHub
7 changed files with 622 additions and 12 deletions
+28 -3
View File
@@ -23,6 +23,7 @@ import (
"github.com/hanzoai/cloud"
"github.com/hanzoai/cloud/apps/finance"
"github.com/hanzoai/cloud/money"
"github.com/hanzoai/cloud/plane"
)
@@ -66,11 +67,35 @@ func coResidentUsage(ctx context.Context, org, product, groupBy string) ([]byte,
}
rows := make([]finance.UsageRow, 0, len(reply.Rows))
for _, r := range reply.Rows {
cents, cerr := r.Amount.Minor()
// Round DOWN, explicitly — the same choice balance.go:126 already made,
// for the same reason, on a row read from the same ledger.
//
// Minor() REFUSES anything finer than a cent rather than round behind
// the caller. A per-token AI charge is routinely finer than a cent, so
// on this path the refusal was not an edge case: ONE sub-cent row in the
// page failed the whole read, and the caller (billing.go:458) tests err
// before coResident, so it answered 502 "billing upstream unreachable"
// with nothing upstream involved. That is the 2026-08-03 balance bug
// verbatim; balance and ai were converted then, this row was missed.
// Measured 2026-08-06: /v1/billing/balance 200, /v1/billing/usage 502,
// same org, same ledger, same process — only this call differed.
cents, cerr := r.Amount.FloorMinor()
if cerr != nil {
return nil, false, cerr
// The peer ANSWERED and the reply did not parse — a real failure,
// not an absent ledger. Report it as handled so it surfaces here
// rather than falling through to a commerce proxy that is not
// configured on this deployment and would mask it as a 501.
return nil, true, fmt.Errorf("usage: commerce ledger row %s: %w", r.ID, cerr)
}
rows = append(rows, finance.UsageRow{ID: r.ID, Model: r.Model, Cents: cents, CreatedAt: r.CreatedAt})
// Carry the EXACT debit, not just its rounding. usageEnvelope emits it as
// `decimal`, and that field is the whole reason a page of sub-cent calls
// totals correctly instead of totalling zero — dropping Amount here would
// have traded the 502 for a silently understated bill.
exact, perr := money.ParseUSD(r.Amount.Decimal)
if perr != nil {
return nil, true, fmt.Errorf("usage: commerce ledger row %s amount %q: %w", r.ID, r.Amount.Decimal, perr)
}
rows = append(rows, finance.UsageRow{ID: r.ID, Model: r.Model, Cents: cents, Amount: exact, CreatedAt: r.CreatedAt})
}
env := usageEnvelope(org, rows)
if out, ok := enrichUsageLedger(env, product, groupBy); ok {
+140
View File
@@ -0,0 +1,140 @@
package billing
// usage_subcent_peer_test.go — a sub-cent debit on the PEER branch must not fail
// the read.
//
// The existing sub-cent test (usage_coresident_test.go) publishes a fake finance
// and exercises the LOCAL branch, where cents come from finance.UsageRow.Cents and
// nothing can error. Production runs the other branch: plugin/billing links no
// ledger, so finance.Current() is nil and the rows arrive over the plane from
// commerce as plane.Money. That branch called Money.Minor(), which REFUSES an
// amount finer than a cent instead of rounding behind the caller — and a
// per-token AI charge is routinely finer than a cent.
//
// One such row failed the whole page, and usage() (billing.go:458) tests err
// before coResident, so the customer got 502 "billing upstream unreachable" with
// nothing upstream involved. That is the 2026-08-03 balance bug exactly;
// balance.go and apps/ai were converted to FloorMinor then, this row was missed.
//
// Measured on the live fleet 2026-08-06, same org, same ledger, same process:
// GET /v1/billing/balance -> 200 {"balance":14989388,...} (FloorMinor)
// GET /v1/billing/usage -> 502 "billing upstream unreachable" (Minor)
// Only the call differed, which is what makes this a code fix and not config.
import (
"context"
"encoding/json"
"net"
"strings"
"testing"
"time"
"github.com/hanzoai/cloud/apps/finance"
"github.com/hanzoai/cloud/plane"
"github.com/zap-proto/zip"
)
// subCentCommerce serves the commerce plane socket and answers the usage op with a
// page whose debits are finer than a cent — the shape a real AI ledger holds.
func subCentCommerce(t *testing.T, rows []plane.UsageRow) {
t.Helper()
app := zip.New(zip.Config{AppName: "commerce"})
zip.Post[struct{}, plane.UsageRows](app, "/finance/usage",
func(context.Context, *struct{}) (*plane.UsageRows, error) {
return &plane.UsageRows{Rows: rows}, nil
}, zip.WithOperationID(plane.FinanceUsage))
go func() { _ = app.Listen(zip.SocketPath("commerce")) }()
t.Cleanup(func() { _ = app.Shutdown() })
path := zip.SocketPath("commerce")
for i := 0; i < 400; i++ {
if c, err := net.DialTimeout("unix", path, time.Second); err == nil {
_ = c.Close()
return
}
time.Sleep(10 * time.Millisecond)
}
t.Fatalf("%s never began listening — this test would otherwise pass by never reaching the peer", path)
}
// TestCoResidentUsage_SubCentPeerRowIsNotAnOutage is the regression. A page of
// sub-cent debits must come back as a page, not as an error the caller renders
// as a dead upstream.
func TestCoResidentUsage_SubCentPeerRowIsNotAnOutage(t *testing.T) {
planeDir(t)
finance.Publish(nil) // no local ledger — force the peer branch, as in prod
t.Cleanup(func() { finance.Publish(nil) })
rows := []plane.UsageRow{
{ID: "tiny-1", Model: "zen-1", Amount: plane.Money{Decimal: "0.0025", Currency: "USD"}, CreatedAt: 1_700_000_000},
{ID: "tiny-2", Model: "zen-1", Amount: plane.Money{Decimal: "0.00007", Currency: "USD"}, CreatedAt: 1_700_000_100},
{ID: "whole", Model: "gpt-x", Amount: plane.Money{Decimal: "1.50", Currency: "USD"}, CreatedAt: 1_700_000_200},
}
subCentCommerce(t, rows)
body, coResident, err := coResidentUsage(context.Background(), "acme", "", "")
if err != nil {
t.Fatalf("a sub-cent debit failed the usage read: %v\n"+
"usage() turns this into 502 \"billing upstream unreachable\" with nothing upstream involved", err)
}
if !coResident {
t.Fatal("the peer answered but the read reported fall-back — usage() would hand this to an " +
"unconfigured commerce proxy and answer 501")
}
var env struct {
Count int `json:"count"`
Usage []struct {
TransactionID string `json:"transactionId"`
Amount int64 `json:"amount"`
Decimal string `json:"decimal"`
} `json:"usage"`
}
if uerr := json.Unmarshal(body, &env); uerr != nil {
t.Fatalf("envelope not valid JSON: %v\n%s", uerr, body)
}
// Guard the iteration source before asserting over it: an empty page would make
// every assertion below vacuously true, and an empty page is itself the bug
// (the customer reads a blank ledger) — so it must fail here, loudly.
if len(env.Usage) != len(rows) {
t.Fatalf("envelope carries %d rows, want %d — the peer's page did not survive the read; "+
"every assertion below would otherwise pass by examining nothing", len(env.Usage), len(rows))
}
byID := map[string]struct {
cents int64
decimal string
}{}
for _, u := range env.Usage {
byID[u.TransactionID] = struct {
cents int64
decimal string
}{u.Amount, u.Decimal}
}
// FloorMinor rounds DOWN: a sub-cent debit is 0 cents. That is correct and is
// precisely why `decimal` must carry the exact value beside it.
for _, id := range []string{"tiny-1", "tiny-2"} {
got, ok := byID[id]
if !ok {
t.Fatalf("row %q missing from the envelope", id)
}
if got.cents != 0 {
t.Errorf("%s: amount = %d cents, want 0 — the fixture must be sub-cent for this test to mean anything", id, got.cents)
}
if got.decimal == "" {
t.Errorf("%s: decimal is EMPTY — the exact debit was dropped on the peer branch, so a page of "+
"sub-cent calls totals ZERO and the customer is billed for work the statement cannot show", id)
}
}
if d := byID["tiny-1"].decimal; d != "" {
if !strings.Contains(d, "0.0025") {
t.Errorf("tiny-1: decimal = %q, want the exact 0.0025 the ledger holds", d)
}
}
// A whole-cent row must still round to its cents, unchanged.
if got := byID["whole"]; got.cents != 150 {
t.Errorf("whole: amount = %d cents, want 150", got.cents)
}
t.Logf("peer page survived: %d rows, sub-cent debits carried as decimal", env.Count)
}
+260
View File
@@ -0,0 +1,260 @@
package o11y
// fleet_scope_test.go — the per-product o11y surface must cover the FLEET, not a
// hand-kept subset of it.
//
// Before this gate, knownServices was the only door into resolveService, and it
// listed 26 k8s workloads. manifest.Apps lists 119 routed apps. The overlap was
// TWELVE. The other 107 — ai, admin, base, platform, projects, team, usage,
// tasks, deploy, exec, index, … — answered honest-empty on
// /v1/o11y/status, /v1/o11y/product/metrics and the scoped log read, not because
// their telemetry was missing (their request spans have been in event.span the
// whole time, stamped by cloud's TracingMiddleware) but because this package had
// never heard of them.
//
// The fix was to DERIVE the set instead of keeping a second one. These tests are
// what stop it from silently becoming a hand list again.
import (
"strings"
"testing"
"github.com/hanzoai/cloud/manifest"
)
// TestEveryRoutedAppHasAProductScope is the fleet direction: an app the host
// routes must be an app the o11y surface can scope to.
func TestEveryRoutedAppHasAProductScope(t *testing.T) {
// Guard the iteration source before iterating it. Every way this could examine
// zero apps — a manifest that failed to load, a package that stopped exporting
// Apps — is a defect elsewhere that would otherwise arrive here as a green
// tick. A floor rather than a zero-check, because a reader that degrades to
// finding 3 of 119 is the same bug wearing a smaller number.
if len(manifest.Apps) < 100 {
t.Fatalf("manifest.Apps has %d rows — the fleet is 119 apps, so this gate is reading the "+
"wrong table and would pass by not looking", len(manifest.Apps))
}
scoped, coresident, fallback := 0, 0, 0
for _, a := range manifest.Apps {
svc, ok := resolveService(a.Name)
// The router's FALLBACK claims the API root, so it bounds nothing. Like a
// co-resident app it has no product scope, for the mirror-image reason.
if claimsAPIRoot(a) {
if ok && len(svc.Routes) > 0 {
t.Errorf("%s claims the API root %q and still resolved to routes %v — that scope is "+
"every request in the fleet", a.Name, apiRoot, svc.Routes)
}
fallback++
continue
}
if a.Coresident {
// A co-resident app ROUTES nothing of its own (zen is a Claim middleware
// on ai's router). There is no route subtree to attribute spans to, so
// there is no honest product scope — and inventing `/v1/zen` would
// silently report zero for a service that is genuinely serving.
if ok && len(svc.Routes) > 0 && !knownServices[a.Name] {
t.Errorf("%s is co-resident but resolved to routes %v — it routes no prefix of its "+
"own, so those spans belong to the app it wraps", a.Name, svc.Routes)
}
coresident++
continue
}
if !ok {
t.Errorf("routed app %q does not resolve to a product scope — its status, metrics and "+
"logs pages answer honest-empty while its spans sit in event.span", a.Name)
continue
}
if len(svc.Routes) == 0 {
t.Errorf("routed app %q resolved with NO routes — the RED query would scope to nothing "+
"and report a healthy idle service", a.Name)
continue
}
// The routes must be the MANIFEST's answer, not a convention that happens to
// agree with it for most rows.
want := manifest.PrefixesFor(a.Name)
if len(want) > 0 {
if len(svc.Routes) != len(want) {
t.Errorf("%s: routes = %v, manifest says %v", a.Name, svc.Routes, want)
continue
}
for i := range want {
if svc.Routes[i] != want[i] {
t.Errorf("%s: routes = %v, manifest says %v", a.Name, svc.Routes, want)
break
}
}
}
scoped++
}
if scoped == 0 {
t.Fatal("no routed app resolved to a product scope — this gate proved nothing")
}
// The two exemptions are exemptions, not a growing list. If either stops
// existing the rule that justifies it has moved and must be re-argued.
if fallback != 1 {
t.Errorf("%d apps claim the API root, want exactly 1 (ai) — a second fallback means two apps "+
"are being handed the same unclaimed traffic", fallback)
}
if coresident != 1 {
t.Errorf("%d co-resident apps, want exactly 1 (zen)", coresident)
}
t.Logf("%d routed apps scoped, %d co-resident, %d API-root fallback, %d manifest rows",
scoped, coresident, fallback, len(manifest.Apps))
}
// claimsAPIRoot reports whether an app declares the prefix every other app is
// under — the router's fallback role.
func claimsAPIRoot(a manifest.App) bool {
for _, p := range a.Prefixes {
if strings.TrimSuffix(p, "/") == apiRoot {
return true
}
}
return false
}
// TestDerivedRoutesBeatTheSlashV1NameConvention pins the REASON the routes come
// from the manifest rather than from string concatenation.
//
// For ~20 apps `/v1/<name>` is not a path anyone serves: plan serves /v1/plans,
// storage serves /v1/s3/buckets, account serves /v1/orgs, knowledge serves
// /v1/kb/*. Scoping their RED series to `/v1/<name>` matches no span at all and
// renders as a healthy service with no traffic — the worst kind of wrong, because
// it looks like an answer.
//
// If this test ever finds ZERO such apps it must fail, not pass: that would mean
// either the manifest changed shape or this test stopped reading it, and in both
// cases the claim it is defending has gone unverified.
func TestDerivedRoutesBeatTheSlashV1NameConvention(t *testing.T) {
var differ []string
for _, a := range manifest.Apps {
if a.Coresident {
continue
}
svc, ok := resolveService(a.Name)
if !ok || len(svc.Routes) == 0 {
continue
}
convention := "/v1/" + a.Name
exact := len(svc.Routes) == 1 && svc.Routes[0] == convention
if !exact {
differ = append(differ, a.Name)
}
}
if len(differ) == 0 {
t.Fatal("every app's routes equal \"/v1/\"+name — either the manifest is no longer being " +
"read (this gate is asserting nothing) or the derivation has been replaced by the " +
"convention it exists to correct")
}
t.Logf("%d apps whose real prefixes differ from \"/v1/\"+name: %v", len(differ), differ)
}
// TestTheFallbackDoesNotSwallowTheFleet is the attribution rule.
//
// ai declares `/v1` — the catch-all it serves the OpenAI-compatible surface from,
// so that zen's c.Next() has somewhere to fall through to. A bare
// startsWith('/v1') scan therefore hands ai every request in the fleet. Over six
// hours of live spans /v1/kms/* alone was 161,705 of 434,765, so the metrics page
// would have reported KMS's traffic as inference.
//
// manifest.OwnerOf already warns that "anything deciding policy from a bare
// HasPrefix scan will attribute those paths to the wrong app". A RED query decides
// policy. ai therefore gets NO route scope at all — the honest answer, and the one
// that does not cost 15.5s to compute.
func TestTheFallbackDoesNotSwallowTheFleet(t *testing.T) {
// The premise: ai really does claim the API root. If the manifest stops saying
// that, this gate is no longer testing anything and must say so rather than pass.
if got := manifest.PrefixesFor("ai"); len(got) == 0 {
t.Fatal(`manifest has no prefixes for "ai" — this gate cannot check the case it exists for`)
} else {
root := false
for _, p := range got {
if strings.TrimSuffix(p, "/") == apiRoot {
root = true
}
}
if !root {
t.Fatalf(`ai no longer claims %q (prefixes=%v) — the swallow case this gate defends `+
`against has moved, so it is asserting nothing`, apiRoot, got)
}
}
if svc, ok := resolveService("ai"); ok && len(svc.Routes) > 0 {
t.Errorf("ai resolved to routes %v — every nested app's spans would count as inference", svc.Routes)
}
// A BOUNDED ancestor is different and must still exclude its children: admin
// owns /v1/admin and seven other apps live under it.
admin, ok := resolveService("admin")
if !ok {
t.Fatal(`resolveService("admin") refused`)
}
if len(admin.Excludes) == 0 {
t.Error("admin owns /v1/admin and excludes NOTHING — apps nested under it count as admin traffic")
}
// A leaf app excludes nothing, and must not: over-excluding hides its own spans.
kms, ok := resolveService("kms")
if !ok {
t.Fatal(`resolveService("kms") refused`)
}
if len(kms.Excludes) != 0 {
t.Errorf("kms excludes %v — nothing nests inside /v1/kms", kms.Excludes)
}
t.Logf("ai: no route scope (fallback); admin excludes %d nested prefixes; kms excludes 0",
len(admin.Excludes))
}
// TestDerivationLosesNoPreviouslyServedProduct is the other direction. Deriving
// the set must ADD apps, never drop a slug the console already asks for — the 26
// verified workloads and the 7 console aliases were all serving before.
func TestDerivationLosesNoPreviouslyServedProduct(t *testing.T) {
if len(knownServices) == 0 || len(productAlias) == 0 {
t.Fatal("knownServices or productAlias is empty — this gate has nothing to compare against")
}
checked := 0
for name := range knownServices {
if _, ok := resolveService(name); !ok {
t.Errorf("workload %q resolved before and does not now — the derivation dropped a "+
"product the console already asks for", name)
}
checked++
}
for slug := range productAlias {
if _, ok := resolveService(slug); !ok {
t.Errorf("console alias %q no longer resolves", slug)
}
checked++
}
if checked == 0 {
t.Fatal("compared nothing")
}
t.Logf("%d previously-served products still resolve", checked)
}
// TestProductScopeStillRefusesWhatItAlwaysRefused — deriving the ALLOWLIST must
// not weaken the SHAPE gate. The product param is still placed into a PromQL
// label, a bound datastore parameter and a hostname, and validProduct is still
// the one thing standing in front of that.
func TestProductScopeStillRefusesWhatItAlwaysRefused(t *testing.T) {
bad := []string{
"", " ", "-leading", "trailing-", "UPPER", "under_score", "dot.dot",
`"} or up{`, "../etc/passwd", "a/b", "sp ace",
"waytoolongwaytoolongwaytoolongwaytoolongwaytoolongwaytoolongwaytoolong",
"definitely-not-an-app-xyz", // well-formed but in neither table
}
for _, p := range bad {
if svc, ok := resolveService(p); ok {
t.Errorf("resolveService(%q) = %+v, true — want refused", p, svc)
}
}
// And the gate must still SAY YES to a real one, or the loop above proves only
// that everything is refused.
if _, ok := resolveService("kms"); !ok {
t.Fatal(`resolveService("kms") refused a real routed app — the gate now refuses everything, ` +
`which would make every assertion above vacuously true`)
}
}
+31 -3
View File
@@ -3,6 +3,7 @@ package o11y
import (
"context"
"fmt"
"strings"
"time"
"github.com/hanzoai/cloud/apps/datastore"
@@ -114,7 +115,35 @@ func queryMetrics(ctx context.Context, q metricsQuery) (metricsResponse, error)
// on the plane (event.span). A non-admin is pinned to its own org; an admin sees
// the whole product.
func redSeries(ctx context.Context, q metricsQuery, resp *metricsResponse) error {
routePrefix := "/v1/" + q.svc.ID
// One product can serve SEVERAL subtrees (account serves /v1/orgs and five
// more), so the route predicate is built per prefix and OR'd. Every prefix
// still rides as a BOUND PARAMETER — the product param reached here through
// validProduct and an allowlist, and the manifest is our own table, but the
// rule that no name is interpolated into SQL is the reason neither of those
// has to be re-argued at this line.
routeSQL := make([]string, 0, len(q.svc.Routes))
args := []any{q.stepSec}
for _, p := range q.svc.Routes {
routeSQL = append(routeSQL, "attributes['http.route'] = ? OR startsWith(attributes['http.route'], ?)")
args = append(args, p, strings.TrimSuffix(p, "/")+"/")
}
routeSQL = append(routeSQL, "service = ?")
args = append(args, q.svc.App)
// Longest prefix wins, exactly as the router decided it. Without this an app
// that owns an ancestor path (ai owns /v1) counts every nested app's traffic as
// its own. See nestedPrefixes in productmap.go.
where := "(" + strings.Join(routeSQL, " OR ") + ")"
if len(q.svc.Excludes) > 0 {
ex := make([]string, 0, len(q.svc.Excludes))
for _, p := range q.svc.Excludes {
ex = append(ex, "attributes['http.route'] = ? OR startsWith(attributes['http.route'], ?)")
args = append(args, p, p+"/")
}
where += " AND NOT (" + strings.Join(ex, " OR ") + ")"
}
args = append(args, q.rangeSec)
sql := "SELECT toStartOfInterval(time, toIntervalSecond(?)) AS bucket, " +
"count() AS reqs, " +
// The HTTP status is a span attribute (Map values are strings); coerce
@@ -123,9 +152,8 @@ func redSeries(ctx context.Context, q metricsQuery, resp *metricsResponse) error
"quantile(0.5)(duration) AS p50, " +
"quantile(0.95)(duration) AS p95 " +
"FROM event.span " +
"WHERE (attributes['http.route'] = ? OR startsWith(attributes['http.route'], ?) OR service = ?) " +
"WHERE " + where + " " +
"AND time > now64(9) - toIntervalSecond(?)"
args := []any{q.stepSec, routePrefix, routePrefix + "/", q.svc.App, q.rangeSec}
// THE tenant gate. A non-admin is pinned to its own org (the plane's first
// sort-key column); a validated SuperAdmin sees the whole product (no org
// predicate).
+151 -2
View File
@@ -1,6 +1,11 @@
package o11y
import "strings"
import (
"sort"
"strings"
"github.com/hanzoai/cloud/manifest"
)
// productmap is the ONE server-side table that resolves a client-supplied
// `product` query param into the concrete infra identities the scoped o11y
@@ -71,6 +76,112 @@ var knownServices = map[string]bool{
"visor": true,
}
// knownServices answers "does a WORKLOAD answer at an address"; manifest.Apps
// answers "does an APP serve routes". They are different questions, and for most
// of the fleet only the second has a yes.
//
// Every routed app's requests are already on the plane: cloud's TracingMiddleware
// stamps `http.route` on every request span, and manifest.Apps is the table the
// host builds its router from — so the RED signal for all 119 apps has been in
// event.span the whole time. The only thing standing between it and a caller was
// this file's hand-maintained workload list, which had heard of 12 of them. An
// app that nobody probes still has no address and is still not probed (URL stays
// empty, status answers honest-empty); what it gains is its own logs and metrics,
// which never needed an address.
//
// This is why the fleet surface is DERIVED rather than a second list to maintain:
// a new plugin gets metrics/logs/status the moment it has a manifest row, which
// it must have to be routable at all (TestEveryPluginNameIsInTheManifest).
func fleetRoutes(name string) []string {
ps := manifest.PrefixesFor(name)
for _, p := range ps {
if strings.TrimSuffix(p, "/") == apiRoot {
// An app that declares the API ROOT is the router's FALLBACK — it is
// handed whatever no other app claimed. That is a routing role, not a
// product boundary, and "every request in the fleet minus everyone
// else's" is not a RED series anyone should read as this product's
// traffic. ai is the only such app (it serves the OpenAI-compatible
// surface at top level so zen's c.Next() has somewhere to fall through
// to), and it has no bounded route scope for the same reason zen has
// none: zen routes nothing, ai routes everything.
//
// Measured, in case anyone is tempted to compute it anyway: expressing
// it as "/v1 minus the 251 nested prefixes" costs 15.5s against six
// hours of live spans, versus a ~4-5s baseline for a bounded app — three
// times the cost, for a number that would have counted KMS's 161,705
// requests as inference.
return nil
}
}
return ps
}
// apiRoot is the prefix every routed app is under, so declaring it claims
// everything and bounds nothing.
const apiRoot = "/v1"
// nestedPrefixes is the LONGEST-PREFIX-WINS half, and it is not optional.
//
// manifest.OwnerOf says it plainly: "Anything deciding policy from a bare
// HasPrefix scan will attribute those paths to the wrong app." A RED query is
// deciding policy. ai declares `/v1` — the catch-all it serves the
// OpenAI-compatible surface from — so a bare startsWith('/v1') scan hands ai every
// request in the fleet. Measured over six hours of live spans: /v1/kms/* alone is
// 161,705 of 434,765, and ai would have reported all of them as its own.
//
// So a product's spans are the ones under its prefixes MINUS the ones a strictly
// longer prefix belonging to a DIFFERENT app claims — the same rule the router
// used to route them in the first place. For nearly every app this list is empty
// (nothing nests inside /v1/kms) and the exclusion costs nothing.
func nestedPrefixes(own []string) []string {
roots := make([]string, 0, len(own))
for _, p := range own {
if r := strings.TrimSuffix(p, "/"); r != "" {
roots = append(roots, r)
}
}
seen := map[string]bool{}
var nested []string
for _, a := range manifest.Apps {
for _, q := range a.Prefixes {
qr := strings.TrimSuffix(q, "/")
if qr == "" || seen[qr] {
continue
}
for _, pr := range roots {
// STRICTLY longer and genuinely nested. An app's own prefixes are
// never longer than themselves, so this cannot exclude the product
// from its own subtree.
if len(qr) > len(pr) && strings.HasPrefix(qr, pr+"/") {
seen[qr] = true
nested = append(nested, qr)
break
}
}
}
}
// Keep only the MINIMAL set. Excluding /v1/platform already excludes
// /v1/platform/fleet, and carrying both puts two predicates in the query where
// one decides. For ai this is the difference between 282 exclusions and a set
// small enough to read.
sort.Slice(nested, func(i, j int) bool { return len(nested[i]) < len(nested[j]) })
var out []string
for _, q := range nested {
covered := false
for _, kept := range out {
if strings.HasPrefix(q, kept+"/") {
covered = true
break
}
}
if !covered {
out = append(out, q)
}
}
sort.Strings(out)
return out
}
// service is the resolved infra identity for a product.
type service struct {
// ID is the canonical product id (== the product param, validated).
@@ -84,6 +195,19 @@ type service struct {
// addresses (probes.go). Empty when the fleet does not watch this workload,
// and the scoped status read then probes nothing.
URL string
// Excludes is the set of longer prefixes owned by OTHER apps that nest inside
// Routes. Without it a product that owns an ancestor path absorbs its
// children's traffic — see nestedPrefixes.
Excludes []string
// Routes is the set of path prefixes whose request spans belong to this
// product — the manifest's own answer where there is one.
//
// It is NOT `/v1/<id>` by convention, because for ~20 apps that convention is
// simply wrong: plan serves /v1/plans, storage serves /v1/s3/buckets, account
// serves /v1/orgs (and five more), knowledge serves /v1/kb/*. Reading the RED
// series off `/v1/<name>` for those scopes the query to a subtree nobody
// serves, which returns zero and looks exactly like a healthy idle service.
Routes []string
}
// resolveService validates + resolves a product param. ok=false means the product
@@ -98,9 +222,32 @@ func resolveService(product string) (service, bool) {
if a, ok := productAlias[p]; ok {
workload = a
}
if !knownServices[workload] {
// Routes come from the PRODUCT id, not the aliased workload, because the alias
// answers a different question. productAlias maps a console slug to the k8s
// workload that ANSWERS (for probing and the prom `service` label); the routing
// table is keyed by APP NAME. For analytics the two disagree in both
// directions — the app is `analytics` and serves five prefixes
// (/v1/analytics, /v1/errors, /v1/event, /v1/insights/*), while the workload is
// `insights-capture` and is not a routed app at all. Looking the routes up by
// workload silently lost four of those five prefixes.
routes := fleetRoutes(p)
if len(routes) == 0 {
routes = fleetRoutes(workload)
}
// EITHER answer admits the product: a verified workload (it has an address, so
// status can probe it) or a routed app (it has request spans, so metrics and
// logs can read it). Requiring both would keep 107 routed apps dark for want of
// a probe they never needed.
if !knownServices[workload] && len(routes) == 0 {
return service{}, false
}
if len(routes) == 0 {
// A verified workload with no manifest row is not a routed app — it is a
// bare k8s service (chat, studio, nats, vector). `/v1/<id>` is what this
// file has always assumed for those, and it stays their answer; the
// manifest simply has nothing better to say about them.
routes = []string{"/v1/" + p}
}
// A workload the fleet does not watch has no address, and the miss is not an
// error: it emits logs and metrics we can still query, we have just never
// measured whether it answers.
@@ -110,6 +257,8 @@ func resolveService(product string) (service, bool) {
App: workload,
PromService: workload,
URL: url,
Routes: routes,
Excludes: nestedPrefixes(routes),
}, true
}
+11 -3
View File
@@ -34,13 +34,21 @@ func init() {
// (the embedded runtime serves only the list) and none is registered here.
//
// Why an explicit cloud route rather than only the order-70 wildcard: this pins the
// public flat path to the runtime's internal /api/sessions route SERVER-SIDE (the
// same discipline query.go uses for the composite query) AND enforces the tenant
// gate at the cloud boundary — an org-less caller gets a clean 403 here before the
// public flat path to the runtime's internal /api/sessions route SERVER-SIDE AND
// enforces the tenant gate at the cloud boundary — an org-less caller gets a clean 403 here before the
// request reaches the runtime, and the org the runtime binds (gen_ai.hanzo.org_id
// from X-Org-Id) is the SAME validated tenant this handler refuses to proceed
// without. Registered by mountScope (order 69), so it precedes the wildcard.
//
// This used to cite query.go's composite-query pin as the precedent for the move.
// That file is GONE — it pinned POST /v1/o11y/query_range to the v3 engine, and when
// it went the module's v5 querier took the address, whose composite accepts only
// {queries:[…]}. The console still sent the v3 {queryType,panelType,builderQueries}
// envelope and every Logs page 400'd on "unknown field \"queryType\" in composite
// query". Citing a deleted pin as the discipline to follow is how the next route
// inherits the same break, so the reference is removed rather than reworded: this
// route stands on its OWN pin, three lines below, which is still here.
//
// The list query (?limit=&offset=) rides through unchanged; the runtime returns the
// llmobstypes.GettableSessions {items,offset,limit} under the {status,data} envelope
// the console's O11yApi.sessions already unwraps.
+1 -1
View File
@@ -129,7 +129,7 @@ func init() {
Example: json.RawMessage(`{"id":"annq_1","status":"PENDING"}`),
})
zip.Describe("GET /v1/o11y/sessions", zip.Doc{
Description: "GET /v1/o11y/sessions — the flat, org-gated public path for the LLM-obs sessions\nlist (traces grouped by session.id on the gen_ai span plane). The console's\nSessionsModule reads this; session DETAIL is composed client-side from this list\n+ the traces list filtered by session, so there is no /sessions/:id backing route\n(the embedded runtime serves only the list) and none is registered here.\n\nWhy an explicit cloud route rather than only the order-70 wildcard: this pins the\npublic flat path to the runtime's internal /api/sessions route SERVER-SIDE (the\nsame discipline query.go uses for the composite query) AND enforces the tenant\ngate at the cloud boundary — an org-less caller gets a clean 403 here before the\nrequest reaches the runtime, and the org the runtime binds (gen_ai.hanzo.org_id\nfrom X-Org-Id) is the SAME validated tenant this handler refuses to proceed\nwithout. Registered by mountScope (order 69), so it precedes the wildcard.\n\nThe list query (?limit=&offset=) rides through unchanged; the runtime returns the\nllmobstypes.GettableSessions {items,offset,limit} under the {status,data} envelope\nthe console's O11yApi.sessions already unwraps.",
Description: "GET /v1/o11y/sessions — the flat, org-gated public path for the LLM-obs sessions\nlist (traces grouped by session.id on the gen_ai span plane). The console's\nSessionsModule reads this; session DETAIL is composed client-side from this list\n+ the traces list filtered by session, so there is no /sessions/:id backing route\n(the embedded runtime serves only the list) and none is registered here.\n\nWhy an explicit cloud route rather than only the order-70 wildcard: this pins the\npublic flat path to the runtime's internal /api/sessions route SERVER-SIDE AND\nenforces the tenant gate at the cloud boundary — an org-less caller gets a clean 403 here before the\nrequest reaches the runtime, and the org the runtime binds (gen_ai.hanzo.org_id\nfrom X-Org-Id) is the SAME validated tenant this handler refuses to proceed\nwithout. Registered by mountScope (order 69), so it precedes the wildcard.\n\nThis used to cite query.go's composite-query pin as the precedent for the move.\nThat file is GONE — it pinned POST /v1/o11y/query_range to the v3 engine, and when\nit went the module's v5 querier took the address, whose composite accepts only\n{queries:[…]}. The console still sent the v3 {queryType,panelType,builderQueries}\nenvelope and every Logs page 400'd on \"unknown field \\\"queryType\\\" in composite\nquery\". Citing a deleted pin as the discipline to follow is how the next route\ninherits the same break, so the reference is removed rather than reworded: this\nroute stands on its OWN pin, three lines below, which is still here.\n\nThe list query (?limit=&offset=) rides through unchanged; the runtime returns the\nllmobstypes.GettableSessions {items,offset,limit} under the {status,data} envelope\nthe console's O11yApi.sessions already unwraps.",
})
zip.Describe("GET /v1/o11y/status", zip.Doc{
Description: "Reports whether a product's service is live: an in-cluster\nhealth probe with its measured latency, fused with the per-replica up\ninventory. Infra health is not tenant-partitioned — a service is up or down\nfor everyone — so any validated caller is served, but an unvalidated one is\nrefused. A product with no backing workload answers down/unknown-service\nwithout probing anything; a malformed slug is a 400.",