Files
o11y/spanmappers.go
zeekayandHanzo Dev cae99adb44 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]>
2026-09-17 15:02:30 -07:00

215 lines
9.2 KiB
Go

package o11y
// The SPAN-MAPPER face — the ingest-time rules that move or copy span
// attributes into resource attributes before a span is stored — as TYPED ops.
//
// These were eight of the routes that reached traffic only through the
// delegation wildcard, and a route behind a wildcard is in no document: no SDK
// method, no CLI command, no agent tool, no reference page. Span mappers are the
// one surface a customer must edit to fix their own telemetry shape, and until
// now they could only do it from a hand-written HTTP call. Typing them is what
// puts the eight operations in the document and therefore in every projection
// built from it.
//
// A GROUP AND ITS MAPPERS. A group carries the condition that decides WHICH
// spans it applies to; the mappers under it carry the moves. That nesting is the
// path shape (/span_mapper_groups/:groupId/span_mappers/:mapperId) and it is why
// the mapper ops take two ids.
//
// THE WIRE DOES NOT MOVE: each op hands the call to the SAME runtime handler the
// wildcard delegates to (relay.go), so the ViewAccess read gate and the
// AdminAccess write gate the runtime has always run still run, in the order they
// always did. The four writes answer 204 with no body, and the ops declare that
// status rather than inventing a payload.
//go:generate go run github.com/zap-proto/zip/cmd/zipdoc
import (
"context"
"net/http"
"github.com/hanzoai/o11y/pkg/types/spantypes"
"github.com/zap-proto/zip"
)
// mountSpanMappers registers the eight typed span-mapper ops.
func mountSpanMappers(app *zip.App) {
// The group is o11yRoot, not o11yRoot+"/span_mapper_groups": a zip group
// 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 := app.Group(o11yRoot)
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))
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 ────────────────────────────────────────────────────────────────────
// spanMapperGroups lists the caller's org's mapping groups, optionally only the
// enabled ones.
//
// Callers need the viewer role; the runtime's own gate enforces it.
func spanMapperGroups(ctx context.Context, in *O11ySpanMapperGroupsIn) (*O11ySpanMapperGroupsOut, error) {
out := new(O11ySpanMapperGroupsOut)
return out, relay(ctx, query("enabled", in.Enabled), nil, out)
}
// spanMapperGroupCreate creates a mapping group: the name it is known by, the
// span and resource attributes whose presence selects a span into it, and
// whether it is on.
//
// Callers need the admin role; the runtime's own gate enforces it.
func spanMapperGroupCreate(ctx context.Context, in *spantypes.PostableSpanMapperGroup) (*O11ySpanMapperGroupOut, error) {
out := new(O11ySpanMapperGroupOut)
return out, relay(ctx, nil, in, out)
}
// spanMapperGroupUpdate changes a group's name, condition or enabled state.
// Every field is optional and only the ones sent are applied.
//
// Callers need the admin role; the runtime's own gate enforces it.
func spanMapperGroupUpdate(ctx context.Context, in *O11ySpanMapperGroupUpdateIn) (*struct{}, error) {
return nil, relay(ctx, nil, in.UpdatableSpanMapperGroup, nil)
}
// spanMapperGroupDelete deletes a mapping group and every mapper under it.
//
// Callers need the admin role; the runtime's own gate enforces it.
func spanMapperGroupDelete(ctx context.Context, in *O11ySpanMapperGroupRef) (*struct{}, error) {
return nil, relay(ctx, nil, nil, nil)
}
// ── mappers ───────────────────────────────────────────────────────────────────
// spanMappers lists the mappers belonging to one group, in the order they are
// applied.
//
// Callers need the viewer role; the runtime's own gate enforces it.
func spanMappers(ctx context.Context, in *O11ySpanMapperGroupRef) (*O11ySpanMappersOut, error) {
out := new(O11ySpanMappersOut)
return out, relay(ctx, nil, nil, out)
}
// spanMapperCreate adds a mapper to a group: which field context it reads, the
// move or copy it performs, and whether it is on.
//
// Callers need the admin role; the runtime's own gate enforces it.
func spanMapperCreate(ctx context.Context, in *O11ySpanMapperCreateIn) (*O11ySpanMapperOut, error) {
out := new(O11ySpanMapperOut)
return out, relay(ctx, nil, in.PostableSpanMapper, out)
}
// spanMapperUpdate changes a mapper's field context, config or enabled state.
// Every field is optional and only the ones sent are applied.
//
// Callers need the admin role; the runtime's own gate enforces it.
func spanMapperUpdate(ctx context.Context, in *O11ySpanMapperUpdateIn) (*struct{}, error) {
return nil, relay(ctx, nil, in.UpdatableSpanMapper, nil)
}
// spanMapperDelete deletes one mapper from a group.
//
// Callers need the admin role; the runtime's own gate enforces it.
func spanMapperDelete(ctx context.Context, in *O11ySpanMapperRef) (*struct{}, error) {
return nil, relay(ctx, nil, nil, nil)
}
// groupPath and mapperPath are the one place these ids become a path. Each id
// goes on VERBATIM, as the segment the router matched.
func groupPath(groupID string) string { return o11yRoot + "/span_mapper_groups/" + groupID }
func mapperPath(groupID, mapperID string) string {
return groupPath(groupID) + "/span_mappers/" + mapperID
}
// ── inputs ────────────────────────────────────────────────────────────────────
// O11ySpanMapperGroupsIn narrows the group listing.
type O11ySpanMapperGroupsIn struct {
// Enabled lists only the groups that are on. False lists all of them —
// which is what the runtime has always read an absent parameter as.
Enabled bool `json:"-" url:"enabled"`
}
// O11ySpanMapperGroupRef names one mapping group.
type O11ySpanMapperGroupRef struct {
// GroupID is the group's id. Required.
GroupID string `json:"-" url:"groupId" validate:"required"`
}
// O11ySpanMapperGroupUpdateIn names the group and carries the change.
type O11ySpanMapperGroupUpdateIn struct {
// GroupID is the group to update. Required.
GroupID string `json:"-" url:"groupId" validate:"required"`
spantypes.UpdatableSpanMapperGroup
}
// O11ySpanMapperRef names one mapper inside its group.
type O11ySpanMapperRef struct {
// GroupID is the group the mapper belongs to. Required.
GroupID string `json:"-" url:"groupId" validate:"required"`
// MapperID is the mapper's id. Required.
MapperID string `json:"-" url:"mapperId" validate:"required"`
}
// O11ySpanMapperCreateIn names the group and describes the mapper to add.
type O11ySpanMapperCreateIn struct {
// GroupID is the group to add to. Required.
GroupID string `json:"-" url:"groupId" validate:"required"`
spantypes.PostableSpanMapper
}
// O11ySpanMapperUpdateIn names the mapper and carries the change.
type O11ySpanMapperUpdateIn struct {
// GroupID is the group the mapper belongs to. Required.
GroupID string `json:"-" url:"groupId" validate:"required"`
// MapperID is the mapper to update. Required.
MapperID string `json:"-" url:"mapperId" validate:"required"`
spantypes.UpdatableSpanMapper
}
// ── answers ───────────────────────────────────────────────────────────────────
// O11ySpanMapperGroupsOut is the org's mapping groups.
type O11ySpanMapperGroupsOut struct {
// Status is "success".
Status string `json:"status"`
// Data holds the groups.
Data spantypes.GettableSpanMapperGroups `json:"data"`
}
// O11ySpanMapperGroupOut is one mapping group.
type O11ySpanMapperGroupOut struct {
// Status is "success".
Status string `json:"status"`
// Data is the group.
Data spantypes.GettableSpanMapperGroup `json:"data"`
}
// O11ySpanMappersOut is one group's mappers.
type O11ySpanMappersOut struct {
// Status is "success".
Status string `json:"status"`
// Data holds the mappers.
Data spantypes.GettableSpanMappers `json:"data"`
}
// O11ySpanMapperOut is one mapper.
type O11ySpanMapperOut struct {
// Status is "success".
Status string `json:"status"`
// Data is the mapper.
Data spantypes.GettableSpanMapper `json:"data"`
}