mirror of
https://github.com/hanzoai/o11y.git
synced 2026-10-15 12:58:33 +00:00
The relay reads the operation zip served
An op learns its address from zip.OpOf and zip.AddressOf, so the claim table, the five declaring verbs and the address bookkeeping are gone. A host that declares one of these addresses itself now gets a Build conflict from zip. Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
@@ -42,30 +42,30 @@ import (
|
||||
// parameterised ones so an id can never shadow them — the same defence
|
||||
// telemetry.go gives its collection routes.
|
||||
func mountAccess(app *zip.App) {
|
||||
g := rooted{app.Group(o11yRoot), o11yRoot}
|
||||
g := app.Group(o11yRoot)
|
||||
|
||||
opPost(g, "/roles", createRole, op("CreateRole"), zip.WithStatus(http.StatusCreated))
|
||||
opGet(g, "/roles", listRoles, op("ListRoles"))
|
||||
opGet(g, "/roles/:id", getRole, op("GetRole"))
|
||||
opPut(g, "/roles/:id", updateRole, op("UpdateRole"), zip.WithStatus(http.StatusNoContent))
|
||||
opDelete(g, "/roles/:id", deleteRole, op("DeleteRole"), zip.WithStatus(http.StatusNoContent))
|
||||
g.Post("/roles", createRole, op("CreateRole"), zip.WithStatus(http.StatusCreated))
|
||||
g.Get("/roles", listRoles, op("ListRoles"))
|
||||
g.Get("/roles/:id", getRole, op("GetRole"))
|
||||
g.Put("/roles/:id", updateRole, op("UpdateRole"), zip.WithStatus(http.StatusNoContent))
|
||||
g.Delete("/roles/:id", deleteRole, op("DeleteRole"), zip.WithStatus(http.StatusNoContent))
|
||||
|
||||
opPost(g, "/service_accounts", createServiceAccount, op("CreateServiceAccount"), zip.WithStatus(http.StatusCreated))
|
||||
opGet(g, "/service_accounts", listServiceAccounts, op("ListServiceAccounts"))
|
||||
opGet(g, "/service_accounts/me", getMyServiceAccount, op("GetMyServiceAccount"))
|
||||
opPut(g, "/service_accounts/me", updateMyServiceAccount, op("UpdateMyServiceAccount"), zip.WithStatus(http.StatusNoContent))
|
||||
opGet(g, "/service_accounts/:id", getServiceAccount, op("GetServiceAccount"))
|
||||
opPut(g, "/service_accounts/:id", updateServiceAccount, op("UpdateServiceAccount"), zip.WithStatus(http.StatusNoContent))
|
||||
opDelete(g, "/service_accounts/:id", deleteServiceAccount, op("DeleteServiceAccount"), zip.WithStatus(http.StatusNoContent))
|
||||
opGet(g, "/service_accounts/:id/roles", listServiceAccountRoles, op("GetServiceAccountRoles"))
|
||||
opPost(g, "/service_accounts/:id/roles", grantServiceAccountRole, op("CreateServiceAccountRole"), zip.WithStatus(http.StatusNoContent))
|
||||
opDelete(g, "/service_accounts/:id/roles/:rid", revokeServiceAccountRole, op("DeleteServiceAccountRole"), zip.WithStatus(http.StatusNoContent))
|
||||
opPost(g, "/service_accounts/:id/keys", createServiceAccountKey, op("CreateServiceAccountKey"), zip.WithStatus(http.StatusCreated))
|
||||
opGet(g, "/service_accounts/:id/keys", listServiceAccountKeys, op("ListServiceAccountKeys"))
|
||||
opPut(g, "/service_accounts/:id/keys/:fid", updateServiceAccountKey, op("UpdateServiceAccountKey"), zip.WithStatus(http.StatusNoContent))
|
||||
opDelete(g, "/service_accounts/:id/keys/:fid", revokeServiceAccountKey, op("RevokeServiceAccountKey"), zip.WithStatus(http.StatusNoContent))
|
||||
g.Post("/service_accounts", createServiceAccount, op("CreateServiceAccount"), zip.WithStatus(http.StatusCreated))
|
||||
g.Get("/service_accounts", listServiceAccounts, op("ListServiceAccounts"))
|
||||
g.Get("/service_accounts/me", getMyServiceAccount, op("GetMyServiceAccount"))
|
||||
g.Put("/service_accounts/me", updateMyServiceAccount, op("UpdateMyServiceAccount"), zip.WithStatus(http.StatusNoContent))
|
||||
g.Get("/service_accounts/:id", getServiceAccount, op("GetServiceAccount"))
|
||||
g.Put("/service_accounts/:id", updateServiceAccount, op("UpdateServiceAccount"), zip.WithStatus(http.StatusNoContent))
|
||||
g.Delete("/service_accounts/:id", deleteServiceAccount, op("DeleteServiceAccount"), zip.WithStatus(http.StatusNoContent))
|
||||
g.Get("/service_accounts/:id/roles", listServiceAccountRoles, op("GetServiceAccountRoles"))
|
||||
g.Post("/service_accounts/:id/roles", grantServiceAccountRole, op("CreateServiceAccountRole"), zip.WithStatus(http.StatusNoContent))
|
||||
g.Delete("/service_accounts/:id/roles/:rid", revokeServiceAccountRole, op("DeleteServiceAccountRole"), zip.WithStatus(http.StatusNoContent))
|
||||
g.Post("/service_accounts/:id/keys", createServiceAccountKey, op("CreateServiceAccountKey"), zip.WithStatus(http.StatusCreated))
|
||||
g.Get("/service_accounts/:id/keys", listServiceAccountKeys, op("ListServiceAccountKeys"))
|
||||
g.Put("/service_accounts/:id/keys/:fid", updateServiceAccountKey, op("UpdateServiceAccountKey"), zip.WithStatus(http.StatusNoContent))
|
||||
g.Delete("/service_accounts/:id/keys/:fid", revokeServiceAccountKey, op("RevokeServiceAccountKey"), zip.WithStatus(http.StatusNoContent))
|
||||
|
||||
opPost(g, "/authz/check", checkAccess, op("AuthzCheck"))
|
||||
g.Post("/authz/check", checkAccess, op("AuthzCheck"))
|
||||
}
|
||||
|
||||
// ── roles ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
-84
@@ -1,84 +0,0 @@
|
||||
package o11y
|
||||
|
||||
// AN OPERATION'S ADDRESS IS A FACT OF ITS REGISTRATION, NOT AN ARGUMENT OF ITS
|
||||
// BODY.
|
||||
//
|
||||
// Every op in this table is declared at an address and then has to reach the
|
||||
// runtime that implements that address. Until now it reached it by saying the
|
||||
// address a second time, by hand, inside the handler:
|
||||
//
|
||||
// opGet(g, "/traces/:traceId", traceSpans) // the declaration
|
||||
// relay(ctx, "GET", o11yRoot+"/traces/"+in.TraceID, …) // the body
|
||||
//
|
||||
// Two spellings of one value, 366 times over, and the second one is what got
|
||||
// requested — so the first could stop agreeing with it and never be contradicted.
|
||||
// It has: three of these addresses declare {traceId} where the runtime registered
|
||||
// {traceID}. A router matches by POSITION, so a parameter's name is invisible to
|
||||
// it and the disagreement cost nothing until something wanted to look an address
|
||||
// up by name. That is exactly what the seam now does.
|
||||
//
|
||||
// So the address travels WITH the call. The declaration verbs (claim.go) wrap
|
||||
// every op in [addressed], which puts the address the op was registered at into
|
||||
// the context, and relay reads it there. The op's body states no method and no
|
||||
// path at all, which is the honest end state: which endpoint was called is not
|
||||
// something the body decides.
|
||||
//
|
||||
// The concrete path comes from [zip.Address], the inverse of the binding zip
|
||||
// already performed — zip put the matched segments INTO the input, so reading
|
||||
// them back out through the same rule cannot disagree with what arrived. The 119
|
||||
// hand-built concatenations it replaces each re-derived that by eye.
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zap-proto/zip"
|
||||
)
|
||||
|
||||
// addr is one operation's address: the method, the public TEMPLATE that names it
|
||||
// at the seam, and the CONCRETE path this particular call is for.
|
||||
//
|
||||
// Two spellings live here on purpose and they are two different values. The
|
||||
// template is the address's NAME — "/v1/o11y/traces/{traceId}" — the string the
|
||||
// document publishes and the string the runtime's registration recorded, so it is
|
||||
// what resolves a handler. The path is where THIS call is going —
|
||||
// "/v1/o11y/traces/abc-123" — so it is what the request carries.
|
||||
type addr struct {
|
||||
method string
|
||||
template string
|
||||
path string
|
||||
}
|
||||
|
||||
// addrKey names the context slot an op's address rides in. A zero-size unexported
|
||||
// type: unforgeable from outside this package, so nothing can plant an address an
|
||||
// op was not registered at.
|
||||
type addrKey struct{}
|
||||
|
||||
// addressed states an op's address once and carries it into the call.
|
||||
//
|
||||
// The template is computed at REGISTRATION — once per op for the life of the
|
||||
// process — because it is a property of the declaration. Only the concrete path
|
||||
// is per-call, because only it depends on the input.
|
||||
//
|
||||
// The full address is the target's own prefix plus the declared path, the same
|
||||
// composition [claims] reads and the same one zip's registry records, so the
|
||||
// three cannot name different addresses.
|
||||
func addressed[In, Out any](root, method, path string, fn zip.TypedHandler[In, Out]) zip.TypedHandler[In, Out] {
|
||||
pattern := root + path
|
||||
template := zip.Template(pattern)
|
||||
return func(ctx context.Context, in *In) (*Out, error) {
|
||||
return fn(context.WithValue(ctx, addrKey{}, addr{
|
||||
method: method,
|
||||
template: template,
|
||||
path: zip.Address(pattern, in),
|
||||
}), in)
|
||||
}
|
||||
}
|
||||
|
||||
// addressOf is the address of the op whose body is running, and whether there is
|
||||
// one. There is not exactly when an op's function is called directly rather than
|
||||
// through the declaration that registered it — a programming error, and one worth
|
||||
// naming rather than papering over with a guessed address.
|
||||
func addressOf(ctx context.Context) (addr, bool) {
|
||||
a, ok := ctx.Value(addrKey{}).(addr)
|
||||
return a, ok
|
||||
}
|
||||
@@ -40,36 +40,36 @@ import (
|
||||
// service catalog first, then the messaging-queue groups in the order the mux
|
||||
// tree declares them, then the third-party API overview.
|
||||
func mountAPM(app *zip.App) {
|
||||
g := rooted{app.Group(o11yRoot), o11yRoot}
|
||||
g := app.Group(o11yRoot)
|
||||
|
||||
// The service catalog: /services collection, /service/* breakdowns.
|
||||
opPost(g, "/services", services)
|
||||
opGet(g, "/services/list", serviceNames)
|
||||
opPost(g, "/service/top_operations", topOperations)
|
||||
opPost(g, "/service/top_level_operations", topLevelOperations)
|
||||
opPost(g, "/service/entry_point_operations", entryPointOperations)
|
||||
g.Post("/services", services)
|
||||
g.Get("/services/list", serviceNames)
|
||||
g.Post("/service/top_operations", topOperations)
|
||||
g.Post("/service/top_level_operations", topLevelOperations)
|
||||
g.Post("/service/entry_point_operations", entryPointOperations)
|
||||
|
||||
// The messaging-queue surface: the queue overview, then the Kafka
|
||||
// onboarding, partition-latency, consumer-lag, topic-throughput and
|
||||
// span-evaluation groups.
|
||||
opPost(g, "/messaging-queues/queue-overview", queueOverview)
|
||||
opPost(g, "/messaging-queues/kafka/onboarding/producers", producersOnboarding)
|
||||
opPost(g, "/messaging-queues/kafka/onboarding/consumers", consumersOnboarding)
|
||||
opPost(g, "/messaging-queues/kafka/onboarding/kafka", kafkaOnboarding)
|
||||
opPost(g, "/messaging-queues/kafka/partition-latency/overview", partitionLatency)
|
||||
opPost(g, "/messaging-queues/kafka/partition-latency/consumer", consumerPartitionLatency)
|
||||
opPost(g, "/messaging-queues/kafka/consumer-lag/producer-details", producerLagDetails)
|
||||
opPost(g, "/messaging-queues/kafka/consumer-lag/consumer-details", consumerLagDetails)
|
||||
opPost(g, "/messaging-queues/kafka/consumer-lag/network-latency", consumerLagNetwork)
|
||||
opPost(g, "/messaging-queues/kafka/topic-throughput/producer", producerThroughput)
|
||||
opPost(g, "/messaging-queues/kafka/topic-throughput/producer-details", producerThroughputDetails)
|
||||
opPost(g, "/messaging-queues/kafka/topic-throughput/consumer", consumerThroughput)
|
||||
opPost(g, "/messaging-queues/kafka/topic-throughput/consumer-details", consumerThroughputDetails)
|
||||
opPost(g, "/messaging-queues/kafka/span/evaluation", spanEvaluation)
|
||||
g.Post("/messaging-queues/queue-overview", queueOverview)
|
||||
g.Post("/messaging-queues/kafka/onboarding/producers", producersOnboarding)
|
||||
g.Post("/messaging-queues/kafka/onboarding/consumers", consumersOnboarding)
|
||||
g.Post("/messaging-queues/kafka/onboarding/kafka", kafkaOnboarding)
|
||||
g.Post("/messaging-queues/kafka/partition-latency/overview", partitionLatency)
|
||||
g.Post("/messaging-queues/kafka/partition-latency/consumer", consumerPartitionLatency)
|
||||
g.Post("/messaging-queues/kafka/consumer-lag/producer-details", producerLagDetails)
|
||||
g.Post("/messaging-queues/kafka/consumer-lag/consumer-details", consumerLagDetails)
|
||||
g.Post("/messaging-queues/kafka/consumer-lag/network-latency", consumerLagNetwork)
|
||||
g.Post("/messaging-queues/kafka/topic-throughput/producer", producerThroughput)
|
||||
g.Post("/messaging-queues/kafka/topic-throughput/producer-details", producerThroughputDetails)
|
||||
g.Post("/messaging-queues/kafka/topic-throughput/consumer", consumerThroughput)
|
||||
g.Post("/messaging-queues/kafka/topic-throughput/consumer-details", consumerThroughputDetails)
|
||||
g.Post("/messaging-queues/kafka/span/evaluation", spanEvaluation)
|
||||
|
||||
// The third-party API overview: external domains and one domain's detail.
|
||||
opPost(g, "/third-party-apis/overview/list", domainList)
|
||||
opPost(g, "/third-party-apis/overview/domain", domainInfo)
|
||||
g.Post("/third-party-apis/overview/list", domainList)
|
||||
g.Post("/third-party-apis/overview/domain", domainInfo)
|
||||
}
|
||||
|
||||
// ── the service catalog ───────────────────────────────────────────────────────
|
||||
|
||||
@@ -1,244 +0,0 @@
|
||||
package o11y
|
||||
|
||||
// A HOST MAY ALREADY SERVE AN ADDRESS THIS TABLE DECLARES, and when it does it
|
||||
// has to be able to say so.
|
||||
//
|
||||
// This module owns /v1/o11y as its own service address (HIP-0106) and, since the
|
||||
// conversion, declares every route under it by name — there is no wildcard left
|
||||
// for a host's own route to win against. That is the property that makes the
|
||||
// table honest, and it is also what turns a host's overlapping route from a
|
||||
// silent shadow into a refusal to compose: zip validates that one address is
|
||||
// declared once, so a host declaring GET /v1/o11y/metrics alongside this table
|
||||
// does not get precedence, it gets a panic.
|
||||
//
|
||||
// In-order shadowing used to make the overlap invisible. It was never a decision,
|
||||
// only an artefact of registration order, and the two handlers it hid could drift
|
||||
// apart forever without anyone being told. Making the host NAME the addresses it
|
||||
// takes is the same fact written down where it can be read.
|
||||
//
|
||||
// So the seam is a statement of ownership, not a filter: [Claimed] says "the host
|
||||
// serves these, this table does not declare them". Everything else is declared
|
||||
// exactly as before. An address named here that this table never declares is
|
||||
// inert rather than an error — the host is describing what IT serves, and a
|
||||
// table that stopped declaring an address should not turn into a boot failure in
|
||||
// every host that had claimed it.
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/zap-proto/zip"
|
||||
)
|
||||
|
||||
// Option configures one [Mount].
|
||||
type Option func(*conf)
|
||||
|
||||
// conf is one Mount's settings.
|
||||
type conf struct {
|
||||
// claimed holds "METHOD /path" for every address the host serves itself.
|
||||
claimed map[string]bool
|
||||
}
|
||||
|
||||
// Claimed names addresses the HOST serves itself, which this table therefore does
|
||||
// not declare. Each is a method and a path, spelled the way zip spells an address
|
||||
// in its own diagnostics — "GET /v1/o11y/metrics" — so the string that names the
|
||||
// conflict is the string that resolves it.
|
||||
//
|
||||
// The path is the FULL public path, not one relative to the mount root: a host
|
||||
// reads it off the route it registered, and an address is the same value on both
|
||||
// sides of this seam or it is not the same address.
|
||||
//
|
||||
// A claim is the host taking responsibility for that address. Nothing about the
|
||||
// runtime changes — the operation is still there, still reachable on the o11y
|
||||
// runtime's own router — but this table stops publishing it, so the host's
|
||||
// handler is the one and only answer at that address in the host's process.
|
||||
func Claimed(addrs ...string) Option {
|
||||
return func(c *conf) {
|
||||
if c.claimed == nil {
|
||||
c.claimed = make(map[string]bool, len(addrs))
|
||||
}
|
||||
for _, a := range addrs {
|
||||
c.claimed[a] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// mounting is the settings of the Mount currently running. Mount is a
|
||||
// composition-root call — once per process, before it serves — and every
|
||||
// declaration below happens inside it, so the set is written once and read
|
||||
// during that one call. It is nil at every other moment, which is what makes an
|
||||
// unclaimed mount cost nothing.
|
||||
var mounting *conf
|
||||
|
||||
// claims reports whether the host has taken the address this declaration would
|
||||
// register at.
|
||||
//
|
||||
// The full address is the target's own prefix plus the declared path, so this
|
||||
// reads the same whether an op is declared on a rooted group (the usual case,
|
||||
// where path is "/metrics") or straight on the app with a full path. The prefix
|
||||
// comes from [*zip.Group], the interface every declaration already goes
|
||||
// through, so nothing here needs to know which group it was handed.
|
||||
func claims(root, method, path string) bool {
|
||||
if mounting == nil || len(mounting.claimed) == 0 {
|
||||
return false
|
||||
}
|
||||
return mounting.claimed[method+" "+root+path]
|
||||
}
|
||||
|
||||
// under is the OpTarget that registers on app at a ROOT — the one place this
|
||||
// package's two public roots become route prefixes.
|
||||
//
|
||||
// It replaces rooted{app.Group(o11yRoot), o11yRoot}, and the difference is the operation ids.
|
||||
// zip qualifies an op's id by the prefix of the OCCURRENCE it answers under
|
||||
// (walk.go occurrenceID), because a definition included TWICE declares one id
|
||||
// and produces two operations, and an OpenAPI document cannot hold two
|
||||
// operations under one operationId. Sound rule — and o11yRoot is not that kind
|
||||
// of prefix. It is not a composition point a host chose; it is this service's
|
||||
// own address, fixed by HIP-0106 and spelled above. Reached through a Group it
|
||||
// read as one, and all 353 published ids became "v1.o11y.CreateRole" — renaming
|
||||
// every MCP tool, OpenAPI operationId, CLI command and generated SDK method in a
|
||||
// patch release.
|
||||
//
|
||||
// zip v1.24.2 fixed HALF of that: a DECLARED id (WithOperationID, or the op
|
||||
// helper below) now survives composition verbatim, so a Group can no longer
|
||||
// rename one. It is still only half. 217 of this package's 353 ops declare an
|
||||
// id; the other 136 take zip's shape-derived id, and that half IS still
|
||||
// qualified by the occurrence's prefix — deterministically and correctly, since
|
||||
// an undeclared id carries no promise to keep. Reached through a Group those 136
|
||||
// would publish as "v1.o11y.get_v1_o11y_…" instead of "get_v1_o11y_…", which is
|
||||
// the same rename in a smaller font. So this type stays until the count is 353,
|
||||
// and TestUnderIsStillLoadBearing measures it rather than trusting this
|
||||
// sentence.
|
||||
//
|
||||
// An occurrence at the ROOT prefix is unqualified, so this registers the ops on
|
||||
// app itself and lets OpScope.Prefix do what it documents — prepend to the op's
|
||||
// path. Same method, same full path, same middleware; the id is the one the
|
||||
// declaration wrote. That is also what mount.go's PATH UNTOUCHED already claimed
|
||||
// and what mountHealth already does by hand, so the Group was the odd one out.
|
||||
//
|
||||
// The middleware comes from app's own scope rather than being zeroed: Group
|
||||
// inherits the app's wrap, and dropping it here would silently unwrap every
|
||||
// typed op.
|
||||
|
||||
// rooted is a group together with the path it answers under. The group does the
|
||||
// registering; the root is what an address is measured from.
|
||||
type rooted struct {
|
||||
*zip.Group
|
||||
root string
|
||||
}
|
||||
|
||||
// The five declaration verbs, each the zip one plus the claim check. Every typed
|
||||
// op in this package goes through these rather than through zip directly, which
|
||||
// is what makes a claim mean the same thing at all 358 declarations instead of
|
||||
// at the handful someone remembered.
|
||||
//
|
||||
// They exist because the check needs the ADDRESS, and an address is the target's
|
||||
// prefix plus the path — known only at the declaration, not at the target. zip
|
||||
// resolves a target once, without a path, so a target cannot refuse one address
|
||||
// and accept another; the verb can.
|
||||
|
||||
func opGet[In, Out any](on rooted, path string, fn zip.TypedHandler[In, Out], opts ...zip.OpOption) {
|
||||
if claims(on.root, http.MethodGet, path) {
|
||||
return
|
||||
}
|
||||
on.Get(path, addressed(on.root, http.MethodGet, path, fn), opts...)
|
||||
}
|
||||
|
||||
func opPost[In, Out any](on rooted, path string, fn zip.TypedHandler[In, Out], opts ...zip.OpOption) {
|
||||
if claims(on.root, http.MethodPost, path) {
|
||||
return
|
||||
}
|
||||
on.Post(path, addressed(on.root, http.MethodPost, path, fn), opts...)
|
||||
}
|
||||
|
||||
func opPut[In, Out any](on rooted, path string, fn zip.TypedHandler[In, Out], opts ...zip.OpOption) {
|
||||
if claims(on.root, http.MethodPut, path) {
|
||||
return
|
||||
}
|
||||
on.Put(path, addressed(on.root, http.MethodPut, path, fn), opts...)
|
||||
}
|
||||
|
||||
func opPatch[In, Out any](on rooted, path string, fn zip.TypedHandler[In, Out], opts ...zip.OpOption) {
|
||||
if claims(on.root, http.MethodPatch, path) {
|
||||
return
|
||||
}
|
||||
on.Patch(path, addressed(on.root, http.MethodPatch, path, fn), opts...)
|
||||
}
|
||||
|
||||
func opDelete[In, Out any](on rooted, path string, fn zip.TypedHandler[In, Out], opts ...zip.OpOption) {
|
||||
if claims(on.root, http.MethodDelete, path) {
|
||||
return
|
||||
}
|
||||
on.Delete(path, addressed(on.root, http.MethodDelete, path, fn), opts...)
|
||||
}
|
||||
|
||||
// route declares one of the ten escape hatches — a raw route rather than a
|
||||
// typed op (see mountHatches) — and honours a claim the same way. A hatch is
|
||||
// still an address, so a host has to be able to take one.
|
||||
//
|
||||
// It takes no handler, because a hatch's handler is the same fact a typed op's
|
||||
// is: the runtime's answer at that address. Naming the address is the whole
|
||||
// registration.
|
||||
func route(app *zip.App, method, path string) {
|
||||
// A hatch names its whole address, so there is no root to measure from.
|
||||
if claims("", method, path) {
|
||||
return
|
||||
}
|
||||
switch method {
|
||||
case http.MethodGet, http.MethodPost:
|
||||
default:
|
||||
panic("o11y: route: unsupported method " + method)
|
||||
}
|
||||
app.Raw(method, path, hatch(method, zip.Template(path)))
|
||||
}
|
||||
|
||||
// bridge is this package's ONE net/http adapter: every route that answers with a
|
||||
// handler the HOST installed is registered through here. (A typed op does not
|
||||
// come this way — relay drives the same handler with a recorder, from inside the
|
||||
// op, and never touches the router.)
|
||||
//
|
||||
// What it adapts is net/http on the far side by construction: a runtime resolves
|
||||
// to an http.Handler ([Runtime]), and a health handler is three
|
||||
// http.HandlerFuncs (factory.Handler). Neither is a value this package could ask
|
||||
// for natively — both are handed in by the host — so the request has to be
|
||||
// rendered as an http.Request once, here, to reach either.
|
||||
//
|
||||
// pick runs per request because both callers choose late: a hatch reads the
|
||||
// runtime SetRuntime installed, a probe the handler SetHealth did, and either can
|
||||
// change while the process serves. Only the CHOICE is late — the adapter itself
|
||||
// is built once, at registration. Building it per request is what this replaces:
|
||||
// probe called zip.AdaptNetHTTP inside its own handler, so every liveness poll
|
||||
// allocated an adapter and wrote zip's terminal marker into a process-wide map
|
||||
// again, to answer a request whose shape has not changed since boot.
|
||||
func bridge(pick func() http.Handler) zip.Handler {
|
||||
return zip.AdaptNetHTTP(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
pick().ServeHTTP(w, r)
|
||||
}))
|
||||
}
|
||||
|
||||
// answer is the runtime's handler for one address, or the refusal that says
|
||||
// which of the two reasons it has none. The refusal is net/http's own shape —
|
||||
// text/plain and nosniff — because it is written on the net/http side of the
|
||||
// bridge, and health_test.go's table pins it that way.
|
||||
func answer(method, template string) http.Handler {
|
||||
h, err := at(method, template)
|
||||
if err != nil {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, err.Error(), http.StatusServiceUnavailable)
|
||||
})
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
// hatch serves ONE address by handing the request straight to the runtime's
|
||||
// handler for it — no recorder, no decode, no re-encode.
|
||||
//
|
||||
// That pass-through is the point. Every route registered this way is one whose
|
||||
// answer cannot be named: an unbounded log tail, a long poll, a chunked export, a
|
||||
// 303 whose whole content is a Location, a Sentry envelope frame. relay buffers a
|
||||
// complete answer before it can decode one, so a typed op would hang on the first
|
||||
// tail and would return a progress report only after the query it reports on had
|
||||
// finished. Here the runtime's handler writes to the caller's own
|
||||
// ResponseWriter, so a stream stays a stream and a header stays a header.
|
||||
func hatch(method, template string) zip.Handler {
|
||||
return bridge(func() http.Handler { return answer(method, template) })
|
||||
}
|
||||
-131
@@ -1,131 +0,0 @@
|
||||
package o11y_test
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/hanzoai/o11y"
|
||||
"github.com/zap-proto/zip"
|
||||
)
|
||||
|
||||
// What a claim has to be true of: the claimed address is NOT declared, every
|
||||
// other address still is, and a host that claims nothing sees the whole table.
|
||||
// A seam that skipped more than it was asked to would delete operations
|
||||
// silently, which is the failure the wildcard used to hide.
|
||||
|
||||
// declaredAt reports whether this METHOD and path is in the table. With no
|
||||
// runtime behind it a declared route answers 503
|
||||
// (TestNamedRouteWithoutRuntimeReturns503), so the two refusals below are
|
||||
// unambiguous — neither can be a handler's own answer.
|
||||
//
|
||||
// Both refusals count as "not declared", and the 405 is the interesting one: an
|
||||
// address is a method AND a path, so claiming POST /query_range while GET
|
||||
// /query_range stays declared leaves the PATH routable and the method not. The
|
||||
// router says 405 to that, not 404, and reading only 404 would have called the
|
||||
// claim a failure when it had done exactly what it was asked.
|
||||
func declaredAt(t *testing.T, app *zip.App, method, target string) bool {
|
||||
t.Helper()
|
||||
resp, err := app.Test(httptest.NewRequest(method, target, nil))
|
||||
if err != nil {
|
||||
t.Fatalf("Test %s %s: %v", method, target, err)
|
||||
}
|
||||
return resp.StatusCode != http.StatusNotFound && resp.StatusCode != http.StatusMethodNotAllowed
|
||||
}
|
||||
|
||||
// mountClaiming builds an app with the given claims. With no runtime installed,
|
||||
// a declared address answers 503 rather than reaching one — which is what lets
|
||||
// this file read "declared" off the answer without standing a runtime up.
|
||||
func mountClaiming(t *testing.T, opts ...o11y.Option) *zip.App {
|
||||
t.Helper()
|
||||
app := zip.New(zip.Config{DisableStartupMessage: true})
|
||||
if err := o11y.Use(app, opts...); err != nil {
|
||||
t.Fatalf("Mount: %v", err)
|
||||
}
|
||||
o11y.SetRuntime(nil)
|
||||
return app
|
||||
}
|
||||
|
||||
// The whole table is declared when nothing is claimed. This is the control: it
|
||||
// is what makes the absences in the next test attributable to the claim rather
|
||||
// than to the address never having been there.
|
||||
func TestUnclaimedMountDeclaresEverything(t *testing.T) {
|
||||
app := mountClaiming(t)
|
||||
for _, a := range []struct{ method, path string }{
|
||||
{http.MethodGet, "/v1/o11y/logs"},
|
||||
{http.MethodGet, "/v1/o11y/metrics"},
|
||||
{http.MethodPost, "/v1/o11y/query_range"},
|
||||
{http.MethodGet, "/v1/o11y/logs/livetail"}, // a hatch
|
||||
} {
|
||||
if !declaredAt(t, app, a.method, a.path) {
|
||||
t.Errorf("%s %s is not declared by an unclaimed Mount", a.method, a.path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A claimed address is not declared, and its NEIGHBOURS are untouched. The
|
||||
// neighbours are the point: /metrics and /metrics/stats differ by one segment,
|
||||
// and a seam that matched by prefix would take the second one with the first.
|
||||
func TestClaimedAddressIsNotDeclared(t *testing.T) {
|
||||
app := mountClaiming(t, o11y.Claimed(
|
||||
"GET /v1/o11y/metrics",
|
||||
"POST /v1/o11y/query_range",
|
||||
))
|
||||
|
||||
for _, a := range []struct{ method, path string }{
|
||||
{http.MethodGet, "/v1/o11y/metrics"},
|
||||
{http.MethodPost, "/v1/o11y/query_range"},
|
||||
} {
|
||||
if declaredAt(t, app, a.method, a.path) {
|
||||
t.Errorf("%s %s was claimed by the host but is still declared", a.method, a.path)
|
||||
}
|
||||
}
|
||||
|
||||
// Everything the claim did NOT name is still there: the sibling under the
|
||||
// claimed path, the same path under a different METHOD, and an unrelated op.
|
||||
for _, a := range []struct{ method, path string }{
|
||||
{http.MethodPost, "/v1/o11y/metrics/stats"}, // sibling below a claimed path
|
||||
{http.MethodGet, "/v1/o11y/query_range"}, // same path, method not claimed
|
||||
{http.MethodPost, "/v1/o11y/query_range/format"}, // sibling below a claimed path
|
||||
{http.MethodGet, "/v1/o11y/logs"}, // unrelated op
|
||||
} {
|
||||
if !declaredAt(t, app, a.method, a.path) {
|
||||
t.Errorf("%s %s was NOT claimed but the claim took it anyway", a.method, a.path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A hatch is an address too, so a host can take one. The hatches are raw routes
|
||||
// rather than typed ops, and they went through a different verb — this is what
|
||||
// keeps the seam from being true of 353 addresses and false of ten.
|
||||
func TestClaimedHatchIsNotDeclared(t *testing.T) {
|
||||
app := mountClaiming(t, o11y.Claimed("GET /v1/o11y/logs/livetail"))
|
||||
if declaredAt(t, app, http.MethodGet, "/v1/o11y/logs/livetail") {
|
||||
t.Error("GET /v1/o11y/logs/livetail was claimed but is still declared")
|
||||
}
|
||||
if !declaredAt(t, app, http.MethodGet, "/v1/o11y/query_progress") {
|
||||
t.Error("an unclaimed hatch stopped being declared")
|
||||
}
|
||||
}
|
||||
|
||||
// A claim for an address this table does not declare is INERT, not an error. The
|
||||
// host is describing what it serves; a table that stopped declaring an address
|
||||
// must not turn into a boot failure in every host that had claimed it.
|
||||
func TestClaimForAnUnknownAddressIsInert(t *testing.T) {
|
||||
app := mountClaiming(t, o11y.Claimed("GET /v1/o11y/no-such-route", "DELETE /v1/o11y/logs"))
|
||||
if !declaredAt(t, app, http.MethodGet, "/v1/o11y/logs") {
|
||||
t.Error("a claim on a method this table does not declare removed the one it does")
|
||||
}
|
||||
}
|
||||
|
||||
// The claim set does not outlive its Mount. It is read by the declaration verbs
|
||||
// during the call, so a second Mount with no options must see the whole table
|
||||
// again — otherwise one host's claim would leak into the next app in the same
|
||||
// process, which is exactly what a test binary does.
|
||||
func TestClaimDoesNotLeakToTheNextMount(t *testing.T) {
|
||||
_ = mountClaiming(t, o11y.Claimed("GET /v1/o11y/metrics"))
|
||||
next := mountClaiming(t)
|
||||
if !declaredAt(t, next, http.MethodGet, "/v1/o11y/metrics") {
|
||||
t.Error("the previous Mount's claim leaked into a later, unclaimed Mount")
|
||||
}
|
||||
}
|
||||
+23
-23
@@ -55,41 +55,41 @@ import (
|
||||
// carries the operation id its mux registration always declared, so the document
|
||||
// names it exactly as it was named before.
|
||||
func mountDashboards(app *zip.App) {
|
||||
g := rooted{app.Group(o11yRoot), o11yRoot}
|
||||
g := app.Group(o11yRoot)
|
||||
|
||||
// Dashboards — list, per-user list, create, clone, read, update, patch, delete.
|
||||
opGet(g, "/dashboards", dashboardListV2, zip.WithOperationID("ListDashboardsV2"))
|
||||
opGet(g, "/users/me/dashboards", dashboardListForUserV2, zip.WithOperationID("ListDashboardsForUserV2"))
|
||||
opPost(g, "/dashboards", dashboardCreateV2, zip.WithStatus(http.StatusCreated), zip.WithOperationID("CreateDashboardV2"))
|
||||
opPost(g, "/dashboards/:id/clone", dashboardCloneV2, zip.WithStatus(http.StatusCreated), zip.WithOperationID("CloneDashboardV2"))
|
||||
opGet(g, "/dashboards/:id", dashboardGetV2, zip.WithOperationID("GetDashboardV2"))
|
||||
opPut(g, "/dashboards/:id", dashboardUpdateV2, zip.WithOperationID("UpdateDashboardV2"))
|
||||
opPatch(g, "/dashboards/:id", dashboardPatchV2, zip.WithOperationID("PatchDashboardV2"))
|
||||
opDelete(g, "/dashboards/:id", dashboardDeleteV2, zip.WithOperationID("DeleteDashboardV2"))
|
||||
g.Get("/dashboards", dashboardListV2, zip.WithOperationID("ListDashboardsV2"))
|
||||
g.Get("/users/me/dashboards", dashboardListForUserV2, zip.WithOperationID("ListDashboardsForUserV2"))
|
||||
g.Post("/dashboards", dashboardCreateV2, zip.WithStatus(http.StatusCreated), zip.WithOperationID("CreateDashboardV2"))
|
||||
g.Post("/dashboards/:id/clone", dashboardCloneV2, zip.WithStatus(http.StatusCreated), zip.WithOperationID("CloneDashboardV2"))
|
||||
g.Get("/dashboards/:id", dashboardGetV2, zip.WithOperationID("GetDashboardV2"))
|
||||
g.Put("/dashboards/:id", dashboardUpdateV2, zip.WithOperationID("UpdateDashboardV2"))
|
||||
g.Patch("/dashboards/:id", dashboardPatchV2, zip.WithOperationID("PatchDashboardV2"))
|
||||
g.Delete("/dashboards/:id", dashboardDeleteV2, zip.WithOperationID("DeleteDashboardV2"))
|
||||
|
||||
// Lock / unlock — only the creator or an org admin; the runtime decides.
|
||||
opPut(g, "/dashboards/:id/lock", dashboardLockV2, zip.WithOperationID("LockDashboardV2"))
|
||||
opDelete(g, "/dashboards/:id/lock", dashboardUnlockV2, zip.WithOperationID("UnlockDashboardV2"))
|
||||
g.Put("/dashboards/:id/lock", dashboardLockV2, zip.WithOperationID("LockDashboardV2"))
|
||||
g.Delete("/dashboards/:id/lock", dashboardUnlockV2, zip.WithOperationID("UnlockDashboardV2"))
|
||||
|
||||
// Per-user pins — a viewer bookmark, mutating only the caller's pin list.
|
||||
opPut(g, "/users/me/dashboards/:id/pins", dashboardPinV2, zip.WithOperationID("PinDashboardV2"))
|
||||
opDelete(g, "/users/me/dashboards/:id/pins", dashboardUnpinV2, zip.WithOperationID("UnpinDashboardV2"))
|
||||
g.Put("/users/me/dashboards/:id/pins", dashboardPinV2, zip.WithOperationID("PinDashboardV2"))
|
||||
g.Delete("/users/me/dashboards/:id/pins", dashboardUnpinV2, zip.WithOperationID("UnpinDashboardV2"))
|
||||
|
||||
// Saved views — org-shared dashboard-listing state.
|
||||
opGet(g, "/dashboard_views", dashboardListViews, zip.WithOperationID("ListDashboardViews"))
|
||||
opPost(g, "/dashboard_views", dashboardCreateView, zip.WithStatus(http.StatusCreated), zip.WithOperationID("CreateDashboardView"))
|
||||
opPut(g, "/dashboard_views/:id", dashboardUpdateView, zip.WithOperationID("UpdateDashboardView"))
|
||||
opDelete(g, "/dashboard_views/:id", dashboardDeleteView, zip.WithOperationID("DeleteDashboardView"))
|
||||
g.Get("/dashboard_views", dashboardListViews, zip.WithOperationID("ListDashboardViews"))
|
||||
g.Post("/dashboard_views", dashboardCreateView, zip.WithStatus(http.StatusCreated), zip.WithOperationID("CreateDashboardView"))
|
||||
g.Put("/dashboard_views/:id", dashboardUpdateView, zip.WithOperationID("UpdateDashboardView"))
|
||||
g.Delete("/dashboard_views/:id", dashboardDeleteView, zip.WithOperationID("DeleteDashboardView"))
|
||||
|
||||
// Public sharing config — org-admin only.
|
||||
opPost(g, "/dashboards/:id/public", dashboardCreatePublic, zip.WithStatus(http.StatusCreated), zip.WithOperationID("CreatePublicDashboard"))
|
||||
opGet(g, "/dashboards/:id/public", dashboardGetPublic, zip.WithOperationID("GetPublicDashboard"))
|
||||
opPut(g, "/dashboards/:id/public", dashboardUpdatePublic, zip.WithOperationID("UpdatePublicDashboard"))
|
||||
opDelete(g, "/dashboards/:id/public", dashboardDeletePublic, zip.WithOperationID("DeletePublicDashboard"))
|
||||
g.Post("/dashboards/:id/public", dashboardCreatePublic, zip.WithStatus(http.StatusCreated), zip.WithOperationID("CreatePublicDashboard"))
|
||||
g.Get("/dashboards/:id/public", dashboardGetPublic, zip.WithOperationID("GetPublicDashboard"))
|
||||
g.Put("/dashboards/:id/public", dashboardUpdatePublic, zip.WithOperationID("UpdatePublicDashboard"))
|
||||
g.Delete("/dashboards/:id/public", dashboardDeletePublic, zip.WithOperationID("DeletePublicDashboard"))
|
||||
|
||||
// Anonymous public reads — scoped to the public dashboard's read scope.
|
||||
opGet(g, "/public/dashboards/:id", dashboardGetPublicData, zip.WithOperationID("GetPublicDashboardData"))
|
||||
opGet(g, "/public/dashboards/:id/widgets/:idx/query_range", dashboardGetPublicWidgetQueryRange, zip.WithOperationID("GetPublicDashboardWidgetQueryRange"))
|
||||
g.Get("/public/dashboards/:id", dashboardGetPublicData, zip.WithOperationID("GetPublicDashboardData"))
|
||||
g.Get("/public/dashboards/:id/widgets/:idx/query_range", dashboardGetPublicWidgetQueryRange, zip.WithOperationID("GetPublicDashboardWidgetQueryRange"))
|
||||
}
|
||||
|
||||
// ── the twenty-two operations ───────────────────────────────────────────────────
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
package o11y
|
||||
|
||||
// The routes that cannot be typed ops, and the net/http seam one is reached
|
||||
// through. A hatch hands the request to the runtime's handler for its own
|
||||
// address, unbuffered, so a stream stays a stream.
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/zap-proto/zip"
|
||||
)
|
||||
|
||||
// route registers one hatch at its full public address. See mountHatches for
|
||||
// why each of them cannot be a typed op.
|
||||
func route(app *zip.App, method, path string) {
|
||||
app.Raw(method, path, hatch(method, zip.Template(path)))
|
||||
}
|
||||
|
||||
// bridge is this package's ONE net/http adapter: every route that answers with a
|
||||
// handler the HOST installed is registered through here. (A typed op does not
|
||||
// come this way — relay drives the same handler with a recorder, from inside the
|
||||
// op, and never touches the router.)
|
||||
//
|
||||
// What it adapts is net/http on the far side by construction: a runtime resolves
|
||||
// to an http.Handler ([Runtime]), and a health handler is three
|
||||
// http.HandlerFuncs (factory.Handler). Neither is a value this package could ask
|
||||
// for natively — both are handed in by the host — so the request has to be
|
||||
// rendered as an http.Request once, here, to reach either.
|
||||
//
|
||||
// pick runs per request because both callers choose late: a hatch reads the
|
||||
// runtime SetRuntime installed, a probe the handler SetHealth did, and either can
|
||||
// change while the process serves. Only the CHOICE is late — the adapter itself
|
||||
// is built once, at registration. Building it per request is what this replaces:
|
||||
// probe called zip.AdaptNetHTTP inside its own handler, so every liveness poll
|
||||
// allocated an adapter and wrote zip's terminal marker into a process-wide map
|
||||
// again, to answer a request whose shape has not changed since boot.
|
||||
func bridge(pick func() http.Handler) zip.Handler {
|
||||
return zip.AdaptNetHTTP(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
pick().ServeHTTP(w, r)
|
||||
}))
|
||||
}
|
||||
|
||||
// answer is the runtime's handler for one address, or the refusal that says
|
||||
// which of the two reasons it has none. The refusal is net/http's own shape —
|
||||
// text/plain and nosniff — because it is written on the net/http side of the
|
||||
// bridge, and health_test.go's table pins it that way.
|
||||
func answer(method, template string) http.Handler {
|
||||
h, err := at(method, template)
|
||||
if err != nil {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, err.Error(), http.StatusServiceUnavailable)
|
||||
})
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
// hatch serves ONE address by handing the request straight to the runtime's
|
||||
// handler for it — no recorder, no decode, no re-encode.
|
||||
//
|
||||
// That pass-through is the point. Every route registered this way is one whose
|
||||
// answer cannot be named: an unbounded log tail, a long poll, a chunked export, a
|
||||
// 303 whose whole content is a Location, a Sentry envelope frame. relay buffers a
|
||||
// complete answer before it can decode one, so a typed op would hang on the first
|
||||
// tail and would return a progress report only after the query it reports on had
|
||||
// finished. Here the runtime's handler writes to the caller's own
|
||||
// ResponseWriter, so a stream stays a stream and a header stays a header.
|
||||
func hatch(method, template string) zip.Handler {
|
||||
return bridge(func() http.Handler { return answer(method, template) })
|
||||
}
|
||||
@@ -59,7 +59,7 @@ func mountHealth(app *zip.App) {
|
||||
}
|
||||
|
||||
// The fall-through is the runtime's handler for the probe's own address, reached
|
||||
// through the same hatch every un-typed route uses (claim.go).
|
||||
// through the same hatch every un-typed route uses (hatch.go).
|
||||
//
|
||||
// It used to be c.Next(), which worked only because a /v1/o11y/* catch-all was
|
||||
// registered after the probes and caught whatever they declined. Every route is
|
||||
|
||||
+13
-13
@@ -54,25 +54,25 @@ import (
|
||||
|
||||
// mountIdentity registers the identity face's typed ops on the native router.
|
||||
func mountIdentity(app *zip.App) {
|
||||
g := rooted{app.Group(o11yRoot), o11yRoot}
|
||||
g := app.Group(o11yRoot)
|
||||
|
||||
// who the caller is
|
||||
opGet(g, "/users/me", getMyUser, zip.WithOperationID("GetMyUser"))
|
||||
g.Get("/users/me", getMyUser, zip.WithOperationID("GetMyUser"))
|
||||
|
||||
// my organization and its quick filters
|
||||
opGet(g, "/orgs/me", getMyOrg, zip.WithOperationID("GetMyOrganization"))
|
||||
opPut(g, "/orgs/me", updateMyOrg, zip.WithOperationID("UpdateMyOrganization"))
|
||||
opGet(g, "/orgs/me/filters", getQuickFilters, zip.WithOperationID("GetQuickFilters"))
|
||||
opGet(g, "/orgs/me/filters/:signal", getSignalFilters, zip.WithOperationID("GetSignalFilters"))
|
||||
opPut(g, "/orgs/me/filters", updateQuickFilters, zip.WithOperationID("UpdateQuickFilters"))
|
||||
g.Get("/orgs/me", getMyOrg, zip.WithOperationID("GetMyOrganization"))
|
||||
g.Put("/orgs/me", updateMyOrg, zip.WithOperationID("UpdateMyOrganization"))
|
||||
g.Get("/orgs/me/filters", getQuickFilters, zip.WithOperationID("GetQuickFilters"))
|
||||
g.Get("/orgs/me/filters/:signal", getSignalFilters, zip.WithOperationID("GetSignalFilters"))
|
||||
g.Put("/orgs/me/filters", updateQuickFilters, zip.WithOperationID("UpdateQuickFilters"))
|
||||
|
||||
// preferences, per user and per org
|
||||
opGet(g, "/user/preferences", listUserPreferences, zip.WithOperationID("ListUserPreferences"))
|
||||
opGet(g, "/user/preferences/:name", getUserPreference, zip.WithOperationID("GetUserPreference"))
|
||||
opPut(g, "/user/preferences/:name", updateUserPreference, zip.WithOperationID("UpdateUserPreference"))
|
||||
opGet(g, "/org/preferences", listOrgPreferences, zip.WithOperationID("ListOrgPreferences"))
|
||||
opGet(g, "/org/preferences/:name", getOrgPreference, zip.WithOperationID("GetOrgPreference"))
|
||||
opPut(g, "/org/preferences/:name", updateOrgPreference, zip.WithOperationID("UpdateOrgPreference"))
|
||||
g.Get("/user/preferences", listUserPreferences, zip.WithOperationID("ListUserPreferences"))
|
||||
g.Get("/user/preferences/:name", getUserPreference, zip.WithOperationID("GetUserPreference"))
|
||||
g.Put("/user/preferences/:name", updateUserPreference, zip.WithOperationID("UpdateUserPreference"))
|
||||
g.Get("/org/preferences", listOrgPreferences, zip.WithOperationID("ListOrgPreferences"))
|
||||
g.Get("/org/preferences/:name", getOrgPreference, zip.WithOperationID("GetOrgPreference"))
|
||||
g.Put("/org/preferences/:name", updateOrgPreference, zip.WithOperationID("UpdateOrgPreference"))
|
||||
}
|
||||
|
||||
// ── who the caller is ─────────────────────────────────────────────────────────
|
||||
|
||||
@@ -44,63 +44,63 @@ import (
|
||||
// eleven infra resources, the ten infra_monitoring rollups, and the setup
|
||||
// checks.
|
||||
func mountInfra(app *zip.App) {
|
||||
g := rooted{app.Group(o11yRoot), o11yRoot}
|
||||
g := app.Group(o11yRoot)
|
||||
|
||||
opGet(g, "/hosts/attribute_keys", hostAttributeKeys)
|
||||
opGet(g, "/hosts/attribute_values", hostAttributeValues)
|
||||
opPost(g, "/hosts/list", hostList)
|
||||
g.Get("/hosts/attribute_keys", hostAttributeKeys)
|
||||
g.Get("/hosts/attribute_values", hostAttributeValues)
|
||||
g.Post("/hosts/list", hostList)
|
||||
|
||||
opGet(g, "/processes/attribute_keys", processAttributeKeys)
|
||||
opGet(g, "/processes/attribute_values", processAttributeValues)
|
||||
opPost(g, "/processes/list", processList)
|
||||
g.Get("/processes/attribute_keys", processAttributeKeys)
|
||||
g.Get("/processes/attribute_values", processAttributeValues)
|
||||
g.Post("/processes/list", processList)
|
||||
|
||||
opGet(g, "/pods/attribute_keys", podAttributeKeys)
|
||||
opGet(g, "/pods/attribute_values", podAttributeValues)
|
||||
opPost(g, "/pods/list", podList)
|
||||
g.Get("/pods/attribute_keys", podAttributeKeys)
|
||||
g.Get("/pods/attribute_values", podAttributeValues)
|
||||
g.Post("/pods/list", podList)
|
||||
|
||||
opGet(g, "/pvcs/attribute_keys", pvcAttributeKeys)
|
||||
opGet(g, "/pvcs/attribute_values", pvcAttributeValues)
|
||||
opPost(g, "/pvcs/list", pvcList)
|
||||
g.Get("/pvcs/attribute_keys", pvcAttributeKeys)
|
||||
g.Get("/pvcs/attribute_values", pvcAttributeValues)
|
||||
g.Post("/pvcs/list", pvcList)
|
||||
|
||||
opGet(g, "/nodes/attribute_keys", nodeAttributeKeys)
|
||||
opGet(g, "/nodes/attribute_values", nodeAttributeValues)
|
||||
opPost(g, "/nodes/list", nodeList)
|
||||
g.Get("/nodes/attribute_keys", nodeAttributeKeys)
|
||||
g.Get("/nodes/attribute_values", nodeAttributeValues)
|
||||
g.Post("/nodes/list", nodeList)
|
||||
|
||||
opGet(g, "/namespaces/attribute_keys", namespaceAttributeKeys)
|
||||
opGet(g, "/namespaces/attribute_values", namespaceAttributeValues)
|
||||
opPost(g, "/namespaces/list", namespaceList)
|
||||
g.Get("/namespaces/attribute_keys", namespaceAttributeKeys)
|
||||
g.Get("/namespaces/attribute_values", namespaceAttributeValues)
|
||||
g.Post("/namespaces/list", namespaceList)
|
||||
|
||||
opGet(g, "/clusters/attribute_keys", clusterAttributeKeys)
|
||||
opGet(g, "/clusters/attribute_values", clusterAttributeValues)
|
||||
opPost(g, "/clusters/list", clusterList)
|
||||
g.Get("/clusters/attribute_keys", clusterAttributeKeys)
|
||||
g.Get("/clusters/attribute_values", clusterAttributeValues)
|
||||
g.Post("/clusters/list", clusterList)
|
||||
|
||||
opGet(g, "/deployments/attribute_keys", deploymentAttributeKeys)
|
||||
opGet(g, "/deployments/attribute_values", deploymentAttributeValues)
|
||||
opPost(g, "/deployments/list", deploymentList)
|
||||
g.Get("/deployments/attribute_keys", deploymentAttributeKeys)
|
||||
g.Get("/deployments/attribute_values", deploymentAttributeValues)
|
||||
g.Post("/deployments/list", deploymentList)
|
||||
|
||||
opGet(g, "/daemonsets/attribute_keys", daemonSetAttributeKeys)
|
||||
opGet(g, "/daemonsets/attribute_values", daemonSetAttributeValues)
|
||||
opPost(g, "/daemonsets/list", daemonSetList)
|
||||
g.Get("/daemonsets/attribute_keys", daemonSetAttributeKeys)
|
||||
g.Get("/daemonsets/attribute_values", daemonSetAttributeValues)
|
||||
g.Post("/daemonsets/list", daemonSetList)
|
||||
|
||||
opGet(g, "/statefulsets/attribute_keys", statefulSetAttributeKeys)
|
||||
opGet(g, "/statefulsets/attribute_values", statefulSetAttributeValues)
|
||||
opPost(g, "/statefulsets/list", statefulSetList)
|
||||
g.Get("/statefulsets/attribute_keys", statefulSetAttributeKeys)
|
||||
g.Get("/statefulsets/attribute_values", statefulSetAttributeValues)
|
||||
g.Post("/statefulsets/list", statefulSetList)
|
||||
|
||||
opGet(g, "/jobs/attribute_keys", jobAttributeKeys)
|
||||
opGet(g, "/jobs/attribute_values", jobAttributeValues)
|
||||
opPost(g, "/jobs/list", jobList)
|
||||
g.Get("/jobs/attribute_keys", jobAttributeKeys)
|
||||
g.Get("/jobs/attribute_values", jobAttributeValues)
|
||||
g.Post("/jobs/list", jobList)
|
||||
|
||||
opPost(g, "/infra_monitoring/hosts", infraHosts)
|
||||
opPost(g, "/infra_monitoring/pods", infraPods)
|
||||
opPost(g, "/infra_monitoring/nodes", infraNodes)
|
||||
opPost(g, "/infra_monitoring/namespaces", infraNamespaces)
|
||||
opPost(g, "/infra_monitoring/clusters", infraClusters)
|
||||
opPost(g, "/infra_monitoring/pvcs", infraVolumes)
|
||||
opPost(g, "/infra_monitoring/deployments", infraDeployments)
|
||||
opPost(g, "/infra_monitoring/statefulsets", infraStatefulSets)
|
||||
opPost(g, "/infra_monitoring/jobs", infraJobs)
|
||||
opPost(g, "/infra_monitoring/daemonsets", infraDaemonSets)
|
||||
opGet(g, "/infra_monitoring/checks", infraChecks)
|
||||
g.Post("/infra_monitoring/hosts", infraHosts)
|
||||
g.Post("/infra_monitoring/pods", infraPods)
|
||||
g.Post("/infra_monitoring/nodes", infraNodes)
|
||||
g.Post("/infra_monitoring/namespaces", infraNamespaces)
|
||||
g.Post("/infra_monitoring/clusters", infraClusters)
|
||||
g.Post("/infra_monitoring/pvcs", infraVolumes)
|
||||
g.Post("/infra_monitoring/deployments", infraDeployments)
|
||||
g.Post("/infra_monitoring/statefulsets", infraStatefulSets)
|
||||
g.Post("/infra_monitoring/jobs", infraJobs)
|
||||
g.Post("/infra_monitoring/daemonsets", infraDaemonSets)
|
||||
g.Get("/infra_monitoring/checks", infraChecks)
|
||||
}
|
||||
|
||||
// ── hosts ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
+30
-30
@@ -59,46 +59,46 @@ import (
|
||||
// be swallowed by /integrations/:integrationId, the same disambiguation the
|
||||
// runtime's own mux tree makes.
|
||||
func mountIntegrations(app *zip.App) {
|
||||
g := rooted{app.Group(o11yRoot), o11yRoot}
|
||||
g := app.Group(o11yRoot)
|
||||
|
||||
// integration catalog and install lifecycle (runtime gate: ViewAccess)
|
||||
opGet(g, "/integrations", listIntegrations, zip.WithOperationID("ListIntegrations"))
|
||||
opGet(g, "/integrations/:integrationId", getIntegration, zip.WithOperationID("GetIntegration"))
|
||||
opGet(g, "/integrations/:integrationId/connection_status", getIntegrationConnectionStatus, zip.WithOperationID("GetIntegrationConnectionStatus"))
|
||||
opPost(g, "/integrations/install", installIntegration, zip.WithOperationID("InstallIntegration"))
|
||||
opPost(g, "/integrations/uninstall", uninstallIntegration, zip.WithOperationID("UninstallIntegration"))
|
||||
g.Get("/integrations", listIntegrations, zip.WithOperationID("ListIntegrations"))
|
||||
g.Get("/integrations/:integrationId", getIntegration, zip.WithOperationID("GetIntegration"))
|
||||
g.Get("/integrations/:integrationId/connection_status", getIntegrationConnectionStatus, zip.WithOperationID("GetIntegrationConnectionStatus"))
|
||||
g.Post("/integrations/install", installIntegration, zip.WithOperationID("InstallIntegration"))
|
||||
g.Post("/integrations/uninstall", uninstallIntegration, zip.WithOperationID("UninstallIntegration"))
|
||||
|
||||
// cloud integrations — accounts and services (runtime gate: AdminAccess)
|
||||
opGet(g, "/cloud_integrations/:cloud_provider/credentials", getConnectionCredentials, zip.WithOperationID("GetConnectionCredentials"))
|
||||
opPost(g, "/cloud_integrations/:cloud_provider/accounts", createAccount, zip.WithStatus(http.StatusCreated), zip.WithOperationID("CreateAccount"))
|
||||
opGet(g, "/cloud_integrations/:cloud_provider/accounts", listAccounts, zip.WithOperationID("ListAccounts"))
|
||||
opGet(g, "/cloud_integrations/:cloud_provider/accounts/:id", getAccount, zip.WithOperationID("GetAccount"))
|
||||
opPut(g, "/cloud_integrations/:cloud_provider/accounts/:id", updateAccount, zip.WithOperationID("UpdateAccount"))
|
||||
opDelete(g, "/cloud_integrations/:cloud_provider/accounts/:id", disconnectAccount, zip.WithOperationID("DisconnectAccount"))
|
||||
opGet(g, "/cloud_integrations/:cloud_provider/services", listServicesMetadata, zip.WithOperationID("ListServicesMetadata"))
|
||||
opGet(g, "/cloud_integrations/:cloud_provider/accounts/:id/services", listAccountServicesMetadata, zip.WithOperationID("ListAccountServicesMetadata"))
|
||||
opGet(g, "/cloud_integrations/:cloud_provider/services/:service_id", getService, zip.WithOperationID("GetService"))
|
||||
opPut(g, "/cloud_integrations/:cloud_provider/accounts/:id/services/:service_id", updateService, zip.WithOperationID("UpdateService"))
|
||||
opGet(g, "/cloud_integrations/:cloud_provider/accounts/:id/services/:service_id", getAccountService, zip.WithOperationID("GetAccountService"))
|
||||
g.Get("/cloud_integrations/:cloud_provider/credentials", getConnectionCredentials, zip.WithOperationID("GetConnectionCredentials"))
|
||||
g.Post("/cloud_integrations/:cloud_provider/accounts", createAccount, zip.WithStatus(http.StatusCreated), zip.WithOperationID("CreateAccount"))
|
||||
g.Get("/cloud_integrations/:cloud_provider/accounts", listAccounts, zip.WithOperationID("ListAccounts"))
|
||||
g.Get("/cloud_integrations/:cloud_provider/accounts/:id", getAccount, zip.WithOperationID("GetAccount"))
|
||||
g.Put("/cloud_integrations/:cloud_provider/accounts/:id", updateAccount, zip.WithOperationID("UpdateAccount"))
|
||||
g.Delete("/cloud_integrations/:cloud_provider/accounts/:id", disconnectAccount, zip.WithOperationID("DisconnectAccount"))
|
||||
g.Get("/cloud_integrations/:cloud_provider/services", listServicesMetadata, zip.WithOperationID("ListServicesMetadata"))
|
||||
g.Get("/cloud_integrations/:cloud_provider/accounts/:id/services", listAccountServicesMetadata, zip.WithOperationID("ListAccountServicesMetadata"))
|
||||
g.Get("/cloud_integrations/:cloud_provider/services/:service_id", getService, zip.WithOperationID("GetService"))
|
||||
g.Put("/cloud_integrations/:cloud_provider/accounts/:id/services/:service_id", updateService, zip.WithOperationID("UpdateService"))
|
||||
g.Get("/cloud_integrations/:cloud_provider/accounts/:id/services/:service_id", getAccountService, zip.WithOperationID("GetAccountService"))
|
||||
// The deprecated agent check-in keeps its hyphenated path for the agents
|
||||
// already deployed against it; the check_in path below is its replacement.
|
||||
opPost(g, "/cloud-integrations/:cloud_provider/agent-check-in", agentCheckInDeprecated, zip.WithOperationID("AgentCheckInDeprecated"))
|
||||
opPost(g, "/cloud_integrations/:cloud_provider/accounts/check_in", agentCheckIn, zip.WithOperationID("AgentCheckIn"))
|
||||
g.Post("/cloud-integrations/:cloud_provider/agent-check-in", agentCheckInDeprecated, zip.WithOperationID("AgentCheckInDeprecated"))
|
||||
g.Post("/cloud_integrations/:cloud_provider/accounts/check_in", agentCheckIn, zip.WithOperationID("AgentCheckIn"))
|
||||
|
||||
// gateway ingestion keys and their limits (runtime gate: EditAccess)
|
||||
opGet(g, "/gateway/ingestion_keys", getIngestionKeys, zip.WithOperationID("GetIngestionKeys"))
|
||||
opGet(g, "/gateway/ingestion_keys/search", searchIngestionKeys, zip.WithOperationID("SearchIngestionKeys"))
|
||||
opPost(g, "/gateway/ingestion_keys", createIngestionKey, zip.WithStatus(http.StatusCreated), zip.WithOperationID("CreateIngestionKey"))
|
||||
opPatch(g, "/gateway/ingestion_keys/:keyId", updateIngestionKey, zip.WithOperationID("UpdateIngestionKey"))
|
||||
opDelete(g, "/gateway/ingestion_keys/:keyId", deleteIngestionKey, zip.WithOperationID("DeleteIngestionKey"))
|
||||
opPost(g, "/gateway/ingestion_keys/:keyId/limits", createIngestionKeyLimit, zip.WithStatus(http.StatusCreated), zip.WithOperationID("CreateIngestionKeyLimit"))
|
||||
opPatch(g, "/gateway/ingestion_keys/limits/:limitId", updateIngestionKeyLimit, zip.WithOperationID("UpdateIngestionKeyLimit"))
|
||||
opDelete(g, "/gateway/ingestion_keys/limits/:limitId", deleteIngestionKeyLimit, zip.WithOperationID("DeleteIngestionKeyLimit"))
|
||||
g.Get("/gateway/ingestion_keys", getIngestionKeys, zip.WithOperationID("GetIngestionKeys"))
|
||||
g.Get("/gateway/ingestion_keys/search", searchIngestionKeys, zip.WithOperationID("SearchIngestionKeys"))
|
||||
g.Post("/gateway/ingestion_keys", createIngestionKey, zip.WithStatus(http.StatusCreated), zip.WithOperationID("CreateIngestionKey"))
|
||||
g.Patch("/gateway/ingestion_keys/:keyId", updateIngestionKey, zip.WithOperationID("UpdateIngestionKey"))
|
||||
g.Delete("/gateway/ingestion_keys/:keyId", deleteIngestionKey, zip.WithOperationID("DeleteIngestionKey"))
|
||||
g.Post("/gateway/ingestion_keys/:keyId/limits", createIngestionKeyLimit, zip.WithStatus(http.StatusCreated), zip.WithOperationID("CreateIngestionKeyLimit"))
|
||||
g.Patch("/gateway/ingestion_keys/limits/:limitId", updateIngestionKeyLimit, zip.WithOperationID("UpdateIngestionKeyLimit"))
|
||||
g.Delete("/gateway/ingestion_keys/limits/:limitId", deleteIngestionKeyLimit, zip.WithOperationID("DeleteIngestionKeyLimit"))
|
||||
|
||||
// Zeus — deployment profile and host (runtime gate: AdminAccess writes, ViewAccess read)
|
||||
opPut(g, "/zeus/profiles", putProfile, zip.WithOperationID("PutProfile"))
|
||||
opGet(g, "/zeus/hosts", getHosts, zip.WithOperationID("GetHosts"))
|
||||
opPut(g, "/zeus/hosts", putHost, zip.WithOperationID("PutHost"))
|
||||
g.Put("/zeus/profiles", putProfile, zip.WithOperationID("PutProfile"))
|
||||
g.Get("/zeus/hosts", getHosts, zip.WithOperationID("GetHosts"))
|
||||
g.Put("/zeus/hosts", putHost, zip.WithOperationID("PutHost"))
|
||||
}
|
||||
|
||||
// ── integration catalog and install lifecycle ────────────────────────────────
|
||||
|
||||
@@ -59,30 +59,30 @@ import (
|
||||
// declarations, so the SDK method, the CLI command and the MCP tool a caller
|
||||
// already knows keep their names as the routes cross into the document.
|
||||
func mountLLMObs(app *zip.App) {
|
||||
g := rooted{app.Group(o11yRoot), o11yRoot}
|
||||
g := app.Group(o11yRoot)
|
||||
|
||||
// The four gen_ai span views — projections over spans, not tables.
|
||||
opGet(g, "/llm/observations", llmObservations, zip.WithOperationID("ListLLMObservations"))
|
||||
opGet(g, "/llm/traces", llmTraces, zip.WithOperationID("ListLLMTraces"))
|
||||
opGet(g, "/llm/sessions", llmSessions, zip.WithOperationID("ListLLMSessions"))
|
||||
opGet(g, "/llm/users", llmUsers, zip.WithOperationID("ListLLMUsers"))
|
||||
g.Get("/llm/observations", llmObservations, zip.WithOperationID("ListLLMObservations"))
|
||||
g.Get("/llm/traces", llmTraces, zip.WithOperationID("ListLLMTraces"))
|
||||
g.Get("/llm/sessions", llmSessions, zip.WithOperationID("ListLLMSessions"))
|
||||
g.Get("/llm/users", llmUsers, zip.WithOperationID("ListLLMUsers"))
|
||||
|
||||
// Eval scores — CRUD over a net-new table.
|
||||
opGet(g, "/llm/scores", llmListScores, zip.WithOperationID("ListLLMScores"))
|
||||
opPost(g, "/llm/scores", llmCreateScore, zip.WithStatus(http.StatusCreated), zip.WithOperationID("CreateLLMScore"))
|
||||
opGet(g, "/llm/score/:id", llmGetScore, zip.WithOperationID("GetLLMScore"))
|
||||
opDelete(g, "/llm/score/:id", llmDeleteScore, zip.WithOperationID("DeleteLLMScore"))
|
||||
g.Get("/llm/scores", llmListScores, zip.WithOperationID("ListLLMScores"))
|
||||
g.Post("/llm/scores", llmCreateScore, zip.WithStatus(http.StatusCreated), zip.WithOperationID("CreateLLMScore"))
|
||||
g.Get("/llm/score/:id", llmGetScore, zip.WithOperationID("GetLLMScore"))
|
||||
g.Delete("/llm/score/:id", llmDeleteScore, zip.WithOperationID("DeleteLLMScore"))
|
||||
|
||||
// Human annotations — a note plus an optional review queue.
|
||||
opGet(g, "/llm/annotation", llmListAnnotations, zip.WithOperationID("ListLLMAnnotations"))
|
||||
opPost(g, "/llm/annotation", llmCreateAnnotation, zip.WithStatus(http.StatusCreated), zip.WithOperationID("CreateLLMAnnotation"))
|
||||
g.Get("/llm/annotation", llmListAnnotations, zip.WithOperationID("ListLLMAnnotations"))
|
||||
g.Post("/llm/annotation", llmCreateAnnotation, zip.WithStatus(http.StatusCreated), zip.WithOperationID("CreateLLMAnnotation"))
|
||||
|
||||
// LLM pricing rules — the read, the single bulk write shared by the user and
|
||||
// the Zeus sync job, and the per-rule read and delete.
|
||||
opGet(g, "/llm_pricing_rules", llmListPricingRules, zip.WithOperationID("ListLLMPricingRules"))
|
||||
opPut(g, "/llm_pricing_rules", llmUpsertPricingRules, zip.WithOperationID("CreateOrUpdateLLMPricingRules"))
|
||||
opGet(g, "/llm_pricing_rules/:id", llmGetPricingRule, zip.WithOperationID("GetLLMPricingRule"))
|
||||
opDelete(g, "/llm_pricing_rules/:id", llmDeletePricingRule, zip.WithOperationID("DeleteLLMPricingRule"))
|
||||
g.Get("/llm_pricing_rules", llmListPricingRules, zip.WithOperationID("ListLLMPricingRules"))
|
||||
g.Put("/llm_pricing_rules", llmUpsertPricingRules, zip.WithOperationID("CreateOrUpdateLLMPricingRules"))
|
||||
g.Get("/llm_pricing_rules/:id", llmGetPricingRule, zip.WithOperationID("GetLLMPricingRule"))
|
||||
g.Delete("/llm_pricing_rules/:id", llmDeletePricingRule, zip.WithOperationID("DeleteLLMPricingRule"))
|
||||
}
|
||||
|
||||
// ── the fourteen operations ─────────────────────────────────────────────────────
|
||||
|
||||
@@ -45,16 +45,16 @@ import (
|
||||
// pipelines routes keep the mux tree's discipline — preview registers before
|
||||
// the parameterised version read so a version can never shadow it.
|
||||
func mountLogs(app *zip.App) {
|
||||
g := rooted{app.Group(o11yRoot), o11yRoot}
|
||||
opGet(g, "/logs", logRecords)
|
||||
opGet(g, "/logs/fields", logFields)
|
||||
opPost(g, "/logs/fields", logFieldUpdate)
|
||||
opGet(g, "/logs/aggregate", logAggregate)
|
||||
opPost(g, "/logs/pipelines/preview", logPipelinePreview)
|
||||
opGet(g, "/logs/pipelines/:version", logPipelines)
|
||||
opPost(g, "/logs/pipelines", logPipelineCreate)
|
||||
opPost(g, "/logs/promote_paths", logPromote, zip.WithStatus(http.StatusCreated))
|
||||
opGet(g, "/logs/promote_paths", logPromoted)
|
||||
g := app.Group(o11yRoot)
|
||||
g.Get("/logs", logRecords)
|
||||
g.Get("/logs/fields", logFields)
|
||||
g.Post("/logs/fields", logFieldUpdate)
|
||||
g.Get("/logs/aggregate", logAggregate)
|
||||
g.Post("/logs/pipelines/preview", logPipelinePreview)
|
||||
g.Get("/logs/pipelines/:version", logPipelines)
|
||||
g.Post("/logs/pipelines", logPipelineCreate)
|
||||
g.Post("/logs/promote_paths", logPromote, zip.WithStatus(http.StatusCreated))
|
||||
g.Get("/logs/promote_paths", logPromoted)
|
||||
}
|
||||
|
||||
// ── the nine operations ───────────────────────────────────────────────────────
|
||||
|
||||
+20
-20
@@ -35,28 +35,28 @@ import (
|
||||
// Collection routes register before the parameterised ones so an id can never
|
||||
// shadow a collection.
|
||||
func mountMetrics(app *zip.App) {
|
||||
g := rooted{app.Group(o11yRoot), o11yRoot}
|
||||
g := app.Group(o11yRoot)
|
||||
|
||||
opGet(g, "/metrics", listMetrics, zip.WithOperationID("ListMetrics"))
|
||||
opPost(g, "/metrics/stats", metricStats, zip.WithOperationID("GetMetricsStats"))
|
||||
opPost(g, "/metrics/treemap", metricTreemap, zip.WithOperationID("GetMetricsTreemap"))
|
||||
opGet(g, "/metrics/attributes", metricAttributes, zip.WithOperationID("GetMetricAttributes"))
|
||||
opGet(g, "/metrics/metadata", metricMetadata, zip.WithOperationID("GetMetricMetadata"))
|
||||
opPost(g, "/metrics/metadata", saveMetricMetadata, zip.WithOperationID("UpdateMetricMetadata"))
|
||||
opGet(g, "/metrics/highlights", metricHighlights, zip.WithOperationID("GetMetricHighlights"))
|
||||
opGet(g, "/metrics/alerts", metricAlerts, zip.WithOperationID("GetMetricAlerts"))
|
||||
opGet(g, "/metrics/dashboards", metricDashboards, zip.WithOperationID("GetMetricDashboardsV2"))
|
||||
opPost(g, "/metrics/inspect", inspectMetric, zip.WithOperationID("InspectMetrics"))
|
||||
opGet(g, "/metrics/onboarding", metricsOnboarding, zip.WithOperationID("GetMetricsOnboardingStatus"))
|
||||
g.Get("/metrics", listMetrics, zip.WithOperationID("ListMetrics"))
|
||||
g.Post("/metrics/stats", metricStats, zip.WithOperationID("GetMetricsStats"))
|
||||
g.Post("/metrics/treemap", metricTreemap, zip.WithOperationID("GetMetricsTreemap"))
|
||||
g.Get("/metrics/attributes", metricAttributes, zip.WithOperationID("GetMetricAttributes"))
|
||||
g.Get("/metrics/metadata", metricMetadata, zip.WithOperationID("GetMetricMetadata"))
|
||||
g.Post("/metrics/metadata", saveMetricMetadata, zip.WithOperationID("UpdateMetricMetadata"))
|
||||
g.Get("/metrics/highlights", metricHighlights, zip.WithOperationID("GetMetricHighlights"))
|
||||
g.Get("/metrics/alerts", metricAlerts, zip.WithOperationID("GetMetricAlerts"))
|
||||
g.Get("/metrics/dashboards", metricDashboards, zip.WithOperationID("GetMetricDashboardsV2"))
|
||||
g.Post("/metrics/inspect", inspectMetric, zip.WithOperationID("InspectMetrics"))
|
||||
g.Get("/metrics/onboarding", metricsOnboarding, zip.WithOperationID("GetMetricsOnboardingStatus"))
|
||||
|
||||
opGet(g, "/metric_reduction_rules", listReductionRules, zip.WithOperationID("ListMetricReductionRules"))
|
||||
opPost(g, "/metric_reduction_rules", createReductionRule, zip.WithOperationID("CreateMetricReductionRule"), zip.WithStatus(http.StatusCreated))
|
||||
opGet(g, "/metric_reduction_rules/stats", reductionRuleStats, zip.WithOperationID("GetMetricReductionRuleStats"))
|
||||
opGet(g, "/metric_reduction_rules/timeseries", reductionRuleTimeseries, zip.WithOperationID("GetMetricReductionRuleTimeseries"))
|
||||
opPost(g, "/metric_reduction_rules/preview", previewReductionRule, zip.WithOperationID("PreviewMetricReductionRule"))
|
||||
opGet(g, "/metric_reduction_rules/:id", reductionRule, zip.WithOperationID("GetMetricReductionRuleByID"))
|
||||
opPut(g, "/metric_reduction_rules/:id", saveReductionRule, zip.WithOperationID("UpdateMetricReductionRuleByID"))
|
||||
opDelete(g, "/metric_reduction_rules/:id", deleteReductionRule, zip.WithOperationID("DeleteMetricReductionRuleByID"))
|
||||
g.Get("/metric_reduction_rules", listReductionRules, zip.WithOperationID("ListMetricReductionRules"))
|
||||
g.Post("/metric_reduction_rules", createReductionRule, zip.WithOperationID("CreateMetricReductionRule"), zip.WithStatus(http.StatusCreated))
|
||||
g.Get("/metric_reduction_rules/stats", reductionRuleStats, zip.WithOperationID("GetMetricReductionRuleStats"))
|
||||
g.Get("/metric_reduction_rules/timeseries", reductionRuleTimeseries, zip.WithOperationID("GetMetricReductionRuleTimeseries"))
|
||||
g.Post("/metric_reduction_rules/preview", previewReductionRule, zip.WithOperationID("PreviewMetricReductionRule"))
|
||||
g.Get("/metric_reduction_rules/:id", reductionRule, zip.WithOperationID("GetMetricReductionRuleByID"))
|
||||
g.Put("/metric_reduction_rules/:id", saveReductionRule, zip.WithOperationID("UpdateMetricReductionRuleByID"))
|
||||
g.Delete("/metric_reduction_rules/:id", deleteReductionRule, zip.WithOperationID("DeleteMetricReductionRuleByID"))
|
||||
}
|
||||
|
||||
// ── the metrics-explorer operations ───────────────────────────────────────────
|
||||
|
||||
@@ -52,23 +52,11 @@ import (
|
||||
// PATH UNTOUCHED. Delegation never rewrites r.URL: every route is registered at
|
||||
// its full public path, so the route literal IS the contract — one spelling,
|
||||
// nothing to translate, nothing to drift.
|
||||
// A HOST MAY TAKE AN ADDRESS. Since every route is named there is no wildcard
|
||||
// left for a host's own route to shadow, so a host that serves one of these
|
||||
// addresses itself has to say so with [Claimed] — see claim.go for why that is a
|
||||
// statement of ownership rather than a filter. Without one, nothing changes and
|
||||
// the whole table is declared.
|
||||
func Use(app *zip.App, opts ...Option) error {
|
||||
c := new(conf)
|
||||
for _, o := range opts {
|
||||
o(c)
|
||||
}
|
||||
// Read by the declaration verbs below, for this call only: Mount is the
|
||||
// composition root, so the set is written once here and every declaration it
|
||||
// makes happens before it returns.
|
||||
mounting = c
|
||||
defer func() { mounting = nil }()
|
||||
|
||||
app.Logger().Info("o11y: mounting routes", "prefix", o11yRoot, "claimed", len(c.claimed))
|
||||
// One address, one declaration. Every route is named, so a host that serves one
|
||||
// of these addresses itself gets a Build conflict from zip rather than a second
|
||||
// handler standing behind the first.
|
||||
func Use(app *zip.App) error {
|
||||
app.Logger().Info("o11y: mounting routes", "prefix", o11yRoot)
|
||||
|
||||
// Native probe group, registered ahead of everything else so Fiber's
|
||||
// in-order match serves it off the mux tree (see health.go).
|
||||
@@ -216,8 +204,10 @@ func mountHatches(app *zip.App) {
|
||||
// /api/<project>/envelope/ suffix to whatever DSN path it is given, so
|
||||
// renaming it would break every SDK in the field. We RECEIVE this shape; we
|
||||
// do not publish it.
|
||||
route(app, http.MethodPost, eventRoot+"/:project/envelope/") // the endpoint a minted DSN names
|
||||
route(app, http.MethodPost, eventRoot+"/:project/store/") // the same endpoint, single-event form
|
||||
route(app, http.MethodPost, o11yRoot+"/api/:project_id/envelope/") // the suffix a stock SDK appends
|
||||
route(app, http.MethodPost, o11yRoot+"/api/:project_id/store/") // the same suffix, single-event form
|
||||
// A declared address never ends in a slash, and fiber matches the SDK's
|
||||
// trailing one against it either way.
|
||||
route(app, http.MethodPost, eventRoot+"/:project/envelope") // the endpoint a minted DSN names
|
||||
route(app, http.MethodPost, eventRoot+"/:project/store") // the same endpoint, single-event form
|
||||
route(app, http.MethodPost, o11yRoot+"/api/:project_id/envelope") // the suffix a stock SDK appends
|
||||
route(app, http.MethodPost, o11yRoot+"/api/:project_id/store") // the same suffix, single-event form
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
// /v1/o11y. Two families:
|
||||
//
|
||||
// - INGEST (public, DSN-authenticated in-handler): the Sentry wire endpoints
|
||||
// POST /v1/o11y/api/{project}/envelope/ and POST /v1/o11y/api/{project}/store/.
|
||||
// POST /v1/o11y/api/{project}/envelope and POST /v1/o11y/api/{project}/store.
|
||||
// They are wrapped with OpenAccess (no IAM) because the Sentry SDK presents a DSN
|
||||
// key, not a Hanzo session; the handler verifies that key. A Sentry DSN of
|
||||
// https://<key>@<host>/v1/o11y/<org> makes the SDK POST to exactly these paths:
|
||||
@@ -41,7 +41,7 @@ func (provider *provider) addErrorTrackingRoutes(router routing.Router) {
|
||||
fn http.HandlerFunc
|
||||
def handler.OpenAPIDef
|
||||
}{
|
||||
{http.MethodPost, "/v1/o11y/api/{project_id}/envelope/", provider.authzMiddleware.OpenAccess(h.EnvelopeIngest), handler.OpenAPIDef{
|
||||
{http.MethodPost, "/v1/o11y/api/{project_id}/envelope", provider.authzMiddleware.OpenAccess(h.EnvelopeIngest), handler.OpenAPIDef{
|
||||
ID: "IngestErrorEnvelope", Tags: []string{"errortracking"}, Summary: "Ingest a Sentry envelope",
|
||||
Description: "Sentry-envelope-compatible ingest. Authenticated by the DSN public key (X-Sentry-Auth or ?sentry_key), not a Hanzo session.",
|
||||
RequestContentType: "application/x-sentry-envelope",
|
||||
@@ -49,7 +49,7 @@ func (provider *provider) addErrorTrackingRoutes(router routing.Router) {
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusServiceUnavailable},
|
||||
SecuritySchemes: []handler.OpenAPISecurityScheme{},
|
||||
}},
|
||||
{http.MethodPost, "/v1/o11y/api/{project_id}/store/", provider.authzMiddleware.OpenAccess(h.StoreIngest), handler.OpenAPIDef{
|
||||
{http.MethodPost, "/v1/o11y/api/{project_id}/store", provider.authzMiddleware.OpenAccess(h.StoreIngest), handler.OpenAPIDef{
|
||||
ID: "IngestErrorStore", Tags: []string{"errortracking"}, Summary: "Ingest a legacy Sentry store event",
|
||||
Description: "Legacy single-event Sentry ingest. Authenticated by the DSN public key.",
|
||||
RequestContentType: "application/json",
|
||||
|
||||
@@ -22,7 +22,7 @@ import (
|
||||
// - The FACE, /v1/o11y/sentinel — Hanzo Sentry, the Sentry-parity product read by a
|
||||
// signed-in person: projects, issues, discover, events, logs, traces, stats. Hanzo
|
||||
// IAM authz, every one scoped to the caller's org from the validated claims.
|
||||
// - INGEST, POST /v1/event/{project}/envelope/ and /store/ — a keyed beacon. It is
|
||||
// - INGEST, POST /v1/event/{project}/envelope and /store — a keyed beacon. It is
|
||||
// the address a minted DSN spells (implsentry's mintDSN), so the address a client
|
||||
// is told and the address this router opens are one string. OpenAccess (no IAM):
|
||||
// a Sentry SDK presents a DSN key, not a Hanzo session; the handler verifies that
|
||||
@@ -161,7 +161,7 @@ func (provider *provider) addSentryRoutes(router routing.Router) {
|
||||
fn http.HandlerFunc
|
||||
def handler.OpenAPIDef
|
||||
}{
|
||||
{"/v1/event/{project:guid}/envelope/", provider.authzMiddleware.OpenAccess(h.EnvelopeIngest), handler.OpenAPIDef{
|
||||
{"/v1/event/{project:guid}/envelope", provider.authzMiddleware.OpenAccess(h.EnvelopeIngest), handler.OpenAPIDef{
|
||||
ID: "SentryIngestEnvelope", Tags: []string{"sentry"}, Summary: "Ingest a Sentry envelope",
|
||||
Description: "Sentry-envelope-compatible ingest. Authenticated by the DSN public key (X-Sentry-Auth or ?sentry_key), not a Hanzo session.",
|
||||
RequestContentType: "application/x-sentry-envelope",
|
||||
@@ -169,7 +169,7 @@ func (provider *provider) addSentryRoutes(router routing.Router) {
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusServiceUnavailable},
|
||||
SecuritySchemes: []handler.OpenAPISecurityScheme{},
|
||||
}},
|
||||
{"/v1/event/{project:guid}/store/", provider.authzMiddleware.OpenAccess(h.StoreIngest), handler.OpenAPIDef{
|
||||
{"/v1/event/{project:guid}/store", provider.authzMiddleware.OpenAccess(h.StoreIngest), handler.OpenAPIDef{
|
||||
ID: "SentryIngestStore", Tags: []string{"sentry"}, Summary: "Ingest a legacy Sentry store event",
|
||||
Description: "Legacy single-event Sentry ingest. Authenticated by the DSN public key.",
|
||||
RequestContentType: "application/json",
|
||||
|
||||
+23
-23
@@ -46,37 +46,37 @@ import (
|
||||
// routes_licenses.go, then the o11yapiserver singletons). It registers on the
|
||||
// SAME o11yRoot group logs.go mounts on, ahead of the delegation wildcard.
|
||||
func mountPlatform(app *zip.App) {
|
||||
g := rooted{app.Group(o11yRoot), o11yRoot}
|
||||
g := app.Group(o11yRoot)
|
||||
|
||||
// routes_misc.go
|
||||
opGet(g, "/query", promQuery)
|
||||
opPost(g, "/variables/query", dashboardVars)
|
||||
opPost(g, "/event", event)
|
||||
opGet(g, "/usage", usage)
|
||||
opPost(g, "/dependency_graph", dependencyGraph)
|
||||
opGet(g, "/version", version)
|
||||
opGet(g, "/health", health)
|
||||
opGet(g, "/disks", disks)
|
||||
opPost(g, "/span_percentile", spanPercentile)
|
||||
opPost(g, "/query_filter/analyze", analyzeQueryFilter)
|
||||
opGet(g, "/filter_suggestions", filterSuggestions)
|
||||
opGet(g, "/infra_onboarding/k8s/status", k8sOnboarding)
|
||||
opGet(g, "/metric/metric_metadata", legacyMetricMetadata)
|
||||
g.Get("/query", promQuery)
|
||||
g.Post("/variables/query", dashboardVars)
|
||||
g.Post("/event", event)
|
||||
g.Get("/usage", usage)
|
||||
g.Post("/dependency_graph", dependencyGraph)
|
||||
g.Get("/version", version)
|
||||
g.Get("/health", health)
|
||||
g.Get("/disks", disks)
|
||||
g.Post("/span_percentile", spanPercentile)
|
||||
g.Post("/query_filter/analyze", analyzeQueryFilter)
|
||||
g.Get("/filter_suggestions", filterSuggestions)
|
||||
g.Get("/infra_onboarding/k8s/status", k8sOnboarding)
|
||||
g.Get("/metric/metric_metadata", legacyMetricMetadata)
|
||||
|
||||
// routes_settings.go
|
||||
opPost(g, "/settings/ttl", setRetention)
|
||||
opGet(g, "/settings/ttl", retention)
|
||||
opPost(g, "/settings/apdex", setApdex)
|
||||
opGet(g, "/settings/apdex", apdex)
|
||||
g.Post("/settings/ttl", setRetention)
|
||||
g.Get("/settings/ttl", retention)
|
||||
g.Post("/settings/apdex", setApdex)
|
||||
g.Get("/settings/apdex", apdex)
|
||||
|
||||
// routes_licenses.go
|
||||
opGet(g, "/licenses", licenses)
|
||||
opGet(g, "/licenses/active", activateLicense)
|
||||
g.Get("/licenses", licenses)
|
||||
g.Get("/licenses/active", activateLicense)
|
||||
|
||||
// o11yapiserver: global.go, flagger.go, statsreporter.go
|
||||
opGet(g, "/global/config", globalConfig)
|
||||
opGet(g, "/features", features)
|
||||
opGet(g, "/stats", orgStats)
|
||||
g.Get("/global/config", globalConfig)
|
||||
g.Get("/features", features)
|
||||
g.Get("/stats", orgStats)
|
||||
}
|
||||
|
||||
// ── the operations ────────────────────────────────────────────────────────────
|
||||
|
||||
+17
-17
@@ -59,33 +59,33 @@ import (
|
||||
// The saved-view collection routes register before the parameterised ones so a
|
||||
// view id can never shadow the collection, exactly as the mux tree ordered them.
|
||||
func mountQueryCore(app *zip.App) {
|
||||
g := rooted{app.Group(o11yRoot), o11yRoot}
|
||||
g := app.Group(o11yRoot)
|
||||
|
||||
// the v5 composite query engine
|
||||
opPost(g, "/query_range", querierQueryRange)
|
||||
opPost(g, "/query_range/preview", querierQueryRangePreview)
|
||||
opPost(g, "/substitute_vars", querierReplaceVariables)
|
||||
g.Post("/query_range", querierQueryRange)
|
||||
g.Post("/query_range/preview", querierQueryRangePreview)
|
||||
g.Post("/substitute_vars", querierReplaceVariables)
|
||||
|
||||
// the legacy metrics range read and the builder-format echo
|
||||
opGet(g, "/query_range", metricsQueryRange)
|
||||
opPost(g, "/query_range/format", queryRangeFormat)
|
||||
g.Get("/query_range", metricsQueryRange)
|
||||
g.Post("/query_range/format", queryRangeFormat)
|
||||
|
||||
// attribute autocomplete for filter building
|
||||
opGet(g, "/autocomplete/aggregate_attributes", autocompleteAggregate)
|
||||
opGet(g, "/autocomplete/attribute_keys", autocompleteKeys)
|
||||
opGet(g, "/autocomplete/attribute_values", autocompleteValues)
|
||||
opPost(g, "/auto_complete/attribute_values", autocompleteValuesPost)
|
||||
g.Get("/autocomplete/aggregate_attributes", autocompleteAggregate)
|
||||
g.Get("/autocomplete/attribute_keys", autocompleteKeys)
|
||||
g.Get("/autocomplete/attribute_values", autocompleteValues)
|
||||
g.Post("/auto_complete/attribute_values", autocompleteValuesPost)
|
||||
|
||||
// the field catalog
|
||||
opGet(g, "/fields/keys", fieldKeys)
|
||||
opGet(g, "/fields/values", fieldValues)
|
||||
g.Get("/fields/keys", fieldKeys)
|
||||
g.Get("/fields/values", fieldValues)
|
||||
|
||||
// saved explorer views
|
||||
opGet(g, "/explorer/views", savedViewList)
|
||||
opPost(g, "/explorer/views", savedViewCreate)
|
||||
opGet(g, "/explorer/views/:viewId", savedViewGet)
|
||||
opPut(g, "/explorer/views/:viewId", savedViewUpdate)
|
||||
opDelete(g, "/explorer/views/:viewId", savedViewDelete)
|
||||
g.Get("/explorer/views", savedViewList)
|
||||
g.Post("/explorer/views", savedViewCreate)
|
||||
g.Get("/explorer/views/:viewId", savedViewGet)
|
||||
g.Put("/explorer/views/:viewId", savedViewUpdate)
|
||||
g.Delete("/explorer/views/:viewId", savedViewDelete)
|
||||
}
|
||||
|
||||
// ── the composite query engine ─────────────────────────────────────────────────
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ package o11y_test
|
||||
// It names every function in the package that SYNTHESIZES a request for the
|
||||
// runtime: one that builds an http.Request and hands it to the runtime
|
||||
// handler's ServeHTTP. Both halves are required, which is what separates a
|
||||
// relay from claim.go's hatch — a hatch passes on the request the SERVER
|
||||
// relay from a hatch (hatch.go) — a hatch passes on the request the SERVER
|
||||
// already built (whose request-target the server itself populated) and so can
|
||||
// never carry this defect. A relay builds the request, and a built request is
|
||||
// only server-shaped if the builder makes it so.
|
||||
|
||||
@@ -14,26 +14,10 @@ package o11y
|
||||
// per value — so a change to the seam had five sites to find, and a change to
|
||||
// the root had eight.
|
||||
//
|
||||
// THE ADDRESS IS NOT A PARAMETER OF THE TRANSPORT. That collapse left one seam
|
||||
// and one root, and one thing still spelled twice: the address. Every op stated
|
||||
// it at its registration and then stated it AGAIN, by hand, in the call —
|
||||
//
|
||||
// opGet(g, "/traces/:traceId", traceSpans) // once
|
||||
// relay(ctx, "GET", o11yRoot+"/traces/"+in.TraceID, …) // and again
|
||||
//
|
||||
// — 706 spellings of 367 addresses, and the second spelling is the one that got
|
||||
// requested, so a route pattern that stopped agreeing with it was never
|
||||
// consulted and never contradicted. That is not hypothetical: three of these
|
||||
// addresses name their segment traceId while the runtime registered traceID, and
|
||||
// nothing in the process could tell, because a router matches by POSITION and a
|
||||
// parameter's name is invisible to it.
|
||||
//
|
||||
// So the address is stated once, at the registration, and travels with the call
|
||||
// (see claim.go's addressed). relay takes no method and no path: it reads the
|
||||
// address the request arrived at, which is a fact about the call and
|
||||
// not a decision this function makes. The 119 hand-built paths are gone with it —
|
||||
// zip.Address renders the concrete one from the same input zip bound the segments
|
||||
// into, so the round trip is exact by construction.
|
||||
// The address is not a parameter of the transport. relay takes no method and no
|
||||
// path: zip.OpOf is the operation being served and zip.AddressOf is the concrete
|
||||
// path it was called at, read back out of the input zip bound the matched
|
||||
// segments into.
|
||||
//
|
||||
// What relay is: it hands a typed op's call to the handler the runtime serves AT
|
||||
// THAT ADDRESS — the same handler the runtime's own router would dispatch to, so
|
||||
@@ -104,22 +88,23 @@ const (
|
||||
// and decodes the answer into the op's Out. Pass out == nil for the operations
|
||||
// whose answer is a 204 with no body.
|
||||
//
|
||||
// The address comes from the context, not from the caller — see the file comment
|
||||
// and claim.go's addressed.
|
||||
// The address comes from the call, not from the caller (see the file comment).
|
||||
func relay(ctx context.Context, params url.Values, body, out any) error {
|
||||
a, known := addressOf(ctx)
|
||||
if !known {
|
||||
op, served := zip.OpOf(ctx)
|
||||
target, known := zip.AddressOf(ctx)
|
||||
if !served || !known {
|
||||
// Only reachable by calling an op's function directly rather than through
|
||||
// the declaration that registered it, which is a programming error and not
|
||||
// a request that can arrive.
|
||||
return zip.ErrInternal("o11y: this operation was invoked outside its own registration, so it has no address to relay to")
|
||||
}
|
||||
h, err := at(a.method, a.template)
|
||||
// The pattern resolves the handler, in the public spelling both sides state an
|
||||
// address in; the concrete path is what the request carries.
|
||||
h, err := at(op.Method, zip.Template(op.Path))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
target := a.path
|
||||
if q := params.Encode(); q != "" {
|
||||
target += "?" + q
|
||||
}
|
||||
@@ -132,7 +117,7 @@ func relay(ctx context.Context, params url.Values, body, out any) error {
|
||||
}
|
||||
payload = bytes.NewReader(b)
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, a.method, target, payload)
|
||||
req, err := http.NewRequestWithContext(ctx, op.Method, target, payload)
|
||||
if err != nil {
|
||||
return zip.ErrBadRequest(err.Error())
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ package o11y_test
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -109,6 +111,47 @@ func TestEveryRouteIsNamedAndCounted(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// The table, address by address. The count above says how many routes there
|
||||
// are; testdata/routes.txt says which, so a route that moves, disappears or
|
||||
// changes its spelling reads as a diff instead of as a count that still adds up.
|
||||
func TestRouteTableIsTheCensus(t *testing.T) {
|
||||
census, err := os.ReadFile("testdata/routes.txt")
|
||||
if err != nil {
|
||||
t.Fatalf("read the census: %v", err)
|
||||
}
|
||||
var got []string
|
||||
for route := range registered(t, mounted(t)) {
|
||||
got = append(got, route)
|
||||
}
|
||||
sort.Strings(got)
|
||||
if moved := drift(strings.Split(strings.TrimSpace(string(census)), "\n"), got); moved != "" {
|
||||
t.Errorf("the route table is not the one in testdata/routes.txt:\n%s", moved)
|
||||
}
|
||||
}
|
||||
|
||||
// drift names what the census holds and the mount does not, and the reverse.
|
||||
func drift(want, got []string) string {
|
||||
held, have := map[string]bool{}, map[string]bool{}
|
||||
for _, w := range want {
|
||||
held[w] = true
|
||||
}
|
||||
for _, g := range got {
|
||||
have[g] = true
|
||||
}
|
||||
var out []string
|
||||
for _, w := range want {
|
||||
if !have[w] {
|
||||
out = append(out, " gone: "+w)
|
||||
}
|
||||
}
|
||||
for _, g := range got {
|
||||
if !held[g] {
|
||||
out = append(out, " new: "+g)
|
||||
}
|
||||
}
|
||||
return strings.Join(out, "\n")
|
||||
}
|
||||
|
||||
// No route may end in a wildcard. This is the invariant the whole change buys:
|
||||
// a NEW un-typed route is a 404 someone has to come and justify, not a silent
|
||||
// fall-through into the runtime.
|
||||
|
||||
+37
-37
@@ -60,57 +60,57 @@ import (
|
||||
// safely: the router matches the most specific pattern, so /rules/test is never
|
||||
// swallowed by /rules/:id, the same disambiguation the runtime's own tree makes.
|
||||
func mountRulesAlerts(app *zip.App) {
|
||||
g := rooted{app.Group(o11yRoot), o11yRoot}
|
||||
g := app.Group(o11yRoot)
|
||||
|
||||
// alert rules (runtime gate per op: see each op's comment)
|
||||
opGet(g, "/rules", listRules, zip.WithOperationID("ListRules"))
|
||||
opGet(g, "/rules/:id", getRuleByID, zip.WithOperationID("GetRuleByID"))
|
||||
opPost(g, "/rules", createRule, zip.WithStatus(http.StatusCreated), zip.WithOperationID("CreateRule"))
|
||||
opPut(g, "/rules/:id", updateRuleByID, zip.WithStatus(http.StatusNoContent), zip.WithOperationID("UpdateRuleByID"))
|
||||
opDelete(g, "/rules/:id", deleteRuleByID, zip.WithStatus(http.StatusNoContent), zip.WithOperationID("DeleteRuleByID"))
|
||||
opPatch(g, "/rules/:id", patchRuleByID, zip.WithOperationID("PatchRuleByID"))
|
||||
opPost(g, "/rules/test", testRule, zip.WithOperationID("TestRule"))
|
||||
g.Get("/rules", listRules, zip.WithOperationID("ListRules"))
|
||||
g.Get("/rules/:id", getRuleByID, zip.WithOperationID("GetRuleByID"))
|
||||
g.Post("/rules", createRule, zip.WithStatus(http.StatusCreated), zip.WithOperationID("CreateRule"))
|
||||
g.Put("/rules/:id", updateRuleByID, zip.WithStatus(http.StatusNoContent), zip.WithOperationID("UpdateRuleByID"))
|
||||
g.Delete("/rules/:id", deleteRuleByID, zip.WithStatus(http.StatusNoContent), zip.WithOperationID("DeleteRuleByID"))
|
||||
g.Patch("/rules/:id", patchRuleByID, zip.WithOperationID("PatchRuleByID"))
|
||||
g.Post("/rules/test", testRule, zip.WithOperationID("TestRule"))
|
||||
|
||||
// planned maintenance / downtime schedules
|
||||
opGet(g, "/downtime_schedules", listDowntimeSchedules, zip.WithOperationID("ListDowntimeSchedules"))
|
||||
opGet(g, "/downtime_schedules/:id", getDowntimeScheduleByID, zip.WithOperationID("GetDowntimeScheduleByID"))
|
||||
opPost(g, "/downtime_schedules", createDowntimeSchedule, zip.WithStatus(http.StatusCreated), zip.WithOperationID("CreateDowntimeSchedule"))
|
||||
opPut(g, "/downtime_schedules/:id", updateDowntimeScheduleByID, zip.WithStatus(http.StatusNoContent), zip.WithOperationID("UpdateDowntimeScheduleByID"))
|
||||
opDelete(g, "/downtime_schedules/:id", deleteDowntimeScheduleByID, zip.WithStatus(http.StatusNoContent), zip.WithOperationID("DeleteDowntimeScheduleByID"))
|
||||
g.Get("/downtime_schedules", listDowntimeSchedules, zip.WithOperationID("ListDowntimeSchedules"))
|
||||
g.Get("/downtime_schedules/:id", getDowntimeScheduleByID, zip.WithOperationID("GetDowntimeScheduleByID"))
|
||||
g.Post("/downtime_schedules", createDowntimeSchedule, zip.WithStatus(http.StatusCreated), zip.WithOperationID("CreateDowntimeSchedule"))
|
||||
g.Put("/downtime_schedules/:id", updateDowntimeScheduleByID, zip.WithStatus(http.StatusNoContent), zip.WithOperationID("UpdateDowntimeScheduleByID"))
|
||||
g.Delete("/downtime_schedules/:id", deleteDowntimeScheduleByID, zip.WithStatus(http.StatusNoContent), zip.WithOperationID("DeleteDowntimeScheduleByID"))
|
||||
|
||||
// rule state history — v2 reads (GET, URL query parameters)
|
||||
opGet(g, "/rules/:id/history/stats", getRuleHistoryStats, zip.WithOperationID("GetRuleHistoryStats"))
|
||||
opGet(g, "/rules/:id/history/timeline", getRuleHistoryTimeline, zip.WithOperationID("GetRuleHistoryTimeline"))
|
||||
opGet(g, "/rules/:id/history/top_contributors", getRuleHistoryTopContributors, zip.WithOperationID("GetRuleHistoryTopContributors"))
|
||||
opGet(g, "/rules/:id/history/filter_keys", getRuleHistoryFilterKeys, zip.WithOperationID("GetRuleHistoryFilterKeys"))
|
||||
opGet(g, "/rules/:id/history/filter_values", getRuleHistoryFilterValues, zip.WithOperationID("GetRuleHistoryFilterValues"))
|
||||
opGet(g, "/rules/:id/history/overall_status", getRuleHistoryOverallStatus, zip.WithOperationID("GetRuleHistoryOverallStatus"))
|
||||
g.Get("/rules/:id/history/stats", getRuleHistoryStats, zip.WithOperationID("GetRuleHistoryStats"))
|
||||
g.Get("/rules/:id/history/timeline", getRuleHistoryTimeline, zip.WithOperationID("GetRuleHistoryTimeline"))
|
||||
g.Get("/rules/:id/history/top_contributors", getRuleHistoryTopContributors, zip.WithOperationID("GetRuleHistoryTopContributors"))
|
||||
g.Get("/rules/:id/history/filter_keys", getRuleHistoryFilterKeys, zip.WithOperationID("GetRuleHistoryFilterKeys"))
|
||||
g.Get("/rules/:id/history/filter_values", getRuleHistoryFilterValues, zip.WithOperationID("GetRuleHistoryFilterValues"))
|
||||
g.Get("/rules/:id/history/overall_status", getRuleHistoryOverallStatus, zip.WithOperationID("GetRuleHistoryOverallStatus"))
|
||||
|
||||
// rule state history — v1 reads (POST, JSON body) and the legacy test-fire
|
||||
opPost(g, "/testRule", testRuleNotification, zip.WithOperationID("TestRuleNotification"))
|
||||
opPost(g, "/rules/:id/history/stats", getRuleStats, zip.WithOperationID("GetRuleStats"))
|
||||
opPost(g, "/rules/:id/history/timeline", getRuleStateHistory, zip.WithOperationID("GetRuleStateHistory"))
|
||||
opPost(g, "/rules/:id/history/top_contributors", getRuleStateHistoryTopContributors, zip.WithOperationID("GetRuleStateHistoryTopContributors"))
|
||||
opPost(g, "/rules/:id/history/overall_status", getOverallStateTransitions, zip.WithOperationID("GetOverallStateTransitions"))
|
||||
g.Post("/testRule", testRuleNotification, zip.WithOperationID("TestRuleNotification"))
|
||||
g.Post("/rules/:id/history/stats", getRuleStats, zip.WithOperationID("GetRuleStats"))
|
||||
g.Post("/rules/:id/history/timeline", getRuleStateHistory, zip.WithOperationID("GetRuleStateHistory"))
|
||||
g.Post("/rules/:id/history/top_contributors", getRuleStateHistoryTopContributors, zip.WithOperationID("GetRuleStateHistoryTopContributors"))
|
||||
g.Post("/rules/:id/history/overall_status", getOverallStateTransitions, zip.WithOperationID("GetOverallStateTransitions"))
|
||||
|
||||
// notification channels
|
||||
opGet(g, "/channels", listChannels, zip.WithOperationID("ListChannels"))
|
||||
opGet(g, "/channels/:id", getChannelByID, zip.WithOperationID("GetChannelByID"))
|
||||
opPost(g, "/channels", createChannel, zip.WithStatus(http.StatusCreated), zip.WithOperationID("CreateChannel"))
|
||||
opPut(g, "/channels/:id", updateChannelByID, zip.WithStatus(http.StatusNoContent), zip.WithOperationID("UpdateChannelByID"))
|
||||
opDelete(g, "/channels/:id", deleteChannelByID, zip.WithStatus(http.StatusNoContent), zip.WithOperationID("DeleteChannelByID"))
|
||||
opPost(g, "/channels/test", testChannel, zip.WithStatus(http.StatusNoContent), zip.WithOperationID("TestChannel"))
|
||||
opPost(g, "/testChannel", testChannelDeprecated, zip.WithStatus(http.StatusNoContent), zip.WithOperationID("TestChannelDeprecated"))
|
||||
g.Get("/channels", listChannels, zip.WithOperationID("ListChannels"))
|
||||
g.Get("/channels/:id", getChannelByID, zip.WithOperationID("GetChannelByID"))
|
||||
g.Post("/channels", createChannel, zip.WithStatus(http.StatusCreated), zip.WithOperationID("CreateChannel"))
|
||||
g.Put("/channels/:id", updateChannelByID, zip.WithStatus(http.StatusNoContent), zip.WithOperationID("UpdateChannelByID"))
|
||||
g.Delete("/channels/:id", deleteChannelByID, zip.WithStatus(http.StatusNoContent), zip.WithOperationID("DeleteChannelByID"))
|
||||
g.Post("/channels/test", testChannel, zip.WithStatus(http.StatusNoContent), zip.WithOperationID("TestChannel"))
|
||||
g.Post("/testChannel", testChannelDeprecated, zip.WithStatus(http.StatusNoContent), zip.WithOperationID("TestChannelDeprecated"))
|
||||
|
||||
// route policies
|
||||
opGet(g, "/route_policies", getAllRoutePolicies, zip.WithOperationID("GetAllRoutePolicies"))
|
||||
opGet(g, "/route_policies/:id", getRoutePolicyByID, zip.WithOperationID("GetRoutePolicyByID"))
|
||||
opPost(g, "/route_policies", createRoutePolicy, zip.WithStatus(http.StatusCreated), zip.WithOperationID("CreateRoutePolicy"))
|
||||
opPut(g, "/route_policies/:id", updateRoutePolicy, zip.WithOperationID("UpdateRoutePolicy"))
|
||||
opDelete(g, "/route_policies/:id", deleteRoutePolicyByID, zip.WithStatus(http.StatusNoContent), zip.WithOperationID("DeleteRoutePolicyByID"))
|
||||
g.Get("/route_policies", getAllRoutePolicies, zip.WithOperationID("GetAllRoutePolicies"))
|
||||
g.Get("/route_policies/:id", getRoutePolicyByID, zip.WithOperationID("GetRoutePolicyByID"))
|
||||
g.Post("/route_policies", createRoutePolicy, zip.WithStatus(http.StatusCreated), zip.WithOperationID("CreateRoutePolicy"))
|
||||
g.Put("/route_policies/:id", updateRoutePolicy, zip.WithOperationID("UpdateRoutePolicy"))
|
||||
g.Delete("/route_policies/:id", deleteRoutePolicyByID, zip.WithStatus(http.StatusNoContent), zip.WithOperationID("DeleteRoutePolicyByID"))
|
||||
|
||||
// alerts
|
||||
opGet(g, "/alerts", getAlerts, zip.WithOperationID("GetAlerts"))
|
||||
g.Get("/alerts", getAlerts, zip.WithOperationID("GetAlerts"))
|
||||
}
|
||||
|
||||
// ── alert rules ─────────────────────────────────────────────────────────────
|
||||
|
||||
+22
-22
@@ -33,8 +33,8 @@ package o11y
|
||||
// - Legacy exceptions (listErrors, countErrors, errorFromErrorID,
|
||||
// errorFromGroupID, nextPrevErrorIDs): ViewAccess, every one.
|
||||
//
|
||||
// The FOUR ingest routes — POST /v1/event/{project}/envelope|store/ and POST
|
||||
// /v1/o11y/api/{project}/envelope|store/ — are the deliberate escape hatches,
|
||||
// The FOUR ingest routes — POST /v1/event/{project}/envelope|store and POST
|
||||
// /v1/o11y/api/{project}/envelope|store — are the deliberate escape hatches,
|
||||
// NOT typed here and not on this face at all: ingest is a keyed beacon and a
|
||||
// face is read by a person, so ingest has its own root (relay.go's eventRoot,
|
||||
// the address a minted DSN spells). They are OpenAccess (a Sentry SDK presents a DSN key,
|
||||
@@ -61,27 +61,27 @@ import (
|
||||
// the o11yRoot group. Both roots and the seam they relay through are spelled
|
||||
// once each, in relay.go.
|
||||
func mountSentryErrors(app *zip.App) {
|
||||
gsentry := rooted{app.Group(sentinelRoot), sentinelRoot}
|
||||
opGet(gsentry, "/projects", sentryListProjects)
|
||||
opPost(gsentry, "/projects", sentryCreateProject)
|
||||
opGet(gsentry, "/projects/:id", sentryGetProject)
|
||||
opDelete(gsentry, "/projects/:id", sentryDeleteProject)
|
||||
opPost(gsentry, "/projects/:id/keys/rotate", sentryRotateProjectKey)
|
||||
opGet(gsentry, "/issues", sentryListIssues)
|
||||
opGet(gsentry, "/issues/:id", sentryGetIssue)
|
||||
opPut(gsentry, "/issues/:id", sentryUpdateIssue)
|
||||
opGet(gsentry, "/issues/:id/events", sentryIssueEvents)
|
||||
opGet(gsentry, "/events/:id", sentryGetEvent)
|
||||
gsentry := app.Group(sentinelRoot)
|
||||
gsentry.Get("/projects", sentryListProjects)
|
||||
gsentry.Post("/projects", sentryCreateProject)
|
||||
gsentry.Get("/projects/:id", sentryGetProject)
|
||||
gsentry.Delete("/projects/:id", sentryDeleteProject)
|
||||
gsentry.Post("/projects/:id/keys/rotate", sentryRotateProjectKey)
|
||||
gsentry.Get("/issues", sentryListIssues)
|
||||
gsentry.Get("/issues/:id", sentryGetIssue)
|
||||
gsentry.Put("/issues/:id", sentryUpdateIssue)
|
||||
gsentry.Get("/issues/:id/events", sentryIssueEvents)
|
||||
gsentry.Get("/events/:id", sentryGetEvent)
|
||||
|
||||
go11y := rooted{app.Group(o11yRoot), o11yRoot}
|
||||
opGet(go11y, "/errortracking/issues", errorListIssues)
|
||||
opGet(go11y, "/errortracking/issues/:id", errorGetIssue)
|
||||
opPost(go11y, "/errortracking/issues/:id", errorUpdateIssue)
|
||||
opPost(go11y, "/listErrors", errorsList)
|
||||
opPost(go11y, "/countErrors", errorsCount)
|
||||
opGet(go11y, "/errorFromErrorID", errorFromErrorID)
|
||||
opGet(go11y, "/errorFromGroupID", errorFromGroupID)
|
||||
opGet(go11y, "/nextPrevErrorIDs", nextPrevErrorIDs)
|
||||
go11y := app.Group(o11yRoot)
|
||||
go11y.Get("/errortracking/issues", errorListIssues)
|
||||
go11y.Get("/errortracking/issues/:id", errorGetIssue)
|
||||
go11y.Post("/errortracking/issues/:id", errorUpdateIssue)
|
||||
go11y.Post("/listErrors", errorsList)
|
||||
go11y.Post("/countErrors", errorsCount)
|
||||
go11y.Get("/errorFromErrorID", errorFromErrorID)
|
||||
go11y.Get("/errorFromGroupID", errorFromGroupID)
|
||||
go11y.Get("/nextPrevErrorIDs", nextPrevErrorIDs)
|
||||
}
|
||||
|
||||
// ── sentry projects ─────────────────────────────────────────────────────────
|
||||
|
||||
+9
-9
@@ -38,17 +38,17 @@ func mountSpanMappers(app *zip.App) {
|
||||
// with the collection as its prefix would register the collection routes at
|
||||
// the empty sub-path, which lands them on a TRAILING SLASH the runtime does
|
||||
// not serve. The collection is a path, not a prefix.
|
||||
g := rooted{app.Group(o11yRoot), o11yRoot}
|
||||
g := app.Group(o11yRoot)
|
||||
|
||||
opGet(g, "/span_mapper_groups", spanMapperGroups, op("ListSpanMapperGroups"))
|
||||
opPost(g, "/span_mapper_groups", spanMapperGroupCreate, op("CreateSpanMapperGroup"), zip.WithStatus(http.StatusCreated))
|
||||
opPatch(g, "/span_mapper_groups/:groupId", spanMapperGroupUpdate, op("UpdateSpanMapperGroup"), zip.WithStatus(http.StatusNoContent))
|
||||
opDelete(g, "/span_mapper_groups/:groupId", spanMapperGroupDelete, op("DeleteSpanMapperGroup"), zip.WithStatus(http.StatusNoContent))
|
||||
g.Get("/span_mapper_groups", spanMapperGroups, op("ListSpanMapperGroups"))
|
||||
g.Post("/span_mapper_groups", spanMapperGroupCreate, op("CreateSpanMapperGroup"), zip.WithStatus(http.StatusCreated))
|
||||
g.Patch("/span_mapper_groups/:groupId", spanMapperGroupUpdate, op("UpdateSpanMapperGroup"), zip.WithStatus(http.StatusNoContent))
|
||||
g.Delete("/span_mapper_groups/:groupId", spanMapperGroupDelete, op("DeleteSpanMapperGroup"), zip.WithStatus(http.StatusNoContent))
|
||||
|
||||
opGet(g, "/span_mapper_groups/:groupId/span_mappers", spanMappers, op("ListSpanMappers"))
|
||||
opPost(g, "/span_mapper_groups/:groupId/span_mappers", spanMapperCreate, op("CreateSpanMapper"), zip.WithStatus(http.StatusCreated))
|
||||
opPatch(g, "/span_mapper_groups/:groupId/span_mappers/:mapperId", spanMapperUpdate, op("UpdateSpanMapper"), zip.WithStatus(http.StatusNoContent))
|
||||
opDelete(g, "/span_mapper_groups/:groupId/span_mappers/:mapperId", spanMapperDelete, op("DeleteSpanMapper"), zip.WithStatus(http.StatusNoContent))
|
||||
g.Get("/span_mapper_groups/:groupId/span_mappers", spanMappers, op("ListSpanMappers"))
|
||||
g.Post("/span_mapper_groups/:groupId/span_mappers", spanMapperCreate, op("CreateSpanMapper"), zip.WithStatus(http.StatusCreated))
|
||||
g.Patch("/span_mapper_groups/:groupId/span_mappers/:mapperId", spanMapperUpdate, op("UpdateSpanMapper"), zip.WithStatus(http.StatusNoContent))
|
||||
g.Delete("/span_mapper_groups/:groupId/span_mappers/:mapperId", spanMapperDelete, op("DeleteSpanMapper"), zip.WithStatus(http.StatusNoContent))
|
||||
}
|
||||
|
||||
// ── groups ────────────────────────────────────────────────────────────────────
|
||||
|
||||
+6
-6
@@ -36,12 +36,12 @@ import (
|
||||
// Collection routes register before the parameterised one so an id can never
|
||||
// shadow a collection.
|
||||
func mountTelemetry(app *zip.App) {
|
||||
g := rooted{app.Group(sentinelRoot), sentinelRoot}
|
||||
opPost(g, "/discover", discover)
|
||||
opGet(g, "/logs", logs)
|
||||
opGet(g, "/traces", traces)
|
||||
opGet(g, "/traces/:id", trace)
|
||||
opGet(g, "/stats", stats)
|
||||
g := app.Group(sentinelRoot)
|
||||
g.Post("/discover", discover)
|
||||
g.Get("/logs", logs)
|
||||
g.Get("/traces", traces)
|
||||
g.Get("/traces/:id", trace)
|
||||
g.Get("/stats", stats)
|
||||
}
|
||||
|
||||
// ── the five operations ───────────────────────────────────────────────────────
|
||||
|
||||
Vendored
+329
@@ -0,0 +1,329 @@
|
||||
DELETE /v1/o11y/channels/:id
|
||||
DELETE /v1/o11y/cloud_integrations/:cloud_provider/accounts/:id
|
||||
DELETE /v1/o11y/dashboard_views/:id
|
||||
DELETE /v1/o11y/dashboards/:id
|
||||
DELETE /v1/o11y/dashboards/:id/lock
|
||||
DELETE /v1/o11y/dashboards/:id/public
|
||||
DELETE /v1/o11y/downtime_schedules/:id
|
||||
DELETE /v1/o11y/explorer/views/:viewId
|
||||
DELETE /v1/o11y/gateway/ingestion_keys/:keyId
|
||||
DELETE /v1/o11y/gateway/ingestion_keys/limits/:limitId
|
||||
DELETE /v1/o11y/llm/score/:id
|
||||
DELETE /v1/o11y/llm_pricing_rules/:id
|
||||
DELETE /v1/o11y/metric_reduction_rules/:id
|
||||
DELETE /v1/o11y/roles/:id
|
||||
DELETE /v1/o11y/route_policies/:id
|
||||
DELETE /v1/o11y/rules/:id
|
||||
DELETE /v1/o11y/sentinel/projects/:id
|
||||
DELETE /v1/o11y/service_accounts/:id
|
||||
DELETE /v1/o11y/service_accounts/:id/keys/:fid
|
||||
DELETE /v1/o11y/service_accounts/:id/roles/:rid
|
||||
DELETE /v1/o11y/span_mapper_groups/:groupId
|
||||
DELETE /v1/o11y/span_mapper_groups/:groupId/span_mappers/:mapperId
|
||||
DELETE /v1/o11y/trace-funnels/:funnel_id
|
||||
DELETE /v1/o11y/users/me/dashboards/:id/pins
|
||||
GET /v1/o11y/alerts
|
||||
GET /v1/o11y/autocomplete/aggregate_attributes
|
||||
GET /v1/o11y/autocomplete/attribute_keys
|
||||
GET /v1/o11y/autocomplete/attribute_values
|
||||
GET /v1/o11y/channels
|
||||
GET /v1/o11y/channels/:id
|
||||
GET /v1/o11y/cloud_integrations/:cloud_provider/accounts
|
||||
GET /v1/o11y/cloud_integrations/:cloud_provider/accounts/:id
|
||||
GET /v1/o11y/cloud_integrations/:cloud_provider/accounts/:id/services
|
||||
GET /v1/o11y/cloud_integrations/:cloud_provider/accounts/:id/services/:service_id
|
||||
GET /v1/o11y/cloud_integrations/:cloud_provider/credentials
|
||||
GET /v1/o11y/cloud_integrations/:cloud_provider/services
|
||||
GET /v1/o11y/cloud_integrations/:cloud_provider/services/:service_id
|
||||
GET /v1/o11y/clusters/attribute_keys
|
||||
GET /v1/o11y/clusters/attribute_values
|
||||
GET /v1/o11y/daemonsets/attribute_keys
|
||||
GET /v1/o11y/daemonsets/attribute_values
|
||||
GET /v1/o11y/dashboard_views
|
||||
GET /v1/o11y/dashboards
|
||||
GET /v1/o11y/dashboards/:id
|
||||
GET /v1/o11y/dashboards/:id/public
|
||||
GET /v1/o11y/deployments/attribute_keys
|
||||
GET /v1/o11y/deployments/attribute_values
|
||||
GET /v1/o11y/disks
|
||||
GET /v1/o11y/downtime_schedules
|
||||
GET /v1/o11y/downtime_schedules/:id
|
||||
GET /v1/o11y/errorFromErrorID
|
||||
GET /v1/o11y/errorFromGroupID
|
||||
GET /v1/o11y/errortracking/issues
|
||||
GET /v1/o11y/errortracking/issues/:id
|
||||
GET /v1/o11y/explorer/views
|
||||
GET /v1/o11y/explorer/views/:viewId
|
||||
GET /v1/o11y/features
|
||||
GET /v1/o11y/fields/keys
|
||||
GET /v1/o11y/fields/values
|
||||
GET /v1/o11y/filter_suggestions
|
||||
GET /v1/o11y/gateway/ingestion_keys
|
||||
GET /v1/o11y/gateway/ingestion_keys/search
|
||||
GET /v1/o11y/global/config
|
||||
GET /v1/o11y/health
|
||||
GET /v1/o11y/healthz
|
||||
GET /v1/o11y/hosts/attribute_keys
|
||||
GET /v1/o11y/hosts/attribute_values
|
||||
GET /v1/o11y/infra_monitoring/checks
|
||||
GET /v1/o11y/infra_onboarding/k8s/status
|
||||
GET /v1/o11y/integrations
|
||||
GET /v1/o11y/integrations/:integrationId
|
||||
GET /v1/o11y/integrations/:integrationId/connection_status
|
||||
GET /v1/o11y/jobs/attribute_keys
|
||||
GET /v1/o11y/jobs/attribute_values
|
||||
GET /v1/o11y/licenses
|
||||
GET /v1/o11y/licenses/active
|
||||
GET /v1/o11y/livez
|
||||
GET /v1/o11y/llm/annotation
|
||||
GET /v1/o11y/llm/observations
|
||||
GET /v1/o11y/llm/score/:id
|
||||
GET /v1/o11y/llm/scores
|
||||
GET /v1/o11y/llm/sessions
|
||||
GET /v1/o11y/llm/traces
|
||||
GET /v1/o11y/llm/users
|
||||
GET /v1/o11y/llm_pricing_rules
|
||||
GET /v1/o11y/llm_pricing_rules/:id
|
||||
GET /v1/o11y/logs
|
||||
GET /v1/o11y/logs/aggregate
|
||||
GET /v1/o11y/logs/fields
|
||||
GET /v1/o11y/logs/livetail
|
||||
GET /v1/o11y/logs/pipelines/:version
|
||||
GET /v1/o11y/logs/promote_paths
|
||||
GET /v1/o11y/metric/metric_metadata
|
||||
GET /v1/o11y/metric_reduction_rules
|
||||
GET /v1/o11y/metric_reduction_rules/:id
|
||||
GET /v1/o11y/metric_reduction_rules/stats
|
||||
GET /v1/o11y/metric_reduction_rules/timeseries
|
||||
GET /v1/o11y/metrics
|
||||
GET /v1/o11y/metrics/alerts
|
||||
GET /v1/o11y/metrics/attributes
|
||||
GET /v1/o11y/metrics/dashboards
|
||||
GET /v1/o11y/metrics/highlights
|
||||
GET /v1/o11y/metrics/metadata
|
||||
GET /v1/o11y/metrics/onboarding
|
||||
GET /v1/o11y/namespaces/attribute_keys
|
||||
GET /v1/o11y/namespaces/attribute_values
|
||||
GET /v1/o11y/nextPrevErrorIDs
|
||||
GET /v1/o11y/nodes/attribute_keys
|
||||
GET /v1/o11y/nodes/attribute_values
|
||||
GET /v1/o11y/org/preferences
|
||||
GET /v1/o11y/org/preferences/:name
|
||||
GET /v1/o11y/orgs/me
|
||||
GET /v1/o11y/orgs/me/filters
|
||||
GET /v1/o11y/orgs/me/filters/:signal
|
||||
GET /v1/o11y/pods/attribute_keys
|
||||
GET /v1/o11y/pods/attribute_values
|
||||
GET /v1/o11y/processes/attribute_keys
|
||||
GET /v1/o11y/processes/attribute_values
|
||||
GET /v1/o11y/public/dashboards/:id
|
||||
GET /v1/o11y/public/dashboards/:id/widgets/:idx/query_range
|
||||
GET /v1/o11y/pvcs/attribute_keys
|
||||
GET /v1/o11y/pvcs/attribute_values
|
||||
GET /v1/o11y/query
|
||||
GET /v1/o11y/query_progress
|
||||
GET /v1/o11y/query_range
|
||||
GET /v1/o11y/readyz
|
||||
GET /v1/o11y/roles
|
||||
GET /v1/o11y/roles/:id
|
||||
GET /v1/o11y/route_policies
|
||||
GET /v1/o11y/route_policies/:id
|
||||
GET /v1/o11y/rules
|
||||
GET /v1/o11y/rules/:id
|
||||
GET /v1/o11y/rules/:id/history/filter_keys
|
||||
GET /v1/o11y/rules/:id/history/filter_values
|
||||
GET /v1/o11y/rules/:id/history/overall_status
|
||||
GET /v1/o11y/rules/:id/history/stats
|
||||
GET /v1/o11y/rules/:id/history/timeline
|
||||
GET /v1/o11y/rules/:id/history/top_contributors
|
||||
GET /v1/o11y/sentinel/events/:id
|
||||
GET /v1/o11y/sentinel/issues
|
||||
GET /v1/o11y/sentinel/issues/:id
|
||||
GET /v1/o11y/sentinel/issues/:id/events
|
||||
GET /v1/o11y/sentinel/logs
|
||||
GET /v1/o11y/sentinel/projects
|
||||
GET /v1/o11y/sentinel/projects/:id
|
||||
GET /v1/o11y/sentinel/stats
|
||||
GET /v1/o11y/sentinel/traces
|
||||
GET /v1/o11y/sentinel/traces/:id
|
||||
GET /v1/o11y/service_accounts
|
||||
GET /v1/o11y/service_accounts/:id
|
||||
GET /v1/o11y/service_accounts/:id/keys
|
||||
GET /v1/o11y/service_accounts/:id/roles
|
||||
GET /v1/o11y/service_accounts/me
|
||||
GET /v1/o11y/services/list
|
||||
GET /v1/o11y/settings/apdex
|
||||
GET /v1/o11y/settings/ttl
|
||||
GET /v1/o11y/span_mapper_groups
|
||||
GET /v1/o11y/span_mapper_groups/:groupId/span_mappers
|
||||
GET /v1/o11y/statefulsets/attribute_keys
|
||||
GET /v1/o11y/statefulsets/attribute_values
|
||||
GET /v1/o11y/stats
|
||||
GET /v1/o11y/trace-funnels/:funnel_id
|
||||
GET /v1/o11y/trace-funnels/list
|
||||
GET /v1/o11y/traces/:traceId
|
||||
GET /v1/o11y/traces/fields
|
||||
GET /v1/o11y/usage
|
||||
GET /v1/o11y/user/preferences
|
||||
GET /v1/o11y/user/preferences/:name
|
||||
GET /v1/o11y/users/me
|
||||
GET /v1/o11y/users/me/dashboards
|
||||
GET /v1/o11y/version
|
||||
GET /v1/o11y/zeus/hosts
|
||||
PATCH /v1/o11y/dashboards/:id
|
||||
PATCH /v1/o11y/gateway/ingestion_keys/:keyId
|
||||
PATCH /v1/o11y/gateway/ingestion_keys/limits/:limitId
|
||||
PATCH /v1/o11y/rules/:id
|
||||
PATCH /v1/o11y/span_mapper_groups/:groupId
|
||||
PATCH /v1/o11y/span_mapper_groups/:groupId/span_mappers/:mapperId
|
||||
POST /v1/event/:project/envelope
|
||||
POST /v1/event/:project/store
|
||||
POST /v1/o11y/api/:project_id/envelope
|
||||
POST /v1/o11y/api/:project_id/store
|
||||
POST /v1/o11y/authz/check
|
||||
POST /v1/o11y/auto_complete/attribute_values
|
||||
POST /v1/o11y/channels
|
||||
POST /v1/o11y/channels/test
|
||||
POST /v1/o11y/cloud-integrations/:cloud_provider/agent-check-in
|
||||
POST /v1/o11y/cloud_integrations/:cloud_provider/accounts
|
||||
POST /v1/o11y/cloud_integrations/:cloud_provider/accounts/check_in
|
||||
POST /v1/o11y/clusters/list
|
||||
POST /v1/o11y/countErrors
|
||||
POST /v1/o11y/daemonsets/list
|
||||
POST /v1/o11y/dashboard_views
|
||||
POST /v1/o11y/dashboards
|
||||
POST /v1/o11y/dashboards/:id/clone
|
||||
POST /v1/o11y/dashboards/:id/public
|
||||
POST /v1/o11y/dependency_graph
|
||||
POST /v1/o11y/deployments/list
|
||||
POST /v1/o11y/downtime_schedules
|
||||
POST /v1/o11y/errortracking/issues/:id
|
||||
POST /v1/o11y/event
|
||||
POST /v1/o11y/explorer/views
|
||||
POST /v1/o11y/export_raw_data
|
||||
POST /v1/o11y/gateway/ingestion_keys
|
||||
POST /v1/o11y/gateway/ingestion_keys/:keyId/limits
|
||||
POST /v1/o11y/hosts/list
|
||||
POST /v1/o11y/infra_monitoring/clusters
|
||||
POST /v1/o11y/infra_monitoring/daemonsets
|
||||
POST /v1/o11y/infra_monitoring/deployments
|
||||
POST /v1/o11y/infra_monitoring/hosts
|
||||
POST /v1/o11y/infra_monitoring/jobs
|
||||
POST /v1/o11y/infra_monitoring/namespaces
|
||||
POST /v1/o11y/infra_monitoring/nodes
|
||||
POST /v1/o11y/infra_monitoring/pods
|
||||
POST /v1/o11y/infra_monitoring/pvcs
|
||||
POST /v1/o11y/infra_monitoring/statefulsets
|
||||
POST /v1/o11y/integrations/install
|
||||
POST /v1/o11y/integrations/uninstall
|
||||
POST /v1/o11y/jobs/list
|
||||
POST /v1/o11y/listErrors
|
||||
POST /v1/o11y/llm/annotation
|
||||
POST /v1/o11y/llm/scores
|
||||
POST /v1/o11y/logs/fields
|
||||
POST /v1/o11y/logs/pipelines
|
||||
POST /v1/o11y/logs/pipelines/preview
|
||||
POST /v1/o11y/logs/promote_paths
|
||||
POST /v1/o11y/messaging-queues/kafka/consumer-lag/consumer-details
|
||||
POST /v1/o11y/messaging-queues/kafka/consumer-lag/network-latency
|
||||
POST /v1/o11y/messaging-queues/kafka/consumer-lag/producer-details
|
||||
POST /v1/o11y/messaging-queues/kafka/onboarding/consumers
|
||||
POST /v1/o11y/messaging-queues/kafka/onboarding/kafka
|
||||
POST /v1/o11y/messaging-queues/kafka/onboarding/producers
|
||||
POST /v1/o11y/messaging-queues/kafka/partition-latency/consumer
|
||||
POST /v1/o11y/messaging-queues/kafka/partition-latency/overview
|
||||
POST /v1/o11y/messaging-queues/kafka/span/evaluation
|
||||
POST /v1/o11y/messaging-queues/kafka/topic-throughput/consumer
|
||||
POST /v1/o11y/messaging-queues/kafka/topic-throughput/consumer-details
|
||||
POST /v1/o11y/messaging-queues/kafka/topic-throughput/producer
|
||||
POST /v1/o11y/messaging-queues/kafka/topic-throughput/producer-details
|
||||
POST /v1/o11y/messaging-queues/queue-overview
|
||||
POST /v1/o11y/metric_reduction_rules
|
||||
POST /v1/o11y/metric_reduction_rules/preview
|
||||
POST /v1/o11y/metrics/inspect
|
||||
POST /v1/o11y/metrics/metadata
|
||||
POST /v1/o11y/metrics/stats
|
||||
POST /v1/o11y/metrics/treemap
|
||||
POST /v1/o11y/namespaces/list
|
||||
POST /v1/o11y/nodes/list
|
||||
POST /v1/o11y/pods/list
|
||||
POST /v1/o11y/processes/list
|
||||
POST /v1/o11y/pvcs/list
|
||||
POST /v1/o11y/query_filter/analyze
|
||||
POST /v1/o11y/query_range
|
||||
POST /v1/o11y/query_range/format
|
||||
POST /v1/o11y/query_range/preview
|
||||
POST /v1/o11y/roles
|
||||
POST /v1/o11y/route_policies
|
||||
POST /v1/o11y/rules
|
||||
POST /v1/o11y/rules/:id/history/overall_status
|
||||
POST /v1/o11y/rules/:id/history/stats
|
||||
POST /v1/o11y/rules/:id/history/timeline
|
||||
POST /v1/o11y/rules/:id/history/top_contributors
|
||||
POST /v1/o11y/rules/test
|
||||
POST /v1/o11y/sentinel/discover
|
||||
POST /v1/o11y/sentinel/projects
|
||||
POST /v1/o11y/sentinel/projects/:id/keys/rotate
|
||||
POST /v1/o11y/service/entry_point_operations
|
||||
POST /v1/o11y/service/top_level_operations
|
||||
POST /v1/o11y/service/top_operations
|
||||
POST /v1/o11y/service_accounts
|
||||
POST /v1/o11y/service_accounts/:id/keys
|
||||
POST /v1/o11y/service_accounts/:id/roles
|
||||
POST /v1/o11y/services
|
||||
POST /v1/o11y/settings/apdex
|
||||
POST /v1/o11y/settings/ttl
|
||||
POST /v1/o11y/span_mapper_groups
|
||||
POST /v1/o11y/span_mapper_groups/:groupId/span_mappers
|
||||
POST /v1/o11y/span_percentile
|
||||
POST /v1/o11y/statefulsets/list
|
||||
POST /v1/o11y/substitute_vars
|
||||
POST /v1/o11y/testChannel
|
||||
POST /v1/o11y/testRule
|
||||
POST /v1/o11y/third-party-apis/overview/domain
|
||||
POST /v1/o11y/third-party-apis/overview/list
|
||||
POST /v1/o11y/trace-funnels/:funnel_id/analytics/error-traces
|
||||
POST /v1/o11y/trace-funnels/:funnel_id/analytics/overview
|
||||
POST /v1/o11y/trace-funnels/:funnel_id/analytics/slow-traces
|
||||
POST /v1/o11y/trace-funnels/:funnel_id/analytics/steps
|
||||
POST /v1/o11y/trace-funnels/:funnel_id/analytics/steps/overview
|
||||
POST /v1/o11y/trace-funnels/:funnel_id/analytics/validate
|
||||
POST /v1/o11y/trace-funnels/analytics/error-traces
|
||||
POST /v1/o11y/trace-funnels/analytics/overview
|
||||
POST /v1/o11y/trace-funnels/analytics/slow-traces
|
||||
POST /v1/o11y/trace-funnels/analytics/steps
|
||||
POST /v1/o11y/trace-funnels/analytics/steps/overview
|
||||
POST /v1/o11y/trace-funnels/analytics/validate
|
||||
POST /v1/o11y/trace-funnels/new
|
||||
POST /v1/o11y/traces/:traceId/aggregations
|
||||
POST /v1/o11y/traces/:traceId/flamegraph
|
||||
POST /v1/o11y/traces/:traceId/waterfall
|
||||
POST /v1/o11y/traces/fields
|
||||
POST /v1/o11y/variables/query
|
||||
PUT /v1/o11y/channels/:id
|
||||
PUT /v1/o11y/cloud_integrations/:cloud_provider/accounts/:id
|
||||
PUT /v1/o11y/cloud_integrations/:cloud_provider/accounts/:id/services/:service_id
|
||||
PUT /v1/o11y/dashboard_views/:id
|
||||
PUT /v1/o11y/dashboards/:id
|
||||
PUT /v1/o11y/dashboards/:id/lock
|
||||
PUT /v1/o11y/dashboards/:id/public
|
||||
PUT /v1/o11y/downtime_schedules/:id
|
||||
PUT /v1/o11y/explorer/views/:viewId
|
||||
PUT /v1/o11y/llm_pricing_rules
|
||||
PUT /v1/o11y/metric_reduction_rules/:id
|
||||
PUT /v1/o11y/org/preferences/:name
|
||||
PUT /v1/o11y/orgs/me
|
||||
PUT /v1/o11y/orgs/me/filters
|
||||
PUT /v1/o11y/roles/:id
|
||||
PUT /v1/o11y/route_policies/:id
|
||||
PUT /v1/o11y/rules/:id
|
||||
PUT /v1/o11y/sentinel/issues/:id
|
||||
PUT /v1/o11y/service_accounts/:id
|
||||
PUT /v1/o11y/service_accounts/:id/keys/:fid
|
||||
PUT /v1/o11y/service_accounts/me
|
||||
PUT /v1/o11y/trace-funnels/:funnel_id
|
||||
PUT /v1/o11y/trace-funnels/steps/update
|
||||
PUT /v1/o11y/user/preferences/:name
|
||||
PUT /v1/o11y/users/me/dashboards/:id/pins
|
||||
PUT /v1/o11y/zeus/hosts
|
||||
PUT /v1/o11y/zeus/profiles
|
||||
+19
-19
@@ -40,31 +40,31 @@ import (
|
||||
// parameterised /:funnel_id ones so a funnel id can never shadow them — the same
|
||||
// discipline the mux tree keeps by registering the literals first.
|
||||
func mountTraceFunnels(app *zip.App) {
|
||||
g := rooted{app.Group(o11yRoot + "/trace-funnels"), o11yRoot + "/trace-funnels"}
|
||||
g := app.Group(o11yRoot + "/trace-funnels")
|
||||
|
||||
opPost(g, "/new", funnelCreate, op("CreateTraceFunnel"))
|
||||
opGet(g, "/list", funnelList, op("ListTraceFunnels"))
|
||||
opPut(g, "/steps/update", funnelStepsUpdate, op("UpdateTraceFunnelSteps"))
|
||||
g.Post("/new", funnelCreate, op("CreateTraceFunnel"))
|
||||
g.Get("/list", funnelList, op("ListTraceFunnels"))
|
||||
g.Put("/steps/update", funnelStepsUpdate, op("UpdateTraceFunnelSteps"))
|
||||
|
||||
// The ad-hoc family: the funnel travels in the body, nothing is stored.
|
||||
opPost(g, "/analytics/validate", draftFunnelValidate, op("ValidateDraftFunnelTraces"))
|
||||
opPost(g, "/analytics/overview", draftFunnelOverview, op("GetDraftFunnelOverview"))
|
||||
opPost(g, "/analytics/steps", draftFunnelSteps, op("GetDraftFunnelStepMetrics"))
|
||||
opPost(g, "/analytics/steps/overview", draftFunnelStepOverview, op("GetDraftFunnelStepOverview"))
|
||||
opPost(g, "/analytics/slow-traces", draftFunnelSlowTraces, op("GetDraftFunnelSlowTraces"))
|
||||
opPost(g, "/analytics/error-traces", draftFunnelErrorTraces, op("GetDraftFunnelErrorTraces"))
|
||||
g.Post("/analytics/validate", draftFunnelValidate, op("ValidateDraftFunnelTraces"))
|
||||
g.Post("/analytics/overview", draftFunnelOverview, op("GetDraftFunnelOverview"))
|
||||
g.Post("/analytics/steps", draftFunnelSteps, op("GetDraftFunnelStepMetrics"))
|
||||
g.Post("/analytics/steps/overview", draftFunnelStepOverview, op("GetDraftFunnelStepOverview"))
|
||||
g.Post("/analytics/slow-traces", draftFunnelSlowTraces, op("GetDraftFunnelSlowTraces"))
|
||||
g.Post("/analytics/error-traces", draftFunnelErrorTraces, op("GetDraftFunnelErrorTraces"))
|
||||
|
||||
opGet(g, "/:funnel_id", funnelGet, op("GetTraceFunnel"))
|
||||
opPut(g, "/:funnel_id", funnelUpdate, op("UpdateTraceFunnel"))
|
||||
opDelete(g, "/:funnel_id", funnelDelete, op("DeleteTraceFunnel"))
|
||||
g.Get("/:funnel_id", funnelGet, op("GetTraceFunnel"))
|
||||
g.Put("/:funnel_id", funnelUpdate, op("UpdateTraceFunnel"))
|
||||
g.Delete("/:funnel_id", funnelDelete, op("DeleteTraceFunnel"))
|
||||
|
||||
// The saved family: the funnel is named by the path, the body is the window.
|
||||
opPost(g, "/:funnel_id/analytics/validate", funnelValidate, op("ValidateTraceFunnelTraces"))
|
||||
opPost(g, "/:funnel_id/analytics/overview", funnelOverview, op("GetTraceFunnelOverview"))
|
||||
opPost(g, "/:funnel_id/analytics/steps", funnelSteps, op("GetTraceFunnelStepMetrics"))
|
||||
opPost(g, "/:funnel_id/analytics/steps/overview", funnelStepOverview, op("GetTraceFunnelStepOverview"))
|
||||
opPost(g, "/:funnel_id/analytics/slow-traces", funnelSlowTraces, op("GetTraceFunnelSlowTraces"))
|
||||
opPost(g, "/:funnel_id/analytics/error-traces", funnelErrorTraces, op("GetTraceFunnelErrorTraces"))
|
||||
g.Post("/:funnel_id/analytics/validate", funnelValidate, op("ValidateTraceFunnelTraces"))
|
||||
g.Post("/:funnel_id/analytics/overview", funnelOverview, op("GetTraceFunnelOverview"))
|
||||
g.Post("/:funnel_id/analytics/steps", funnelSteps, op("GetTraceFunnelStepMetrics"))
|
||||
g.Post("/:funnel_id/analytics/steps/overview", funnelStepOverview, op("GetTraceFunnelStepOverview"))
|
||||
g.Post("/:funnel_id/analytics/slow-traces", funnelSlowTraces, op("GetTraceFunnelSlowTraces"))
|
||||
g.Post("/:funnel_id/analytics/error-traces", funnelErrorTraces, op("GetTraceFunnelErrorTraces"))
|
||||
}
|
||||
|
||||
// ── the funnel itself ─────────────────────────────────────────────────────────
|
||||
|
||||
@@ -39,13 +39,13 @@ import (
|
||||
// so a trace id can never shadow the catalog — the same defence the mux tree
|
||||
// gives them by registering fields first.
|
||||
func mountTraces(app *zip.App) {
|
||||
g := rooted{app.Group(o11yRoot), o11yRoot}
|
||||
opGet(g, "/traces/fields", traceFields, op("GetTraceFields"))
|
||||
opPost(g, "/traces/fields", traceFieldUpdate, op("UpdateTraceField"))
|
||||
opGet(g, "/traces/:traceId", traceSpans, op("SearchTraces"))
|
||||
opPost(g, "/traces/:traceId/waterfall", traceWaterfall, op("GetWaterfallV4"))
|
||||
opPost(g, "/traces/:traceId/flamegraph", traceFlamegraph, op("GetFlamegraph"))
|
||||
opPost(g, "/traces/:traceId/aggregations", traceAggregations, op("GetTraceAggregations"))
|
||||
g := app.Group(o11yRoot)
|
||||
g.Get("/traces/fields", traceFields, op("GetTraceFields"))
|
||||
g.Post("/traces/fields", traceFieldUpdate, op("UpdateTraceField"))
|
||||
g.Get("/traces/:traceId", traceSpans, op("SearchTraces"))
|
||||
g.Post("/traces/:traceId/waterfall", traceWaterfall, op("GetWaterfallV4"))
|
||||
g.Post("/traces/:traceId/flamegraph", traceFlamegraph, op("GetFlamegraph"))
|
||||
g.Post("/traces/:traceId/aggregations", traceAggregations, op("GetTraceAggregations"))
|
||||
}
|
||||
|
||||
// ── the six operations ────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user