mirror of
https://github.com/hanzoai/o11y.git
synced 2026-11-02 14:03:38 +00:00
gorilla/mux, otelmux, gorilla/handlers and rs/cors leave go.mod. Every route the runtime serves is a zip route on a zip group: 233 from the apiserver tree, 134 from query-service, 367 total — the same 367 the mount publishes. pkg/http/routing is the one registration surface. It exists because a registration carries three facts zip has no slot for, and the tree this replaces recovered all three PER REQUEST by asking the router what it had just matched: the bound segments, the path template, and the route's declared resources. mux.CurrentRoute(req).GetHandler() was type-asserted back to handler.Handler in two separate packages to re-derive at request time what was already known once at boot. Stating them at registration deletes both lookups. The middleware chain is composed at the LEAF rather than registered ambient, because Audit keys on the route template and Resource resolves the route's declared resources — ambient middleware runs before the router has matched, so both would read an empty route. Per-leaf composition reproduces the order the old tree ran in exactly: match, then middleware, then handler. Duplicate registration now panics at boot; gorilla let the first one win. The Sentry ingest constraint becomes fiber's own guid — a UUID parse instead of a hand-written character class. Paths keep their public brace spelling in the source and in the table; the respelling happens in one function. The standalone server hands its listener to the app, so streams and the websocket upgrade run native fasthttp. Counted, not asserted: routes_test.go in the apiserver pins 233 against the real AddToRouter, routing_test.go pins the table against the router itself. This repo has already shipped 83 typed ops that reached no router while the suite stayed green — an uncalled func is legal Go, so arithmetic is the test. Co-authored-by: Hanzo Dev <[email protected]>
124 lines
7.6 KiB
Go
124 lines
7.6 KiB
Go
package o11yapiserver
|
|
|
|
import (
|
|
"net/http"
|
|
|
|
"github.com/hanzoai/o11y/pkg/http/handler"
|
|
"github.com/hanzoai/o11y/pkg/http/routing"
|
|
"github.com/hanzoai/o11y/pkg/types"
|
|
"github.com/hanzoai/o11y/pkg/types/llmobstypes"
|
|
)
|
|
|
|
// addLLMObsRoutes serves the native LLM-observability surface (absorbing
|
|
// the upstream product) under /v1/o11y/llm. Observations/traces/sessions/users are views
|
|
// over gen_ai spans; scores and annotations are CRUD over net-new tables. Every
|
|
// route is behind the shared Hanzo IAM authz middleware.
|
|
//
|
|
// The /llm/ segment is what makes each name unambiguous: this family's "traces",
|
|
// "sessions" and "users" are gen_ai-span projections, NOT the APM trace search,
|
|
// the auth sessions, or the IAM user records that own those words at /v1/o11y/.
|
|
// Flat names collided outright — GET /v1/o11y/users is IAM's, registered first,
|
|
// so a flat llmobs twin would be dead on arrival with no error from mux.
|
|
//
|
|
// ALL of these routes are ALSO declared as typed ops at the module's mount seam
|
|
// (llmobs.go in the repo root), which is what carries them into the composed
|
|
// document, the SDK, the CLI and the agent surface. That is a second DISPATCH,
|
|
// never a second implementation: the ops answer by handing the call to this
|
|
// router, so the handlers below stay the one place the work is performed — and
|
|
// the gates declared here (ViewAccess, EditAccess, AdminAccess) stay the one
|
|
// place access is decided. Both halves are needed — the ops serve the composed
|
|
// binary, this router serves the standalone process, which has no native router
|
|
// to register an op on — so deleting either drops one of the two deployments.
|
|
func (provider *provider) addLLMObsRoutes(router routing.Router) {
|
|
h := provider.llmObsHandler
|
|
|
|
routes := []struct {
|
|
method string
|
|
path string
|
|
fn http.HandlerFunc
|
|
def handler.OpenAPIDef
|
|
}{
|
|
{http.MethodGet, "/v1/o11y/llm/observations", provider.authzMiddleware.ViewAccess(h.Observations), handler.OpenAPIDef{
|
|
ID: "ListLLMObservations", Tags: []string{"llmobs"}, Summary: "List observations",
|
|
Description: "Lists gen_ai spans (LLM observations) with model, tokens, cost and latency projected from gen_ai.* attributes.",
|
|
RequestQuery: new(llmobstypes.ViewQuery),
|
|
Response: new(llmobstypes.GettableObservations),
|
|
ResponseContentType: "application/json", SuccessStatusCode: http.StatusOK,
|
|
ErrorStatusCodes: []int{http.StatusBadRequest}, SecuritySchemes: newSecuritySchemes(types.RoleViewer),
|
|
}},
|
|
{http.MethodGet, "/v1/o11y/llm/traces", provider.authzMiddleware.ViewAccess(h.Traces), handler.OpenAPIDef{
|
|
ID: "ListLLMTraces", Tags: []string{"llmobs"}, Summary: "List traces",
|
|
Description: "Lists LLM traces (gen_ai spans grouped by trace_id) with rolled-up cost, tokens and latency.",
|
|
RequestQuery: new(llmobstypes.ViewQuery),
|
|
Response: new(llmobstypes.GettableTraces),
|
|
ResponseContentType: "application/json", SuccessStatusCode: http.StatusOK,
|
|
ErrorStatusCodes: []int{http.StatusBadRequest}, SecuritySchemes: newSecuritySchemes(types.RoleViewer),
|
|
}},
|
|
{http.MethodGet, "/v1/o11y/llm/sessions", provider.authzMiddleware.ViewAccess(h.Sessions), handler.OpenAPIDef{
|
|
ID: "ListLLMSessions", Tags: []string{"llmobs"}, Summary: "List sessions",
|
|
Description: "Lists conversations (gen_ai spans grouped by session.id) with trace/observation counts, tokens and cost.",
|
|
RequestQuery: new(llmobstypes.ViewQuery),
|
|
Response: new(llmobstypes.GettableSessions),
|
|
ResponseContentType: "application/json", SuccessStatusCode: http.StatusOK,
|
|
ErrorStatusCodes: []int{http.StatusBadRequest}, SecuritySchemes: newSecuritySchemes(types.RoleViewer),
|
|
}},
|
|
{http.MethodGet, "/v1/o11y/llm/users", provider.authzMiddleware.ViewAccess(h.Users), handler.OpenAPIDef{
|
|
ID: "ListLLMUsers", Tags: []string{"llmobs"}, Summary: "List users",
|
|
Description: "Lists end users (gen_ai spans grouped by user.id) with session/trace/observation counts, tokens and cost.",
|
|
RequestQuery: new(llmobstypes.ViewQuery),
|
|
Response: new(llmobstypes.GettableUsers),
|
|
ResponseContentType: "application/json", SuccessStatusCode: http.StatusOK,
|
|
ErrorStatusCodes: []int{http.StatusBadRequest}, SecuritySchemes: newSecuritySchemes(types.RoleViewer),
|
|
}},
|
|
{http.MethodGet, "/v1/o11y/llm/scores", provider.authzMiddleware.ViewAccess(h.ListScores), handler.OpenAPIDef{
|
|
ID: "ListLLMScores", Tags: []string{"llmobs"}, Summary: "List scores",
|
|
Description: "Lists eval scores and human-feedback signals attached to traces/observations.",
|
|
RequestQuery: new(llmobstypes.ScoresQuery),
|
|
Response: new(llmobstypes.GettableScores),
|
|
ResponseContentType: "application/json", SuccessStatusCode: http.StatusOK,
|
|
ErrorStatusCodes: []int{http.StatusBadRequest}, SecuritySchemes: newSecuritySchemes(types.RoleViewer),
|
|
}},
|
|
{http.MethodPost, "/v1/o11y/llm/scores", provider.authzMiddleware.EditAccess(h.CreateScore), handler.OpenAPIDef{
|
|
ID: "CreateLLMScore", Tags: []string{"llmobs"}, Summary: "Create a score",
|
|
Description: "Attaches an eval score or human-feedback signal to a trace or observation.",
|
|
Request: new(llmobstypes.IngestScore), RequestContentType: "application/json",
|
|
Response: new(llmobstypes.Score),
|
|
ResponseContentType: "application/json", SuccessStatusCode: http.StatusCreated,
|
|
ErrorStatusCodes: []int{http.StatusBadRequest}, SecuritySchemes: newSecuritySchemes(types.RoleEditor),
|
|
}},
|
|
{http.MethodGet, "/v1/o11y/llm/score/{id}", provider.authzMiddleware.ViewAccess(h.GetScore), handler.OpenAPIDef{
|
|
ID: "GetLLMScore", Tags: []string{"llmobs"}, Summary: "Get a score",
|
|
Description: "Returns a single score by id.",
|
|
Response: new(llmobstypes.Score),
|
|
ResponseContentType: "application/json", SuccessStatusCode: http.StatusOK,
|
|
ErrorStatusCodes: []int{http.StatusNotFound}, SecuritySchemes: newSecuritySchemes(types.RoleViewer),
|
|
}},
|
|
{http.MethodDelete, "/v1/o11y/llm/score/{id}", provider.authzMiddleware.EditAccess(h.DeleteScore), handler.OpenAPIDef{
|
|
ID: "DeleteLLMScore", Tags: []string{"llmobs"}, Summary: "Delete a score",
|
|
Description: "Hard-deletes a score by id.",
|
|
SuccessStatusCode: http.StatusNoContent,
|
|
ErrorStatusCodes: []int{http.StatusNotFound}, SecuritySchemes: newSecuritySchemes(types.RoleEditor),
|
|
}},
|
|
{http.MethodGet, "/v1/o11y/llm/annotation", provider.authzMiddleware.ViewAccess(h.Annotations), handler.OpenAPIDef{
|
|
ID: "ListLLMAnnotations", Tags: []string{"llmobs"}, Summary: "List annotations",
|
|
Description: "Lists human annotations (optionally scoped to a review queue) on traces/observations.",
|
|
RequestQuery: new(llmobstypes.AnnotationsQuery),
|
|
Response: new(llmobstypes.GettableAnnotations),
|
|
ResponseContentType: "application/json", SuccessStatusCode: http.StatusOK,
|
|
ErrorStatusCodes: []int{http.StatusBadRequest}, SecuritySchemes: newSecuritySchemes(types.RoleViewer),
|
|
}},
|
|
{http.MethodPost, "/v1/o11y/llm/annotation", provider.authzMiddleware.EditAccess(h.CreateAnnotation), handler.OpenAPIDef{
|
|
ID: "CreateLLMAnnotation", Tags: []string{"llmobs"}, Summary: "Create an annotation",
|
|
Description: "Adds a human annotation to a trace or observation, optionally in a review queue.",
|
|
Request: new(llmobstypes.IngestAnnotation), RequestContentType: "application/json",
|
|
Response: new(llmobstypes.Annotation),
|
|
ResponseContentType: "application/json", SuccessStatusCode: http.StatusCreated,
|
|
ErrorStatusCodes: []int{http.StatusBadRequest}, SecuritySchemes: newSecuritySchemes(types.RoleEditor),
|
|
}},
|
|
}
|
|
|
|
for _, rt := range routes {
|
|
router.Handle(rt.method, rt.path, handler.New(rt.fn, rt.def))
|
|
}
|
|
}
|