mount: serve the whole o11y surface at /v1/o11y/<resource> (kill the /api/ leak) (#26)

* sqlite: one driver-registration site (drop redundant blank modernc import)

The unified cloud binary must register the database/sql \"sqlite\" driver exactly
once. o11y contributed a SECOND registration import: a blank _ \"modernc.org/sqlite\"
in pkg/query-service/app/http_handler.go, on top of the one in
pkg/sqlstore/sqlitesqlstore/provider.go (which MUST import modernc non-blank for the
*sqlite.Error type + modernc/sqlite/lib error-code constants — github.com/hanzoai/sqlite
does not re-export either).

Remove the redundant blank import. The driver stays registered by provider.go, which
pkg/query-service/app transitively imports in every binary (verified via go list -deps
for both cmd/community and the cloud embed) — so registration survives the removal.

Do NOT swap the blank import to github.com/hanzoai/sqlite: under CGO_ENABLED=1 (o11y
CI default) the fork Register()s mattn while modernc also Register()s modernc → the
exact \"sql: Register called twice for driver sqlite\" panic. Under CGO_ENABLED=0
(cloud prod build) the fork routes to modernc and shares its single init with o11y, so
the cloud binary already registers \"sqlite\" once — no fork import needed here.

Add TestDriverRegisteredOnce (pkg/sqlstore/sqlitesqlstore): opens the provider and
asserts journal_mode=wal + busy_timeout>0 through the driver — fails loudly if either
regression (lost registration, or double-register panic) returns. Passes CGO on and off.

* mount: serve the whole o11y surface at /v1/o11y/<resource> — no /api/ leak

The public contract is api.hanzo.ai/v1/o11y/<resource> (one /v1/, no /api/). But
o11y is a SigNoz fork whose FE+BE speak /api/vN (5 live versions), and the mount
delegated to SigNoz's StripPrefix, so the surface leaked as /v1/o11y/api/vN/... AND
the Hanzo llmobs routes (registered at /v1/o11y/*) 404'd once StripPrefix ate the
prefix. The earlier attempt to rip /api/ from SigNoz's 291 route literals was reverted
by an upstream re-sync (o11y/CLAUDE.md) — so this normalizes at the ONE Hanzo-owned
seam (mount.go) instead, zero SigNoz fork diff:

  /v1/o11y/vN/...      -> /api/vN/...   (canonical SigNoz; the /api/ never surfaces)
  /v1/o11y/api/vN/...  -> /api/vN/...   (deprecated alias so current callers keep working)
  /v1/o11y/traces,...  -> unchanged     (Hanzo llmobs, native)

Paired with cloud CR O11Y_GLOBAL_EXTERNAL__URL="" (StripPrefix off) so the llmobs
/v1/o11y/* routes survive to the router — deploy together. Logic table-tested
(mount_test.go); full-graph build is CI-authoritative (root pkg pulls the cloud graph).

---------

Co-authored-by: hanzo <z@hanzo.ai>
This commit is contained in:
Hanzo Dev
2026-07-09 00:38:04 -07:00
committed by GitHub
co-authored by z
parent eb2de99089
commit 3ac8ca7395
2 changed files with 86 additions and 21 deletions
+54 -4
View File
@@ -2,11 +2,13 @@ package o11y
import (
"net/http"
"net/url"
"strings"
"sync"
"github.com/hanzoai/cloud"
"github.com/zap-proto/zip"
luxlog "github.com/luxfi/log"
"github.com/zap-proto/zip"
)
// Mount registers Hanzo o11y's HTTP surface under /v1/o11y per HIP-0106.
@@ -25,9 +27,10 @@ import (
// after o11y.New + app.NewServer.
// - Until a handler is registered, the routes 503 with a clear error.
//
// All traffic under /v1/o11y is delegated to the registered http.Handler
// via zip.AdaptNetHTTP; the o11y handler internally rewrites /v1/o11y/*
// to /api/* so existing controllers stay untouched (see app.createPublicServer).
// All traffic under /v1/o11y is delegated to the registered http.Handler via
// zip.AdaptNetHTTP; handlerAdapter normalizes the /v1/o11y/<resource> public
// contract onto the two internal route families HERE — the ONE Hanzo-owned seam —
// so the embedded SigNoz route literals stay untouched (see rewriteExternalPath).
func Mount(app *zip.App, deps cloud.Deps) error {
log := deps.Logger
if log == nil {
@@ -55,9 +58,56 @@ func (handlerAdapter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
http.Error(w, "o11y runtime not initialized", http.StatusServiceUnavailable)
return
}
rewriteExternalPath(r.URL)
h.ServeHTTP(w, r)
}
// rewriteExternalPath maps the ONE public o11y contract — api.hanzo.ai/v1/o11y/<resource>,
// one /v1/, no /api/ — onto the two internal route families, at this single Hanzo-owned
// seam. It is done HERE, never by editing the embedded SigNoz route literals: SigNoz's
// whole frontend and backend speak /api/vN, and rewriting those literals is a fork diff
// that a later upstream re-sync silently reverts (it already happened once — see
// o11y/CLAUDE.md).
//
// SigNoz native (registered at /api/vN/*):
// /v1/o11y/vN/… → /api/vN/… (canonical — the /api/ never surfaces)
// /v1/o11y/api/vN/… → /api/vN/… (deprecated alias: the leaked form callers emit
// today. Drop once every consumer emits the
// canonical form — one and one way.)
// Hanzo llmobs (registered natively at /v1/o11y/{traces,observations,…}): passed
// through unchanged.
//
// This requires the embedded SigNoz StripPrefix wrapper to be OFF — cloud CR
// O11Y_GLOBAL_EXTERNAL__URL="" — so a /v1/o11y/* llmobs path survives to the router.
func rewriteExternalPath(u *url.URL) {
rest, ok := strings.CutPrefix(u.Path, "/v1/o11y/")
if !ok {
return
}
switch {
case strings.HasPrefix(rest, "api/v"): // deprecated leaked alias: /v1/o11y/api/vN/x
setPath(u, "/"+rest) // → /api/vN/x
case isVersionSegment(rest): // canonical SigNoz form: /v1/o11y/vN/x
setPath(u, "/api/"+rest) // → /api/vN/x
default: // Hanzo llmobs / native resource — the router owns /v1/o11y/x directly.
}
}
// isVersionSegment reports whether rest begins with a SigNoz API version segment
// (v followed by a digit — "v1/health", "v3/query_range"): the marker that tells an
// embedded-SigNoz route apart from a Hanzo-native llmobs resource (traces, sessions, …).
func isVersionSegment(rest string) bool {
return len(rest) >= 2 && rest[0] == 'v' && rest[1] >= '0' && rest[1] <= '9'
}
// setPath rewrites the request path, clearing RawPath so EscapedPath re-derives from the
// new value — the rewritten SigNoz paths contain no characters needing escaping, and
// llmobs paths (the only ones carrying an {id}) are never rewritten.
func setPath(u *url.URL, p string) {
u.Path = p
u.RawPath = ""
}
var (
hmu sync.RWMutex
registered http.Handler
+32 -17
View File
@@ -1,7 +1,6 @@
package o11y_test
import (
"io"
"net/http"
"net/http/httptest"
"testing"
@@ -28,7 +27,10 @@ func TestMountWithoutHandlerReturns503(t *testing.T) {
}
}
func TestMountForwardsToRegisteredHandler(t *testing.T) {
// TestMountNormalizesExternalPath proves the one public contract /v1/o11y/<resource>
// (one /v1/, no /api/) is normalized at the mount seam onto the two internal route
// families: SigNoz native (/api/vN/*) and Hanzo llmobs (/v1/o11y/*, passed through).
func TestMountNormalizesExternalPath(t *testing.T) {
app := zip.New(zip.Config{DisableStartupMessage: true})
if err := o11y.Mount(app, cloud.Deps{}); err != nil {
t.Fatalf("Mount: %v", err)
@@ -37,25 +39,38 @@ func TestMountForwardsToRegisteredHandler(t *testing.T) {
var sawPath string
o11y.SetHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
sawPath = r.URL.Path
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"ok":true}`))
}))
defer o11y.SetHandler(nil)
req := httptest.NewRequest(http.MethodGet, "/v1/o11y/api/v1/health", nil)
resp, err := app.Fiber().Test(req)
if err != nil {
t.Fatalf("Test: %v", err)
cases := []struct {
name, external, internal string
}{
// SigNoz native — canonical Hanzo form; the /api/ never surfaces externally.
{"signoz canonical v1", "/v1/o11y/v1/health", "/api/v1/health"},
{"signoz canonical v3", "/v1/o11y/v3/query_range", "/api/v3/query_range"},
{"signoz canonical v5", "/v1/o11y/v5/query_range", "/api/v5/query_range"},
// SigNoz native — deprecated /api/ alias still resolves during migration.
{"signoz legacy alias", "/v1/o11y/api/v1/health", "/api/v1/health"},
// Hanzo llmobs — registered natively at /v1/o11y/*, passed through untouched.
{"llmobs traces", "/v1/o11y/traces", "/v1/o11y/traces"},
{"llmobs observations", "/v1/o11y/observations", "/v1/o11y/observations"},
{"llmobs score by id", "/v1/o11y/score/abc123", "/v1/o11y/score/abc123"},
}
if resp.StatusCode != http.StatusOK {
t.Fatalf("status=%d want 200", resp.StatusCode)
}
body, _ := io.ReadAll(resp.Body)
if string(body) != `{"ok":true}` {
t.Fatalf("body=%q want {\"ok\":true}", body)
}
if sawPath != "/v1/o11y/api/v1/health" {
t.Fatalf("sawPath=%q want /v1/o11y/api/v1/health", sawPath)
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
sawPath = ""
req := httptest.NewRequest(http.MethodGet, tc.external, nil)
resp, err := app.Fiber().Test(req)
if err != nil {
t.Fatalf("Test: %v", err)
}
if resp.StatusCode != http.StatusOK {
t.Fatalf("status=%d want 200", resp.StatusCode)
}
if sawPath != tc.internal {
t.Fatalf("external %s → internal %q, want %q", tc.external, sawPath, tc.internal)
}
})
}
}