Files
zeekayandHanzo Dev 3011b92ea4 routing: one router, and it is zip
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]>
2026-08-01 19:30:40 -07:00

218 lines
11 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/metricsexplorertypes"
)
// ALL of these routes are ALSO declared as typed ops at the module's mount seam
// (metrics.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 on the reads, EditAccess on the metadata
// write) 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) addMetricsExplorerRoutes(router routing.Router) {
router.Get("/v1/o11y/metrics", handler.New(
provider.authzMiddleware.ViewAccess(provider.metricsExplorerHandler.ListMetrics),
handler.OpenAPIDef{
ID: "ListMetrics",
Tags: []string{"metrics"},
Summary: "List metric names",
Description: "This endpoint returns a list of distinct metric names within the specified time range",
Request: nil,
RequestQuery: new(metricsexplorertypes.ListMetricsParams),
RequestContentType: "",
Response: new(metricsexplorertypes.ListMetricsResponse),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusInternalServerError},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
}))
router.Post("/v1/o11y/metrics/stats", handler.New(
provider.authzMiddleware.ViewAccess(provider.metricsExplorerHandler.GetStats),
handler.OpenAPIDef{
ID: "GetMetricsStats",
Tags: []string{"metrics"},
Summary: "Get metrics statistics",
Description: "This endpoint provides list of metrics with their number of samples and timeseries for the given time range",
Request: new(metricsexplorertypes.StatsRequest),
RequestContentType: "application/json",
Response: new(metricsexplorertypes.StatsResponse),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusInternalServerError},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
}))
router.Post("/v1/o11y/metrics/treemap", handler.New(
provider.authzMiddleware.ViewAccess(provider.metricsExplorerHandler.GetTreemap),
handler.OpenAPIDef{
ID: "GetMetricsTreemap",
Tags: []string{"metrics"},
Summary: "Get metrics treemap",
Description: "This endpoint returns a treemap visualization showing the proportional distribution of metrics by sample count or time series count",
Request: new(metricsexplorertypes.TreemapRequest),
RequestContentType: "application/json",
Response: new(metricsexplorertypes.TreemapResponse),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusInternalServerError},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
}))
router.Get("/v1/o11y/metrics/attributes", handler.New(
provider.authzMiddleware.ViewAccess(provider.metricsExplorerHandler.GetMetricAttributes),
handler.OpenAPIDef{
ID: "GetMetricAttributes",
Tags: []string{"metrics"},
Summary: "Get metric attributes",
Description: "This endpoint returns attribute keys and their unique values for a specified metric",
Request: nil,
RequestQuery: new(metricsexplorertypes.MetricAttributesRequest),
RequestContentType: "",
Response: new(metricsexplorertypes.MetricAttributesResponse),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusNotFound, http.StatusInternalServerError},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
}))
router.Get("/v1/o11y/metrics/metadata", handler.New(
provider.authzMiddleware.ViewAccess(provider.metricsExplorerHandler.GetMetricMetadata),
handler.OpenAPIDef{
ID: "GetMetricMetadata",
Tags: []string{"metrics"},
Summary: "Get metric metadata",
Description: "This endpoint returns metadata information like metric description, unit, type, temporality, monotonicity for a specified metric",
Request: nil,
RequestQuery: new(metricsexplorertypes.MetricNameQuery),
RequestContentType: "",
Response: new(metricsexplorertypes.MetricMetadata),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusNotFound, http.StatusInternalServerError},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
}))
router.Post("/v1/o11y/metrics/metadata", handler.New(
provider.authzMiddleware.EditAccess(provider.metricsExplorerHandler.UpdateMetricMetadata),
handler.OpenAPIDef{
ID: "UpdateMetricMetadata",
Tags: []string{"metrics"},
Summary: "Update metric metadata",
Description: "This endpoint helps to update metadata information like metric description, unit, type, temporality, monotonicity for a specified metric",
Request: new(metricsexplorertypes.UpdateMetricMetadataRequest),
RequestContentType: "application/json",
Response: nil,
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusInternalServerError},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
}))
router.Get("/v1/o11y/metrics/highlights", handler.New(
provider.authzMiddleware.ViewAccess(provider.metricsExplorerHandler.GetMetricHighlights),
handler.OpenAPIDef{
ID: "GetMetricHighlights",
Tags: []string{"metrics"},
Summary: "Get metric highlights",
Description: "This endpoint returns highlights like number of datapoints, totaltimeseries, active time series, last received time for a specified metric",
Request: nil,
RequestQuery: new(metricsexplorertypes.MetricNameQuery),
RequestContentType: "",
Response: new(metricsexplorertypes.MetricHighlightsResponse),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusNotFound, http.StatusInternalServerError},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
}))
router.Get("/v1/o11y/metrics/alerts", handler.New(
provider.authzMiddleware.ViewAccess(provider.metricsExplorerHandler.GetMetricAlerts),
handler.OpenAPIDef{
ID: "GetMetricAlerts",
Tags: []string{"metrics"},
Summary: "Get metric alerts",
Description: "This endpoint returns associated alerts for a specified metric",
Request: nil,
RequestQuery: new(metricsexplorertypes.MetricNameQuery),
RequestContentType: "",
Response: new(metricsexplorertypes.MetricAlertsResponse),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusNotFound, http.StatusInternalServerError},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
}))
// GetMetricDashboards (formerly /api/v2/metrics/dashboards) is superseded by
// GetMetricDashboardsV2 below (formerly /api/v3) — highest version wins.
router.Get("/v1/o11y/metrics/dashboards", handler.New(
provider.authzMiddleware.ViewAccess(provider.metricsExplorerHandler.GetMetricDashboardsV2),
handler.OpenAPIDef{
ID: "GetMetricDashboardsV2",
Tags: []string{"metrics"},
Summary: "Get metric dashboards (v2)",
Description: "This endpoint returns associated v2 dashboards for a specified metric",
Request: nil,
RequestQuery: new(metricsexplorertypes.MetricNameQuery),
RequestContentType: "",
Response: new(metricsexplorertypes.MetricDashboardPanelsResponse),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusNotFound, http.StatusInternalServerError},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
}))
router.Post("/v1/o11y/metrics/inspect", handler.New(
provider.authzMiddleware.ViewAccess(provider.metricsExplorerHandler.InspectMetrics),
handler.OpenAPIDef{
ID: "InspectMetrics",
Tags: []string{"metrics"},
Summary: "Inspect raw metric data points",
Description: "Returns raw time series data points for a metric within a time range (max 30 minutes). Each series includes labels and timestamp/value pairs.",
Request: new(metricsexplorertypes.InspectMetricsRequest),
RequestContentType: "application/json",
Response: new(metricsexplorertypes.InspectMetricsResponse),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusInternalServerError},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
}))
router.Get("/v1/o11y/metrics/onboarding", handler.New(
provider.authzMiddleware.ViewAccess(provider.metricsExplorerHandler.GetOnboardingStatus),
handler.OpenAPIDef{
ID: "GetMetricsOnboardingStatus",
Tags: []string{"metrics"},
Summary: "Check if non-O11y metrics have been received",
Description: "Lightweight endpoint that checks if any non-O11y metrics have been ingested, used for onboarding status detection",
Request: nil,
RequestContentType: "",
Response: new(metricsexplorertypes.MetricsOnboardingResponse),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusUnauthorized, http.StatusInternalServerError},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
}))
}