mirror of
https://github.com/hanzoai/o11y.git
synced 2026-10-15 12:58:33 +00:00
mount: the wildcard is gone — every one of 367 routes is named
The delegation catch-all made three facts indistinguishable: a route that was
converted, a route that was not, and a route that did not exist. That is not a
theoretical cost — it is exactly how 83 typed ops shipped in files nothing
called, passing a full green suite while every one of those routes fell through
the wildcard.
So: derive the gap precisely, close it, and delete the catch-all.
THE GAP, re-derived. Extract every path the runtime registers (134 in
pkg/query-service/app/routes_*.go, 233 in pkg/apiserver/o11yapiserver/*.go — the
134 matching the count routes.go documents), normalise :params on both sides,
and diff against what Mount installs. 367 runtime routes, 43 unnamed. Not the
four hatch classes the plan expected: eleven were genuine hatches and
THIRTY-TWO were simply never converted.
CONVERTED (32 new typed ops, three new slices):
* traces.go — a trace's spans, the trace field catalog and its tuning
write, waterfall, flamegraph, span aggregations
* tracefunnels.go — funnel CRUD plus twelve analytics reads, six over a saved
funnel and six over one described inline
* spanmappers.go — the ingest-time attribute mapping groups and their mappers
THE ELEVEN HATCHES, each its own route literal with its reason beside it
(mount.go mountHatches): four streams with no single answer to name (livetail,
query_progress long-poll, its websocket, the chunked export), three 303
redirects whose answer is a Location and not a body, and four Sentry-SDK ingest
doors — a foreign protocol we RECEIVE, whose /api/ segment the SDK hardcodes and
which is not ours to rename.
TWO OF THOSE ELEVEN WERE UNREACHABLE. /ws/query_progress and the two
/v1/sentry/<project>/{envelope,store}/ doors sit outside /v1/o11y, so the old
catch-all never saw them: a Sentry SDK pointed at the clean root got a 404 in
the composed binary, and telemetry_test.go passed only because the test
registered the wildcard the deployment did not have. Naming every route is what
surfaced it.
ONE SEAM, TWO ROOTS. Five copies of the relay function (relay, relayAt,
relayO11y, relayAPM, send) differing only in a prefix and whether they took
query parameters, and NINE names for two values — identityPrefix, infraPrefix,
integrationsPrefix, metricsRoot, apmPrefix, accessPrefix, rulesAlertsPrefix and
o11yRoot were all "/v1/o11y". Ops now pass the whole public path and relay
concatenates nothing: 193 call sites, one implementation, -520 lines.
Also: the per-slice *_zipdoc.go files fold back into zipdoc_gen.go as their own
headers instructed; the health probes' fall-through names its destination
instead of relying on a catch-all being registered after it (they would have
started 404ing the moment SetHealth was late — precisely when liveness matters);
O11yLogField/O11yLogFieldsOut/O11yLogFieldSetting become
O11yTelemetryField/O11yFieldCatalogOut/O11yFieldSetting because logs and traces
tune ONE catalog through one runtime type.
PAYOFF, counted off the document rather than the router: 353 operations in the
published OpenAPI spec and 353 MCP tools, from 0 for this surface before the
conversion began. 353 typed + 11 hatches + 3 probes = 367.
RECORDED, NOT FIXED: the runtime renders with jsoniter and zip with the stdlib
encoder, and jsoniter does not implement `,omitzero` — so for the seventeen
fields across pkg/types that use it the runtime writes the zero value where a
typed op omits the key. Pre-existing on every shipped op reaching
TelemetryFieldKey. Pinned by traces_test.go rather than hidden.
gorilla/mux and gin stay: mux is the runtime's own router in 100 files, and gin
is imported only for its form binder by pkg/valuer/valuer.go
(ginbinding.BindUnmarshaler) and pkg/http/binding/query.go
(ginbinding.MapFormWithTag). go mod tidy dropped 687 lines of go.mod/go.sum.
Build and full suite green: 133 packages ok, 0 fail.
Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
@@ -12,7 +12,7 @@ package o11y
|
||||
//
|
||||
// THE WIRE DOES NOT MOVE, and the way it does not move is that these ops do not
|
||||
// re-implement access control. Each hands the call to the SAME runtime handler
|
||||
// the wildcard delegates to (see send), so identity resolution, the org gate,
|
||||
// the wildcard delegates to (see relay.go), so identity resolution, the org gate,
|
||||
// the CheckResources/OpenAccess gate on every route, the FGA resource checks,
|
||||
// the audit record and the success envelope are all still the runtime's,
|
||||
// executed in the order they always were. What is new here is the TYPE — the In
|
||||
@@ -30,33 +30,19 @@ package o11y
|
||||
// dispatches end in the same handler.
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"time"
|
||||
|
||||
"github.com/zap-proto/zip"
|
||||
)
|
||||
|
||||
// accessPrefix is the face's path root, spelled ONCE: the group the ops
|
||||
// register on, and the first segment send rebuilds a call onto.
|
||||
const accessPrefix = "/v1/o11y"
|
||||
|
||||
// op names a typed registration with the operation id the face has always
|
||||
// published — the swaggest ids (CreateRole, ListServiceAccounts, …) carry over
|
||||
// verbatim so the operation's name survives the move into the composed
|
||||
// document.
|
||||
func op(id string) zip.OpOption { return zip.WithOperationID(id) }
|
||||
|
||||
// mountAccess registers the twenty typed access-control ops on the native
|
||||
// router. The literal /service_accounts/me routes register ahead of the
|
||||
// parameterised ones so an id can never shadow them — the same defence
|
||||
// telemetry.go gives its collection routes.
|
||||
func mountAccess(app *zip.App) {
|
||||
g := app.Group(accessPrefix)
|
||||
g := app.Group(o11yRoot)
|
||||
|
||||
zip.Post(g, "/roles", createRole, op("CreateRole"), zip.WithStatus(http.StatusCreated))
|
||||
zip.Get(g, "/roles", listRoles, op("ListRoles"))
|
||||
@@ -91,34 +77,34 @@ func mountAccess(app *zip.App) {
|
||||
// reserved managed-role prefix; the runtime refuses anything else.
|
||||
func createRole(ctx context.Context, in *O11yRoleCreateIn) (*O11yRoleCreateOut, error) {
|
||||
out := new(O11yRoleCreateOut)
|
||||
return out, send(ctx, http.MethodPost, "/roles", in, out)
|
||||
return out, relay(ctx, http.MethodPost, o11yRoot+"/roles", nil, in, out)
|
||||
}
|
||||
|
||||
// listRoles lists every role in the caller's org — the managed ones the
|
||||
// platform seeds and the custom ones its admins created.
|
||||
func listRoles(ctx context.Context, in *O11yRolesIn) (*O11yRolesOut, error) {
|
||||
out := new(O11yRolesOut)
|
||||
return out, send(ctx, http.MethodGet, "/roles", nil, out)
|
||||
return out, relay(ctx, http.MethodGet, o11yRoot+"/roles", nil, nil, out)
|
||||
}
|
||||
|
||||
// getRole returns one role with the transaction groups it grants.
|
||||
func getRole(ctx context.Context, in *O11yRoleGetIn) (*O11yRoleOut, error) {
|
||||
out := new(O11yRoleOut)
|
||||
return out, send(ctx, http.MethodGet, "/roles/"+in.ID, nil, out)
|
||||
return out, relay(ctx, http.MethodGet, o11yRoot+"/roles/"+in.ID, nil, nil, out)
|
||||
}
|
||||
|
||||
// updateRole replaces a custom role's description and transaction groups.
|
||||
// Both fields are mandatory — send an empty string or an empty array to clear
|
||||
// one — and managed roles cannot be edited.
|
||||
func updateRole(ctx context.Context, in *O11yRoleUpdateIn) (*struct{}, error) {
|
||||
return nil, send(ctx, http.MethodPut, "/roles/"+in.ID, in, nil)
|
||||
return nil, relay(ctx, http.MethodPut, o11yRoot+"/roles/"+in.ID, nil, in, nil)
|
||||
}
|
||||
|
||||
// deleteRole deletes a custom role. A role that still has user or
|
||||
// service-account assignees, or an auth-domain mapping, is refused; managed
|
||||
// roles cannot be deleted.
|
||||
func deleteRole(ctx context.Context, in *O11yRoleDeleteIn) (*struct{}, error) {
|
||||
return nil, send(ctx, http.MethodDelete, "/roles/"+in.ID, nil, nil)
|
||||
return nil, relay(ctx, http.MethodDelete, o11yRoot+"/roles/"+in.ID, nil, nil, nil)
|
||||
}
|
||||
|
||||
// ── service accounts ──────────────────────────────────────────────────────────
|
||||
@@ -128,59 +114,59 @@ func deleteRole(ctx context.Context, in *O11yRoleDeleteIn) (*struct{}, error) {
|
||||
// letters, digits or hyphens — becomes the account's email local part.
|
||||
func createServiceAccount(ctx context.Context, in *O11yServiceAccountCreateIn) (*O11yServiceAccountCreateOut, error) {
|
||||
out := new(O11yServiceAccountCreateOut)
|
||||
return out, send(ctx, http.MethodPost, "/service_accounts", in, out)
|
||||
return out, relay(ctx, http.MethodPost, o11yRoot+"/service_accounts", nil, in, out)
|
||||
}
|
||||
|
||||
// listServiceAccounts lists the caller's org's service accounts.
|
||||
func listServiceAccounts(ctx context.Context, in *O11yServiceAccountsIn) (*O11yServiceAccountsOut, error) {
|
||||
out := new(O11yServiceAccountsOut)
|
||||
return out, send(ctx, http.MethodGet, "/service_accounts", nil, out)
|
||||
return out, relay(ctx, http.MethodGet, o11yRoot+"/service_accounts", nil, nil, out)
|
||||
}
|
||||
|
||||
// getMyServiceAccount returns the calling service account itself, with the
|
||||
// roles it holds — the self-inspection read for a key-authenticated caller.
|
||||
func getMyServiceAccount(ctx context.Context, in *O11yMyServiceAccountIn) (*O11yServiceAccountOut, error) {
|
||||
out := new(O11yServiceAccountOut)
|
||||
return out, send(ctx, http.MethodGet, "/service_accounts/me", nil, out)
|
||||
return out, relay(ctx, http.MethodGet, o11yRoot+"/service_accounts/me", nil, nil, out)
|
||||
}
|
||||
|
||||
// updateMyServiceAccount renames the calling service account.
|
||||
func updateMyServiceAccount(ctx context.Context, in *O11yMyServiceAccountUpdateIn) (*struct{}, error) {
|
||||
return nil, send(ctx, http.MethodPut, "/service_accounts/me", in, nil)
|
||||
return nil, relay(ctx, http.MethodPut, o11yRoot+"/service_accounts/me", nil, in, nil)
|
||||
}
|
||||
|
||||
// getServiceAccount returns one service account with the roles it holds.
|
||||
func getServiceAccount(ctx context.Context, in *O11yServiceAccountGetIn) (*O11yServiceAccountOut, error) {
|
||||
out := new(O11yServiceAccountOut)
|
||||
return out, send(ctx, http.MethodGet, "/service_accounts/"+in.ID, nil, out)
|
||||
return out, relay(ctx, http.MethodGet, o11yRoot+"/service_accounts/"+in.ID, nil, nil, out)
|
||||
}
|
||||
|
||||
// updateServiceAccount renames a service account.
|
||||
func updateServiceAccount(ctx context.Context, in *O11yServiceAccountUpdateIn) (*struct{}, error) {
|
||||
return nil, send(ctx, http.MethodPut, "/service_accounts/"+in.ID, in, nil)
|
||||
return nil, relay(ctx, http.MethodPut, o11yRoot+"/service_accounts/"+in.ID, nil, in, nil)
|
||||
}
|
||||
|
||||
// deleteServiceAccount deletes a service account and revokes every key it
|
||||
// holds.
|
||||
func deleteServiceAccount(ctx context.Context, in *O11yServiceAccountDeleteIn) (*struct{}, error) {
|
||||
return nil, send(ctx, http.MethodDelete, "/service_accounts/"+in.ID, nil, nil)
|
||||
return nil, relay(ctx, http.MethodDelete, o11yRoot+"/service_accounts/"+in.ID, nil, nil, nil)
|
||||
}
|
||||
|
||||
// listServiceAccountRoles lists the roles a service account holds.
|
||||
func listServiceAccountRoles(ctx context.Context, in *O11yServiceAccountRolesIn) (*O11yServiceAccountRolesOut, error) {
|
||||
out := new(O11yServiceAccountRolesOut)
|
||||
return out, send(ctx, http.MethodGet, "/service_accounts/"+in.ID+"/roles", nil, out)
|
||||
return out, relay(ctx, http.MethodGet, o11yRoot+"/service_accounts/"+in.ID+"/roles", nil, nil, out)
|
||||
}
|
||||
|
||||
// grantServiceAccountRole assigns a role, named by its id, to a service
|
||||
// account.
|
||||
func grantServiceAccountRole(ctx context.Context, in *O11yServiceAccountRoleGrantIn) (*struct{}, error) {
|
||||
return nil, send(ctx, http.MethodPost, "/service_accounts/"+in.ServiceAccountID+"/roles", in, nil)
|
||||
return nil, relay(ctx, http.MethodPost, o11yRoot+"/service_accounts/"+in.ServiceAccountID+"/roles", nil, in, nil)
|
||||
}
|
||||
|
||||
// revokeServiceAccountRole removes a role from a service account.
|
||||
func revokeServiceAccountRole(ctx context.Context, in *O11yServiceAccountRoleRevokeIn) (*struct{}, error) {
|
||||
return nil, send(ctx, http.MethodDelete, "/service_accounts/"+in.ID+"/roles/"+in.RoleID, nil, nil)
|
||||
return nil, relay(ctx, http.MethodDelete, o11yRoot+"/service_accounts/"+in.ID+"/roles/"+in.RoleID, nil, nil, nil)
|
||||
}
|
||||
|
||||
// ── service-account keys ──────────────────────────────────────────────────────
|
||||
@@ -192,25 +178,25 @@ func revokeServiceAccountRole(ctx context.Context, in *O11yServiceAccountRoleRev
|
||||
// and a timestamp in the past is refused.
|
||||
func createServiceAccountKey(ctx context.Context, in *O11yAPIKeyCreateIn) (*O11yAPIKeyCreateOut, error) {
|
||||
out := new(O11yAPIKeyCreateOut)
|
||||
return out, send(ctx, http.MethodPost, "/service_accounts/"+in.ServiceAccountID+"/keys", in, out)
|
||||
return out, relay(ctx, http.MethodPost, o11yRoot+"/service_accounts/"+in.ServiceAccountID+"/keys", nil, in, out)
|
||||
}
|
||||
|
||||
// listServiceAccountKeys lists a service account's API keys — metadata only,
|
||||
// never the secrets.
|
||||
func listServiceAccountKeys(ctx context.Context, in *O11yAPIKeysIn) (*O11yAPIKeysOut, error) {
|
||||
out := new(O11yAPIKeysOut)
|
||||
return out, send(ctx, http.MethodGet, "/service_accounts/"+in.ID+"/keys", nil, out)
|
||||
return out, relay(ctx, http.MethodGet, o11yRoot+"/service_accounts/"+in.ID+"/keys", nil, nil, out)
|
||||
}
|
||||
|
||||
// updateServiceAccountKey renames an API key or moves its expiry.
|
||||
func updateServiceAccountKey(ctx context.Context, in *O11yAPIKeyUpdateIn) (*struct{}, error) {
|
||||
return nil, send(ctx, http.MethodPut, "/service_accounts/"+in.ServiceAccountID+"/keys/"+in.KeyID, in, nil)
|
||||
return nil, relay(ctx, http.MethodPut, o11yRoot+"/service_accounts/"+in.ServiceAccountID+"/keys/"+in.KeyID, nil, in, nil)
|
||||
}
|
||||
|
||||
// revokeServiceAccountKey revokes an API key. Revocation is immediate and
|
||||
// permanent.
|
||||
func revokeServiceAccountKey(ctx context.Context, in *O11yAPIKeyRevokeIn) (*struct{}, error) {
|
||||
return nil, send(ctx, http.MethodDelete, "/service_accounts/"+in.ID+"/keys/"+in.KeyID, nil, nil)
|
||||
return nil, relay(ctx, http.MethodDelete, o11yRoot+"/service_accounts/"+in.ID+"/keys/"+in.KeyID, nil, nil, nil)
|
||||
}
|
||||
|
||||
// ── authorization probe ───────────────────────────────────────────────────────
|
||||
@@ -221,7 +207,7 @@ func revokeServiceAccountKey(ctx context.Context, in *O11yAPIKeyRevokeIn) (*stru
|
||||
// to show.
|
||||
func checkAccess(ctx context.Context, in *O11yCheckIn) (*O11yCheckOut, error) {
|
||||
out := new(O11yCheckOut)
|
||||
return out, send(ctx, http.MethodPost, "/authz/check", in, out)
|
||||
return out, relay(ctx, http.MethodPost, o11yRoot+"/authz/check", nil, in, out)
|
||||
}
|
||||
|
||||
// ── inputs ────────────────────────────────────────────────────────────────────
|
||||
@@ -620,71 +606,3 @@ type O11yTransactionResult struct {
|
||||
}
|
||||
|
||||
// ── the seam ──────────────────────────────────────────────────────────────────
|
||||
|
||||
// send hands a typed op's call to the o11y runtime and decodes the answer into
|
||||
// the op's Out — nil out for the operations that answer no content.
|
||||
//
|
||||
// It is telemetry.go's relay with this face's root: the same identity
|
||||
// propagation, the same refusal reading, the same recorder hop onto the SAME
|
||||
// handler SetHandler registered, so the request runs the whole chain it always
|
||||
// ran — authn, the CheckResources or OpenAccess gate, the FGA checks, the audit
|
||||
// record — in the order it always did. No policy lives here. (Folding send and
|
||||
// relay into one prefix-parameterised seam is a one-line change in
|
||||
// telemetry.go, which belongs to its own unit.)
|
||||
func send(ctx context.Context, method, path string, body, out any) error {
|
||||
h := getHandler()
|
||||
if h == nil {
|
||||
return zip.Errorf(http.StatusServiceUnavailable, "o11y runtime not initialized")
|
||||
}
|
||||
|
||||
payload := io.Reader(http.NoBody)
|
||||
if body != nil {
|
||||
b, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return zip.ErrBadRequest(err.Error())
|
||||
}
|
||||
payload = bytes.NewReader(b)
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, method, accessPrefix+path, payload)
|
||||
if err != nil {
|
||||
return zip.ErrBadRequest(err.Error())
|
||||
}
|
||||
if body != nil {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
caller := zip.CallerOf(ctx)
|
||||
for header, value := range map[string]string{
|
||||
zip.HeaderOrg: caller.Org,
|
||||
zip.HeaderProject: caller.Project,
|
||||
zip.HeaderUser: caller.User,
|
||||
zip.HeaderUserName: caller.Name,
|
||||
zip.HeaderUserEmail: caller.Email,
|
||||
zip.HeaderUserOwner: caller.Owner,
|
||||
zip.HeaderRequestID: caller.RequestID,
|
||||
} {
|
||||
if value != "" {
|
||||
req.Header.Set(header, value)
|
||||
}
|
||||
}
|
||||
if caller.Admin {
|
||||
req.Header.Set(zip.HeaderUserAdmin, "true")
|
||||
}
|
||||
if caller.OrgAdmin {
|
||||
req.Header.Set(zip.HeaderUserOrgAdmin, "true")
|
||||
}
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code < http.StatusOK || rec.Code >= http.StatusMultipleChoices {
|
||||
code, reason := refusal(rec.Body.Bytes())
|
||||
return &zip.HTTPError{Status: rec.Code, Code: code, Msg: reason}
|
||||
}
|
||||
if out == nil {
|
||||
return nil
|
||||
}
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), out); err != nil {
|
||||
return zip.ErrInternal("cannot read the runtime's answer: " + err.Error())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,251 +0,0 @@
|
||||
// Code generated by zipdoc; DO NOT EDIT.
|
||||
//
|
||||
// The access face's slice of the package extraction, split into its own file
|
||||
// so concurrent slices do not contend on zipdoc_gen.go; a later `go generate
|
||||
// -run zipdoc ./` folds every slice back into zipdoc_gen.go, at which point
|
||||
// this file is deleted.
|
||||
|
||||
package o11y
|
||||
|
||||
import (
|
||||
"github.com/zap-proto/zip"
|
||||
)
|
||||
|
||||
func init() {
|
||||
zip.Describe("DELETE /v1/o11y/roles/:id", zip.Doc{
|
||||
Description: "Deletes a custom role. A role that still has user or\nservice-account assignees, or an auth-domain mapping, is refused; managed\nroles cannot be deleted.",
|
||||
})
|
||||
zip.Describe("DELETE /v1/o11y/service_accounts/:id", zip.Doc{
|
||||
Description: "Deletes a service account and revokes every key it\nholds.",
|
||||
})
|
||||
zip.Describe("DELETE /v1/o11y/service_accounts/:id/keys/:fid", zip.Doc{
|
||||
Description: "Revokes an API key. Revocation is immediate and\npermanent.",
|
||||
})
|
||||
zip.Describe("DELETE /v1/o11y/service_accounts/:id/roles/:rid", zip.Doc{
|
||||
Description: "Removes a role from a service account.",
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/roles", zip.Doc{
|
||||
Description: "Lists every role in the caller's org — the managed ones the\nplatform seeds and the custom ones its admins created.",
|
||||
Fields: map[string]string{
|
||||
"O11yRole.createdAt": "CreatedAt is when the role was created.",
|
||||
"O11yRole.description": "Description says what the role is for.",
|
||||
"O11yRole.id": "ID is the role id.",
|
||||
"O11yRole.name": "Name is the role's name.",
|
||||
"O11yRole.orgId": "OrgID is the org the role belongs to.",
|
||||
"O11yRole.type": "Type is custom or managed.",
|
||||
"O11yRole.updatedAt": "UpdatedAt is when it last changed.",
|
||||
"O11yRolesOut.data": "Data holds the roles.",
|
||||
"O11yRolesOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/roles/:id", zip.Doc{
|
||||
Description: "Returns one role with the transaction groups it grants.",
|
||||
Fields: map[string]string{
|
||||
"O11yObjectGroup.resource": "Resource is the objects' type and kind.",
|
||||
"O11yObjectGroup.selectors": "Selectors pick the instances; a wildcard selects them all.",
|
||||
"O11yResourceRef.kind": "Kind is the resource kind the type belongs to.",
|
||||
"O11yResourceRef.type": "Type is the resource type, e.g. role, dashboard, serviceaccount.",
|
||||
"O11yRoleDetail.createdAt": "CreatedAt is when the role was created.",
|
||||
"O11yRoleDetail.description": "Description says what the role is for.",
|
||||
"O11yRoleDetail.id": "ID is the role id.",
|
||||
"O11yRoleDetail.name": "Name is the role's name.",
|
||||
"O11yRoleDetail.orgId": "OrgID is the org the role belongs to.",
|
||||
"O11yRoleDetail.transactionGroups": "TransactionGroups are the grants the role carries.",
|
||||
"O11yRoleDetail.type": "Type is custom or managed.",
|
||||
"O11yRoleDetail.updatedAt": "UpdatedAt is when it last changed.",
|
||||
"O11yRoleOut.data": "Data holds the role and its transaction groups.",
|
||||
"O11yRoleOut.status": "Status is \"success\".",
|
||||
"O11yTransactionGroup.objectGroup": "ObjectGroup is the set of objects it allows the verb on.",
|
||||
"O11yTransactionGroup.relation": "Relation is the verb the grant allows.",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/service_accounts", zip.Doc{
|
||||
Description: "Lists the caller's org's service accounts.",
|
||||
Fields: map[string]string{
|
||||
"O11yServiceAccount.createdAt": "CreatedAt is when the account was created.",
|
||||
"O11yServiceAccount.email": "Email is the address the account authenticates as.",
|
||||
"O11yServiceAccount.id": "ID is the service account id.",
|
||||
"O11yServiceAccount.name": "Name is the account's name.",
|
||||
"O11yServiceAccount.orgId": "OrgID is the org the account belongs to.",
|
||||
"O11yServiceAccount.status": "Status is active or deleted.",
|
||||
"O11yServiceAccount.updatedAt": "UpdatedAt is when it last changed.",
|
||||
"O11yServiceAccountsOut.data": "Data holds the service accounts.",
|
||||
"O11yServiceAccountsOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/service_accounts/:id", zip.Doc{
|
||||
Description: "Returns one service account with the roles it holds.",
|
||||
Fields: map[string]string{
|
||||
"O11yRole.createdAt": "CreatedAt is when the role was created.",
|
||||
"O11yRole.description": "Description says what the role is for.",
|
||||
"O11yRole.id": "ID is the role id.",
|
||||
"O11yRole.name": "Name is the role's name.",
|
||||
"O11yRole.orgId": "OrgID is the org the role belongs to.",
|
||||
"O11yRole.type": "Type is custom or managed.",
|
||||
"O11yRole.updatedAt": "UpdatedAt is when it last changed.",
|
||||
"O11yServiceAccountDetail.createdAt": "CreatedAt is when the account was created.",
|
||||
"O11yServiceAccountDetail.email": "Email is the address the account authenticates as.",
|
||||
"O11yServiceAccountDetail.id": "ID is the service account id.",
|
||||
"O11yServiceAccountDetail.name": "Name is the account's name.",
|
||||
"O11yServiceAccountDetail.orgId": "OrgID is the org the account belongs to.",
|
||||
"O11yServiceAccountDetail.serviceAccountRoles": "ServiceAccountRoles are the account's role assignments.",
|
||||
"O11yServiceAccountDetail.status": "Status is active or deleted.",
|
||||
"O11yServiceAccountDetail.updatedAt": "UpdatedAt is when it last changed.",
|
||||
"O11yServiceAccountOut.data": "Data holds the account and its role assignments.",
|
||||
"O11yServiceAccountOut.status": "Status is \"success\".",
|
||||
"O11yServiceAccountRole.createdAt": "CreatedAt is when the role was assigned.",
|
||||
"O11yServiceAccountRole.id": "ID is the assignment's own id.",
|
||||
"O11yServiceAccountRole.role": "Role is the role itself.",
|
||||
"O11yServiceAccountRole.roleId": "RoleID is the role held.",
|
||||
"O11yServiceAccountRole.serviceAccountId": "ServiceAccountID is the account holding the role.",
|
||||
"O11yServiceAccountRole.updatedAt": "UpdatedAt is when the assignment last changed.",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/service_accounts/:id/keys", zip.Doc{
|
||||
Description: "Lists a service account's API keys — metadata only,\nnever the secrets.",
|
||||
Fields: map[string]string{
|
||||
"O11yAPIKey.createdAt": "CreatedAt is when the key was minted.",
|
||||
"O11yAPIKey.expiresAt": "ExpiresAt is when the key stops working, as a unix timestamp in seconds;\nzero means never.",
|
||||
"O11yAPIKey.id": "ID is the key id.",
|
||||
"O11yAPIKey.lastObservedAt": "LastObservedAt is when the key was last seen authenticating.",
|
||||
"O11yAPIKey.name": "Name is the key's name.",
|
||||
"O11yAPIKey.serviceAccountId": "ServiceAccountID is the account the key belongs to.",
|
||||
"O11yAPIKey.updatedAt": "UpdatedAt is when it last changed.",
|
||||
"O11yAPIKeysOut.data": "Data holds the keys.",
|
||||
"O11yAPIKeysOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/service_accounts/:id/roles", zip.Doc{
|
||||
Description: "Lists the roles a service account holds.",
|
||||
Fields: map[string]string{
|
||||
"O11yRole.createdAt": "CreatedAt is when the role was created.",
|
||||
"O11yRole.description": "Description says what the role is for.",
|
||||
"O11yRole.id": "ID is the role id.",
|
||||
"O11yRole.name": "Name is the role's name.",
|
||||
"O11yRole.orgId": "OrgID is the org the role belongs to.",
|
||||
"O11yRole.type": "Type is custom or managed.",
|
||||
"O11yRole.updatedAt": "UpdatedAt is when it last changed.",
|
||||
"O11yServiceAccountRolesOut.data": "Data holds the roles.",
|
||||
"O11yServiceAccountRolesOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/service_accounts/me", zip.Doc{
|
||||
Description: "Returns the calling service account itself, with the\nroles it holds — the self-inspection read for a key-authenticated caller.",
|
||||
Fields: map[string]string{
|
||||
"O11yRole.createdAt": "CreatedAt is when the role was created.",
|
||||
"O11yRole.description": "Description says what the role is for.",
|
||||
"O11yRole.id": "ID is the role id.",
|
||||
"O11yRole.name": "Name is the role's name.",
|
||||
"O11yRole.orgId": "OrgID is the org the role belongs to.",
|
||||
"O11yRole.type": "Type is custom or managed.",
|
||||
"O11yRole.updatedAt": "UpdatedAt is when it last changed.",
|
||||
"O11yServiceAccountDetail.createdAt": "CreatedAt is when the account was created.",
|
||||
"O11yServiceAccountDetail.email": "Email is the address the account authenticates as.",
|
||||
"O11yServiceAccountDetail.id": "ID is the service account id.",
|
||||
"O11yServiceAccountDetail.name": "Name is the account's name.",
|
||||
"O11yServiceAccountDetail.orgId": "OrgID is the org the account belongs to.",
|
||||
"O11yServiceAccountDetail.serviceAccountRoles": "ServiceAccountRoles are the account's role assignments.",
|
||||
"O11yServiceAccountDetail.status": "Status is active or deleted.",
|
||||
"O11yServiceAccountDetail.updatedAt": "UpdatedAt is when it last changed.",
|
||||
"O11yServiceAccountOut.data": "Data holds the account and its role assignments.",
|
||||
"O11yServiceAccountOut.status": "Status is \"success\".",
|
||||
"O11yServiceAccountRole.createdAt": "CreatedAt is when the role was assigned.",
|
||||
"O11yServiceAccountRole.id": "ID is the assignment's own id.",
|
||||
"O11yServiceAccountRole.role": "Role is the role itself.",
|
||||
"O11yServiceAccountRole.roleId": "RoleID is the role held.",
|
||||
"O11yServiceAccountRole.serviceAccountId": "ServiceAccountID is the account holding the role.",
|
||||
"O11yServiceAccountRole.updatedAt": "UpdatedAt is when the assignment last changed.",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/authz/check", zip.Doc{
|
||||
Description: "Evaluates a batch of transactions — relation plus object — for\nthe authenticated caller and answers each with its authorization verdict, in\nthe order they were asked. It is the read a UI uses to decide which controls\nto show.",
|
||||
Fields: map[string]string{
|
||||
"O11yCheckOut.data": "Data holds the verdicts.",
|
||||
"O11yCheckOut.status": "Status is \"success\".",
|
||||
"O11yObject.resource": "Resource is the resource's type and kind.",
|
||||
"O11yObject.selector": "Selector picks the instance — an FGA object string, wildcard allowed.",
|
||||
"O11yResourceRef.kind": "Kind is the resource kind the type belongs to.",
|
||||
"O11yResourceRef.type": "Type is the resource type, e.g. role, dashboard, serviceaccount.",
|
||||
"O11yTransaction.object": "Object is the resource the verb would act on.",
|
||||
"O11yTransaction.relation": "Relation is the verb being asked about, e.g. read, create, update, delete.",
|
||||
"O11yTransactionResult.authorized": "Authorized says whether the caller may do it.",
|
||||
"O11yTransactionResult.object": "Object is the resource it would act on.",
|
||||
"O11yTransactionResult.relation": "Relation is the verb that was asked about.",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/roles", zip.Doc{
|
||||
Description: "Creates a custom role in the caller's org from a name, an optional\ndescription and the transaction groups it grants, answering the new role's id.\n\nNames are lowercase letters and hyphens only, and may not start with the\nreserved managed-role prefix; the runtime refuses anything else.",
|
||||
Fields: map[string]string{
|
||||
"O11yCreated.id": "ID is the new record's id.",
|
||||
"O11yObjectGroup.resource": "Resource is the objects' type and kind.",
|
||||
"O11yObjectGroup.selectors": "Selectors pick the instances; a wildcard selects them all.",
|
||||
"O11yResourceRef.kind": "Kind is the resource kind the type belongs to.",
|
||||
"O11yResourceRef.type": "Type is the resource type, e.g. role, dashboard, serviceaccount.",
|
||||
"O11yRoleCreateIn.description": "Description says what the role is for.",
|
||||
"O11yRoleCreateIn.name": "Name is the role's name: lowercase letters and hyphens, at most 50\ncharacters, not starting with the reserved managed-role prefix. Required.",
|
||||
"O11yRoleCreateIn.transactionGroups": "TransactionGroups are the grants the role carries.",
|
||||
"O11yRoleCreateOut.data": "Data carries the new role's id.",
|
||||
"O11yRoleCreateOut.status": "Status is \"success\".",
|
||||
"O11yTransactionGroup.objectGroup": "ObjectGroup is the set of objects it allows the verb on.",
|
||||
"O11yTransactionGroup.relation": "Relation is the verb the grant allows.",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/service_accounts", zip.Doc{
|
||||
Description: "Creates a service account in the caller's org,\nanswering its id. The name — a lowercase letter followed by lowercase\nletters, digits or hyphens — becomes the account's email local part.",
|
||||
Fields: map[string]string{
|
||||
"O11yCreated.id": "ID is the new record's id.",
|
||||
"O11yServiceAccountCreateIn.name": "Name is the account's name: a lowercase letter followed by lowercase\nletters, digits or hyphens, at most 50 characters. Required.",
|
||||
"O11yServiceAccountCreateOut.data": "Data carries the new account's id.",
|
||||
"O11yServiceAccountCreateOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/service_accounts/:id/keys", zip.Doc{
|
||||
Description: "Mints an API key for a service account and answers\nthe key's id and its secret — the one time the secret is ever shown.\n\nExpiresAt is a unix timestamp in seconds; zero means the key never expires,\nand a timestamp in the past is refused.",
|
||||
Fields: map[string]string{
|
||||
"O11yAPIKeyCreateIn.expiresAt": "ExpiresAt is when the key stops working, as a unix timestamp in seconds.\nZero means it never expires; a past timestamp is refused.",
|
||||
"O11yAPIKeyCreateIn.name": "Name is the key's name: a lowercase letter followed by lowercase letters,\ndigits or hyphens, at most 80 characters. Required.",
|
||||
"O11yAPIKeyCreateOut.data": "Data carries the key's id and secret.",
|
||||
"O11yAPIKeyCreateOut.status": "Status is \"success\".",
|
||||
"O11yAPIKeySecret.id": "ID is the key id.",
|
||||
"O11yAPIKeySecret.key": "Key is the secret the service account authenticates with.",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/service_accounts/:id/roles", zip.Doc{
|
||||
Description: "Assigns a role, named by its id, to a service\naccount.",
|
||||
Fields: map[string]string{
|
||||
"O11yServiceAccountRoleGrantIn.id": "RoleID is the id of the role to assign. Required.",
|
||||
},
|
||||
})
|
||||
zip.Describe("PUT /v1/o11y/roles/:id", zip.Doc{
|
||||
Description: "Replaces a custom role's description and transaction groups.\nBoth fields are mandatory — send an empty string or an empty array to clear\none — and managed roles cannot be edited.",
|
||||
Fields: map[string]string{
|
||||
"O11yObjectGroup.resource": "Resource is the objects' type and kind.",
|
||||
"O11yObjectGroup.selectors": "Selectors pick the instances; a wildcard selects them all.",
|
||||
"O11yResourceRef.kind": "Kind is the resource kind the type belongs to.",
|
||||
"O11yResourceRef.type": "Type is the resource type, e.g. role, dashboard, serviceaccount.",
|
||||
"O11yRoleUpdateIn.description": "Description says what the role is for. Required — send an empty string to\nclear it.",
|
||||
"O11yRoleUpdateIn.transactionGroups": "TransactionGroups are the grants the role carries. Required — send an\nempty array to clear them.",
|
||||
"O11yTransactionGroup.objectGroup": "ObjectGroup is the set of objects it allows the verb on.",
|
||||
"O11yTransactionGroup.relation": "Relation is the verb the grant allows.",
|
||||
},
|
||||
})
|
||||
zip.Describe("PUT /v1/o11y/service_accounts/:id", zip.Doc{
|
||||
Description: "Renames a service account.",
|
||||
Fields: map[string]string{
|
||||
"O11yServiceAccountUpdateIn.name": "Name is the account's new name, under the same rules it was created with.\nRequired.",
|
||||
},
|
||||
})
|
||||
zip.Describe("PUT /v1/o11y/service_accounts/:id/keys/:fid", zip.Doc{
|
||||
Description: "Renames an API key or moves its expiry.",
|
||||
Fields: map[string]string{
|
||||
"O11yAPIKeyUpdateIn.expiresAt": "ExpiresAt is when the key stops working, as a unix timestamp in seconds.\nZero means it never expires; a past timestamp is refused.",
|
||||
"O11yAPIKeyUpdateIn.name": "Name is the key's new name, under the same rules it was created with.\nRequired.",
|
||||
},
|
||||
})
|
||||
zip.Describe("PUT /v1/o11y/service_accounts/me", zip.Doc{
|
||||
Description: "Renames the calling service account.",
|
||||
Fields: map[string]string{
|
||||
"O11yMyServiceAccountUpdateIn.name": "Name is the account's new name, under the same rules it was created with.\nRequired.",
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -10,7 +10,7 @@ package o11y
|
||||
//
|
||||
// THE WIRE DOES NOT MOVE, the same way telemetry.go's five ops do not move it:
|
||||
// each op hands the call to the SAME runtime handler the wildcard delegates to
|
||||
// (see relayAPM), so identity resolution, the org gate, the role check —
|
||||
// (see relay.go), so identity resolution, the org gate, the role check —
|
||||
// every one of these routes has always answered behind the runtime's
|
||||
// ViewAccess gate — the audit record and the success envelope are all still
|
||||
// the runtime's, executed in the order they always were. What is new here is
|
||||
@@ -29,27 +29,19 @@ package o11y
|
||||
// bytes are. Nothing here is a re-implementation and nothing is invented.
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"time"
|
||||
|
||||
"github.com/zap-proto/zip"
|
||||
)
|
||||
|
||||
// apmPrefix is the product face's path root, spelled ONCE: the group the ops
|
||||
// register on, and the first segment relayAPM rebuilds a call onto. It is the
|
||||
// same literal the delegation wildcard in mount.go covers.
|
||||
const apmPrefix = "/v1/o11y"
|
||||
|
||||
// mountAPM registers the twenty-one typed APM ops on the native router — the
|
||||
// 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 := app.Group(apmPrefix)
|
||||
g := app.Group(o11yRoot)
|
||||
|
||||
// The service catalog: /services collection, /service/* breakdowns.
|
||||
zip.Post(g, "/services", services)
|
||||
@@ -88,35 +80,35 @@ func mountAPM(app *zip.App) {
|
||||
// error rates, and the entry-point operations the numbers were computed over.
|
||||
func services(ctx context.Context, in *O11yServicesIn) (*O11yServicesOut, error) {
|
||||
out := new(O11yServicesOut)
|
||||
return out, relayAPM(ctx, http.MethodPost, "/services", in, out)
|
||||
return out, relay(ctx, http.MethodPost, o11yRoot+"/services", nil, in, out)
|
||||
}
|
||||
|
||||
// serviceNames lists the name of every service the trace store holds, with no
|
||||
// window applied — the complete catalog, for pickers and autocomplete.
|
||||
func serviceNames(ctx context.Context, in *O11yServiceNamesIn) (*O11yServiceNames, error) {
|
||||
out := new(O11yServiceNames)
|
||||
return out, relayAPM(ctx, http.MethodGet, "/services/list", nil, out)
|
||||
return out, relay(ctx, http.MethodGet, o11yRoot+"/services/list", nil, nil, out)
|
||||
}
|
||||
|
||||
// topOperations returns one service's heaviest operations in the window, each
|
||||
// with p50/p95/p99 latency, how often it ran and how often it errored.
|
||||
func topOperations(ctx context.Context, in *O11yOperationsIn) (*O11yOperationsOut, error) {
|
||||
out := new(O11yOperationsOut)
|
||||
return out, relayAPM(ctx, http.MethodPost, "/service/top_operations", in, out)
|
||||
return out, relay(ctx, http.MethodPost, o11yRoot+"/service/top_operations", nil, in, out)
|
||||
}
|
||||
|
||||
// topLevelOperations maps each service to its entry-point span names — for the
|
||||
// one service named in the request, or for every service when none is.
|
||||
func topLevelOperations(ctx context.Context, in *O11yTopLevelOpsIn) (*O11yServiceOperations, error) {
|
||||
out := new(O11yServiceOperations)
|
||||
return out, relayAPM(ctx, http.MethodPost, "/service/top_level_operations", in, out)
|
||||
return out, relay(ctx, http.MethodPost, o11yRoot+"/service/top_level_operations", nil, in, out)
|
||||
}
|
||||
|
||||
// entryPointOperations returns one service's entry-point operations with the
|
||||
// same latency and error profile topOperations reports.
|
||||
func entryPointOperations(ctx context.Context, in *O11yOperationsIn) (*O11yOperationsOut, error) {
|
||||
out := new(O11yOperationsOut)
|
||||
return out, relayAPM(ctx, http.MethodPost, "/service/entry_point_operations", in, out)
|
||||
return out, relay(ctx, http.MethodPost, o11yRoot+"/service/entry_point_operations", nil, in, out)
|
||||
}
|
||||
|
||||
// ── the messaging-queue surface ───────────────────────────────────────────────
|
||||
@@ -127,7 +119,7 @@ func entryPointOperations(ctx context.Context, in *O11yOperationsIn) (*O11yOpera
|
||||
// span attribute.
|
||||
func queueOverview(ctx context.Context, in *O11yQueueListIn) (*O11yQueueRowsOut, error) {
|
||||
out := new(O11yQueueRowsOut)
|
||||
return out, relayAPM(ctx, http.MethodPost, "/messaging-queues/queue-overview", in, out)
|
||||
return out, relay(ctx, http.MethodPost, o11yRoot+"/messaging-queues/queue-overview", nil, in, out)
|
||||
}
|
||||
|
||||
// producersOnboarding checks whether the spans the Kafka producer views need
|
||||
@@ -135,35 +127,35 @@ func queueOverview(ctx context.Context, in *O11yQueueListIn) (*O11yQueueRowsOut,
|
||||
// and, on failure, what is missing from the instrumentation.
|
||||
func producersOnboarding(ctx context.Context, in *O11yQueueIn) (*O11yQueueChecksOut, error) {
|
||||
out := new(O11yQueueChecksOut)
|
||||
return out, relayAPM(ctx, http.MethodPost, "/messaging-queues/kafka/onboarding/producers", in, out)
|
||||
return out, relay(ctx, http.MethodPost, o11yRoot+"/messaging-queues/kafka/onboarding/producers", nil, in, out)
|
||||
}
|
||||
|
||||
// consumersOnboarding checks whether the spans the Kafka consumer views need
|
||||
// are arriving, row for row like producersOnboarding.
|
||||
func consumersOnboarding(ctx context.Context, in *O11yQueueIn) (*O11yQueueChecksOut, error) {
|
||||
out := new(O11yQueueChecksOut)
|
||||
return out, relayAPM(ctx, http.MethodPost, "/messaging-queues/kafka/onboarding/consumers", in, out)
|
||||
return out, relay(ctx, http.MethodPost, o11yRoot+"/messaging-queues/kafka/onboarding/consumers", nil, in, out)
|
||||
}
|
||||
|
||||
// kafkaOnboarding checks whether Kafka's own metrics — consumer lag and
|
||||
// partition telemetry — are arriving, so the lag views can be lit up.
|
||||
func kafkaOnboarding(ctx context.Context, in *O11yQueueIn) (*O11yQueueChecksOut, error) {
|
||||
out := new(O11yQueueChecksOut)
|
||||
return out, relayAPM(ctx, http.MethodPost, "/messaging-queues/kafka/onboarding/kafka", in, out)
|
||||
return out, relay(ctx, http.MethodPost, o11yRoot+"/messaging-queues/kafka/onboarding/kafka", nil, in, out)
|
||||
}
|
||||
|
||||
// partitionLatency returns the per-partition latency overview for the window —
|
||||
// each topic/partition with its throughput and latency profile.
|
||||
func partitionLatency(ctx context.Context, in *O11yQueueIn) (*O11yQueryRangeOut, error) {
|
||||
out := new(O11yQueryRangeOut)
|
||||
return out, relayAPM(ctx, http.MethodPost, "/messaging-queues/kafka/partition-latency/overview", in, out)
|
||||
return out, relay(ctx, http.MethodPost, o11yRoot+"/messaging-queues/kafka/partition-latency/overview", nil, in, out)
|
||||
}
|
||||
|
||||
// consumerPartitionLatency returns the consumer-group latency detail for the
|
||||
// topic and partition named in the request's variables.
|
||||
func consumerPartitionLatency(ctx context.Context, in *O11yQueueIn) (*O11yQueryRangeOut, error) {
|
||||
out := new(O11yQueryRangeOut)
|
||||
return out, relayAPM(ctx, http.MethodPost, "/messaging-queues/kafka/partition-latency/consumer", in, out)
|
||||
return out, relay(ctx, http.MethodPost, o11yRoot+"/messaging-queues/kafka/partition-latency/consumer", nil, in, out)
|
||||
}
|
||||
|
||||
// producerLagDetails returns the producer side of a consumer-lag view: the
|
||||
@@ -171,7 +163,7 @@ func consumerPartitionLatency(ctx context.Context, in *O11yQueueIn) (*O11yQueryR
|
||||
// throughput and latency over the window.
|
||||
func producerLagDetails(ctx context.Context, in *O11yQueueIn) (*O11yQueryRangeOut, error) {
|
||||
out := new(O11yQueryRangeOut)
|
||||
return out, relayAPM(ctx, http.MethodPost, "/messaging-queues/kafka/consumer-lag/producer-details", in, out)
|
||||
return out, relay(ctx, http.MethodPost, o11yRoot+"/messaging-queues/kafka/consumer-lag/producer-details", nil, in, out)
|
||||
}
|
||||
|
||||
// consumerLagDetails returns the consumer side of a consumer-lag view: the
|
||||
@@ -179,7 +171,7 @@ func producerLagDetails(ctx context.Context, in *O11yQueueIn) (*O11yQueryRangeOu
|
||||
// throughput and latency over the window.
|
||||
func consumerLagDetails(ctx context.Context, in *O11yQueueIn) (*O11yQueryRangeOut, error) {
|
||||
out := new(O11yQueryRangeOut)
|
||||
return out, relayAPM(ctx, http.MethodPost, "/messaging-queues/kafka/consumer-lag/consumer-details", in, out)
|
||||
return out, relay(ctx, http.MethodPost, o11yRoot+"/messaging-queues/kafka/consumer-lag/consumer-details", nil, in, out)
|
||||
}
|
||||
|
||||
// consumerLagNetwork returns consumer network latency correlated per client:
|
||||
@@ -187,35 +179,35 @@ func consumerLagDetails(ctx context.Context, in *O11yQueueIn) (*O11yQueryRangeOu
|
||||
// fetch latency joins in as a latency column per client/instance/service.
|
||||
func consumerLagNetwork(ctx context.Context, in *O11yQueueIn) (*O11yQueryRangeOut, error) {
|
||||
out := new(O11yQueryRangeOut)
|
||||
return out, relayAPM(ctx, http.MethodPost, "/messaging-queues/kafka/consumer-lag/network-latency", in, out)
|
||||
return out, relay(ctx, http.MethodPost, o11yRoot+"/messaging-queues/kafka/consumer-lag/network-latency", nil, in, out)
|
||||
}
|
||||
|
||||
// producerThroughput returns the producer topic-throughput overview for the
|
||||
// window — what each producer service wrote, per topic.
|
||||
func producerThroughput(ctx context.Context, in *O11yQueueIn) (*O11yQueryRangeOut, error) {
|
||||
out := new(O11yQueryRangeOut)
|
||||
return out, relayAPM(ctx, http.MethodPost, "/messaging-queues/kafka/topic-throughput/producer", in, out)
|
||||
return out, relay(ctx, http.MethodPost, o11yRoot+"/messaging-queues/kafka/topic-throughput/producer", nil, in, out)
|
||||
}
|
||||
|
||||
// producerThroughputDetails breaks one producer topic's throughput down using
|
||||
// the topic and service named in variables.
|
||||
func producerThroughputDetails(ctx context.Context, in *O11yQueueIn) (*O11yQueryRangeOut, error) {
|
||||
out := new(O11yQueryRangeOut)
|
||||
return out, relayAPM(ctx, http.MethodPost, "/messaging-queues/kafka/topic-throughput/producer-details", in, out)
|
||||
return out, relay(ctx, http.MethodPost, o11yRoot+"/messaging-queues/kafka/topic-throughput/producer-details", nil, in, out)
|
||||
}
|
||||
|
||||
// consumerThroughput returns the consumer topic-throughput overview for the
|
||||
// window — what each consumer group read, per topic.
|
||||
func consumerThroughput(ctx context.Context, in *O11yQueueIn) (*O11yQueryRangeOut, error) {
|
||||
out := new(O11yQueryRangeOut)
|
||||
return out, relayAPM(ctx, http.MethodPost, "/messaging-queues/kafka/topic-throughput/consumer", in, out)
|
||||
return out, relay(ctx, http.MethodPost, o11yRoot+"/messaging-queues/kafka/topic-throughput/consumer", nil, in, out)
|
||||
}
|
||||
|
||||
// consumerThroughputDetails breaks one consumer topic's throughput down using
|
||||
// the topic and service named in variables.
|
||||
func consumerThroughputDetails(ctx context.Context, in *O11yQueueIn) (*O11yQueryRangeOut, error) {
|
||||
out := new(O11yQueryRangeOut)
|
||||
return out, relayAPM(ctx, http.MethodPost, "/messaging-queues/kafka/topic-throughput/consumer-details", in, out)
|
||||
return out, relay(ctx, http.MethodPost, o11yRoot+"/messaging-queues/kafka/topic-throughput/consumer-details", nil, in, out)
|
||||
}
|
||||
|
||||
// spanEvaluation correlates producer and consumer spans over the evaluation
|
||||
@@ -223,7 +215,7 @@ func consumerThroughputDetails(ctx context.Context, in *O11yQueueIn) (*O11yQuery
|
||||
// end-to-end delay — the check that messages produced are being consumed.
|
||||
func spanEvaluation(ctx context.Context, in *O11yQueueIn) (*O11yQueryRangeOut, error) {
|
||||
out := new(O11yQueryRangeOut)
|
||||
return out, relayAPM(ctx, http.MethodPost, "/messaging-queues/kafka/span/evaluation", in, out)
|
||||
return out, relay(ctx, http.MethodPost, o11yRoot+"/messaging-queues/kafka/span/evaluation", nil, in, out)
|
||||
}
|
||||
|
||||
// ── the third-party API overview ──────────────────────────────────────────────
|
||||
@@ -233,21 +225,21 @@ func spanEvaluation(ctx context.Context, in *O11yQueueIn) (*O11yQueryRangeOut, e
|
||||
// a bare IP address are dropped unless show_ip asks for them.
|
||||
func domainList(ctx context.Context, in *O11yDomainsIn) (*O11yDomainsOut, error) {
|
||||
out := new(O11yDomainsOut)
|
||||
return out, relayAPM(ctx, http.MethodPost, "/third-party-apis/overview/list", in, out)
|
||||
return out, relay(ctx, http.MethodPost, o11yRoot+"/third-party-apis/overview/list", nil, in, out)
|
||||
}
|
||||
|
||||
// domainInfo returns one external domain's endpoint-level breakdown — each
|
||||
// endpoint with its rate, error and latency columns over the window.
|
||||
func domainInfo(ctx context.Context, in *O11yDomainsIn) (*O11yDomainsOut, error) {
|
||||
out := new(O11yDomainsOut)
|
||||
return out, relayAPM(ctx, http.MethodPost, "/third-party-apis/overview/domain", in, out)
|
||||
return out, relay(ctx, http.MethodPost, o11yRoot+"/third-party-apis/overview/domain", nil, in, out)
|
||||
}
|
||||
|
||||
// ── inputs ────────────────────────────────────────────────────────────────────
|
||||
//
|
||||
// Every type carries the O11y qualifier because these names enter a fleet-wide
|
||||
// document, where one name may have exactly one shape. Each In mirrors the
|
||||
// runtime's own request type tag for tag, so the body relayAPM sends is the
|
||||
// runtime's own request type tag for tag, so the body relay sends is the
|
||||
// body the runtime has always decoded — validation stays the runtime's, one
|
||||
// layer in, exactly where it has always run.
|
||||
|
||||
@@ -634,83 +626,6 @@ type O11yDomainsData struct {
|
||||
|
||||
// ── the seam ──────────────────────────────────────────────────────────────────
|
||||
|
||||
// relayAPM hands a typed op's call to the o11y runtime and decodes the answer
|
||||
// into the op's Out. It is telemetry.go's relay with this face's root: relay
|
||||
// rebuilds calls onto /v1/sentry, this onto /v1/o11y — the two faces' ONE
|
||||
// difference, and the two seams are candidates to fold together the moment
|
||||
// either needs a second change.
|
||||
//
|
||||
// Everything relay guarantees holds here unchanged. The handler called is the
|
||||
// one SetHandler registered — the same value the delegation wildcard forwards
|
||||
// to — so the request runs the whole chain it always ran: identity resolution,
|
||||
// the org gate, the ViewAccess role check every route in this file has always
|
||||
// sat behind, the audit record and the success envelope, in the order they
|
||||
// always ran. There is no policy here: no tenant is resolved, no role is
|
||||
// checked, nothing is scoped.
|
||||
//
|
||||
// Identity is PROPAGATED, never minted: the gateway's assertion travels on as
|
||||
// the same headers it arrived on, which are exactly the headers the runtime's
|
||||
// own identity provider reads. A context with no request behind it — a
|
||||
// command, an agent call — carries no assertion, so the runtime's gate refuses
|
||||
// it, the honest answer rather than an identity invented at this hop.
|
||||
//
|
||||
// A non-2xx becomes an error carrying the runtime's own status and reason, so
|
||||
// the status a caller sees is the status the runtime chose.
|
||||
func relayAPM(ctx context.Context, method, path string, body, out any) error {
|
||||
h := getHandler()
|
||||
if h == nil {
|
||||
return zip.Errorf(http.StatusServiceUnavailable, "o11y runtime not initialized")
|
||||
}
|
||||
|
||||
payload := io.Reader(http.NoBody)
|
||||
if body != nil {
|
||||
b, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return zip.ErrBadRequest(err.Error())
|
||||
}
|
||||
payload = bytes.NewReader(b)
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, method, apmPrefix+path, payload)
|
||||
if err != nil {
|
||||
return zip.ErrBadRequest(err.Error())
|
||||
}
|
||||
if body != nil {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
caller := zip.CallerOf(ctx)
|
||||
for header, value := range map[string]string{
|
||||
zip.HeaderOrg: caller.Org,
|
||||
zip.HeaderProject: caller.Project,
|
||||
zip.HeaderUser: caller.User,
|
||||
zip.HeaderUserName: caller.Name,
|
||||
zip.HeaderUserEmail: caller.Email,
|
||||
zip.HeaderUserOwner: caller.Owner,
|
||||
zip.HeaderRequestID: caller.RequestID,
|
||||
} {
|
||||
if value != "" {
|
||||
req.Header.Set(header, value)
|
||||
}
|
||||
}
|
||||
if caller.Admin {
|
||||
req.Header.Set(zip.HeaderUserAdmin, "true")
|
||||
}
|
||||
if caller.OrgAdmin {
|
||||
req.Header.Set(zip.HeaderUserOrgAdmin, "true")
|
||||
}
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code < http.StatusOK || rec.Code >= http.StatusMultipleChoices {
|
||||
code, reason := apmRefusal(rec.Body.Bytes())
|
||||
return &zip.HTTPError{Status: rec.Code, Code: code, Msg: reason}
|
||||
}
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), out); err != nil {
|
||||
return zip.ErrInternal("cannot read the runtime's answer: " + err.Error())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// apmRefusal reads a refusal for its code and reason. The query-service face
|
||||
// answers refusals in one more shape than the sentry face does — the legacy
|
||||
// {status, errorType, error} — so that shape is read here and every other one
|
||||
|
||||
+35
-35
@@ -271,22 +271,22 @@ func TestDomainAnswerIsTheRuntimeAnswer(t *testing.T) {
|
||||
func TestAPMRoutesAreTheSameTwentyOne(t *testing.T) {
|
||||
app := mounted(t)
|
||||
want := map[string]bool{
|
||||
"POST /v1/o11y/services": true,
|
||||
"GET /v1/o11y/services/list": true,
|
||||
"POST /v1/o11y/service/top_operations": true,
|
||||
"POST /v1/o11y/service/top_level_operations": true,
|
||||
"POST /v1/o11y/services": true,
|
||||
"GET /v1/o11y/services/list": true,
|
||||
"POST /v1/o11y/service/top_operations": true,
|
||||
"POST /v1/o11y/service/top_level_operations": true,
|
||||
"POST /v1/o11y/service/entry_point_operations": true,
|
||||
|
||||
"POST /v1/o11y/messaging-queues/queue-overview": true,
|
||||
"POST /v1/o11y/messaging-queues/kafka/onboarding/producers": true,
|
||||
"POST /v1/o11y/messaging-queues/kafka/onboarding/consumers": true,
|
||||
"POST /v1/o11y/messaging-queues/kafka/onboarding/kafka": true,
|
||||
"POST /v1/o11y/messaging-queues/kafka/partition-latency/overview": true,
|
||||
"POST /v1/o11y/messaging-queues/kafka/partition-latency/consumer": true,
|
||||
"POST /v1/o11y/messaging-queues/kafka/consumer-lag/producer-details": true,
|
||||
"POST /v1/o11y/messaging-queues/kafka/consumer-lag/consumer-details": true,
|
||||
"POST /v1/o11y/messaging-queues/kafka/consumer-lag/network-latency": true,
|
||||
"POST /v1/o11y/messaging-queues/kafka/topic-throughput/producer": true,
|
||||
"POST /v1/o11y/messaging-queues/queue-overview": true,
|
||||
"POST /v1/o11y/messaging-queues/kafka/onboarding/producers": true,
|
||||
"POST /v1/o11y/messaging-queues/kafka/onboarding/consumers": true,
|
||||
"POST /v1/o11y/messaging-queues/kafka/onboarding/kafka": true,
|
||||
"POST /v1/o11y/messaging-queues/kafka/partition-latency/overview": true,
|
||||
"POST /v1/o11y/messaging-queues/kafka/partition-latency/consumer": true,
|
||||
"POST /v1/o11y/messaging-queues/kafka/consumer-lag/producer-details": true,
|
||||
"POST /v1/o11y/messaging-queues/kafka/consumer-lag/consumer-details": true,
|
||||
"POST /v1/o11y/messaging-queues/kafka/consumer-lag/network-latency": true,
|
||||
"POST /v1/o11y/messaging-queues/kafka/topic-throughput/producer": true,
|
||||
"POST /v1/o11y/messaging-queues/kafka/topic-throughput/producer-details": true,
|
||||
"POST /v1/o11y/messaging-queues/kafka/topic-throughput/consumer": true,
|
||||
"POST /v1/o11y/messaging-queues/kafka/topic-throughput/consumer-details": true,
|
||||
@@ -353,27 +353,27 @@ func TestAPMReachesTheDocument(t *testing.T) {
|
||||
}
|
||||
|
||||
for path, method := range map[string]string{
|
||||
"/v1/o11y/services": "post",
|
||||
"/v1/o11y/services/list": "get",
|
||||
"/v1/o11y/service/top_operations": "post",
|
||||
"/v1/o11y/service/top_level_operations": "post",
|
||||
"/v1/o11y/service/entry_point_operations": "post",
|
||||
"/v1/o11y/messaging-queues/queue-overview": "post",
|
||||
"/v1/o11y/messaging-queues/kafka/onboarding/producers": "post",
|
||||
"/v1/o11y/messaging-queues/kafka/onboarding/consumers": "post",
|
||||
"/v1/o11y/messaging-queues/kafka/onboarding/kafka": "post",
|
||||
"/v1/o11y/messaging-queues/kafka/partition-latency/overview": "post",
|
||||
"/v1/o11y/messaging-queues/kafka/partition-latency/consumer": "post",
|
||||
"/v1/o11y/messaging-queues/kafka/consumer-lag/producer-details": "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/topic-throughput/producer": "post",
|
||||
"/v1/o11y/messaging-queues/kafka/topic-throughput/producer-details": "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/span/evaluation": "post",
|
||||
"/v1/o11y/third-party-apis/overview/list": "post",
|
||||
"/v1/o11y/third-party-apis/overview/domain": "post",
|
||||
"/v1/o11y/services": "post",
|
||||
"/v1/o11y/services/list": "get",
|
||||
"/v1/o11y/service/top_operations": "post",
|
||||
"/v1/o11y/service/top_level_operations": "post",
|
||||
"/v1/o11y/service/entry_point_operations": "post",
|
||||
"/v1/o11y/messaging-queues/queue-overview": "post",
|
||||
"/v1/o11y/messaging-queues/kafka/onboarding/producers": "post",
|
||||
"/v1/o11y/messaging-queues/kafka/onboarding/consumers": "post",
|
||||
"/v1/o11y/messaging-queues/kafka/onboarding/kafka": "post",
|
||||
"/v1/o11y/messaging-queues/kafka/partition-latency/overview": "post",
|
||||
"/v1/o11y/messaging-queues/kafka/partition-latency/consumer": "post",
|
||||
"/v1/o11y/messaging-queues/kafka/consumer-lag/producer-details": "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/topic-throughput/producer": "post",
|
||||
"/v1/o11y/messaging-queues/kafka/topic-throughput/producer-details": "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/span/evaluation": "post",
|
||||
"/v1/o11y/third-party-apis/overview/list": "post",
|
||||
"/v1/o11y/third-party-apis/overview/domain": "post",
|
||||
} {
|
||||
op, ok := spec.Paths[path][method]
|
||||
if !ok {
|
||||
|
||||
-593
@@ -1,593 +0,0 @@
|
||||
// Code generated by zipdoc; DO NOT EDIT.
|
||||
//
|
||||
// The APM slice's entries, split out of zipdoc_gen.go so concurrent slices
|
||||
// do not contend on one generated file; regenerating the package folds them
|
||||
// back together, which is the intended end state.
|
||||
|
||||
package o11y
|
||||
|
||||
import (
|
||||
"github.com/zap-proto/zip"
|
||||
)
|
||||
|
||||
func init() {
|
||||
zip.Describe("GET /v1/o11y/services/list", zip.Doc{
|
||||
Description: "Lists the name of every service the trace store holds, with no\nwindow applied — the complete catalog, for pickers and autocomplete.",
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/messaging-queues/kafka/consumer-lag/consumer-details", zip.Doc{
|
||||
Description: "Returns the consumer side of a consumer-lag view: the\nconsumer groups reading the topic/partition named in variables, with their\nthroughput and latency over the window.",
|
||||
Fields: map[string]string{
|
||||
"O11yQueryColumn.isValueColumn": "IsValueColumn marks the column carrying the plotted value.",
|
||||
"O11yQueryColumn.name": "Name is the column's name.",
|
||||
"O11yQueryColumn.queryName": "QueryName is the query the column came from.",
|
||||
"O11yQueryRange.contextTimeout": "ContextTimeout reports that the read hit its deadline.",
|
||||
"O11yQueryRange.contextTimeoutMessage": "ContextTimeoutMessage says where the deadline hit.",
|
||||
"O11yQueryRange.result": "Result holds one entry per query.",
|
||||
"O11yQueryRange.resultType": "ResultType names the overall shape, when the view sets one.",
|
||||
"O11yQueryRangeOut.data": "Data holds the result.",
|
||||
"O11yQueryRangeOut.status": "Status is \"success\".",
|
||||
"O11yQueryResult.anomalyScores": "AnomalyScores score each point's deviation, when the view scores.",
|
||||
"O11yQueryResult.list": "List holds row-shaped results.",
|
||||
"O11yQueryResult.lowerBoundSeries": "LowerBoundSeries bound the prediction from below.",
|
||||
"O11yQueryResult.predictedSeries": "PredictedSeries are model-predicted values, when the view predicts.",
|
||||
"O11yQueryResult.queryName": "QueryName names the query within the view.",
|
||||
"O11yQueryResult.series": "Series are the timeseries, for series-shaped results.",
|
||||
"O11yQueryResult.table": "Table holds table-shaped results.",
|
||||
"O11yQueryResult.upperBoundSeries": "UpperBoundSeries bound the prediction from above.",
|
||||
"O11yQueryTable.columns": "Columns name and type the table's columns.",
|
||||
"O11yQueryTable.rows": "Rows are the table's rows.",
|
||||
"O11yQueryTableRow.data": "Data holds the row's cells keyed by column name; each cell's JSON type is\nthe column's own, so the bytes pass through verbatim.",
|
||||
"O11yQueueIn.end": "End is the window's end, epoch nanoseconds.",
|
||||
"O11yQueueIn.eval_time": "EvalTime bounds the span-evaluation scan, nanoseconds; only the\nspan/evaluation view reads it.",
|
||||
"O11yQueueIn.start": "Start is the window's start, epoch nanoseconds.",
|
||||
"O11yQueueIn.variables": "Variables name what the view drills into — topic, partition, service,\nconsumer_group — keyed by the name the view expects.",
|
||||
"O11yQueueRow.data": "Data holds the row's cells keyed by column name; each cell's JSON type is\nthe column's own, so the bytes pass through verbatim.",
|
||||
"O11yQueueRow.timestamp": "Timestamp anchors the row in time.",
|
||||
"O11ySeries.labels": "Labels key the series, e.g. topic, partition, service_name.",
|
||||
"O11ySeries.labelsArray": "LabelsArray is the same labels in the store's array form.",
|
||||
"O11ySeries.values": "Points are the series' points, under the wire key \"values\".",
|
||||
"O11ySeriesPoint.timestamp": "Timestamp is the point's time, epoch milliseconds.",
|
||||
"O11ySeriesPoint.value": "Value is the point's value, as a decimal string.",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/messaging-queues/kafka/consumer-lag/network-latency", zip.Doc{
|
||||
Description: "Returns consumer network latency correlated per client:\na throughput pass over the window finds the consumer clients, then their\nfetch latency joins in as a latency column per client/instance/service.",
|
||||
Fields: map[string]string{
|
||||
"O11yQueryColumn.isValueColumn": "IsValueColumn marks the column carrying the plotted value.",
|
||||
"O11yQueryColumn.name": "Name is the column's name.",
|
||||
"O11yQueryColumn.queryName": "QueryName is the query the column came from.",
|
||||
"O11yQueryRange.contextTimeout": "ContextTimeout reports that the read hit its deadline.",
|
||||
"O11yQueryRange.contextTimeoutMessage": "ContextTimeoutMessage says where the deadline hit.",
|
||||
"O11yQueryRange.result": "Result holds one entry per query.",
|
||||
"O11yQueryRange.resultType": "ResultType names the overall shape, when the view sets one.",
|
||||
"O11yQueryRangeOut.data": "Data holds the result.",
|
||||
"O11yQueryRangeOut.status": "Status is \"success\".",
|
||||
"O11yQueryResult.anomalyScores": "AnomalyScores score each point's deviation, when the view scores.",
|
||||
"O11yQueryResult.list": "List holds row-shaped results.",
|
||||
"O11yQueryResult.lowerBoundSeries": "LowerBoundSeries bound the prediction from below.",
|
||||
"O11yQueryResult.predictedSeries": "PredictedSeries are model-predicted values, when the view predicts.",
|
||||
"O11yQueryResult.queryName": "QueryName names the query within the view.",
|
||||
"O11yQueryResult.series": "Series are the timeseries, for series-shaped results.",
|
||||
"O11yQueryResult.table": "Table holds table-shaped results.",
|
||||
"O11yQueryResult.upperBoundSeries": "UpperBoundSeries bound the prediction from above.",
|
||||
"O11yQueryTable.columns": "Columns name and type the table's columns.",
|
||||
"O11yQueryTable.rows": "Rows are the table's rows.",
|
||||
"O11yQueryTableRow.data": "Data holds the row's cells keyed by column name; each cell's JSON type is\nthe column's own, so the bytes pass through verbatim.",
|
||||
"O11yQueueIn.end": "End is the window's end, epoch nanoseconds.",
|
||||
"O11yQueueIn.eval_time": "EvalTime bounds the span-evaluation scan, nanoseconds; only the\nspan/evaluation view reads it.",
|
||||
"O11yQueueIn.start": "Start is the window's start, epoch nanoseconds.",
|
||||
"O11yQueueIn.variables": "Variables name what the view drills into — topic, partition, service,\nconsumer_group — keyed by the name the view expects.",
|
||||
"O11yQueueRow.data": "Data holds the row's cells keyed by column name; each cell's JSON type is\nthe column's own, so the bytes pass through verbatim.",
|
||||
"O11yQueueRow.timestamp": "Timestamp anchors the row in time.",
|
||||
"O11ySeries.labels": "Labels key the series, e.g. topic, partition, service_name.",
|
||||
"O11ySeries.labelsArray": "LabelsArray is the same labels in the store's array form.",
|
||||
"O11ySeries.values": "Points are the series' points, under the wire key \"values\".",
|
||||
"O11ySeriesPoint.timestamp": "Timestamp is the point's time, epoch milliseconds.",
|
||||
"O11ySeriesPoint.value": "Value is the point's value, as a decimal string.",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/messaging-queues/kafka/consumer-lag/producer-details", zip.Doc{
|
||||
Description: "Returns the producer side of a consumer-lag view: the\nproducers writing to the topic/partition named in variables, with their\nthroughput and latency over the window.",
|
||||
Fields: map[string]string{
|
||||
"O11yQueryColumn.isValueColumn": "IsValueColumn marks the column carrying the plotted value.",
|
||||
"O11yQueryColumn.name": "Name is the column's name.",
|
||||
"O11yQueryColumn.queryName": "QueryName is the query the column came from.",
|
||||
"O11yQueryRange.contextTimeout": "ContextTimeout reports that the read hit its deadline.",
|
||||
"O11yQueryRange.contextTimeoutMessage": "ContextTimeoutMessage says where the deadline hit.",
|
||||
"O11yQueryRange.result": "Result holds one entry per query.",
|
||||
"O11yQueryRange.resultType": "ResultType names the overall shape, when the view sets one.",
|
||||
"O11yQueryRangeOut.data": "Data holds the result.",
|
||||
"O11yQueryRangeOut.status": "Status is \"success\".",
|
||||
"O11yQueryResult.anomalyScores": "AnomalyScores score each point's deviation, when the view scores.",
|
||||
"O11yQueryResult.list": "List holds row-shaped results.",
|
||||
"O11yQueryResult.lowerBoundSeries": "LowerBoundSeries bound the prediction from below.",
|
||||
"O11yQueryResult.predictedSeries": "PredictedSeries are model-predicted values, when the view predicts.",
|
||||
"O11yQueryResult.queryName": "QueryName names the query within the view.",
|
||||
"O11yQueryResult.series": "Series are the timeseries, for series-shaped results.",
|
||||
"O11yQueryResult.table": "Table holds table-shaped results.",
|
||||
"O11yQueryResult.upperBoundSeries": "UpperBoundSeries bound the prediction from above.",
|
||||
"O11yQueryTable.columns": "Columns name and type the table's columns.",
|
||||
"O11yQueryTable.rows": "Rows are the table's rows.",
|
||||
"O11yQueryTableRow.data": "Data holds the row's cells keyed by column name; each cell's JSON type is\nthe column's own, so the bytes pass through verbatim.",
|
||||
"O11yQueueIn.end": "End is the window's end, epoch nanoseconds.",
|
||||
"O11yQueueIn.eval_time": "EvalTime bounds the span-evaluation scan, nanoseconds; only the\nspan/evaluation view reads it.",
|
||||
"O11yQueueIn.start": "Start is the window's start, epoch nanoseconds.",
|
||||
"O11yQueueIn.variables": "Variables name what the view drills into — topic, partition, service,\nconsumer_group — keyed by the name the view expects.",
|
||||
"O11yQueueRow.data": "Data holds the row's cells keyed by column name; each cell's JSON type is\nthe column's own, so the bytes pass through verbatim.",
|
||||
"O11yQueueRow.timestamp": "Timestamp anchors the row in time.",
|
||||
"O11ySeries.labels": "Labels key the series, e.g. topic, partition, service_name.",
|
||||
"O11ySeries.labelsArray": "LabelsArray is the same labels in the store's array form.",
|
||||
"O11ySeries.values": "Points are the series' points, under the wire key \"values\".",
|
||||
"O11ySeriesPoint.timestamp": "Timestamp is the point's time, epoch milliseconds.",
|
||||
"O11ySeriesPoint.value": "Value is the point's value, as a decimal string.",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/messaging-queues/kafka/onboarding/consumers", zip.Doc{
|
||||
Description: "Checks whether the spans the Kafka consumer views need\nare arriving, row for row like producersOnboarding.",
|
||||
Fields: map[string]string{
|
||||
"O11yQueueCheck.attribute": "Attribute is the span attribute or telemetry the check looked for.",
|
||||
"O11yQueueCheck.error_message": "Message says what is missing when the check fails; empty on a pass. Its\nwire key is error_message.",
|
||||
"O11yQueueCheck.status": "Status is \"1\" when the telemetry is present, \"0\" when it is not.",
|
||||
"O11yQueueChecksOut.data": "Data holds one check per required attribute, sorted by attribute.",
|
||||
"O11yQueueChecksOut.status": "Status is \"success\".",
|
||||
"O11yQueueIn.end": "End is the window's end, epoch nanoseconds.",
|
||||
"O11yQueueIn.eval_time": "EvalTime bounds the span-evaluation scan, nanoseconds; only the\nspan/evaluation view reads it.",
|
||||
"O11yQueueIn.start": "Start is the window's start, epoch nanoseconds.",
|
||||
"O11yQueueIn.variables": "Variables name what the view drills into — topic, partition, service,\nconsumer_group — keyed by the name the view expects.",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/messaging-queues/kafka/onboarding/kafka", zip.Doc{
|
||||
Description: "Checks whether Kafka's own metrics — consumer lag and\npartition telemetry — are arriving, so the lag views can be lit up.",
|
||||
Fields: map[string]string{
|
||||
"O11yQueueCheck.attribute": "Attribute is the span attribute or telemetry the check looked for.",
|
||||
"O11yQueueCheck.error_message": "Message says what is missing when the check fails; empty on a pass. Its\nwire key is error_message.",
|
||||
"O11yQueueCheck.status": "Status is \"1\" when the telemetry is present, \"0\" when it is not.",
|
||||
"O11yQueueChecksOut.data": "Data holds one check per required attribute, sorted by attribute.",
|
||||
"O11yQueueChecksOut.status": "Status is \"success\".",
|
||||
"O11yQueueIn.end": "End is the window's end, epoch nanoseconds.",
|
||||
"O11yQueueIn.eval_time": "EvalTime bounds the span-evaluation scan, nanoseconds; only the\nspan/evaluation view reads it.",
|
||||
"O11yQueueIn.start": "Start is the window's start, epoch nanoseconds.",
|
||||
"O11yQueueIn.variables": "Variables name what the view drills into — topic, partition, service,\nconsumer_group — keyed by the name the view expects.",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/messaging-queues/kafka/onboarding/producers", zip.Doc{
|
||||
Description: "Checks whether the spans the Kafka producer views need\nare arriving — one row per required span attribute, with a pass/fail status\nand, on failure, what is missing from the instrumentation.",
|
||||
Fields: map[string]string{
|
||||
"O11yQueueCheck.attribute": "Attribute is the span attribute or telemetry the check looked for.",
|
||||
"O11yQueueCheck.error_message": "Message says what is missing when the check fails; empty on a pass. Its\nwire key is error_message.",
|
||||
"O11yQueueCheck.status": "Status is \"1\" when the telemetry is present, \"0\" when it is not.",
|
||||
"O11yQueueChecksOut.data": "Data holds one check per required attribute, sorted by attribute.",
|
||||
"O11yQueueChecksOut.status": "Status is \"success\".",
|
||||
"O11yQueueIn.end": "End is the window's end, epoch nanoseconds.",
|
||||
"O11yQueueIn.eval_time": "EvalTime bounds the span-evaluation scan, nanoseconds; only the\nspan/evaluation view reads it.",
|
||||
"O11yQueueIn.start": "Start is the window's start, epoch nanoseconds.",
|
||||
"O11yQueueIn.variables": "Variables name what the view drills into — topic, partition, service,\nconsumer_group — keyed by the name the view expects.",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/messaging-queues/kafka/partition-latency/consumer", zip.Doc{
|
||||
Description: "Returns the consumer-group latency detail for the\ntopic and partition named in the request's variables.",
|
||||
Fields: map[string]string{
|
||||
"O11yQueryColumn.isValueColumn": "IsValueColumn marks the column carrying the plotted value.",
|
||||
"O11yQueryColumn.name": "Name is the column's name.",
|
||||
"O11yQueryColumn.queryName": "QueryName is the query the column came from.",
|
||||
"O11yQueryRange.contextTimeout": "ContextTimeout reports that the read hit its deadline.",
|
||||
"O11yQueryRange.contextTimeoutMessage": "ContextTimeoutMessage says where the deadline hit.",
|
||||
"O11yQueryRange.result": "Result holds one entry per query.",
|
||||
"O11yQueryRange.resultType": "ResultType names the overall shape, when the view sets one.",
|
||||
"O11yQueryRangeOut.data": "Data holds the result.",
|
||||
"O11yQueryRangeOut.status": "Status is \"success\".",
|
||||
"O11yQueryResult.anomalyScores": "AnomalyScores score each point's deviation, when the view scores.",
|
||||
"O11yQueryResult.list": "List holds row-shaped results.",
|
||||
"O11yQueryResult.lowerBoundSeries": "LowerBoundSeries bound the prediction from below.",
|
||||
"O11yQueryResult.predictedSeries": "PredictedSeries are model-predicted values, when the view predicts.",
|
||||
"O11yQueryResult.queryName": "QueryName names the query within the view.",
|
||||
"O11yQueryResult.series": "Series are the timeseries, for series-shaped results.",
|
||||
"O11yQueryResult.table": "Table holds table-shaped results.",
|
||||
"O11yQueryResult.upperBoundSeries": "UpperBoundSeries bound the prediction from above.",
|
||||
"O11yQueryTable.columns": "Columns name and type the table's columns.",
|
||||
"O11yQueryTable.rows": "Rows are the table's rows.",
|
||||
"O11yQueryTableRow.data": "Data holds the row's cells keyed by column name; each cell's JSON type is\nthe column's own, so the bytes pass through verbatim.",
|
||||
"O11yQueueIn.end": "End is the window's end, epoch nanoseconds.",
|
||||
"O11yQueueIn.eval_time": "EvalTime bounds the span-evaluation scan, nanoseconds; only the\nspan/evaluation view reads it.",
|
||||
"O11yQueueIn.start": "Start is the window's start, epoch nanoseconds.",
|
||||
"O11yQueueIn.variables": "Variables name what the view drills into — topic, partition, service,\nconsumer_group — keyed by the name the view expects.",
|
||||
"O11yQueueRow.data": "Data holds the row's cells keyed by column name; each cell's JSON type is\nthe column's own, so the bytes pass through verbatim.",
|
||||
"O11yQueueRow.timestamp": "Timestamp anchors the row in time.",
|
||||
"O11ySeries.labels": "Labels key the series, e.g. topic, partition, service_name.",
|
||||
"O11ySeries.labelsArray": "LabelsArray is the same labels in the store's array form.",
|
||||
"O11ySeries.values": "Points are the series' points, under the wire key \"values\".",
|
||||
"O11ySeriesPoint.timestamp": "Timestamp is the point's time, epoch milliseconds.",
|
||||
"O11ySeriesPoint.value": "Value is the point's value, as a decimal string.",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/messaging-queues/kafka/partition-latency/overview", zip.Doc{
|
||||
Description: "Returns the per-partition latency overview for the window —\neach topic/partition with its throughput and latency profile.",
|
||||
Fields: map[string]string{
|
||||
"O11yQueryColumn.isValueColumn": "IsValueColumn marks the column carrying the plotted value.",
|
||||
"O11yQueryColumn.name": "Name is the column's name.",
|
||||
"O11yQueryColumn.queryName": "QueryName is the query the column came from.",
|
||||
"O11yQueryRange.contextTimeout": "ContextTimeout reports that the read hit its deadline.",
|
||||
"O11yQueryRange.contextTimeoutMessage": "ContextTimeoutMessage says where the deadline hit.",
|
||||
"O11yQueryRange.result": "Result holds one entry per query.",
|
||||
"O11yQueryRange.resultType": "ResultType names the overall shape, when the view sets one.",
|
||||
"O11yQueryRangeOut.data": "Data holds the result.",
|
||||
"O11yQueryRangeOut.status": "Status is \"success\".",
|
||||
"O11yQueryResult.anomalyScores": "AnomalyScores score each point's deviation, when the view scores.",
|
||||
"O11yQueryResult.list": "List holds row-shaped results.",
|
||||
"O11yQueryResult.lowerBoundSeries": "LowerBoundSeries bound the prediction from below.",
|
||||
"O11yQueryResult.predictedSeries": "PredictedSeries are model-predicted values, when the view predicts.",
|
||||
"O11yQueryResult.queryName": "QueryName names the query within the view.",
|
||||
"O11yQueryResult.series": "Series are the timeseries, for series-shaped results.",
|
||||
"O11yQueryResult.table": "Table holds table-shaped results.",
|
||||
"O11yQueryResult.upperBoundSeries": "UpperBoundSeries bound the prediction from above.",
|
||||
"O11yQueryTable.columns": "Columns name and type the table's columns.",
|
||||
"O11yQueryTable.rows": "Rows are the table's rows.",
|
||||
"O11yQueryTableRow.data": "Data holds the row's cells keyed by column name; each cell's JSON type is\nthe column's own, so the bytes pass through verbatim.",
|
||||
"O11yQueueIn.end": "End is the window's end, epoch nanoseconds.",
|
||||
"O11yQueueIn.eval_time": "EvalTime bounds the span-evaluation scan, nanoseconds; only the\nspan/evaluation view reads it.",
|
||||
"O11yQueueIn.start": "Start is the window's start, epoch nanoseconds.",
|
||||
"O11yQueueIn.variables": "Variables name what the view drills into — topic, partition, service,\nconsumer_group — keyed by the name the view expects.",
|
||||
"O11yQueueRow.data": "Data holds the row's cells keyed by column name; each cell's JSON type is\nthe column's own, so the bytes pass through verbatim.",
|
||||
"O11yQueueRow.timestamp": "Timestamp anchors the row in time.",
|
||||
"O11ySeries.labels": "Labels key the series, e.g. topic, partition, service_name.",
|
||||
"O11ySeries.labelsArray": "LabelsArray is the same labels in the store's array form.",
|
||||
"O11ySeries.values": "Points are the series' points, under the wire key \"values\".",
|
||||
"O11ySeriesPoint.timestamp": "Timestamp is the point's time, epoch milliseconds.",
|
||||
"O11ySeriesPoint.value": "Value is the point's value, as a decimal string.",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/messaging-queues/kafka/span/evaluation", zip.Doc{
|
||||
Description: "Correlates producer and consumer spans over the evaluation\nwindow (eval_time bounds the scan) and returns the pairings with their\nend-to-end delay — the check that messages produced are being consumed.",
|
||||
Fields: map[string]string{
|
||||
"O11yQueryColumn.isValueColumn": "IsValueColumn marks the column carrying the plotted value.",
|
||||
"O11yQueryColumn.name": "Name is the column's name.",
|
||||
"O11yQueryColumn.queryName": "QueryName is the query the column came from.",
|
||||
"O11yQueryRange.contextTimeout": "ContextTimeout reports that the read hit its deadline.",
|
||||
"O11yQueryRange.contextTimeoutMessage": "ContextTimeoutMessage says where the deadline hit.",
|
||||
"O11yQueryRange.result": "Result holds one entry per query.",
|
||||
"O11yQueryRange.resultType": "ResultType names the overall shape, when the view sets one.",
|
||||
"O11yQueryRangeOut.data": "Data holds the result.",
|
||||
"O11yQueryRangeOut.status": "Status is \"success\".",
|
||||
"O11yQueryResult.anomalyScores": "AnomalyScores score each point's deviation, when the view scores.",
|
||||
"O11yQueryResult.list": "List holds row-shaped results.",
|
||||
"O11yQueryResult.lowerBoundSeries": "LowerBoundSeries bound the prediction from below.",
|
||||
"O11yQueryResult.predictedSeries": "PredictedSeries are model-predicted values, when the view predicts.",
|
||||
"O11yQueryResult.queryName": "QueryName names the query within the view.",
|
||||
"O11yQueryResult.series": "Series are the timeseries, for series-shaped results.",
|
||||
"O11yQueryResult.table": "Table holds table-shaped results.",
|
||||
"O11yQueryResult.upperBoundSeries": "UpperBoundSeries bound the prediction from above.",
|
||||
"O11yQueryTable.columns": "Columns name and type the table's columns.",
|
||||
"O11yQueryTable.rows": "Rows are the table's rows.",
|
||||
"O11yQueryTableRow.data": "Data holds the row's cells keyed by column name; each cell's JSON type is\nthe column's own, so the bytes pass through verbatim.",
|
||||
"O11yQueueIn.end": "End is the window's end, epoch nanoseconds.",
|
||||
"O11yQueueIn.eval_time": "EvalTime bounds the span-evaluation scan, nanoseconds; only the\nspan/evaluation view reads it.",
|
||||
"O11yQueueIn.start": "Start is the window's start, epoch nanoseconds.",
|
||||
"O11yQueueIn.variables": "Variables name what the view drills into — topic, partition, service,\nconsumer_group — keyed by the name the view expects.",
|
||||
"O11yQueueRow.data": "Data holds the row's cells keyed by column name; each cell's JSON type is\nthe column's own, so the bytes pass through verbatim.",
|
||||
"O11yQueueRow.timestamp": "Timestamp anchors the row in time.",
|
||||
"O11ySeries.labels": "Labels key the series, e.g. topic, partition, service_name.",
|
||||
"O11ySeries.labelsArray": "LabelsArray is the same labels in the store's array form.",
|
||||
"O11ySeries.values": "Points are the series' points, under the wire key \"values\".",
|
||||
"O11ySeriesPoint.timestamp": "Timestamp is the point's time, epoch milliseconds.",
|
||||
"O11ySeriesPoint.value": "Value is the point's value, as a decimal string.",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/messaging-queues/kafka/topic-throughput/consumer", zip.Doc{
|
||||
Description: "Returns the consumer topic-throughput overview for the\nwindow — what each consumer group read, per topic.",
|
||||
Fields: map[string]string{
|
||||
"O11yQueryColumn.isValueColumn": "IsValueColumn marks the column carrying the plotted value.",
|
||||
"O11yQueryColumn.name": "Name is the column's name.",
|
||||
"O11yQueryColumn.queryName": "QueryName is the query the column came from.",
|
||||
"O11yQueryRange.contextTimeout": "ContextTimeout reports that the read hit its deadline.",
|
||||
"O11yQueryRange.contextTimeoutMessage": "ContextTimeoutMessage says where the deadline hit.",
|
||||
"O11yQueryRange.result": "Result holds one entry per query.",
|
||||
"O11yQueryRange.resultType": "ResultType names the overall shape, when the view sets one.",
|
||||
"O11yQueryRangeOut.data": "Data holds the result.",
|
||||
"O11yQueryRangeOut.status": "Status is \"success\".",
|
||||
"O11yQueryResult.anomalyScores": "AnomalyScores score each point's deviation, when the view scores.",
|
||||
"O11yQueryResult.list": "List holds row-shaped results.",
|
||||
"O11yQueryResult.lowerBoundSeries": "LowerBoundSeries bound the prediction from below.",
|
||||
"O11yQueryResult.predictedSeries": "PredictedSeries are model-predicted values, when the view predicts.",
|
||||
"O11yQueryResult.queryName": "QueryName names the query within the view.",
|
||||
"O11yQueryResult.series": "Series are the timeseries, for series-shaped results.",
|
||||
"O11yQueryResult.table": "Table holds table-shaped results.",
|
||||
"O11yQueryResult.upperBoundSeries": "UpperBoundSeries bound the prediction from above.",
|
||||
"O11yQueryTable.columns": "Columns name and type the table's columns.",
|
||||
"O11yQueryTable.rows": "Rows are the table's rows.",
|
||||
"O11yQueryTableRow.data": "Data holds the row's cells keyed by column name; each cell's JSON type is\nthe column's own, so the bytes pass through verbatim.",
|
||||
"O11yQueueIn.end": "End is the window's end, epoch nanoseconds.",
|
||||
"O11yQueueIn.eval_time": "EvalTime bounds the span-evaluation scan, nanoseconds; only the\nspan/evaluation view reads it.",
|
||||
"O11yQueueIn.start": "Start is the window's start, epoch nanoseconds.",
|
||||
"O11yQueueIn.variables": "Variables name what the view drills into — topic, partition, service,\nconsumer_group — keyed by the name the view expects.",
|
||||
"O11yQueueRow.data": "Data holds the row's cells keyed by column name; each cell's JSON type is\nthe column's own, so the bytes pass through verbatim.",
|
||||
"O11yQueueRow.timestamp": "Timestamp anchors the row in time.",
|
||||
"O11ySeries.labels": "Labels key the series, e.g. topic, partition, service_name.",
|
||||
"O11ySeries.labelsArray": "LabelsArray is the same labels in the store's array form.",
|
||||
"O11ySeries.values": "Points are the series' points, under the wire key \"values\".",
|
||||
"O11ySeriesPoint.timestamp": "Timestamp is the point's time, epoch milliseconds.",
|
||||
"O11ySeriesPoint.value": "Value is the point's value, as a decimal string.",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/messaging-queues/kafka/topic-throughput/consumer-details", zip.Doc{
|
||||
Description: "Breaks one consumer topic's throughput down using\nthe topic and service named in variables.",
|
||||
Fields: map[string]string{
|
||||
"O11yQueryColumn.isValueColumn": "IsValueColumn marks the column carrying the plotted value.",
|
||||
"O11yQueryColumn.name": "Name is the column's name.",
|
||||
"O11yQueryColumn.queryName": "QueryName is the query the column came from.",
|
||||
"O11yQueryRange.contextTimeout": "ContextTimeout reports that the read hit its deadline.",
|
||||
"O11yQueryRange.contextTimeoutMessage": "ContextTimeoutMessage says where the deadline hit.",
|
||||
"O11yQueryRange.result": "Result holds one entry per query.",
|
||||
"O11yQueryRange.resultType": "ResultType names the overall shape, when the view sets one.",
|
||||
"O11yQueryRangeOut.data": "Data holds the result.",
|
||||
"O11yQueryRangeOut.status": "Status is \"success\".",
|
||||
"O11yQueryResult.anomalyScores": "AnomalyScores score each point's deviation, when the view scores.",
|
||||
"O11yQueryResult.list": "List holds row-shaped results.",
|
||||
"O11yQueryResult.lowerBoundSeries": "LowerBoundSeries bound the prediction from below.",
|
||||
"O11yQueryResult.predictedSeries": "PredictedSeries are model-predicted values, when the view predicts.",
|
||||
"O11yQueryResult.queryName": "QueryName names the query within the view.",
|
||||
"O11yQueryResult.series": "Series are the timeseries, for series-shaped results.",
|
||||
"O11yQueryResult.table": "Table holds table-shaped results.",
|
||||
"O11yQueryResult.upperBoundSeries": "UpperBoundSeries bound the prediction from above.",
|
||||
"O11yQueryTable.columns": "Columns name and type the table's columns.",
|
||||
"O11yQueryTable.rows": "Rows are the table's rows.",
|
||||
"O11yQueryTableRow.data": "Data holds the row's cells keyed by column name; each cell's JSON type is\nthe column's own, so the bytes pass through verbatim.",
|
||||
"O11yQueueIn.end": "End is the window's end, epoch nanoseconds.",
|
||||
"O11yQueueIn.eval_time": "EvalTime bounds the span-evaluation scan, nanoseconds; only the\nspan/evaluation view reads it.",
|
||||
"O11yQueueIn.start": "Start is the window's start, epoch nanoseconds.",
|
||||
"O11yQueueIn.variables": "Variables name what the view drills into — topic, partition, service,\nconsumer_group — keyed by the name the view expects.",
|
||||
"O11yQueueRow.data": "Data holds the row's cells keyed by column name; each cell's JSON type is\nthe column's own, so the bytes pass through verbatim.",
|
||||
"O11yQueueRow.timestamp": "Timestamp anchors the row in time.",
|
||||
"O11ySeries.labels": "Labels key the series, e.g. topic, partition, service_name.",
|
||||
"O11ySeries.labelsArray": "LabelsArray is the same labels in the store's array form.",
|
||||
"O11ySeries.values": "Points are the series' points, under the wire key \"values\".",
|
||||
"O11ySeriesPoint.timestamp": "Timestamp is the point's time, epoch milliseconds.",
|
||||
"O11ySeriesPoint.value": "Value is the point's value, as a decimal string.",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/messaging-queues/kafka/topic-throughput/producer", zip.Doc{
|
||||
Description: "Returns the producer topic-throughput overview for the\nwindow — what each producer service wrote, per topic.",
|
||||
Fields: map[string]string{
|
||||
"O11yQueryColumn.isValueColumn": "IsValueColumn marks the column carrying the plotted value.",
|
||||
"O11yQueryColumn.name": "Name is the column's name.",
|
||||
"O11yQueryColumn.queryName": "QueryName is the query the column came from.",
|
||||
"O11yQueryRange.contextTimeout": "ContextTimeout reports that the read hit its deadline.",
|
||||
"O11yQueryRange.contextTimeoutMessage": "ContextTimeoutMessage says where the deadline hit.",
|
||||
"O11yQueryRange.result": "Result holds one entry per query.",
|
||||
"O11yQueryRange.resultType": "ResultType names the overall shape, when the view sets one.",
|
||||
"O11yQueryRangeOut.data": "Data holds the result.",
|
||||
"O11yQueryRangeOut.status": "Status is \"success\".",
|
||||
"O11yQueryResult.anomalyScores": "AnomalyScores score each point's deviation, when the view scores.",
|
||||
"O11yQueryResult.list": "List holds row-shaped results.",
|
||||
"O11yQueryResult.lowerBoundSeries": "LowerBoundSeries bound the prediction from below.",
|
||||
"O11yQueryResult.predictedSeries": "PredictedSeries are model-predicted values, when the view predicts.",
|
||||
"O11yQueryResult.queryName": "QueryName names the query within the view.",
|
||||
"O11yQueryResult.series": "Series are the timeseries, for series-shaped results.",
|
||||
"O11yQueryResult.table": "Table holds table-shaped results.",
|
||||
"O11yQueryResult.upperBoundSeries": "UpperBoundSeries bound the prediction from above.",
|
||||
"O11yQueryTable.columns": "Columns name and type the table's columns.",
|
||||
"O11yQueryTable.rows": "Rows are the table's rows.",
|
||||
"O11yQueryTableRow.data": "Data holds the row's cells keyed by column name; each cell's JSON type is\nthe column's own, so the bytes pass through verbatim.",
|
||||
"O11yQueueIn.end": "End is the window's end, epoch nanoseconds.",
|
||||
"O11yQueueIn.eval_time": "EvalTime bounds the span-evaluation scan, nanoseconds; only the\nspan/evaluation view reads it.",
|
||||
"O11yQueueIn.start": "Start is the window's start, epoch nanoseconds.",
|
||||
"O11yQueueIn.variables": "Variables name what the view drills into — topic, partition, service,\nconsumer_group — keyed by the name the view expects.",
|
||||
"O11yQueueRow.data": "Data holds the row's cells keyed by column name; each cell's JSON type is\nthe column's own, so the bytes pass through verbatim.",
|
||||
"O11yQueueRow.timestamp": "Timestamp anchors the row in time.",
|
||||
"O11ySeries.labels": "Labels key the series, e.g. topic, partition, service_name.",
|
||||
"O11ySeries.labelsArray": "LabelsArray is the same labels in the store's array form.",
|
||||
"O11ySeries.values": "Points are the series' points, under the wire key \"values\".",
|
||||
"O11ySeriesPoint.timestamp": "Timestamp is the point's time, epoch milliseconds.",
|
||||
"O11ySeriesPoint.value": "Value is the point's value, as a decimal string.",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/messaging-queues/kafka/topic-throughput/producer-details", zip.Doc{
|
||||
Description: "Breaks one producer topic's throughput down using\nthe topic and service named in variables.",
|
||||
Fields: map[string]string{
|
||||
"O11yQueryColumn.isValueColumn": "IsValueColumn marks the column carrying the plotted value.",
|
||||
"O11yQueryColumn.name": "Name is the column's name.",
|
||||
"O11yQueryColumn.queryName": "QueryName is the query the column came from.",
|
||||
"O11yQueryRange.contextTimeout": "ContextTimeout reports that the read hit its deadline.",
|
||||
"O11yQueryRange.contextTimeoutMessage": "ContextTimeoutMessage says where the deadline hit.",
|
||||
"O11yQueryRange.result": "Result holds one entry per query.",
|
||||
"O11yQueryRange.resultType": "ResultType names the overall shape, when the view sets one.",
|
||||
"O11yQueryRangeOut.data": "Data holds the result.",
|
||||
"O11yQueryRangeOut.status": "Status is \"success\".",
|
||||
"O11yQueryResult.anomalyScores": "AnomalyScores score each point's deviation, when the view scores.",
|
||||
"O11yQueryResult.list": "List holds row-shaped results.",
|
||||
"O11yQueryResult.lowerBoundSeries": "LowerBoundSeries bound the prediction from below.",
|
||||
"O11yQueryResult.predictedSeries": "PredictedSeries are model-predicted values, when the view predicts.",
|
||||
"O11yQueryResult.queryName": "QueryName names the query within the view.",
|
||||
"O11yQueryResult.series": "Series are the timeseries, for series-shaped results.",
|
||||
"O11yQueryResult.table": "Table holds table-shaped results.",
|
||||
"O11yQueryResult.upperBoundSeries": "UpperBoundSeries bound the prediction from above.",
|
||||
"O11yQueryTable.columns": "Columns name and type the table's columns.",
|
||||
"O11yQueryTable.rows": "Rows are the table's rows.",
|
||||
"O11yQueryTableRow.data": "Data holds the row's cells keyed by column name; each cell's JSON type is\nthe column's own, so the bytes pass through verbatim.",
|
||||
"O11yQueueIn.end": "End is the window's end, epoch nanoseconds.",
|
||||
"O11yQueueIn.eval_time": "EvalTime bounds the span-evaluation scan, nanoseconds; only the\nspan/evaluation view reads it.",
|
||||
"O11yQueueIn.start": "Start is the window's start, epoch nanoseconds.",
|
||||
"O11yQueueIn.variables": "Variables name what the view drills into — topic, partition, service,\nconsumer_group — keyed by the name the view expects.",
|
||||
"O11yQueueRow.data": "Data holds the row's cells keyed by column name; each cell's JSON type is\nthe column's own, so the bytes pass through verbatim.",
|
||||
"O11yQueueRow.timestamp": "Timestamp anchors the row in time.",
|
||||
"O11ySeries.labels": "Labels key the series, e.g. topic, partition, service_name.",
|
||||
"O11ySeries.labelsArray": "LabelsArray is the same labels in the store's array form.",
|
||||
"O11ySeries.values": "Points are the series' points, under the wire key \"values\".",
|
||||
"O11ySeriesPoint.timestamp": "Timestamp is the point's time, epoch milliseconds.",
|
||||
"O11ySeriesPoint.value": "Value is the point's value, as a decimal string.",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/messaging-queues/queue-overview", zip.Doc{
|
||||
Description: "Lists the messaging destinations observed in the window — one\nrow per queue/destination/service combination with its throughput and\nlatency columns. Filters narrow by queue system, destination, service or any\nspan attribute.",
|
||||
Fields: map[string]string{
|
||||
"O11yQueueFilterKey.dataType": "DataType is the attribute's type: string, int64, float64 or bool.",
|
||||
"O11yQueueFilterKey.isColumn": "IsColumn marks an attribute materialized as its own store column.",
|
||||
"O11yQueueFilterKey.isJSON": "IsJSON marks an attribute read out of the span's JSON body.",
|
||||
"O11yQueueFilterKey.key": "Key is the attribute name.",
|
||||
"O11yQueueFilterKey.type": "Type says which plane the attribute lives on: tag or resource.",
|
||||
"O11yQueueFilterRule.key": "Key names the attribute the predicate tests.",
|
||||
"O11yQueueFilterRule.op": "Op is the comparison, e.g. =, !=, in, contains.",
|
||||
"O11yQueueFilterRule.value": "Value is the operand; its JSON type follows the attribute's dataType.",
|
||||
"O11yQueueFilterSet.items": "Items are the predicates.",
|
||||
"O11yQueueFilterSet.op": "Op combines the items: AND or OR.",
|
||||
"O11yQueueListIn.end": "End is the window's end, epoch nanoseconds.",
|
||||
"O11yQueueListIn.filters": "Filters narrow the rows by span attribute; null means all rows.",
|
||||
"O11yQueueListIn.limit": "Limit caps how many rows come back.",
|
||||
"O11yQueueListIn.start": "Start is the window's start, epoch nanoseconds.",
|
||||
"O11yQueueRow.data": "Data holds the row's cells keyed by column name; each cell's JSON type is\nthe column's own, so the bytes pass through verbatim.",
|
||||
"O11yQueueRow.timestamp": "Timestamp anchors the row in time.",
|
||||
"O11yQueueRowsOut.data": "Data holds one row per messaging destination.",
|
||||
"O11yQueueRowsOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/service/entry_point_operations", zip.Doc{
|
||||
Description: "Returns one service's entry-point operations with the\nsame latency and error profile topOperations reports.",
|
||||
Fields: map[string]string{
|
||||
"O11yOperation.errorCount": "ErrorCount is how many of those runs errored.",
|
||||
"O11yOperation.name": "Name is the operation (span name).",
|
||||
"O11yOperation.numCalls": "NumCalls is how many times it ran in the window.",
|
||||
"O11yOperation.p50": "P50 is its median latency, nanoseconds.",
|
||||
"O11yOperation.p95": "P95 is its p95 latency, nanoseconds.",
|
||||
"O11yOperation.p99": "P99 is its p99 latency, nanoseconds.",
|
||||
"O11yOperationsIn.end": "End is the window's end, epoch nanoseconds as a string.",
|
||||
"O11yOperationsIn.limit": "Limit caps how many operations come back.",
|
||||
"O11yOperationsIn.service": "Service is the service whose operations are read.",
|
||||
"O11yOperationsIn.start": "Start is the window's start, epoch nanoseconds as a string.",
|
||||
"O11yOperationsIn.tags": "Tags narrow the spans counted, each a span-attribute predicate.",
|
||||
"O11yOperationsOut.data": "Data holds one entry per operation.",
|
||||
"O11yOperationsOut.status": "Status is \"success\".",
|
||||
"O11yServiceTag.BoolValues": "BoolValues are the boolean operands, when the attribute is a bool.",
|
||||
"O11yServiceTag.Key": "Key is the span attribute to test.",
|
||||
"O11yServiceTag.NumberValues": "NumberValues are the numeric operands, when the attribute is a number.",
|
||||
"O11yServiceTag.Operator": "Operator is how to test it, e.g. in, not_in.",
|
||||
"O11yServiceTag.StringValues": "StringValues are the string operands, when the attribute is a string.",
|
||||
"O11yServiceTag.TagType": "TagType says which plane the attribute lives on, e.g. tag or resource.",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/service/top_level_operations", zip.Doc{
|
||||
Description: "Maps each service to its entry-point span names — for the\none service named in the request, or for every service when none is.",
|
||||
Fields: map[string]string{
|
||||
"O11yTopLevelOpsIn.end": "End is the window's end, epoch nanoseconds as a string; empty means\nunbounded.",
|
||||
"O11yTopLevelOpsIn.service": "Service narrows the map to one service when set.",
|
||||
"O11yTopLevelOpsIn.start": "Start is the window's start, epoch nanoseconds as a string; empty means\nunbounded.",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/service/top_operations", zip.Doc{
|
||||
Description: "Returns one service's heaviest operations in the window, each\nwith p50/p95/p99 latency, how often it ran and how often it errored.",
|
||||
Fields: map[string]string{
|
||||
"O11yOperation.errorCount": "ErrorCount is how many of those runs errored.",
|
||||
"O11yOperation.name": "Name is the operation (span name).",
|
||||
"O11yOperation.numCalls": "NumCalls is how many times it ran in the window.",
|
||||
"O11yOperation.p50": "P50 is its median latency, nanoseconds.",
|
||||
"O11yOperation.p95": "P95 is its p95 latency, nanoseconds.",
|
||||
"O11yOperation.p99": "P99 is its p99 latency, nanoseconds.",
|
||||
"O11yOperationsIn.end": "End is the window's end, epoch nanoseconds as a string.",
|
||||
"O11yOperationsIn.limit": "Limit caps how many operations come back.",
|
||||
"O11yOperationsIn.service": "Service is the service whose operations are read.",
|
||||
"O11yOperationsIn.start": "Start is the window's start, epoch nanoseconds as a string.",
|
||||
"O11yOperationsIn.tags": "Tags narrow the spans counted, each a span-attribute predicate.",
|
||||
"O11yOperationsOut.data": "Data holds one entry per operation.",
|
||||
"O11yOperationsOut.status": "Status is \"success\".",
|
||||
"O11yServiceTag.BoolValues": "BoolValues are the boolean operands, when the attribute is a bool.",
|
||||
"O11yServiceTag.Key": "Key is the span attribute to test.",
|
||||
"O11yServiceTag.NumberValues": "NumberValues are the numeric operands, when the attribute is a number.",
|
||||
"O11yServiceTag.Operator": "Operator is how to test it, e.g. in, not_in.",
|
||||
"O11yServiceTag.StringValues": "StringValues are the string operands, when the attribute is a string.",
|
||||
"O11yServiceTag.TagType": "TagType says which plane the attribute lives on, e.g. tag or resource.",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/services", zip.Doc{
|
||||
Description: "Lists the instrumented services seen in the window, each with the\nrequest profile of its entry-point spans: p99 and average latency, call and\nerror rates, and the entry-point operations the numbers were computed over.",
|
||||
Fields: map[string]string{
|
||||
"O11yService.avgDuration": "AvgDuration is their average latency, nanoseconds.",
|
||||
"O11yService.callRate": "CallRate is calls per second over the window.",
|
||||
"O11yService.dataWarning": "DataWarning carries the entry-point operations the numbers were computed\nover.",
|
||||
"O11yService.errorRate": "ErrorRate is the percentage of calls that errored.",
|
||||
"O11yService.fourXXRate": "FourXXRate is the percentage of calls that answered 4xx.",
|
||||
"O11yService.num4XX": "Num4XX is how many of the calls answered 4xx.",
|
||||
"O11yService.numCalls": "NumCalls is how many entry-point spans landed in the window.",
|
||||
"O11yService.numErrors": "NumErrors is how many of the calls errored.",
|
||||
"O11yService.p99": "Percentile99 is the p99 latency of its entry-point spans, nanoseconds.",
|
||||
"O11yService.serviceName": "ServiceName is the service.",
|
||||
"O11yServiceTag.BoolValues": "BoolValues are the boolean operands, when the attribute is a bool.",
|
||||
"O11yServiceTag.Key": "Key is the span attribute to test.",
|
||||
"O11yServiceTag.NumberValues": "NumberValues are the numeric operands, when the attribute is a number.",
|
||||
"O11yServiceTag.Operator": "Operator is how to test it, e.g. in, not_in.",
|
||||
"O11yServiceTag.StringValues": "StringValues are the string operands, when the attribute is a string.",
|
||||
"O11yServiceTag.TagType": "TagType says which plane the attribute lives on, e.g. tag or resource.",
|
||||
"O11yServiceWarning.topLevelOps": "TopLevelOps are the entry-point operations the profile was computed over.",
|
||||
"O11yServicesIn.end": "End is the window's end, epoch nanoseconds as a string.",
|
||||
"O11yServicesIn.start": "Start is the window's start, epoch nanoseconds as a string.",
|
||||
"O11yServicesIn.tags": "Tags narrow the spans counted, each a span-attribute predicate.",
|
||||
"O11yServicesOut.data": "Data holds one entry per service.",
|
||||
"O11yServicesOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/third-party-apis/overview/domain", zip.Doc{
|
||||
Description: "Returns one external domain's endpoint-level breakdown — each\nendpoint with its rate, error and latency columns over the window.",
|
||||
Fields: map[string]string{
|
||||
"O11yDomainFilter.expression": "Expression is the predicate, e.g. `http.status_code >= 500`.",
|
||||
"O11yDomainGroupBy.description": "Description describes the field, when known.",
|
||||
"O11yDomainGroupBy.fieldContext": "FieldContext says which plane the field lives on, e.g. attribute,\nresource, span.",
|
||||
"O11yDomainGroupBy.fieldDataType": "FieldDataType is the field's type: string, int64, float64 or bool.",
|
||||
"O11yDomainGroupBy.name": "Name is the field's name. Required.",
|
||||
"O11yDomainGroupBy.signal": "Signal is the telemetry signal the field belongs to, e.g. traces.",
|
||||
"O11yDomainGroupBy.unit": "Unit is the field's unit, when known.",
|
||||
"O11yDomainsAnswer.data": "Data holds the per-query results.",
|
||||
"O11yDomainsAnswer.meta": "Meta reports what the read cost.",
|
||||
"O11yDomainsAnswer.type": "Type names the result shape: scalar, time_series or raw.",
|
||||
"O11yDomainsAnswer.warning": "Warning carries the store's warning for this read, when it raised one.",
|
||||
"O11yDomainsData.results": "Results is one entry per query. Each entry's shape follows the answer's\ntype — time-series data, scalar data or raw rows — so the bytes pass\nthrough verbatim.",
|
||||
"O11yDomainsIn.domain": "Domain narrows the read to one external domain (the domain view requires\nit).",
|
||||
"O11yDomainsIn.end": "End is the window's end, epoch milliseconds.",
|
||||
"O11yDomainsIn.endpoint": "Endpoint narrows the domain view to one endpoint.",
|
||||
"O11yDomainsIn.filter": "Filter is an additional predicate in the query-builder filter syntax.",
|
||||
"O11yDomainsIn.groupBy": "GroupBy adds grouping columns to the result.",
|
||||
"O11yDomainsIn.show_ip": "ShowIP keeps rows whose domain is a bare IP address; they are dropped\notherwise.",
|
||||
"O11yDomainsIn.start": "Start is the window's start, epoch milliseconds.",
|
||||
"O11yDomainsOut.data": "Data holds the result.",
|
||||
"O11yDomainsOut.status": "Status is \"success\".",
|
||||
"O11yQueryStats.bytesScanned": "BytesScanned is how many bytes the store read.",
|
||||
"O11yQueryStats.durationMs": "DurationMS is how long the read took, milliseconds.",
|
||||
"O11yQueryStats.rowsScanned": "RowsScanned is how many rows the store read.",
|
||||
"O11yQueryStats.stepIntervals": "StepIntervals is the step used per query, seconds.",
|
||||
"O11yQueryWarning.message": "Message is the warning.",
|
||||
"O11yQueryWarning.url": "URL links the relevant documentation, when there is some.",
|
||||
"O11yQueryWarning.warnings": "Warnings itemizes the finding.",
|
||||
"O11yQueryWarningNote.message": "Message is the item.",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/third-party-apis/overview/list", zip.Doc{
|
||||
Description: "Lists the external domains the instrumented services call, with\nrequest rate, error percentage and latency per domain. Rows whose domain is\na bare IP address are dropped unless show_ip asks for them.",
|
||||
Fields: map[string]string{
|
||||
"O11yDomainFilter.expression": "Expression is the predicate, e.g. `http.status_code >= 500`.",
|
||||
"O11yDomainGroupBy.description": "Description describes the field, when known.",
|
||||
"O11yDomainGroupBy.fieldContext": "FieldContext says which plane the field lives on, e.g. attribute,\nresource, span.",
|
||||
"O11yDomainGroupBy.fieldDataType": "FieldDataType is the field's type: string, int64, float64 or bool.",
|
||||
"O11yDomainGroupBy.name": "Name is the field's name. Required.",
|
||||
"O11yDomainGroupBy.signal": "Signal is the telemetry signal the field belongs to, e.g. traces.",
|
||||
"O11yDomainGroupBy.unit": "Unit is the field's unit, when known.",
|
||||
"O11yDomainsAnswer.data": "Data holds the per-query results.",
|
||||
"O11yDomainsAnswer.meta": "Meta reports what the read cost.",
|
||||
"O11yDomainsAnswer.type": "Type names the result shape: scalar, time_series or raw.",
|
||||
"O11yDomainsAnswer.warning": "Warning carries the store's warning for this read, when it raised one.",
|
||||
"O11yDomainsData.results": "Results is one entry per query. Each entry's shape follows the answer's\ntype — time-series data, scalar data or raw rows — so the bytes pass\nthrough verbatim.",
|
||||
"O11yDomainsIn.domain": "Domain narrows the read to one external domain (the domain view requires\nit).",
|
||||
"O11yDomainsIn.end": "End is the window's end, epoch milliseconds.",
|
||||
"O11yDomainsIn.endpoint": "Endpoint narrows the domain view to one endpoint.",
|
||||
"O11yDomainsIn.filter": "Filter is an additional predicate in the query-builder filter syntax.",
|
||||
"O11yDomainsIn.groupBy": "GroupBy adds grouping columns to the result.",
|
||||
"O11yDomainsIn.show_ip": "ShowIP keeps rows whose domain is a bare IP address; they are dropped\notherwise.",
|
||||
"O11yDomainsIn.start": "Start is the window's start, epoch milliseconds.",
|
||||
"O11yDomainsOut.data": "Data holds the result.",
|
||||
"O11yDomainsOut.status": "Status is \"success\".",
|
||||
"O11yQueryStats.bytesScanned": "BytesScanned is how many bytes the store read.",
|
||||
"O11yQueryStats.durationMs": "DurationMS is how long the read took, milliseconds.",
|
||||
"O11yQueryStats.rowsScanned": "RowsScanned is how many rows the store read.",
|
||||
"O11yQueryStats.stepIntervals": "StepIntervals is the step used per query, seconds.",
|
||||
"O11yQueryWarning.message": "Message is the warning.",
|
||||
"O11yQueryWarning.url": "URL links the relevant documentation, when there is some.",
|
||||
"O11yQueryWarning.warnings": "Warnings itemizes the finding.",
|
||||
"O11yQueryWarningNote.message": "Message is the item.",
|
||||
},
|
||||
})
|
||||
}
|
||||
+23
-25
@@ -13,7 +13,7 @@ package o11y
|
||||
//
|
||||
// THE WIRE DOES NOT MOVE, and the way it does not move is that these ops do not
|
||||
// re-implement the reads and writes. Each hands the call to the SAME runtime
|
||||
// handler the wildcard delegates to (relayO11y — the seam identity.go relays
|
||||
// handler the wildcard delegates to (relay.go — the one seam every face relays
|
||||
// on, the o11y face root), so identity resolution, the org gate, the exact role
|
||||
// check each route has always run (ViewAccess on the reads and pins, EditAccess
|
||||
// on the dashboard and view writes, AdminAccess on the public-sharing config,
|
||||
@@ -103,7 +103,7 @@ func mountDashboards(app *zip.App) {
|
||||
// Callers need the viewer role; the runtime's own gate enforces it.
|
||||
func dashboardListV2(ctx context.Context, in *O11yDashboardListParams) (*O11yDashboardListOut, error) {
|
||||
out := new(O11yDashboardListOut)
|
||||
return out, relayO11y(ctx, http.MethodGet, "/dashboards", listParams(in), nil, out)
|
||||
return out, relay(ctx, http.MethodGet, o11yRoot+"/dashboards", listParams(in), nil, out)
|
||||
}
|
||||
|
||||
// dashboardListForUserV2 is dashboardListV2 personalized for the calling user:
|
||||
@@ -114,7 +114,7 @@ func dashboardListV2(ctx context.Context, in *O11yDashboardListParams) (*O11yDas
|
||||
// Callers need the viewer role; the runtime's own gate enforces it.
|
||||
func dashboardListForUserV2(ctx context.Context, in *O11yDashboardListParams) (*O11yDashboardListForUserOut, error) {
|
||||
out := new(O11yDashboardListForUserOut)
|
||||
return out, relayO11y(ctx, http.MethodGet, "/users/me/dashboards", listParams(in), nil, out)
|
||||
return out, relay(ctx, http.MethodGet, o11yRoot+"/users/me/dashboards", listParams(in), nil, out)
|
||||
}
|
||||
|
||||
// dashboardCreateV2 creates a dashboard in the v2 format that follows the Perses
|
||||
@@ -123,7 +123,7 @@ func dashboardListForUserV2(ctx context.Context, in *O11yDashboardListParams) (*
|
||||
// Callers need the editor role; the runtime's own gate enforces it.
|
||||
func dashboardCreateV2(ctx context.Context, in *O11yDashboardPostable) (*O11yDashboardOut, error) {
|
||||
out := new(O11yDashboardOut)
|
||||
return out, relayO11y(ctx, http.MethodPost, "/dashboards", nil, in, out)
|
||||
return out, relay(ctx, http.MethodPost, o11yRoot+"/dashboards", nil, in, out)
|
||||
}
|
||||
|
||||
// dashboardCloneV2 clones an existing v2-shape dashboard. User and integration
|
||||
@@ -135,7 +135,7 @@ func dashboardCreateV2(ctx context.Context, in *O11yDashboardPostable) (*O11yDas
|
||||
// Callers need the editor role; the runtime's own gate enforces it.
|
||||
func dashboardCloneV2(ctx context.Context, in *O11yDashboardIDIn) (*O11yDashboardOut, error) {
|
||||
out := new(O11yDashboardOut)
|
||||
return out, relayO11y(ctx, http.MethodPost, "/dashboards/"+in.ID+"/clone", nil, nil, out)
|
||||
return out, relay(ctx, http.MethodPost, o11yRoot+"/dashboards/"+in.ID+"/clone", nil, nil, out)
|
||||
}
|
||||
|
||||
// dashboardGetV2 returns a v2-shape dashboard.
|
||||
@@ -143,7 +143,7 @@ func dashboardCloneV2(ctx context.Context, in *O11yDashboardIDIn) (*O11yDashboar
|
||||
// Callers need the viewer role; the runtime's own gate enforces it.
|
||||
func dashboardGetV2(ctx context.Context, in *O11yDashboardIDIn) (*O11yDashboardOut, error) {
|
||||
out := new(O11yDashboardOut)
|
||||
return out, relayO11y(ctx, http.MethodGet, "/dashboards/"+in.ID, nil, nil, out)
|
||||
return out, relay(ctx, http.MethodGet, o11yRoot+"/dashboards/"+in.ID, nil, nil, out)
|
||||
}
|
||||
|
||||
// dashboardUpdateV2 updates a v2-shape dashboard's metadata, spec and tag set.
|
||||
@@ -152,7 +152,7 @@ func dashboardGetV2(ctx context.Context, in *O11yDashboardIDIn) (*O11yDashboardO
|
||||
// Callers need the editor role; the runtime's own gate enforces it.
|
||||
func dashboardUpdateV2(ctx context.Context, in *O11yDashboardUpdateIn) (*O11yDashboardOut, error) {
|
||||
out := new(O11yDashboardOut)
|
||||
return out, relayO11y(ctx, http.MethodPut, "/dashboards/"+in.ID, nil, in.O11yDashboardUpdatable, out)
|
||||
return out, relay(ctx, http.MethodPut, o11yRoot+"/dashboards/"+in.ID, nil, in.O11yDashboardUpdatable, out)
|
||||
}
|
||||
|
||||
// dashboardPatchV2 applies an RFC 6902 JSON Patch to a v2-shape dashboard. The
|
||||
@@ -167,7 +167,7 @@ func dashboardPatchV2(ctx context.Context, in *O11yDashboardPatchIn) (*O11yDashb
|
||||
out := new(O11yDashboardOut)
|
||||
// The body goes on as the bare ops array the runtime's patch decoder reads;
|
||||
// the id rode in on the path, not the body.
|
||||
return out, relayO11y(ctx, http.MethodPatch, "/dashboards/"+in.ID, nil, in.Ops, out)
|
||||
return out, relay(ctx, http.MethodPatch, o11yRoot+"/dashboards/"+in.ID, nil, in.Ops, out)
|
||||
}
|
||||
|
||||
// dashboardDeleteV2 deletes a v2-shape dashboard along with its tag relations.
|
||||
@@ -175,7 +175,7 @@ func dashboardPatchV2(ctx context.Context, in *O11yDashboardPatchIn) (*O11yDashb
|
||||
//
|
||||
// Callers need the editor role; the runtime's own gate enforces it.
|
||||
func dashboardDeleteV2(ctx context.Context, in *O11yDashboardIDIn) (*struct{}, error) {
|
||||
return nil, relayO11y(ctx, http.MethodDelete, "/dashboards/"+in.ID, nil, nil, nil)
|
||||
return nil, relay(ctx, http.MethodDelete, o11yRoot+"/dashboards/"+in.ID, nil, nil, nil)
|
||||
}
|
||||
|
||||
// dashboardLockV2 locks a v2-shape dashboard. Only the dashboard's creator or an
|
||||
@@ -183,7 +183,7 @@ func dashboardDeleteV2(ctx context.Context, in *O11yDashboardIDIn) (*struct{}, e
|
||||
//
|
||||
// Callers need the editor role; the runtime's own gate enforces it.
|
||||
func dashboardLockV2(ctx context.Context, in *O11yDashboardIDIn) (*struct{}, error) {
|
||||
return nil, relayO11y(ctx, http.MethodPut, "/dashboards/"+in.ID+"/lock", nil, nil, nil)
|
||||
return nil, relay(ctx, http.MethodPut, o11yRoot+"/dashboards/"+in.ID+"/lock", nil, nil, nil)
|
||||
}
|
||||
|
||||
// dashboardUnlockV2 unlocks a v2-shape dashboard. Only the dashboard's creator or
|
||||
@@ -191,7 +191,7 @@ func dashboardLockV2(ctx context.Context, in *O11yDashboardIDIn) (*struct{}, err
|
||||
//
|
||||
// Callers need the editor role; the runtime's own gate enforces it.
|
||||
func dashboardUnlockV2(ctx context.Context, in *O11yDashboardIDIn) (*struct{}, error) {
|
||||
return nil, relayO11y(ctx, http.MethodDelete, "/dashboards/"+in.ID+"/lock", nil, nil, nil)
|
||||
return nil, relay(ctx, http.MethodDelete, o11yRoot+"/dashboards/"+in.ID+"/lock", nil, nil, nil)
|
||||
}
|
||||
|
||||
// dashboardPinV2 pins a dashboard for the calling user. A user can pin at most ten
|
||||
@@ -201,7 +201,7 @@ func dashboardUnlockV2(ctx context.Context, in *O11yDashboardIDIn) (*struct{}, e
|
||||
//
|
||||
// Callers need the viewer role; the runtime's own gate enforces it.
|
||||
func dashboardPinV2(ctx context.Context, in *O11yDashboardIDIn) (*struct{}, error) {
|
||||
return nil, relayO11y(ctx, http.MethodPut, "/users/me/dashboards/"+in.ID+"/pins", nil, nil, nil)
|
||||
return nil, relay(ctx, http.MethodPut, o11yRoot+"/users/me/dashboards/"+in.ID+"/pins", nil, nil, nil)
|
||||
}
|
||||
|
||||
// dashboardUnpinV2 removes the caller's pin for a dashboard. Idempotent —
|
||||
@@ -209,7 +209,7 @@ func dashboardPinV2(ctx context.Context, in *O11yDashboardIDIn) (*struct{}, erro
|
||||
//
|
||||
// Callers need the viewer role; the runtime's own gate enforces it.
|
||||
func dashboardUnpinV2(ctx context.Context, in *O11yDashboardIDIn) (*struct{}, error) {
|
||||
return nil, relayO11y(ctx, http.MethodDelete, "/users/me/dashboards/"+in.ID+"/pins", nil, nil, nil)
|
||||
return nil, relay(ctx, http.MethodDelete, o11yRoot+"/users/me/dashboards/"+in.ID+"/pins", nil, nil, nil)
|
||||
}
|
||||
|
||||
// dashboardListViews returns every saved view in the calling user's org. Saved
|
||||
@@ -218,7 +218,7 @@ func dashboardUnpinV2(ctx context.Context, in *O11yDashboardIDIn) (*struct{}, er
|
||||
// Callers need the viewer role; the runtime's own gate enforces it.
|
||||
func dashboardListViews(ctx context.Context, _ *struct{}) (*O11yDashboardViewListOut, error) {
|
||||
out := new(O11yDashboardViewListOut)
|
||||
return out, relayO11y(ctx, http.MethodGet, "/dashboard_views", nil, nil, out)
|
||||
return out, relay(ctx, http.MethodGet, o11yRoot+"/dashboard_views", nil, nil, out)
|
||||
}
|
||||
|
||||
// dashboardCreateView persists the calling user's dashboard-listing state (query,
|
||||
@@ -227,7 +227,7 @@ func dashboardListViews(ctx context.Context, _ *struct{}) (*O11yDashboardViewLis
|
||||
// Callers need the editor role; the runtime's own gate enforces it.
|
||||
func dashboardCreateView(ctx context.Context, in *O11yDashboardViewPostable) (*O11yDashboardViewOut, error) {
|
||||
out := new(O11yDashboardViewOut)
|
||||
return out, relayO11y(ctx, http.MethodPost, "/dashboard_views", nil, in, out)
|
||||
return out, relay(ctx, http.MethodPost, o11yRoot+"/dashboard_views", nil, in, out)
|
||||
}
|
||||
|
||||
// dashboardUpdateView replaces a saved view's name and data. Saved views are shared
|
||||
@@ -236,7 +236,7 @@ func dashboardCreateView(ctx context.Context, in *O11yDashboardViewPostable) (*O
|
||||
// Callers need the editor role; the runtime's own gate enforces it.
|
||||
func dashboardUpdateView(ctx context.Context, in *O11yDashboardViewUpdateIn) (*O11yDashboardViewOut, error) {
|
||||
out := new(O11yDashboardViewOut)
|
||||
return out, relayO11y(ctx, http.MethodPut, "/dashboard_views/"+in.ID, nil, in.O11yDashboardViewPostable, out)
|
||||
return out, relay(ctx, http.MethodPut, o11yRoot+"/dashboard_views/"+in.ID, nil, in.O11yDashboardViewPostable, out)
|
||||
}
|
||||
|
||||
// dashboardDeleteView removes a saved view. Saved views are shared org-wide.
|
||||
@@ -244,7 +244,7 @@ func dashboardUpdateView(ctx context.Context, in *O11yDashboardViewUpdateIn) (*O
|
||||
//
|
||||
// Callers need the editor role; the runtime's own gate enforces it.
|
||||
func dashboardDeleteView(ctx context.Context, in *O11yDashboardIDIn) (*struct{}, error) {
|
||||
return nil, relayO11y(ctx, http.MethodDelete, "/dashboard_views/"+in.ID, nil, nil, nil)
|
||||
return nil, relay(ctx, http.MethodDelete, o11yRoot+"/dashboard_views/"+in.ID, nil, nil, nil)
|
||||
}
|
||||
|
||||
// dashboardCreatePublic creates the public-sharing config for a dashboard and
|
||||
@@ -253,7 +253,7 @@ func dashboardDeleteView(ctx context.Context, in *O11yDashboardIDIn) (*struct{},
|
||||
// Callers need the admin role; the runtime's own gate enforces it.
|
||||
func dashboardCreatePublic(ctx context.Context, in *O11yPublicDashboardWriteIn) (*O11yIdentifiableOut, error) {
|
||||
out := new(O11yIdentifiableOut)
|
||||
return out, relayO11y(ctx, http.MethodPost, "/dashboards/"+in.ID+"/public", nil, in.O11yPublicDashboardWrite, out)
|
||||
return out, relay(ctx, http.MethodPost, o11yRoot+"/dashboards/"+in.ID+"/public", nil, in.O11yPublicDashboardWrite, out)
|
||||
}
|
||||
|
||||
// dashboardGetPublic returns the public-sharing config for a dashboard.
|
||||
@@ -261,14 +261,14 @@ func dashboardCreatePublic(ctx context.Context, in *O11yPublicDashboardWriteIn)
|
||||
// Callers need the admin role; the runtime's own gate enforces it.
|
||||
func dashboardGetPublic(ctx context.Context, in *O11yDashboardIDIn) (*O11yPublicDashboardOut, error) {
|
||||
out := new(O11yPublicDashboardOut)
|
||||
return out, relayO11y(ctx, http.MethodGet, "/dashboards/"+in.ID+"/public", nil, nil, out)
|
||||
return out, relay(ctx, http.MethodGet, o11yRoot+"/dashboards/"+in.ID+"/public", nil, nil, out)
|
||||
}
|
||||
|
||||
// dashboardUpdatePublic updates the public-sharing config for a dashboard.
|
||||
//
|
||||
// Callers need the admin role; the runtime's own gate enforces it.
|
||||
func dashboardUpdatePublic(ctx context.Context, in *O11yPublicDashboardWriteIn) (*struct{}, error) {
|
||||
return nil, relayO11y(ctx, http.MethodPut, "/dashboards/"+in.ID+"/public", nil, in.O11yPublicDashboardWrite, nil)
|
||||
return nil, relay(ctx, http.MethodPut, o11yRoot+"/dashboards/"+in.ID+"/public", nil, in.O11yPublicDashboardWrite, nil)
|
||||
}
|
||||
|
||||
// dashboardDeletePublic deletes the public-sharing config and disables public
|
||||
@@ -276,7 +276,7 @@ func dashboardUpdatePublic(ctx context.Context, in *O11yPublicDashboardWriteIn)
|
||||
//
|
||||
// Callers need the admin role; the runtime's own gate enforces it.
|
||||
func dashboardDeletePublic(ctx context.Context, in *O11yDashboardIDIn) (*struct{}, error) {
|
||||
return nil, relayO11y(ctx, http.MethodDelete, "/dashboards/"+in.ID+"/public", nil, nil, nil)
|
||||
return nil, relay(ctx, http.MethodDelete, o11yRoot+"/dashboards/"+in.ID+"/public", nil, nil, nil)
|
||||
}
|
||||
|
||||
// dashboardGetPublicData returns the sanitized dashboard data for public access —
|
||||
@@ -286,7 +286,7 @@ func dashboardDeletePublic(ctx context.Context, in *O11yDashboardIDIn) (*struct{
|
||||
// enforces it.
|
||||
func dashboardGetPublicData(ctx context.Context, in *O11yDashboardIDIn) (*O11yPublicDashboardDataOut, error) {
|
||||
out := new(O11yPublicDashboardDataOut)
|
||||
return out, relayO11y(ctx, http.MethodGet, "/public/dashboards/"+in.ID, nil, nil, out)
|
||||
return out, relay(ctx, http.MethodGet, o11yRoot+"/public/dashboards/"+in.ID, nil, nil, out)
|
||||
}
|
||||
|
||||
// dashboardGetPublicWidgetQueryRange returns the query-range result for one widget
|
||||
@@ -298,9 +298,7 @@ func dashboardGetPublicData(ctx context.Context, in *O11yDashboardIDIn) (*O11yPu
|
||||
// enforces it.
|
||||
func dashboardGetPublicWidgetQueryRange(ctx context.Context, in *O11yWidgetQueryRangeIn) (*O11yWidgetQueryRangeOut, error) {
|
||||
out := new(O11yWidgetQueryRangeOut)
|
||||
return out, relayO11y(ctx, http.MethodGet,
|
||||
"/public/dashboards/"+in.ID+"/widgets/"+in.Idx+"/query_range",
|
||||
query("startTime", in.StartTime, "endTime", in.EndTime), nil, out)
|
||||
return out, relay(ctx, http.MethodGet, o11yRoot+"/public/dashboards/"+in.ID+"/widgets/"+in.Idx+"/query_range", query("startTime", in.StartTime, "endTime", in.EndTime), nil, out)
|
||||
}
|
||||
|
||||
// ── the list query ──────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -1,382 +0,0 @@
|
||||
// Code generated by zipdoc; DO NOT EDIT.
|
||||
//
|
||||
// The dashboards face's slice of the package extraction, split into its own file
|
||||
// so concurrent slices do not contend on zipdoc_gen.go; a later `go generate
|
||||
// -run zipdoc ./` folds every slice back into zipdoc_gen.go, at which point
|
||||
// this file is deleted.
|
||||
|
||||
package o11y
|
||||
|
||||
import (
|
||||
"github.com/zap-proto/zip"
|
||||
)
|
||||
|
||||
func init() {
|
||||
zip.Describe("DELETE /v1/o11y/dashboard_views/:id", zip.Doc{
|
||||
Description: "Removes a saved view. Saved views are shared org-wide.\nDeleting a non-existent view refuses with the runtime's not-found.\n\nCallers need the editor role; the runtime's own gate enforces it.",
|
||||
Fields: map[string]string{
|
||||
"O11yDashboardIDIn.id": "ID is the resource id from the path.",
|
||||
},
|
||||
})
|
||||
zip.Describe("DELETE /v1/o11y/dashboards/:id", zip.Doc{
|
||||
Description: "Deletes a v2-shape dashboard along with its tag relations.\nLocked dashboards are rejected.\n\nCallers need the editor role; the runtime's own gate enforces it.",
|
||||
Fields: map[string]string{
|
||||
"O11yDashboardIDIn.id": "ID is the resource id from the path.",
|
||||
},
|
||||
})
|
||||
zip.Describe("DELETE /v1/o11y/dashboards/:id/lock", zip.Doc{
|
||||
Description: "Unlocks a v2-shape dashboard. Only the dashboard's creator or\nan org admin may lock or unlock.\n\nCallers need the editor role; the runtime's own gate enforces it.",
|
||||
Fields: map[string]string{
|
||||
"O11yDashboardIDIn.id": "ID is the resource id from the path.",
|
||||
},
|
||||
})
|
||||
zip.Describe("DELETE /v1/o11y/dashboards/:id/public", zip.Doc{
|
||||
Description: "Deletes the public-sharing config and disables public\nsharing of a dashboard.\n\nCallers need the admin role; the runtime's own gate enforces it.",
|
||||
Fields: map[string]string{
|
||||
"O11yDashboardIDIn.id": "ID is the resource id from the path.",
|
||||
},
|
||||
})
|
||||
zip.Describe("DELETE /v1/o11y/users/me/dashboards/:id/pins", zip.Doc{
|
||||
Description: "Removes the caller's pin for a dashboard. Idempotent —\nunpinning a dashboard that was not pinned still succeeds.\n\nCallers need the viewer role; the runtime's own gate enforces it.",
|
||||
Fields: map[string]string{
|
||||
"O11yDashboardIDIn.id": "ID is the resource id from the path.",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/dashboard_views", zip.Doc{
|
||||
Description: "Returns every saved view in the calling user's org. Saved\nviews are shared org-wide.\n\nCallers need the viewer role; the runtime's own gate enforces it.",
|
||||
Fields: map[string]string{
|
||||
"O11yDashboardView.createdAt": "CreatedAt is when the view was created.",
|
||||
"O11yDashboardView.data": "Data is the listing state the view captures.",
|
||||
"O11yDashboardView.id": "ID is the saved view's id.",
|
||||
"O11yDashboardView.name": "Name is the saved view's name.",
|
||||
"O11yDashboardView.orgId": "OrgID is the org the view belongs to.",
|
||||
"O11yDashboardView.updatedAt": "UpdatedAt is when it last changed.",
|
||||
"O11yDashboardViewData.order": "Order is the captured sort direction.",
|
||||
"O11yDashboardViewData.query": "Query is the captured filter DSL.",
|
||||
"O11yDashboardViewData.sort": "Sort is the captured sort field.",
|
||||
"O11yDashboardViewData.version": "Version is the saved-view schema version; must be v1.",
|
||||
"O11yDashboardViewList.views": "Views are the saved views, shared org-wide.",
|
||||
"O11yDashboardViewListOut.data": "Data is the saved views.",
|
||||
"O11yDashboardViewListOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/dashboards", zip.Doc{
|
||||
Description: "Returns a page of v2-shape dashboards for the org. This is the\npure, user-independent list — it carries no pin state; use\ndashboardListForUserV2 for the personalized, pin-aware list. Supports a filter\nDSL (query), sort (updated_at/created_at/name), order (asc/desc), and\noffset-based pagination (limit/offset).\n\nCallers need the viewer role; the runtime's own gate enforces it.",
|
||||
Fields: map[string]string{
|
||||
"O11yDashboardDisplay.description": "Description says what the dashboard is for.",
|
||||
"O11yDashboardDisplay.name": "Name is the human-facing dashboard name.",
|
||||
"O11yDashboardList.dashboards": "Dashboards are the rows for this page.",
|
||||
"O11yDashboardList.tags": "Tags are all tags in use across the org's dashboards.",
|
||||
"O11yDashboardList.total": "Total is the count across all pages.",
|
||||
"O11yDashboardListItem.createdAt": "CreatedAt is when the dashboard was created.",
|
||||
"O11yDashboardListItem.createdBy": "CreatedBy is who created it.",
|
||||
"O11yDashboardListItem.id": "ID is the dashboard's id.",
|
||||
"O11yDashboardListItem.image": "Image is an optional cover image reference.",
|
||||
"O11yDashboardListItem.locked": "Locked reports whether the dashboard is locked against edits.",
|
||||
"O11yDashboardListItem.name": "Name is the dashboard's unique internal name.",
|
||||
"O11yDashboardListItem.orgId": "OrgID is the org the dashboard belongs to.",
|
||||
"O11yDashboardListItem.schemaVersion": "SchemaVersion is the dashboard schema version.",
|
||||
"O11yDashboardListItem.source": "Source is where the dashboard came from: user, system or integration.",
|
||||
"O11yDashboardListItem.spec": "Spec is the display-only slice of the dashboard spec.",
|
||||
"O11yDashboardListItem.tags": "Tags are the dashboard's tags.",
|
||||
"O11yDashboardListItem.updatedAt": "UpdatedAt is when it last changed.",
|
||||
"O11yDashboardListItem.updatedBy": "UpdatedBy is who last changed it.",
|
||||
"O11yDashboardListOut.data": "Data is the list page.",
|
||||
"O11yDashboardListOut.status": "Status is \"success\".",
|
||||
"O11yDashboardListParams.limit": "Limit caps how many dashboards come back. Zero means the default of 20;\nthe runtime caps it at 200.",
|
||||
"O11yDashboardListParams.offset": "Offset is how many dashboards to skip for pagination.",
|
||||
"O11yDashboardListParams.order": "Order is the sort direction: asc or desc. Empty orders desc.",
|
||||
"O11yDashboardListParams.query": "Query is the filter DSL over dashboard columns and tags, e.g.\n`name:cpu source:user`. Empty lists everything.",
|
||||
"O11yDashboardListParams.sort": "Sort is the sort field: updated_at, created_at or name. Empty sorts by\nupdated_at.",
|
||||
"O11yDashboardListSpec.display": "Display is the dashboard's display metadata.",
|
||||
"O11yDashboardTag.key": "Key is the tag key.",
|
||||
"O11yDashboardTag.value": "Value is the tag value.",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/dashboards/:id", zip.Doc{
|
||||
Description: "Returns a v2-shape dashboard.\n\nCallers need the viewer role; the runtime's own gate enforces it.",
|
||||
Fields: map[string]string{
|
||||
"O11yDashboard.createdAt": "CreatedAt is when the dashboard was created.",
|
||||
"O11yDashboard.createdBy": "CreatedBy is who created it.",
|
||||
"O11yDashboard.id": "ID is the dashboard's id.",
|
||||
"O11yDashboard.image": "Image is an optional cover image reference.",
|
||||
"O11yDashboard.locked": "Locked reports whether the dashboard is locked against edits.",
|
||||
"O11yDashboard.name": "Name is the dashboard's unique internal name.",
|
||||
"O11yDashboard.orgId": "OrgID is the org the dashboard belongs to.",
|
||||
"O11yDashboard.schemaVersion": "SchemaVersion is the dashboard schema version.",
|
||||
"O11yDashboard.source": "Source is where the dashboard came from: user, system or integration.",
|
||||
"O11yDashboard.spec": "Spec is the Perses dashboard spec, carried verbatim as open JSON.",
|
||||
"O11yDashboard.tags": "Tags are the dashboard's tags.",
|
||||
"O11yDashboard.updatedAt": "UpdatedAt is when it last changed.",
|
||||
"O11yDashboard.updatedBy": "UpdatedBy is who last changed it.",
|
||||
"O11yDashboardIDIn.id": "ID is the resource id from the path.",
|
||||
"O11yDashboardOut.data": "Data is the dashboard.",
|
||||
"O11yDashboardOut.status": "Status is \"success\".",
|
||||
"O11yDashboardTag.key": "Key is the tag key.",
|
||||
"O11yDashboardTag.value": "Value is the tag value.",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/dashboards/:id/public", zip.Doc{
|
||||
Description: "Returns the public-sharing config for a dashboard.\n\nCallers need the admin role; the runtime's own gate enforces it.",
|
||||
Fields: map[string]string{
|
||||
"O11yDashboardIDIn.id": "ID is the resource id from the path.",
|
||||
"O11yPublicDashboard.defaultTimeRange": "DefaultTimeRange is the fixed window when the range is not caller-chosen.",
|
||||
"O11yPublicDashboard.publicPath": "PublicPath is the public URL path the share is reachable at.",
|
||||
"O11yPublicDashboard.timeRangeEnabled": "TimeRangeEnabled reports whether the public page may pick its own range.",
|
||||
"O11yPublicDashboardOut.data": "Data is the public-sharing config.",
|
||||
"O11yPublicDashboardOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/public/dashboards/:id", zip.Doc{
|
||||
Description: "Returns the sanitized dashboard data for public access —\nthe read a shared dashboard's public page makes.\n\nAnonymous, scoped to the public dashboard's read scope; the runtime's own gate\nenforces it.",
|
||||
Fields: map[string]string{
|
||||
"O11yDashboardIDIn.id": "ID is the resource id from the path.",
|
||||
"O11yPublicDashboard.defaultTimeRange": "DefaultTimeRange is the fixed window when the range is not caller-chosen.",
|
||||
"O11yPublicDashboard.publicPath": "PublicPath is the public URL path the share is reachable at.",
|
||||
"O11yPublicDashboard.timeRangeEnabled": "TimeRangeEnabled reports whether the public page may pick its own range.",
|
||||
"O11yPublicDashboardData.dashboard": "Dashboard is the sanitized dashboard.",
|
||||
"O11yPublicDashboardData.publicDashboard": "PublicDashboard is the public-sharing config.",
|
||||
"O11yPublicDashboardDataOut.data": "Data is the sanitized dashboard and its share config.",
|
||||
"O11yPublicDashboardDataOut.status": "Status is \"success\".",
|
||||
"O11yPublicDashboardV1.createdAt": "CreatedAt is when the dashboard was created.",
|
||||
"O11yPublicDashboardV1.createdBy": "CreatedBy is who created it.",
|
||||
"O11yPublicDashboardV1.data": "Data is the sanitized widget data as stored, carried verbatim as open JSON.",
|
||||
"O11yPublicDashboardV1.id": "ID is the dashboard's id.",
|
||||
"O11yPublicDashboardV1.locked": "Locked reports whether the dashboard is locked.",
|
||||
"O11yPublicDashboardV1.org_id": "OrgID is the org the dashboard belongs to.",
|
||||
"O11yPublicDashboardV1.source": "Source is where the dashboard came from.",
|
||||
"O11yPublicDashboardV1.updatedAt": "UpdatedAt is when it last changed.",
|
||||
"O11yPublicDashboardV1.updatedBy": "UpdatedBy is who last changed it.",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/public/dashboards/:id/widgets/:idx/query_range", zip.Doc{
|
||||
Description: "Returns the query-range result for one widget\nof a public dashboard. When the share fixes its own time range the caller's\nstartTime/endTime are ignored; otherwise they bound the window as millisecond\nepochs.\n\nAnonymous, scoped to the public dashboard's read scope; the runtime's own gate\nenforces it.",
|
||||
Fields: map[string]string{
|
||||
"O11yWidgetQueryRange.data": "Data is the query result payload, carried verbatim as open JSON.",
|
||||
"O11yWidgetQueryRange.meta": "Meta is the execution stats, carried verbatim as open JSON.",
|
||||
"O11yWidgetQueryRange.type": "Type is the request type the result answers, e.g. time_series, scalar.",
|
||||
"O11yWidgetQueryRange.warning": "Warning is any query warning, carried verbatim as open JSON when present.",
|
||||
"O11yWidgetQueryRangeIn.endTime": "EndTime is the window end as a millisecond epoch. Used only when the share\nenables a caller-chosen time range.",
|
||||
"O11yWidgetQueryRangeIn.id": "ID is the public dashboard id from the path.",
|
||||
"O11yWidgetQueryRangeIn.idx": "Idx is the widget's index from the path.",
|
||||
"O11yWidgetQueryRangeIn.startTime": "StartTime is the window start as a millisecond epoch. Used only when the\nshare enables a caller-chosen time range.",
|
||||
"O11yWidgetQueryRangeOut.data": "Data is the query-range result.",
|
||||
"O11yWidgetQueryRangeOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/users/me/dashboards", zip.Doc{
|
||||
Description: "Is dashboardListV2 personalized for the calling user:\neach dashboard carries the caller's pinned state, and pinned dashboards float to\nthe top of the requested ordering. Supports the same filter DSL, sort, order and\npagination.\n\nCallers need the viewer role; the runtime's own gate enforces it.",
|
||||
Fields: map[string]string{
|
||||
"O11yDashboardDisplay.description": "Description says what the dashboard is for.",
|
||||
"O11yDashboardDisplay.name": "Name is the human-facing dashboard name.",
|
||||
"O11yDashboardListForUser.dashboards": "Dashboards are the rows for this page, each with the caller's pin state.",
|
||||
"O11yDashboardListForUser.tags": "Tags are all tags in use across the org's dashboards.",
|
||||
"O11yDashboardListForUser.total": "Total is the count across all pages.",
|
||||
"O11yDashboardListForUserOut.data": "Data is the list page with pins.",
|
||||
"O11yDashboardListForUserOut.status": "Status is \"success\".",
|
||||
"O11yDashboardListItem.createdAt": "CreatedAt is when the dashboard was created.",
|
||||
"O11yDashboardListItem.createdBy": "CreatedBy is who created it.",
|
||||
"O11yDashboardListItem.id": "ID is the dashboard's id.",
|
||||
"O11yDashboardListItem.image": "Image is an optional cover image reference.",
|
||||
"O11yDashboardListItem.locked": "Locked reports whether the dashboard is locked against edits.",
|
||||
"O11yDashboardListItem.name": "Name is the dashboard's unique internal name.",
|
||||
"O11yDashboardListItem.orgId": "OrgID is the org the dashboard belongs to.",
|
||||
"O11yDashboardListItem.schemaVersion": "SchemaVersion is the dashboard schema version.",
|
||||
"O11yDashboardListItem.source": "Source is where the dashboard came from: user, system or integration.",
|
||||
"O11yDashboardListItem.spec": "Spec is the display-only slice of the dashboard spec.",
|
||||
"O11yDashboardListItem.tags": "Tags are the dashboard's tags.",
|
||||
"O11yDashboardListItem.updatedAt": "UpdatedAt is when it last changed.",
|
||||
"O11yDashboardListItem.updatedBy": "UpdatedBy is who last changed it.",
|
||||
"O11yDashboardListItemForUser.pinned": "Pinned reports whether the calling user has pinned this dashboard.",
|
||||
"O11yDashboardListParams.limit": "Limit caps how many dashboards come back. Zero means the default of 20;\nthe runtime caps it at 200.",
|
||||
"O11yDashboardListParams.offset": "Offset is how many dashboards to skip for pagination.",
|
||||
"O11yDashboardListParams.order": "Order is the sort direction: asc or desc. Empty orders desc.",
|
||||
"O11yDashboardListParams.query": "Query is the filter DSL over dashboard columns and tags, e.g.\n`name:cpu source:user`. Empty lists everything.",
|
||||
"O11yDashboardListParams.sort": "Sort is the sort field: updated_at, created_at or name. Empty sorts by\nupdated_at.",
|
||||
"O11yDashboardListSpec.display": "Display is the dashboard's display metadata.",
|
||||
"O11yDashboardTag.key": "Key is the tag key.",
|
||||
"O11yDashboardTag.value": "Value is the tag value.",
|
||||
},
|
||||
})
|
||||
zip.Describe("PATCH /v1/o11y/dashboards/:id", zip.Doc{
|
||||
Description: "Applies an RFC 6902 JSON Patch to a v2-shape dashboard. The\npatch is applied against the postable view (metadata, spec, tags), so individual\npanels, queries, variables, layouts or tags can be updated without re-sending the\nrest. Apply is lenient — remove on a missing path is a no-op and add creates any\nmissing parent objects — and the result is still validated. Locked dashboards are\nrejected. The request body is the bare JSON Patch operations array.\n\nCallers need the editor role; the runtime's own gate enforces it.",
|
||||
Fields: map[string]string{
|
||||
"O11yDashboard.createdAt": "CreatedAt is when the dashboard was created.",
|
||||
"O11yDashboard.createdBy": "CreatedBy is who created it.",
|
||||
"O11yDashboard.id": "ID is the dashboard's id.",
|
||||
"O11yDashboard.image": "Image is an optional cover image reference.",
|
||||
"O11yDashboard.locked": "Locked reports whether the dashboard is locked against edits.",
|
||||
"O11yDashboard.name": "Name is the dashboard's unique internal name.",
|
||||
"O11yDashboard.orgId": "OrgID is the org the dashboard belongs to.",
|
||||
"O11yDashboard.schemaVersion": "SchemaVersion is the dashboard schema version.",
|
||||
"O11yDashboard.source": "Source is where the dashboard came from: user, system or integration.",
|
||||
"O11yDashboard.spec": "Spec is the Perses dashboard spec, carried verbatim as open JSON.",
|
||||
"O11yDashboard.tags": "Tags are the dashboard's tags.",
|
||||
"O11yDashboard.updatedAt": "UpdatedAt is when it last changed.",
|
||||
"O11yDashboard.updatedBy": "UpdatedBy is who last changed it.",
|
||||
"O11yDashboardOut.data": "Data is the dashboard.",
|
||||
"O11yDashboardOut.status": "Status is \"success\".",
|
||||
"O11yDashboardPatchIn.id": "ID is the dashboard id from the path.",
|
||||
"O11yDashboardPatchIn.ops": "Ops are the JSON Patch operations, applied in order. On the wire this IS the\nrequest body — a bare array, not an object.",
|
||||
"O11yDashboardPatchOp.from": "From is the source JSON Pointer for move and copy; ignored otherwise.",
|
||||
"O11yDashboardPatchOp.op": "Op is the verb: add, remove, replace, move, copy or test.",
|
||||
"O11yDashboardPatchOp.path": "Path is a JSON Pointer into the postable dashboard, e.g.\n/spec/display/name, /spec/panels/<id>, /tags/-.",
|
||||
"O11yDashboardPatchOp.value": "Value is the value to add/replace/test; its type depends on Path. Required\nfor add, replace and test; ignored otherwise.",
|
||||
"O11yDashboardTag.key": "Key is the tag key.",
|
||||
"O11yDashboardTag.value": "Value is the tag value.",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/dashboard_views", zip.Doc{
|
||||
Description: "Persists the calling user's dashboard-listing state (query,\nsort, order) as a named, reusable view shared across the org.\n\nCallers need the editor role; the runtime's own gate enforces it.",
|
||||
Fields: map[string]string{
|
||||
"O11yDashboardView.createdAt": "CreatedAt is when the view was created.",
|
||||
"O11yDashboardView.data": "Data is the listing state the view captures.",
|
||||
"O11yDashboardView.id": "ID is the saved view's id.",
|
||||
"O11yDashboardView.name": "Name is the saved view's name.",
|
||||
"O11yDashboardView.orgId": "OrgID is the org the view belongs to.",
|
||||
"O11yDashboardView.updatedAt": "UpdatedAt is when it last changed.",
|
||||
"O11yDashboardViewData.order": "Order is the captured sort direction.",
|
||||
"O11yDashboardViewData.query": "Query is the captured filter DSL.",
|
||||
"O11yDashboardViewData.sort": "Sort is the captured sort field.",
|
||||
"O11yDashboardViewData.version": "Version is the saved-view schema version; must be v1.",
|
||||
"O11yDashboardViewOut.data": "Data is the saved view.",
|
||||
"O11yDashboardViewOut.status": "Status is \"success\".",
|
||||
"O11yDashboardViewPostable.data": "Data is the listing state the view captures.",
|
||||
"O11yDashboardViewPostable.name": "Name is the saved view's name; at most 32 characters, no surrounding space.",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/dashboards", zip.Doc{
|
||||
Description: "Creates a dashboard in the v2 format that follows the Perses\nspec and answers with the stored dashboard.\n\nCallers need the editor role; the runtime's own gate enforces it.",
|
||||
Fields: map[string]string{
|
||||
"O11yDashboard.createdAt": "CreatedAt is when the dashboard was created.",
|
||||
"O11yDashboard.createdBy": "CreatedBy is who created it.",
|
||||
"O11yDashboard.id": "ID is the dashboard's id.",
|
||||
"O11yDashboard.image": "Image is an optional cover image reference.",
|
||||
"O11yDashboard.locked": "Locked reports whether the dashboard is locked against edits.",
|
||||
"O11yDashboard.name": "Name is the dashboard's unique internal name.",
|
||||
"O11yDashboard.orgId": "OrgID is the org the dashboard belongs to.",
|
||||
"O11yDashboard.schemaVersion": "SchemaVersion is the dashboard schema version.",
|
||||
"O11yDashboard.source": "Source is where the dashboard came from: user, system or integration.",
|
||||
"O11yDashboard.spec": "Spec is the Perses dashboard spec, carried verbatim as open JSON.",
|
||||
"O11yDashboard.tags": "Tags are the dashboard's tags.",
|
||||
"O11yDashboard.updatedAt": "UpdatedAt is when it last changed.",
|
||||
"O11yDashboard.updatedBy": "UpdatedBy is who last changed it.",
|
||||
"O11yDashboardOut.data": "Data is the dashboard.",
|
||||
"O11yDashboardOut.status": "Status is \"success\".",
|
||||
"O11yDashboardPostable.generateName": "GenerateName derives a fresh unique name from spec.display.name instead of\ntaking Name.",
|
||||
"O11yDashboardPostable.image": "Image is an optional cover image reference.",
|
||||
"O11yDashboardPostable.name": "Name is the dashboard's unique internal name (a DNS-1123 label). Omit it\nwith generateName to derive one from the display name.",
|
||||
"O11yDashboardPostable.schemaVersion": "SchemaVersion is the dashboard schema version; must be the current v6.",
|
||||
"O11yDashboardPostable.spec": "Spec is the Perses dashboard spec (display, variables, panels, layouts,\ndatasources). Its plugin-defined leaves are open JSON, carried verbatim.",
|
||||
"O11yDashboardPostable.tags": "Tags are the dashboard's tags; at most ten, and none may use a reserved DSL\nkey.",
|
||||
"O11yDashboardPostableTag.key": "Key is the tag key.",
|
||||
"O11yDashboardPostableTag.value": "Value is the tag value.",
|
||||
"O11yDashboardTag.key": "Key is the tag key.",
|
||||
"O11yDashboardTag.value": "Value is the tag value.",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/dashboards/:id/clone", zip.Doc{
|
||||
Description: "Clones an existing v2-shape dashboard. User and integration\ndashboards can be cloned; system dashboards are rejected. The clone keeps the\nsource's display name, panels and tags, but gets a freshly generated unique\ninternal name and is always created as an unlocked user dashboard owned by the\ncaller.\n\nCallers need the editor role; the runtime's own gate enforces it.",
|
||||
Fields: map[string]string{
|
||||
"O11yDashboard.createdAt": "CreatedAt is when the dashboard was created.",
|
||||
"O11yDashboard.createdBy": "CreatedBy is who created it.",
|
||||
"O11yDashboard.id": "ID is the dashboard's id.",
|
||||
"O11yDashboard.image": "Image is an optional cover image reference.",
|
||||
"O11yDashboard.locked": "Locked reports whether the dashboard is locked against edits.",
|
||||
"O11yDashboard.name": "Name is the dashboard's unique internal name.",
|
||||
"O11yDashboard.orgId": "OrgID is the org the dashboard belongs to.",
|
||||
"O11yDashboard.schemaVersion": "SchemaVersion is the dashboard schema version.",
|
||||
"O11yDashboard.source": "Source is where the dashboard came from: user, system or integration.",
|
||||
"O11yDashboard.spec": "Spec is the Perses dashboard spec, carried verbatim as open JSON.",
|
||||
"O11yDashboard.tags": "Tags are the dashboard's tags.",
|
||||
"O11yDashboard.updatedAt": "UpdatedAt is when it last changed.",
|
||||
"O11yDashboard.updatedBy": "UpdatedBy is who last changed it.",
|
||||
"O11yDashboardIDIn.id": "ID is the resource id from the path.",
|
||||
"O11yDashboardOut.data": "Data is the dashboard.",
|
||||
"O11yDashboardOut.status": "Status is \"success\".",
|
||||
"O11yDashboardTag.key": "Key is the tag key.",
|
||||
"O11yDashboardTag.value": "Value is the tag value.",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/dashboards/:id/public", zip.Doc{
|
||||
Description: "Creates the public-sharing config for a dashboard and\nenables public sharing, answering with the new share's id.\n\nCallers need the admin role; the runtime's own gate enforces it.",
|
||||
Fields: map[string]string{
|
||||
"O11yIdentifiable.id": "ID is the created resource's id.",
|
||||
"O11yIdentifiableOut.data": "Data is the created resource's id.",
|
||||
"O11yIdentifiableOut.status": "Status is \"success\".",
|
||||
"O11yPublicDashboardWrite.defaultTimeRange": "DefaultTimeRange is the fixed window as a Go duration, e.g. 1h, 30m.",
|
||||
"O11yPublicDashboardWrite.timeRangeEnabled": "TimeRangeEnabled lets the public page pick its own time range; when false\nthe share fixes DefaultTimeRange.",
|
||||
"O11yPublicDashboardWriteIn.id": "ID is the dashboard id from the path.",
|
||||
},
|
||||
})
|
||||
zip.Describe("PUT /v1/o11y/dashboard_views/:id", zip.Doc{
|
||||
Description: "Replaces a saved view's name and data. Saved views are shared\norg-wide.\n\nCallers need the editor role; the runtime's own gate enforces it.",
|
||||
Fields: map[string]string{
|
||||
"O11yDashboardView.createdAt": "CreatedAt is when the view was created.",
|
||||
"O11yDashboardView.data": "Data is the listing state the view captures.",
|
||||
"O11yDashboardView.id": "ID is the saved view's id.",
|
||||
"O11yDashboardView.name": "Name is the saved view's name.",
|
||||
"O11yDashboardView.orgId": "OrgID is the org the view belongs to.",
|
||||
"O11yDashboardView.updatedAt": "UpdatedAt is when it last changed.",
|
||||
"O11yDashboardViewData.order": "Order is the captured sort direction.",
|
||||
"O11yDashboardViewData.query": "Query is the captured filter DSL.",
|
||||
"O11yDashboardViewData.sort": "Sort is the captured sort field.",
|
||||
"O11yDashboardViewData.version": "Version is the saved-view schema version; must be v1.",
|
||||
"O11yDashboardViewOut.data": "Data is the saved view.",
|
||||
"O11yDashboardViewOut.status": "Status is \"success\".",
|
||||
"O11yDashboardViewPostable.data": "Data is the listing state the view captures.",
|
||||
"O11yDashboardViewPostable.name": "Name is the saved view's name; at most 32 characters, no surrounding space.",
|
||||
"O11yDashboardViewUpdateIn.id": "ID is the saved view id from the path.",
|
||||
},
|
||||
})
|
||||
zip.Describe("PUT /v1/o11y/dashboards/:id", zip.Doc{
|
||||
Description: "Updates a v2-shape dashboard's metadata, spec and tag set.\nThe name is immutable and locked dashboards are rejected.\n\nCallers need the editor role; the runtime's own gate enforces it.",
|
||||
Fields: map[string]string{
|
||||
"O11yDashboard.createdAt": "CreatedAt is when the dashboard was created.",
|
||||
"O11yDashboard.createdBy": "CreatedBy is who created it.",
|
||||
"O11yDashboard.id": "ID is the dashboard's id.",
|
||||
"O11yDashboard.image": "Image is an optional cover image reference.",
|
||||
"O11yDashboard.locked": "Locked reports whether the dashboard is locked against edits.",
|
||||
"O11yDashboard.name": "Name is the dashboard's unique internal name.",
|
||||
"O11yDashboard.orgId": "OrgID is the org the dashboard belongs to.",
|
||||
"O11yDashboard.schemaVersion": "SchemaVersion is the dashboard schema version.",
|
||||
"O11yDashboard.source": "Source is where the dashboard came from: user, system or integration.",
|
||||
"O11yDashboard.spec": "Spec is the Perses dashboard spec, carried verbatim as open JSON.",
|
||||
"O11yDashboard.tags": "Tags are the dashboard's tags.",
|
||||
"O11yDashboard.updatedAt": "UpdatedAt is when it last changed.",
|
||||
"O11yDashboard.updatedBy": "UpdatedBy is who last changed it.",
|
||||
"O11yDashboardOut.data": "Data is the dashboard.",
|
||||
"O11yDashboardOut.status": "Status is \"success\".",
|
||||
"O11yDashboardPostableTag.key": "Key is the tag key.",
|
||||
"O11yDashboardPostableTag.value": "Value is the tag value.",
|
||||
"O11yDashboardTag.key": "Key is the tag key.",
|
||||
"O11yDashboardTag.value": "Value is the tag value.",
|
||||
"O11yDashboardUpdatable.image": "Image is an optional cover image reference.",
|
||||
"O11yDashboardUpdatable.name": "Name must equal the dashboard's existing name — it is immutable.",
|
||||
"O11yDashboardUpdatable.schemaVersion": "SchemaVersion is the dashboard schema version; must be the current v6.",
|
||||
"O11yDashboardUpdatable.spec": "Spec is the replacement Perses spec, carried verbatim as open JSON.",
|
||||
"O11yDashboardUpdatable.tags": "Tags replace the dashboard's tags; at most ten.",
|
||||
"O11yDashboardUpdateIn.id": "ID is the dashboard id from the path.",
|
||||
},
|
||||
})
|
||||
zip.Describe("PUT /v1/o11y/dashboards/:id/lock", zip.Doc{
|
||||
Description: "Locks a v2-shape dashboard. Only the dashboard's creator or an\norg admin may lock or unlock.\n\nCallers need the editor role; the runtime's own gate enforces it.",
|
||||
Fields: map[string]string{
|
||||
"O11yDashboardIDIn.id": "ID is the resource id from the path.",
|
||||
},
|
||||
})
|
||||
zip.Describe("PUT /v1/o11y/dashboards/:id/public", zip.Doc{
|
||||
Description: "Updates the public-sharing config for a dashboard.\n\nCallers need the admin role; the runtime's own gate enforces it.",
|
||||
Fields: map[string]string{
|
||||
"O11yPublicDashboardWrite.defaultTimeRange": "DefaultTimeRange is the fixed window as a Go duration, e.g. 1h, 30m.",
|
||||
"O11yPublicDashboardWrite.timeRangeEnabled": "TimeRangeEnabled lets the public page pick its own time range; when false\nthe share fixes DefaultTimeRange.",
|
||||
"O11yPublicDashboardWriteIn.id": "ID is the dashboard id from the path.",
|
||||
},
|
||||
})
|
||||
zip.Describe("PUT /v1/o11y/users/me/dashboards/:id/pins", zip.Doc{
|
||||
Description: "Pins a dashboard for the calling user. A user can pin at most ten\ndashboards; pinning at the limit refuses with the runtime's conflict. Re-pinning\nan already-pinned dashboard is a no-op success. Pinning mutates only the caller's\npin list, not the dashboard, so a viewer may pin what a viewer may read.\n\nCallers need the viewer role; the runtime's own gate enforces it.",
|
||||
Fields: map[string]string{
|
||||
"O11yDashboardIDIn.id": "ID is the resource id from the path.",
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -94,93 +94,33 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go v0.123.0 // indirect
|
||||
filippo.io/edwards25519 v1.2.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute v1.0.0 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/ProtonMail/go-crypto v1.1.6 // indirect
|
||||
github.com/ThinkInAIXYZ/go-mcp v0.2.24 // indirect
|
||||
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 // indirect
|
||||
github.com/alibabacloud-go/darabonba-openapi v0.1.18 // indirect
|
||||
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.16 // indirect
|
||||
github.com/alibabacloud-go/debug v1.0.1 // indirect
|
||||
github.com/alibabacloud-go/ecs-20140526/v4 v4.26.10 // indirect
|
||||
github.com/alibabacloud-go/endpoint-util v1.1.0 // indirect
|
||||
github.com/alibabacloud-go/openapi-util v0.1.2 // indirect
|
||||
github.com/alibabacloud-go/resourcecenter-20221201 v1.5.1 // indirect
|
||||
github.com/alibabacloud-go/tea v1.4.0 // indirect
|
||||
github.com/alibabacloud-go/tea-utils v1.4.5 // indirect
|
||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.9 // indirect
|
||||
github.com/alibabacloud-go/tea-xml v1.1.3 // indirect
|
||||
github.com/alibabacloud-go/vod-20170321/v2 v2.16.10 // indirect
|
||||
github.com/aliyun/alibabacloud-oss-go-sdk-v2 v1.3.0 // indirect
|
||||
github.com/aliyun/credentials-go v1.4.7 // indirect
|
||||
github.com/anthropics/anthropic-sdk-go v1.4.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.14 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.14 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.21 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.8.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ec2 v1.296.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.99.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.9 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sns v1.39.11 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.15 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.19 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.10 // indirect
|
||||
github.com/aws/smithy-go v1.24.2 // indirect
|
||||
github.com/baidubce/bce-qianfan-sdk/go/qianfan v0.0.14 // indirect
|
||||
github.com/baidubce/bce-sdk-go v0.9.264 // indirect
|
||||
github.com/bits-and-blooms/bitset v1.24.4 // indirect
|
||||
github.com/blang/semver/v4 v4.0.0 // indirect
|
||||
github.com/buger/goterm v1.0.4 // indirect
|
||||
github.com/bytedance/gopkg v0.1.4 // indirect
|
||||
github.com/bytedance/sonic/loader v0.5.1 // indirect
|
||||
github.com/carmel/gooxml v0.0.0-20220216072414-40ff56130850 // indirect
|
||||
github.com/cenkalti/backoff v2.2.1+incompatible // indirect
|
||||
github.com/clbanning/mxj/v2 v2.7.0 // indirect
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
||||
github.com/cloudflare/circl v1.6.3 // indirect
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
github.com/coder/websocket v1.8.14 // indirect
|
||||
github.com/cohere-ai/cohere-go/v2 v2.5.2 // indirect
|
||||
github.com/consensys/gnark-crypto v0.20.1 // indirect
|
||||
github.com/containerd/errdefs v1.0.0 // indirect
|
||||
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||
github.com/crate-crypto/go-eth-kzg v1.5.0 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.7.0 // indirect
|
||||
github.com/deckarep/golang-set/v2 v2.9.0 // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 // indirect
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/digitalocean/go-libvirt v0.0.0-20260217163227-273eaa321819 // indirect
|
||||
github.com/diskfs/go-diskfs v1.9.1 // indirect
|
||||
github.com/distribution/reference v0.6.0 // indirect
|
||||
github.com/djherbis/times v1.6.0 // indirect
|
||||
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||
github.com/docker/docker v28.5.2+incompatible // indirect
|
||||
github.com/docker/go-connections v0.7.0 // indirect
|
||||
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.13.0 // indirect
|
||||
github.com/emirpasic/gods v1.18.1 // indirect
|
||||
github.com/ethereum/c-kzg-4844/v2 v2.1.7 // indirect
|
||||
github.com/fasthttp/websocket v1.5.12 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.9.1 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
|
||||
github.com/gage-technologies/mistral-go v1.1.0 // indirect
|
||||
github.com/go-errors/errors v1.5.1 // indirect
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.9.0 // indirect
|
||||
github.com/go-git/go-git/v5 v5.19.1 // indirect
|
||||
github.com/go-ini/ini v1.67.0 // indirect
|
||||
github.com/go-openapi/swag/cmdutils v0.25.5 // indirect
|
||||
github.com/go-openapi/swag/conv v0.25.5 // indirect
|
||||
@@ -195,144 +135,66 @@ require (
|
||||
github.com/go-openapi/swag/yamlutils v0.25.5 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-sql-driver/mysql v1.10.0 // indirect
|
||||
github.com/goccy/go-yaml v1.19.2 // indirect
|
||||
github.com/gofiber/schema v1.7.1 // indirect
|
||||
github.com/gofiber/utils/v2 v2.0.4 // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
|
||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
|
||||
github.com/golang-sql/sqlexp v0.1.0 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||
github.com/google/gnostic-models v0.7.1 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/gorilla/rpc v1.2.1 // indirect
|
||||
github.com/grandcat/zeroconf v1.0.0 // indirect
|
||||
github.com/hanzo-ds/native v0.72.0 // indirect
|
||||
github.com/hanzoai/account v0.2.0 // indirect
|
||||
github.com/hanzoai/ai v1.832.5 // indirect
|
||||
github.com/hanzoai/authz v1.10.29 // indirect
|
||||
github.com/hanzoai/commerce v1.49.36 // indirect
|
||||
github.com/hanzoai/csqlite v0.1.0 // indirect
|
||||
github.com/hanzoai/dashscope-go-sdk v0.0.2 // indirect
|
||||
github.com/hanzoai/dashscopego v0.6.0 // indirect
|
||||
github.com/hanzoai/dbx v1.17.2 // indirect
|
||||
github.com/hanzoai/decimal v0.1.1 // indirect
|
||||
github.com/hanzoai/go-cosyvoice v1.0.0 // indirect
|
||||
github.com/hanzoai/go-openai v1.41.0 // indirect
|
||||
github.com/hanzoai/go-openai-realtime v1.0.0 // indirect
|
||||
github.com/hanzoai/go-openai-realtime/contrib/ws-gorilla v1.0.0 // indirect
|
||||
github.com/hanzoai/go-openrouter v1.0.0 // indirect
|
||||
github.com/hanzoai/ha v0.1.1 // indirect
|
||||
github.com/hanzoai/iam v1.33.26 // indirect
|
||||
github.com/hanzoai/metrics v1.110.2 // indirect
|
||||
github.com/hanzoai/money v0.2.1 // indirect
|
||||
github.com/hanzoai/orm v0.6.18 // indirect
|
||||
github.com/hanzoai/pdf v1.2.0 // indirect
|
||||
github.com/hanzoai/s3-go v1.0.0 // indirect
|
||||
github.com/hanzoai/search-go v0.36.0 // indirect
|
||||
github.com/hanzoai/sqlcipher v0.1.1 // indirect
|
||||
github.com/hanzoai/tasks v1.52.4 // indirect
|
||||
github.com/hanzoai/vfs v0.6.6 // indirect
|
||||
github.com/hashicorp/go-metrics v0.5.4 // indirect
|
||||
github.com/henomis/lingoose v0.1.0 // indirect
|
||||
github.com/hhrutter/lzw v1.0.0 // indirect
|
||||
github.com/hhrutter/pkcs7 v0.2.0 // indirect
|
||||
github.com/hhrutter/tiff v1.0.2 // indirect
|
||||
github.com/holiman/uint256 v1.3.2 // indirect
|
||||
github.com/huandu/go-clone v1.7.3 // indirect
|
||||
github.com/hupe1980/go-huggingface v0.0.15 // indirect
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||
github.com/jinzhu/copier v0.4.0 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
||||
github.com/klauspost/crc32 v1.3.0 // indirect
|
||||
github.com/knadh/koanf/maps v0.1.2 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/leverly/ChatGLM v1.2.0 // indirect
|
||||
github.com/lib/pq v1.12.3 // indirect
|
||||
github.com/lithammer/shortuuid v3.0.0+incompatible // indirect
|
||||
github.com/luthermonson/go-proxmox v0.4.0 // indirect
|
||||
github.com/luxfi/accel v1.2.4 // indirect
|
||||
github.com/luxfi/cache v1.3.1 // indirect
|
||||
github.com/luxfi/container v0.2.1 // indirect
|
||||
github.com/luxfi/crypto v1.20.2 // indirect
|
||||
github.com/luxfi/crypto/ipa v1.2.4 // indirect
|
||||
github.com/luxfi/geth v1.20.1 // indirect
|
||||
github.com/luxfi/ids v1.3.2 // indirect
|
||||
github.com/luxfi/math v1.5.1 // indirect
|
||||
github.com/luxfi/math/big v0.1.0 // indirect
|
||||
github.com/luxfi/mdns v0.1.1 // indirect
|
||||
github.com/luxfi/metric v1.9.0 // indirect
|
||||
github.com/luxfi/mock v0.1.1 // indirect
|
||||
github.com/luxfi/pq v1.1.0 // indirect
|
||||
github.com/luxfi/trace v1.4.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.15 // indirect
|
||||
github.com/mattn/go-isatty v0.0.22 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.21 // indirect
|
||||
github.com/microsoft/go-mssqldb v1.9.5 // indirect
|
||||
github.com/minio/crc64nvme v1.1.1 // indirect
|
||||
github.com/minio/md5-simd v1.1.2 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect
|
||||
github.com/mr-tron/base58 v1.3.0 // indirect
|
||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||
github.com/nxadm/tail v1.4.11 // indirect
|
||||
github.com/openai/openai-go/v2 v2.1.1 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||
github.com/orcaman/concurrent-map/v2 v2.0.1 // indirect
|
||||
github.com/oschwald/geoip2-golang v1.11.0 // indirect
|
||||
github.com/oschwald/maxminddb-golang v1.13.0 // indirect
|
||||
github.com/pdfcpu/pdfcpu v0.11.0 // indirect
|
||||
github.com/philhofer/fwd v1.2.0 // indirect
|
||||
github.com/pjbgf/sha1cd v0.6.0 // indirect
|
||||
github.com/pkoukk/tiktoken-go v0.1.7 // indirect
|
||||
github.com/prometheus/client_golang/exp v0.0.0-20260325093428-d8591d0db856 // indirect
|
||||
github.com/puzpuzpuz/xsync/v4 v4.4.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/robfig/cron/v3 v3.0.1 // indirect
|
||||
github.com/rs/xid v1.6.0 // indirect
|
||||
github.com/sagikazarmark/locafero v0.12.0 // indirect
|
||||
github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 // indirect
|
||||
github.com/sergi/go-diff v1.4.0 // indirect
|
||||
github.com/shirou/gopsutil v3.21.11+incompatible // indirect
|
||||
github.com/sirupsen/logrus v1.9.4 // indirect
|
||||
github.com/skeema/knownhosts v1.3.1 // indirect
|
||||
github.com/spf13/afero v1.15.0 // indirect
|
||||
github.com/spf13/cast v1.10.0 // indirect
|
||||
github.com/spf13/viper v1.21.0 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/supranational/blst v0.3.16 // indirect
|
||||
github.com/swaggest/refl v1.4.0 // indirect
|
||||
github.com/swaggest/usecase v1.3.1 // indirect
|
||||
github.com/tealeg/xlsx v1.0.5 // indirect
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.77 // indirect
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cvm v1.0.1116 // indirect
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/hunyuan v1.3.48 // indirect
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/tbaas v1.1.13 // indirect
|
||||
github.com/tidwall/sjson v1.2.5 // indirect
|
||||
github.com/tinylib/msgp v1.6.4 // indirect
|
||||
github.com/tjfoc/gmsm v1.4.1 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ua-parser/uap-go v0.0.0-20251207011819-db9adb27a0b8 // indirect
|
||||
github.com/ugorji/go/codec v1.3.1 // indirect
|
||||
github.com/uptrace/opentelemetry-go-extra/otelsql v0.3.2 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasthttp v1.72.0 // indirect
|
||||
github.com/volcengine/volc-sdk-golang v1.0.241 // indirect
|
||||
github.com/volcengine/volcengine-go-sdk v1.0.141 // indirect
|
||||
github.com/wangbin/jiebago v0.3.2 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||
github.com/xlab/treeprint v1.2.0 // indirect
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
|
||||
github.com/zap-proto/fiber/v3 v3.2.1 // indirect
|
||||
github.com/zap-proto/go v1.3.0 // indirect
|
||||
github.com/zap-proto/http v0.3.1 // indirect
|
||||
github.com/zap-proto/md v0.1.0 // indirect
|
||||
github.com/zeebo/xxh3 v1.1.0 // indirect
|
||||
go.mongodb.org/mongo-driver v1.17.9 // indirect
|
||||
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
|
||||
go.opentelemetry.io/collector/client v1.54.0 // indirect
|
||||
go.opentelemetry.io/collector/config/configoptional v1.50.0 // indirect
|
||||
@@ -343,25 +205,16 @@ require (
|
||||
go.uber.org/goleak v1.3.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.4 // indirect
|
||||
golang.org/x/arch v0.25.0 // indirect
|
||||
golang.org/x/image v0.41.0 // indirect
|
||||
golang.org/x/term v0.45.0 // indirect
|
||||
google.golang.org/genai v1.10.0 // indirect
|
||||
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect
|
||||
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/ini.v1 v1.67.1 // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
k8s.io/api v0.36.2 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20260330154417-16be699c7b31 // indirect
|
||||
k8s.io/metrics v0.36.1 // indirect
|
||||
modernc.org/libc v1.72.3 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
modernc.org/sqlite v1.51.0 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
|
||||
sigs.k8s.io/kustomize/api v0.21.1 // indirect
|
||||
sigs.k8s.io/kustomize/kyaml v0.21.1 // indirect
|
||||
sigs.k8s.io/randfill v1.0.0 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.2 // indirect
|
||||
sigs.k8s.io/yaml v1.6.0 // indirect
|
||||
|
||||
@@ -57,12 +57,25 @@ func mountHealth(app *zip.App) {
|
||||
app.Get("/v1/o11y/readyz", probe(func(h factory.Handler) http.HandlerFunc { return h.Readyz }))
|
||||
}
|
||||
|
||||
// delegate is the probe group's fall-through: the registered runtime handler,
|
||||
// named directly.
|
||||
//
|
||||
// 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
|
||||
// named now and the catch-all is gone, so c.Next() would fall through to a 404
|
||||
// — the probes would start FAILING the moment the deployment had not called
|
||||
// SetHealth yet, which is exactly when a liveness probe matters most. Naming the
|
||||
// destination is both the fix and the honest statement of what "fall through"
|
||||
// always meant here.
|
||||
var delegate = zip.AdaptNetHTTP(handlerAdapter{})
|
||||
|
||||
// livez reports process liveness. factory.Handler.Livez renders an empty success
|
||||
// envelope with 200 unconditionally, so it is rendered natively here through the
|
||||
// shared render types rather than bridged. Falls through when no handler is set.
|
||||
// shared render types rather than bridged. Falls through to the runtime when no
|
||||
// handler is set.
|
||||
func livez(c *zip.Ctx) error {
|
||||
if getHealth() == nil {
|
||||
return c.Next()
|
||||
return delegate(c)
|
||||
}
|
||||
return c.JSON(http.StatusOK, render.SuccessResponse{Status: render.StatusSuccess.String()})
|
||||
}
|
||||
@@ -70,12 +83,12 @@ func livez(c *zip.Ctx) error {
|
||||
// probe dispatches a stateful health check (healthz, readyz) through the runtime
|
||||
// handler selected by sel. The check reads the service registry, so its body
|
||||
// stays in factory.Handler (one home) and is reached over the net/http bridge;
|
||||
// the routing is native. Falls through when no handler is set.
|
||||
// the routing is native. Falls through to the runtime when no handler is set.
|
||||
func probe(sel func(factory.Handler) http.HandlerFunc) zip.Handler {
|
||||
return func(c *zip.Ctx) error {
|
||||
h := getHealth()
|
||||
if h == nil {
|
||||
return c.Next()
|
||||
return delegate(c)
|
||||
}
|
||||
return zip.AdaptNetHTTP(sel(h))(c)
|
||||
}
|
||||
|
||||
+47
-141
@@ -13,7 +13,7 @@ package o11y
|
||||
//
|
||||
// THE WIRE DOES NOT MOVE, the same way telemetry.go's five reads did not move
|
||||
// it: no op here re-implements anything. Each hands the call to the SAME
|
||||
// runtime handler the wildcard delegates to (see relayO11y), so identity
|
||||
// runtime handler the wildcard delegates to (see relay.go), so identity
|
||||
// resolution, the org gate, the ROLE CHECK the mux registration declared —
|
||||
// AdminAccess, ViewAccess, SelfAccess, OpenAccess — the audit record, the
|
||||
// timeout and the success envelope are all still the runtime's, executed in
|
||||
@@ -34,28 +34,19 @@ package o11y
|
||||
// (identity_test.go pins both halves).
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/zap-proto/zip"
|
||||
)
|
||||
|
||||
// identityPrefix is the face's path root, spelled ONCE: the group the ops
|
||||
// register on, and the first segment of every path relayO11y is handed.
|
||||
const identityPrefix = "/v1/o11y"
|
||||
|
||||
// mountIdentity registers the identity face's typed ops on the native router.
|
||||
// Collection routes sit beside their parameterised siblings safely: the router
|
||||
// matches the most specific pattern, so /users/me can never be swallowed by
|
||||
// /users/:id, which is the same disambiguation the runtime's own tree makes.
|
||||
func mountIdentity(app *zip.App) {
|
||||
g := app.Group(identityPrefix)
|
||||
g := app.Group(o11yRoot)
|
||||
|
||||
// users and invites (runtime gate per op: see each op's comment)
|
||||
zip.Post(g, "/invite", createInvite, zip.WithStatus(http.StatusCreated), zip.WithOperationID("CreateInvite"))
|
||||
@@ -126,7 +117,7 @@ func mountIdentity(app *zip.App) {
|
||||
// runtime this op relays to.
|
||||
func createInvite(ctx context.Context, in *O11yInviteIn) (*O11yInviteOut, error) {
|
||||
out := new(O11yInviteOut)
|
||||
return out, relayO11y(ctx, http.MethodPost, "/invite", nil, in, out)
|
||||
return out, relay(ctx, http.MethodPost, o11yRoot+"/invite", nil, in, out)
|
||||
}
|
||||
|
||||
// createBulkInvite invites several people to the caller's org in one call,
|
||||
@@ -134,46 +125,46 @@ func createInvite(ctx context.Context, in *O11yInviteIn) (*O11yInviteOut, error)
|
||||
// createInvite. Admin gate.
|
||||
func createBulkInvite(ctx context.Context, in *O11yBulkInviteIn) (*O11yAck, error) {
|
||||
out := new(O11yAck)
|
||||
return out, relayO11y(ctx, http.MethodPost, "/invite/bulk", nil, in, out)
|
||||
return out, relay(ctx, http.MethodPost, o11yRoot+"/invite/bulk", nil, in, out)
|
||||
}
|
||||
|
||||
// listUsersDeprecated lists the org's members with their single legacy role.
|
||||
// Deprecated in favor of listUsers, which answers without the role. Admin gate.
|
||||
func listUsersDeprecated(ctx context.Context, _ *struct{}) (*O11yDeprecatedUsersOut, error) {
|
||||
out := new(O11yDeprecatedUsersOut)
|
||||
return out, relayO11y(ctx, http.MethodGet, "/user", nil, nil, out)
|
||||
return out, relay(ctx, http.MethodGet, o11yRoot+"/user", nil, nil, out)
|
||||
}
|
||||
|
||||
// listUsers lists the caller's org members. Admin gate.
|
||||
func listUsers(ctx context.Context, _ *struct{}) (*O11yUsersOut, error) {
|
||||
out := new(O11yUsersOut)
|
||||
return out, relayO11y(ctx, http.MethodGet, "/users", nil, nil, out)
|
||||
return out, relay(ctx, http.MethodGet, o11yRoot+"/users", nil, nil, out)
|
||||
}
|
||||
|
||||
// getMyUserDeprecated returns the calling user with their single legacy role.
|
||||
// Deprecated in favor of getMyUser. Open to any authenticated caller.
|
||||
func getMyUserDeprecated(ctx context.Context, _ *struct{}) (*O11yDeprecatedUserOut, error) {
|
||||
out := new(O11yDeprecatedUserOut)
|
||||
return out, relayO11y(ctx, http.MethodGet, "/user/me", nil, nil, out)
|
||||
return out, relay(ctx, http.MethodGet, o11yRoot+"/user/me", nil, nil, out)
|
||||
}
|
||||
|
||||
// getMyUser returns the calling user together with every role they hold. Open
|
||||
// to any authenticated caller.
|
||||
func getMyUser(ctx context.Context, _ *struct{}) (*O11yUserWithRolesOut, error) {
|
||||
out := new(O11yUserWithRolesOut)
|
||||
return out, relayO11y(ctx, http.MethodGet, "/users/me", nil, nil, out)
|
||||
return out, relay(ctx, http.MethodGet, o11yRoot+"/users/me", nil, nil, out)
|
||||
}
|
||||
|
||||
// createUser creates a member of the caller's org in the pending-invite state
|
||||
// and mails them their invitation; the answer is the new user's id. Admin gate.
|
||||
func createUser(ctx context.Context, in *O11yPostableUser) (*O11yCreatedOut, error) {
|
||||
out := new(O11yCreatedOut)
|
||||
return out, relayO11y(ctx, http.MethodPost, "/users", nil, in, out)
|
||||
return out, relay(ctx, http.MethodPost, o11yRoot+"/users", nil, in, out)
|
||||
}
|
||||
|
||||
// updateMyUser renames the calling user. Open to any authenticated caller.
|
||||
func updateMyUser(ctx context.Context, in *O11yUpdatableUser) (*struct{}, error) {
|
||||
return nil, relayO11y(ctx, http.MethodPut, "/users/me", nil, in, nil)
|
||||
return nil, relay(ctx, http.MethodPut, o11yRoot+"/users/me", nil, in, nil)
|
||||
}
|
||||
|
||||
// getUserDeprecated returns one org member with their single legacy role, by
|
||||
@@ -181,14 +172,14 @@ func updateMyUser(ctx context.Context, in *O11yUpdatableUser) (*struct{}, error)
|
||||
// self-access gate).
|
||||
func getUserDeprecated(ctx context.Context, in *O11yUserRef) (*O11yDeprecatedUserOut, error) {
|
||||
out := new(O11yDeprecatedUserOut)
|
||||
return out, relayO11y(ctx, http.MethodGet, "/user/"+in.ID, nil, nil, out)
|
||||
return out, relay(ctx, http.MethodGet, o11yRoot+"/user/"+in.ID, nil, nil, out)
|
||||
}
|
||||
|
||||
// getUser returns one org member together with every role they hold, by user
|
||||
// id. Admin gate.
|
||||
func getUser(ctx context.Context, in *O11yUserRef) (*O11yUserWithRolesOut, error) {
|
||||
out := new(O11yUserWithRolesOut)
|
||||
return out, relayO11y(ctx, http.MethodGet, "/users/"+in.ID, nil, nil, out)
|
||||
return out, relay(ctx, http.MethodGet, o11yRoot+"/users/"+in.ID, nil, nil, out)
|
||||
}
|
||||
|
||||
// updateUserDeprecated renames one org member and may move their legacy role,
|
||||
@@ -196,24 +187,24 @@ func getUser(ctx context.Context, in *O11yUserRef) (*O11yUserWithRolesOut, error
|
||||
// only themselves (the runtime's self-access gate).
|
||||
func updateUserDeprecated(ctx context.Context, in *O11yDeprecatedUserUpdate) (*O11yDeprecatedUserOut, error) {
|
||||
out := new(O11yDeprecatedUserOut)
|
||||
return out, relayO11y(ctx, http.MethodPut, "/user/"+in.ID, nil, in, out)
|
||||
return out, relay(ctx, http.MethodPut, o11yRoot+"/user/"+in.ID, nil, in, out)
|
||||
}
|
||||
|
||||
// updateUser renames one org member, by user id — someone else, never the
|
||||
// caller, who renames themselves through updateMyUser. Admin gate.
|
||||
func updateUser(ctx context.Context, in *O11yUserUpdate) (*struct{}, error) {
|
||||
return nil, relayO11y(ctx, http.MethodPut, "/users/"+in.ID, nil, in, nil)
|
||||
return nil, relay(ctx, http.MethodPut, o11yRoot+"/users/"+in.ID, nil, in, nil)
|
||||
}
|
||||
|
||||
// deleteUserDeprecated removes one org member, by user id. The same operation
|
||||
// as deleteUser on the legacy singular path. Admin gate.
|
||||
func deleteUserDeprecated(ctx context.Context, in *O11yUserRef) (*struct{}, error) {
|
||||
return nil, relayO11y(ctx, http.MethodDelete, "/user/"+in.ID, nil, nil, nil)
|
||||
return nil, relay(ctx, http.MethodDelete, o11yRoot+"/user/"+in.ID, nil, nil, nil)
|
||||
}
|
||||
|
||||
// deleteUser removes one org member, by user id. Admin gate.
|
||||
func deleteUser(ctx context.Context, in *O11yUserRef) (*struct{}, error) {
|
||||
return nil, relayO11y(ctx, http.MethodDelete, "/users/"+in.ID, nil, nil, nil)
|
||||
return nil, relay(ctx, http.MethodDelete, o11yRoot+"/users/"+in.ID, nil, nil, nil)
|
||||
}
|
||||
|
||||
// ── passwords and their reset tokens ──────────────────────────────────────────
|
||||
@@ -223,46 +214,46 @@ func deleteUser(ctx context.Context, in *O11yUserRef) (*struct{}, error) {
|
||||
// which separates reading from minting. Admin gate.
|
||||
func getResetTokenDeprecated(ctx context.Context, in *O11yUserRef) (*O11yResetTokenOut, error) {
|
||||
out := new(O11yResetTokenOut)
|
||||
return out, relayO11y(ctx, http.MethodGet, "/getResetPasswordToken/"+in.ID, nil, nil, out)
|
||||
return out, relay(ctx, http.MethodGet, o11yRoot+"/getResetPasswordToken/"+in.ID, nil, nil, out)
|
||||
}
|
||||
|
||||
// getResetToken returns the reset-password token a user already has; absent
|
||||
// one, the answer is a not-found rather than a fresh token. Admin gate.
|
||||
func getResetToken(ctx context.Context, in *O11yUserRef) (*O11yResetTokenOut, error) {
|
||||
out := new(O11yResetTokenOut)
|
||||
return out, relayO11y(ctx, http.MethodGet, "/users/"+in.ID+"/reset_password_tokens", nil, nil, out)
|
||||
return out, relay(ctx, http.MethodGet, o11yRoot+"/users/"+in.ID+"/reset_password_tokens", nil, nil, out)
|
||||
}
|
||||
|
||||
// createResetToken creates or regenerates a user's reset-password token: a
|
||||
// live token is returned as it is, an expired one is replaced. Admin gate.
|
||||
func createResetToken(ctx context.Context, in *O11yUserRef) (*O11yResetTokenOut, error) {
|
||||
out := new(O11yResetTokenOut)
|
||||
return out, relayO11y(ctx, http.MethodPut, "/users/"+in.ID+"/reset_password_tokens", nil, nil, out)
|
||||
return out, relay(ctx, http.MethodPut, o11yRoot+"/users/"+in.ID+"/reset_password_tokens", nil, nil, out)
|
||||
}
|
||||
|
||||
// verifyResetToken checks that a reset-password token exists and has not
|
||||
// expired, without consuming it. Unauthenticated: the token is the proof.
|
||||
func verifyResetToken(ctx context.Context, in *O11yResetTokenRef) (*struct{}, error) {
|
||||
return nil, relayO11y(ctx, http.MethodPost, "/reset_password_tokens/verify", nil, in, nil)
|
||||
return nil, relay(ctx, http.MethodPost, o11yRoot+"/reset_password_tokens/verify", nil, in, nil)
|
||||
}
|
||||
|
||||
// resetPassword sets a new password for whoever the reset token was minted
|
||||
// for, consuming the token. Unauthenticated: the token is the proof.
|
||||
func resetPassword(ctx context.Context, in *O11yResetPasswordIn) (*struct{}, error) {
|
||||
return nil, relayO11y(ctx, http.MethodPost, "/resetPassword", nil, in, nil)
|
||||
return nil, relay(ctx, http.MethodPost, o11yRoot+"/resetPassword", nil, in, nil)
|
||||
}
|
||||
|
||||
// changeMyPassword replaces the calling user's password, refusing when the old
|
||||
// one does not match. Open to any authenticated caller.
|
||||
func changeMyPassword(ctx context.Context, in *O11yChangePasswordIn) (*struct{}, error) {
|
||||
return nil, relayO11y(ctx, http.MethodPut, "/users/me/factor_password", nil, in, nil)
|
||||
return nil, relay(ctx, http.MethodPut, o11yRoot+"/users/me/factor_password", nil, in, nil)
|
||||
}
|
||||
|
||||
// forgotPassword starts the forgotten-password flow: the named user is mailed
|
||||
// a reset link. Unauthenticated by design, and deliberately quiet about
|
||||
// whether the address exists.
|
||||
func forgotPassword(ctx context.Context, in *O11yForgotPasswordIn) (*struct{}, error) {
|
||||
return nil, relayO11y(ctx, http.MethodPost, "/factor_password/forgot", nil, in, nil)
|
||||
return nil, relay(ctx, http.MethodPost, o11yRoot+"/factor_password/forgot", nil, in, nil)
|
||||
}
|
||||
|
||||
// ── roles on users ────────────────────────────────────────────────────────────
|
||||
@@ -271,27 +262,27 @@ func forgotPassword(ctx context.Context, in *O11yForgotPasswordIn) (*struct{}, e
|
||||
// gate.
|
||||
func listUserRoles(ctx context.Context, in *O11yUserRef) (*O11yRolesOut, error) {
|
||||
out := new(O11yRolesOut)
|
||||
return out, relayO11y(ctx, http.MethodGet, "/users/"+in.ID+"/roles", nil, nil, out)
|
||||
return out, relay(ctx, http.MethodGet, o11yRoot+"/users/"+in.ID+"/roles", nil, nil, out)
|
||||
}
|
||||
|
||||
// setUserRole assigns a role, by role name, to one org member — someone else,
|
||||
// never the caller. Admin gate.
|
||||
func setUserRole(ctx context.Context, in *O11ySetRoleIn) (*O11yAck, error) {
|
||||
out := new(O11yAck)
|
||||
return out, relayO11y(ctx, http.MethodPost, "/users/"+in.ID+"/roles", nil, in, out)
|
||||
return out, relay(ctx, http.MethodPost, o11yRoot+"/users/"+in.ID+"/roles", nil, in, out)
|
||||
}
|
||||
|
||||
// removeUserRole takes a role away from one org member, by user id and role
|
||||
// id — someone else, never the caller. Admin gate.
|
||||
func removeUserRole(ctx context.Context, in *O11yUserRoleRef) (*struct{}, error) {
|
||||
return nil, relayO11y(ctx, http.MethodDelete, "/users/"+in.ID+"/roles/"+in.RoleID, nil, nil, nil)
|
||||
return nil, relay(ctx, http.MethodDelete, o11yRoot+"/users/"+in.ID+"/roles/"+in.RoleID, nil, nil, nil)
|
||||
}
|
||||
|
||||
// listRoleUsers returns every org member holding a role, by role id. Admin
|
||||
// gate.
|
||||
func listRoleUsers(ctx context.Context, in *O11yRoleUsersIn) (*O11yUsersOut, error) {
|
||||
out := new(O11yUsersOut)
|
||||
return out, relayO11y(ctx, http.MethodGet, "/roles/"+in.ID+"/users", nil, nil, out)
|
||||
return out, relay(ctx, http.MethodGet, o11yRoot+"/roles/"+in.ID+"/users", nil, nil, out)
|
||||
}
|
||||
|
||||
// ── sessions ──────────────────────────────────────────────────────────────────
|
||||
@@ -301,7 +292,7 @@ func listRoleUsers(ctx context.Context, in *O11yRoleUsersIn) (*O11yUsersOut, err
|
||||
// authentication begins.
|
||||
func createEmailPasswordSession(ctx context.Context, in *O11yEmailPasswordSessionIn) (*O11yTokenOut, error) {
|
||||
out := new(O11yTokenOut)
|
||||
return out, relayO11y(ctx, http.MethodPost, "/sessions/email_password", nil, in, out)
|
||||
return out, relay(ctx, http.MethodPost, o11yRoot+"/sessions/email_password", nil, in, out)
|
||||
}
|
||||
|
||||
// getSessionContext tells a sign-in page what an email address can do: which
|
||||
@@ -309,7 +300,7 @@ func createEmailPasswordSession(ctx context.Context, in *O11yEmailPasswordSessio
|
||||
// open to it. Unauthenticated: it runs before any session exists.
|
||||
func getSessionContext(ctx context.Context, in *O11ySessionContextIn) (*O11ySessionContextOut, error) {
|
||||
out := new(O11ySessionContextOut)
|
||||
return out, relayO11y(ctx, http.MethodGet, "/sessions/context", query(
|
||||
return out, relay(ctx, http.MethodGet, o11yRoot+"/sessions/context", query(
|
||||
"email", in.Email,
|
||||
"ref", in.Ref,
|
||||
), nil, out)
|
||||
@@ -319,13 +310,13 @@ func getSessionContext(ctx context.Context, in *O11ySessionContextIn) (*O11ySess
|
||||
// old pair. The access token being rotated identifies the session.
|
||||
func rotateSession(ctx context.Context, in *O11yRotateSessionIn) (*O11yTokenOut, error) {
|
||||
out := new(O11yTokenOut)
|
||||
return out, relayO11y(ctx, http.MethodPost, "/sessions/rotate", nil, in, out)
|
||||
return out, relay(ctx, http.MethodPost, o11yRoot+"/sessions/rotate", nil, in, out)
|
||||
}
|
||||
|
||||
// deleteSession signs the calling session out, invalidating its tokens. The
|
||||
// access token on the call names the session to end.
|
||||
func deleteSession(ctx context.Context, _ *struct{}) (*struct{}, error) {
|
||||
return nil, relayO11y(ctx, http.MethodDelete, "/sessions", nil, nil, nil)
|
||||
return nil, relay(ctx, http.MethodDelete, o11yRoot+"/sessions", nil, nil, nil)
|
||||
}
|
||||
|
||||
// ── auth domains ──────────────────────────────────────────────────────────────
|
||||
@@ -334,33 +325,33 @@ func deleteSession(ctx context.Context, _ *struct{}) (*struct{}, error) {
|
||||
// configuration this org owns. Admin gate.
|
||||
func listAuthDomains(ctx context.Context, _ *struct{}) (*O11yAuthDomainsOut, error) {
|
||||
out := new(O11yAuthDomainsOut)
|
||||
return out, relayO11y(ctx, http.MethodGet, "/domains", nil, nil, out)
|
||||
return out, relay(ctx, http.MethodGet, o11yRoot+"/domains", nil, nil, out)
|
||||
}
|
||||
|
||||
// createAuthDomain claims an email domain for the org and configures how its
|
||||
// users sign in; the answer is the new domain's id. Admin gate.
|
||||
func createAuthDomain(ctx context.Context, in *O11yPostableAuthDomain) (*O11yCreatedOut, error) {
|
||||
out := new(O11yCreatedOut)
|
||||
return out, relayO11y(ctx, http.MethodPost, "/domains", nil, in, out)
|
||||
return out, relay(ctx, http.MethodPost, o11yRoot+"/domains", nil, in, out)
|
||||
}
|
||||
|
||||
// getAuthDomain returns one auth domain with its SSO configuration, by id.
|
||||
// Admin gate.
|
||||
func getAuthDomain(ctx context.Context, in *O11yDomainRef) (*O11yAuthDomainOut, error) {
|
||||
out := new(O11yAuthDomainOut)
|
||||
return out, relayO11y(ctx, http.MethodGet, "/domains/"+in.ID, nil, nil, out)
|
||||
return out, relay(ctx, http.MethodGet, o11yRoot+"/domains/"+in.ID, nil, nil, out)
|
||||
}
|
||||
|
||||
// updateAuthDomain replaces one auth domain's SSO configuration, by id. Admin
|
||||
// gate.
|
||||
func updateAuthDomain(ctx context.Context, in *O11yUpdatableAuthDomain) (*struct{}, error) {
|
||||
return nil, relayO11y(ctx, http.MethodPut, "/domains/"+in.ID, nil, in, nil)
|
||||
return nil, relay(ctx, http.MethodPut, o11yRoot+"/domains/"+in.ID, nil, in, nil)
|
||||
}
|
||||
|
||||
// deleteAuthDomain releases an email domain and discards its SSO
|
||||
// configuration, by id. Admin gate.
|
||||
func deleteAuthDomain(ctx context.Context, in *O11yDomainRef) (*struct{}, error) {
|
||||
return nil, relayO11y(ctx, http.MethodDelete, "/domains/"+in.ID, nil, nil, nil)
|
||||
return nil, relay(ctx, http.MethodDelete, o11yRoot+"/domains/"+in.ID, nil, nil, nil)
|
||||
}
|
||||
|
||||
// ── my organization and its quick filters ─────────────────────────────────────
|
||||
@@ -368,33 +359,33 @@ func deleteAuthDomain(ctx context.Context, in *O11yDomainRef) (*struct{}, error)
|
||||
// getMyOrg returns the caller's own organization. Admin gate.
|
||||
func getMyOrg(ctx context.Context, _ *struct{}) (*O11yOrganizationOut, error) {
|
||||
out := new(O11yOrganizationOut)
|
||||
return out, relayO11y(ctx, http.MethodGet, "/orgs/me", nil, nil, out)
|
||||
return out, relay(ctx, http.MethodGet, o11yRoot+"/orgs/me", nil, nil, out)
|
||||
}
|
||||
|
||||
// updateMyOrg rewrites the caller's own organization record — display name,
|
||||
// name, alias — always addressed as "me", never by id. Admin gate.
|
||||
func updateMyOrg(ctx context.Context, in *O11yOrganization) (*struct{}, error) {
|
||||
return nil, relayO11y(ctx, http.MethodPut, "/orgs/me", nil, in, nil)
|
||||
return nil, relay(ctx, http.MethodPut, o11yRoot+"/orgs/me", nil, in, nil)
|
||||
}
|
||||
|
||||
// getQuickFilters returns the org's quick filters for every signal — the
|
||||
// attribute shortlists its explorers offer as one-click filters. Viewer gate.
|
||||
func getQuickFilters(ctx context.Context, _ *struct{}) (*O11yQuickFiltersOut, error) {
|
||||
out := new(O11yQuickFiltersOut)
|
||||
return out, relayO11y(ctx, http.MethodGet, "/orgs/me/filters", nil, nil, out)
|
||||
return out, relay(ctx, http.MethodGet, o11yRoot+"/orgs/me/filters", nil, nil, out)
|
||||
}
|
||||
|
||||
// getSignalFilters returns the org's quick filters for one signal — traces,
|
||||
// logs, metrics, exceptions or api_monitoring. Viewer gate.
|
||||
func getSignalFilters(ctx context.Context, in *O11ySignalRef) (*O11ySignalFiltersOut, error) {
|
||||
out := new(O11ySignalFiltersOut)
|
||||
return out, relayO11y(ctx, http.MethodGet, "/orgs/me/filters/"+in.Signal, nil, nil, out)
|
||||
return out, relay(ctx, http.MethodGet, o11yRoot+"/orgs/me/filters/"+in.Signal, nil, nil, out)
|
||||
}
|
||||
|
||||
// updateQuickFilters replaces the org's quick filters for one signal with the
|
||||
// attribute list given. Admin gate.
|
||||
func updateQuickFilters(ctx context.Context, in *O11yUpdatableQuickFilters) (*struct{}, error) {
|
||||
return nil, relayO11y(ctx, http.MethodPut, "/orgs/me/filters", nil, in, nil)
|
||||
return nil, relay(ctx, http.MethodPut, o11yRoot+"/orgs/me/filters", nil, in, nil)
|
||||
}
|
||||
|
||||
// ── preferences ───────────────────────────────────────────────────────────────
|
||||
@@ -403,38 +394,38 @@ func updateQuickFilters(ctx context.Context, in *O11yUpdatableQuickFilters) (*st
|
||||
// its current and default value. Viewer gate.
|
||||
func listUserPreferences(ctx context.Context, _ *struct{}) (*O11yPreferencesOut, error) {
|
||||
out := new(O11yPreferencesOut)
|
||||
return out, relayO11y(ctx, http.MethodGet, "/user/preferences", nil, nil, out)
|
||||
return out, relay(ctx, http.MethodGet, o11yRoot+"/user/preferences", nil, nil, out)
|
||||
}
|
||||
|
||||
// getUserPreference returns one preference of the calling user, by name.
|
||||
// Viewer gate.
|
||||
func getUserPreference(ctx context.Context, in *O11yPreferenceRef) (*O11yPreferenceOut, error) {
|
||||
out := new(O11yPreferenceOut)
|
||||
return out, relayO11y(ctx, http.MethodGet, "/user/preferences/"+in.Name, nil, nil, out)
|
||||
return out, relay(ctx, http.MethodGet, o11yRoot+"/user/preferences/"+in.Name, nil, nil, out)
|
||||
}
|
||||
|
||||
// updateUserPreference sets one preference of the calling user, by name.
|
||||
// Viewer gate.
|
||||
func updateUserPreference(ctx context.Context, in *O11yUpdatablePreference) (*struct{}, error) {
|
||||
return nil, relayO11y(ctx, http.MethodPut, "/user/preferences/"+in.Name, nil, in, nil)
|
||||
return nil, relay(ctx, http.MethodPut, o11yRoot+"/user/preferences/"+in.Name, nil, in, nil)
|
||||
}
|
||||
|
||||
// listOrgPreferences lists every org-scoped preference, each with its current
|
||||
// and default value. Admin gate.
|
||||
func listOrgPreferences(ctx context.Context, _ *struct{}) (*O11yPreferencesOut, error) {
|
||||
out := new(O11yPreferencesOut)
|
||||
return out, relayO11y(ctx, http.MethodGet, "/org/preferences", nil, nil, out)
|
||||
return out, relay(ctx, http.MethodGet, o11yRoot+"/org/preferences", nil, nil, out)
|
||||
}
|
||||
|
||||
// getOrgPreference returns one org-scoped preference, by name. Admin gate.
|
||||
func getOrgPreference(ctx context.Context, in *O11yPreferenceRef) (*O11yPreferenceOut, error) {
|
||||
out := new(O11yPreferenceOut)
|
||||
return out, relayO11y(ctx, http.MethodGet, "/org/preferences/"+in.Name, nil, nil, out)
|
||||
return out, relay(ctx, http.MethodGet, o11yRoot+"/org/preferences/"+in.Name, nil, nil, out)
|
||||
}
|
||||
|
||||
// updateOrgPreference sets one org-scoped preference, by name. Admin gate.
|
||||
func updateOrgPreference(ctx context.Context, in *O11yUpdatablePreference) (*struct{}, error) {
|
||||
return nil, relayO11y(ctx, http.MethodPut, "/org/preferences/"+in.Name, nil, in, nil)
|
||||
return nil, relay(ctx, http.MethodPut, o11yRoot+"/org/preferences/"+in.Name, nil, in, nil)
|
||||
}
|
||||
|
||||
// ── inputs ────────────────────────────────────────────────────────────────────
|
||||
@@ -1213,88 +1204,3 @@ type O11yPreference struct {
|
||||
}
|
||||
|
||||
// ── the seam ──────────────────────────────────────────────────────────────────
|
||||
|
||||
// relayO11y hands a typed op's call to the o11y runtime and decodes the answer
|
||||
// into the op's Out — telemetry.go's relay, for paths under /v1/o11y. (That
|
||||
// relay bakes in the sentry prefix; hoisting the prefix out of it is a
|
||||
// cross-face refactor for when the last face is typed, not a thing to half-do
|
||||
// from one of them.)
|
||||
//
|
||||
// It is what keeps these ops a NAMING of the wire rather than a second
|
||||
// implementation of it. The handler it calls is the one SetHandler registered —
|
||||
// the same value the delegation wildcard forwards to — so the request runs the
|
||||
// whole chain it always ran: identity resolution, the role gate the route
|
||||
// declared, the audit record, the timeout, the handler. There is no policy
|
||||
// here; those all happen where they already do, one layer in.
|
||||
//
|
||||
// Identity is PROPAGATED, never minted: the gateway's assertion about the
|
||||
// caller travels on as the same headers it arrived on. A context with no
|
||||
// request behind it — a command, an agent call — carries no assertion, so the
|
||||
// runtime's gate refuses it, which is the honest answer rather than an
|
||||
// identity invented at this hop.
|
||||
//
|
||||
// A non-2xx becomes an error carrying the runtime's own status and reason, so
|
||||
// the status a caller sees is the status the runtime chose. A nil out says the
|
||||
// op answers with no body — the runtime's 204s — and skips decoding.
|
||||
func relayO11y(ctx context.Context, method, path string, params url.Values, body, out any) error {
|
||||
h := getHandler()
|
||||
if h == nil {
|
||||
return zip.Errorf(http.StatusServiceUnavailable, "o11y runtime not initialized")
|
||||
}
|
||||
|
||||
target := identityPrefix + path
|
||||
if q := params.Encode(); q != "" {
|
||||
target += "?" + q
|
||||
}
|
||||
|
||||
payload := io.Reader(http.NoBody)
|
||||
if body != nil {
|
||||
b, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return zip.ErrBadRequest(err.Error())
|
||||
}
|
||||
payload = bytes.NewReader(b)
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, method, target, payload)
|
||||
if err != nil {
|
||||
return zip.ErrBadRequest(err.Error())
|
||||
}
|
||||
if body != nil {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
caller := zip.CallerOf(ctx)
|
||||
for header, value := range map[string]string{
|
||||
zip.HeaderOrg: caller.Org,
|
||||
zip.HeaderProject: caller.Project,
|
||||
zip.HeaderUser: caller.User,
|
||||
zip.HeaderUserName: caller.Name,
|
||||
zip.HeaderUserEmail: caller.Email,
|
||||
zip.HeaderUserOwner: caller.Owner,
|
||||
zip.HeaderRequestID: caller.RequestID,
|
||||
} {
|
||||
if value != "" {
|
||||
req.Header.Set(header, value)
|
||||
}
|
||||
}
|
||||
if caller.Admin {
|
||||
req.Header.Set(zip.HeaderUserAdmin, "true")
|
||||
}
|
||||
if caller.OrgAdmin {
|
||||
req.Header.Set(zip.HeaderUserOrgAdmin, "true")
|
||||
}
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code < http.StatusOK || rec.Code >= http.StatusMultipleChoices {
|
||||
code, reason := refusal(rec.Body.Bytes())
|
||||
return &zip.HTTPError{Status: rec.Code, Code: code, Msg: reason}
|
||||
}
|
||||
if out == nil {
|
||||
return nil
|
||||
}
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), out); err != nil {
|
||||
return zip.ErrInternal("cannot read the runtime's answer: " + err.Error())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
+48
-29
@@ -9,7 +9,6 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hanzoai/cloud"
|
||||
"github.com/hanzoai/o11y"
|
||||
"github.com/zap-proto/zip"
|
||||
)
|
||||
@@ -109,47 +108,67 @@ func TestIdentityRoutesAreTheSameFortyFive(t *testing.T) {
|
||||
t.Errorf("%s is not registered as a typed op", key)
|
||||
}
|
||||
}
|
||||
// The three SSO callbacks are NAMED HATCHES now (mount.go mountHatches), not
|
||||
// typed ops and no longer a fall-through: they answer 303 with a Location and
|
||||
// no body, so a typed op would publish a response schema for a response that
|
||||
// does not exist. Each must be registered — a hatch that is not registered is
|
||||
// a 404 now that the catch-all is gone.
|
||||
for _, cb := range []string{
|
||||
"GET /v1/o11y/complete/google",
|
||||
"POST /v1/o11y/complete/saml",
|
||||
"GET /v1/o11y/complete/oidc",
|
||||
} {
|
||||
if got[cb] {
|
||||
t.Errorf("%s is registered natively; it must stay on the wildcard (it answers 303)", cb)
|
||||
if !got[cb] {
|
||||
t.Errorf("%s is not registered; it is a named hatch (it answers 303)", cb)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The three SSO callbacks still reach the runtime through the host's wildcard,
|
||||
// and the typed identity paths take precedence over that same wildcard however
|
||||
// the host ordered its mounts — the escape hatch is honest and the ops win.
|
||||
func TestCompleteCallbacksStayOnTheWildcard(t *testing.T) {
|
||||
app := zip.New(zip.Config{DisableStartupMessage: true})
|
||||
// The host registers its /v1/o11y wildcard BEFORE the module mounts, which is
|
||||
// the order the composed binary uses.
|
||||
app.All("/v1/o11y/*", zip.AdaptNetHTTP(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = io.WriteString(w, `{"door":"wildcard","path":"`+r.URL.Path+`"}`)
|
||||
})))
|
||||
if err := o11y.Mount(app, cloud.Deps{}); err != nil {
|
||||
t.Fatalf("Mount: %v", err)
|
||||
}
|
||||
runtime(t, []any{})
|
||||
// The three SSO callbacks reach the runtime through their OWN named routes now,
|
||||
// path untouched, with the 303 and its Location passed straight back — while the
|
||||
// typed identity paths next to them dispatch as ops. Both halves in one test,
|
||||
// because the interesting failure is one of them stealing the other's traffic.
|
||||
//
|
||||
// This test used to install a host wildcard and assert the callbacks fell into
|
||||
// it. That assertion could not distinguish a deliberate hatch from a route
|
||||
// nobody had converted, which is how three unmounted slices shipped; it now
|
||||
// asserts the named door instead.
|
||||
func TestCompleteCallbacksAreNamedHatches(t *testing.T) {
|
||||
app := mounted(t)
|
||||
|
||||
for method, target := range map[string]string{
|
||||
http.MethodGet: "/v1/o11y/complete/google",
|
||||
http.MethodPost: "/v1/o11y/complete/saml",
|
||||
var saw string
|
||||
o11y.SetHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
saw = r.URL.Path
|
||||
w.Header().Set("Location", "https://console.hanzo.ai/")
|
||||
w.WriteHeader(http.StatusSeeOther)
|
||||
}))
|
||||
t.Cleanup(func() { o11y.SetHandler(nil) })
|
||||
|
||||
for _, cb := range []struct{ method, target string }{
|
||||
{http.MethodGet, "/v1/o11y/complete/google"},
|
||||
{http.MethodPost, "/v1/o11y/complete/saml"},
|
||||
{http.MethodGet, "/v1/o11y/complete/oidc"},
|
||||
} {
|
||||
if _, body := call(t, app, member(method, target, nil)); !strings.Contains(string(body), `"door":"wildcard"`) {
|
||||
t.Errorf("%s no longer reaches the runtime through the wildcard: %s", target, body)
|
||||
saw = ""
|
||||
resp, err := app.Fiber().Test(member(cb.method, cb.target, nil))
|
||||
if err != nil {
|
||||
t.Fatalf("Test: %v", err)
|
||||
}
|
||||
if resp.StatusCode != http.StatusSeeOther {
|
||||
t.Errorf("%s status=%d, want 303 — the redirect is the answer", cb.target, resp.StatusCode)
|
||||
}
|
||||
if got := resp.Header.Get("Location"); got != "https://console.hanzo.ai/" {
|
||||
t.Errorf("%s Location=%q — the header a typed op would have hidden", cb.target, got)
|
||||
}
|
||||
if saw != cb.target {
|
||||
t.Errorf("%s reached the runtime as %q", cb.target, saw)
|
||||
}
|
||||
}
|
||||
if _, body := call(t, app, member(http.MethodGet, "/v1/o11y/complete/oidc", nil)); !strings.Contains(string(body), `"door":"wildcard"`) {
|
||||
t.Errorf("/v1/o11y/complete/oidc no longer reaches the wildcard: %s", body)
|
||||
}
|
||||
// ...and the typed op wins over that wildcard.
|
||||
if _, body := call(t, app, member(http.MethodGet, "/v1/o11y/users", nil)); strings.Contains(string(body), `"door":"wildcard"`) {
|
||||
t.Fatalf("the typed users op did not take precedence: %s", body)
|
||||
|
||||
// ...and a typed identity op next door still dispatches as an op.
|
||||
runtime(t, []any{})
|
||||
if status, body := call(t, app, member(http.MethodGet, "/v1/o11y/users", nil)); status != http.StatusOK {
|
||||
t.Fatalf("the typed users op did not answer: status=%d %s", status, body)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,689 +0,0 @@
|
||||
// Code generated by zipdoc; DO NOT EDIT.
|
||||
|
||||
package o11y
|
||||
|
||||
import (
|
||||
"github.com/zap-proto/zip"
|
||||
)
|
||||
|
||||
func init() {
|
||||
zip.Describe("DELETE /v1/o11y/domains/:id", zip.Doc{
|
||||
Description: "Releases an email domain and discards its SSO\nconfiguration, by id. Admin gate.",
|
||||
})
|
||||
zip.Describe("DELETE /v1/o11y/sessions", zip.Doc{
|
||||
Description: "Signs the calling session out, invalidating its tokens. The\naccess token on the call names the session to end.",
|
||||
})
|
||||
zip.Describe("DELETE /v1/o11y/user/:id", zip.Doc{
|
||||
Description: "Removes one org member, by user id. The same operation\nas deleteUser on the legacy singular path. Admin gate.",
|
||||
})
|
||||
zip.Describe("DELETE /v1/o11y/users/:id", zip.Doc{
|
||||
Description: "Removes one org member, by user id. Admin gate.",
|
||||
})
|
||||
zip.Describe("DELETE /v1/o11y/users/:id/roles/:roleId", zip.Doc{
|
||||
Description: "Takes a role away from one org member, by user id and role\nid — someone else, never the caller. Admin gate.",
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/domains", zip.Doc{
|
||||
Description: "Lists the org's auth domains — the email domains whose SSO\nconfiguration this org owns. Admin gate.",
|
||||
Fields: map[string]string{
|
||||
"O11yAttributeMapping.email": "Email is the key carrying the email; defaults to \"email\".",
|
||||
"O11yAttributeMapping.groups": "Groups is the key carrying the group list; defaults to \"groups\".",
|
||||
"O11yAttributeMapping.name": "Name is the key carrying the display name; defaults to \"name\".",
|
||||
"O11yAttributeMapping.role": "Role is the key carrying the role; defaults to \"role\".",
|
||||
"O11yAuthDomain.authNProviderInfo": "AuthNProviderInfo is provider detail the console needs to finish setup.",
|
||||
"O11yAuthDomain.config": "Config is the domain's SSO configuration.",
|
||||
"O11yAuthDomain.createdAt": "CreatedAt is when it was claimed.",
|
||||
"O11yAuthDomain.id": "ID is the auth domain id.",
|
||||
"O11yAuthDomain.name": "Name is the email domain, e.g. example.com.",
|
||||
"O11yAuthDomain.orgId": "OrgID is the org that claimed it.",
|
||||
"O11yAuthDomain.updatedAt": "UpdatedAt is when its configuration last changed.",
|
||||
"O11yAuthDomainConfig.googleAuthConfig": "Google is the Google provider's settings, when SSOType is google_auth.",
|
||||
"O11yAuthDomainConfig.oidcConfig": "OIDC is the OIDC provider's settings, when SSOType is oidc.",
|
||||
"O11yAuthDomainConfig.roleMapping": "RoleMapping maps the provider's groups onto roles for new users.",
|
||||
"O11yAuthDomainConfig.samlConfig": "SAML is the SAML provider's settings, when SSOType is saml.",
|
||||
"O11yAuthDomainConfig.ssoEnabled": "SSOEnabled turns enforced SSO on for the domain.",
|
||||
"O11yAuthDomainConfig.ssoType": "SSOType picks the provider — saml, google_auth or oidc.",
|
||||
"O11yAuthDomainsOut.data": "Data holds the domains.",
|
||||
"O11yAuthDomainsOut.status": "Status is \"success\".",
|
||||
"O11yAuthNProviderInfo.relayStatePath": "RelayStatePath is the relay-state path a SAML IdP must be configured\nwith, when the provider needs one.",
|
||||
"O11yGoogleConfig.allowedGroups": "AllowedGroups, when set, admits only members of these groups.",
|
||||
"O11yGoogleConfig.clientId": "ClientID is the OAuth application's id.",
|
||||
"O11yGoogleConfig.clientSecret": "ClientSecret is the OAuth application's secret.",
|
||||
"O11yGoogleConfig.domainToAdminEmail": "DomainToAdminEmail maps each Workspace domain to the admin the service\naccount impersonates; \"*\" is the fallback.",
|
||||
"O11yGoogleConfig.fetchGroups": "FetchGroups reads the user's Workspace groups for role mapping.",
|
||||
"O11yGoogleConfig.fetchTransitiveGroupMembership": "FetchTransitiveGroupMembership also reads groups held through other\ngroups.",
|
||||
"O11yGoogleConfig.insecureSkipEmailVerified": "InsecureSkipEmailVerified admits addresses Google has not verified.",
|
||||
"O11yGoogleConfig.redirectURI": "RedirectURI is the callback the flow returns to.",
|
||||
"O11yGoogleConfig.serviceAccountJson": "ServiceAccountJSON is the service-account credential used to read\ngroups, when FetchGroups is on.",
|
||||
"O11yOIDCConfig.claimMapping": "ClaimMapping names the token claims to read identity from.",
|
||||
"O11yOIDCConfig.clientId": "ClientID is the OAuth application's id.",
|
||||
"O11yOIDCConfig.clientSecret": "ClientSecret is the OAuth application's secret.",
|
||||
"O11yOIDCConfig.getUserInfo": "GetUserInfo also queries the userinfo endpoint, for providers whose id\ntokens are thin.",
|
||||
"O11yOIDCConfig.insecureSkipEmailVerified": "InsecureSkipEmailVerified admits addresses the provider has not\nverified.",
|
||||
"O11yOIDCConfig.issuer": "Issuer is the provider's issuer URL.",
|
||||
"O11yOIDCConfig.issuerAlias": "IssuerAlias overrides the issuer for providers whose discovery document\ndisagrees with their issuer URL.",
|
||||
"O11yRoleMapping.defaultRole": "DefaultRole is the role when no group mapping applies.",
|
||||
"O11yRoleMapping.groupMappings": "GroupMappings maps a provider group name to a role name.",
|
||||
"O11yRoleMapping.useRoleAttribute": "UseRoleAttribute reads the role straight from the provider's role claim\ninstead of the group mappings.",
|
||||
"O11ySAMLConfig.attributeMapping": "AttributeMapping names the assertion attributes to read identity from.",
|
||||
"O11ySAMLConfig.insecureSkipAuthNRequestsSigned": "InsecureSkipAuthNRequestsSigned skips signing outgoing AuthN requests,\nfor IdPs that refuse signed ones.",
|
||||
"O11ySAMLConfig.samlCert": "SamlCert is the IdP's signing certificate.",
|
||||
"O11ySAMLConfig.samlEntity": "SamlEntity is the IdP's entityID.",
|
||||
"O11ySAMLConfig.samlIdp": "SamlIdp is the IdP's single-sign-on endpoint.",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/domains/:id", zip.Doc{
|
||||
Description: "Returns one auth domain with its SSO configuration, by id.\nAdmin gate.",
|
||||
Fields: map[string]string{
|
||||
"O11yAttributeMapping.email": "Email is the key carrying the email; defaults to \"email\".",
|
||||
"O11yAttributeMapping.groups": "Groups is the key carrying the group list; defaults to \"groups\".",
|
||||
"O11yAttributeMapping.name": "Name is the key carrying the display name; defaults to \"name\".",
|
||||
"O11yAttributeMapping.role": "Role is the key carrying the role; defaults to \"role\".",
|
||||
"O11yAuthDomain.authNProviderInfo": "AuthNProviderInfo is provider detail the console needs to finish setup.",
|
||||
"O11yAuthDomain.config": "Config is the domain's SSO configuration.",
|
||||
"O11yAuthDomain.createdAt": "CreatedAt is when it was claimed.",
|
||||
"O11yAuthDomain.id": "ID is the auth domain id.",
|
||||
"O11yAuthDomain.name": "Name is the email domain, e.g. example.com.",
|
||||
"O11yAuthDomain.orgId": "OrgID is the org that claimed it.",
|
||||
"O11yAuthDomain.updatedAt": "UpdatedAt is when its configuration last changed.",
|
||||
"O11yAuthDomainConfig.googleAuthConfig": "Google is the Google provider's settings, when SSOType is google_auth.",
|
||||
"O11yAuthDomainConfig.oidcConfig": "OIDC is the OIDC provider's settings, when SSOType is oidc.",
|
||||
"O11yAuthDomainConfig.roleMapping": "RoleMapping maps the provider's groups onto roles for new users.",
|
||||
"O11yAuthDomainConfig.samlConfig": "SAML is the SAML provider's settings, when SSOType is saml.",
|
||||
"O11yAuthDomainConfig.ssoEnabled": "SSOEnabled turns enforced SSO on for the domain.",
|
||||
"O11yAuthDomainConfig.ssoType": "SSOType picks the provider — saml, google_auth or oidc.",
|
||||
"O11yAuthDomainOut.data": "Data is the domain.",
|
||||
"O11yAuthDomainOut.status": "Status is \"success\".",
|
||||
"O11yAuthNProviderInfo.relayStatePath": "RelayStatePath is the relay-state path a SAML IdP must be configured\nwith, when the provider needs one.",
|
||||
"O11yGoogleConfig.allowedGroups": "AllowedGroups, when set, admits only members of these groups.",
|
||||
"O11yGoogleConfig.clientId": "ClientID is the OAuth application's id.",
|
||||
"O11yGoogleConfig.clientSecret": "ClientSecret is the OAuth application's secret.",
|
||||
"O11yGoogleConfig.domainToAdminEmail": "DomainToAdminEmail maps each Workspace domain to the admin the service\naccount impersonates; \"*\" is the fallback.",
|
||||
"O11yGoogleConfig.fetchGroups": "FetchGroups reads the user's Workspace groups for role mapping.",
|
||||
"O11yGoogleConfig.fetchTransitiveGroupMembership": "FetchTransitiveGroupMembership also reads groups held through other\ngroups.",
|
||||
"O11yGoogleConfig.insecureSkipEmailVerified": "InsecureSkipEmailVerified admits addresses Google has not verified.",
|
||||
"O11yGoogleConfig.redirectURI": "RedirectURI is the callback the flow returns to.",
|
||||
"O11yGoogleConfig.serviceAccountJson": "ServiceAccountJSON is the service-account credential used to read\ngroups, when FetchGroups is on.",
|
||||
"O11yOIDCConfig.claimMapping": "ClaimMapping names the token claims to read identity from.",
|
||||
"O11yOIDCConfig.clientId": "ClientID is the OAuth application's id.",
|
||||
"O11yOIDCConfig.clientSecret": "ClientSecret is the OAuth application's secret.",
|
||||
"O11yOIDCConfig.getUserInfo": "GetUserInfo also queries the userinfo endpoint, for providers whose id\ntokens are thin.",
|
||||
"O11yOIDCConfig.insecureSkipEmailVerified": "InsecureSkipEmailVerified admits addresses the provider has not\nverified.",
|
||||
"O11yOIDCConfig.issuer": "Issuer is the provider's issuer URL.",
|
||||
"O11yOIDCConfig.issuerAlias": "IssuerAlias overrides the issuer for providers whose discovery document\ndisagrees with their issuer URL.",
|
||||
"O11yRoleMapping.defaultRole": "DefaultRole is the role when no group mapping applies.",
|
||||
"O11yRoleMapping.groupMappings": "GroupMappings maps a provider group name to a role name.",
|
||||
"O11yRoleMapping.useRoleAttribute": "UseRoleAttribute reads the role straight from the provider's role claim\ninstead of the group mappings.",
|
||||
"O11ySAMLConfig.attributeMapping": "AttributeMapping names the assertion attributes to read identity from.",
|
||||
"O11ySAMLConfig.insecureSkipAuthNRequestsSigned": "InsecureSkipAuthNRequestsSigned skips signing outgoing AuthN requests,\nfor IdPs that refuse signed ones.",
|
||||
"O11ySAMLConfig.samlCert": "SamlCert is the IdP's signing certificate.",
|
||||
"O11ySAMLConfig.samlEntity": "SamlEntity is the IdP's entityID.",
|
||||
"O11ySAMLConfig.samlIdp": "SamlIdp is the IdP's single-sign-on endpoint.",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/getResetPasswordToken/:id", zip.Doc{
|
||||
Description: "Returns a user's password-reset token, creating one\nif none is live. Deprecated in favor of the reset_password_tokens pair,\nwhich separates reading from minting. Admin gate.",
|
||||
Fields: map[string]string{
|
||||
"O11yResetToken.expiresAt": "ExpiresAt is when it stops working.",
|
||||
"O11yResetToken.id": "ID is the grant's id.",
|
||||
"O11yResetToken.passwordId": "PasswordID is the password record it resets.",
|
||||
"O11yResetToken.token": "Token is the secret that redeems it.",
|
||||
"O11yResetTokenOut.data": "Data is the token.",
|
||||
"O11yResetTokenOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/org/preferences", zip.Doc{
|
||||
Description: "Lists every org-scoped preference, each with its current\nand default value. Admin gate.",
|
||||
Fields: map[string]string{
|
||||
"O11yPreference.allowedScopes": "AllowedScopes are the scopes the preference may be set at — org, user.",
|
||||
"O11yPreference.allowedValues": "AllowedValues restricts a string preference to these values.",
|
||||
"O11yPreference.defaultValue": "DefaultValue is the value before anyone set one.",
|
||||
"O11yPreference.description": "Description says what the preference does.",
|
||||
"O11yPreference.name": "Name is the preference name.",
|
||||
"O11yPreference.value": "Value is the current value.",
|
||||
"O11yPreference.valueType": "ValueType is the JSON type a value must have — string, integer, float or\nboolean.",
|
||||
"O11yPreferencesOut.data": "Data holds the preferences.",
|
||||
"O11yPreferencesOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/org/preferences/:name", zip.Doc{
|
||||
Description: "Returns one org-scoped preference, by name. Admin gate.",
|
||||
Fields: map[string]string{
|
||||
"O11yPreference.allowedScopes": "AllowedScopes are the scopes the preference may be set at — org, user.",
|
||||
"O11yPreference.allowedValues": "AllowedValues restricts a string preference to these values.",
|
||||
"O11yPreference.defaultValue": "DefaultValue is the value before anyone set one.",
|
||||
"O11yPreference.description": "Description says what the preference does.",
|
||||
"O11yPreference.name": "Name is the preference name.",
|
||||
"O11yPreference.value": "Value is the current value.",
|
||||
"O11yPreference.valueType": "ValueType is the JSON type a value must have — string, integer, float or\nboolean.",
|
||||
"O11yPreferenceOut.data": "Data is the preference.",
|
||||
"O11yPreferenceOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/orgs/me", zip.Doc{
|
||||
Description: "Returns the caller's own organization. Admin gate.",
|
||||
Fields: map[string]string{
|
||||
"O11yOrganization.alias": "Alias is an alternate name the org also answers to.",
|
||||
"O11yOrganization.createdAt": "CreatedAt is when the org was created.",
|
||||
"O11yOrganization.displayName": "DisplayName is what the console shows for the org.",
|
||||
"O11yOrganization.id": "ID is the org id. On update it is ignored: the call always addresses the\ncaller's own org.",
|
||||
"O11yOrganization.key": "Key is the org's stable numeric key, derived from its id.",
|
||||
"O11yOrganization.name": "Name is the org's short name.",
|
||||
"O11yOrganization.updatedAt": "UpdatedAt is when it last changed.",
|
||||
"O11yOrganizationOut.data": "Data is the organization.",
|
||||
"O11yOrganizationOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/orgs/me/filters", zip.Doc{
|
||||
Description: "Returns the org's quick filters for every signal — the\nattribute shortlists its explorers offer as one-click filters. Viewer gate.",
|
||||
Fields: map[string]string{
|
||||
"O11yFilterKey.dataType": "DataType is the attribute's value type — string, int64, float64 or bool.",
|
||||
"O11yFilterKey.isColumn": "IsColumn marks an attribute stored as its own column.",
|
||||
"O11yFilterKey.isJSON": "IsJSON marks an attribute read out of a JSON body.",
|
||||
"O11yFilterKey.key": "Key is the attribute name.",
|
||||
"O11yFilterKey.type": "Type says where the attribute lives — tag or resource.",
|
||||
"O11yQuickFiltersOut.data": "Data holds one entry per signal.",
|
||||
"O11yQuickFiltersOut.status": "Status is \"success\".",
|
||||
"O11ySignalFilters.filters": "Filters are the attributes offered, in display order.",
|
||||
"O11ySignalFilters.signal": "Signal is the signal the filters belong to.",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/orgs/me/filters/:signal", zip.Doc{
|
||||
Description: "Returns the org's quick filters for one signal — traces,\nlogs, metrics, exceptions or api_monitoring. Viewer gate.",
|
||||
Fields: map[string]string{
|
||||
"O11yFilterKey.dataType": "DataType is the attribute's value type — string, int64, float64 or bool.",
|
||||
"O11yFilterKey.isColumn": "IsColumn marks an attribute stored as its own column.",
|
||||
"O11yFilterKey.isJSON": "IsJSON marks an attribute read out of a JSON body.",
|
||||
"O11yFilterKey.key": "Key is the attribute name.",
|
||||
"O11yFilterKey.type": "Type says where the attribute lives — tag or resource.",
|
||||
"O11ySignalFilters.filters": "Filters are the attributes offered, in display order.",
|
||||
"O11ySignalFilters.signal": "Signal is the signal the filters belong to.",
|
||||
"O11ySignalFiltersOut.data": "Data is the signal's entry.",
|
||||
"O11ySignalFiltersOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/roles/:id/users", zip.Doc{
|
||||
Description: "Returns every org member holding a role, by role id. Admin\ngate.",
|
||||
Fields: map[string]string{
|
||||
"O11yUser.createdAt": "CreatedAt is when they joined.",
|
||||
"O11yUser.displayName": "DisplayName is what the console shows for them.",
|
||||
"O11yUser.email": "Email is their address.",
|
||||
"O11yUser.id": "ID is the user id.",
|
||||
"O11yUser.isRoot": "IsRoot marks the org's root user, which cannot be deleted or demoted.",
|
||||
"O11yUser.orgId": "OrgID is the org they belong to.",
|
||||
"O11yUser.status": "Status is their lifecycle state — active, pending_invite or deleted.",
|
||||
"O11yUser.updatedAt": "UpdatedAt is when their record last changed.",
|
||||
"O11yUsersOut.data": "Data holds the members.",
|
||||
"O11yUsersOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/sessions/context", zip.Doc{
|
||||
Description: "Tells a sign-in page what an email address can do: which\norgs the address belongs to and, per org, which password and SSO routes are\nopen to it. Unauthenticated: it runs before any session exists.",
|
||||
Fields: map[string]string{
|
||||
"O11yAuthNSupport.callback": "Callback are the SSO routes; each is begun by visiting its URL.",
|
||||
"O11yAuthNSupport.password": "Password are the password routes.",
|
||||
"O11yCallbackAuthN.provider": "Provider is the route's provider — google_auth, saml or oidc.",
|
||||
"O11yCallbackAuthN.url": "URL is where the browser goes to begin the flow.",
|
||||
"O11yErrorDetail.code": "Code is the machine-readable code.",
|
||||
"O11yErrorDetail.errors": "Errors are further details, one message and its suggestions each.",
|
||||
"O11yErrorDetail.message": "Message is the human-readable reason.",
|
||||
"O11yErrorDetail.retry": "Retry says when it is worth trying again, for errors that pass.",
|
||||
"O11yErrorDetail.suggestions": "Suggestions say what to try instead.",
|
||||
"O11yErrorDetail.type": "Type is the error's category, e.g. invalid_input, not_found.",
|
||||
"O11yErrorDetail.url": "Url points at documentation for the error, when there is any.",
|
||||
"O11yErrorItem.message": "Message is the detail.",
|
||||
"O11yErrorItem.suggestions": "Suggestions say what to try about this detail.",
|
||||
"O11yPasswordAuthN.provider": "Provider is the route's provider, e.g. email_password.",
|
||||
"O11yRetry.delay": "Delay is how long to wait before retrying, in nanoseconds.",
|
||||
"O11ySessionContext.exists": "Exists says whether any account carries the address.",
|
||||
"O11ySessionContext.orgs": "Orgs are the orgs the address belongs to, each with its sign-in routes.",
|
||||
"O11ySessionContextIn.email": "Email is the address about to sign in. Required.",
|
||||
"O11ySessionContextIn.ref": "Ref is the page the sign-in started from, carried into SSO redirects.",
|
||||
"O11ySessionContextOut.data": "Data is the context.",
|
||||
"O11ySessionContextOut.status": "Status is \"success\".",
|
||||
"O11ySessionOrg.authNSupport": "AuthNSupport lists the org's open sign-in routes.",
|
||||
"O11ySessionOrg.id": "ID is the org id.",
|
||||
"O11ySessionOrg.name": "Name is the org's display name.",
|
||||
"O11ySessionOrg.warning": "Warning reports an org whose SSO is configured but not currently usable,\nin the platform's error shape.",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/user", zip.Doc{
|
||||
Description: "Lists the org's members with their single legacy role.\nDeprecated in favor of listUsers, which answers without the role. Admin gate.",
|
||||
Fields: map[string]string{
|
||||
"O11yDeprecatedUser.createdAt": "CreatedAt is when they joined.",
|
||||
"O11yDeprecatedUser.displayName": "DisplayName is what the console shows for them.",
|
||||
"O11yDeprecatedUser.email": "Email is their address.",
|
||||
"O11yDeprecatedUser.id": "ID is the user id.",
|
||||
"O11yDeprecatedUser.isRoot": "IsRoot marks the org's root user.",
|
||||
"O11yDeprecatedUser.orgId": "OrgID is the org they belong to.",
|
||||
"O11yDeprecatedUser.role": "Role is their legacy role — ADMIN, EDITOR or VIEWER.",
|
||||
"O11yDeprecatedUser.status": "Status is their lifecycle state — active, pending_invite or deleted.",
|
||||
"O11yDeprecatedUser.updatedAt": "UpdatedAt is when their record last changed.",
|
||||
"O11yDeprecatedUsersOut.data": "Data holds the members.",
|
||||
"O11yDeprecatedUsersOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/user/:id", zip.Doc{
|
||||
Description: "Returns one org member with their single legacy role, by\nuser id. Admins may read anyone; a non-admin only themselves (the runtime's\nself-access gate).",
|
||||
Fields: map[string]string{
|
||||
"O11yDeprecatedUser.createdAt": "CreatedAt is when they joined.",
|
||||
"O11yDeprecatedUser.displayName": "DisplayName is what the console shows for them.",
|
||||
"O11yDeprecatedUser.email": "Email is their address.",
|
||||
"O11yDeprecatedUser.id": "ID is the user id.",
|
||||
"O11yDeprecatedUser.isRoot": "IsRoot marks the org's root user.",
|
||||
"O11yDeprecatedUser.orgId": "OrgID is the org they belong to.",
|
||||
"O11yDeprecatedUser.role": "Role is their legacy role — ADMIN, EDITOR or VIEWER.",
|
||||
"O11yDeprecatedUser.status": "Status is their lifecycle state — active, pending_invite or deleted.",
|
||||
"O11yDeprecatedUser.updatedAt": "UpdatedAt is when their record last changed.",
|
||||
"O11yDeprecatedUserOut.data": "Data is the member.",
|
||||
"O11yDeprecatedUserOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/user/me", zip.Doc{
|
||||
Description: "Returns the calling user with their single legacy role.\nDeprecated in favor of getMyUser. Open to any authenticated caller.",
|
||||
Fields: map[string]string{
|
||||
"O11yDeprecatedUser.createdAt": "CreatedAt is when they joined.",
|
||||
"O11yDeprecatedUser.displayName": "DisplayName is what the console shows for them.",
|
||||
"O11yDeprecatedUser.email": "Email is their address.",
|
||||
"O11yDeprecatedUser.id": "ID is the user id.",
|
||||
"O11yDeprecatedUser.isRoot": "IsRoot marks the org's root user.",
|
||||
"O11yDeprecatedUser.orgId": "OrgID is the org they belong to.",
|
||||
"O11yDeprecatedUser.role": "Role is their legacy role — ADMIN, EDITOR or VIEWER.",
|
||||
"O11yDeprecatedUser.status": "Status is their lifecycle state — active, pending_invite or deleted.",
|
||||
"O11yDeprecatedUser.updatedAt": "UpdatedAt is when their record last changed.",
|
||||
"O11yDeprecatedUserOut.data": "Data is the member.",
|
||||
"O11yDeprecatedUserOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/user/preferences", zip.Doc{
|
||||
Description: "Lists every preference of the calling user, each with\nits current and default value. Viewer gate.",
|
||||
Fields: map[string]string{
|
||||
"O11yPreference.allowedScopes": "AllowedScopes are the scopes the preference may be set at — org, user.",
|
||||
"O11yPreference.allowedValues": "AllowedValues restricts a string preference to these values.",
|
||||
"O11yPreference.defaultValue": "DefaultValue is the value before anyone set one.",
|
||||
"O11yPreference.description": "Description says what the preference does.",
|
||||
"O11yPreference.name": "Name is the preference name.",
|
||||
"O11yPreference.value": "Value is the current value.",
|
||||
"O11yPreference.valueType": "ValueType is the JSON type a value must have — string, integer, float or\nboolean.",
|
||||
"O11yPreferencesOut.data": "Data holds the preferences.",
|
||||
"O11yPreferencesOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/user/preferences/:name", zip.Doc{
|
||||
Description: "Returns one preference of the calling user, by name.\nViewer gate.",
|
||||
Fields: map[string]string{
|
||||
"O11yPreference.allowedScopes": "AllowedScopes are the scopes the preference may be set at — org, user.",
|
||||
"O11yPreference.allowedValues": "AllowedValues restricts a string preference to these values.",
|
||||
"O11yPreference.defaultValue": "DefaultValue is the value before anyone set one.",
|
||||
"O11yPreference.description": "Description says what the preference does.",
|
||||
"O11yPreference.name": "Name is the preference name.",
|
||||
"O11yPreference.value": "Value is the current value.",
|
||||
"O11yPreference.valueType": "ValueType is the JSON type a value must have — string, integer, float or\nboolean.",
|
||||
"O11yPreferenceOut.data": "Data is the preference.",
|
||||
"O11yPreferenceOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/users", zip.Doc{
|
||||
Description: "Lists the caller's org members. Admin gate.",
|
||||
Fields: map[string]string{
|
||||
"O11yUser.createdAt": "CreatedAt is when they joined.",
|
||||
"O11yUser.displayName": "DisplayName is what the console shows for them.",
|
||||
"O11yUser.email": "Email is their address.",
|
||||
"O11yUser.id": "ID is the user id.",
|
||||
"O11yUser.isRoot": "IsRoot marks the org's root user, which cannot be deleted or demoted.",
|
||||
"O11yUser.orgId": "OrgID is the org they belong to.",
|
||||
"O11yUser.status": "Status is their lifecycle state — active, pending_invite or deleted.",
|
||||
"O11yUser.updatedAt": "UpdatedAt is when their record last changed.",
|
||||
"O11yUsersOut.data": "Data holds the members.",
|
||||
"O11yUsersOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/users/:id", zip.Doc{
|
||||
Description: "Returns one org member together with every role they hold, by user\nid. Admin gate.",
|
||||
Fields: map[string]string{
|
||||
"O11yRole.createdAt": "CreatedAt is when the role was created.",
|
||||
"O11yRole.description": "Description says what the role is for.",
|
||||
"O11yRole.id": "ID is the role id.",
|
||||
"O11yRole.name": "Name is the role's name.",
|
||||
"O11yRole.orgId": "OrgID is the org the role belongs to.",
|
||||
"O11yRole.type": "Type is how the role came to be — managed by the platform or custom.",
|
||||
"O11yRole.updatedAt": "UpdatedAt is when it last changed.",
|
||||
"O11yUserRole.createdAt": "CreatedAt is when it was assigned.",
|
||||
"O11yUserRole.id": "ID is the assignment's own id.",
|
||||
"O11yUserRole.role": "Role is the role itself.",
|
||||
"O11yUserRole.roleId": "RoleID is the role held.",
|
||||
"O11yUserRole.updatedAt": "UpdatedAt is when the assignment last changed.",
|
||||
"O11yUserRole.userId": "UserID is the user holding the role.",
|
||||
"O11yUserWithRoles.createdAt": "CreatedAt is when they joined.",
|
||||
"O11yUserWithRoles.displayName": "DisplayName is what the console shows for them.",
|
||||
"O11yUserWithRoles.email": "Email is their address.",
|
||||
"O11yUserWithRoles.id": "ID is the user id.",
|
||||
"O11yUserWithRoles.isRoot": "IsRoot marks the org's root user.",
|
||||
"O11yUserWithRoles.orgId": "OrgID is the org they belong to.",
|
||||
"O11yUserWithRoles.status": "Status is their lifecycle state — active, pending_invite or deleted.",
|
||||
"O11yUserWithRoles.updatedAt": "UpdatedAt is when their record last changed.",
|
||||
"O11yUserWithRoles.userRoles": "UserRoles are their role assignments.",
|
||||
"O11yUserWithRolesOut.data": "Data is the member.",
|
||||
"O11yUserWithRolesOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/users/:id/reset_password_tokens", zip.Doc{
|
||||
Description: "Returns the reset-password token a user already has; absent\none, the answer is a not-found rather than a fresh token. Admin gate.",
|
||||
Fields: map[string]string{
|
||||
"O11yResetToken.expiresAt": "ExpiresAt is when it stops working.",
|
||||
"O11yResetToken.id": "ID is the grant's id.",
|
||||
"O11yResetToken.passwordId": "PasswordID is the password record it resets.",
|
||||
"O11yResetToken.token": "Token is the secret that redeems it.",
|
||||
"O11yResetTokenOut.data": "Data is the token.",
|
||||
"O11yResetTokenOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/users/:id/roles", zip.Doc{
|
||||
Description: "Returns every role one org member holds, by user id. Admin\ngate.",
|
||||
Fields: map[string]string{
|
||||
"O11yRole.createdAt": "CreatedAt is when the role was created.",
|
||||
"O11yRole.description": "Description says what the role is for.",
|
||||
"O11yRole.id": "ID is the role id.",
|
||||
"O11yRole.name": "Name is the role's name.",
|
||||
"O11yRole.orgId": "OrgID is the org the role belongs to.",
|
||||
"O11yRole.type": "Type is how the role came to be — managed by the platform or custom.",
|
||||
"O11yRole.updatedAt": "UpdatedAt is when it last changed.",
|
||||
"O11yRolesOut.data": "Data holds the roles.",
|
||||
"O11yRolesOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/users/me", zip.Doc{
|
||||
Description: "Returns the calling user together with every role they hold. Open\nto any authenticated caller.",
|
||||
Fields: map[string]string{
|
||||
"O11yRole.createdAt": "CreatedAt is when the role was created.",
|
||||
"O11yRole.description": "Description says what the role is for.",
|
||||
"O11yRole.id": "ID is the role id.",
|
||||
"O11yRole.name": "Name is the role's name.",
|
||||
"O11yRole.orgId": "OrgID is the org the role belongs to.",
|
||||
"O11yRole.type": "Type is how the role came to be — managed by the platform or custom.",
|
||||
"O11yRole.updatedAt": "UpdatedAt is when it last changed.",
|
||||
"O11yUserRole.createdAt": "CreatedAt is when it was assigned.",
|
||||
"O11yUserRole.id": "ID is the assignment's own id.",
|
||||
"O11yUserRole.role": "Role is the role itself.",
|
||||
"O11yUserRole.roleId": "RoleID is the role held.",
|
||||
"O11yUserRole.updatedAt": "UpdatedAt is when the assignment last changed.",
|
||||
"O11yUserRole.userId": "UserID is the user holding the role.",
|
||||
"O11yUserWithRoles.createdAt": "CreatedAt is when they joined.",
|
||||
"O11yUserWithRoles.displayName": "DisplayName is what the console shows for them.",
|
||||
"O11yUserWithRoles.email": "Email is their address.",
|
||||
"O11yUserWithRoles.id": "ID is the user id.",
|
||||
"O11yUserWithRoles.isRoot": "IsRoot marks the org's root user.",
|
||||
"O11yUserWithRoles.orgId": "OrgID is the org they belong to.",
|
||||
"O11yUserWithRoles.status": "Status is their lifecycle state — active, pending_invite or deleted.",
|
||||
"O11yUserWithRoles.updatedAt": "UpdatedAt is when their record last changed.",
|
||||
"O11yUserWithRoles.userRoles": "UserRoles are their role assignments.",
|
||||
"O11yUserWithRolesOut.data": "Data is the member.",
|
||||
"O11yUserWithRolesOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/domains", zip.Doc{
|
||||
Description: "Claims an email domain for the org and configures how its\nusers sign in; the answer is the new domain's id. Admin gate.",
|
||||
Fields: map[string]string{
|
||||
"O11yAttributeMapping.email": "Email is the key carrying the email; defaults to \"email\".",
|
||||
"O11yAttributeMapping.groups": "Groups is the key carrying the group list; defaults to \"groups\".",
|
||||
"O11yAttributeMapping.name": "Name is the key carrying the display name; defaults to \"name\".",
|
||||
"O11yAttributeMapping.role": "Role is the key carrying the role; defaults to \"role\".",
|
||||
"O11yAuthDomainConfig.googleAuthConfig": "Google is the Google provider's settings, when SSOType is google_auth.",
|
||||
"O11yAuthDomainConfig.oidcConfig": "OIDC is the OIDC provider's settings, when SSOType is oidc.",
|
||||
"O11yAuthDomainConfig.roleMapping": "RoleMapping maps the provider's groups onto roles for new users.",
|
||||
"O11yAuthDomainConfig.samlConfig": "SAML is the SAML provider's settings, when SSOType is saml.",
|
||||
"O11yAuthDomainConfig.ssoEnabled": "SSOEnabled turns enforced SSO on for the domain.",
|
||||
"O11yAuthDomainConfig.ssoType": "SSOType picks the provider — saml, google_auth or oidc.",
|
||||
"O11yCreated.id": "ID is the new record's id.",
|
||||
"O11yCreatedOut.data": "Data carries the id.",
|
||||
"O11yCreatedOut.status": "Status is \"success\".",
|
||||
"O11yGoogleConfig.allowedGroups": "AllowedGroups, when set, admits only members of these groups.",
|
||||
"O11yGoogleConfig.clientId": "ClientID is the OAuth application's id.",
|
||||
"O11yGoogleConfig.clientSecret": "ClientSecret is the OAuth application's secret.",
|
||||
"O11yGoogleConfig.domainToAdminEmail": "DomainToAdminEmail maps each Workspace domain to the admin the service\naccount impersonates; \"*\" is the fallback.",
|
||||
"O11yGoogleConfig.fetchGroups": "FetchGroups reads the user's Workspace groups for role mapping.",
|
||||
"O11yGoogleConfig.fetchTransitiveGroupMembership": "FetchTransitiveGroupMembership also reads groups held through other\ngroups.",
|
||||
"O11yGoogleConfig.insecureSkipEmailVerified": "InsecureSkipEmailVerified admits addresses Google has not verified.",
|
||||
"O11yGoogleConfig.redirectURI": "RedirectURI is the callback the flow returns to.",
|
||||
"O11yGoogleConfig.serviceAccountJson": "ServiceAccountJSON is the service-account credential used to read\ngroups, when FetchGroups is on.",
|
||||
"O11yOIDCConfig.claimMapping": "ClaimMapping names the token claims to read identity from.",
|
||||
"O11yOIDCConfig.clientId": "ClientID is the OAuth application's id.",
|
||||
"O11yOIDCConfig.clientSecret": "ClientSecret is the OAuth application's secret.",
|
||||
"O11yOIDCConfig.getUserInfo": "GetUserInfo also queries the userinfo endpoint, for providers whose id\ntokens are thin.",
|
||||
"O11yOIDCConfig.insecureSkipEmailVerified": "InsecureSkipEmailVerified admits addresses the provider has not\nverified.",
|
||||
"O11yOIDCConfig.issuer": "Issuer is the provider's issuer URL.",
|
||||
"O11yOIDCConfig.issuerAlias": "IssuerAlias overrides the issuer for providers whose discovery document\ndisagrees with their issuer URL.",
|
||||
"O11yPostableAuthDomain.config": "Config is the domain's SSO configuration.",
|
||||
"O11yPostableAuthDomain.name": "Name is the email domain being claimed, e.g. example.com.",
|
||||
"O11yRoleMapping.defaultRole": "DefaultRole is the role when no group mapping applies.",
|
||||
"O11yRoleMapping.groupMappings": "GroupMappings maps a provider group name to a role name.",
|
||||
"O11yRoleMapping.useRoleAttribute": "UseRoleAttribute reads the role straight from the provider's role claim\ninstead of the group mappings.",
|
||||
"O11ySAMLConfig.attributeMapping": "AttributeMapping names the assertion attributes to read identity from.",
|
||||
"O11ySAMLConfig.insecureSkipAuthNRequestsSigned": "InsecureSkipAuthNRequestsSigned skips signing outgoing AuthN requests,\nfor IdPs that refuse signed ones.",
|
||||
"O11ySAMLConfig.samlCert": "SamlCert is the IdP's signing certificate.",
|
||||
"O11ySAMLConfig.samlEntity": "SamlEntity is the IdP's entityID.",
|
||||
"O11ySAMLConfig.samlIdp": "SamlIdp is the IdP's single-sign-on endpoint.",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/factor_password/forgot", zip.Doc{
|
||||
Description: "Starts the forgotten-password flow: the named user is mailed\na reset link. Unauthenticated by design, and deliberately quiet about\nwhether the address exists.",
|
||||
Fields: map[string]string{
|
||||
"O11yForgotPasswordIn.email": "Email is the address to mail the reset link to. Required.",
|
||||
"O11yForgotPasswordIn.frontendBaseURL": "FrontendBaseURL is the console origin the reset link is built on.",
|
||||
"O11yForgotPasswordIn.orgId": "OrgID is the org the address belongs to. Required.",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/invite", zip.Doc{
|
||||
Description: "Invites one person to the caller's org by email, with the role\nthey will hold when they accept. Deprecated in favor of creating users\ndirectly; kept because callers still hold it. Admin gate, enforced by the\nruntime this op relays to.",
|
||||
Fields: map[string]string{
|
||||
"O11yInvite.createdAt": "CreatedAt is when it was created.",
|
||||
"O11yInvite.email": "Email is the address it was sent to.",
|
||||
"O11yInvite.id": "ID is the invitation's id.",
|
||||
"O11yInvite.inviteLink": "InviteLink is the full link mailed to them.",
|
||||
"O11yInvite.name": "Name is the invitee's display name.",
|
||||
"O11yInvite.orgId": "OrgID is the org they are invited into.",
|
||||
"O11yInvite.role": "Role is the role the invitee will hold.",
|
||||
"O11yInvite.token": "Token is the secret that redeems it.",
|
||||
"O11yInvite.updatedAt": "UpdatedAt is when it last changed.",
|
||||
"O11yInviteIn.email": "Email is the address the invitation goes to.",
|
||||
"O11yInviteIn.frontendBaseUrl": "FrontendBaseUrl is the console origin the invite link is built on.",
|
||||
"O11yInviteIn.name": "Name is the invitee's display name.",
|
||||
"O11yInviteIn.role": "Role is the role they will hold on accepting — ADMIN, EDITOR or VIEWER.",
|
||||
"O11yInviteOut.data": "Data is the invitation.",
|
||||
"O11yInviteOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/invite/bulk", zip.Doc{
|
||||
Description: "Invites several people to the caller's org in one call,\nrefusing the whole batch when any email repeats. Deprecated alongside\ncreateInvite. Admin gate.",
|
||||
Fields: map[string]string{
|
||||
"O11yAck.status": "Status is \"success\".",
|
||||
"O11yBulkInviteIn.invites": "Invites are the invitations to create; an email may appear only once.",
|
||||
"O11yInviteIn.email": "Email is the address the invitation goes to.",
|
||||
"O11yInviteIn.frontendBaseUrl": "FrontendBaseUrl is the console origin the invite link is built on.",
|
||||
"O11yInviteIn.name": "Name is the invitee's display name.",
|
||||
"O11yInviteIn.role": "Role is the role they will hold on accepting — ADMIN, EDITOR or VIEWER.",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/resetPassword", zip.Doc{
|
||||
Description: "Sets a new password for whoever the reset token was minted\nfor, consuming the token. Unauthenticated: the token is the proof.",
|
||||
Fields: map[string]string{
|
||||
"O11yResetPasswordIn.password": "Password is the new password.",
|
||||
"O11yResetPasswordIn.token": "Token is the reset-password token authorizing the change.",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/reset_password_tokens/verify", zip.Doc{
|
||||
Description: "Checks that a reset-password token exists and has not\nexpired, without consuming it. Unauthenticated: the token is the proof.",
|
||||
Fields: map[string]string{
|
||||
"O11yResetTokenRef.token": "Token is the reset-password token.",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/sessions/email_password", zip.Doc{
|
||||
Description: "Signs a user in with email and password and\nanswers with the session's token pair. Unauthenticated: this call is how\nauthentication begins.",
|
||||
Fields: map[string]string{
|
||||
"O11yEmailPasswordSessionIn.email": "Email is the account's address. Required.",
|
||||
"O11yEmailPasswordSessionIn.orgId": "OrgID picks the org to sign into when the address belongs to several.",
|
||||
"O11yEmailPasswordSessionIn.password": "Password is the account's password. Required.",
|
||||
"O11yToken.accessToken": "AccessToken authenticates requests until it expires.",
|
||||
"O11yToken.expiresIn": "ExpiresIn is the access token's lifetime in seconds.",
|
||||
"O11yToken.refreshToken": "RefreshToken buys the next pair via rotateSession.",
|
||||
"O11yToken.tokenType": "TokenType is how to present the access token, e.g. bearer.",
|
||||
"O11yTokenOut.data": "Data is the pair.",
|
||||
"O11yTokenOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/sessions/rotate", zip.Doc{
|
||||
Description: "Exchanges a refresh token for a fresh token pair, retiring the\nold pair. The access token being rotated identifies the session.",
|
||||
Fields: map[string]string{
|
||||
"O11yRotateSessionIn.refreshToken": "RefreshToken is the refresh token being redeemed.",
|
||||
"O11yToken.accessToken": "AccessToken authenticates requests until it expires.",
|
||||
"O11yToken.expiresIn": "ExpiresIn is the access token's lifetime in seconds.",
|
||||
"O11yToken.refreshToken": "RefreshToken buys the next pair via rotateSession.",
|
||||
"O11yToken.tokenType": "TokenType is how to present the access token, e.g. bearer.",
|
||||
"O11yTokenOut.data": "Data is the pair.",
|
||||
"O11yTokenOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/users", zip.Doc{
|
||||
Description: "Creates a member of the caller's org in the pending-invite state\nand mails them their invitation; the answer is the new user's id. Admin gate.",
|
||||
Fields: map[string]string{
|
||||
"O11yCreated.id": "ID is the new record's id.",
|
||||
"O11yCreatedOut.data": "Data carries the id.",
|
||||
"O11yCreatedOut.status": "Status is \"success\".",
|
||||
"O11yPostableUser.displayName": "DisplayName is the new member's display name.",
|
||||
"O11yPostableUser.email": "Email is the new member's address. Required.",
|
||||
"O11yPostableUser.frontendBaseUrl": "FrontendBaseUrl is the console origin the invite link is built on.",
|
||||
"O11yPostableUser.userRoles": "UserRoles are the roles the member starts with, each by id.",
|
||||
"O11yRoleID.id": "ID is the role id.",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/users/:id/roles", zip.Doc{
|
||||
Description: "Assigns a role, by role name, to one org member — someone else,\nnever the caller. Admin gate.",
|
||||
Fields: map[string]string{
|
||||
"O11yAck.status": "Status is \"success\".",
|
||||
"O11ySetRoleIn.name": "Name is the role name to assign.",
|
||||
},
|
||||
})
|
||||
zip.Describe("PUT /v1/o11y/domains/:id", zip.Doc{
|
||||
Description: "Replaces one auth domain's SSO configuration, by id. Admin\ngate.",
|
||||
Fields: map[string]string{
|
||||
"O11yAttributeMapping.email": "Email is the key carrying the email; defaults to \"email\".",
|
||||
"O11yAttributeMapping.groups": "Groups is the key carrying the group list; defaults to \"groups\".",
|
||||
"O11yAttributeMapping.name": "Name is the key carrying the display name; defaults to \"name\".",
|
||||
"O11yAttributeMapping.role": "Role is the key carrying the role; defaults to \"role\".",
|
||||
"O11yAuthDomainConfig.googleAuthConfig": "Google is the Google provider's settings, when SSOType is google_auth.",
|
||||
"O11yAuthDomainConfig.oidcConfig": "OIDC is the OIDC provider's settings, when SSOType is oidc.",
|
||||
"O11yAuthDomainConfig.roleMapping": "RoleMapping maps the provider's groups onto roles for new users.",
|
||||
"O11yAuthDomainConfig.samlConfig": "SAML is the SAML provider's settings, when SSOType is saml.",
|
||||
"O11yAuthDomainConfig.ssoEnabled": "SSOEnabled turns enforced SSO on for the domain.",
|
||||
"O11yAuthDomainConfig.ssoType": "SSOType picks the provider — saml, google_auth or oidc.",
|
||||
"O11yGoogleConfig.allowedGroups": "AllowedGroups, when set, admits only members of these groups.",
|
||||
"O11yGoogleConfig.clientId": "ClientID is the OAuth application's id.",
|
||||
"O11yGoogleConfig.clientSecret": "ClientSecret is the OAuth application's secret.",
|
||||
"O11yGoogleConfig.domainToAdminEmail": "DomainToAdminEmail maps each Workspace domain to the admin the service\naccount impersonates; \"*\" is the fallback.",
|
||||
"O11yGoogleConfig.fetchGroups": "FetchGroups reads the user's Workspace groups for role mapping.",
|
||||
"O11yGoogleConfig.fetchTransitiveGroupMembership": "FetchTransitiveGroupMembership also reads groups held through other\ngroups.",
|
||||
"O11yGoogleConfig.insecureSkipEmailVerified": "InsecureSkipEmailVerified admits addresses Google has not verified.",
|
||||
"O11yGoogleConfig.redirectURI": "RedirectURI is the callback the flow returns to.",
|
||||
"O11yGoogleConfig.serviceAccountJson": "ServiceAccountJSON is the service-account credential used to read\ngroups, when FetchGroups is on.",
|
||||
"O11yOIDCConfig.claimMapping": "ClaimMapping names the token claims to read identity from.",
|
||||
"O11yOIDCConfig.clientId": "ClientID is the OAuth application's id.",
|
||||
"O11yOIDCConfig.clientSecret": "ClientSecret is the OAuth application's secret.",
|
||||
"O11yOIDCConfig.getUserInfo": "GetUserInfo also queries the userinfo endpoint, for providers whose id\ntokens are thin.",
|
||||
"O11yOIDCConfig.insecureSkipEmailVerified": "InsecureSkipEmailVerified admits addresses the provider has not\nverified.",
|
||||
"O11yOIDCConfig.issuer": "Issuer is the provider's issuer URL.",
|
||||
"O11yOIDCConfig.issuerAlias": "IssuerAlias overrides the issuer for providers whose discovery document\ndisagrees with their issuer URL.",
|
||||
"O11yRoleMapping.defaultRole": "DefaultRole is the role when no group mapping applies.",
|
||||
"O11yRoleMapping.groupMappings": "GroupMappings maps a provider group name to a role name.",
|
||||
"O11yRoleMapping.useRoleAttribute": "UseRoleAttribute reads the role straight from the provider's role claim\ninstead of the group mappings.",
|
||||
"O11ySAMLConfig.attributeMapping": "AttributeMapping names the assertion attributes to read identity from.",
|
||||
"O11ySAMLConfig.insecureSkipAuthNRequestsSigned": "InsecureSkipAuthNRequestsSigned skips signing outgoing AuthN requests,\nfor IdPs that refuse signed ones.",
|
||||
"O11ySAMLConfig.samlCert": "SamlCert is the IdP's signing certificate.",
|
||||
"O11ySAMLConfig.samlEntity": "SamlEntity is the IdP's entityID.",
|
||||
"O11ySAMLConfig.samlIdp": "SamlIdp is the IdP's single-sign-on endpoint.",
|
||||
"O11yUpdatableAuthDomain.config": "Config is the SSO configuration to store.",
|
||||
},
|
||||
})
|
||||
zip.Describe("PUT /v1/o11y/org/preferences/:name", zip.Doc{
|
||||
Description: "Sets one org-scoped preference, by name. Admin gate.",
|
||||
Fields: map[string]string{
|
||||
"O11yUpdatablePreference.value": "Value is the value to set; its JSON type must match the preference's\ndeclared value type.",
|
||||
},
|
||||
})
|
||||
zip.Describe("PUT /v1/o11y/orgs/me", zip.Doc{
|
||||
Description: "Rewrites the caller's own organization record — display name,\nname, alias — always addressed as \"me\", never by id. Admin gate.",
|
||||
Fields: map[string]string{
|
||||
"O11yOrganization.alias": "Alias is an alternate name the org also answers to.",
|
||||
"O11yOrganization.createdAt": "CreatedAt is when the org was created.",
|
||||
"O11yOrganization.displayName": "DisplayName is what the console shows for the org.",
|
||||
"O11yOrganization.id": "ID is the org id. On update it is ignored: the call always addresses the\ncaller's own org.",
|
||||
"O11yOrganization.key": "Key is the org's stable numeric key, derived from its id.",
|
||||
"O11yOrganization.name": "Name is the org's short name.",
|
||||
"O11yOrganization.updatedAt": "UpdatedAt is when it last changed.",
|
||||
},
|
||||
})
|
||||
zip.Describe("PUT /v1/o11y/orgs/me/filters", zip.Doc{
|
||||
Description: "Replaces the org's quick filters for one signal with the\nattribute list given. Admin gate.",
|
||||
Fields: map[string]string{
|
||||
"O11yFilterKey.dataType": "DataType is the attribute's value type — string, int64, float64 or bool.",
|
||||
"O11yFilterKey.isColumn": "IsColumn marks an attribute stored as its own column.",
|
||||
"O11yFilterKey.isJSON": "IsJSON marks an attribute read out of a JSON body.",
|
||||
"O11yFilterKey.key": "Key is the attribute name.",
|
||||
"O11yFilterKey.type": "Type says where the attribute lives — tag or resource.",
|
||||
"O11yUpdatableQuickFilters.filters": "Filters are the attributes to offer, in the order to offer them.",
|
||||
"O11yUpdatableQuickFilters.signal": "Signal is the signal whose filters are being replaced.",
|
||||
},
|
||||
})
|
||||
zip.Describe("PUT /v1/o11y/user/:id", zip.Doc{
|
||||
Description: "Renames one org member and may move their legacy role,\nanswering with the updated record. Admins may update anyone; a non-admin\nonly themselves (the runtime's self-access gate).",
|
||||
Fields: map[string]string{
|
||||
"O11yDeprecatedUser.createdAt": "CreatedAt is when they joined.",
|
||||
"O11yDeprecatedUser.displayName": "DisplayName is what the console shows for them.",
|
||||
"O11yDeprecatedUser.email": "Email is their address.",
|
||||
"O11yDeprecatedUser.id": "ID is the user id.",
|
||||
"O11yDeprecatedUser.isRoot": "IsRoot marks the org's root user.",
|
||||
"O11yDeprecatedUser.orgId": "OrgID is the org they belong to.",
|
||||
"O11yDeprecatedUser.role": "Role is their legacy role — ADMIN, EDITOR or VIEWER.",
|
||||
"O11yDeprecatedUser.status": "Status is their lifecycle state — active, pending_invite or deleted.",
|
||||
"O11yDeprecatedUser.updatedAt": "UpdatedAt is when their record last changed.",
|
||||
"O11yDeprecatedUserOut.data": "Data is the member.",
|
||||
"O11yDeprecatedUserOut.status": "Status is \"success\".",
|
||||
"O11yDeprecatedUserUpdate.displayName": "DisplayName is the new display name; empty leaves it unchanged.",
|
||||
"O11yDeprecatedUserUpdate.role": "Role is the legacy role to move to — ADMIN, EDITOR or VIEWER; empty\nleaves it unchanged.",
|
||||
},
|
||||
})
|
||||
zip.Describe("PUT /v1/o11y/user/preferences/:name", zip.Doc{
|
||||
Description: "Sets one preference of the calling user, by name.\nViewer gate.",
|
||||
Fields: map[string]string{
|
||||
"O11yUpdatablePreference.value": "Value is the value to set; its JSON type must match the preference's\ndeclared value type.",
|
||||
},
|
||||
})
|
||||
zip.Describe("PUT /v1/o11y/users/:id", zip.Doc{
|
||||
Description: "Renames one org member, by user id — someone else, never the\ncaller, who renames themselves through updateMyUser. Admin gate.",
|
||||
Fields: map[string]string{
|
||||
"O11yUserUpdate.displayName": "DisplayName is the new display name.",
|
||||
},
|
||||
})
|
||||
zip.Describe("PUT /v1/o11y/users/:id/reset_password_tokens", zip.Doc{
|
||||
Description: "Creates or regenerates a user's reset-password token: a\nlive token is returned as it is, an expired one is replaced. Admin gate.",
|
||||
Fields: map[string]string{
|
||||
"O11yResetToken.expiresAt": "ExpiresAt is when it stops working.",
|
||||
"O11yResetToken.id": "ID is the grant's id.",
|
||||
"O11yResetToken.passwordId": "PasswordID is the password record it resets.",
|
||||
"O11yResetToken.token": "Token is the secret that redeems it.",
|
||||
"O11yResetTokenOut.data": "Data is the token.",
|
||||
"O11yResetTokenOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("PUT /v1/o11y/users/me", zip.Doc{
|
||||
Description: "Renames the calling user. Open to any authenticated caller.",
|
||||
Fields: map[string]string{
|
||||
"O11yUpdatableUser.displayName": "DisplayName is the new display name.",
|
||||
},
|
||||
})
|
||||
zip.Describe("PUT /v1/o11y/users/me/factor_password", zip.Doc{
|
||||
Description: "Replaces the calling user's password, refusing when the old\none does not match. Open to any authenticated caller.",
|
||||
Fields: map[string]string{
|
||||
"O11yChangePasswordIn.newPassword": "NewPassword is the password to set.",
|
||||
"O11yChangePasswordIn.oldPassword": "OldPassword is the current password; the change is refused when it does\nnot match.",
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -45,16 +45,12 @@ import (
|
||||
"github.com/zap-proto/zip"
|
||||
)
|
||||
|
||||
// infraPrefix is the infra face's path root, spelled ONCE: the group the ops
|
||||
// register on, and the first segment infraRelay rebuilds a call onto.
|
||||
const infraPrefix = "/v1/o11y"
|
||||
|
||||
// mountInfra registers the forty-four typed infra ops on the native router:
|
||||
// three per resource face (attribute_keys, attribute_values, list) for the
|
||||
// eleven infra resources, the ten infra_monitoring rollups, and the setup
|
||||
// checks.
|
||||
func mountInfra(app *zip.App) {
|
||||
g := app.Group(infraPrefix)
|
||||
g := app.Group(o11yRoot)
|
||||
|
||||
zip.Get(g, "/hosts/attribute_keys", hostAttributeKeys)
|
||||
zip.Get(g, "/hosts/attribute_values", hostAttributeValues)
|
||||
@@ -819,7 +815,7 @@ func infraRelay(ctx context.Context, method, path string, params url.Values, bod
|
||||
return zip.Errorf(http.StatusServiceUnavailable, "o11y runtime not initialized")
|
||||
}
|
||||
|
||||
target := infraPrefix + path
|
||||
target := o11yRoot + path
|
||||
if q := params.Encode(); q != "" {
|
||||
target += "?" + q
|
||||
}
|
||||
|
||||
+18
-7
@@ -309,6 +309,10 @@ func TestInfraRoutesAreTheSameFortyFour(t *testing.T) {
|
||||
// sole owner of it. The explicit infraList literal is the real anchor.
|
||||
!strings.HasPrefix(r.Path, "/v1/o11y/services") &&
|
||||
!strings.Contains(r.Path, "third-party-apis") &&
|
||||
// trace-funnels owns a /list too (tracefunnels.go). Same lesson the
|
||||
// APM census learned about /service: a suffix is only unambiguous
|
||||
// while one face owns it.
|
||||
!strings.HasPrefix(r.Path, "/v1/o11y/trace-funnels") &&
|
||||
(strings.Contains(r.Path, "attribute_") || infraList[r.Path] ||
|
||||
strings.HasSuffix(r.Path, "/list") || strings.Contains(r.Path, "infra_monitoring")) {
|
||||
got[r.Method+" "+r.Path] = true
|
||||
@@ -326,9 +330,11 @@ func TestInfraRoutesAreTheSameFortyFour(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Everything else under /v1/o11y still reaches the runtime through the
|
||||
// wildcard, and the typed paths take precedence over it.
|
||||
func TestTheRestOfO11yStillReachesTheRuntime(t *testing.T) {
|
||||
// A path no slice names is NOT served — there is no wildcard behind these ops
|
||||
// any more — while a typed path reaches the runtime with the path untouched.
|
||||
// Both halves in one test: the interesting failure is a typed op that quietly
|
||||
// stops dispatching and gets answered by something else.
|
||||
func TestUnnamedPathsAreNotServedButTypedOnesReachTheRuntime(t *testing.T) {
|
||||
app := mounted(t)
|
||||
var askedPath string
|
||||
o11y.SetHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -338,10 +344,15 @@ func TestTheRestOfO11yStillReachesTheRuntime(t *testing.T) {
|
||||
}))
|
||||
t.Cleanup(func() { o11y.SetHandler(nil) })
|
||||
|
||||
// A path no slice types: still delegated, path untouched, bytes untouched.
|
||||
_, body := call(t, app, member(http.MethodGet, "/v1/o11y/never-a-typed-op/at-all", nil))
|
||||
if !strings.Contains(string(body), `"path":"/v1/o11y/never-a-typed-op/at-all"`) {
|
||||
t.Fatalf("the wildcard no longer delegates: %s", body)
|
||||
// A path no slice names: 404, and the runtime is never asked. Under the old
|
||||
// catch-all this same request was answered, which is exactly why an unmounted
|
||||
// slice could go unnoticed for a release.
|
||||
askedPath = ""
|
||||
if status, body := call(t, app, member(http.MethodGet, "/v1/o11y/never-a-typed-op/at-all", nil)); status != http.StatusNotFound {
|
||||
t.Fatalf("status=%d want 404: %s", status, body)
|
||||
}
|
||||
if askedPath != "" {
|
||||
t.Fatalf("an unnamed path still reached the runtime as %q", askedPath)
|
||||
}
|
||||
// A typed path: the op dispatches, and what reaches the runtime is the
|
||||
// SAME path — the op is a naming of the route, not a move of it.
|
||||
|
||||
-540
@@ -1,540 +0,0 @@
|
||||
// Code generated by zipdoc; DO NOT EDIT.
|
||||
//
|
||||
// The infra slice's entries, split out of zipdoc_gen.go so concurrent slices
|
||||
// do not contend on one generated file; regenerating the package folds them
|
||||
// back together, which is the intended end state.
|
||||
|
||||
package o11y
|
||||
|
||||
import (
|
||||
"github.com/zap-proto/zip"
|
||||
)
|
||||
|
||||
func init() {
|
||||
zip.Describe("GET /v1/o11y/clusters/attribute_keys", zip.Doc{
|
||||
Description: "Lists the metric attribute keys Kubernetes clusters\nreport, for building cluster filters.",
|
||||
Fields: map[string]string{
|
||||
"O11yInfraAttributeKeysIn.aggregateAttribute": "AggregateAttribute is the metric the keys must appear on.",
|
||||
"O11yInfraAttributeKeysIn.aggregateOperator": "AggregateOperator is the aggregation the keys will be used under, e.g.\nnoop, count, avg. The runtime requires it for non-metrics sources.",
|
||||
"O11yInfraAttributeKeysIn.dataSource": "DataSource is the telemetry the keys come from — metrics for the infra\nfaces. The runtime requires it.",
|
||||
"O11yInfraAttributeKeysIn.limit": "Limit caps how many keys come back. Absent means 50.",
|
||||
"O11yInfraAttributeKeysIn.searchText": "SearchText narrows the keys to those containing it.",
|
||||
"O11yInfraAttributeKeysIn.tagType": "TagType narrows the keys to one kind — tag or resource. Empty means all;\nan invalid value reads as empty.",
|
||||
"O11yInfraAttributeKeysOut.data": "Data holds the keys.",
|
||||
"O11yInfraAttributeKeysOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/clusters/attribute_values", zip.Doc{
|
||||
Description: "Lists the values one cluster attribute key has taken,\nfor building cluster filters.",
|
||||
Fields: map[string]string{
|
||||
"O11yInfraAttributeValuesIn.aggregateAttribute": "AggregateAttribute is the metric the values must appear on.",
|
||||
"O11yInfraAttributeValuesIn.aggregateOperator": "AggregateOperator is the aggregation the values will be used under, e.g.\nnoop, count, avg. The runtime requires it for non-metrics sources.",
|
||||
"O11yInfraAttributeValuesIn.attributeKey": "AttributeKey is the key whose values to list.",
|
||||
"O11yInfraAttributeValuesIn.dataSource": "DataSource is the telemetry the values come from — metrics for the infra\nfaces. The runtime requires it.",
|
||||
"O11yInfraAttributeValuesIn.filterAttributeKeyDataType": "FilterAttributeKeyDataType is the key's data type — string, int64,\nfloat64 or bool. Empty means unspecified.",
|
||||
"O11yInfraAttributeValuesIn.limit": "Limit caps how many values come back. Absent means 50.",
|
||||
"O11yInfraAttributeValuesIn.searchText": "SearchText narrows the values to those containing it.",
|
||||
"O11yInfraAttributeValuesIn.tagType": "TagType narrows the search to one kind of key — tag or resource.",
|
||||
"O11yInfraAttributeValuesOut.data": "Data holds the values, split by data type.",
|
||||
"O11yInfraAttributeValuesOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/daemonsets/attribute_keys", zip.Doc{
|
||||
Description: "Lists the metric attribute keys Kubernetes daemonsets\nreport, for building daemonset filters.",
|
||||
Fields: map[string]string{
|
||||
"O11yInfraAttributeKeysIn.aggregateAttribute": "AggregateAttribute is the metric the keys must appear on.",
|
||||
"O11yInfraAttributeKeysIn.aggregateOperator": "AggregateOperator is the aggregation the keys will be used under, e.g.\nnoop, count, avg. The runtime requires it for non-metrics sources.",
|
||||
"O11yInfraAttributeKeysIn.dataSource": "DataSource is the telemetry the keys come from — metrics for the infra\nfaces. The runtime requires it.",
|
||||
"O11yInfraAttributeKeysIn.limit": "Limit caps how many keys come back. Absent means 50.",
|
||||
"O11yInfraAttributeKeysIn.searchText": "SearchText narrows the keys to those containing it.",
|
||||
"O11yInfraAttributeKeysIn.tagType": "TagType narrows the keys to one kind — tag or resource. Empty means all;\nan invalid value reads as empty.",
|
||||
"O11yInfraAttributeKeysOut.data": "Data holds the keys.",
|
||||
"O11yInfraAttributeKeysOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/daemonsets/attribute_values", zip.Doc{
|
||||
Description: "Lists the values one daemonset attribute key has\ntaken, for building daemonset filters.",
|
||||
Fields: map[string]string{
|
||||
"O11yInfraAttributeValuesIn.aggregateAttribute": "AggregateAttribute is the metric the values must appear on.",
|
||||
"O11yInfraAttributeValuesIn.aggregateOperator": "AggregateOperator is the aggregation the values will be used under, e.g.\nnoop, count, avg. The runtime requires it for non-metrics sources.",
|
||||
"O11yInfraAttributeValuesIn.attributeKey": "AttributeKey is the key whose values to list.",
|
||||
"O11yInfraAttributeValuesIn.dataSource": "DataSource is the telemetry the values come from — metrics for the infra\nfaces. The runtime requires it.",
|
||||
"O11yInfraAttributeValuesIn.filterAttributeKeyDataType": "FilterAttributeKeyDataType is the key's data type — string, int64,\nfloat64 or bool. Empty means unspecified.",
|
||||
"O11yInfraAttributeValuesIn.limit": "Limit caps how many values come back. Absent means 50.",
|
||||
"O11yInfraAttributeValuesIn.searchText": "SearchText narrows the values to those containing it.",
|
||||
"O11yInfraAttributeValuesIn.tagType": "TagType narrows the search to one kind of key — tag or resource.",
|
||||
"O11yInfraAttributeValuesOut.data": "Data holds the values, split by data type.",
|
||||
"O11yInfraAttributeValuesOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/deployments/attribute_keys", zip.Doc{
|
||||
Description: "Lists the metric attribute keys Kubernetes\ndeployments report, for building deployment filters.",
|
||||
Fields: map[string]string{
|
||||
"O11yInfraAttributeKeysIn.aggregateAttribute": "AggregateAttribute is the metric the keys must appear on.",
|
||||
"O11yInfraAttributeKeysIn.aggregateOperator": "AggregateOperator is the aggregation the keys will be used under, e.g.\nnoop, count, avg. The runtime requires it for non-metrics sources.",
|
||||
"O11yInfraAttributeKeysIn.dataSource": "DataSource is the telemetry the keys come from — metrics for the infra\nfaces. The runtime requires it.",
|
||||
"O11yInfraAttributeKeysIn.limit": "Limit caps how many keys come back. Absent means 50.",
|
||||
"O11yInfraAttributeKeysIn.searchText": "SearchText narrows the keys to those containing it.",
|
||||
"O11yInfraAttributeKeysIn.tagType": "TagType narrows the keys to one kind — tag or resource. Empty means all;\nan invalid value reads as empty.",
|
||||
"O11yInfraAttributeKeysOut.data": "Data holds the keys.",
|
||||
"O11yInfraAttributeKeysOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/deployments/attribute_values", zip.Doc{
|
||||
Description: "Lists the values one deployment attribute key has\ntaken, for building deployment filters.",
|
||||
Fields: map[string]string{
|
||||
"O11yInfraAttributeValuesIn.aggregateAttribute": "AggregateAttribute is the metric the values must appear on.",
|
||||
"O11yInfraAttributeValuesIn.aggregateOperator": "AggregateOperator is the aggregation the values will be used under, e.g.\nnoop, count, avg. The runtime requires it for non-metrics sources.",
|
||||
"O11yInfraAttributeValuesIn.attributeKey": "AttributeKey is the key whose values to list.",
|
||||
"O11yInfraAttributeValuesIn.dataSource": "DataSource is the telemetry the values come from — metrics for the infra\nfaces. The runtime requires it.",
|
||||
"O11yInfraAttributeValuesIn.filterAttributeKeyDataType": "FilterAttributeKeyDataType is the key's data type — string, int64,\nfloat64 or bool. Empty means unspecified.",
|
||||
"O11yInfraAttributeValuesIn.limit": "Limit caps how many values come back. Absent means 50.",
|
||||
"O11yInfraAttributeValuesIn.searchText": "SearchText narrows the values to those containing it.",
|
||||
"O11yInfraAttributeValuesIn.tagType": "TagType narrows the search to one kind of key — tag or resource.",
|
||||
"O11yInfraAttributeValuesOut.data": "Data holds the values, split by data type.",
|
||||
"O11yInfraAttributeValuesOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/hosts/attribute_keys", zip.Doc{
|
||||
Description: "Lists the metric attribute keys hosts report, for building\nhost filters — each with its data type and whether it is a materialized\ncolumn.",
|
||||
Fields: map[string]string{
|
||||
"O11yInfraAttributeKeysIn.aggregateAttribute": "AggregateAttribute is the metric the keys must appear on.",
|
||||
"O11yInfraAttributeKeysIn.aggregateOperator": "AggregateOperator is the aggregation the keys will be used under, e.g.\nnoop, count, avg. The runtime requires it for non-metrics sources.",
|
||||
"O11yInfraAttributeKeysIn.dataSource": "DataSource is the telemetry the keys come from — metrics for the infra\nfaces. The runtime requires it.",
|
||||
"O11yInfraAttributeKeysIn.limit": "Limit caps how many keys come back. Absent means 50.",
|
||||
"O11yInfraAttributeKeysIn.searchText": "SearchText narrows the keys to those containing it.",
|
||||
"O11yInfraAttributeKeysIn.tagType": "TagType narrows the keys to one kind — tag or resource. Empty means all;\nan invalid value reads as empty.",
|
||||
"O11yInfraAttributeKeysOut.data": "Data holds the keys.",
|
||||
"O11yInfraAttributeKeysOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/hosts/attribute_values", zip.Doc{
|
||||
Description: "Lists the values one host attribute key has taken, for\nbuilding host filters — string, number and bool values in their own lists.",
|
||||
Fields: map[string]string{
|
||||
"O11yInfraAttributeValuesIn.aggregateAttribute": "AggregateAttribute is the metric the values must appear on.",
|
||||
"O11yInfraAttributeValuesIn.aggregateOperator": "AggregateOperator is the aggregation the values will be used under, e.g.\nnoop, count, avg. The runtime requires it for non-metrics sources.",
|
||||
"O11yInfraAttributeValuesIn.attributeKey": "AttributeKey is the key whose values to list.",
|
||||
"O11yInfraAttributeValuesIn.dataSource": "DataSource is the telemetry the values come from — metrics for the infra\nfaces. The runtime requires it.",
|
||||
"O11yInfraAttributeValuesIn.filterAttributeKeyDataType": "FilterAttributeKeyDataType is the key's data type — string, int64,\nfloat64 or bool. Empty means unspecified.",
|
||||
"O11yInfraAttributeValuesIn.limit": "Limit caps how many values come back. Absent means 50.",
|
||||
"O11yInfraAttributeValuesIn.searchText": "SearchText narrows the values to those containing it.",
|
||||
"O11yInfraAttributeValuesIn.tagType": "TagType narrows the search to one kind of key — tag or resource.",
|
||||
"O11yInfraAttributeValuesOut.data": "Data holds the values, split by data type.",
|
||||
"O11yInfraAttributeValuesOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/infra_monitoring/checks", zip.Doc{
|
||||
Description: "Reports whether the metrics and attributes an infra-monitoring\nsection needs are being received — for each collector receiver or processor\ninvolved, what is present and what is missing, with a user-facing message\nand a docs link per missing piece. Ready is true only when nothing is\nmissing.",
|
||||
Fields: map[string]string{
|
||||
"O11yInfraChecksIn.type": "Type is the section to check — hosts, processes, pods, nodes,\ndeployments, daemonsets, statefulsets, jobs, namespaces, clusters or\nvolumes. Required.",
|
||||
"O11yInfraChecksOut.data": "Data holds what is present, what is missing and whether the section is\nready.",
|
||||
"O11yInfraChecksOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/jobs/attribute_keys", zip.Doc{
|
||||
Description: "Lists the metric attribute keys Kubernetes jobs report, for\nbuilding job filters.",
|
||||
Fields: map[string]string{
|
||||
"O11yInfraAttributeKeysIn.aggregateAttribute": "AggregateAttribute is the metric the keys must appear on.",
|
||||
"O11yInfraAttributeKeysIn.aggregateOperator": "AggregateOperator is the aggregation the keys will be used under, e.g.\nnoop, count, avg. The runtime requires it for non-metrics sources.",
|
||||
"O11yInfraAttributeKeysIn.dataSource": "DataSource is the telemetry the keys come from — metrics for the infra\nfaces. The runtime requires it.",
|
||||
"O11yInfraAttributeKeysIn.limit": "Limit caps how many keys come back. Absent means 50.",
|
||||
"O11yInfraAttributeKeysIn.searchText": "SearchText narrows the keys to those containing it.",
|
||||
"O11yInfraAttributeKeysIn.tagType": "TagType narrows the keys to one kind — tag or resource. Empty means all;\nan invalid value reads as empty.",
|
||||
"O11yInfraAttributeKeysOut.data": "Data holds the keys.",
|
||||
"O11yInfraAttributeKeysOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/jobs/attribute_values", zip.Doc{
|
||||
Description: "Lists the values one job attribute key has taken, for\nbuilding job filters.",
|
||||
Fields: map[string]string{
|
||||
"O11yInfraAttributeValuesIn.aggregateAttribute": "AggregateAttribute is the metric the values must appear on.",
|
||||
"O11yInfraAttributeValuesIn.aggregateOperator": "AggregateOperator is the aggregation the values will be used under, e.g.\nnoop, count, avg. The runtime requires it for non-metrics sources.",
|
||||
"O11yInfraAttributeValuesIn.attributeKey": "AttributeKey is the key whose values to list.",
|
||||
"O11yInfraAttributeValuesIn.dataSource": "DataSource is the telemetry the values come from — metrics for the infra\nfaces. The runtime requires it.",
|
||||
"O11yInfraAttributeValuesIn.filterAttributeKeyDataType": "FilterAttributeKeyDataType is the key's data type — string, int64,\nfloat64 or bool. Empty means unspecified.",
|
||||
"O11yInfraAttributeValuesIn.limit": "Limit caps how many values come back. Absent means 50.",
|
||||
"O11yInfraAttributeValuesIn.searchText": "SearchText narrows the values to those containing it.",
|
||||
"O11yInfraAttributeValuesIn.tagType": "TagType narrows the search to one kind of key — tag or resource.",
|
||||
"O11yInfraAttributeValuesOut.data": "Data holds the values, split by data type.",
|
||||
"O11yInfraAttributeValuesOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/namespaces/attribute_keys", zip.Doc{
|
||||
Description: "Lists the metric attribute keys Kubernetes namespaces\nreport, for building namespace filters.",
|
||||
Fields: map[string]string{
|
||||
"O11yInfraAttributeKeysIn.aggregateAttribute": "AggregateAttribute is the metric the keys must appear on.",
|
||||
"O11yInfraAttributeKeysIn.aggregateOperator": "AggregateOperator is the aggregation the keys will be used under, e.g.\nnoop, count, avg. The runtime requires it for non-metrics sources.",
|
||||
"O11yInfraAttributeKeysIn.dataSource": "DataSource is the telemetry the keys come from — metrics for the infra\nfaces. The runtime requires it.",
|
||||
"O11yInfraAttributeKeysIn.limit": "Limit caps how many keys come back. Absent means 50.",
|
||||
"O11yInfraAttributeKeysIn.searchText": "SearchText narrows the keys to those containing it.",
|
||||
"O11yInfraAttributeKeysIn.tagType": "TagType narrows the keys to one kind — tag or resource. Empty means all;\nan invalid value reads as empty.",
|
||||
"O11yInfraAttributeKeysOut.data": "Data holds the keys.",
|
||||
"O11yInfraAttributeKeysOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/namespaces/attribute_values", zip.Doc{
|
||||
Description: "Lists the values one namespace attribute key has\ntaken, for building namespace filters.",
|
||||
Fields: map[string]string{
|
||||
"O11yInfraAttributeValuesIn.aggregateAttribute": "AggregateAttribute is the metric the values must appear on.",
|
||||
"O11yInfraAttributeValuesIn.aggregateOperator": "AggregateOperator is the aggregation the values will be used under, e.g.\nnoop, count, avg. The runtime requires it for non-metrics sources.",
|
||||
"O11yInfraAttributeValuesIn.attributeKey": "AttributeKey is the key whose values to list.",
|
||||
"O11yInfraAttributeValuesIn.dataSource": "DataSource is the telemetry the values come from — metrics for the infra\nfaces. The runtime requires it.",
|
||||
"O11yInfraAttributeValuesIn.filterAttributeKeyDataType": "FilterAttributeKeyDataType is the key's data type — string, int64,\nfloat64 or bool. Empty means unspecified.",
|
||||
"O11yInfraAttributeValuesIn.limit": "Limit caps how many values come back. Absent means 50.",
|
||||
"O11yInfraAttributeValuesIn.searchText": "SearchText narrows the values to those containing it.",
|
||||
"O11yInfraAttributeValuesIn.tagType": "TagType narrows the search to one kind of key — tag or resource.",
|
||||
"O11yInfraAttributeValuesOut.data": "Data holds the values, split by data type.",
|
||||
"O11yInfraAttributeValuesOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/nodes/attribute_keys", zip.Doc{
|
||||
Description: "Lists the metric attribute keys Kubernetes nodes report,\nfor building node filters.",
|
||||
Fields: map[string]string{
|
||||
"O11yInfraAttributeKeysIn.aggregateAttribute": "AggregateAttribute is the metric the keys must appear on.",
|
||||
"O11yInfraAttributeKeysIn.aggregateOperator": "AggregateOperator is the aggregation the keys will be used under, e.g.\nnoop, count, avg. The runtime requires it for non-metrics sources.",
|
||||
"O11yInfraAttributeKeysIn.dataSource": "DataSource is the telemetry the keys come from — metrics for the infra\nfaces. The runtime requires it.",
|
||||
"O11yInfraAttributeKeysIn.limit": "Limit caps how many keys come back. Absent means 50.",
|
||||
"O11yInfraAttributeKeysIn.searchText": "SearchText narrows the keys to those containing it.",
|
||||
"O11yInfraAttributeKeysIn.tagType": "TagType narrows the keys to one kind — tag or resource. Empty means all;\nan invalid value reads as empty.",
|
||||
"O11yInfraAttributeKeysOut.data": "Data holds the keys.",
|
||||
"O11yInfraAttributeKeysOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/nodes/attribute_values", zip.Doc{
|
||||
Description: "Lists the values one node attribute key has taken, for\nbuilding node filters.",
|
||||
Fields: map[string]string{
|
||||
"O11yInfraAttributeValuesIn.aggregateAttribute": "AggregateAttribute is the metric the values must appear on.",
|
||||
"O11yInfraAttributeValuesIn.aggregateOperator": "AggregateOperator is the aggregation the values will be used under, e.g.\nnoop, count, avg. The runtime requires it for non-metrics sources.",
|
||||
"O11yInfraAttributeValuesIn.attributeKey": "AttributeKey is the key whose values to list.",
|
||||
"O11yInfraAttributeValuesIn.dataSource": "DataSource is the telemetry the values come from — metrics for the infra\nfaces. The runtime requires it.",
|
||||
"O11yInfraAttributeValuesIn.filterAttributeKeyDataType": "FilterAttributeKeyDataType is the key's data type — string, int64,\nfloat64 or bool. Empty means unspecified.",
|
||||
"O11yInfraAttributeValuesIn.limit": "Limit caps how many values come back. Absent means 50.",
|
||||
"O11yInfraAttributeValuesIn.searchText": "SearchText narrows the values to those containing it.",
|
||||
"O11yInfraAttributeValuesIn.tagType": "TagType narrows the search to one kind of key — tag or resource.",
|
||||
"O11yInfraAttributeValuesOut.data": "Data holds the values, split by data type.",
|
||||
"O11yInfraAttributeValuesOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/pods/attribute_keys", zip.Doc{
|
||||
Description: "Lists the metric attribute keys Kubernetes pods report, for\nbuilding pod filters.",
|
||||
Fields: map[string]string{
|
||||
"O11yInfraAttributeKeysIn.aggregateAttribute": "AggregateAttribute is the metric the keys must appear on.",
|
||||
"O11yInfraAttributeKeysIn.aggregateOperator": "AggregateOperator is the aggregation the keys will be used under, e.g.\nnoop, count, avg. The runtime requires it for non-metrics sources.",
|
||||
"O11yInfraAttributeKeysIn.dataSource": "DataSource is the telemetry the keys come from — metrics for the infra\nfaces. The runtime requires it.",
|
||||
"O11yInfraAttributeKeysIn.limit": "Limit caps how many keys come back. Absent means 50.",
|
||||
"O11yInfraAttributeKeysIn.searchText": "SearchText narrows the keys to those containing it.",
|
||||
"O11yInfraAttributeKeysIn.tagType": "TagType narrows the keys to one kind — tag or resource. Empty means all;\nan invalid value reads as empty.",
|
||||
"O11yInfraAttributeKeysOut.data": "Data holds the keys.",
|
||||
"O11yInfraAttributeKeysOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/pods/attribute_values", zip.Doc{
|
||||
Description: "Lists the values one pod attribute key has taken, for\nbuilding pod filters.",
|
||||
Fields: map[string]string{
|
||||
"O11yInfraAttributeValuesIn.aggregateAttribute": "AggregateAttribute is the metric the values must appear on.",
|
||||
"O11yInfraAttributeValuesIn.aggregateOperator": "AggregateOperator is the aggregation the values will be used under, e.g.\nnoop, count, avg. The runtime requires it for non-metrics sources.",
|
||||
"O11yInfraAttributeValuesIn.attributeKey": "AttributeKey is the key whose values to list.",
|
||||
"O11yInfraAttributeValuesIn.dataSource": "DataSource is the telemetry the values come from — metrics for the infra\nfaces. The runtime requires it.",
|
||||
"O11yInfraAttributeValuesIn.filterAttributeKeyDataType": "FilterAttributeKeyDataType is the key's data type — string, int64,\nfloat64 or bool. Empty means unspecified.",
|
||||
"O11yInfraAttributeValuesIn.limit": "Limit caps how many values come back. Absent means 50.",
|
||||
"O11yInfraAttributeValuesIn.searchText": "SearchText narrows the values to those containing it.",
|
||||
"O11yInfraAttributeValuesIn.tagType": "TagType narrows the search to one kind of key — tag or resource.",
|
||||
"O11yInfraAttributeValuesOut.data": "Data holds the values, split by data type.",
|
||||
"O11yInfraAttributeValuesOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/processes/attribute_keys", zip.Doc{
|
||||
Description: "Lists the metric attribute keys processes report, for\nbuilding process filters.",
|
||||
Fields: map[string]string{
|
||||
"O11yInfraAttributeKeysIn.aggregateAttribute": "AggregateAttribute is the metric the keys must appear on.",
|
||||
"O11yInfraAttributeKeysIn.aggregateOperator": "AggregateOperator is the aggregation the keys will be used under, e.g.\nnoop, count, avg. The runtime requires it for non-metrics sources.",
|
||||
"O11yInfraAttributeKeysIn.dataSource": "DataSource is the telemetry the keys come from — metrics for the infra\nfaces. The runtime requires it.",
|
||||
"O11yInfraAttributeKeysIn.limit": "Limit caps how many keys come back. Absent means 50.",
|
||||
"O11yInfraAttributeKeysIn.searchText": "SearchText narrows the keys to those containing it.",
|
||||
"O11yInfraAttributeKeysIn.tagType": "TagType narrows the keys to one kind — tag or resource. Empty means all;\nan invalid value reads as empty.",
|
||||
"O11yInfraAttributeKeysOut.data": "Data holds the keys.",
|
||||
"O11yInfraAttributeKeysOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/processes/attribute_values", zip.Doc{
|
||||
Description: "Lists the values one process attribute key has taken,\nfor building process filters.",
|
||||
Fields: map[string]string{
|
||||
"O11yInfraAttributeValuesIn.aggregateAttribute": "AggregateAttribute is the metric the values must appear on.",
|
||||
"O11yInfraAttributeValuesIn.aggregateOperator": "AggregateOperator is the aggregation the values will be used under, e.g.\nnoop, count, avg. The runtime requires it for non-metrics sources.",
|
||||
"O11yInfraAttributeValuesIn.attributeKey": "AttributeKey is the key whose values to list.",
|
||||
"O11yInfraAttributeValuesIn.dataSource": "DataSource is the telemetry the values come from — metrics for the infra\nfaces. The runtime requires it.",
|
||||
"O11yInfraAttributeValuesIn.filterAttributeKeyDataType": "FilterAttributeKeyDataType is the key's data type — string, int64,\nfloat64 or bool. Empty means unspecified.",
|
||||
"O11yInfraAttributeValuesIn.limit": "Limit caps how many values come back. Absent means 50.",
|
||||
"O11yInfraAttributeValuesIn.searchText": "SearchText narrows the values to those containing it.",
|
||||
"O11yInfraAttributeValuesIn.tagType": "TagType narrows the search to one kind of key — tag or resource.",
|
||||
"O11yInfraAttributeValuesOut.data": "Data holds the values, split by data type.",
|
||||
"O11yInfraAttributeValuesOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/pvcs/attribute_keys", zip.Doc{
|
||||
Description: "Lists the metric attribute keys persistent volume claims\nreport, for building volume filters.",
|
||||
Fields: map[string]string{
|
||||
"O11yInfraAttributeKeysIn.aggregateAttribute": "AggregateAttribute is the metric the keys must appear on.",
|
||||
"O11yInfraAttributeKeysIn.aggregateOperator": "AggregateOperator is the aggregation the keys will be used under, e.g.\nnoop, count, avg. The runtime requires it for non-metrics sources.",
|
||||
"O11yInfraAttributeKeysIn.dataSource": "DataSource is the telemetry the keys come from — metrics for the infra\nfaces. The runtime requires it.",
|
||||
"O11yInfraAttributeKeysIn.limit": "Limit caps how many keys come back. Absent means 50.",
|
||||
"O11yInfraAttributeKeysIn.searchText": "SearchText narrows the keys to those containing it.",
|
||||
"O11yInfraAttributeKeysIn.tagType": "TagType narrows the keys to one kind — tag or resource. Empty means all;\nan invalid value reads as empty.",
|
||||
"O11yInfraAttributeKeysOut.data": "Data holds the keys.",
|
||||
"O11yInfraAttributeKeysOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/pvcs/attribute_values", zip.Doc{
|
||||
Description: "Lists the values one persistent-volume-claim attribute\nkey has taken, for building volume filters.",
|
||||
Fields: map[string]string{
|
||||
"O11yInfraAttributeValuesIn.aggregateAttribute": "AggregateAttribute is the metric the values must appear on.",
|
||||
"O11yInfraAttributeValuesIn.aggregateOperator": "AggregateOperator is the aggregation the values will be used under, e.g.\nnoop, count, avg. The runtime requires it for non-metrics sources.",
|
||||
"O11yInfraAttributeValuesIn.attributeKey": "AttributeKey is the key whose values to list.",
|
||||
"O11yInfraAttributeValuesIn.dataSource": "DataSource is the telemetry the values come from — metrics for the infra\nfaces. The runtime requires it.",
|
||||
"O11yInfraAttributeValuesIn.filterAttributeKeyDataType": "FilterAttributeKeyDataType is the key's data type — string, int64,\nfloat64 or bool. Empty means unspecified.",
|
||||
"O11yInfraAttributeValuesIn.limit": "Limit caps how many values come back. Absent means 50.",
|
||||
"O11yInfraAttributeValuesIn.searchText": "SearchText narrows the values to those containing it.",
|
||||
"O11yInfraAttributeValuesIn.tagType": "TagType narrows the search to one kind of key — tag or resource.",
|
||||
"O11yInfraAttributeValuesOut.data": "Data holds the values, split by data type.",
|
||||
"O11yInfraAttributeValuesOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/statefulsets/attribute_keys", zip.Doc{
|
||||
Description: "Lists the metric attribute keys Kubernetes\nstatefulsets report, for building statefulset filters.",
|
||||
Fields: map[string]string{
|
||||
"O11yInfraAttributeKeysIn.aggregateAttribute": "AggregateAttribute is the metric the keys must appear on.",
|
||||
"O11yInfraAttributeKeysIn.aggregateOperator": "AggregateOperator is the aggregation the keys will be used under, e.g.\nnoop, count, avg. The runtime requires it for non-metrics sources.",
|
||||
"O11yInfraAttributeKeysIn.dataSource": "DataSource is the telemetry the keys come from — metrics for the infra\nfaces. The runtime requires it.",
|
||||
"O11yInfraAttributeKeysIn.limit": "Limit caps how many keys come back. Absent means 50.",
|
||||
"O11yInfraAttributeKeysIn.searchText": "SearchText narrows the keys to those containing it.",
|
||||
"O11yInfraAttributeKeysIn.tagType": "TagType narrows the keys to one kind — tag or resource. Empty means all;\nan invalid value reads as empty.",
|
||||
"O11yInfraAttributeKeysOut.data": "Data holds the keys.",
|
||||
"O11yInfraAttributeKeysOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/statefulsets/attribute_values", zip.Doc{
|
||||
Description: "Lists the values one statefulset attribute key\nhas taken, for building statefulset filters.",
|
||||
Fields: map[string]string{
|
||||
"O11yInfraAttributeValuesIn.aggregateAttribute": "AggregateAttribute is the metric the values must appear on.",
|
||||
"O11yInfraAttributeValuesIn.aggregateOperator": "AggregateOperator is the aggregation the values will be used under, e.g.\nnoop, count, avg. The runtime requires it for non-metrics sources.",
|
||||
"O11yInfraAttributeValuesIn.attributeKey": "AttributeKey is the key whose values to list.",
|
||||
"O11yInfraAttributeValuesIn.dataSource": "DataSource is the telemetry the values come from — metrics for the infra\nfaces. The runtime requires it.",
|
||||
"O11yInfraAttributeValuesIn.filterAttributeKeyDataType": "FilterAttributeKeyDataType is the key's data type — string, int64,\nfloat64 or bool. Empty means unspecified.",
|
||||
"O11yInfraAttributeValuesIn.limit": "Limit caps how many values come back. Absent means 50.",
|
||||
"O11yInfraAttributeValuesIn.searchText": "SearchText narrows the values to those containing it.",
|
||||
"O11yInfraAttributeValuesIn.tagType": "TagType narrows the search to one kind of key — tag or resource.",
|
||||
"O11yInfraAttributeValuesOut.data": "Data holds the values, split by data type.",
|
||||
"O11yInfraAttributeValuesOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/clusters/list", zip.Doc{
|
||||
Description: "Lists Kubernetes clusters over a time range, each with its CPU\nand memory usage against allocatable capacity and its attributes;\nfilterable, groupable and paginated.",
|
||||
Fields: map[string]string{
|
||||
"ClusterListRequest.end": "epoch time in ms",
|
||||
"ClusterListRequest.start": "epoch time in ms",
|
||||
"O11yClusterListOut.data": "Data holds the cluster records.",
|
||||
"O11yClusterListOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/daemonsets/list", zip.Doc{
|
||||
Description: "Lists Kubernetes daemonsets over a time range, each with the\nCPU and memory its pods used against request and limit, desired and\navailable node counts, restarts and attributes; filterable, groupable and\npaginated.",
|
||||
Fields: map[string]string{
|
||||
"DaemonSetListRequest.end": "epoch time in ms",
|
||||
"DaemonSetListRequest.start": "epoch time in ms",
|
||||
"O11yDaemonSetListOut.data": "Data holds the daemonset records.",
|
||||
"O11yDaemonSetListOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/deployments/list", zip.Doc{
|
||||
Description: "Lists Kubernetes deployments over a time range, each with the\nCPU and memory its pods used against request and limit, desired and\navailable replica counts, restarts and attributes; filterable, groupable and\npaginated.",
|
||||
Fields: map[string]string{
|
||||
"DeploymentListRequest.end": "epoch time in ms",
|
||||
"DeploymentListRequest.start": "epoch time in ms",
|
||||
"O11yDeploymentListOut.data": "Data holds the deployment records.",
|
||||
"O11yDeploymentListOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/hosts/list", zip.Doc{
|
||||
Description: "Lists monitored hosts over a time range, each with its CPU,\nmemory, I/O wait and 15-minute load, whether it is actively reporting, its\nOS and its attributes; filterable, groupable and paginated. The answer also\nsays whether any host metrics were received at all and which clusters and\nnodes sent them, so an empty page is distinguishable from a fleet that never\nreported.",
|
||||
Fields: map[string]string{
|
||||
"HostListRequest.end": "epoch time in ms",
|
||||
"HostListRequest.start": "epoch time in ms",
|
||||
"O11yHostListOut.data": "Data holds the host records and the fleet's reporting state.",
|
||||
"O11yHostListOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/infra_monitoring/clusters", zip.Doc{
|
||||
Description: "Lists Kubernetes clusters with CPU and memory usage against\nallocatable capacity summed over their nodes, plus per-group node readiness\nand pod phase counts. Rows answer as 'list' under the default\nk8s.cluster.name grouping or 'grouped_list' under a custom groupBy; a metric\nwith no data in the window answers -1. Filterable by expression, orderable\nby usage or allocatable, paginated by offset and limit.",
|
||||
Fields: map[string]string{
|
||||
"ClusterRecord.clusterName": "TODO(nikhilmantri0902): once the underlying attr key is migrated to\nk8s.cluster.uid (see ClusterNameAttrKey), surface ClusterUID alongside\n(or replace) ClusterName.",
|
||||
"Filter.expression": "expression to filter by following the filter syntax",
|
||||
"O11yInfraClustersOut.data": "Data holds the rows.",
|
||||
"O11yInfraClustersOut.status": "Status is \"success\".",
|
||||
"OrderBy.direction": "direction to order by",
|
||||
"OrderBy.key": "key to order by",
|
||||
"TelemetryFieldKeySkipIndex.name": "Name is TelemetryFieldKey.Name not IndexName from Datastore",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/infra_monitoring/daemonsets", zip.Doc{
|
||||
Description: "Lists Kubernetes daemonsets with the CPU and memory their\npods used against request and limit, the latest desired and current\nscheduled NODE counts (node counts, not pod counts), and per-group pod phase\ncounts. Rows answer as 'list' under the default k8s.daemonset.name grouping\nor 'grouped_list' under a custom groupBy; a metric with no data in the\nwindow answers -1. Filterable by expression, orderable by the pod metrics or\nthe node counts, paginated by offset and limit.",
|
||||
Fields: map[string]string{
|
||||
"Filter.expression": "expression to filter by following the filter syntax",
|
||||
"O11yInfraDaemonSetsOut.data": "Data holds the rows.",
|
||||
"O11yInfraDaemonSetsOut.status": "Status is \"success\".",
|
||||
"OrderBy.direction": "direction to order by",
|
||||
"OrderBy.key": "key to order by",
|
||||
"TelemetryFieldKeySkipIndex.name": "Name is TelemetryFieldKey.Name not IndexName from Datastore",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/infra_monitoring/deployments", zip.Doc{
|
||||
Description: "Lists Kubernetes deployments with the CPU and memory their\npods used against request and limit, the latest desired and available\nreplica counts, and per-group pod phase counts. Rows answer as 'list' under\nthe default k8s.deployment.name grouping or 'grouped_list' under a custom\ngroupBy; a metric with no data in the window answers -1. Filterable by\nexpression, orderable by the pod metrics or the replica counts, paginated by\noffset and limit.",
|
||||
Fields: map[string]string{
|
||||
"Filter.expression": "expression to filter by following the filter syntax",
|
||||
"O11yInfraDeploymentsOut.data": "Data holds the rows.",
|
||||
"O11yInfraDeploymentsOut.status": "Status is \"success\".",
|
||||
"OrderBy.direction": "direction to order by",
|
||||
"OrderBy.key": "key to order by",
|
||||
"TelemetryFieldKeySkipIndex.name": "Name is TelemetryFieldKey.Name not IndexName from Datastore",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/infra_monitoring/hosts", zip.Doc{
|
||||
Description: "Lists hosts with key infrastructure metrics — CPU, memory, I/O\nwait and disk usage percentages and 15-minute load — plus an\nactive/inactive status from whether the host reported in the last ten\nminutes. Rows answer as 'list' under the default host.name grouping or\n'grouped_list' under a custom groupBy; a metric with no data in the window\nanswers -1. Filterable by expression and by status, orderable by any of the\nfive metrics, paginated by offset and limit.",
|
||||
Fields: map[string]string{
|
||||
"Filter.expression": "expression to filter by following the filter syntax",
|
||||
"O11yInfraHostsOut.data": "Data holds the rows.",
|
||||
"O11yInfraHostsOut.status": "Status is \"success\".",
|
||||
"OrderBy.direction": "direction to order by",
|
||||
"OrderBy.key": "key to order by",
|
||||
"TelemetryFieldKeySkipIndex.name": "Name is TelemetryFieldKey.Name not IndexName from Datastore",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/infra_monitoring/jobs", zip.Doc{
|
||||
Description: "Lists Kubernetes jobs with the CPU and memory their pods used\nagainst request and limit, the latest desired-successful, active, failed and\nsuccessful pod counters, and per-group pod phase counts — the phase counts\nare current state while the counters are cumulative over the job's life.\nRows answer as 'list' under the default k8s.job.name grouping or\n'grouped_list' under a custom groupBy; a metric with no data in the window\nanswers -1. Filterable by expression, orderable by the pod metrics or the\njob counters, paginated by offset and limit.",
|
||||
Fields: map[string]string{
|
||||
"Filter.expression": "expression to filter by following the filter syntax",
|
||||
"O11yInfraJobsOut.data": "Data holds the rows.",
|
||||
"O11yInfraJobsOut.status": "Status is \"success\".",
|
||||
"OrderBy.direction": "direction to order by",
|
||||
"OrderBy.key": "key to order by",
|
||||
"TelemetryFieldKeySkipIndex.name": "Name is TelemetryFieldKey.Name not IndexName from Datastore",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/infra_monitoring/namespaces", zip.Doc{
|
||||
Description: "Lists Kubernetes namespaces with the CPU and memory their\npods used and per-group pod phase counts. Rows answer as 'list' under the\ndefault k8s.namespace.name grouping or 'grouped_list' under a custom\ngroupBy, aggregating pods either way; a metric with no data in the window\nanswers -1. Filterable by expression, orderable by cpu or memory, paginated\nby offset and limit.",
|
||||
Fields: map[string]string{
|
||||
"Filter.expression": "expression to filter by following the filter syntax",
|
||||
"O11yInfraNamespacesOut.data": "Data holds the rows.",
|
||||
"O11yInfraNamespacesOut.status": "Status is \"success\".",
|
||||
"OrderBy.direction": "direction to order by",
|
||||
"OrderBy.key": "key to order by",
|
||||
"TelemetryFieldKeySkipIndex.name": "Name is TelemetryFieldKey.Name not IndexName from Datastore",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/infra_monitoring/nodes", zip.Doc{
|
||||
Description: "Lists Kubernetes nodes with CPU and memory usage against\nallocatable capacity, per-group readiness counts and per-group phase counts\nfor the pods scheduled on them. Rows answer as 'list' under the default\nk8s.node.name grouping (each row one node with its readiness condition) or\n'grouped_list' under a custom groupBy; a metric with no data in the window\nanswers -1. Filterable by expression, orderable by usage or allocatable,\npaginated by offset and limit.",
|
||||
Fields: map[string]string{
|
||||
"Filter.expression": "expression to filter by following the filter syntax",
|
||||
"O11yInfraNodesOut.data": "Data holds the rows.",
|
||||
"O11yInfraNodesOut.status": "Status is \"success\".",
|
||||
"OrderBy.direction": "direction to order by",
|
||||
"OrderBy.key": "key to order by",
|
||||
"TelemetryFieldKeySkipIndex.name": "Name is TelemetryFieldKey.Name not IndexName from Datastore",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/infra_monitoring/pods", zip.Doc{
|
||||
Description: "Lists Kubernetes pods with CPU and memory usage against request\nand limit, the pod's phase and its age, plus its namespace, node, owning\nworkload and cluster attributes. Rows answer as 'list' under the default\nk8s.pod.uid grouping (each row one pod) or 'grouped_list' under a custom\ngroupBy (each row aggregating its pods with per-phase counts); a metric with\nno data in the window answers -1. Filterable by expression, orderable by the\nsix pod metrics, paginated by offset and limit.",
|
||||
Fields: map[string]string{
|
||||
"Filter.expression": "expression to filter by following the filter syntax",
|
||||
"O11yInfraPodsOut.data": "Data holds the rows.",
|
||||
"O11yInfraPodsOut.status": "Status is \"success\".",
|
||||
"OrderBy.direction": "direction to order by",
|
||||
"OrderBy.key": "key to order by",
|
||||
"TelemetryFieldKeySkipIndex.name": "Name is TelemetryFieldKey.Name not IndexName from Datastore",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/infra_monitoring/pvcs", zip.Doc{
|
||||
Description: "Lists Kubernetes persistent volume claims with available,\ncapacity and used bytes and inode counts, plus the claim's pod, namespace,\nnode, statefulset and cluster attributes. Rows answer as 'list' under the\ndefault k8s.persistentvolumeclaim.name grouping or 'grouped_list' under a\ncustom groupBy; a metric with no data in the window answers -1. Filterable\nby expression, orderable by the six volume metrics, paginated by offset and\nlimit.",
|
||||
Fields: map[string]string{
|
||||
"Filter.expression": "expression to filter by following the filter syntax",
|
||||
"O11yInfraVolumesOut.data": "Data holds the rows.",
|
||||
"O11yInfraVolumesOut.status": "Status is \"success\".",
|
||||
"OrderBy.direction": "direction to order by",
|
||||
"OrderBy.key": "key to order by",
|
||||
"TelemetryFieldKeySkipIndex.name": "Name is TelemetryFieldKey.Name not IndexName from Datastore",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/infra_monitoring/statefulsets", zip.Doc{
|
||||
Description: "Lists Kubernetes statefulsets with the CPU and memory\ntheir pods used against request and limit, the latest desired and current\nreplica counts, and per-group pod phase counts. Rows answer as 'list' under\nthe default k8s.statefulset.name grouping or 'grouped_list' under a custom\ngroupBy; a metric with no data in the window answers -1. Filterable by\nexpression, orderable by the pod metrics or the replica counts, paginated by\noffset and limit.",
|
||||
Fields: map[string]string{
|
||||
"Filter.expression": "expression to filter by following the filter syntax",
|
||||
"O11yInfraStatefulSetsOut.data": "Data holds the rows.",
|
||||
"O11yInfraStatefulSetsOut.status": "Status is \"success\".",
|
||||
"OrderBy.direction": "direction to order by",
|
||||
"OrderBy.key": "key to order by",
|
||||
"TelemetryFieldKeySkipIndex.name": "Name is TelemetryFieldKey.Name not IndexName from Datastore",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/jobs/list", zip.Doc{
|
||||
Description: "Lists Kubernetes jobs over a time range, each with the CPU and\nmemory its pods used against request and limit, desired-successful, active,\nfailed and successful pod counts, restarts and attributes; filterable,\ngroupable and paginated.",
|
||||
Fields: map[string]string{
|
||||
"JobListRequest.end": "epoch time in ms",
|
||||
"JobListRequest.start": "epoch time in ms",
|
||||
"O11yJobListOut.data": "Data holds the job records.",
|
||||
"O11yJobListOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/namespaces/list", zip.Doc{
|
||||
Description: "Lists Kubernetes namespaces over a time range, each with the\nCPU and memory its pods used, their phase counts and its attributes;\nfilterable, groupable and paginated.",
|
||||
Fields: map[string]string{
|
||||
"NamespaceListRequest.end": "epoch time in ms",
|
||||
"NamespaceListRequest.start": "epoch time in ms",
|
||||
"O11yNamespaceListOut.data": "Data holds the namespace records.",
|
||||
"O11yNamespaceListOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/nodes/list", zip.Doc{
|
||||
Description: "Lists Kubernetes nodes over a time range, each with its CPU and\nmemory usage against allocatable capacity, readiness condition counts and\nattributes; filterable, groupable and paginated.",
|
||||
Fields: map[string]string{
|
||||
"NodeListRequest.end": "epoch time in ms",
|
||||
"NodeListRequest.start": "epoch time in ms",
|
||||
"O11yNodeListOut.data": "Data holds the node records.",
|
||||
"O11yNodeListOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/pods/list", zip.Doc{
|
||||
Description: "Lists Kubernetes pods over a time range, each with its CPU and\nmemory usage against request and limit, restart count, phase counts and\nattributes; filterable, groupable and paginated.",
|
||||
Fields: map[string]string{
|
||||
"O11yPodListOut.data": "Data holds the pod records.",
|
||||
"O11yPodListOut.status": "Status is \"success\".",
|
||||
"PodListRequest.end": "epoch time in ms",
|
||||
"PodListRequest.start": "epoch time in ms",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/processes/list", zip.Doc{
|
||||
Description: "Lists monitored processes over a time range, each with its name,\nPID, command line and CPU and memory usage; filterable, groupable and\npaginated.",
|
||||
Fields: map[string]string{
|
||||
"O11yProcessListOut.data": "Data holds the process records.",
|
||||
"O11yProcessListOut.status": "Status is \"success\".",
|
||||
"ProcessListRequest.end": "epoch time in ms",
|
||||
"ProcessListRequest.start": "epoch time in ms",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/pvcs/list", zip.Doc{
|
||||
Description: "Lists Kubernetes persistent volume claims over a time range, each\nwith its available, capacity and used bytes, inode counts and attributes;\nfilterable, groupable and paginated.",
|
||||
Fields: map[string]string{
|
||||
"O11yPvcListOut.data": "Data holds the volume records.",
|
||||
"O11yPvcListOut.status": "Status is \"success\".",
|
||||
"VolumeListRequest.end": "epoch time in ms",
|
||||
"VolumeListRequest.start": "epoch time in ms",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/statefulsets/list", zip.Doc{
|
||||
Description: "Lists Kubernetes statefulsets over a time range, each with\nthe CPU and memory its pods used against request and limit, desired and\navailable replica counts, restarts and attributes; filterable, groupable and\npaginated.",
|
||||
Fields: map[string]string{
|
||||
"O11yStatefulSetListOut.data": "Data holds the statefulset records.",
|
||||
"O11yStatefulSetListOut.status": "Status is \"success\".",
|
||||
"StatefulSetListRequest.end": "epoch time in ms",
|
||||
"StatefulSetListRequest.start": "epoch time in ms",
|
||||
},
|
||||
})
|
||||
}
|
||||
+2
-6
@@ -58,17 +58,13 @@ import (
|
||||
"github.com/zap-proto/zip"
|
||||
)
|
||||
|
||||
// integrationsPrefix is the face's path root, spelled ONCE: the group the ops
|
||||
// register on, and the first segment integrationsRelay rebuilds a call onto.
|
||||
const integrationsPrefix = "/v1/o11y"
|
||||
|
||||
// mountIntegrations registers the integrations face's typed ops on the native
|
||||
// router. Static segments sit beside their parameterised siblings safely — the
|
||||
// router matches the most specific pattern, so /integrations/install can never
|
||||
// be swallowed by /integrations/:integrationId, the same disambiguation the
|
||||
// runtime's own mux tree makes.
|
||||
func mountIntegrations(app *zip.App) {
|
||||
g := app.Group(integrationsPrefix)
|
||||
g := app.Group(o11yRoot)
|
||||
|
||||
// integration catalog and install lifecycle (runtime gate: ViewAccess)
|
||||
zip.Get(g, "/integrations", listIntegrations, zip.WithOperationID("ListIntegrations"))
|
||||
@@ -616,7 +612,7 @@ func integrationsRelay(ctx context.Context, method, path string, params url.Valu
|
||||
return zip.Errorf(http.StatusServiceUnavailable, "o11y runtime not initialized")
|
||||
}
|
||||
|
||||
target := integrationsPrefix + path
|
||||
target := o11yRoot + path
|
||||
if q := params.Encode(); q != "" {
|
||||
target += "?" + q
|
||||
}
|
||||
|
||||
@@ -1,252 +0,0 @@
|
||||
// Code generated by zipdoc; DO NOT EDIT.
|
||||
//
|
||||
// The integrations slice's entries, split out of zipdoc_gen.go so concurrent
|
||||
// slices do not contend on one generated file; regenerating the package folds
|
||||
// them back together, which is the intended end state.
|
||||
|
||||
package o11y
|
||||
|
||||
import (
|
||||
"github.com/zap-proto/zip"
|
||||
)
|
||||
|
||||
func init() {
|
||||
zip.Describe("DELETE /v1/o11y/cloud_integrations/:cloud_provider/accounts/:id", zip.Doc{
|
||||
Description: "Tears down a connected account for the given provider, by\nid. Admin gate.",
|
||||
})
|
||||
zip.Describe("DELETE /v1/o11y/gateway/ingestion_keys/:keyId", zip.Doc{
|
||||
Description: "Removes an ingestion key, by id. Editor gate.",
|
||||
})
|
||||
zip.Describe("DELETE /v1/o11y/gateway/ingestion_keys/limits/:limitId", zip.Doc{
|
||||
Description: "Removes an ingestion key limit, by limit id. Editor\ngate.",
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/cloud_integrations/:cloud_provider/accounts", zip.Doc{
|
||||
Description: "Lists the cloud-integration accounts connected for the given\nprovider. Admin gate.",
|
||||
Fields: map[string]string{
|
||||
"GCPAccountConfig.deploymentProjectId": "Project ID where central pub/sub for logs exist",
|
||||
"GCPAccountConfig.deploymentRegion": "Project ID where otel collector will be deployed",
|
||||
"GCPAccountConfig.projectIds": "List of project IDs to monitor",
|
||||
"O11yAccountsOut.data": "Data holds the connected accounts.",
|
||||
"O11yAccountsOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/cloud_integrations/:cloud_provider/accounts/:id", zip.Doc{
|
||||
Description: "Returns one connected account for the given provider, by id. Admin\ngate.",
|
||||
Fields: map[string]string{
|
||||
"GCPAccountConfig.deploymentProjectId": "Project ID where central pub/sub for logs exist",
|
||||
"GCPAccountConfig.deploymentRegion": "Project ID where otel collector will be deployed",
|
||||
"GCPAccountConfig.projectIds": "List of project IDs to monitor",
|
||||
"O11yAccountOut.data": "Data holds the account.",
|
||||
"O11yAccountOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/cloud_integrations/:cloud_provider/accounts/:id/services", zip.Doc{
|
||||
Description: "Lists the services metadata for one connected\naccount of the given provider, by account id. Admin gate.",
|
||||
Fields: map[string]string{
|
||||
"O11yServicesMetadataOut.data": "Data holds the services metadata.",
|
||||
"O11yServicesMetadataOut.status": "Status is \"success\".",
|
||||
"ServiceMetadata.enabled": "if the service is enabled for the account",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/cloud_integrations/:cloud_provider/accounts/:id/services/:service_id", zip.Doc{
|
||||
Description: "Returns one service and its configuration for a connected\naccount of the given provider, by account id and service id. Admin gate.",
|
||||
Fields: map[string]string{
|
||||
"O11yServiceOut.data": "Data holds the service.",
|
||||
"O11yServiceOut.status": "Status is \"success\".",
|
||||
"Service.overview": "markdown",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/cloud_integrations/:cloud_provider/credentials", zip.Doc{
|
||||
Description: "Returns the credentials the connecting agent needs\nto establish the cloud integration, for the given cloud provider. Admin gate.",
|
||||
Fields: map[string]string{
|
||||
"Credentials.o11yApiKey": "PAT",
|
||||
"O11yCredentialsOut.data": "Data holds the connection credentials.",
|
||||
"O11yCredentialsOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/cloud_integrations/:cloud_provider/services", zip.Doc{
|
||||
Description: "Lists the services the given provider can collect from,\noptionally scoped to one cloud integration. Admin gate.",
|
||||
Fields: map[string]string{
|
||||
"O11yListServicesMetadataIn.cloud_integration_id": "CloudIntegrationID, when set, scopes the listing to one cloud integration.",
|
||||
"O11yServicesMetadataOut.data": "Data holds the services metadata.",
|
||||
"O11yServicesMetadataOut.status": "Status is \"success\".",
|
||||
"ServiceMetadata.enabled": "if the service is enabled for the account",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/cloud_integrations/:cloud_provider/services/:service_id", zip.Doc{
|
||||
Description: "Returns one service the given provider can collect from, by\nservice id, optionally scoped to one cloud integration. Admin gate.",
|
||||
Fields: map[string]string{
|
||||
"O11yGetServiceIn.cloud_integration_id": "CloudIntegrationID, when set, scopes the service to one cloud integration.",
|
||||
"O11yServiceOut.data": "Data holds the service.",
|
||||
"O11yServiceOut.status": "Status is \"success\".",
|
||||
"Service.overview": "markdown",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/gateway/ingestion_keys", zip.Doc{
|
||||
Description: "Lists the workspace's ingestion keys, paginated. Editor\ngate.",
|
||||
Fields: map[string]string{
|
||||
"Limit.signal": "\"logs\", \"traces\", \"metrics\"",
|
||||
"O11yIngestionKeysIn.page": "Page is the 1-based page number.",
|
||||
"O11yIngestionKeysIn.per_page": "PerPage is the page size.",
|
||||
"O11yIngestionKeysOut.data": "Data holds the ingestion keys.",
|
||||
"O11yIngestionKeysOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/gateway/ingestion_keys/search", zip.Doc{
|
||||
Description: "Lists the workspace's ingestion keys whose name matches\nthe search, paginated. Editor gate.",
|
||||
Fields: map[string]string{
|
||||
"Limit.signal": "\"logs\", \"traces\", \"metrics\"",
|
||||
"O11yIngestionKeysOut.data": "Data holds the ingestion keys.",
|
||||
"O11yIngestionKeysOut.status": "Status is \"success\".",
|
||||
"O11ySearchIngestionKeysIn.name": "Name is the substring to match ingestion-key names against.",
|
||||
"O11ySearchIngestionKeysIn.page": "Page is the 1-based page number.",
|
||||
"O11ySearchIngestionKeysIn.per_page": "PerPage is the page size.",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/integrations", zip.Doc{
|
||||
Description: "Lists the available integrations and whether each is\ninstalled in the caller's org, optionally narrowed to installed or\nnot-installed. Viewer gate.",
|
||||
Fields: map[string]string{
|
||||
"IntegrationSummary.description": "A short description",
|
||||
"O11yIntegrationsListOut.data": "Data holds the available integrations and their installed state.",
|
||||
"O11yIntegrationsListOut.status": "Status is \"success\".",
|
||||
"O11yListIntegrationsIn.is_installed": "IsInstalled, when \"true\" or \"false\", keeps only integrations in that\ninstalled state; empty lists them all.",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/integrations/:integrationId", zip.Doc{
|
||||
Description: "Returns one integration's full detail — its overview,\nconfiguration steps, collected data and assets — together with its\ninstallation record when the org has installed it. Viewer gate.",
|
||||
Fields: map[string]string{
|
||||
"AlertCompositeQuery.unit": "Unit for the time series data shown in the graph\nThis is used to format the value and threshold",
|
||||
"IntegrationConnectionTests.metrics": "Metric names expected to have been received for the integration.",
|
||||
"IntegrationDetails.overview": "markdown",
|
||||
"IntegrationSummary.description": "A short description",
|
||||
"NotificationSettings.newGroupEvalDelay": "NewGroupEvalDelay is the grace period for new series to be excluded from alerts evaluation",
|
||||
"O11yIntegrationOut.data": "Data holds the integration and its installation record.",
|
||||
"O11yIntegrationOut.status": "Status is \"success\".",
|
||||
"PipelineOperator.enable_flattening": "json_parser fields",
|
||||
"PipelineOperator.layout": "time_parser fields.",
|
||||
"PipelineOperator.mapping": "Used in Severity Parsing and JSON Flattening mapping",
|
||||
"PipelineOperator.orderId": "don't need the following in the final config",
|
||||
"PipelineOperator.overwrite_text": "severity parser fields",
|
||||
"PipelineOperator.parse_to": "optional keys depending on the type",
|
||||
"PostableRule.source": "Source captures the source url where rule has been created",
|
||||
"QueryEnvelope.spec": "Spec is the deferred decoding of the query if any.",
|
||||
"QueryEnvelope.type": "Type is the type of the query.",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/integrations/:integrationId/connection_status", zip.Doc{
|
||||
Description: "Reports whether the integration's logs and\nmetrics have been received over the lookback window, so the console can show\na live connection state. An integration that is not installed answers with an\nempty status rather than an error. Viewer gate.",
|
||||
Fields: map[string]string{
|
||||
"O11yConnectionStatusIn.lookback_seconds": "LookbackSeconds is how far back to look for received telemetry, in\nseconds.",
|
||||
"O11yConnectionStatusOut.data": "Data holds the logs and metrics connection status.",
|
||||
"O11yConnectionStatusOut.status": "Status is \"success\".",
|
||||
"SignalConnectionStatus.last_received_from": "resource identifier",
|
||||
"SignalConnectionStatus.last_received_ts_ms": "epoch milliseconds",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/zeus/hosts", zip.Doc{
|
||||
Description: "Returns the deployment's host info from Zeus. Viewer gate.",
|
||||
Fields: map[string]string{
|
||||
"O11yGettableHostOut.data": "Data holds the host info.",
|
||||
"O11yGettableHostOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("PATCH /v1/o11y/gateway/ingestion_keys/:keyId", zip.Doc{
|
||||
Description: "Changes an ingestion key, by id. Editor gate.",
|
||||
})
|
||||
zip.Describe("PATCH /v1/o11y/gateway/ingestion_keys/limits/:limitId", zip.Doc{
|
||||
Description: "Changes an ingestion key limit, by limit id. Editor\ngate.",
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/cloud-integrations/:cloud_provider/agent-check-in", zip.Doc{
|
||||
Description: "Is the deployed agent's check-in on its original\nhyphenated path, kept for backward compatibility with agents already\nrunning. Viewer gate — the agent's role is viewer.",
|
||||
Fields: map[string]string{
|
||||
"AWSCloudWatchLogsSubscription.filterPattern": "https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/FilterAndPatternSyntax.html\n\"\" implies no filtering is required",
|
||||
"AWSCloudWatchLogsSubscription.logGroupNamePrefix": "subscribe to all logs groups with specified prefix.\neg: `/aws/rds/`",
|
||||
"AWSCloudWatchMetricStreamFilter.namespace": "https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cloudwatch-metricstream-metricstreamfilter.html",
|
||||
"AWSMetricsCollectionStrategy.streamFilters": "to be used as https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cloudwatch-metricstream.html#cfn-cloudwatch-metricstream-includefilters",
|
||||
"AWSTelemetryCollectionStrategy.s3Buckets": "Only available in S3 Sync Service Type in AWS",
|
||||
"AzureLogsCollectionStrategy.categoryGroups": "List of categories to enable for diagnostic settings, to start with it will have 'allLogs' and no filtering.",
|
||||
"AzureTelemetryCollectionStrategy.resourceProvider": "https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/resource-providers-and-types",
|
||||
"GettableAgentCheckIn.account_id": "Older fields for backward compatibility with existing AWS agents",
|
||||
"IntegrationConfig.enabled_regions": "backward compatible",
|
||||
"IntegrationConfig.telemetry": "backward compatible",
|
||||
"O11yAgentCheckInOut.data": "Data holds the check-in result.",
|
||||
"O11yAgentCheckInOut.status": "Status is \"success\".",
|
||||
"PostableAgentCheckIn.account_id": "following are backward compatible fields for older running agents, hence snake case JSON keys.\nWhich get mapped to new fields in AgentCheckInRequest",
|
||||
"PostableAgentCheckIn.cloud_account_id": "=> ProviderAccountID",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/cloud_integrations/:cloud_provider/accounts", zip.Doc{
|
||||
Description: "Connects a new cloud-integration account for the given\nprovider from its posted config and credentials, answering with the account\nand the artifact the agent deploys to complete the connection. Admin gate.",
|
||||
Fields: map[string]string{
|
||||
"ConnectionArtifact.aws": "required till new providers are added",
|
||||
"Credentials.o11yApiKey": "PAT",
|
||||
"O11yCreateAccountOut.data": "Data holds the account and the connection artifact.",
|
||||
"O11yCreateAccountOut.status": "Status is \"success\".",
|
||||
"PostableAccountConfig.AgentVersion": "as agent version is common for all providers, we can keep it at top level of this struct",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/cloud_integrations/:cloud_provider/accounts/check_in", zip.Doc{
|
||||
Description: "Is the deployed agent's check-in — the path consistent with the\naccount surface, reporting the agent's account and telemetry state so the\nconnection can be tracked. Viewer gate — the agent's role is viewer.",
|
||||
Fields: map[string]string{
|
||||
"AWSCloudWatchLogsSubscription.filterPattern": "https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/FilterAndPatternSyntax.html\n\"\" implies no filtering is required",
|
||||
"AWSCloudWatchLogsSubscription.logGroupNamePrefix": "subscribe to all logs groups with specified prefix.\neg: `/aws/rds/`",
|
||||
"AWSCloudWatchMetricStreamFilter.namespace": "https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cloudwatch-metricstream-metricstreamfilter.html",
|
||||
"AWSMetricsCollectionStrategy.streamFilters": "to be used as https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cloudwatch-metricstream.html#cfn-cloudwatch-metricstream-includefilters",
|
||||
"AWSTelemetryCollectionStrategy.s3Buckets": "Only available in S3 Sync Service Type in AWS",
|
||||
"AzureLogsCollectionStrategy.categoryGroups": "List of categories to enable for diagnostic settings, to start with it will have 'allLogs' and no filtering.",
|
||||
"AzureTelemetryCollectionStrategy.resourceProvider": "https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/resource-providers-and-types",
|
||||
"GettableAgentCheckIn.account_id": "Older fields for backward compatibility with existing AWS agents",
|
||||
"IntegrationConfig.enabled_regions": "backward compatible",
|
||||
"IntegrationConfig.telemetry": "backward compatible",
|
||||
"O11yAgentCheckInOut.data": "Data holds the check-in result.",
|
||||
"O11yAgentCheckInOut.status": "Status is \"success\".",
|
||||
"PostableAgentCheckIn.account_id": "following are backward compatible fields for older running agents, hence snake case JSON keys.\nWhich get mapped to new fields in AgentCheckInRequest",
|
||||
"PostableAgentCheckIn.cloud_account_id": "=> ProviderAccountID",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/gateway/ingestion_keys", zip.Doc{
|
||||
Description: "Mints an ingestion key for the workspace, answering with\nthe created key. Editor gate.",
|
||||
Fields: map[string]string{
|
||||
"O11yCreatedIngestionKeyOut.data": "Data holds the created ingestion key.",
|
||||
"O11yCreatedIngestionKeyOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/gateway/ingestion_keys/:keyId/limits", zip.Doc{
|
||||
Description: "Sets a signal limit on an ingestion key, by key id,\nanswering with the created limit. Editor gate.",
|
||||
Fields: map[string]string{
|
||||
"O11yCreatedLimitOut.data": "Data holds the created limit.",
|
||||
"O11yCreatedLimitOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/integrations/install", zip.Doc{
|
||||
Description: "Installs an integration into the caller's org from its id\nand configuration, answering with the installed catalog item. Viewer gate.",
|
||||
Fields: map[string]string{
|
||||
"IntegrationSummary.description": "A short description",
|
||||
"O11yInstallOut.data": "Data holds the installed integration.",
|
||||
"O11yInstallOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/integrations/uninstall", zip.Doc{
|
||||
Description: "Removes an integration from the caller's org by id.\nViewer gate.",
|
||||
Fields: map[string]string{
|
||||
"O11yIntegrationAck.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("PUT /v1/o11y/cloud_integrations/:cloud_provider/accounts/:id", zip.Doc{
|
||||
Description: "Changes a connected account's configuration for the given\nprovider, by id. Admin gate.",
|
||||
Fields: map[string]string{
|
||||
"UpdatableGCPAccountConfig.deploymentProjectId": "Project ID where central pub/sub for logs exist",
|
||||
"UpdatableGCPAccountConfig.deploymentRegion": "Compute service region where otel collector will be deployed",
|
||||
"UpdatableGCPAccountConfig.projectIds": "List of project IDs to monitor",
|
||||
},
|
||||
})
|
||||
zip.Describe("PUT /v1/o11y/cloud_integrations/:cloud_provider/accounts/:id/services/:service_id", zip.Doc{
|
||||
Description: "Changes a service's configuration for one connected account of\nthe given provider, by account id and service id. Admin gate.",
|
||||
})
|
||||
zip.Describe("PUT /v1/o11y/zeus/hosts", zip.Doc{
|
||||
Description: "Records the deployment's host in Zeus, overwriting any prior one.\nAdmin gate.",
|
||||
})
|
||||
zip.Describe("PUT /v1/o11y/zeus/profiles", zip.Doc{
|
||||
Description: "Records the deployment's profile in Zeus — how the team uses\nobservability today and what they plan — overwriting any prior one. Admin\ngate.",
|
||||
})
|
||||
}
|
||||
@@ -13,7 +13,7 @@ package o11y
|
||||
//
|
||||
// THE WIRE DOES NOT MOVE, and the way it does not move is that these ops do not
|
||||
// re-implement the reads and writes. Each hands the call to the SAME runtime
|
||||
// handler the wildcard delegates to (see relayO11y), so identity resolution, the
|
||||
// handler the wildcard delegates to (see relay.go), so identity resolution, the
|
||||
// org gate, the role check (viewer on every read, editor on the score and
|
||||
// annotation writes, admin on the pricing-rule writes — the exact
|
||||
// ViewAccess/EditAccess/AdminAccess gates the runtime has always run), the
|
||||
@@ -29,11 +29,11 @@ package o11y
|
||||
//
|
||||
// The three writes whose answer is only their status — the score delete, the
|
||||
// pricing-rule bulk write and the pricing-rule delete — answer the empty 204 a
|
||||
// nil Out has always meant: relayO11y leaves the Out untouched, and zip writes a
|
||||
// nil Out has always meant: relay leaves the Out untouched, and zip writes a
|
||||
// clean 204 with no body.
|
||||
//
|
||||
// The mux registrations these ops mirror STAY (pkg/apiserver/o11yapiserver):
|
||||
// they are the runtime router relayO11y forwards into, and the only router the
|
||||
// they are the runtime router relay forwards into, and the only router the
|
||||
// standalone server has. Registered ahead of the wildcard, and
|
||||
// specific-beats-wildcard is what the router does regardless of registration
|
||||
// order, so these fourteen paths dispatch here and every other path under the
|
||||
@@ -95,7 +95,7 @@ func mountLLMObs(app *zip.App) {
|
||||
// the read to the caller's validated tenant.
|
||||
func llmObservations(ctx context.Context, in *O11yLLMViewQuery) (*O11yLLMObservationsOut, error) {
|
||||
out := new(O11yLLMObservationsOut)
|
||||
return out, relayO11y(ctx, http.MethodGet, "/llm/observations", viewParams(in), nil, out)
|
||||
return out, relay(ctx, http.MethodGet, o11yRoot+"/llm/observations", viewParams(in), nil, out)
|
||||
}
|
||||
|
||||
// llmTraces lists LLM traces — gen_ai spans grouped by trace_id, with cost,
|
||||
@@ -104,7 +104,7 @@ func llmObservations(ctx context.Context, in *O11yLLMViewQuery) (*O11yLLMObserva
|
||||
// Callers need the viewer role; the runtime's own gate enforces it.
|
||||
func llmTraces(ctx context.Context, in *O11yLLMViewQuery) (*O11yLLMTracesOut, error) {
|
||||
out := new(O11yLLMTracesOut)
|
||||
return out, relayO11y(ctx, http.MethodGet, "/llm/traces", viewParams(in), nil, out)
|
||||
return out, relay(ctx, http.MethodGet, o11yRoot+"/llm/traces", viewParams(in), nil, out)
|
||||
}
|
||||
|
||||
// llmSessions lists conversations — gen_ai spans grouped by session.id, with
|
||||
@@ -113,7 +113,7 @@ func llmTraces(ctx context.Context, in *O11yLLMViewQuery) (*O11yLLMTracesOut, er
|
||||
// Callers need the viewer role; the runtime's own gate enforces it.
|
||||
func llmSessions(ctx context.Context, in *O11yLLMViewQuery) (*O11yLLMSessionsOut, error) {
|
||||
out := new(O11yLLMSessionsOut)
|
||||
return out, relayO11y(ctx, http.MethodGet, "/llm/sessions", viewParams(in), nil, out)
|
||||
return out, relay(ctx, http.MethodGet, o11yRoot+"/llm/sessions", viewParams(in), nil, out)
|
||||
}
|
||||
|
||||
// llmUsers lists end users — gen_ai spans grouped by user.id, with their
|
||||
@@ -122,7 +122,7 @@ func llmSessions(ctx context.Context, in *O11yLLMViewQuery) (*O11yLLMSessionsOut
|
||||
// Callers need the viewer role; the runtime's own gate enforces it.
|
||||
func llmUsers(ctx context.Context, in *O11yLLMViewQuery) (*O11yLLMUsersOut, error) {
|
||||
out := new(O11yLLMUsersOut)
|
||||
return out, relayO11y(ctx, http.MethodGet, "/llm/users", viewParams(in), nil, out)
|
||||
return out, relay(ctx, http.MethodGet, o11yRoot+"/llm/users", viewParams(in), nil, out)
|
||||
}
|
||||
|
||||
// llmListScores lists eval scores and human-feedback signals attached to traces
|
||||
@@ -131,7 +131,7 @@ func llmUsers(ctx context.Context, in *O11yLLMViewQuery) (*O11yLLMUsersOut, erro
|
||||
// Callers need the viewer role; the runtime's own gate enforces it.
|
||||
func llmListScores(ctx context.Context, in *O11yLLMScoresQuery) (*O11yLLMScoresOut, error) {
|
||||
out := new(O11yLLMScoresOut)
|
||||
return out, relayO11y(ctx, http.MethodGet, "/llm/scores", query(
|
||||
return out, relay(ctx, http.MethodGet, o11yRoot+"/llm/scores", query(
|
||||
"traceId", in.TraceID,
|
||||
"observationId", in.ObservationID,
|
||||
"name", in.Name,
|
||||
@@ -148,7 +148,7 @@ func llmListScores(ctx context.Context, in *O11yLLMScoresQuery) (*O11yLLMScoresO
|
||||
// validates the payload and stamps the score's author and org.
|
||||
func llmCreateScore(ctx context.Context, in *O11yLLMIngestScore) (*O11yLLMScoreOut, error) {
|
||||
out := new(O11yLLMScoreOut)
|
||||
return out, relayO11y(ctx, http.MethodPost, "/llm/scores", nil, in, out)
|
||||
return out, relay(ctx, http.MethodPost, o11yRoot+"/llm/scores", nil, in, out)
|
||||
}
|
||||
|
||||
// llmGetScore returns a single score by id.
|
||||
@@ -156,14 +156,14 @@ func llmCreateScore(ctx context.Context, in *O11yLLMIngestScore) (*O11yLLMScoreO
|
||||
// Callers need the viewer role; the runtime's own gate enforces it.
|
||||
func llmGetScore(ctx context.Context, in *O11yLLMScoreRef) (*O11yLLMScoreOut, error) {
|
||||
out := new(O11yLLMScoreOut)
|
||||
return out, relayO11y(ctx, http.MethodGet, "/llm/score/"+in.ID, nil, nil, out)
|
||||
return out, relay(ctx, http.MethodGet, o11yRoot+"/llm/score/"+in.ID, nil, nil, out)
|
||||
}
|
||||
|
||||
// llmDeleteScore hard-deletes a score by id.
|
||||
//
|
||||
// Callers need the editor role; the runtime's own gate enforces it.
|
||||
func llmDeleteScore(ctx context.Context, in *O11yLLMScoreRef) (*struct{}, error) {
|
||||
return nil, relayO11y(ctx, http.MethodDelete, "/llm/score/"+in.ID, nil, nil, nil)
|
||||
return nil, relay(ctx, http.MethodDelete, o11yRoot+"/llm/score/"+in.ID, nil, nil, nil)
|
||||
}
|
||||
|
||||
// llmListAnnotations lists human annotations on traces and observations,
|
||||
@@ -172,7 +172,7 @@ func llmDeleteScore(ctx context.Context, in *O11yLLMScoreRef) (*struct{}, error)
|
||||
// Callers need the viewer role; the runtime's own gate enforces it.
|
||||
func llmListAnnotations(ctx context.Context, in *O11yLLMAnnotationsQuery) (*O11yLLMAnnotationsOut, error) {
|
||||
out := new(O11yLLMAnnotationsOut)
|
||||
return out, relayO11y(ctx, http.MethodGet, "/llm/annotation", query(
|
||||
return out, relay(ctx, http.MethodGet, o11yRoot+"/llm/annotation", query(
|
||||
"traceId", in.TraceID,
|
||||
"queue", in.Queue,
|
||||
"status", in.Status,
|
||||
@@ -188,7 +188,7 @@ func llmListAnnotations(ctx context.Context, in *O11yLLMAnnotationsQuery) (*O11y
|
||||
// validates the payload and stamps the annotation's author and org.
|
||||
func llmCreateAnnotation(ctx context.Context, in *O11yLLMIngestAnnotation) (*O11yLLMAnnotationOut, error) {
|
||||
out := new(O11yLLMAnnotationOut)
|
||||
return out, relayO11y(ctx, http.MethodPost, "/llm/annotation", nil, in, out)
|
||||
return out, relay(ctx, http.MethodPost, o11yRoot+"/llm/annotation", nil, in, out)
|
||||
}
|
||||
|
||||
// llmListPricingRules returns the LLM pricing rules for the caller's org, with
|
||||
@@ -197,7 +197,7 @@ func llmCreateAnnotation(ctx context.Context, in *O11yLLMIngestAnnotation) (*O11
|
||||
// Callers need the viewer role; the runtime's own gate enforces it.
|
||||
func llmListPricingRules(ctx context.Context, in *O11yLLMPricingRulesQuery) (*O11yLLMPricingRulesOut, error) {
|
||||
out := new(O11yLLMPricingRulesOut)
|
||||
return out, relayO11y(ctx, http.MethodGet, "/llm_pricing_rules", query(
|
||||
return out, relay(ctx, http.MethodGet, o11yRoot+"/llm_pricing_rules", query(
|
||||
"q", in.Search,
|
||||
"isOverride", in.IsOverride,
|
||||
"offset", in.Offset,
|
||||
@@ -212,7 +212,7 @@ func llmListPricingRules(ctx context.Context, in *O11yLLMPricingRulesQuery) (*O1
|
||||
//
|
||||
// Callers need the admin role; the runtime's own gate enforces it.
|
||||
func llmUpsertPricingRules(ctx context.Context, in *O11yLLMUpdatablePricingRules) (*struct{}, error) {
|
||||
return nil, relayO11y(ctx, http.MethodPut, "/llm_pricing_rules", nil, in, nil)
|
||||
return nil, relay(ctx, http.MethodPut, o11yRoot+"/llm_pricing_rules", nil, in, nil)
|
||||
}
|
||||
|
||||
// llmGetPricingRule returns a single LLM pricing rule by id.
|
||||
@@ -220,7 +220,7 @@ func llmUpsertPricingRules(ctx context.Context, in *O11yLLMUpdatablePricingRules
|
||||
// Callers need the viewer role; the runtime's own gate enforces it.
|
||||
func llmGetPricingRule(ctx context.Context, in *O11yLLMPricingRuleRef) (*O11yLLMPricingRuleOut, error) {
|
||||
out := new(O11yLLMPricingRuleOut)
|
||||
return out, relayO11y(ctx, http.MethodGet, "/llm_pricing_rules/"+in.ID, nil, nil, out)
|
||||
return out, relay(ctx, http.MethodGet, o11yRoot+"/llm_pricing_rules/"+in.ID, nil, nil, out)
|
||||
}
|
||||
|
||||
// llmDeletePricingRule hard-deletes a pricing rule by id. If the rule was
|
||||
@@ -228,14 +228,14 @@ func llmGetPricingRule(ctx context.Context, in *O11yLLMPricingRuleRef) (*O11yLLM
|
||||
//
|
||||
// Callers need the admin role; the runtime's own gate enforces it.
|
||||
func llmDeletePricingRule(ctx context.Context, in *O11yLLMPricingRuleRef) (*struct{}, error) {
|
||||
return nil, relayO11y(ctx, http.MethodDelete, "/llm_pricing_rules/"+in.ID, nil, nil, nil)
|
||||
return nil, relay(ctx, http.MethodDelete, o11yRoot+"/llm_pricing_rules/"+in.ID, nil, nil, nil)
|
||||
}
|
||||
|
||||
// viewParams renders the shared span-view filter onto the wire — the ONE place
|
||||
// the four views' query is encoded, so they cannot drift. Start and End are
|
||||
// unix-millisecond epochs; a non-positive value is left off, and the runtime
|
||||
// reads an absent window as the last 24h. The tenant is NOT among these: it is
|
||||
// the validated X-Org-Id relayO11y forwards, never a caller-supplied param.
|
||||
// the validated X-Org-Id relay forwards, never a caller-supplied param.
|
||||
func viewParams(in *O11yLLMViewQuery) url.Values {
|
||||
params := query(
|
||||
"traceId", in.TraceID,
|
||||
|
||||
@@ -1,348 +0,0 @@
|
||||
// Code generated by zipdoc; DO NOT EDIT.
|
||||
//
|
||||
// The llmobs face's slice of the package documentation, split into its own file
|
||||
// so concurrent slices do not contend on zipdoc_gen.go; a later `go generate`
|
||||
// folds every slice back into zipdoc_gen.go, at which point this file is deleted.
|
||||
|
||||
package o11y
|
||||
|
||||
import (
|
||||
"github.com/zap-proto/zip"
|
||||
)
|
||||
|
||||
func init() {
|
||||
zip.Describe("DELETE /v1/o11y/llm/score/:id", zip.Doc{
|
||||
Description: "Hard-deletes a score by id.\n\nCallers need the editor role; the runtime's own gate enforces it.",
|
||||
})
|
||||
zip.Describe("DELETE /v1/o11y/llm_pricing_rules/:id", zip.Doc{
|
||||
Description: "Hard-deletes a pricing rule by id. If the rule was\nauto-synced, the next sync cycle recreates it.\n\nCallers need the admin role; the runtime's own gate enforces it.",
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/llm/annotation", zip.Doc{
|
||||
Description: "Lists human annotations on traces and observations,\noptionally scoped to one review queue.\n\nCallers need the viewer role; the runtime's own gate enforces it.",
|
||||
Fields: map[string]string{
|
||||
"O11yLLMAnnotation.author": "Author is who wrote it.",
|
||||
"O11yLLMAnnotation.content": "Content is the note itself.",
|
||||
"O11yLLMAnnotation.createdAt": "CreatedAt is when the annotation was stored.",
|
||||
"O11yLLMAnnotation.id": "ID is the annotation's id.",
|
||||
"O11yLLMAnnotation.observationId": "ObservationID is the observation the annotation attaches to, when narrowed.",
|
||||
"O11yLLMAnnotation.queue": "Queue is the review queue the annotation sits in, when queued.",
|
||||
"O11yLLMAnnotation.status": "Status is the annotation's review status, e.g. PENDING.",
|
||||
"O11yLLMAnnotation.traceId": "TraceID is the trace the annotation attaches to.",
|
||||
"O11yLLMAnnotation.updatedAt": "UpdatedAt is when the annotation last changed.",
|
||||
"O11yLLMAnnotationsOut.data": "Data is the page.",
|
||||
"O11yLLMAnnotationsOut.status": "Status is \"success\".",
|
||||
"O11yLLMAnnotationsPage.items": "Items are the annotations, newest first.",
|
||||
"O11yLLMAnnotationsPage.limit": "Limit is the page cap the read ran with.",
|
||||
"O11yLLMAnnotationsPage.offset": "Offset is the row offset this page started at.",
|
||||
"O11yLLMAnnotationsPage.total": "Total is how many annotations match, across all pages.",
|
||||
"O11yLLMAnnotationsQuery.limit": "Limit caps how many rows come back.",
|
||||
"O11yLLMAnnotationsQuery.offset": "Offset is how many rows to skip, for paging.",
|
||||
"O11yLLMAnnotationsQuery.queue": "Queue narrows to one review queue.",
|
||||
"O11yLLMAnnotationsQuery.status": "Status narrows to one review status, e.g. PENDING.",
|
||||
"O11yLLMAnnotationsQuery.traceId": "TraceID narrows to annotations on one trace.",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/llm/observations", zip.Doc{
|
||||
Description: "Lists gen_ai spans as LLM observations — each an LLM call with\nits model, token counts, cost and latency projected from gen_ai.* attributes,\nnewest first, over the query window.\n\nCallers need the viewer role; the runtime's own gate enforces it, and scopes\nthe read to the caller's validated tenant.",
|
||||
Fields: map[string]string{
|
||||
"O11yLLMObservation.completionTokens": "CompletionTokens is the output token count.",
|
||||
"O11yLLMObservation.id": "ID is the observation's id (the span id).",
|
||||
"O11yLLMObservation.latencyMs": "LatencyMs is how long it took, in milliseconds.",
|
||||
"O11yLLMObservation.model": "Model is the model that served it.",
|
||||
"O11yLLMObservation.name": "Name is the observation's name.",
|
||||
"O11yLLMObservation.parentObservationId": "ParentID is the parent observation, when the span has one.",
|
||||
"O11yLLMObservation.promptTokens": "PromptTokens is the input token count.",
|
||||
"O11yLLMObservation.provider": "Provider is the model's provider.",
|
||||
"O11yLLMObservation.serviceName": "ServiceName is the app that emitted it.",
|
||||
"O11yLLMObservation.sessionId": "SessionID is the conversation the observation belongs to.",
|
||||
"O11yLLMObservation.startTime": "StartTime is when the observation started.",
|
||||
"O11yLLMObservation.statusCode": "StatusCode is the observation's status, e.g. OK, ERROR.",
|
||||
"O11yLLMObservation.totalCost": "TotalCost is the observation's cost.",
|
||||
"O11yLLMObservation.totalTokens": "TotalTokens is the sum of prompt and completion tokens.",
|
||||
"O11yLLMObservation.traceId": "TraceID is the trace the observation belongs to.",
|
||||
"O11yLLMObservation.type": "Type is the observation kind, e.g. chat, embeddings, tool.",
|
||||
"O11yLLMObservation.userId": "UserID is the end user the observation is attributed to.",
|
||||
"O11yLLMObservationsOut.data": "Data is the page.",
|
||||
"O11yLLMObservationsOut.status": "Status is \"success\".",
|
||||
"O11yLLMObservationsPage.items": "Items are the observations, newest first.",
|
||||
"O11yLLMObservationsPage.limit": "Limit is the page cap the read ran with.",
|
||||
"O11yLLMObservationsPage.offset": "Offset is the row offset this page started at.",
|
||||
"O11yLLMViewQuery.end": "End is the end of the window as a unix-millisecond epoch. Zero means now.",
|
||||
"O11yLLMViewQuery.limit": "Limit caps how many rows come back.",
|
||||
"O11yLLMViewQuery.model": "Model narrows the view to one model.",
|
||||
"O11yLLMViewQuery.name": "Name narrows the view to observations of one name.",
|
||||
"O11yLLMViewQuery.offset": "Offset is how many rows to skip, for paging.",
|
||||
"O11yLLMViewQuery.sessionId": "SessionID narrows the view to one conversation.",
|
||||
"O11yLLMViewQuery.start": "Start is the start of the window as a unix-millisecond epoch. Zero means\n24h before the end.",
|
||||
"O11yLLMViewQuery.traceId": "TraceID narrows the view to one trace.",
|
||||
"O11yLLMViewQuery.userId": "UserID narrows the view to one end user.",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/llm/score/:id", zip.Doc{
|
||||
Description: "Returns a single score by id.\n\nCallers need the viewer role; the runtime's own gate enforces it.",
|
||||
Fields: map[string]string{
|
||||
"O11yLLMScore.comment": "Comment is a free-text note.",
|
||||
"O11yLLMScore.createdAt": "CreatedAt is when the score was stored.",
|
||||
"O11yLLMScore.createdBy": "CreatedBy is who created the score.",
|
||||
"O11yLLMScore.dataType": "DataType is the score's kind — NUMERIC or CATEGORICAL.",
|
||||
"O11yLLMScore.id": "ID is the score's id.",
|
||||
"O11yLLMScore.name": "Name is the score's name.",
|
||||
"O11yLLMScore.observationId": "ObservationID is the observation the score attaches to, when narrowed.",
|
||||
"O11yLLMScore.source": "Source is where the score came from, e.g. API, EVAL.",
|
||||
"O11yLLMScore.stringValue": "StringValue is the categorical score, when the score is categorical.",
|
||||
"O11yLLMScore.timestamp": "Timestamp is the score's own event time.",
|
||||
"O11yLLMScore.traceId": "TraceID is the trace the score attaches to.",
|
||||
"O11yLLMScore.updatedAt": "UpdatedAt is when the score last changed.",
|
||||
"O11yLLMScore.value": "Value is the numeric score.",
|
||||
"O11yLLMScoreOut.data": "Data is the score.",
|
||||
"O11yLLMScoreOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/llm/scores", zip.Doc{
|
||||
Description: "Lists eval scores and human-feedback signals attached to traces\nand observations, newest first.\n\nCallers need the viewer role; the runtime's own gate enforces it.",
|
||||
Fields: map[string]string{
|
||||
"O11yLLMScore.comment": "Comment is a free-text note.",
|
||||
"O11yLLMScore.createdAt": "CreatedAt is when the score was stored.",
|
||||
"O11yLLMScore.createdBy": "CreatedBy is who created the score.",
|
||||
"O11yLLMScore.dataType": "DataType is the score's kind — NUMERIC or CATEGORICAL.",
|
||||
"O11yLLMScore.id": "ID is the score's id.",
|
||||
"O11yLLMScore.name": "Name is the score's name.",
|
||||
"O11yLLMScore.observationId": "ObservationID is the observation the score attaches to, when narrowed.",
|
||||
"O11yLLMScore.source": "Source is where the score came from, e.g. API, EVAL.",
|
||||
"O11yLLMScore.stringValue": "StringValue is the categorical score, when the score is categorical.",
|
||||
"O11yLLMScore.timestamp": "Timestamp is the score's own event time.",
|
||||
"O11yLLMScore.traceId": "TraceID is the trace the score attaches to.",
|
||||
"O11yLLMScore.updatedAt": "UpdatedAt is when the score last changed.",
|
||||
"O11yLLMScore.value": "Value is the numeric score.",
|
||||
"O11yLLMScoresOut.data": "Data is the page.",
|
||||
"O11yLLMScoresOut.status": "Status is \"success\".",
|
||||
"O11yLLMScoresPage.items": "Items are the scores, newest first.",
|
||||
"O11yLLMScoresPage.limit": "Limit is the page cap the read ran with.",
|
||||
"O11yLLMScoresPage.offset": "Offset is the row offset this page started at.",
|
||||
"O11yLLMScoresPage.total": "Total is how many scores match, across all pages.",
|
||||
"O11yLLMScoresQuery.limit": "Limit caps how many rows come back.",
|
||||
"O11yLLMScoresQuery.name": "Name narrows to scores of one name.",
|
||||
"O11yLLMScoresQuery.observationId": "ObservationID narrows to scores on one observation.",
|
||||
"O11yLLMScoresQuery.offset": "Offset is how many rows to skip, for paging.",
|
||||
"O11yLLMScoresQuery.source": "Source narrows to scores from one source, e.g. API, EVAL.",
|
||||
"O11yLLMScoresQuery.traceId": "TraceID narrows to scores on one trace.",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/llm/sessions", zip.Doc{
|
||||
Description: "Lists conversations — gen_ai spans grouped by session.id, with\ntheir trace and observation counts, tokens and cost.\n\nCallers need the viewer role; the runtime's own gate enforces it.",
|
||||
Fields: map[string]string{
|
||||
"O11yLLMSession.completionTokens": "CompletionTokens is the conversation's total output tokens.",
|
||||
"O11yLLMSession.id": "ID is the session id.",
|
||||
"O11yLLMSession.observations": "Observations is how many observations the conversation holds.",
|
||||
"O11yLLMSession.promptTokens": "PromptTokens is the conversation's total input tokens.",
|
||||
"O11yLLMSession.totalCost": "TotalCost is the conversation's total cost.",
|
||||
"O11yLLMSession.totalTokens": "TotalTokens is the conversation's total tokens.",
|
||||
"O11yLLMSession.traces": "Traces is how many traces the conversation holds.",
|
||||
"O11yLLMSession.userId": "UserID is the end user the conversation is attributed to.",
|
||||
"O11yLLMSessionsOut.data": "Data is the page.",
|
||||
"O11yLLMSessionsOut.status": "Status is \"success\".",
|
||||
"O11yLLMSessionsPage.items": "Items are the conversations, newest first.",
|
||||
"O11yLLMSessionsPage.limit": "Limit is the page cap the read ran with.",
|
||||
"O11yLLMSessionsPage.offset": "Offset is the row offset this page started at.",
|
||||
"O11yLLMViewQuery.end": "End is the end of the window as a unix-millisecond epoch. Zero means now.",
|
||||
"O11yLLMViewQuery.limit": "Limit caps how many rows come back.",
|
||||
"O11yLLMViewQuery.model": "Model narrows the view to one model.",
|
||||
"O11yLLMViewQuery.name": "Name narrows the view to observations of one name.",
|
||||
"O11yLLMViewQuery.offset": "Offset is how many rows to skip, for paging.",
|
||||
"O11yLLMViewQuery.sessionId": "SessionID narrows the view to one conversation.",
|
||||
"O11yLLMViewQuery.start": "Start is the start of the window as a unix-millisecond epoch. Zero means\n24h before the end.",
|
||||
"O11yLLMViewQuery.traceId": "TraceID narrows the view to one trace.",
|
||||
"O11yLLMViewQuery.userId": "UserID narrows the view to one end user.",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/llm/traces", zip.Doc{
|
||||
Description: "Lists LLM traces — gen_ai spans grouped by trace_id, with cost,\ntokens and latency rolled up across each trace.\n\nCallers need the viewer role; the runtime's own gate enforces it.",
|
||||
Fields: map[string]string{
|
||||
"O11yLLMTrace.completionTokens": "CompletionTokens is the trace's total output tokens.",
|
||||
"O11yLLMTrace.id": "ID is the trace id.",
|
||||
"O11yLLMTrace.latencyMs": "LatencyMs is the trace's span, in milliseconds.",
|
||||
"O11yLLMTrace.observations": "Observations is how many observations the trace holds.",
|
||||
"O11yLLMTrace.promptTokens": "PromptTokens is the trace's total input tokens.",
|
||||
"O11yLLMTrace.serviceName": "ServiceName is the app that emitted it.",
|
||||
"O11yLLMTrace.sessionId": "SessionID is the conversation the trace belongs to.",
|
||||
"O11yLLMTrace.totalCost": "TotalCost is the trace's total cost.",
|
||||
"O11yLLMTrace.totalTokens": "TotalTokens is the trace's total tokens.",
|
||||
"O11yLLMTrace.userId": "UserID is the end user the trace is attributed to.",
|
||||
"O11yLLMTracesOut.data": "Data is the page.",
|
||||
"O11yLLMTracesOut.status": "Status is \"success\".",
|
||||
"O11yLLMTracesPage.items": "Items are the traces, newest first.",
|
||||
"O11yLLMTracesPage.limit": "Limit is the page cap the read ran with.",
|
||||
"O11yLLMTracesPage.offset": "Offset is the row offset this page started at.",
|
||||
"O11yLLMViewQuery.end": "End is the end of the window as a unix-millisecond epoch. Zero means now.",
|
||||
"O11yLLMViewQuery.limit": "Limit caps how many rows come back.",
|
||||
"O11yLLMViewQuery.model": "Model narrows the view to one model.",
|
||||
"O11yLLMViewQuery.name": "Name narrows the view to observations of one name.",
|
||||
"O11yLLMViewQuery.offset": "Offset is how many rows to skip, for paging.",
|
||||
"O11yLLMViewQuery.sessionId": "SessionID narrows the view to one conversation.",
|
||||
"O11yLLMViewQuery.start": "Start is the start of the window as a unix-millisecond epoch. Zero means\n24h before the end.",
|
||||
"O11yLLMViewQuery.traceId": "TraceID narrows the view to one trace.",
|
||||
"O11yLLMViewQuery.userId": "UserID narrows the view to one end user.",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/llm/users", zip.Doc{
|
||||
Description: "Lists end users — gen_ai spans grouped by user.id, with their\nsession, trace and observation counts, tokens and cost.\n\nCallers need the viewer role; the runtime's own gate enforces it.",
|
||||
Fields: map[string]string{
|
||||
"O11yLLMUser.completionTokens": "CompletionTokens is their total output tokens.",
|
||||
"O11yLLMUser.id": "ID is the end user's id (user.id).",
|
||||
"O11yLLMUser.observations": "Observations is how many observations they produced.",
|
||||
"O11yLLMUser.promptTokens": "PromptTokens is their total input tokens.",
|
||||
"O11yLLMUser.sessions": "Sessions is how many conversations they had.",
|
||||
"O11yLLMUser.totalCost": "TotalCost is their total cost.",
|
||||
"O11yLLMUser.totalTokens": "TotalTokens is their total tokens.",
|
||||
"O11yLLMUser.traces": "Traces is how many traces they produced.",
|
||||
"O11yLLMUsersOut.data": "Data is the page.",
|
||||
"O11yLLMUsersOut.status": "Status is \"success\".",
|
||||
"O11yLLMUsersPage.items": "Items are the end users, newest first.",
|
||||
"O11yLLMUsersPage.limit": "Limit is the page cap the read ran with.",
|
||||
"O11yLLMUsersPage.offset": "Offset is the row offset this page started at.",
|
||||
"O11yLLMViewQuery.end": "End is the end of the window as a unix-millisecond epoch. Zero means now.",
|
||||
"O11yLLMViewQuery.limit": "Limit caps how many rows come back.",
|
||||
"O11yLLMViewQuery.model": "Model narrows the view to one model.",
|
||||
"O11yLLMViewQuery.name": "Name narrows the view to observations of one name.",
|
||||
"O11yLLMViewQuery.offset": "Offset is how many rows to skip, for paging.",
|
||||
"O11yLLMViewQuery.sessionId": "SessionID narrows the view to one conversation.",
|
||||
"O11yLLMViewQuery.start": "Start is the start of the window as a unix-millisecond epoch. Zero means\n24h before the end.",
|
||||
"O11yLLMViewQuery.traceId": "TraceID narrows the view to one trace.",
|
||||
"O11yLLMViewQuery.userId": "UserID narrows the view to one end user.",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/llm_pricing_rules", zip.Doc{
|
||||
Description: "Returns the LLM pricing rules for the caller's org, with\npagination and an optional search and override filter.\n\nCallers need the viewer role; the runtime's own gate enforces it.",
|
||||
Fields: map[string]string{
|
||||
"O11yLLMPricingCacheCosts.mode": "Mode is how cached tokens are counted — subtract (inside input_tokens,\nOpenAI-style), additive (reported separately, Anthropic-style) or unknown.",
|
||||
"O11yLLMPricingCacheCosts.read": "Read is the cost per unit of cache-read tokens.",
|
||||
"O11yLLMPricingCacheCosts.write": "Write is the cost per unit of cache-write tokens.",
|
||||
"O11yLLMPricingRule.createdAt": "CreatedAt is when the rule was stored.",
|
||||
"O11yLLMPricingRule.createdBy": "CreatedBy is who created the rule.",
|
||||
"O11yLLMPricingRule.enabled": "Enabled says whether the rule is on.",
|
||||
"O11yLLMPricingRule.id": "ID is the rule's id.",
|
||||
"O11yLLMPricingRule.isOverride": "IsOverride marks the rule user-pinned; when true the sync job skips it.",
|
||||
"O11yLLMPricingRule.modelName": "Model is the model the rule prices.",
|
||||
"O11yLLMPricingRule.modelPattern": "ModelPattern are the model-name globs the rule matches.",
|
||||
"O11yLLMPricingRule.orgId": "OrgID is the org the rule belongs to.",
|
||||
"O11yLLMPricingRule.pricing": "Pricing is the per-unit cost.",
|
||||
"O11yLLMPricingRule.provider": "Provider is the model's provider.",
|
||||
"O11yLLMPricingRule.sourceId": "SourceID is the upstream source the rule was synced from, when synced.",
|
||||
"O11yLLMPricingRule.syncedAt": "SyncedAt is when the rule was last synced, when it is synced.",
|
||||
"O11yLLMPricingRule.unit": "Unit is the pricing unit, e.g. per_million_tokens.",
|
||||
"O11yLLMPricingRule.updatedAt": "UpdatedAt is when the rule last changed.",
|
||||
"O11yLLMPricingRule.updatedBy": "UpdatedBy is who last changed it.",
|
||||
"O11yLLMPricingRulesOut.data": "Data is the page.",
|
||||
"O11yLLMPricingRulesOut.status": "Status is \"success\".",
|
||||
"O11yLLMPricingRulesPage.items": "Items are the rules.",
|
||||
"O11yLLMPricingRulesPage.limit": "Limit is the page cap the read ran with.",
|
||||
"O11yLLMPricingRulesPage.offset": "Offset is the row offset this page started at.",
|
||||
"O11yLLMPricingRulesPage.total": "Total is how many rules match, across all pages.",
|
||||
"O11yLLMPricingRulesQuery.isOverride": "IsOverride, when \"true\" or \"false\", narrows to user-pinned rules or to\nsynced ones; empty returns both. It is a string because a query param is a\nstring on the wire, and the runtime reads absent as \"no filter\".",
|
||||
"O11yLLMPricingRulesQuery.limit": "Limit caps how many rows come back.",
|
||||
"O11yLLMPricingRulesQuery.offset": "Offset is how many rows to skip, for paging.",
|
||||
"O11yLLMPricingRulesQuery.q": "Search matches rules by model or provider.",
|
||||
"O11yLLMRulePricing.cache": "Cache is the cost of cached tokens, when the model prices them.",
|
||||
"O11yLLMRulePricing.input": "Input is the cost per unit of input tokens.",
|
||||
"O11yLLMRulePricing.output": "Output is the cost per unit of output tokens.",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/llm_pricing_rules/:id", zip.Doc{
|
||||
Description: "Returns a single LLM pricing rule by id.\n\nCallers need the viewer role; the runtime's own gate enforces it.",
|
||||
Fields: map[string]string{
|
||||
"O11yLLMPricingCacheCosts.mode": "Mode is how cached tokens are counted — subtract (inside input_tokens,\nOpenAI-style), additive (reported separately, Anthropic-style) or unknown.",
|
||||
"O11yLLMPricingCacheCosts.read": "Read is the cost per unit of cache-read tokens.",
|
||||
"O11yLLMPricingCacheCosts.write": "Write is the cost per unit of cache-write tokens.",
|
||||
"O11yLLMPricingRule.createdAt": "CreatedAt is when the rule was stored.",
|
||||
"O11yLLMPricingRule.createdBy": "CreatedBy is who created the rule.",
|
||||
"O11yLLMPricingRule.enabled": "Enabled says whether the rule is on.",
|
||||
"O11yLLMPricingRule.id": "ID is the rule's id.",
|
||||
"O11yLLMPricingRule.isOverride": "IsOverride marks the rule user-pinned; when true the sync job skips it.",
|
||||
"O11yLLMPricingRule.modelName": "Model is the model the rule prices.",
|
||||
"O11yLLMPricingRule.modelPattern": "ModelPattern are the model-name globs the rule matches.",
|
||||
"O11yLLMPricingRule.orgId": "OrgID is the org the rule belongs to.",
|
||||
"O11yLLMPricingRule.pricing": "Pricing is the per-unit cost.",
|
||||
"O11yLLMPricingRule.provider": "Provider is the model's provider.",
|
||||
"O11yLLMPricingRule.sourceId": "SourceID is the upstream source the rule was synced from, when synced.",
|
||||
"O11yLLMPricingRule.syncedAt": "SyncedAt is when the rule was last synced, when it is synced.",
|
||||
"O11yLLMPricingRule.unit": "Unit is the pricing unit, e.g. per_million_tokens.",
|
||||
"O11yLLMPricingRule.updatedAt": "UpdatedAt is when the rule last changed.",
|
||||
"O11yLLMPricingRule.updatedBy": "UpdatedBy is who last changed it.",
|
||||
"O11yLLMPricingRuleOut.data": "Data is the rule.",
|
||||
"O11yLLMPricingRuleOut.status": "Status is \"success\".",
|
||||
"O11yLLMRulePricing.cache": "Cache is the cost of cached tokens, when the model prices them.",
|
||||
"O11yLLMRulePricing.input": "Input is the cost per unit of input tokens.",
|
||||
"O11yLLMRulePricing.output": "Output is the cost per unit of output tokens.",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/llm/annotation", zip.Doc{
|
||||
Description: "Adds a human annotation to a trace or observation,\noptionally in a review queue.\n\nCallers need the editor role; the runtime's own gate enforces it, and it\nvalidates the payload and stamps the annotation's author and org.",
|
||||
Fields: map[string]string{
|
||||
"O11yLLMAnnotation.author": "Author is who wrote it.",
|
||||
"O11yLLMAnnotation.content": "Content is the note itself.",
|
||||
"O11yLLMAnnotation.createdAt": "CreatedAt is when the annotation was stored.",
|
||||
"O11yLLMAnnotation.id": "ID is the annotation's id.",
|
||||
"O11yLLMAnnotation.observationId": "ObservationID is the observation the annotation attaches to, when narrowed.",
|
||||
"O11yLLMAnnotation.queue": "Queue is the review queue the annotation sits in, when queued.",
|
||||
"O11yLLMAnnotation.status": "Status is the annotation's review status, e.g. PENDING.",
|
||||
"O11yLLMAnnotation.traceId": "TraceID is the trace the annotation attaches to.",
|
||||
"O11yLLMAnnotation.updatedAt": "UpdatedAt is when the annotation last changed.",
|
||||
"O11yLLMAnnotationOut.data": "Data is the annotation.",
|
||||
"O11yLLMAnnotationOut.status": "Status is \"success\".",
|
||||
"O11yLLMIngestAnnotation.content": "Content is the note itself. Required.",
|
||||
"O11yLLMIngestAnnotation.observationId": "ObservationID is the single observation the annotation attaches to, when\nnarrowed to one.",
|
||||
"O11yLLMIngestAnnotation.queue": "Queue is the review queue to file the annotation in.",
|
||||
"O11yLLMIngestAnnotation.status": "Status is the annotation's initial review status. Defaults to PENDING.",
|
||||
"O11yLLMIngestAnnotation.traceId": "TraceID is the trace the annotation attaches to. Required.",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/llm/scores", zip.Doc{
|
||||
Description: "Attaches an eval score or human-feedback signal to a trace or a\nsingle observation.\n\nCallers need the editor role; the runtime's own gate enforces it, and it\nvalidates the payload and stamps the score's author and org.",
|
||||
Fields: map[string]string{
|
||||
"O11yLLMIngestScore.comment": "Comment is a free-text note.",
|
||||
"O11yLLMIngestScore.dataType": "DataType is the score's kind — NUMERIC or CATEGORICAL. Defaults from the\nvalue when empty.",
|
||||
"O11yLLMIngestScore.name": "Name is the score's name, e.g. helpfulness. Required.",
|
||||
"O11yLLMIngestScore.observationId": "ObservationID is the single observation the score attaches to, when\nnarrowed to one.",
|
||||
"O11yLLMIngestScore.source": "Source is where the score came from, e.g. API, EVAL. Defaults to API.",
|
||||
"O11yLLMIngestScore.stringValue": "StringValue is the categorical score, when the score is categorical.",
|
||||
"O11yLLMIngestScore.traceId": "TraceID is the trace the score attaches to. Required.",
|
||||
"O11yLLMIngestScore.value": "Value is the numeric score.",
|
||||
"O11yLLMScore.comment": "Comment is a free-text note.",
|
||||
"O11yLLMScore.createdAt": "CreatedAt is when the score was stored.",
|
||||
"O11yLLMScore.createdBy": "CreatedBy is who created the score.",
|
||||
"O11yLLMScore.dataType": "DataType is the score's kind — NUMERIC or CATEGORICAL.",
|
||||
"O11yLLMScore.id": "ID is the score's id.",
|
||||
"O11yLLMScore.name": "Name is the score's name.",
|
||||
"O11yLLMScore.observationId": "ObservationID is the observation the score attaches to, when narrowed.",
|
||||
"O11yLLMScore.source": "Source is where the score came from, e.g. API, EVAL.",
|
||||
"O11yLLMScore.stringValue": "StringValue is the categorical score, when the score is categorical.",
|
||||
"O11yLLMScore.timestamp": "Timestamp is the score's own event time.",
|
||||
"O11yLLMScore.traceId": "TraceID is the trace the score attaches to.",
|
||||
"O11yLLMScore.updatedAt": "UpdatedAt is when the score last changed.",
|
||||
"O11yLLMScore.value": "Value is the numeric score.",
|
||||
"O11yLLMScoreOut.data": "Data is the score.",
|
||||
"O11yLLMScoreOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("PUT /v1/o11y/llm_pricing_rules", zip.Doc{
|
||||
Description: "Writes the pricing-rule batch — the single write\nendpoint used by both the user and the Zeus sync job. Per-rule match is by id,\nthen sourceId, then insert; an override row is fully preserved when the\nrequest omits isOverride, only its synced_at stamped.\n\nCallers need the admin role; the runtime's own gate enforces it.",
|
||||
Fields: map[string]string{
|
||||
"O11yLLMPricingCacheCosts.mode": "Mode is how cached tokens are counted — subtract (inside input_tokens,\nOpenAI-style), additive (reported separately, Anthropic-style) or unknown.",
|
||||
"O11yLLMPricingCacheCosts.read": "Read is the cost per unit of cache-read tokens.",
|
||||
"O11yLLMPricingCacheCosts.write": "Write is the cost per unit of cache-write tokens.",
|
||||
"O11yLLMRulePricing.cache": "Cache is the cost of cached tokens, when the model prices them.",
|
||||
"O11yLLMRulePricing.input": "Input is the cost per unit of input tokens.",
|
||||
"O11yLLMRulePricing.output": "Output is the cost per unit of output tokens.",
|
||||
"O11yLLMUpdatablePricingRule.enabled": "Enabled turns the rule on.",
|
||||
"O11yLLMUpdatablePricingRule.id": "ID matches an existing rule by its id.",
|
||||
"O11yLLMUpdatablePricingRule.isOverride": "IsOverride pins the rule so the sync job skips it. Omit to leave a matched\noverride untouched.",
|
||||
"O11yLLMUpdatablePricingRule.modelName": "Model is the model the rule prices. Required.",
|
||||
"O11yLLMUpdatablePricingRule.modelPattern": "ModelPattern are the model-name globs the rule matches. Required.",
|
||||
"O11yLLMUpdatablePricingRule.pricing": "Pricing is the per-unit cost. Required.",
|
||||
"O11yLLMUpdatablePricingRule.provider": "Provider is the model's provider. Required.",
|
||||
"O11yLLMUpdatablePricingRule.sourceId": "SourceID matches an existing rule by its upstream source id.",
|
||||
"O11yLLMUpdatablePricingRule.unit": "Unit is the pricing unit, e.g. per_million_tokens. Required.",
|
||||
"O11yLLMUpdatablePricingRules.rules": "Rules are the rules to create or update, matched per rule.",
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -13,7 +13,7 @@ package o11y
|
||||
//
|
||||
// THE WIRE DOES NOT MOVE, and the way it does not move is that these ops do not
|
||||
// re-implement the reads and writes. Each hands the call to the SAME runtime
|
||||
// handler the wildcard delegates to (see relayAt), so identity resolution, the
|
||||
// handler the wildcard delegates to (see relay.go), so identity resolution, the
|
||||
// org gate, the role check (viewer on the reads, editor on the writes — the
|
||||
// exact ViewAccess/EditAccess gates the runtime has always run), the audit
|
||||
// record and the success envelope are all still the runtime's, executed in the
|
||||
@@ -33,23 +33,14 @@ package o11y
|
||||
// reaches the runtime untouched (logs_test.go pins both halves).
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/zap-proto/zip"
|
||||
)
|
||||
|
||||
// o11yRoot is the o11y face's path root, spelled ONCE: the group the logs ops
|
||||
// register on, and the first segment relayAt rebuilds a call onto.
|
||||
const o11yRoot = "/v1/o11y"
|
||||
|
||||
// mountLogs registers the nine typed logs ops on the native router. The
|
||||
// pipelines routes keep the mux tree's discipline — preview registers before
|
||||
// the parameterised version read so a version can never shadow it.
|
||||
@@ -75,7 +66,7 @@ func mountLogs(app *zip.App) {
|
||||
// Callers need the viewer role; the runtime's own gate enforces it.
|
||||
func logRecords(ctx context.Context, in *O11yLogRecordsIn) (*O11yLogRecordsOut, error) {
|
||||
out := new(O11yLogRecordsOut)
|
||||
return out, relayAt(ctx, http.MethodGet, "/logs", query(
|
||||
return out, relay(ctx, http.MethodGet, o11yRoot+"/logs", query(
|
||||
"limit", in.Limit,
|
||||
"timestampStart", nanos(in.TimestampStart),
|
||||
"timestampEnd", nanos(in.TimestampEnd),
|
||||
@@ -86,18 +77,18 @@ func logRecords(ctx context.Context, in *O11yLogRecordsIn) (*O11yLogRecordsOut,
|
||||
// indexed columns, and the interesting ones seen in the data that could be.
|
||||
//
|
||||
// Callers need the viewer role; the runtime's own gate enforces it.
|
||||
func logFields(ctx context.Context, _ *struct{}) (*O11yLogFieldsOut, error) {
|
||||
out := new(O11yLogFieldsOut)
|
||||
return out, relayAt(ctx, http.MethodGet, "/logs/fields", nil, nil, out)
|
||||
func logFields(ctx context.Context, _ *struct{}) (*O11yFieldCatalogOut, error) {
|
||||
out := new(O11yFieldCatalogOut)
|
||||
return out, relay(ctx, http.MethodGet, o11yRoot+"/logs/fields", nil, nil, out)
|
||||
}
|
||||
|
||||
// logFieldUpdate changes how one log field is stored — selects or deselects it
|
||||
// as a materialized column and tunes its index — and echoes the setting back.
|
||||
//
|
||||
// Callers need the editor role; the runtime's own gate enforces it.
|
||||
func logFieldUpdate(ctx context.Context, in *O11yLogFieldSetting) (*O11yLogFieldSetting, error) {
|
||||
out := new(O11yLogFieldSetting)
|
||||
return out, relayAt(ctx, http.MethodPost, "/logs/fields", nil, in, out)
|
||||
func logFieldUpdate(ctx context.Context, in *O11yFieldSetting) (*O11yFieldSetting, error) {
|
||||
out := new(O11yFieldSetting)
|
||||
return out, relay(ctx, http.MethodPost, o11yRoot+"/logs/fields", nil, in, out)
|
||||
}
|
||||
|
||||
// logAggregate returns the logs aggregate buckets for the query window. The
|
||||
@@ -106,7 +97,7 @@ func logFieldUpdate(ctx context.Context, in *O11yLogFieldSetting) (*O11yLogField
|
||||
// Callers need the viewer role; the runtime's own gate enforces it.
|
||||
func logAggregate(ctx context.Context, _ *struct{}) (*O11yLogAggregateOut, error) {
|
||||
out := new(O11yLogAggregateOut)
|
||||
return out, relayAt(ctx, http.MethodGet, "/logs/aggregate", nil, nil, out)
|
||||
return out, relay(ctx, http.MethodGet, o11yRoot+"/logs/aggregate", nil, nil, out)
|
||||
}
|
||||
|
||||
// logPipelinePreview runs the given log parsing pipelines over the given
|
||||
@@ -116,7 +107,7 @@ func logAggregate(ctx context.Context, _ *struct{}) (*O11yLogAggregateOut, error
|
||||
// Callers need the viewer role; the runtime's own gate enforces it.
|
||||
func logPipelinePreview(ctx context.Context, in *O11yLogPipelinePreviewIn) (*O11yLogPipelinePreviewOut, error) {
|
||||
out := new(O11yLogPipelinePreviewOut)
|
||||
return out, relayAt(ctx, http.MethodPost, "/logs/pipelines/preview", nil, in, out)
|
||||
return out, relay(ctx, http.MethodPost, o11yRoot+"/logs/pipelines/preview", nil, in, out)
|
||||
}
|
||||
|
||||
// logPipelines returns the caller's org's log parsing pipelines at one config
|
||||
@@ -128,7 +119,7 @@ func logPipelines(ctx context.Context, in *O11yLogPipelinesIn) (*O11yLogPipeline
|
||||
out := new(O11yLogPipelinesOut)
|
||||
// The version goes on VERBATIM, as the segment the router matched, so the
|
||||
// runtime reads exactly the version the caller named.
|
||||
return out, relayAt(ctx, http.MethodGet, "/logs/pipelines/"+in.Version, nil, nil, out)
|
||||
return out, relay(ctx, http.MethodGet, o11yRoot+"/logs/pipelines/"+in.Version, nil, nil, out)
|
||||
}
|
||||
|
||||
// logPipelineCreate saves the given log parsing pipelines as the new config
|
||||
@@ -139,7 +130,7 @@ func logPipelines(ctx context.Context, in *O11yLogPipelinesIn) (*O11yLogPipeline
|
||||
// Callers need the editor role; the runtime's own gate enforces it.
|
||||
func logPipelineCreate(ctx context.Context, in *O11yLogPipelineCreateIn) (*O11yLogPipelinesOut, error) {
|
||||
out := new(O11yLogPipelinesOut)
|
||||
return out, relayAt(ctx, http.MethodPost, "/logs/pipelines", nil, in, out)
|
||||
return out, relay(ctx, http.MethodPost, o11yRoot+"/logs/pipelines", nil, in, out)
|
||||
}
|
||||
|
||||
// logPromote promotes and indexes log body paths: each named path is lifted
|
||||
@@ -149,7 +140,7 @@ func logPipelineCreate(ctx context.Context, in *O11yLogPipelineCreateIn) (*O11yL
|
||||
// Callers need the editor role; the runtime's own gate enforces it.
|
||||
func logPromote(ctx context.Context, in *O11yLogPromoteIn) (*O11yLogPromoteOut, error) {
|
||||
out := new(O11yLogPromoteOut)
|
||||
return out, relayAt(ctx, http.MethodPost, "/logs/promote_paths", nil, in, out)
|
||||
return out, relay(ctx, http.MethodPost, o11yRoot+"/logs/promote_paths", nil, in, out)
|
||||
}
|
||||
|
||||
// logPromoted lists the log body paths already promoted or indexed, with the
|
||||
@@ -158,7 +149,7 @@ func logPromote(ctx context.Context, in *O11yLogPromoteIn) (*O11yLogPromoteOut,
|
||||
// Callers need the viewer role; the runtime's own gate enforces it.
|
||||
func logPromoted(ctx context.Context, _ *struct{}) (*O11yLogPromotedOut, error) {
|
||||
out := new(O11yLogPromotedOut)
|
||||
return out, relayAt(ctx, http.MethodGet, "/logs/promote_paths", nil, nil, out)
|
||||
return out, relay(ctx, http.MethodGet, o11yRoot+"/logs/promote_paths", nil, nil, out)
|
||||
}
|
||||
|
||||
// ── inputs ────────────────────────────────────────────────────────────────────
|
||||
@@ -177,10 +168,13 @@ type O11yLogRecordsIn struct {
|
||||
TimestampEnd int64 `json:"timestampEnd,omitempty"`
|
||||
}
|
||||
|
||||
// O11yLogFieldSetting is one log field's storage setting — the request a caller
|
||||
// sends to change it, and the echo they get back. One name because it is one
|
||||
// shape: the write is acknowledged with the setting it wrote.
|
||||
type O11yLogFieldSetting struct {
|
||||
// O11yFieldSetting is one telemetry field's storage setting — the request a
|
||||
// caller sends to change it, and the echo they get back. One name because it is
|
||||
// one shape: the write is acknowledged with the setting it wrote. The logs face
|
||||
// and the traces face tune the SAME catalog through it (see traces.go); the
|
||||
// runtime has always used one type for both, so naming it per-signal here would
|
||||
// have invented two names for one value.
|
||||
type O11yFieldSetting struct {
|
||||
// Name is the field to tune. Required.
|
||||
Name string `json:"name" validate:"required"`
|
||||
// DataType is the field's data type, e.g. string, int64, float64, bool.
|
||||
@@ -266,16 +260,17 @@ type O11yLogRecordsOut struct {
|
||||
// the data.
|
||||
type O11yLogRow map[string]json.RawMessage
|
||||
|
||||
// O11yLogFieldsOut is the log field catalog.
|
||||
type O11yLogFieldsOut struct {
|
||||
// O11yFieldCatalogOut is a signal's field catalog — logs (here) and traces
|
||||
// (traces.go) both answer with it, because on the runtime they are one type.
|
||||
type O11yFieldCatalogOut struct {
|
||||
// Selected are the fields materialized as their own columns.
|
||||
Selected []O11yLogField `json:"selected"`
|
||||
Selected []O11yTelemetryField `json:"selected"`
|
||||
// Interesting are fields seen in the data that could be selected.
|
||||
Interesting []O11yLogField `json:"interesting"`
|
||||
Interesting []O11yTelemetryField `json:"interesting"`
|
||||
}
|
||||
|
||||
// O11yLogField is one field of the log field catalog.
|
||||
type O11yLogField struct {
|
||||
// O11yTelemetryField is one field of a signal's field catalog.
|
||||
type O11yTelemetryField struct {
|
||||
// Name is the field's name.
|
||||
Name string `json:"name"`
|
||||
// DataType is the field's data type, e.g. string, int64, float64, bool.
|
||||
@@ -608,99 +603,3 @@ type O11yLogPromotedOut struct {
|
||||
}
|
||||
|
||||
// ── the seam ──────────────────────────────────────────────────────────────────
|
||||
|
||||
// relayAt hands a typed op's call to the o11y runtime at o11yRoot+path and
|
||||
// decodes the answer into the op's Out. It is telemetry.go's relay with the
|
||||
// face root as the parameterised first segment instead of a burned-in
|
||||
// /v1/sentry — the sentry ops fold onto this seam whenever a pass may touch
|
||||
// both files at once; the two are kept twins until then.
|
||||
//
|
||||
// It is what keeps these nine ops a NAMING of the wire rather than a second
|
||||
// implementation of it. The handler it calls is the one SetHandler registered —
|
||||
// the same value the delegation wildcard forwards to — so the request runs the
|
||||
// whole chain it always ran, in order: the auth middleware resolves the same
|
||||
// identity, the SAME ViewAccess/EditAccess gate each route has always had
|
||||
// refuses the same callers, and the bytes it answers with are the bytes the
|
||||
// runtime wrote. There is no policy here: no tenant is resolved, no role is
|
||||
// checked, nothing is scoped. Those all happen where they already do, one
|
||||
// layer in.
|
||||
//
|
||||
// Identity is PROPAGATED, never minted: the gateway's assertion about the
|
||||
// caller travels on as the same headers it arrived on (zip.CallerOf reads
|
||||
// exactly the set the call plane forwards). A context with no request behind
|
||||
// it — a command, an agent call — carries no assertion, so the runtime's gate
|
||||
// refuses it, which is the honest answer rather than an identity invented at
|
||||
// this hop.
|
||||
//
|
||||
// A non-2xx becomes an error carrying the runtime's own status and reason, so
|
||||
// the status a caller sees is the status the runtime chose.
|
||||
func relayAt(ctx context.Context, method, path string, params url.Values, body, out any) error {
|
||||
h := getHandler()
|
||||
if h == nil {
|
||||
return zip.Errorf(http.StatusServiceUnavailable, "o11y runtime not initialized")
|
||||
}
|
||||
|
||||
target := o11yRoot + path
|
||||
if q := params.Encode(); q != "" {
|
||||
target += "?" + q
|
||||
}
|
||||
|
||||
payload := io.Reader(http.NoBody)
|
||||
if body != nil {
|
||||
b, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return zip.ErrBadRequest(err.Error())
|
||||
}
|
||||
payload = bytes.NewReader(b)
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, method, target, payload)
|
||||
if err != nil {
|
||||
return zip.ErrBadRequest(err.Error())
|
||||
}
|
||||
if body != nil {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
caller := zip.CallerOf(ctx)
|
||||
for header, value := range map[string]string{
|
||||
zip.HeaderOrg: caller.Org,
|
||||
zip.HeaderProject: caller.Project,
|
||||
zip.HeaderUser: caller.User,
|
||||
zip.HeaderUserName: caller.Name,
|
||||
zip.HeaderUserEmail: caller.Email,
|
||||
zip.HeaderUserOwner: caller.Owner,
|
||||
zip.HeaderRequestID: caller.RequestID,
|
||||
} {
|
||||
if value != "" {
|
||||
req.Header.Set(header, value)
|
||||
}
|
||||
}
|
||||
if caller.Admin {
|
||||
req.Header.Set(zip.HeaderUserAdmin, "true")
|
||||
}
|
||||
if caller.OrgAdmin {
|
||||
req.Header.Set(zip.HeaderUserOrgAdmin, "true")
|
||||
}
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code < http.StatusOK || rec.Code >= http.StatusMultipleChoices {
|
||||
code, reason := refusal(rec.Body.Bytes())
|
||||
return &zip.HTTPError{Status: rec.Code, Code: code, Msg: reason}
|
||||
}
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), out); err != nil {
|
||||
return zip.ErrInternal("cannot read the runtime's answer: " + err.Error())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// nanos renders a nanosecond epoch for the wire, or nothing when unset — the
|
||||
// runtime reads a missing or non-positive value as "use the default window",
|
||||
// so an absent input stays absent. It feeds query, which stays the ONE place
|
||||
// inputs are rendered onto the wire.
|
||||
func nanos(v int64) string {
|
||||
if v <= 0 {
|
||||
return ""
|
||||
}
|
||||
return strconv.FormatInt(v, 10)
|
||||
}
|
||||
|
||||
+11
-6
@@ -463,6 +463,11 @@ func TestLogsRoutesAreTheSameNine(t *testing.T) {
|
||||
"POST /v1/o11y/logs/pipelines": true,
|
||||
"POST /v1/o11y/logs/promote_paths": true,
|
||||
"GET /v1/o11y/logs/promote_paths": true,
|
||||
|
||||
// The tenth door: a NAMED hatch, not a typed op and not a fall-through.
|
||||
// It is in this census precisely because it is registered — a hatch that
|
||||
// stops being registered is a 404 now, and this is what catches that.
|
||||
"GET /v1/o11y/logs/livetail": true,
|
||||
}
|
||||
got := map[string]bool{}
|
||||
for _, r := range app.Fiber().GetRoutes(true) {
|
||||
@@ -482,12 +487,12 @@ func TestLogsRoutesAreTheSameNine(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Live tail is the tenth route and the one honest escape hatch: a stream that
|
||||
// never completes cannot be a buffered typed op. It still reaches the runtime
|
||||
// through the delegation wildcard, bytes untouched — a typed relay would have
|
||||
// choked on a non-JSON stream frame, so verbatim arrival IS the proof of the
|
||||
// door it went through.
|
||||
func TestLivetailStaysOnTheWildcard(t *testing.T) {
|
||||
// Live tail is the tenth route and one of the eleven honest escape hatches: a
|
||||
// stream that never completes cannot be a buffered typed op. It reaches the
|
||||
// runtime through its OWN named route now, bytes untouched — a typed relay would
|
||||
// have choked on a non-JSON stream frame, so verbatim arrival IS the proof of
|
||||
// the door it went through.
|
||||
func TestLivetailIsANamedHatch(t *testing.T) {
|
||||
app := mounted(t)
|
||||
frame := "event: log\ndata: {\"body\":\"boom\"}\n\n"
|
||||
var askedPath string
|
||||
|
||||
-352
@@ -1,352 +0,0 @@
|
||||
// Code generated by zipdoc; DO NOT EDIT.
|
||||
//
|
||||
// The logs face's slice of the package extraction, split into its own file so
|
||||
// concurrent slices do not contend on zipdoc_gen.go; a later `go generate
|
||||
// -run zipdoc ./` folds every slice back into zipdoc_gen.go, at which point
|
||||
// this file is deleted.
|
||||
|
||||
package o11y
|
||||
|
||||
import (
|
||||
"github.com/zap-proto/zip"
|
||||
)
|
||||
|
||||
func init() {
|
||||
zip.Describe("GET /v1/o11y/logs", zip.Doc{
|
||||
Description: "Returns the most recent log records in the query window, newest\nfirst — each record an open object carrying its nanosecond timestamp and\nwhatever fields the record was ingested with.\n\nCallers need the viewer role; the runtime's own gate enforces it.",
|
||||
Fields: map[string]string{
|
||||
"O11yLogRecordsIn.limit": "Limit caps how many records come back. Zero means the default of 100.",
|
||||
"O11yLogRecordsIn.timestampEnd": "TimestampEnd is the end of the window as a nanosecond epoch. Zero means\nnow.",
|
||||
"O11yLogRecordsIn.timestampStart": "TimestampStart is the start of the window as a nanosecond epoch. Zero\nmeans fifteen minutes before the end.",
|
||||
"O11yLogRecordsOut.results": "Results are the records, newest first.",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/logs/aggregate", zip.Doc{
|
||||
Description: "Returns the logs aggregate buckets for the query window. The\nruntime currently answers the empty set; the shape is the contract.\n\nCallers need the viewer role; the runtime's own gate enforces it.",
|
||||
Fields: map[string]string{
|
||||
"O11yLogAggregateBucket.groupBy": "GroupBy carries the group's key values when the aggregate grouped.",
|
||||
"O11yLogAggregateBucket.timestamp": "Timestamp is the start of the bucket.",
|
||||
"O11yLogAggregateBucket.value": "Value is the bucket's aggregated value.",
|
||||
"O11yLogAggregateOut.items": "Items are the buckets, keyed by bucket timestamp.",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/logs/fields", zip.Doc{
|
||||
Description: "Returns the log field catalog: the fields already selected as\nindexed columns, and the interesting ones seen in the data that could be.\n\nCallers need the viewer role; the runtime's own gate enforces it.",
|
||||
Fields: map[string]string{
|
||||
"O11yLogField.dataType": "DataType is the field's data type, e.g. string, int64, float64, bool.",
|
||||
"O11yLogField.name": "Name is the field's name.",
|
||||
"O11yLogField.type": "Type is where the field lives: attributes or resources.",
|
||||
"O11yLogFieldsOut.interesting": "Interesting are fields seen in the data that could be selected.",
|
||||
"O11yLogFieldsOut.selected": "Selected are the fields materialized as their own columns.",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/logs/pipelines/:version", zip.Doc{
|
||||
Description: "Returns the caller's org's log parsing pipelines at one config\nversion — \"latest\" for the newest — along with that version's deployment\nrecord and the recent version history.\n\nCallers need the viewer role; the runtime's own gate enforces it.",
|
||||
Fields: map[string]string{
|
||||
"O11yLogConfigVersion.config": "Config is the rendered collector config the version deployed.",
|
||||
"O11yLogConfigVersion.createdAt": "CreatedAt is when the version was created.",
|
||||
"O11yLogConfigVersion.createdBy": "CreatedBy is the id of who created the version.",
|
||||
"O11yLogConfigVersion.createdByName": "CreatedByName is the display name of who created the version.",
|
||||
"O11yLogConfigVersion.deployResult": "DeployResult is the deployment's outcome message.",
|
||||
"O11yLogConfigVersion.deploySequence": "DeploySequence orders this deployment among the version's deployments.",
|
||||
"O11yLogConfigVersion.deployStatus": "DeployStatus is where the deployment stands, e.g. dirty, deploying,\ndeployed, in_progress, failed, unknown.",
|
||||
"O11yLogConfigVersion.elementType": "ElementType is the config element the version carries — log_pipelines.",
|
||||
"O11yLogConfigVersion.id": "ID is the version record's id.",
|
||||
"O11yLogConfigVersion.lastHash": "LastHash is the deployed config's hash.",
|
||||
"O11yLogConfigVersion.orgId": "OrgID is the org the version belongs to.",
|
||||
"O11yLogConfigVersion.updatedAt": "UpdatedAt is when the version last changed.",
|
||||
"O11yLogConfigVersion.updatedBy": "UpdatedBy is the id of who last changed it.",
|
||||
"O11yLogConfigVersion.version": "Version is the config version number.",
|
||||
"O11yLogFilter.items": "Items are the predicates.",
|
||||
"O11yLogFilter.op": "Op combines the items: AND or OR.",
|
||||
"O11yLogFilterItem.key": "Key is the field the predicate tests.",
|
||||
"O11yLogFilterItem.op": "Op is the comparison, e.g. =, !=, in, contains.",
|
||||
"O11yLogFilterItem.value": "Value is what it tests against, in the value's own JSON type.",
|
||||
"O11yLogFilterKey.dataType": "DataType is the field's data type, e.g. string, int64, float64, bool.",
|
||||
"O11yLogFilterKey.isColumn": "IsColumn marks a field materialized as its own column.",
|
||||
"O11yLogFilterKey.isJSON": "IsJSON marks a path into the record's JSON body.",
|
||||
"O11yLogFilterKey.key": "Key is the field's name.",
|
||||
"O11yLogFilterKey.type": "Type is where the field lives: tag or resource.",
|
||||
"O11yLogParseFrom.parse_from": "ParseFrom is the field to read.",
|
||||
"O11yLogPipeline.alias": "Alias is the pipeline's short name.",
|
||||
"O11yLogPipeline.config": "Config is the pipeline's processors, in order.",
|
||||
"O11yLogPipeline.createdAt": "CreatedAt is when the pipeline was created.",
|
||||
"O11yLogPipeline.createdBy": "CreatedBy is the id of who created the pipeline.",
|
||||
"O11yLogPipeline.description": "Description says what the pipeline is for.",
|
||||
"O11yLogPipeline.enabled": "Enabled says whether the pipeline is on.",
|
||||
"O11yLogPipeline.filter": "Filter selects which records the pipeline processes.",
|
||||
"O11yLogPipeline.id": "ID is the pipeline's id.",
|
||||
"O11yLogPipeline.name": "Name is the pipeline's display name.",
|
||||
"O11yLogPipeline.orderId": "OrderID is the pipeline's 1-based position in the set.",
|
||||
"O11yLogPipeline.updatedAt": "UpdatedAt is when the pipeline last changed.",
|
||||
"O11yLogPipeline.updatedBy": "UpdatedBy is the id of who last changed it.",
|
||||
"O11yLogPipelineOperator.default": "Default is the id of the processor a router falls through to.",
|
||||
"O11yLogPipelineOperator.enable_flattening": "EnableFlattening flattens parsed JSON one level when true.",
|
||||
"O11yLogPipelineOperator.enable_paths": "EnablePaths keeps the JSON path in flattened keys when true.",
|
||||
"O11yLogPipelineOperator.enabled": "Enabled turns the processor on.",
|
||||
"O11yLogPipelineOperator.expr": "Expr is a router route's expression.",
|
||||
"O11yLogPipelineOperator.field": "Field is the field an add/remove processor works on.",
|
||||
"O11yLogPipelineOperator.fields": "Fields are the fields a retain processor keeps.",
|
||||
"O11yLogPipelineOperator.from": "From is the source field of a move or copy.",
|
||||
"O11yLogPipelineOperator.id": "ID is the processor's id, unique within the pipeline.",
|
||||
"O11yLogPipelineOperator.if": "If gates the processor on an expression.",
|
||||
"O11yLogPipelineOperator.layout": "Layout is a time parser's layout.",
|
||||
"O11yLogPipelineOperator.layout_type": "LayoutType is the layout's kind, e.g. strptime, gotime, epoch.",
|
||||
"O11yLogPipelineOperator.mapping": "Mapping maps severity levels (or flattened keys) to the values that\nmean them.",
|
||||
"O11yLogPipelineOperator.name": "Name is the processor's display name.",
|
||||
"O11yLogPipelineOperator.on_error": "OnError says what to do when the processor fails, e.g. send, drop.",
|
||||
"O11yLogPipelineOperator.orderId": "OrderID is the processor's 1-based position in the pipeline.",
|
||||
"O11yLogPipelineOperator.output": "Output is the id of the processor that runs next.",
|
||||
"O11yLogPipelineOperator.overwrite_text": "OverwriteSeverityText rewrites the severity text alongside the number\nwhen true.",
|
||||
"O11yLogPipelineOperator.parse_from": "ParseFrom is where a parser reads from.",
|
||||
"O11yLogPipelineOperator.parse_to": "ParseTo is where a parser writes its result.",
|
||||
"O11yLogPipelineOperator.path_prefix": "PathPrefix prefixes flattened keys.",
|
||||
"O11yLogPipelineOperator.pattern": "Pattern is a grok parser's pattern.",
|
||||
"O11yLogPipelineOperator.regex": "Regex is a regex parser's expression.",
|
||||
"O11yLogPipelineOperator.routes": "Routes are a router processor's routes.",
|
||||
"O11yLogPipelineOperator.span_id": "SpanID says where a trace parser reads the span id from.",
|
||||
"O11yLogPipelineOperator.to": "To is the destination field of a move or copy.",
|
||||
"O11yLogPipelineOperator.trace_flags": "TraceFlags says where a trace parser reads the trace flags from.",
|
||||
"O11yLogPipelineOperator.trace_id": "TraceID says where a trace parser reads the trace id from.",
|
||||
"O11yLogPipelineOperator.type": "Type is the processor type, e.g. grok_parser, regex_parser, json_parser,\ntrace_parser, time_parser, severity_parser, add, remove, move, copy.",
|
||||
"O11yLogPipelineOperator.value": "Value is the value an add processor writes.",
|
||||
"O11yLogPipelineRoute.expr": "Expr is the expression that selects the route.",
|
||||
"O11yLogPipelineRoute.output": "Output is the id of the processor the route sends to.",
|
||||
"O11yLogPipelines.config": "Config is the rendered collector config the version deployed.",
|
||||
"O11yLogPipelines.createdAt": "CreatedAt is when the version was created.",
|
||||
"O11yLogPipelines.createdBy": "CreatedBy is the id of who created the version.",
|
||||
"O11yLogPipelines.createdByName": "CreatedByName is the display name of who created the version.",
|
||||
"O11yLogPipelines.deployResult": "DeployResult is the deployment's outcome message.",
|
||||
"O11yLogPipelines.deploySequence": "DeploySequence orders this deployment among the version's deployments.",
|
||||
"O11yLogPipelines.deployStatus": "DeployStatus is where the deployment stands, e.g. dirty, deploying,\ndeployed, in_progress, failed, unknown.",
|
||||
"O11yLogPipelines.elementType": "ElementType is the config element this version carries — log_pipelines.",
|
||||
"O11yLogPipelines.history": "History is the recent version history, newest first.",
|
||||
"O11yLogPipelines.id": "ID is the version record's id.",
|
||||
"O11yLogPipelines.lastHash": "LastHash is the deployed config's hash.",
|
||||
"O11yLogPipelines.orgId": "OrgID is the org the version belongs to.",
|
||||
"O11yLogPipelines.pipelines": "Pipelines are the version's pipelines, in order.",
|
||||
"O11yLogPipelines.updatedAt": "UpdatedAt is when the version last changed.",
|
||||
"O11yLogPipelines.updatedBy": "UpdatedBy is the id of who last changed it.",
|
||||
"O11yLogPipelines.version": "Version is the config version number.",
|
||||
"O11yLogPipelinesIn.version": "Version is the config version to read — a positive number, or \"latest\".",
|
||||
"O11yLogPipelinesOut.data": "Data is the config version.",
|
||||
"O11yLogPipelinesOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/logs/promote_paths", zip.Doc{
|
||||
Description: "Lists the log body paths already promoted or indexed, with the\nindexes each carries.\n\nCallers need the viewer role; the runtime's own gate enforces it.",
|
||||
Fields: map[string]string{
|
||||
"O11yLogPromoteIndex.fieldDataType": "FieldDataType is the path's data type, e.g. string, number, bool.",
|
||||
"O11yLogPromoteIndex.granularity": "Granularity is the index granularity in rows.",
|
||||
"O11yLogPromoteIndex.type": "Type is the index type, e.g. minmax, set(N), bloom_filter(P).",
|
||||
"O11yLogPromotePath.indexes": "Indexes are the indexes to put on the path.",
|
||||
"O11yLogPromotePath.path": "Path is the body path, e.g. body.user.id on the way in; listed without\nthe body. prefix.",
|
||||
"O11yLogPromotePath.promote": "Promote lifts the path into its own column when true.",
|
||||
"O11yLogPromotedOut.data": "Data holds the paths.",
|
||||
"O11yLogPromotedOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/logs/fields", zip.Doc{
|
||||
Description: "Changes how one log field is stored — selects or deselects it\nas a materialized column and tunes its index — and echoes the setting back.\n\nCallers need the editor role; the runtime's own gate enforces it.",
|
||||
Fields: map[string]string{
|
||||
"O11yLogFieldSetting.dataType": "DataType is the field's data type, e.g. string, int64, float64, bool.\nRequired.",
|
||||
"O11yLogFieldSetting.index": "Index is the index expression to put on the column, e.g. minmax,\nset(N), bloom_filter(P), tokenbf_v1(S,H,SEED). Empty keeps the default.",
|
||||
"O11yLogFieldSetting.indexGranularity": "IndexGranularity is the index granularity in rows.",
|
||||
"O11yLogFieldSetting.name": "Name is the field to tune. Required.",
|
||||
"O11yLogFieldSetting.selected": "Selected materializes the field as its own column when true.",
|
||||
"O11yLogFieldSetting.type": "Type is where the field lives: attributes or resources. Required.",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/logs/pipelines", zip.Doc{
|
||||
Description: "Saves the given log parsing pipelines as the new config\nversion for the caller's org and starts deploying it. The set REPLACES the\ncurrent one: a pipeline left out of the request is dropped from the new\nversion, and an empty set drops them all.\n\nCallers need the editor role; the runtime's own gate enforces it.",
|
||||
Fields: map[string]string{
|
||||
"O11yLogConfigVersion.config": "Config is the rendered collector config the version deployed.",
|
||||
"O11yLogConfigVersion.createdAt": "CreatedAt is when the version was created.",
|
||||
"O11yLogConfigVersion.createdBy": "CreatedBy is the id of who created the version.",
|
||||
"O11yLogConfigVersion.createdByName": "CreatedByName is the display name of who created the version.",
|
||||
"O11yLogConfigVersion.deployResult": "DeployResult is the deployment's outcome message.",
|
||||
"O11yLogConfigVersion.deploySequence": "DeploySequence orders this deployment among the version's deployments.",
|
||||
"O11yLogConfigVersion.deployStatus": "DeployStatus is where the deployment stands, e.g. dirty, deploying,\ndeployed, in_progress, failed, unknown.",
|
||||
"O11yLogConfigVersion.elementType": "ElementType is the config element the version carries — log_pipelines.",
|
||||
"O11yLogConfigVersion.id": "ID is the version record's id.",
|
||||
"O11yLogConfigVersion.lastHash": "LastHash is the deployed config's hash.",
|
||||
"O11yLogConfigVersion.orgId": "OrgID is the org the version belongs to.",
|
||||
"O11yLogConfigVersion.updatedAt": "UpdatedAt is when the version last changed.",
|
||||
"O11yLogConfigVersion.updatedBy": "UpdatedBy is the id of who last changed it.",
|
||||
"O11yLogConfigVersion.version": "Version is the config version number.",
|
||||
"O11yLogFilter.items": "Items are the predicates.",
|
||||
"O11yLogFilter.op": "Op combines the items: AND or OR.",
|
||||
"O11yLogFilterItem.key": "Key is the field the predicate tests.",
|
||||
"O11yLogFilterItem.op": "Op is the comparison, e.g. =, !=, in, contains.",
|
||||
"O11yLogFilterItem.value": "Value is what it tests against, in the value's own JSON type.",
|
||||
"O11yLogFilterKey.dataType": "DataType is the field's data type, e.g. string, int64, float64, bool.",
|
||||
"O11yLogFilterKey.isColumn": "IsColumn marks a field materialized as its own column.",
|
||||
"O11yLogFilterKey.isJSON": "IsJSON marks a path into the record's JSON body.",
|
||||
"O11yLogFilterKey.key": "Key is the field's name.",
|
||||
"O11yLogFilterKey.type": "Type is where the field lives: tag or resource.",
|
||||
"O11yLogParseFrom.parse_from": "ParseFrom is the field to read.",
|
||||
"O11yLogPipeline.alias": "Alias is the pipeline's short name.",
|
||||
"O11yLogPipeline.config": "Config is the pipeline's processors, in order.",
|
||||
"O11yLogPipeline.createdAt": "CreatedAt is when the pipeline was created.",
|
||||
"O11yLogPipeline.createdBy": "CreatedBy is the id of who created the pipeline.",
|
||||
"O11yLogPipeline.description": "Description says what the pipeline is for.",
|
||||
"O11yLogPipeline.enabled": "Enabled says whether the pipeline is on.",
|
||||
"O11yLogPipeline.filter": "Filter selects which records the pipeline processes.",
|
||||
"O11yLogPipeline.id": "ID is the pipeline's id.",
|
||||
"O11yLogPipeline.name": "Name is the pipeline's display name.",
|
||||
"O11yLogPipeline.orderId": "OrderID is the pipeline's 1-based position in the set.",
|
||||
"O11yLogPipeline.updatedAt": "UpdatedAt is when the pipeline last changed.",
|
||||
"O11yLogPipeline.updatedBy": "UpdatedBy is the id of who last changed it.",
|
||||
"O11yLogPipelineCreateIn.pipelines": "Pipelines are the pipelines the new version holds, in order.",
|
||||
"O11yLogPipelineOperator.default": "Default is the id of the processor a router falls through to.",
|
||||
"O11yLogPipelineOperator.enable_flattening": "EnableFlattening flattens parsed JSON one level when true.",
|
||||
"O11yLogPipelineOperator.enable_paths": "EnablePaths keeps the JSON path in flattened keys when true.",
|
||||
"O11yLogPipelineOperator.enabled": "Enabled turns the processor on.",
|
||||
"O11yLogPipelineOperator.expr": "Expr is a router route's expression.",
|
||||
"O11yLogPipelineOperator.field": "Field is the field an add/remove processor works on.",
|
||||
"O11yLogPipelineOperator.fields": "Fields are the fields a retain processor keeps.",
|
||||
"O11yLogPipelineOperator.from": "From is the source field of a move or copy.",
|
||||
"O11yLogPipelineOperator.id": "ID is the processor's id, unique within the pipeline.",
|
||||
"O11yLogPipelineOperator.if": "If gates the processor on an expression.",
|
||||
"O11yLogPipelineOperator.layout": "Layout is a time parser's layout.",
|
||||
"O11yLogPipelineOperator.layout_type": "LayoutType is the layout's kind, e.g. strptime, gotime, epoch.",
|
||||
"O11yLogPipelineOperator.mapping": "Mapping maps severity levels (or flattened keys) to the values that\nmean them.",
|
||||
"O11yLogPipelineOperator.name": "Name is the processor's display name.",
|
||||
"O11yLogPipelineOperator.on_error": "OnError says what to do when the processor fails, e.g. send, drop.",
|
||||
"O11yLogPipelineOperator.orderId": "OrderID is the processor's 1-based position in the pipeline.",
|
||||
"O11yLogPipelineOperator.output": "Output is the id of the processor that runs next.",
|
||||
"O11yLogPipelineOperator.overwrite_text": "OverwriteSeverityText rewrites the severity text alongside the number\nwhen true.",
|
||||
"O11yLogPipelineOperator.parse_from": "ParseFrom is where a parser reads from.",
|
||||
"O11yLogPipelineOperator.parse_to": "ParseTo is where a parser writes its result.",
|
||||
"O11yLogPipelineOperator.path_prefix": "PathPrefix prefixes flattened keys.",
|
||||
"O11yLogPipelineOperator.pattern": "Pattern is a grok parser's pattern.",
|
||||
"O11yLogPipelineOperator.regex": "Regex is a regex parser's expression.",
|
||||
"O11yLogPipelineOperator.routes": "Routes are a router processor's routes.",
|
||||
"O11yLogPipelineOperator.span_id": "SpanID says where a trace parser reads the span id from.",
|
||||
"O11yLogPipelineOperator.to": "To is the destination field of a move or copy.",
|
||||
"O11yLogPipelineOperator.trace_flags": "TraceFlags says where a trace parser reads the trace flags from.",
|
||||
"O11yLogPipelineOperator.trace_id": "TraceID says where a trace parser reads the trace id from.",
|
||||
"O11yLogPipelineOperator.type": "Type is the processor type, e.g. grok_parser, regex_parser, json_parser,\ntrace_parser, time_parser, severity_parser, add, remove, move, copy.",
|
||||
"O11yLogPipelineOperator.value": "Value is the value an add processor writes.",
|
||||
"O11yLogPipelineRoute.expr": "Expr is the expression that selects the route.",
|
||||
"O11yLogPipelineRoute.output": "Output is the id of the processor the route sends to.",
|
||||
"O11yLogPipelines.config": "Config is the rendered collector config the version deployed.",
|
||||
"O11yLogPipelines.createdAt": "CreatedAt is when the version was created.",
|
||||
"O11yLogPipelines.createdBy": "CreatedBy is the id of who created the version.",
|
||||
"O11yLogPipelines.createdByName": "CreatedByName is the display name of who created the version.",
|
||||
"O11yLogPipelines.deployResult": "DeployResult is the deployment's outcome message.",
|
||||
"O11yLogPipelines.deploySequence": "DeploySequence orders this deployment among the version's deployments.",
|
||||
"O11yLogPipelines.deployStatus": "DeployStatus is where the deployment stands, e.g. dirty, deploying,\ndeployed, in_progress, failed, unknown.",
|
||||
"O11yLogPipelines.elementType": "ElementType is the config element this version carries — log_pipelines.",
|
||||
"O11yLogPipelines.history": "History is the recent version history, newest first.",
|
||||
"O11yLogPipelines.id": "ID is the version record's id.",
|
||||
"O11yLogPipelines.lastHash": "LastHash is the deployed config's hash.",
|
||||
"O11yLogPipelines.orgId": "OrgID is the org the version belongs to.",
|
||||
"O11yLogPipelines.pipelines": "Pipelines are the version's pipelines, in order.",
|
||||
"O11yLogPipelines.updatedAt": "UpdatedAt is when the version last changed.",
|
||||
"O11yLogPipelines.updatedBy": "UpdatedBy is the id of who last changed it.",
|
||||
"O11yLogPipelines.version": "Version is the config version number.",
|
||||
"O11yLogPipelinesOut.data": "Data is the config version.",
|
||||
"O11yLogPipelinesOut.status": "Status is \"success\".",
|
||||
"O11yLogPostablePipeline.alias": "Alias is the pipeline's short name.",
|
||||
"O11yLogPostablePipeline.config": "Config is the pipeline's processors, in order.",
|
||||
"O11yLogPostablePipeline.description": "Description says what the pipeline is for.",
|
||||
"O11yLogPostablePipeline.enabled": "Enabled turns the pipeline on.",
|
||||
"O11yLogPostablePipeline.filter": "Filter selects which records the pipeline processes.",
|
||||
"O11yLogPostablePipeline.id": "ID is the pipeline's id. Empty on a new pipeline; the id it was listed\nwith to keep an existing one.",
|
||||
"O11yLogPostablePipeline.name": "Name is the pipeline's display name.",
|
||||
"O11yLogPostablePipeline.orderId": "OrderID is the pipeline's 1-based position in the set.",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/logs/pipelines/preview", zip.Doc{
|
||||
Description: "Runs the given log parsing pipelines over the given\nsample records without saving anything, and returns the transformed records\nplus whatever the collector logged while simulating them.\n\nCallers need the viewer role; the runtime's own gate enforces it.",
|
||||
Fields: map[string]string{
|
||||
"O11yLogFilter.items": "Items are the predicates.",
|
||||
"O11yLogFilter.op": "Op combines the items: AND or OR.",
|
||||
"O11yLogFilterItem.key": "Key is the field the predicate tests.",
|
||||
"O11yLogFilterItem.op": "Op is the comparison, e.g. =, !=, in, contains.",
|
||||
"O11yLogFilterItem.value": "Value is what it tests against, in the value's own JSON type.",
|
||||
"O11yLogFilterKey.dataType": "DataType is the field's data type, e.g. string, int64, float64, bool.",
|
||||
"O11yLogFilterKey.isColumn": "IsColumn marks a field materialized as its own column.",
|
||||
"O11yLogFilterKey.isJSON": "IsJSON marks a path into the record's JSON body.",
|
||||
"O11yLogFilterKey.key": "Key is the field's name.",
|
||||
"O11yLogFilterKey.type": "Type is where the field lives: tag or resource.",
|
||||
"O11yLogParseFrom.parse_from": "ParseFrom is the field to read.",
|
||||
"O11yLogPipeline.alias": "Alias is the pipeline's short name.",
|
||||
"O11yLogPipeline.config": "Config is the pipeline's processors, in order.",
|
||||
"O11yLogPipeline.createdAt": "CreatedAt is when the pipeline was created.",
|
||||
"O11yLogPipeline.createdBy": "CreatedBy is the id of who created the pipeline.",
|
||||
"O11yLogPipeline.description": "Description says what the pipeline is for.",
|
||||
"O11yLogPipeline.enabled": "Enabled says whether the pipeline is on.",
|
||||
"O11yLogPipeline.filter": "Filter selects which records the pipeline processes.",
|
||||
"O11yLogPipeline.id": "ID is the pipeline's id.",
|
||||
"O11yLogPipeline.name": "Name is the pipeline's display name.",
|
||||
"O11yLogPipeline.orderId": "OrderID is the pipeline's 1-based position in the set.",
|
||||
"O11yLogPipeline.updatedAt": "UpdatedAt is when the pipeline last changed.",
|
||||
"O11yLogPipeline.updatedBy": "UpdatedBy is the id of who last changed it.",
|
||||
"O11yLogPipelineOperator.default": "Default is the id of the processor a router falls through to.",
|
||||
"O11yLogPipelineOperator.enable_flattening": "EnableFlattening flattens parsed JSON one level when true.",
|
||||
"O11yLogPipelineOperator.enable_paths": "EnablePaths keeps the JSON path in flattened keys when true.",
|
||||
"O11yLogPipelineOperator.enabled": "Enabled turns the processor on.",
|
||||
"O11yLogPipelineOperator.expr": "Expr is a router route's expression.",
|
||||
"O11yLogPipelineOperator.field": "Field is the field an add/remove processor works on.",
|
||||
"O11yLogPipelineOperator.fields": "Fields are the fields a retain processor keeps.",
|
||||
"O11yLogPipelineOperator.from": "From is the source field of a move or copy.",
|
||||
"O11yLogPipelineOperator.id": "ID is the processor's id, unique within the pipeline.",
|
||||
"O11yLogPipelineOperator.if": "If gates the processor on an expression.",
|
||||
"O11yLogPipelineOperator.layout": "Layout is a time parser's layout.",
|
||||
"O11yLogPipelineOperator.layout_type": "LayoutType is the layout's kind, e.g. strptime, gotime, epoch.",
|
||||
"O11yLogPipelineOperator.mapping": "Mapping maps severity levels (or flattened keys) to the values that\nmean them.",
|
||||
"O11yLogPipelineOperator.name": "Name is the processor's display name.",
|
||||
"O11yLogPipelineOperator.on_error": "OnError says what to do when the processor fails, e.g. send, drop.",
|
||||
"O11yLogPipelineOperator.orderId": "OrderID is the processor's 1-based position in the pipeline.",
|
||||
"O11yLogPipelineOperator.output": "Output is the id of the processor that runs next.",
|
||||
"O11yLogPipelineOperator.overwrite_text": "OverwriteSeverityText rewrites the severity text alongside the number\nwhen true.",
|
||||
"O11yLogPipelineOperator.parse_from": "ParseFrom is where a parser reads from.",
|
||||
"O11yLogPipelineOperator.parse_to": "ParseTo is where a parser writes its result.",
|
||||
"O11yLogPipelineOperator.path_prefix": "PathPrefix prefixes flattened keys.",
|
||||
"O11yLogPipelineOperator.pattern": "Pattern is a grok parser's pattern.",
|
||||
"O11yLogPipelineOperator.regex": "Regex is a regex parser's expression.",
|
||||
"O11yLogPipelineOperator.routes": "Routes are a router processor's routes.",
|
||||
"O11yLogPipelineOperator.span_id": "SpanID says where a trace parser reads the span id from.",
|
||||
"O11yLogPipelineOperator.to": "To is the destination field of a move or copy.",
|
||||
"O11yLogPipelineOperator.trace_flags": "TraceFlags says where a trace parser reads the trace flags from.",
|
||||
"O11yLogPipelineOperator.trace_id": "TraceID says where a trace parser reads the trace id from.",
|
||||
"O11yLogPipelineOperator.type": "Type is the processor type, e.g. grok_parser, regex_parser, json_parser,\ntrace_parser, time_parser, severity_parser, add, remove, move, copy.",
|
||||
"O11yLogPipelineOperator.value": "Value is the value an add processor writes.",
|
||||
"O11yLogPipelinePreview.collectorLogs": "CollectorLogs is what the collector logged while simulating.",
|
||||
"O11yLogPipelinePreview.logs": "Logs are the sample records after the pipelines ran over them.",
|
||||
"O11yLogPipelinePreviewIn.logs": "Logs are the sample records to transform.",
|
||||
"O11yLogPipelinePreviewIn.pipelines": "Pipelines are the pipelines to simulate, in order.",
|
||||
"O11yLogPipelinePreviewOut.data": "Data is the dry run's result.",
|
||||
"O11yLogPipelinePreviewOut.status": "Status is \"success\".",
|
||||
"O11yLogPipelineRoute.expr": "Expr is the expression that selects the route.",
|
||||
"O11yLogPipelineRoute.output": "Output is the id of the processor the route sends to.",
|
||||
"O11yLogRecord.attributes_bool": "AttributesBool are the record's boolean attributes.",
|
||||
"O11yLogRecord.attributes_float": "AttributesFloat are the record's float attributes.",
|
||||
"O11yLogRecord.attributes_int": "AttributesInt are the record's integer attributes.",
|
||||
"O11yLogRecord.attributes_string": "AttributesString are the record's string attributes.",
|
||||
"O11yLogRecord.body": "Body is the record's body.",
|
||||
"O11yLogRecord.id": "ID is the record's id.",
|
||||
"O11yLogRecord.resources_string": "ResourcesString are the record's string resource attributes.",
|
||||
"O11yLogRecord.severity_number": "SeverityNumber is the record's severity as a number.",
|
||||
"O11yLogRecord.severity_text": "SeverityText is the record's severity as text, e.g. ERROR.",
|
||||
"O11yLogRecord.span_id": "SpanID is the span the record belongs to.",
|
||||
"O11yLogRecord.timestamp": "Timestamp is the record's time as a nanosecond epoch.",
|
||||
"O11yLogRecord.trace_flags": "TraceFlags are the record's trace flags.",
|
||||
"O11yLogRecord.trace_id": "TraceID is the trace the record belongs to.",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/logs/promote_paths", zip.Doc{
|
||||
Description: "Promotes and indexes log body paths: each named path is lifted\nout of the JSON body into its own column, with the indexes the caller asked\nfor. Paths must start with \"body.\".\n\nCallers need the editor role; the runtime's own gate enforces it.",
|
||||
Fields: map[string]string{
|
||||
"O11yLogPromoteIndex.fieldDataType": "FieldDataType is the path's data type, e.g. string, number, bool.",
|
||||
"O11yLogPromoteIndex.granularity": "Granularity is the index granularity in rows.",
|
||||
"O11yLogPromoteIndex.type": "Type is the index type, e.g. minmax, set(N), bloom_filter(P).",
|
||||
"O11yLogPromoteOut.status": "Status is \"success\".",
|
||||
"O11yLogPromotePath.indexes": "Indexes are the indexes to put on the path.",
|
||||
"O11yLogPromotePath.path": "Path is the body path, e.g. body.user.id on the way in; listed without\nthe body. prefix.",
|
||||
"O11yLogPromotePath.promote": "Promote lifts the path into its own column when true.",
|
||||
},
|
||||
})
|
||||
}
|
||||
+2
-6
@@ -36,15 +36,11 @@ import (
|
||||
"github.com/zap-proto/zip"
|
||||
)
|
||||
|
||||
// metricsRoot is the face's path root, spelled ONCE: the group the ops
|
||||
// register on, and the first segment metricsRelay rebuilds a call onto.
|
||||
const metricsRoot = "/v1/o11y"
|
||||
|
||||
// mountMetrics registers the nineteen typed metrics ops on the native router.
|
||||
// Collection routes register before the parameterised ones so an id can never
|
||||
// shadow a collection.
|
||||
func mountMetrics(app *zip.App) {
|
||||
g := app.Group(metricsRoot)
|
||||
g := app.Group(o11yRoot)
|
||||
|
||||
zip.Get(g, "/metrics", listMetrics, zip.WithOperationID("ListMetrics"))
|
||||
zip.Post(g, "/metrics/stats", metricStats, zip.WithOperationID("GetMetricsStats"))
|
||||
@@ -938,7 +934,7 @@ func metricsRelay(ctx context.Context, method, path string, params url.Values, b
|
||||
return zip.Errorf(http.StatusServiceUnavailable, "o11y runtime not initialized")
|
||||
}
|
||||
|
||||
target := metricsRoot + path
|
||||
target := o11yRoot + path
|
||||
if q := params.Encode(); q != "" {
|
||||
target += "?" + q
|
||||
}
|
||||
|
||||
@@ -1,382 +0,0 @@
|
||||
// Code generated by zipdoc; DO NOT EDIT.
|
||||
//
|
||||
// The metrics face's slice of the package extraction, split into its own file so
|
||||
// concurrent slices do not contend on zipdoc_gen.go; a later `go generate
|
||||
// -run zipdoc ./` folds every slice back into zipdoc_gen.go, at which point
|
||||
// this file is deleted.
|
||||
|
||||
package o11y
|
||||
|
||||
import (
|
||||
"github.com/zap-proto/zip"
|
||||
)
|
||||
|
||||
func init() {
|
||||
zip.Describe("DELETE /v1/o11y/metric_reduction_rules/:id", zip.Doc{
|
||||
Description: "Deletes a volume-control rule by its id.",
|
||||
Fields: map[string]string{
|
||||
"O11yReductionRuleRef.id": "ID is the rule's id.",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/metric_reduction_rules", zip.Doc{
|
||||
Description: "Lists the org's metric volume-control (label reduction)\nrules, pageable and sortable by name, volume or recency.",
|
||||
Fields: map[string]string{
|
||||
"O11yReductionRule.active": "Active says whether the rule is in force.",
|
||||
"O11yReductionRule.createdAt": "CreatedAt is when the rule was created.",
|
||||
"O11yReductionRule.createdBy": "CreatedBy is who created it.",
|
||||
"O11yReductionRule.effectiveFrom": "EffectiveFrom is when the rule took effect.",
|
||||
"O11yReductionRule.id": "ID is the rule's id.",
|
||||
"O11yReductionRule.ingestedSamples": "IngestedSamples is how many samples arrived while the rule was active.",
|
||||
"O11yReductionRule.ingestedSeries": "IngestedSeries is how many series arrived while the rule was active.",
|
||||
"O11yReductionRule.labels": "Labels are the label names the rule matches.",
|
||||
"O11yReductionRule.matchType": "MatchType is drop or keep.",
|
||||
"O11yReductionRule.metricName": "MetricName is the metric the rule governs.",
|
||||
"O11yReductionRule.retainedSamples": "RetainedSamples is how many of them were kept.",
|
||||
"O11yReductionRule.retainedSeries": "RetainedSeries is how many of them were kept.",
|
||||
"O11yReductionRule.updatedAt": "UpdatedAt is when the rule last changed.",
|
||||
"O11yReductionRule.updatedBy": "UpdatedBy is who last changed it.",
|
||||
"O11yReductionRuleListIn.limit": "Limit caps how many rules come back, at most 1000. Unset means 10.",
|
||||
"O11yReductionRuleListIn.metricName": "MetricName narrows the page to one metric's rule.",
|
||||
"O11yReductionRuleListIn.offset": "Offset is how many rules to skip, for paging.",
|
||||
"O11yReductionRuleListIn.order": "Order is asc or desc. Unset means desc.",
|
||||
"O11yReductionRuleListIn.orderBy": "OrderBy sorts the page: metric, ingested_volume, reduced_volume or\nlast_updated. Unset means ingested_volume.",
|
||||
"O11yReductionRuleListIn.search": "Search narrows the page to rules whose metric name contains it.",
|
||||
"O11yReductionRuleListOut.data": "Data holds the rules.",
|
||||
"O11yReductionRuleListOut.status": "Status is \"success\".",
|
||||
"O11yReductionRules.rules": "Rules are the rules.",
|
||||
"O11yReductionRules.total": "Total is how many rules matched, across all pages.",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/metric_reduction_rules/:id", zip.Doc{
|
||||
Description: "Returns one volume-control rule by its id.",
|
||||
Fields: map[string]string{
|
||||
"O11yReductionRule.active": "Active says whether the rule is in force.",
|
||||
"O11yReductionRule.createdAt": "CreatedAt is when the rule was created.",
|
||||
"O11yReductionRule.createdBy": "CreatedBy is who created it.",
|
||||
"O11yReductionRule.effectiveFrom": "EffectiveFrom is when the rule took effect.",
|
||||
"O11yReductionRule.id": "ID is the rule's id.",
|
||||
"O11yReductionRule.ingestedSamples": "IngestedSamples is how many samples arrived while the rule was active.",
|
||||
"O11yReductionRule.ingestedSeries": "IngestedSeries is how many series arrived while the rule was active.",
|
||||
"O11yReductionRule.labels": "Labels are the label names the rule matches.",
|
||||
"O11yReductionRule.matchType": "MatchType is drop or keep.",
|
||||
"O11yReductionRule.metricName": "MetricName is the metric the rule governs.",
|
||||
"O11yReductionRule.retainedSamples": "RetainedSamples is how many of them were kept.",
|
||||
"O11yReductionRule.retainedSeries": "RetainedSeries is how many of them were kept.",
|
||||
"O11yReductionRule.updatedAt": "UpdatedAt is when the rule last changed.",
|
||||
"O11yReductionRule.updatedBy": "UpdatedBy is who last changed it.",
|
||||
"O11yReductionRuleOut.data": "Data holds the rule.",
|
||||
"O11yReductionRuleOut.status": "Status is \"success\".",
|
||||
"O11yReductionRuleRef.id": "ID is the rule's id.",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/metric_reduction_rules/stats", zip.Doc{
|
||||
Description: "Returns total ingested vs retained series and samples and\nthe estimated monthly savings across all volume-control rules.",
|
||||
Fields: map[string]string{
|
||||
"O11yReductionStats.estimatedMonthlySavingsUsd": "EstimatedMonthlySavingsUsd is the estimated monthly savings, in USD.",
|
||||
"O11yReductionStats.ingestedSamples": "IngestedSamples is how many samples arrived across all rules.",
|
||||
"O11yReductionStats.ingestedSeries": "IngestedSeries is how many series arrived across all rules.",
|
||||
"O11yReductionStats.retainedSamples": "RetainedSamples is how many of them were kept.",
|
||||
"O11yReductionStats.retainedSeries": "RetainedSeries is how many of them were kept.",
|
||||
"O11yReductionStatsOut.data": "Data holds the totals.",
|
||||
"O11yReductionStatsOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/metric_reduction_rules/timeseries", zip.Doc{
|
||||
Description: "Returns ingested vs retained series over time across\nall volume-control rules, in hourly buckets, in the query-range time-series\nresponse shape.",
|
||||
Fields: map[string]string{
|
||||
"O11yAggregation.alias": "Alias is the aggregation's alias.",
|
||||
"O11yAggregation.anomalyScores": "AnomalyScores are anomaly overlays.",
|
||||
"O11yAggregation.index": "Index is the aggregation's position in the query.",
|
||||
"O11yAggregation.lowerBoundSeries": "LowerBoundSeries are forecast lower bounds.",
|
||||
"O11yAggregation.meta": "Meta describes the aggregation.",
|
||||
"O11yAggregation.predictedSeries": "PredictedSeries are forecast overlays, when the query asked for them.",
|
||||
"O11yAggregation.series": "Series are the aggregated time series.",
|
||||
"O11yAggregation.upperBoundSeries": "UpperBoundSeries are forecast upper bounds.",
|
||||
"O11yAggregationMeta.unit": "Unit is the aggregation's unit.",
|
||||
"O11yMetricField.description": "Description describes the field.",
|
||||
"O11yMetricField.fieldContext": "FieldContext is the context the field lives in, e.g. resource, attribute.",
|
||||
"O11yMetricField.fieldDataType": "FieldDataType is the field's data type.",
|
||||
"O11yMetricField.name": "Name is the field's name.",
|
||||
"O11yMetricField.signal": "Signal is the telemetry signal the field belongs to.",
|
||||
"O11yMetricField.unit": "Unit is the field's unit.",
|
||||
"O11yMetricLabel.key": "Key is the label's field.",
|
||||
"O11yMetricLabel.value": "Value is the label's value.",
|
||||
"O11yMetricPoint.partial": "Partial marks a point whose bucket the window only partly covers.",
|
||||
"O11yMetricPoint.timestamp": "Timestamp is the point's time as a Unix timestamp in milliseconds.",
|
||||
"O11yMetricPoint.value": "Value is the point's value.",
|
||||
"O11yMetricPoint.values": "Values carries the bucket values of a heatmap point.",
|
||||
"O11yMetricSeries.labels": "Labels identify the series.",
|
||||
"O11yMetricSeries.values": "Values are the series' points, in time order.",
|
||||
"O11yQueryRange.data": "Data holds the results.",
|
||||
"O11yQueryRange.meta": "Meta reports what the query scanned.",
|
||||
"O11yQueryRange.type": "Type is the result kind; time_series here.",
|
||||
"O11yQueryRange.warning": "Warning carries a non-fatal warning, when the query raised one.",
|
||||
"O11yQueryRangeData.results": "Results are the per-query results, each a set of aggregated series.",
|
||||
"O11yQueryStats.bytesScanned": "BytesScanned is how many bytes the query read.",
|
||||
"O11yQueryStats.durationMs": "DurationMS is how long the query took, in milliseconds.",
|
||||
"O11yQueryStats.rowsScanned": "RowsScanned is how many rows the query read.",
|
||||
"O11yQueryStats.stepIntervals": "StepIntervals is the step used per query, in seconds.",
|
||||
"O11yQueryWarning.message": "Message is the warning.",
|
||||
"O11yQueryWarning.url": "URL points at the relevant documentation.",
|
||||
"O11yQueryWarning.warnings": "Warnings carries additional notes.",
|
||||
"O11yQueryWarningNote.message": "Message is the note.",
|
||||
"O11yReductionSeriesOut.data": "Data holds the query-range answer.",
|
||||
"O11yReductionSeriesOut.status": "Status is \"success\".",
|
||||
"O11yReductionSeriesResult.aggregations": "Aggregations are the query's aggregation buckets.",
|
||||
"O11yReductionSeriesResult.queryName": "QueryName names the query the result answers.",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/metrics", zip.Doc{
|
||||
Description: "Lists the distinct metric names seen in a time range, each with\nits description, type, unit, temporality and monotonicity.",
|
||||
Fields: map[string]string{
|
||||
"O11yMetricList.metrics": "Metrics are the metrics, with their metadata.",
|
||||
"O11yMetricListIn.end": "End is the end of the window as a Unix timestamp in milliseconds.",
|
||||
"O11yMetricListIn.limit": "Limit caps how many metrics come back; unset means 100, at most 5000.",
|
||||
"O11yMetricListIn.searchText": "SearchText narrows the page to metric names containing it.",
|
||||
"O11yMetricListIn.source": "Source narrows the page by ingestion source.",
|
||||
"O11yMetricListIn.start": "Start is the start of the window as a Unix timestamp in milliseconds.",
|
||||
"O11yMetricListOut.data": "Data holds the metrics.",
|
||||
"O11yMetricListOut.status": "Status is \"success\".",
|
||||
"O11yMetricSummary.description": "Description describes the metric.",
|
||||
"O11yMetricSummary.isMonotonic": "IsMonotonic marks a sum that only ever increases.",
|
||||
"O11yMetricSummary.metricName": "MetricName is the metric's name.",
|
||||
"O11yMetricSummary.temporality": "Temporality is delta or cumulative.",
|
||||
"O11yMetricSummary.type": "Type is the metric type, e.g. gauge, sum, histogram.",
|
||||
"O11yMetricSummary.unit": "Unit is the metric's unit.",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/metrics/alerts", zip.Doc{
|
||||
Description: "Lists the alert rules that reference a metric.",
|
||||
Fields: map[string]string{
|
||||
"O11yMetricAlert.alertId": "AlertID is the alert rule's id.",
|
||||
"O11yMetricAlert.alertName": "AlertName is the alert rule's name.",
|
||||
"O11yMetricAlerts.alerts": "Alerts are the alert rules referencing the metric.",
|
||||
"O11yMetricAlertsOut.data": "Data holds the alerts.",
|
||||
"O11yMetricAlertsOut.status": "Status is \"success\".",
|
||||
"O11yMetricNameIn.metricName": "MetricName is the metric's name; it may contain slashes, e.g.\nrun.googleapis.com/request_latencies. Required.",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/metrics/attributes", zip.Doc{
|
||||
Description: "Returns one metric's attribute keys, each with its unique\nvalues and their count.",
|
||||
Fields: map[string]string{
|
||||
"O11yMetricAttribute.key": "Key is the attribute's name.",
|
||||
"O11yMetricAttribute.valueCount": "ValueCount is how many distinct values the attribute has.",
|
||||
"O11yMetricAttribute.values": "Values are the attribute's distinct values.",
|
||||
"O11yMetricAttributes.attributes": "Attributes are the keys, each with its values.",
|
||||
"O11yMetricAttributes.totalKeys": "TotalKeys is how many keys the metric has.",
|
||||
"O11yMetricAttributesIn.end": "End is the end of the window as a Unix timestamp in milliseconds.",
|
||||
"O11yMetricAttributesIn.metricName": "MetricName is the metric's name; it may contain slashes. Required.",
|
||||
"O11yMetricAttributesIn.start": "Start is the start of the window as a Unix timestamp in milliseconds.",
|
||||
"O11yMetricAttributesOut.data": "Data holds the attributes.",
|
||||
"O11yMetricAttributesOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/metrics/dashboards", zip.Doc{
|
||||
Description: "Lists the dashboard panels that reference a metric.",
|
||||
Fields: map[string]string{
|
||||
"O11yMetricDashboards.dashboards": "Dashboards are the panels referencing the metric.",
|
||||
"O11yMetricDashboardsOut.data": "Data holds the panels.",
|
||||
"O11yMetricDashboardsOut.status": "Status is \"success\".",
|
||||
"O11yMetricNameIn.metricName": "MetricName is the metric's name; it may contain slashes, e.g.\nrun.googleapis.com/request_latencies. Required.",
|
||||
"O11yMetricPanel.dashboardId": "DashboardID is the dashboard's id.",
|
||||
"O11yMetricPanel.dashboardName": "DashboardName is the dashboard's name.",
|
||||
"O11yMetricPanel.filterBy": "FilterBy are the labels the panel filters the metric by.",
|
||||
"O11yMetricPanel.groupBy": "GroupBy are the labels the panel groups the metric by.",
|
||||
"O11yMetricPanel.panelId": "PanelID is the panel's id.",
|
||||
"O11yMetricPanel.panelName": "PanelName is the panel's name.",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/metrics/highlights", zip.Doc{
|
||||
Description: "Returns one metric's headline numbers: data points, total\nand active time series, and when it was last received.",
|
||||
Fields: map[string]string{
|
||||
"O11yMetricHighlights.activeTimeSeries": "ActiveTimeSeries is how many of them are active.",
|
||||
"O11yMetricHighlights.dataPoints": "DataPoints is how many data points the metric has.",
|
||||
"O11yMetricHighlights.lastReceived": "LastReceived is when the metric last arrived, as a Unix timestamp in\nmilliseconds.",
|
||||
"O11yMetricHighlights.totalTimeSeries": "TotalTimeSeries is how many time series the metric has ever had.",
|
||||
"O11yMetricHighlightsOut.data": "Data holds the highlights.",
|
||||
"O11yMetricHighlightsOut.status": "Status is \"success\".",
|
||||
"O11yMetricNameIn.metricName": "MetricName is the metric's name; it may contain slashes, e.g.\nrun.googleapis.com/request_latencies. Required.",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/metrics/metadata", zip.Doc{
|
||||
Description: "Returns one metric's metadata: description, type, unit,\ntemporality and monotonicity.",
|
||||
Fields: map[string]string{
|
||||
"O11yMetricMetadata.description": "Description describes the metric.",
|
||||
"O11yMetricMetadata.isMonotonic": "IsMonotonic marks a sum that only ever increases.",
|
||||
"O11yMetricMetadata.temporality": "Temporality is delta or cumulative.",
|
||||
"O11yMetricMetadata.type": "Type is the metric type, e.g. gauge, sum, histogram.",
|
||||
"O11yMetricMetadata.unit": "Unit is the metric's unit.",
|
||||
"O11yMetricMetadataOut.data": "Data holds the metadata.",
|
||||
"O11yMetricMetadataOut.status": "Status is \"success\".",
|
||||
"O11yMetricNameIn.metricName": "MetricName is the metric's name; it may contain slashes, e.g.\nrun.googleapis.com/request_latencies. Required.",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/metrics/onboarding", zip.Doc{
|
||||
Description: "Reports whether any non-O11y metrics have been ingested —\nthe lightweight check onboarding polls.",
|
||||
Fields: map[string]string{
|
||||
"O11yMetricOnboarding.hasMetrics": "HasMetrics is true once any non-O11y metric has been ingested.",
|
||||
"O11yMetricOnboardingOut.data": "Data holds the flag.",
|
||||
"O11yMetricOnboardingOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/metric_reduction_rules", zip.Doc{
|
||||
Description: "Creates a volume-control rule for a metric and returns\nit with its id; a metric that already has a rule is refused.",
|
||||
Fields: map[string]string{
|
||||
"O11yReductionRule.active": "Active says whether the rule is in force.",
|
||||
"O11yReductionRule.createdAt": "CreatedAt is when the rule was created.",
|
||||
"O11yReductionRule.createdBy": "CreatedBy is who created it.",
|
||||
"O11yReductionRule.effectiveFrom": "EffectiveFrom is when the rule took effect.",
|
||||
"O11yReductionRule.id": "ID is the rule's id.",
|
||||
"O11yReductionRule.ingestedSamples": "IngestedSamples is how many samples arrived while the rule was active.",
|
||||
"O11yReductionRule.ingestedSeries": "IngestedSeries is how many series arrived while the rule was active.",
|
||||
"O11yReductionRule.labels": "Labels are the label names the rule matches.",
|
||||
"O11yReductionRule.matchType": "MatchType is drop or keep.",
|
||||
"O11yReductionRule.metricName": "MetricName is the metric the rule governs.",
|
||||
"O11yReductionRule.retainedSamples": "RetainedSamples is how many of them were kept.",
|
||||
"O11yReductionRule.retainedSeries": "RetainedSeries is how many of them were kept.",
|
||||
"O11yReductionRule.updatedAt": "UpdatedAt is when the rule last changed.",
|
||||
"O11yReductionRule.updatedBy": "UpdatedBy is who last changed it.",
|
||||
"O11yReductionRuleCreateIn.labels": "Labels are the label names the rule matches. Required, at least one.",
|
||||
"O11yReductionRuleCreateIn.matchType": "MatchType is drop or keep: drop the named labels, or keep only them. Required.",
|
||||
"O11yReductionRuleCreateIn.metricName": "MetricName is the metric the rule governs; one rule per metric. Required.",
|
||||
"O11yReductionRuleOut.data": "Data holds the rule.",
|
||||
"O11yReductionRuleOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/metric_reduction_rules/preview", zip.Doc{
|
||||
Description: "Estimates the series reduction and the dashboards and\nalerts a candidate volume-control rule would touch, without persisting it.",
|
||||
Fields: map[string]string{
|
||||
"O11yAffectedAsset.id": "ID is the asset's id.",
|
||||
"O11yAffectedAsset.impactedLabels": "ImpactedLabels are the rule labels the asset uses.",
|
||||
"O11yAffectedAsset.name": "Name is the asset's name.",
|
||||
"O11yAffectedAsset.type": "Type is dashboard or alert_rule.",
|
||||
"O11yAffectedAsset.widget": "Widget is the affected panel, for a dashboard.",
|
||||
"O11yAffectedWidget.id": "ID is the panel's id.",
|
||||
"O11yAffectedWidget.name": "Name is the panel's name.",
|
||||
"O11yReductionRulePreview.affectedAssets": "AffectedAssets are the dashboards and alerts the rule would touch.",
|
||||
"O11yReductionRulePreview.currentRetainedSeries": "CurrentRetainedSeries is how many survive the rules in force today.",
|
||||
"O11yReductionRulePreview.droppedLabels": "DroppedLabels are the labels the rule would drop.",
|
||||
"O11yReductionRulePreview.effectiveFrom": "EffectiveFrom is when the rule would take effect.",
|
||||
"O11yReductionRulePreview.ingestedSeries": "IngestedSeries is how many series the metric ingests today.",
|
||||
"O11yReductionRulePreview.reductionPercent": "ReductionPercent is the estimated reduction, in percent.",
|
||||
"O11yReductionRulePreview.retainedSeries": "RetainedSeries is how many would survive with the candidate rule.",
|
||||
"O11yReductionRulePreviewIn.labels": "Labels are the label names the rule would match. Required, at least one.",
|
||||
"O11yReductionRulePreviewIn.lookbackMs": "LookbackMs is how far back to sample when estimating.",
|
||||
"O11yReductionRulePreviewIn.matchType": "MatchType is drop or keep. Required.",
|
||||
"O11yReductionRulePreviewIn.metricName": "MetricName is the metric the rule would govern. Required.",
|
||||
"O11yReductionRulePreviewOut.data": "Data holds the estimate.",
|
||||
"O11yReductionRulePreviewOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/metrics/inspect", zip.Doc{
|
||||
Description: "Returns one metric's raw time series over a window of at most\nthirty minutes — each series with its labels and timestamp/value pairs.",
|
||||
Fields: map[string]string{
|
||||
"O11yMetricField.description": "Description describes the field.",
|
||||
"O11yMetricField.fieldContext": "FieldContext is the context the field lives in, e.g. resource, attribute.",
|
||||
"O11yMetricField.fieldDataType": "FieldDataType is the field's data type.",
|
||||
"O11yMetricField.name": "Name is the field's name.",
|
||||
"O11yMetricField.signal": "Signal is the telemetry signal the field belongs to.",
|
||||
"O11yMetricField.unit": "Unit is the field's unit.",
|
||||
"O11yMetricFilter.expression": "Expression is the filter, in the query-builder filter syntax.",
|
||||
"O11yMetricInspectIn.end": "End is the end of the window as a Unix timestamp in milliseconds, at most\nthirty minutes after start. Required.",
|
||||
"O11yMetricInspectIn.filter": "Filter narrows the series returned.",
|
||||
"O11yMetricInspectIn.metricName": "MetricName is the metric to inspect. Required.",
|
||||
"O11yMetricInspectIn.start": "Start is the start of the window as a Unix timestamp in milliseconds. Required.",
|
||||
"O11yMetricInspectOut.data": "Data holds the series.",
|
||||
"O11yMetricInspectOut.status": "Status is \"success\".",
|
||||
"O11yMetricLabel.key": "Key is the label's field.",
|
||||
"O11yMetricLabel.value": "Value is the label's value.",
|
||||
"O11yMetricPoint.partial": "Partial marks a point whose bucket the window only partly covers.",
|
||||
"O11yMetricPoint.timestamp": "Timestamp is the point's time as a Unix timestamp in milliseconds.",
|
||||
"O11yMetricPoint.value": "Value is the point's value.",
|
||||
"O11yMetricPoint.values": "Values carries the bucket values of a heatmap point.",
|
||||
"O11yMetricSeries.labels": "Labels identify the series.",
|
||||
"O11yMetricSeries.values": "Values are the series' points, in time order.",
|
||||
"O11yMetricSeriesSet.series": "Series are the time series.",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/metrics/metadata", zip.Doc{
|
||||
Description: "Updates one metric's metadata — description, type, unit,\ntemporality, monotonicity — and answers with the bare success envelope.",
|
||||
Fields: map[string]string{
|
||||
"O11yMetricAckOut.status": "Status is \"success\".",
|
||||
"O11yMetricMetadataSaveIn.description": "Description describes the metric.",
|
||||
"O11yMetricMetadataSaveIn.isMonotonic": "IsMonotonic marks a sum that only ever increases.",
|
||||
"O11yMetricMetadataSaveIn.metricName": "MetricName is the metric to update. Required.",
|
||||
"O11yMetricMetadataSaveIn.temporality": "Temporality is delta or cumulative.",
|
||||
"O11yMetricMetadataSaveIn.type": "Type is the metric type, e.g. gauge, sum, histogram.",
|
||||
"O11yMetricMetadataSaveIn.unit": "Unit is the metric's unit.",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/metrics/stats", zip.Doc{
|
||||
Description: "Lists metrics with their sample and time-series counts for a\ntime range — the volume view of the metrics explorer, pageable and sortable.",
|
||||
Fields: map[string]string{
|
||||
"O11yMetricField.description": "Description describes the field.",
|
||||
"O11yMetricField.fieldContext": "FieldContext is the context the field lives in, e.g. resource, attribute.",
|
||||
"O11yMetricField.fieldDataType": "FieldDataType is the field's data type.",
|
||||
"O11yMetricField.name": "Name is the field's name.",
|
||||
"O11yMetricField.signal": "Signal is the telemetry signal the field belongs to.",
|
||||
"O11yMetricField.unit": "Unit is the field's unit.",
|
||||
"O11yMetricFilter.expression": "Expression is the filter, in the query-builder filter syntax.",
|
||||
"O11yMetricOrder.direction": "Direction is asc or desc.",
|
||||
"O11yMetricOrder.key": "Key is the field to order by.",
|
||||
"O11yMetricStat.description": "Description describes the metric.",
|
||||
"O11yMetricStat.metricName": "MetricName is the metric's name.",
|
||||
"O11yMetricStat.samples": "Samples is how many samples the metric had.",
|
||||
"O11yMetricStat.timeseries": "TimeSeries is how many time series the metric had.",
|
||||
"O11yMetricStat.type": "Type is the metric type.",
|
||||
"O11yMetricStat.unit": "Unit is the metric's unit.",
|
||||
"O11yMetricStats.metrics": "Metrics are the counted metrics.",
|
||||
"O11yMetricStats.total": "Total is how many metrics matched, across all pages.",
|
||||
"O11yMetricStatsIn.end": "End is the end of the window as a Unix timestamp in milliseconds. Required.",
|
||||
"O11yMetricStatsIn.filter": "Filter narrows the metrics counted.",
|
||||
"O11yMetricStatsIn.limit": "Limit caps how many metrics come back, between 1 and 5000. Required.",
|
||||
"O11yMetricStatsIn.offset": "Offset is how many metrics to skip, for paging.",
|
||||
"O11yMetricStatsIn.orderBy": "OrderBy sorts the page, by samples or timeseries.",
|
||||
"O11yMetricStatsIn.start": "Start is the start of the window as a Unix timestamp in milliseconds. Required.",
|
||||
"O11yMetricStatsOut.data": "Data holds the statistics.",
|
||||
"O11yMetricStatsOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/metrics/treemap", zip.Doc{
|
||||
Description: "Returns the proportional distribution of metrics by sample\ncount or time-series count, as the entries of a treemap.",
|
||||
Fields: map[string]string{
|
||||
"O11yMetricFilter.expression": "Expression is the filter, in the query-builder filter syntax.",
|
||||
"O11yMetricTreemap.samples": "Samples are the entries when measuring by sample count.",
|
||||
"O11yMetricTreemap.timeseries": "TimeSeries are the entries when measuring by time-series count.",
|
||||
"O11yMetricTreemapIn.end": "End is the end of the window as a Unix timestamp in milliseconds. Required.",
|
||||
"O11yMetricTreemapIn.filter": "Filter narrows the metrics counted.",
|
||||
"O11yMetricTreemapIn.limit": "Limit caps how many entries come back, between 1 and 5000. Required.",
|
||||
"O11yMetricTreemapIn.mode": "Mode picks the measure: timeseries or samples. Required.",
|
||||
"O11yMetricTreemapIn.start": "Start is the start of the window as a Unix timestamp in milliseconds. Required.",
|
||||
"O11yMetricTreemapOut.data": "Data holds the treemap.",
|
||||
"O11yMetricTreemapOut.status": "Status is \"success\".",
|
||||
"O11yTreemapEntry.metricName": "MetricName is the metric's name.",
|
||||
"O11yTreemapEntry.percentage": "Percentage is the metric's share, in percent.",
|
||||
"O11yTreemapEntry.totalValue": "TotalValue is the metric's absolute count.",
|
||||
},
|
||||
})
|
||||
zip.Describe("PUT /v1/o11y/metric_reduction_rules/:id", zip.Doc{
|
||||
Description: "Updates the match type and labels of a volume-control rule\nby its id; the metric name is immutable.",
|
||||
Fields: map[string]string{
|
||||
"O11yReductionRule.active": "Active says whether the rule is in force.",
|
||||
"O11yReductionRule.createdAt": "CreatedAt is when the rule was created.",
|
||||
"O11yReductionRule.createdBy": "CreatedBy is who created it.",
|
||||
"O11yReductionRule.effectiveFrom": "EffectiveFrom is when the rule took effect.",
|
||||
"O11yReductionRule.id": "ID is the rule's id.",
|
||||
"O11yReductionRule.ingestedSamples": "IngestedSamples is how many samples arrived while the rule was active.",
|
||||
"O11yReductionRule.ingestedSeries": "IngestedSeries is how many series arrived while the rule was active.",
|
||||
"O11yReductionRule.labels": "Labels are the label names the rule matches.",
|
||||
"O11yReductionRule.matchType": "MatchType is drop or keep.",
|
||||
"O11yReductionRule.metricName": "MetricName is the metric the rule governs.",
|
||||
"O11yReductionRule.retainedSamples": "RetainedSamples is how many of them were kept.",
|
||||
"O11yReductionRule.retainedSeries": "RetainedSeries is how many of them were kept.",
|
||||
"O11yReductionRule.updatedAt": "UpdatedAt is when the rule last changed.",
|
||||
"O11yReductionRule.updatedBy": "UpdatedBy is who last changed it.",
|
||||
"O11yReductionRuleOut.data": "Data holds the rule.",
|
||||
"O11yReductionRuleOut.status": "Status is \"success\".",
|
||||
"O11yReductionRuleSaveIn.id": "ID is the rule's id.",
|
||||
"O11yReductionRuleSaveIn.labels": "Labels are the label names the rule matches. Required, at least one.",
|
||||
"O11yReductionRuleSaveIn.matchType": "MatchType is drop or keep. Required.",
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -25,18 +25,27 @@ import (
|
||||
// after o11y.New + app.NewServer.
|
||||
// - Until a handler is registered, the routes 503 with a clear error.
|
||||
//
|
||||
// All traffic under /v1/o11y is delegated to the registered http.Handler via
|
||||
// zip.AdaptNetHTTP, PATH UNTOUCHED. There is no rewrite seam: every route is
|
||||
// registered at its full public path /v1/o11y/<resource>, so the route literal
|
||||
// IS the contract — one spelling, nothing to translate, nothing to drift.
|
||||
// EVERY ROUTE IS NAMED. There is no /v1/o11y/* catch-all any more. The runtime
|
||||
// registers 367 method+path pairs; 356 of them are typed ops declared in the
|
||||
// slice files below, and the remaining 11 are registered one by one in
|
||||
// mountHatches with the reason each cannot be typed written next to it. A
|
||||
// catch-all hides the difference between "converted" and "not converted" — the
|
||||
// dark-slice defect this package already shipped once was invisible precisely
|
||||
// because a wildcard answered for the routes nobody had wired up. A named route
|
||||
// per hatch makes the escape hatches countable, and makes any NEW un-typed route
|
||||
// a 404 that someone has to come and justify rather than a silent fall-through.
|
||||
//
|
||||
// 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.
|
||||
func Mount(app *zip.App, deps cloud.Deps) error {
|
||||
log := deps.Logger
|
||||
if log == nil {
|
||||
log = luxlog.New("module", "o11y")
|
||||
}
|
||||
log.Info("o11y: mounting routes", "prefix", "/v1/o11y")
|
||||
log.Info("o11y: mounting routes", "prefix", o11yRoot)
|
||||
|
||||
// Native probe group, registered ahead of the delegation wildcard so Fiber's
|
||||
// Native probe group, registered ahead of everything else so Fiber's
|
||||
// in-order match serves it off the mux tree (see health.go).
|
||||
mountHealth(app)
|
||||
// The TYPED telemetry ops of the error-tracking face — the five reads that
|
||||
@@ -51,14 +60,14 @@ func Mount(app *zip.App, deps cloud.Deps) error {
|
||||
mountInfra(app)
|
||||
// The TYPED logs ops — the record read, the field catalog and its tuning
|
||||
// write, the aggregate read, pipelines and path promotion; live tail stays
|
||||
// on the wildcard because it is a stream. Same construction: they answer
|
||||
// on the runtime, so the wire is unchanged; see logs.go.
|
||||
// a hatch because it is a stream. Same construction: they answer on the
|
||||
// runtime, so the wire is unchanged; see logs.go.
|
||||
mountLogs(app)
|
||||
// The TYPED identity ops — users, invites, passwords, roles-on-users,
|
||||
// sessions, auth domains, my-org, preferences and quick filters. Same
|
||||
// construction: named In, named Out, answered on the runtime, wire
|
||||
// unchanged; see identity.go. The three /complete/* sign-in callbacks
|
||||
// stay on the wildcard below — they answer with redirects, not JSON.
|
||||
// unchanged; see identity.go. The three /complete/* sign-in callbacks stay
|
||||
// hatches — they answer with redirects, not JSON.
|
||||
mountIdentity(app)
|
||||
// The TYPED access-control ops — roles, service accounts, service-account
|
||||
// keys and the authorization probe. Same construction: named In, named Out,
|
||||
@@ -94,8 +103,8 @@ func Mount(app *zip.App, deps cloud.Deps) error {
|
||||
// registration, retention and apdex settings, licenses, global config,
|
||||
// feature flags, org stats, filter suggestions, k8s onboarding, metric
|
||||
// metadata, span percentiles and query-filter analysis. Same construction:
|
||||
// they answer on the runtime through the shared relayAt seam, so the wire is
|
||||
// unchanged; see platform.go. The three service probes (healthz, readyz,
|
||||
// they answer on the runtime through the one shared relay seam, so the wire
|
||||
// is unchanged; see platform.go. The three service probes (healthz, readyz,
|
||||
// livez) stay on mountHealth above — its fall-through-when-unset is
|
||||
// load-bearing.
|
||||
mountPlatform(app)
|
||||
@@ -104,24 +113,89 @@ func Mount(app *zip.App, deps cloud.Deps) error {
|
||||
// the builder-format echo, attribute autocomplete, the field catalog and the
|
||||
// saved explorer views. Same construction: named In, named Out, answered on
|
||||
// the runtime, wire unchanged; see querycore.go. Query progress (long-poll
|
||||
// and websocket) and raw-data export stay on the wildcard below — they are
|
||||
// streams, not JSON answers.
|
||||
// and websocket) and raw-data export stay hatches — they are streams, not
|
||||
// JSON answers.
|
||||
mountQueryCore(app)
|
||||
// The last three slices. They were DARK: their files landed with the rest of
|
||||
// the conversion, but nothing here called them, so 83 typed ops existed in the
|
||||
// source and reached no router at all. An uncalled package-level func is legal
|
||||
// Go, so the package built and the whole suite passed while every one of those
|
||||
// routes still fell through the wildcard below — the conversion was true of the
|
||||
// source and false of the binary. Route-table arithmetic is what caught it:
|
||||
// Mount registered 250 routes against 321 zip.* registrations across the slice
|
||||
// files, and the 83 missing were exactly these three.
|
||||
// The last three slices of the first conversion pass. They were DARK: their
|
||||
// files landed with the rest of the conversion, but nothing here called them,
|
||||
// so 83 typed ops existed in the source and reached no router at all. An
|
||||
// uncalled package-level func is legal Go, so the package built and the whole
|
||||
// suite passed while every one of those routes still fell through the
|
||||
// wildcard — the conversion was true of the source and false of the binary.
|
||||
// Route-table arithmetic is what caught it, and it is why the wildcard is
|
||||
// gone: with every route named, a slice that is not mounted 404s loudly
|
||||
// instead of being quietly answered by a catch-all.
|
||||
mountSentryErrors(app)
|
||||
mountRulesAlerts(app)
|
||||
mountIntegrations(app)
|
||||
app.All("/v1/o11y/*", zip.AdaptNetHTTP(handlerAdapter{}))
|
||||
// The TYPED traces ops — one trace's spans, the trace field catalog and its
|
||||
// tuning write, and the three trace-detail views (waterfall, flamegraph,
|
||||
// span aggregations); see traces.go.
|
||||
mountTraces(app)
|
||||
// The TYPED trace-funnel ops — funnel CRUD plus the twelve analytics reads,
|
||||
// six over a saved funnel and six over one described inline; see
|
||||
// tracefunnels.go.
|
||||
mountTraceFunnels(app)
|
||||
// The TYPED span-mapper ops — the ingest-time rules that move or copy span
|
||||
// attributes into resource attributes; see spanmappers.go.
|
||||
mountSpanMappers(app)
|
||||
// And the eleven that cannot be typed, each named and justified.
|
||||
mountHatches(app)
|
||||
return nil
|
||||
}
|
||||
|
||||
// mountHatches registers the ELEVEN routes that cannot be typed ops, one route
|
||||
// literal each, with the reason next to it. This list is meant to shrink and is
|
||||
// meant to be hard to grow: adding to it costs a justification in review, where
|
||||
// a catch-all cost nothing.
|
||||
//
|
||||
// A typed op declares an input type, an output type and a success status, and
|
||||
// zip projects that declaration into the OpenAPI document, the MCP tool, the CLI
|
||||
// command and the by-name call plane. Every route here breaks that declaration
|
||||
// in a way that would make the document LIE, and lying in the document is worse
|
||||
// than being absent from it — a generated client that trusts a false contract
|
||||
// fails at the customer, not at review.
|
||||
//
|
||||
// Two of these eleven were not reachable AT ALL from the composed binary before
|
||||
// this pass: /ws/query_progress and the two /v1/sentry ingest routes sit outside
|
||||
// /v1/o11y, so the old catch-all never saw them. That is the second thing a
|
||||
// wildcard hides — not just which routes are un-typed, but which are missing.
|
||||
func mountHatches(app *zip.App) {
|
||||
h := zip.AdaptNetHTTP(handlerAdapter{})
|
||||
|
||||
// ── 1. STREAMS: there is no one answer to name ───────────────────────────
|
||||
// These never produce a single complete JSON value. relay buffers a whole
|
||||
// answer through an httptest recorder before decoding it, so a typed
|
||||
// livetail would hang on the first tail, and a typed progress poll would
|
||||
// return only after the query it reports on had already finished — which is
|
||||
// the one thing a progress endpoint must not do.
|
||||
app.Get(o11yRoot+"/logs/livetail", h) // unbounded stream of log records
|
||||
app.Get(o11yRoot+"/query_progress", h) // long-poll: holds the connection until the next tick
|
||||
app.Get("/ws/query_progress", h) // the same read over a websocket; the Upgrade IS the contract
|
||||
app.Post(o11yRoot+"/export_raw_data", h) // chunked CSV/JSONL attachment, X-Response-Complete trailer
|
||||
|
||||
// ── 2. REDIRECTS: the answer is a Location, not a body ───────────────────
|
||||
// The three sign-in callbacks answer 303 with a Location header and no
|
||||
// payload. A typed op declares a 2xx JSON contract, so typing these would
|
||||
// publish a response schema for a response that does not exist, and hide the
|
||||
// header that is the entire point of the call.
|
||||
app.Get(o11yRoot+"/complete/google", h) // Google OIDC callback → 303 to the console
|
||||
app.Get(o11yRoot+"/complete/oidc", h) // generic OIDC callback → 303
|
||||
app.Post(o11yRoot+"/complete/saml", h) // SAML assertion consumer → 303
|
||||
|
||||
// ── 3. A FOREIGN PROTOCOL WE RECEIVE ─────────────────────────────────────
|
||||
// Sentry-compatible ingest. The body is an application/x-sentry-envelope
|
||||
// frame, not JSON, and the caller is a Sentry SDK authenticating with a DSN
|
||||
// public key rather than a Hanzo session. The /api/ segment is NOT ours to
|
||||
// name: an SDK appends its own fixed /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.
|
||||
app.Post(o11yRoot+"/api/:project_id/envelope/", h) // Sentry envelope ingest
|
||||
app.Post(o11yRoot+"/api/:project_id/store/", h) // legacy single-event ingest
|
||||
app.Post(sentryRoot+"/:project/envelope/", h) // the same wire on the clean /v1/sentry root
|
||||
app.Post(sentryRoot+"/:project/store/", h) // same
|
||||
}
|
||||
|
||||
// handlerAdapter forwards each request to the registered runtime handler
|
||||
// or returns 503 if none is set yet. It does NOT touch r.URL: the runtime's
|
||||
// routes are registered at their full public /v1/o11y/<resource> paths, so a
|
||||
|
||||
+39
-65
@@ -10,77 +10,51 @@ import (
|
||||
"github.com/zap-proto/zip"
|
||||
)
|
||||
|
||||
func TestMountWithoutHandlerReturns503(t *testing.T) {
|
||||
// Mount is total: it must not fail, and it must leave every route the runtime
|
||||
// serves reachable. The counting proof lives in routes_test.go — this file holds
|
||||
// what is true of the SEAM itself.
|
||||
|
||||
func TestMountSucceedsWithoutDeps(t *testing.T) {
|
||||
app := zip.New(zip.Config{DisableStartupMessage: true})
|
||||
if err := o11y.Mount(app, cloud.Deps{}); err != nil {
|
||||
t.Fatalf("Mount: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// A NAMED route with no runtime behind it answers 503, not 404: "the runtime is
|
||||
// not up yet" is a different fact from "there is no such route", and a caller —
|
||||
// or a load balancer — needs to be able to tell them apart.
|
||||
//
|
||||
// This test used to ask for /v1/o11y/anything and expect 503, which was only
|
||||
// true because a catch-all answered every path under the prefix. That is the
|
||||
// assertion that let three unmounted slices pass CI: a wildcard makes an
|
||||
// unconverted route and a missing one indistinguishable. It now asks for routes
|
||||
// that actually exist — one hatch and one typed op.
|
||||
func TestNamedRouteWithoutRuntimeReturns503(t *testing.T) {
|
||||
app := zip.New(zip.Config{DisableStartupMessage: true})
|
||||
if err := o11y.Mount(app, cloud.Deps{}); err != nil {
|
||||
t.Fatalf("Mount: %v", err)
|
||||
}
|
||||
o11y.SetHandler(nil)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/v1/o11y/anything", nil)
|
||||
resp, err := app.Fiber().Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("Test: %v", err)
|
||||
}
|
||||
if resp.StatusCode != http.StatusServiceUnavailable {
|
||||
t.Fatalf("status=%d want 503", resp.StatusCode)
|
||||
for _, target := range []string{
|
||||
"/v1/o11y/logs/livetail", // a hatch
|
||||
"/v1/o11y/logs", // a typed op
|
||||
} {
|
||||
resp, err := app.Fiber().Test(httptest.NewRequest(http.MethodGet, target, nil))
|
||||
if err != nil {
|
||||
t.Fatalf("Test: %v", err)
|
||||
}
|
||||
if resp.StatusCode != http.StatusServiceUnavailable {
|
||||
t.Fatalf("%s status=%d want 503", target, resp.StatusCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestMountDelegatesPathVerbatim proves the mount seam does not touch the path.
|
||||
// Routes are registered at their full public /v1/o11y/<resource> names, so the
|
||||
// runtime handler must receive exactly what the client sent — any rewrite here
|
||||
// can only move a request off its own route. A mangler is invisible to the
|
||||
// compiler, so this is the only thing that catches one coming back.
|
||||
func TestMountDelegatesPathVerbatim(t *testing.T) {
|
||||
app := zip.New(zip.Config{DisableStartupMessage: true})
|
||||
if err := o11y.Mount(app, cloud.Deps{}); err != nil {
|
||||
t.Fatalf("Mount: %v", err)
|
||||
}
|
||||
|
||||
var sawPath string
|
||||
o11y.SetHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
sawPath = r.URL.Path
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer o11y.SetHandler(nil)
|
||||
|
||||
// Each is a real registered route literal (grep them: routes_*.go,
|
||||
// pkg/apiserver/o11yapiserver/*.go). What arrives is what dispatches.
|
||||
paths := []string{
|
||||
"/v1/o11y/services",
|
||||
// REMOVED as the conversion advanced — each of these became a TYPED op and
|
||||
// therefore dispatches off the mux tree AHEAD of the wildcard, which is the
|
||||
// whole point of the migration: /query_range (metrics.go), /settings/ttl and
|
||||
// /global/config (platform.go). A typed op is proved by its own slice test;
|
||||
// asserting it still delegates verbatim would assert the migration failed.
|
||||
// What remains here are the genuinely-wildcarded surfaces — the escape
|
||||
// hatches — which is exactly what this test should guard.
|
||||
"/v1/o11y/query_progress", // long-poll progress: deliberately wildcarded, a stream (querycore.go)
|
||||
"/v1/o11y/complete/google", // sign-in callback: 303 redirect, deliberately wildcarded (identity.go)
|
||||
// the /v1/o11y/llm* surface is TYPED now (llmobs.go), so it dispatches to
|
||||
// ops and takes precedence over this wildcard — proved in llmobs_test.go.
|
||||
// REMOVED: /v1/o11y/errortracking/issues is a TYPED op (sentryerrors.go) and
|
||||
// dispatches ahead of the wildcard. It only ever passed here because
|
||||
// mountSentryErrors was never called — this assertion was ENCODING the dark-
|
||||
// slice defect rather than catching it, and flipped red the moment Mount was
|
||||
// fixed. That is the tell: a wildcard-delegation assertion that passes for a
|
||||
// route someone converted is proof the conversion did not reach the router.
|
||||
"/v1/o11y/api/hanzo/envelope/", // Sentry SDK wire path, received as-is
|
||||
}
|
||||
for _, p := range paths {
|
||||
t.Run(p, func(t *testing.T) {
|
||||
sawPath = ""
|
||||
resp, err := app.Fiber().Test(httptest.NewRequest(http.MethodGet, p, nil))
|
||||
if err != nil {
|
||||
t.Fatalf("Test: %v", err)
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("status=%d want 200", resp.StatusCode)
|
||||
}
|
||||
if sawPath != p {
|
||||
t.Fatalf("runtime received %q, want %q verbatim", sawPath, p)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
// Path delegation is proved once, over the whole hatch census, in
|
||||
// routes_test.go's TestHatchesDelegateVerbatim. It is not repeated here: the
|
||||
// earlier version of this test sampled three paths by hand and drifted every
|
||||
// time one of them was converted — including once when it asserted a route
|
||||
// delegated that had in fact been typed, which ENCODED the dark-slice defect
|
||||
// instead of catching it. One census, derived from the same list mount.go
|
||||
// registers, cannot drift that way.
|
||||
|
||||
+26
-26
@@ -14,14 +14,14 @@ package o11y
|
||||
//
|
||||
// THE WIRE DOES NOT MOVE, the same way logs.go's nine reads and telemetry.go's
|
||||
// five sentry reads do not move it: each op hands the call to the SAME runtime
|
||||
// handler the wildcard delegates to, through the ONE shared seam relayAt (logs.go)
|
||||
// handler the wildcard delegates to, through the ONE shared seam relay (relay.go)
|
||||
// — so identity resolution, the org gate, the role check (OpenAccess, ViewAccess
|
||||
// and AdminAccess exactly as the mux tree declares them), the audit record and
|
||||
// the success envelope are all still the runtime's, executed in the order they
|
||||
// always were. What is new here is the TYPE — the In a caller may send and the
|
||||
// Out they get back — and the prose that goes with it. This face is /v1/o11y,
|
||||
// the same root as logs, so it shares logs' relayAt and telemetry's query
|
||||
// rather than minting a third relay: one seam per root, not one per face.
|
||||
// the same root as logs, and every face shares the one seam in relay.go
|
||||
// rather than minting its own: one seam, not one per face.
|
||||
//
|
||||
// Registered ahead of the wildcard, and specific-beats-wildcard is what the
|
||||
// router does regardless of registration order, so these paths dispatch here
|
||||
@@ -93,7 +93,7 @@ func mountPlatform(app *zip.App) {
|
||||
// Callers need the viewer role; the runtime's own gate enforces it.
|
||||
func promQuery(ctx context.Context, in *O11yPromQueryIn) (*O11yPromQueryOut, error) {
|
||||
out := new(O11yPromQueryOut)
|
||||
return out, relayAt(ctx, http.MethodGet, "/query", query(
|
||||
return out, relay(ctx, http.MethodGet, o11yRoot+"/query", query(
|
||||
"query", in.Query,
|
||||
"time", in.Time,
|
||||
"stats", in.Stats,
|
||||
@@ -107,7 +107,7 @@ func promQuery(ctx context.Context, in *O11yPromQueryIn) (*O11yPromQueryOut, err
|
||||
// Callers need the viewer role; the runtime's own gate enforces it.
|
||||
func dashboardVars(ctx context.Context, in *O11yDashboardVarsIn) (*O11yDashboardVarsOut, error) {
|
||||
out := new(O11yDashboardVarsOut)
|
||||
return out, relayAt(ctx, http.MethodPost, "/variables/query", nil, in, out)
|
||||
return out, relay(ctx, http.MethodPost, o11yRoot+"/variables/query", nil, in, out)
|
||||
}
|
||||
|
||||
// event records one product-analytics event for the signed-in user — a track
|
||||
@@ -116,7 +116,7 @@ func dashboardVars(ctx context.Context, in *O11yDashboardVarsIn) (*O11yDashboard
|
||||
// Callers need the viewer role; the runtime's own gate enforces it.
|
||||
func event(ctx context.Context, in *O11yEventIn) (*O11yMessage, error) {
|
||||
out := new(O11yMessage)
|
||||
return out, relayAt(ctx, http.MethodPost, "/event", nil, in, out)
|
||||
return out, relay(ctx, http.MethodPost, o11yRoot+"/event", nil, in, out)
|
||||
}
|
||||
|
||||
// usage returns ingestion usage counts bucketed over the requested window,
|
||||
@@ -125,7 +125,7 @@ func event(ctx context.Context, in *O11yEventIn) (*O11yMessage, error) {
|
||||
// Callers need the viewer role; the runtime's own gate enforces it.
|
||||
func usage(ctx context.Context, in *O11yUsageIn) (*O11yUsageList, error) {
|
||||
out := new(O11yUsageList)
|
||||
return out, relayAt(ctx, http.MethodGet, "/usage", query(
|
||||
return out, relay(ctx, http.MethodGet, o11yRoot+"/usage", query(
|
||||
"start", in.Start,
|
||||
"end", in.End,
|
||||
"step", in.Step,
|
||||
@@ -140,7 +140,7 @@ func usage(ctx context.Context, in *O11yUsageIn) (*O11yUsageList, error) {
|
||||
// Callers need the viewer role; the runtime's own gate enforces it.
|
||||
func dependencyGraph(ctx context.Context, in *O11yDependencyGraphIn) (*O11yDependencyList, error) {
|
||||
out := new(O11yDependencyList)
|
||||
return out, relayAt(ctx, http.MethodPost, "/dependency_graph", nil, in, out)
|
||||
return out, relay(ctx, http.MethodPost, o11yRoot+"/dependency_graph", nil, in, out)
|
||||
}
|
||||
|
||||
// version reports the running build: its version, whether an enterprise
|
||||
@@ -150,7 +150,7 @@ func dependencyGraph(ctx context.Context, in *O11yDependencyGraphIn) (*O11yDepen
|
||||
// Open by design; the runtime's own gate is OpenAccess.
|
||||
func version(ctx context.Context, _ *struct{}) (*O11yVersionOut, error) {
|
||||
out := new(O11yVersionOut)
|
||||
return out, relayAt(ctx, http.MethodGet, "/version", nil, nil, out)
|
||||
return out, relay(ctx, http.MethodGet, o11yRoot+"/version", nil, nil, out)
|
||||
}
|
||||
|
||||
// health reports service health. With live set, the datastore connection is
|
||||
@@ -164,7 +164,7 @@ func health(ctx context.Context, in *O11yHealthIn) (*O11yHealthOut, error) {
|
||||
// The runtime tests presence, not value.
|
||||
p.Set("live", "true")
|
||||
}
|
||||
return out, relayAt(ctx, http.MethodGet, "/health", p, nil, out)
|
||||
return out, relay(ctx, http.MethodGet, o11yRoot+"/health", p, nil, out)
|
||||
}
|
||||
|
||||
// disks lists the storage disks the datastore reports, with their names and
|
||||
@@ -173,7 +173,7 @@ func health(ctx context.Context, in *O11yHealthIn) (*O11yHealthOut, error) {
|
||||
// Callers need the viewer role; the runtime's own gate enforces it.
|
||||
func disks(ctx context.Context, _ *struct{}) (*O11yDiskList, error) {
|
||||
out := new(O11yDiskList)
|
||||
return out, relayAt(ctx, http.MethodGet, "/disks", nil, nil, out)
|
||||
return out, relay(ctx, http.MethodGet, o11yRoot+"/disks", nil, nil, out)
|
||||
}
|
||||
|
||||
// register creates the FIRST organization and its admin user. It is open by
|
||||
@@ -183,7 +183,7 @@ func disks(ctx context.Context, _ *struct{}) (*O11yDiskList, error) {
|
||||
// Open by design; the runtime's own gate is OpenAccess.
|
||||
func register(ctx context.Context, in *O11yRegisterIn) (*O11yRegisterOut, error) {
|
||||
out := new(O11yRegisterOut)
|
||||
return out, relayAt(ctx, http.MethodPost, "/register", nil, in, out)
|
||||
return out, relay(ctx, http.MethodPost, o11yRoot+"/register", nil, in, out)
|
||||
}
|
||||
|
||||
// spanPercentile places one span's duration among its peers: the p50/p90/p99
|
||||
@@ -192,7 +192,7 @@ func register(ctx context.Context, in *O11yRegisterIn) (*O11yRegisterOut, error)
|
||||
// Callers need the viewer role; the runtime's own gate enforces it.
|
||||
func spanPercentile(ctx context.Context, in *O11ySpanPercentileIn) (*O11ySpanPercentileOut, error) {
|
||||
out := new(O11ySpanPercentileOut)
|
||||
return out, relayAt(ctx, http.MethodPost, "/span_percentile", nil, in, out)
|
||||
return out, relay(ctx, http.MethodPost, o11yRoot+"/span_percentile", nil, in, out)
|
||||
}
|
||||
|
||||
// analyzeQueryFilter analyzes a query and extracts the metric names it reads
|
||||
@@ -201,7 +201,7 @@ func spanPercentile(ctx context.Context, in *O11ySpanPercentileIn) (*O11ySpanPer
|
||||
// Callers need the viewer role; the runtime's own gate enforces it.
|
||||
func analyzeQueryFilter(ctx context.Context, in *O11yAnalyzeIn) (*O11yAnalyzeOut, error) {
|
||||
out := new(O11yAnalyzeOut)
|
||||
return out, relayAt(ctx, http.MethodPost, "/query_filter/analyze", nil, in, out)
|
||||
return out, relay(ctx, http.MethodPost, o11yRoot+"/query_filter/analyze", nil, in, out)
|
||||
}
|
||||
|
||||
// filterSuggestions suggests attribute keys and example filter queries for the
|
||||
@@ -211,7 +211,7 @@ func analyzeQueryFilter(ctx context.Context, in *O11yAnalyzeIn) (*O11yAnalyzeOut
|
||||
// Callers need the viewer role; the runtime's own gate enforces it.
|
||||
func filterSuggestions(ctx context.Context, in *O11yFilterSuggestionsIn) (*O11yFilterSuggestionsOut, error) {
|
||||
out := new(O11yFilterSuggestionsOut)
|
||||
return out, relayAt(ctx, http.MethodGet, "/filter_suggestions", query(
|
||||
return out, relay(ctx, http.MethodGet, o11yRoot+"/filter_suggestions", query(
|
||||
"dataSource", in.DataSource,
|
||||
"searchText", in.SearchText,
|
||||
"existingFilter", in.ExistingFilter,
|
||||
@@ -227,7 +227,7 @@ func filterSuggestions(ctx context.Context, in *O11yFilterSuggestionsIn) (*O11yF
|
||||
// Callers need the viewer role; the runtime's own gate enforces it.
|
||||
func k8sOnboarding(ctx context.Context, _ *struct{}) (*O11yOnboardingOut, error) {
|
||||
out := new(O11yOnboardingOut)
|
||||
return out, relayAt(ctx, http.MethodGet, "/infra_onboarding/k8s/status", nil, nil, out)
|
||||
return out, relay(ctx, http.MethodGet, o11yRoot+"/infra_onboarding/k8s/status", nil, nil, out)
|
||||
}
|
||||
|
||||
// legacyMetricMetadata serves the OLDER /metric/metric_metadata route. It is
|
||||
@@ -243,7 +243,7 @@ func k8sOnboarding(ctx context.Context, _ *struct{}) (*O11yOnboardingOut, error)
|
||||
// Callers need the viewer role; the runtime's own gate enforces it.
|
||||
func legacyMetricMetadata(ctx context.Context, in *O11yMetricMetadataIn) (*O11yMetricMetadataOut, error) {
|
||||
out := new(O11yMetricMetadataOut)
|
||||
return out, relayAt(ctx, http.MethodGet, "/metric/metric_metadata", query(
|
||||
return out, relay(ctx, http.MethodGet, o11yRoot+"/metric/metric_metadata", query(
|
||||
"metricName", in.MetricName,
|
||||
"serviceName", in.ServiceName,
|
||||
), nil, out)
|
||||
@@ -257,7 +257,7 @@ func legacyMetricMetadata(ctx context.Context, in *O11yMetricMetadataIn) (*O11yM
|
||||
// own gate enforces it.
|
||||
func setRetention(ctx context.Context, in *O11yRetentionSetIn) (*O11yRetentionSetOut, error) {
|
||||
out := new(O11yRetentionSetOut)
|
||||
return out, relayAt(ctx, http.MethodPost, "/settings/ttl", nil, in, out)
|
||||
return out, relay(ctx, http.MethodPost, o11yRoot+"/settings/ttl", nil, in, out)
|
||||
}
|
||||
|
||||
// retention returns the org's current retention policy: default TTL, custom
|
||||
@@ -266,7 +266,7 @@ func setRetention(ctx context.Context, in *O11yRetentionSetIn) (*O11yRetentionSe
|
||||
// Callers need the viewer role; the runtime's own gate enforces it.
|
||||
func retention(ctx context.Context, _ *struct{}) (*O11yRetentionOut, error) {
|
||||
out := new(O11yRetentionOut)
|
||||
return out, relayAt(ctx, http.MethodGet, "/settings/ttl", nil, nil, out)
|
||||
return out, relay(ctx, http.MethodGet, o11yRoot+"/settings/ttl", nil, nil, out)
|
||||
}
|
||||
|
||||
// setApdex sets one service's apdex threshold and the status codes excluded
|
||||
@@ -276,7 +276,7 @@ func retention(ctx context.Context, _ *struct{}) (*O11yRetentionOut, error) {
|
||||
// own gate enforces it.
|
||||
func setApdex(ctx context.Context, in *O11yApdexSetIn) (*O11yApdexSetOut, error) {
|
||||
out := new(O11yApdexSetOut)
|
||||
return out, relayAt(ctx, http.MethodPost, "/settings/apdex", nil, in, out)
|
||||
return out, relay(ctx, http.MethodPost, o11yRoot+"/settings/apdex", nil, in, out)
|
||||
}
|
||||
|
||||
// apdex returns apdex settings for the named services.
|
||||
@@ -284,7 +284,7 @@ func setApdex(ctx context.Context, in *O11yApdexSetIn) (*O11yApdexSetOut, error)
|
||||
// Callers need the viewer role; the runtime's own gate enforces it.
|
||||
func apdex(ctx context.Context, in *O11yApdexIn) (*O11yApdexOut, error) {
|
||||
out := new(O11yApdexOut)
|
||||
return out, relayAt(ctx, http.MethodGet, "/settings/apdex", query(
|
||||
return out, relay(ctx, http.MethodGet, o11yRoot+"/settings/apdex", query(
|
||||
"services", in.Services,
|
||||
), nil, out)
|
||||
}
|
||||
@@ -295,7 +295,7 @@ func apdex(ctx context.Context, in *O11yApdexIn) (*O11yApdexOut, error) {
|
||||
// Callers need the viewer role; the runtime's own gate enforces it.
|
||||
func licenses(ctx context.Context, _ *struct{}) (*O11yLicensesOut, error) {
|
||||
out := new(O11yLicensesOut)
|
||||
return out, relayAt(ctx, http.MethodGet, "/licenses", nil, nil, out)
|
||||
return out, relay(ctx, http.MethodGet, o11yRoot+"/licenses", nil, nil, out)
|
||||
}
|
||||
|
||||
// activateLicense activates the enterprise license. This build has no
|
||||
@@ -304,7 +304,7 @@ func licenses(ctx context.Context, _ *struct{}) (*O11yLicensesOut, error) {
|
||||
// Callers need the viewer role; the runtime's own gate enforces it.
|
||||
func activateLicense(ctx context.Context, _ *struct{}) (*O11yLicenseActiveOut, error) {
|
||||
out := new(O11yLicenseActiveOut)
|
||||
return out, relayAt(ctx, http.MethodGet, "/licenses/active", nil, nil, out)
|
||||
return out, relay(ctx, http.MethodGet, o11yRoot+"/licenses/active", nil, nil, out)
|
||||
}
|
||||
|
||||
// globalConfig returns the deployment's global configuration: its public
|
||||
@@ -314,7 +314,7 @@ func activateLicense(ctx context.Context, _ *struct{}) (*O11yLicenseActiveOut, e
|
||||
// Open by design; the runtime's own gate is OpenAccess.
|
||||
func globalConfig(ctx context.Context, _ *struct{}) (*O11yGlobalConfigOut, error) {
|
||||
out := new(O11yGlobalConfigOut)
|
||||
return out, relayAt(ctx, http.MethodGet, "/global/config", nil, nil, out)
|
||||
return out, relay(ctx, http.MethodGet, o11yRoot+"/global/config", nil, nil, out)
|
||||
}
|
||||
|
||||
// features returns the supported feature flags and their resolved values for
|
||||
@@ -323,7 +323,7 @@ func globalConfig(ctx context.Context, _ *struct{}) (*O11yGlobalConfigOut, error
|
||||
// Callers need the viewer role; the runtime's own gate enforces it.
|
||||
func features(ctx context.Context, _ *struct{}) (*O11yFeaturesOut, error) {
|
||||
out := new(O11yFeaturesOut)
|
||||
return out, relayAt(ctx, http.MethodGet, "/features", nil, nil, out)
|
||||
return out, relay(ctx, http.MethodGet, o11yRoot+"/features", nil, nil, out)
|
||||
}
|
||||
|
||||
// orgStats returns the collected usage statistics for the caller's org, as the
|
||||
@@ -333,7 +333,7 @@ func features(ctx context.Context, _ *struct{}) (*O11yFeaturesOut, error) {
|
||||
// Callers need the viewer role; the runtime's own gate enforces it.
|
||||
func orgStats(ctx context.Context, _ *struct{}) (*O11yOrgStatsOut, error) {
|
||||
out := new(O11yOrgStatsOut)
|
||||
return out, relayAt(ctx, http.MethodGet, "/stats", nil, nil, out)
|
||||
return out, relay(ctx, http.MethodGet, o11yRoot+"/stats", nil, nil, out)
|
||||
}
|
||||
|
||||
// ── inputs ────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -1,324 +0,0 @@
|
||||
// Code generated by zipdoc; DO NOT EDIT.
|
||||
|
||||
package o11y
|
||||
|
||||
import (
|
||||
"github.com/zap-proto/zip"
|
||||
)
|
||||
|
||||
func init() {
|
||||
zip.Describe("GET /v1/o11y/disks", zip.Doc{
|
||||
Description: "Lists the storage disks the datastore reports, with their names and\ntypes.\n\nCallers need the viewer role; the runtime's own gate enforces it.",
|
||||
Fields: map[string]string{
|
||||
"O11yDisk.name": "Name is the disk's name.",
|
||||
"O11yDisk.type": "Type is the disk's type, e.g. local or s3.",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/features", zip.Doc{
|
||||
Description: "Returns the supported feature flags and their resolved values for\nthe caller's org.\n\nCallers need the viewer role; the runtime's own gate enforces it.",
|
||||
Fields: map[string]string{
|
||||
"O11yFeature.defaultVariant": "DefaultVariant is the variant used when nothing overrides it.",
|
||||
"O11yFeature.description": "Description says what the flag gates.",
|
||||
"O11yFeature.kind": "Kind is the flag's value kind, e.g. boolean.",
|
||||
"O11yFeature.name": "Name is the flag's name.",
|
||||
"O11yFeature.resolvedValue": "ResolvedValue is the value resolved for the caller's org.",
|
||||
"O11yFeature.stage": "Stage is the flag's lifecycle stage, e.g. stable.",
|
||||
"O11yFeature.variants": "Variants are the flag's possible values, by variant name.",
|
||||
"O11yFeaturesOut.data": "Data are the features.",
|
||||
"O11yFeaturesOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/filter_suggestions", zip.Doc{
|
||||
Description: "Suggests attribute keys and example filter queries for the\nquery builder, seeded by what the org's own telemetry carries. Only the logs\ndata source is supported today.\n\nCallers need the viewer role; the runtime's own gate enforces it.",
|
||||
Fields: map[string]string{
|
||||
"O11yAttributeKey.dataType": "DataType is the attribute's value type — string, int64, float64 or bool.",
|
||||
"O11yAttributeKey.isColumn": "IsColumn marks an attribute materialized as its own column.",
|
||||
"O11yAttributeKey.isJSON": "IsJSON marks an attribute extracted from a JSON body.",
|
||||
"O11yAttributeKey.key": "Key is the attribute's name.",
|
||||
"O11yAttributeKey.type": "Type says where the attribute lives: tag or resource.",
|
||||
"O11yFilterItem.key": "Key is the attribute tested.",
|
||||
"O11yFilterItem.op": "Operator is the comparison, e.g. =, !=, in, contains.",
|
||||
"O11yFilterItem.value": "Value is what it is tested against; its type follows the attribute's.",
|
||||
"O11yFilterSet.items": "Items are the conditions.",
|
||||
"O11yFilterSet.op": "Operator combines the items — AND or OR.",
|
||||
"O11yFilterSuggestions.attributes": "Attributes are the suggested attribute keys.",
|
||||
"O11yFilterSuggestions.example_queries": "ExampleQueries are ready-to-run filter sets.",
|
||||
"O11yFilterSuggestionsIn.attributesLimit": "AttributesLimit caps how many attribute keys come back.",
|
||||
"O11yFilterSuggestionsIn.dataSource": "DataSource is the signal suggestions are drawn from; only logs is\nsupported today. Required.",
|
||||
"O11yFilterSuggestionsIn.examplesLimit": "ExamplesLimit caps how many example queries come back.",
|
||||
"O11yFilterSuggestionsIn.existingFilter": "ExistingFilter is the current filter set, JSON base64url-encoded, so\nexample queries build on it rather than repeat it.",
|
||||
"O11yFilterSuggestionsIn.searchText": "SearchText narrows attribute suggestions to keys containing it.",
|
||||
"O11yFilterSuggestionsOut.data": "Data holds the suggestions.",
|
||||
"O11yFilterSuggestionsOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/global/config", zip.Doc{
|
||||
Description: "Returns the deployment's global configuration: its public\nendpoints and which identity providers are enabled. Open by design — the\nsign-in page reads it before anyone is signed in.\n\nOpen by design; the runtime's own gate is OpenAccess.",
|
||||
Fields: map[string]string{
|
||||
"O11yGlobalConfig.ai_assistant_url": "AIAssistantURL is the AI assistant endpoint, when one is exposed.",
|
||||
"O11yGlobalConfig.external_url": "ExternalURL is the deployment's public URL.",
|
||||
"O11yGlobalConfig.identN": "IdentN says which identity providers are enabled.",
|
||||
"O11yGlobalConfig.ingestion_url": "IngestionURL is where telemetry is sent.",
|
||||
"O11yGlobalConfig.mcp_url": "MCPURL is the MCP endpoint, when one is exposed.",
|
||||
"O11yGlobalConfigOut.data": "Data is the configuration.",
|
||||
"O11yGlobalConfigOut.status": "Status is \"success\".",
|
||||
"O11yIdentN.apikey": "APIKey is the API-key provider.",
|
||||
"O11yIdentN.impersonation": "Impersonation is the impersonation provider.",
|
||||
"O11yIdentN.tokenizer": "Tokenizer is the token-based provider.",
|
||||
"O11yToggle.enabled": "Enabled says whether the feature is on.",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/health", zip.Doc{
|
||||
Description: "Reports service health. With live set, the datastore connection is\nchecked too and an unhealthy store refuses with 503.\n\nOpen by design; the runtime's own gate is OpenAccess.",
|
||||
Fields: map[string]string{
|
||||
"O11yHealthIn.live": "Live also checks the datastore connection; an unreachable store refuses\nwith 503.",
|
||||
"O11yHealthOut.status": "Status is \"ok\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/infra_onboarding/k8s/status", zip.Doc{
|
||||
Description: "Reports how far Kubernetes infra onboarding has progressed:\nwhich metric families have arrived and, per pod, which required metadata\nlabels are present.\n\nCallers need the viewer role; the runtime's own gate enforces it.",
|
||||
Fields: map[string]string{
|
||||
"O11yK8sOnboarding.didSendClusterMetrics": "DidSendClusterMetrics says whether cluster metrics have arrived.",
|
||||
"O11yK8sOnboarding.didSendNodeMetrics": "DidSendNodeMetrics says whether node metrics have arrived.",
|
||||
"O11yK8sOnboarding.didSendPodMetrics": "DidSendPodMetrics says whether pod metrics have arrived.",
|
||||
"O11yK8sOnboarding.isSendingOptionalPodMetrics": "IsSendingOptionalPodMetrics says whether optional pod metrics are\nflowing.",
|
||||
"O11yK8sOnboarding.isSendingRequiredMetadata": "IsSendingRequiredMetadata reports, per pod, which required metadata\nlabels are present.",
|
||||
"O11yOnboardingOut.data": "Data is the progress report.",
|
||||
"O11yOnboardingOut.status": "Status is \"success\".",
|
||||
"O11yPodOnboarding.clusterName": "ClusterName is the pod's cluster.",
|
||||
"O11yPodOnboarding.hasClusterName": "HasClusterName says whether the cluster label is present.",
|
||||
"O11yPodOnboarding.hasCronjobName": "HasCronjobName says whether the cronjob label is present.",
|
||||
"O11yPodOnboarding.hasDaemonsetName": "HasDaemonsetName says whether the daemonset label is present.",
|
||||
"O11yPodOnboarding.hasDeploymentName": "HasDeploymentName says whether the deployment label is present.",
|
||||
"O11yPodOnboarding.hasJobName": "HasJobName says whether the job label is present.",
|
||||
"O11yPodOnboarding.hasNamespaceName": "HasNamespaceName says whether the namespace label is present.",
|
||||
"O11yPodOnboarding.hasNodeName": "HasNodeName says whether the node label is present.",
|
||||
"O11yPodOnboarding.hasStatefulsetName": "HasStatefulsetName says whether the statefulset label is present.",
|
||||
"O11yPodOnboarding.namespaceName": "NamespaceName is the pod's namespace.",
|
||||
"O11yPodOnboarding.nodeName": "NodeName is the pod's node.",
|
||||
"O11yPodOnboarding.podName": "PodName is the pod.",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/licenses", zip.Doc{
|
||||
Description: "Lists the org's licenses. This build has no enterprise edition, so\nthe list is intentionally empty.\n\nCallers need the viewer role; the runtime's own gate enforces it.",
|
||||
Fields: map[string]string{
|
||||
"O11yLicensesOut.data": "Data are the licenses.",
|
||||
"O11yLicensesOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/licenses/active", zip.Doc{
|
||||
Description: "Activates the enterprise license. This build has no\nenterprise edition, so the licensing provider refuses it as unsupported.\n\nCallers need the viewer role; the runtime's own gate enforces it.",
|
||||
Fields: map[string]string{
|
||||
"O11yLicenseActiveOut.data": "Data is the license.",
|
||||
"O11yLicenseActiveOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/metric/metric_metadata", zip.Doc{
|
||||
Description: "Returns one metric's metadata — its type, unit, description,\ntemporality, monotonicity and histogram buckets — optionally scoped to the\nmetric as one service reports it.\n\nCallers need the viewer role; the runtime's own gate enforces it.",
|
||||
Fields: map[string]string{
|
||||
"O11yMetricMetadataIn.metricName": "MetricName is the metric to read.",
|
||||
"O11yMetricMetadataIn.serviceName": "ServiceName scopes the metadata to the metric as one service reports it.",
|
||||
"O11yMetricMetadataOut.delta": "Delta says whether the metric is delta-temporality.",
|
||||
"O11yMetricMetadataOut.description": "Description is the metric's description.",
|
||||
"O11yMetricMetadataOut.isMonotonic": "IsMonotonic says whether a sum only ever increases.",
|
||||
"O11yMetricMetadataOut.le": "Le are the histogram bucket bounds, for histograms.",
|
||||
"O11yMetricMetadataOut.temporality": "Temporality is cumulative or delta.",
|
||||
"O11yMetricMetadataOut.type": "Type is the metric's type — gauge, sum, histogram.",
|
||||
"O11yMetricMetadataOut.unit": "Unit is the metric's unit.",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/query", zip.Doc{
|
||||
Description: "Evaluates one instant PromQL query against the org's metrics and\nreturns the result at a single point in time.\n\nThe result is polymorphic by PromQL's own contract — a matrix, vector,\nscalar or string, discriminated by resultType — so it is carried verbatim\nrather than forced into one of its shapes.\n\nCallers need the viewer role; the runtime's own gate enforces it.",
|
||||
Fields: map[string]string{
|
||||
"O11yPromQueryIn.query": "Query is the PromQL expression to evaluate. Required.",
|
||||
"O11yPromQueryIn.stats": "Stats set to any non-empty value includes query statistics in the answer.",
|
||||
"O11yPromQueryIn.time": "Time is the evaluation timestamp — epoch seconds or RFC3339. Empty\nevaluates at now.",
|
||||
"O11yPromQueryIn.timeout": "Timeout caps evaluation time, as a duration in seconds.",
|
||||
"O11yPromQueryOut.data": "Data is the evaluation result.",
|
||||
"O11yPromQueryOut.status": "Status is \"success\".",
|
||||
"O11yPromResult.result": "Result is the result in PromQL's own wire shape for ResultType, carried\nverbatim.",
|
||||
"O11yPromResult.resultType": "ResultType discriminates Result: matrix, vector, scalar or string.",
|
||||
"O11yPromResult.stats": "Stats holds query statistics when the caller asked for them.",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/settings/apdex", zip.Doc{
|
||||
Description: "Returns apdex settings for the named services.\n\nCallers need the viewer role; the runtime's own gate enforces it.",
|
||||
Fields: map[string]string{
|
||||
"O11yApdexIn.services": "Services are the service names, comma separated.",
|
||||
"O11yApdexOut.data": "Data holds one settings row per service.",
|
||||
"O11yApdexOut.status": "Status is \"success\".",
|
||||
"O11yApdexSettings.excludeStatusCodes": "ExcludeStatusCodes are status codes excluded from the score, comma\nseparated.",
|
||||
"O11yApdexSettings.id": "ID is the settings row's id.",
|
||||
"O11yApdexSettings.orgId": "OrgID is the org the settings belong to.",
|
||||
"O11yApdexSettings.serviceName": "ServiceName is the service.",
|
||||
"O11yApdexSettings.threshold": "Threshold is the satisfied-response time in seconds.",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/settings/ttl", zip.Doc{
|
||||
Description: "Returns the org's current retention policy: default TTL, custom\nper-label rules, and cold-storage settings where configured.\n\nCallers need the viewer role; the runtime's own gate enforces it.",
|
||||
Fields: map[string]string{
|
||||
"O11yRetentionMatch.key": "Key is the label to test.",
|
||||
"O11yRetentionMatch.values": "Values are the label values the rule matches.",
|
||||
"O11yRetentionOut.cold_storage_ttl_days": "ColdStorageTTLDays is how old data must be before it moves, in days.",
|
||||
"O11yRetentionOut.cold_storage_volume": "ColdStorageVolume names the volume aged data moves to.",
|
||||
"O11yRetentionOut.default_ttl_days": "DefaultTTLDays is the retention for data no rule matches, in days.",
|
||||
"O11yRetentionOut.expected_logs_move_ttl_duration_hrs": "ExpectedLogsMoveTTLHours is the pending logs cold-storage move TTL, in\nhours.",
|
||||
"O11yRetentionOut.expected_logs_ttl_duration_hrs": "ExpectedLogsTTLHours is the pending logs TTL, in hours.",
|
||||
"O11yRetentionOut.status": "Status is the last TTL operation's state.",
|
||||
"O11yRetentionOut.ttl_conditions": "TTLConditions are the ordered per-label rules; the first match wins.",
|
||||
"O11yRetentionOut.version": "Version is the policy format version.",
|
||||
"O11yRetentionRule.conditions": "Conditions all have to hold for the rule to match.",
|
||||
"O11yRetentionRule.ttlDays": "TTLDays is the retention applied when it does, in days.",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/stats", zip.Doc{
|
||||
Description: "Returns the collected usage statistics for the caller's org, as the\nstats reporter aggregates them — a map whose keys are the reporter's own\ncounter names.\n\nCallers need the viewer role; the runtime's own gate enforces it.",
|
||||
Fields: map[string]string{
|
||||
"O11yOrgStatsOut.data": "Data are the statistics, keyed by the reporter's own counter names.",
|
||||
"O11yOrgStatsOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/usage", zip.Doc{
|
||||
Description: "Returns ingestion usage counts bucketed over the requested window,\noptionally narrowed to one service.\n\nCallers need the viewer role; the runtime's own gate enforces it.",
|
||||
Fields: map[string]string{
|
||||
"O11yUsageIn.end": "End is the window end, as epoch nanoseconds. Required.",
|
||||
"O11yUsageIn.service": "Service narrows usage to one service. Empty covers all.",
|
||||
"O11yUsageIn.start": "Start is the window start, as epoch nanoseconds. Required.",
|
||||
"O11yUsageIn.step": "Step is the bucket width in seconds. The runtime requires it.",
|
||||
"O11yUsageItem.count": "Count is how many spans were ingested in the bucket.",
|
||||
"O11yUsageItem.time": "Time is the bucket start.",
|
||||
"O11yUsageItem.timestamp": "Timestamp is the bucket start, as epoch nanoseconds.",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/version", zip.Doc{
|
||||
Description: "Reports the running build: its version, whether an enterprise\nedition is present (\"N\" in this build), and whether first-user setup has\ncompleted.\n\nOpen by design; the runtime's own gate is OpenAccess.",
|
||||
Fields: map[string]string{
|
||||
"O11yVersionOut.ee": "EE says whether an enterprise edition is present; \"N\" in this build.",
|
||||
"O11yVersionOut.setupCompleted": "SetupCompleted says whether the first user has been created.",
|
||||
"O11yVersionOut.version": "Version is the build version.",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/dependency_graph", zip.Doc{
|
||||
Description: "Returns the service dependency graph over the requested\nwindow: every parent→child edge observed, with call and error rates and\nlatency percentiles per edge.\n\nCallers need the viewer role; the runtime's own gate enforces it.",
|
||||
Fields: map[string]string{
|
||||
"O11yDependency.callCount": "CallCount is how many calls crossed the edge in the window.",
|
||||
"O11yDependency.callRate": "CallRate is calls per second.",
|
||||
"O11yDependency.child": "Child is the called service.",
|
||||
"O11yDependency.errorRate": "ErrorRate is the percentage of calls that erred.",
|
||||
"O11yDependency.p50": "P50 is the median call duration, in nanoseconds.",
|
||||
"O11yDependency.p75": "P75 is the 75th-percentile call duration, in nanoseconds.",
|
||||
"O11yDependency.p90": "P90 is the 90th-percentile call duration, in nanoseconds.",
|
||||
"O11yDependency.p95": "P95 is the 95th-percentile call duration, in nanoseconds.",
|
||||
"O11yDependency.p99": "P99 is the 99th-percentile call duration, in nanoseconds.",
|
||||
"O11yDependency.parent": "Parent is the calling service.",
|
||||
"O11yDependencyGraphIn.end": "End is the window end, as epoch nanoseconds. Required.",
|
||||
"O11yDependencyGraphIn.start": "Start is the window start, as epoch nanoseconds. Required.",
|
||||
"O11yDependencyGraphIn.tags": "Tags narrow the graph to spans matching every condition.",
|
||||
"O11yTagFilter.boolValues": "BoolValues are the values matched when the tag holds booleans.",
|
||||
"O11yTagFilter.key": "Key is the tag to test.",
|
||||
"O11yTagFilter.numberValues": "NumberValues are the values matched when the tag holds numbers.",
|
||||
"O11yTagFilter.operator": "Operator is the comparison to apply — in, not_in, equals, contains and\nthe other operators the trace filter grammar names.",
|
||||
"O11yTagFilter.stringValues": "StringValues are the values matched when the tag holds strings.",
|
||||
"O11yTagFilter.tagType": "TagType says which kind of value the tag holds: string, number or bool.",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/event", zip.Doc{
|
||||
Description: "Records one product-analytics event for the signed-in user — a track\nevent with a name and free-form attributes.\n\nCallers need the viewer role; the runtime's own gate enforces it.",
|
||||
Fields: map[string]string{
|
||||
"O11yEventIn.attributes": "Attributes are free-form event properties.",
|
||||
"O11yEventIn.eventName": "EventName names the event; required for track events.",
|
||||
"O11yEventIn.eventType": "EventType is the kind of event — track, identify or group. Required.",
|
||||
"O11yEventIn.rateLimited": "RateLimited marks an event the reporting client rate-limited.",
|
||||
"O11yMessage.data": "Data is the acknowledgment message.",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/query_filter/analyze", zip.Doc{
|
||||
Description: "Analyzes a query and extracts the metric names it reads\nand the columns it groups by.\n\nCallers need the viewer role; the runtime's own gate enforces it.",
|
||||
Fields: map[string]string{
|
||||
"O11yAnalyzeIn.query": "Query is the query text. Required.",
|
||||
"O11yAnalyzeIn.queryType": "QueryType says which language the query is in — promql or the datastore's\nSQL dialect. Required.",
|
||||
"O11yAnalyzeOut.data": "Data is the analysis.",
|
||||
"O11yAnalyzeOut.status": "Status is \"success\".",
|
||||
"O11yColumnInfo.columnAlias": "Alias is the column's alias in the query, when it has one.",
|
||||
"O11yColumnInfo.columnName": "Name is the column's name.",
|
||||
"O11yQueryFilterAnalysis.groups": "Groups are the columns the query groups by.",
|
||||
"O11yQueryFilterAnalysis.metricNames": "MetricNames are the metrics the query reads.",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/register", zip.Doc{
|
||||
Description: "Creates the FIRST organization and its admin user. It is open by\ndesign — there is nobody to be signed in as yet — and refuses once setup has\ncompleted, after which new users arrive by invitation only.\n\nOpen by design; the runtime's own gate is OpenAccess.",
|
||||
Fields: map[string]string{
|
||||
"O11yRegisterIn.email": "Email is the admin's email. Required.",
|
||||
"O11yRegisterIn.name": "Name is the admin's display name.",
|
||||
"O11yRegisterIn.orgDisplayName": "OrgDisplayName is the organization's display name.",
|
||||
"O11yRegisterIn.orgName": "OrgName is the organization's name.",
|
||||
"O11yRegisterIn.password": "Password is the admin's password.",
|
||||
"O11yRegisterOut.data": "Data is the user. The runtime answers register with the same user shape\nthe identity face reads, so it is the ONE O11yUser (identity.go) — a\ncreated user is a user, and the document names it once.",
|
||||
"O11yRegisterOut.status": "Status is \"success\".",
|
||||
"O11yUser.createdAt": "CreatedAt is when they joined.",
|
||||
"O11yUser.displayName": "DisplayName is what the console shows for them.",
|
||||
"O11yUser.email": "Email is their address.",
|
||||
"O11yUser.id": "ID is the user id.",
|
||||
"O11yUser.isRoot": "IsRoot marks the org's root user, which cannot be deleted or demoted.",
|
||||
"O11yUser.orgId": "OrgID is the org they belong to.",
|
||||
"O11yUser.status": "Status is their lifecycle state — active, pending_invite or deleted.",
|
||||
"O11yUser.updatedAt": "UpdatedAt is when their record last changed.",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/settings/apdex", zip.Doc{
|
||||
Description: "Sets one service's apdex threshold and the status codes excluded\nfrom its score.\n\nAdmin only, as the mux tree has always gated it (AdminAccess); the runtime's\nown gate enforces it.",
|
||||
Fields: map[string]string{
|
||||
"O11yApdexSetIn.excludeStatusCodes": "ExcludeStatusCodes are status codes excluded from the score, comma\nseparated.",
|
||||
"O11yApdexSetIn.serviceName": "ServiceName is the service the threshold applies to.",
|
||||
"O11yApdexSetIn.threshold": "Threshold is the satisfied-response time in seconds.",
|
||||
"O11yApdexSetOut.data": "Data is the acknowledgment.",
|
||||
"O11yApdexSetOut.status": "Status is \"success\".",
|
||||
"O11yMessage.data": "Data is the acknowledgment message.",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/settings/ttl", zip.Doc{
|
||||
Description: "Sets the org's retention policy for one signal: the default TTL\nin days, ordered per-label retention rules, and optional cold-storage\nsettings.\n\nAdmin only, as the mux tree has always gated it (AdminAccess); the runtime's\nown gate enforces it.",
|
||||
Fields: map[string]string{
|
||||
"O11yRetentionMatch.key": "Key is the label to test.",
|
||||
"O11yRetentionMatch.values": "Values are the label values the rule matches.",
|
||||
"O11yRetentionRule.conditions": "Conditions all have to hold for the rule to match.",
|
||||
"O11yRetentionRule.ttlDays": "TTLDays is the retention applied when it does, in days.",
|
||||
"O11yRetentionSetIn.coldStorageDurationDays": "ColdStorageDurationDays is how old data must be before it moves, in days.",
|
||||
"O11yRetentionSetIn.coldStorageVolume": "ColdStorageVolume names the volume aged data moves to, when set.",
|
||||
"O11yRetentionSetIn.defaultTTLDays": "DefaultTTLDays is the retention for data no rule matches, in days.",
|
||||
"O11yRetentionSetIn.ttlConditions": "TTLConditions are ordered per-label rules; the first matching rule wins.",
|
||||
"O11yRetentionSetIn.type": "Type is the signal the policy applies to — traces, metrics or logs.",
|
||||
"O11yRetentionSetOut.message": "Message says what was done.",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/span_percentile", zip.Doc{
|
||||
Description: "Places one span's duration among its peers: the p50/p90/p99\ndurations of like spans, and the percentile the given duration lands at.\n\nCallers need the viewer role; the runtime's own gate enforces it.",
|
||||
Fields: map[string]string{
|
||||
"O11yPercentilePosition.description": "Description says the same in words.",
|
||||
"O11yPercentilePosition.percentile": "Percentile is the percentile the duration lands at.",
|
||||
"O11yPercentiles.p50": "P50 is the median.",
|
||||
"O11yPercentiles.p90": "P90 is the 90th percentile.",
|
||||
"O11yPercentiles.p99": "P99 is the 99th percentile.",
|
||||
"O11ySpanPercentile.percentiles": "Percentiles are the peer group's duration percentiles.",
|
||||
"O11ySpanPercentile.position": "Position is where the given duration lands.",
|
||||
"O11ySpanPercentileIn.end": "End is the window end, as epoch nanoseconds.",
|
||||
"O11ySpanPercentileIn.name": "Name is the span name whose peers are compared. Required.",
|
||||
"O11ySpanPercentileIn.resourceAttributes": "ResourceAttributes narrow the peer group to spans carrying them all.",
|
||||
"O11ySpanPercentileIn.serviceName": "ServiceName is the service the span belongs to. Required.",
|
||||
"O11ySpanPercentileIn.spanDuration": "SpanDuration is the span's duration in nanoseconds.",
|
||||
"O11ySpanPercentileIn.start": "Start is the window start, as epoch nanoseconds.",
|
||||
"O11ySpanPercentileOut.data": "Data is the placement.",
|
||||
"O11ySpanPercentileOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/variables/query", zip.Doc{
|
||||
Description: "Evaluates a dashboard variable query and returns the values the\nvariable may take.\n\nCallers need the viewer role; the runtime's own gate enforces it.",
|
||||
Fields: map[string]string{
|
||||
"O11yDashboardVarValues.variableValues": "VariableValues are the values, in the order the query produced them.",
|
||||
"O11yDashboardVarsIn.query": "Query is the variable query to evaluate. Required.",
|
||||
"O11yDashboardVarsIn.variables": "Variables are the current values of the other dashboard variables, for\nqueries that reference them.",
|
||||
"O11yDashboardVarsOut.data": "Data holds the values.",
|
||||
"O11yDashboardVarsOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
}
|
||||
+16
-17
@@ -98,7 +98,7 @@ func mountQueryCore(app *zip.App) {
|
||||
// Callers need the viewer role; the runtime's own gate enforces it.
|
||||
func querierQueryRange(ctx context.Context, in *qbtypes.QueryRangeRequest) (*O11yQueryRangeOut, error) {
|
||||
out := new(O11yQueryRangeOut)
|
||||
return out, relayAt(ctx, http.MethodPost, "/query_range", nil, in, out)
|
||||
return out, relay(ctx, http.MethodPost, o11yRoot+"/query_range", nil, in, out)
|
||||
}
|
||||
|
||||
// querierQueryRangePreview validates a composite query and renders the Datastore
|
||||
@@ -111,8 +111,7 @@ func querierQueryRangePreview(ctx context.Context, in *O11yQueryRangePreviewIn)
|
||||
out := new(O11yQueryRangePreviewOut)
|
||||
// verbose rides the query string, the request rides the body — the same
|
||||
// split the runtime reads.
|
||||
return out, relayAt(ctx, http.MethodPost, "/query_range/preview",
|
||||
query("verbose", in.Verbose), &in.QueryRangeRequest, out)
|
||||
return out, relay(ctx, http.MethodPost, o11yRoot+"/query_range/preview", query("verbose", in.Verbose), &in.QueryRangeRequest, out)
|
||||
}
|
||||
|
||||
// querierReplaceVariables substitutes a query's variables and returns the
|
||||
@@ -122,7 +121,7 @@ func querierQueryRangePreview(ctx context.Context, in *O11yQueryRangePreviewIn)
|
||||
// Callers need the viewer role; the runtime's own gate enforces it.
|
||||
func querierReplaceVariables(ctx context.Context, in *qbtypes.QueryRangeRequest) (*O11ySubstituteVarsOut, error) {
|
||||
out := new(O11ySubstituteVarsOut)
|
||||
return out, relayAt(ctx, http.MethodPost, "/substitute_vars", nil, in, out)
|
||||
return out, relay(ctx, http.MethodPost, o11yRoot+"/substitute_vars", nil, in, out)
|
||||
}
|
||||
|
||||
// metricsQueryRange runs a Prometheus-style range query over metrics — the
|
||||
@@ -132,7 +131,7 @@ func querierReplaceVariables(ctx context.Context, in *qbtypes.QueryRangeRequest)
|
||||
// Callers need the viewer role; the runtime's own gate enforces it.
|
||||
func metricsQueryRange(ctx context.Context, in *O11yMetricsQueryRangeIn) (*O11yMetricsQueryRangeOut, error) {
|
||||
out := new(O11yMetricsQueryRangeOut)
|
||||
return out, relayAt(ctx, http.MethodGet, "/query_range", query(
|
||||
return out, relay(ctx, http.MethodGet, o11yRoot+"/query_range", query(
|
||||
"start", in.Start,
|
||||
"end", in.End,
|
||||
"step", in.Step,
|
||||
@@ -149,7 +148,7 @@ func metricsQueryRange(ctx context.Context, in *O11yMetricsQueryRangeIn) (*O11yM
|
||||
// Callers need the viewer role; the runtime's own gate enforces it.
|
||||
func queryRangeFormat(ctx context.Context, in *v3.QueryRangeParamsV3) (*O11yQueryRangeFormatOut, error) {
|
||||
out := new(O11yQueryRangeFormatOut)
|
||||
return out, relayAt(ctx, http.MethodPost, "/query_range/format", nil, in, out)
|
||||
return out, relay(ctx, http.MethodPost, o11yRoot+"/query_range/format", nil, in, out)
|
||||
}
|
||||
|
||||
// ── attribute autocomplete ─────────────────────────────────────────────────────
|
||||
@@ -161,7 +160,7 @@ func queryRangeFormat(ctx context.Context, in *v3.QueryRangeParamsV3) (*O11yQuer
|
||||
// Callers need the viewer role; the runtime's own gate enforces it.
|
||||
func autocompleteAggregate(ctx context.Context, in *O11yAggregateAttributesIn) (*O11yAggregateAttributesOut, error) {
|
||||
out := new(O11yAggregateAttributesOut)
|
||||
return out, relayAt(ctx, http.MethodGet, "/autocomplete/aggregate_attributes", query(
|
||||
return out, relay(ctx, http.MethodGet, o11yRoot+"/autocomplete/aggregate_attributes", query(
|
||||
"aggregateOperator", in.AggregateOperator,
|
||||
"dataSource", in.DataSource,
|
||||
"searchText", in.SearchText,
|
||||
@@ -175,7 +174,7 @@ func autocompleteAggregate(ctx context.Context, in *O11yAggregateAttributesIn) (
|
||||
// Callers need the viewer role; the runtime's own gate enforces it.
|
||||
func autocompleteKeys(ctx context.Context, in *O11yAttributeKeysIn) (*O11yAttributeKeysOut, error) {
|
||||
out := new(O11yAttributeKeysOut)
|
||||
return out, relayAt(ctx, http.MethodGet, "/autocomplete/attribute_keys", query(
|
||||
return out, relay(ctx, http.MethodGet, o11yRoot+"/autocomplete/attribute_keys", query(
|
||||
"dataSource", in.DataSource,
|
||||
"aggregateOperator", in.AggregateOperator,
|
||||
"aggregateAttribute", in.AggregateAttribute,
|
||||
@@ -191,7 +190,7 @@ func autocompleteKeys(ctx context.Context, in *O11yAttributeKeysIn) (*O11yAttrib
|
||||
// Callers need the viewer role; the runtime's own gate enforces it.
|
||||
func autocompleteValues(ctx context.Context, in *O11yAttributeValuesIn) (*O11yAttributeValuesOut, error) {
|
||||
out := new(O11yAttributeValuesOut)
|
||||
return out, relayAt(ctx, http.MethodGet, "/autocomplete/attribute_values", query(
|
||||
return out, relay(ctx, http.MethodGet, o11yRoot+"/autocomplete/attribute_values", query(
|
||||
"dataSource", in.DataSource,
|
||||
"aggregateOperator", in.AggregateOperator,
|
||||
"aggregateAttribute", in.AggregateAttribute,
|
||||
@@ -210,7 +209,7 @@ func autocompleteValues(ctx context.Context, in *O11yAttributeValuesIn) (*O11yAt
|
||||
// Callers need the viewer role; the runtime's own gate enforces it.
|
||||
func autocompleteValuesPost(ctx context.Context, in *v3.FilterAttributeValueRequest) (*O11yAttributeValuesOut, error) {
|
||||
out := new(O11yAttributeValuesOut)
|
||||
return out, relayAt(ctx, http.MethodPost, "/auto_complete/attribute_values", nil, in, out)
|
||||
return out, relay(ctx, http.MethodPost, o11yRoot+"/auto_complete/attribute_values", nil, in, out)
|
||||
}
|
||||
|
||||
// ── the field catalog ──────────────────────────────────────────────────────────
|
||||
@@ -221,7 +220,7 @@ func autocompleteValuesPost(ctx context.Context, in *v3.FilterAttributeValueRequ
|
||||
// Callers need the viewer role; the runtime's own gate enforces it.
|
||||
func fieldKeys(ctx context.Context, in *O11yFieldKeysIn) (*O11yFieldKeysOut, error) {
|
||||
out := new(O11yFieldKeysOut)
|
||||
return out, relayAt(ctx, http.MethodGet, "/fields/keys", query(
|
||||
return out, relay(ctx, http.MethodGet, o11yRoot+"/fields/keys", query(
|
||||
"signal", in.Signal,
|
||||
"source", in.Source,
|
||||
"limit", in.Limit,
|
||||
@@ -241,7 +240,7 @@ func fieldKeys(ctx context.Context, in *O11yFieldKeysIn) (*O11yFieldKeysOut, err
|
||||
// Callers need the viewer role; the runtime's own gate enforces it.
|
||||
func fieldValues(ctx context.Context, in *O11yFieldValuesIn) (*O11yFieldValuesOut, error) {
|
||||
out := new(O11yFieldValuesOut)
|
||||
return out, relayAt(ctx, http.MethodGet, "/fields/values", query(
|
||||
return out, relay(ctx, http.MethodGet, o11yRoot+"/fields/values", query(
|
||||
"signal", in.Signal,
|
||||
"source", in.Source,
|
||||
"limit", in.Limit,
|
||||
@@ -265,7 +264,7 @@ func fieldValues(ctx context.Context, in *O11yFieldValuesIn) (*O11yFieldValuesOu
|
||||
// Callers need the viewer role; the runtime's own gate enforces it.
|
||||
func savedViewList(ctx context.Context, in *O11ySavedViewListIn) (*O11ySavedViewListOut, error) {
|
||||
out := new(O11ySavedViewListOut)
|
||||
return out, relayAt(ctx, http.MethodGet, "/explorer/views", query(
|
||||
return out, relay(ctx, http.MethodGet, o11yRoot+"/explorer/views", query(
|
||||
"sourcePage", in.SourcePage,
|
||||
"name", in.Name,
|
||||
"category", in.Category,
|
||||
@@ -278,7 +277,7 @@ func savedViewList(ctx context.Context, in *O11ySavedViewListIn) (*O11ySavedView
|
||||
// Callers need the editor role; the runtime's own gate enforces it.
|
||||
func savedViewCreate(ctx context.Context, in *v3.SavedView) (*O11ySavedViewCreateOut, error) {
|
||||
out := new(O11ySavedViewCreateOut)
|
||||
return out, relayAt(ctx, http.MethodPost, "/explorer/views", nil, in, out)
|
||||
return out, relay(ctx, http.MethodPost, o11yRoot+"/explorer/views", nil, in, out)
|
||||
}
|
||||
|
||||
// savedViewGet returns one saved explorer view by id.
|
||||
@@ -286,7 +285,7 @@ func savedViewCreate(ctx context.Context, in *v3.SavedView) (*O11ySavedViewCreat
|
||||
// Callers need the viewer role; the runtime's own gate enforces it.
|
||||
func savedViewGet(ctx context.Context, in *O11ySavedViewRef) (*O11ySavedViewOut, error) {
|
||||
out := new(O11ySavedViewOut)
|
||||
return out, relayAt(ctx, http.MethodGet, "/explorer/views/"+in.ViewID, nil, nil, out)
|
||||
return out, relay(ctx, http.MethodGet, o11yRoot+"/explorer/views/"+in.ViewID, nil, nil, out)
|
||||
}
|
||||
|
||||
// savedViewUpdate replaces one saved explorer view by id with the given view and
|
||||
@@ -297,7 +296,7 @@ func savedViewUpdate(ctx context.Context, in *O11ySavedViewUpdateIn) (*O11ySaved
|
||||
out := new(O11ySavedViewOut)
|
||||
// The view id addresses the target from the URL; the body carries the new
|
||||
// view, relayed without the id so the URL stays the one authority.
|
||||
return out, relayAt(ctx, http.MethodPut, "/explorer/views/"+in.ViewID, nil, &in.SavedView, out)
|
||||
return out, relay(ctx, http.MethodPut, o11yRoot+"/explorer/views/"+in.ViewID, nil, &in.SavedView, out)
|
||||
}
|
||||
|
||||
// savedViewDelete deletes one saved explorer view by id.
|
||||
@@ -305,7 +304,7 @@ func savedViewUpdate(ctx context.Context, in *O11ySavedViewUpdateIn) (*O11ySaved
|
||||
// Callers need the editor role; the runtime's own gate enforces it.
|
||||
func savedViewDelete(ctx context.Context, in *O11ySavedViewRef) (*O11ySavedViewDeleteOut, error) {
|
||||
out := new(O11ySavedViewDeleteOut)
|
||||
return out, relayAt(ctx, http.MethodDelete, "/explorer/views/"+in.ViewID, nil, nil, out)
|
||||
return out, relay(ctx, http.MethodDelete, o11yRoot+"/explorer/views/"+in.ViewID, nil, nil, out)
|
||||
}
|
||||
|
||||
// ── inputs ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -1,237 +0,0 @@
|
||||
// Code generated by zipdoc; DO NOT EDIT.
|
||||
//
|
||||
// The query-core face's slice of the package extraction, split into its own
|
||||
// file so concurrent slices do not contend on zipdoc_gen.go; a later `go
|
||||
// generate -run zipdoc ./` folds every slice back into zipdoc_gen.go, at which
|
||||
// point this file is deleted.
|
||||
|
||||
package o11y
|
||||
|
||||
import (
|
||||
"github.com/zap-proto/zip"
|
||||
)
|
||||
|
||||
func init() {
|
||||
zip.Describe("DELETE /v1/o11y/explorer/views/:viewId", zip.Doc{
|
||||
Description: "Deletes one saved explorer view by id.\n\nCallers need the editor role; the runtime's own gate enforces it.",
|
||||
Fields: map[string]string{
|
||||
"O11ySavedViewDeleteOut.status": "Status is \"success\".",
|
||||
"O11ySavedViewRef.viewId": "ViewID is the view's id.",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/autocomplete/aggregate_attributes", zip.Doc{
|
||||
Description: "Lists the attributes usable as an aggregate target for\nthe given telemetry and operator — what a filter builder offers after the\naggregation is chosen.\n\nCallers need the viewer role; the runtime's own gate enforces it.",
|
||||
Fields: map[string]string{
|
||||
"O11yAggregateAttributesIn.aggregateOperator": "AggregateOperator is the aggregation the attribute will be used under, e.g.\ncount, avg, sum. The runtime requires it for non-metrics sources.",
|
||||
"O11yAggregateAttributesIn.dataSource": "DataSource is the telemetry the attributes come from — traces, logs,\nmetrics or meter. The runtime requires it.",
|
||||
"O11yAggregateAttributesIn.limit": "Limit caps how many attributes come back. Absent means 50.",
|
||||
"O11yAggregateAttributesIn.searchText": "SearchText narrows the attributes to those containing it.",
|
||||
"O11yAggregateAttributesOut.data": "Data holds the attribute keys.",
|
||||
"O11yAggregateAttributesOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/autocomplete/attribute_keys", zip.Doc{
|
||||
Description: "Lists the attribute keys available for filtering the given\ntelemetry, each with its data type and whether it is a materialized column.\n\nCallers need the viewer role; the runtime's own gate enforces it.",
|
||||
Fields: map[string]string{
|
||||
"O11yAttributeKeysIn.aggregateAttribute": "AggregateAttribute is the metric the keys must appear on.",
|
||||
"O11yAttributeKeysIn.aggregateOperator": "AggregateOperator is the aggregation the keys will be used under. The\nruntime requires it for non-metrics sources.",
|
||||
"O11yAttributeKeysIn.dataSource": "DataSource is the telemetry the keys come from — traces, logs, metrics or\nmeter. The runtime requires it.",
|
||||
"O11yAttributeKeysIn.limit": "Limit caps how many keys come back. Absent means 50.",
|
||||
"O11yAttributeKeysIn.searchText": "SearchText narrows the keys to those containing it.",
|
||||
"O11yAttributeKeysIn.tagType": "TagType narrows the keys to one kind — tag or resource. Empty means all;\nan invalid value reads as empty.",
|
||||
"O11yAttributeKeysOut.data": "Data holds the attribute keys.",
|
||||
"O11yAttributeKeysOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/autocomplete/attribute_values", zip.Doc{
|
||||
Description: "Lists the values one attribute key has taken — string,\nnumber and bool values in their own lists — for completing a filter.\n\nCallers need the viewer role; the runtime's own gate enforces it.",
|
||||
Fields: map[string]string{
|
||||
"O11yAttributeValuesIn.aggregateAttribute": "AggregateAttribute is the metric the values must appear on.",
|
||||
"O11yAttributeValuesIn.aggregateOperator": "AggregateOperator is the aggregation the values will be used under. The\nruntime requires it for non-metrics sources.",
|
||||
"O11yAttributeValuesIn.attributeKey": "AttributeKey is the key whose values to list.",
|
||||
"O11yAttributeValuesIn.dataSource": "DataSource is the telemetry the values come from — traces, logs or\nmetrics. The runtime requires it.",
|
||||
"O11yAttributeValuesIn.filterAttributeKeyDataType": "FilterAttributeKeyDataType is the key's data type — string, int64, float64\nor bool. Empty means unspecified.",
|
||||
"O11yAttributeValuesIn.limit": "Limit caps how many values come back. Absent means 50.",
|
||||
"O11yAttributeValuesIn.searchText": "SearchText narrows the values to those containing it.",
|
||||
"O11yAttributeValuesIn.tagType": "TagType narrows the search to one kind of key — tag or resource.",
|
||||
"O11yAttributeValuesOut.data": "Data holds the values, split by data type.",
|
||||
"O11yAttributeValuesOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/explorer/views", zip.Doc{
|
||||
Description: "Lists the caller's org's saved explorer views, optionally\nnarrowed to one source page, name or category.\n\nCallers need the viewer role; the runtime's own gate enforces it.",
|
||||
Fields: map[string]string{
|
||||
"CompositeQuery.fillGaps": "FillGaps is used to fill the gaps in the time series data",
|
||||
"CompositeQuery.unit": "Unit for the time series data shown in the graph\nThis is used in alerts to format the value and threshold",
|
||||
"O11ySavedViewListIn.category": "Category narrows the views to one category.",
|
||||
"O11ySavedViewListIn.name": "Name narrows the views to one name.",
|
||||
"O11ySavedViewListIn.sourcePage": "SourcePage narrows the views to one source page, e.g. traces, logs.",
|
||||
"O11ySavedViewListOut.data": "Data holds the views.",
|
||||
"O11ySavedViewListOut.status": "Status is \"success\".",
|
||||
"QueryEnvelope.spec": "Spec is the deferred decoding of the query if any.",
|
||||
"QueryEnvelope.type": "Type is the type of the query.",
|
||||
"SavedView.extraData": "ExtraData is JSON encoded data used by frontend to store additional data",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/explorer/views/:viewId", zip.Doc{
|
||||
Description: "Returns one saved explorer view by id.\n\nCallers need the viewer role; the runtime's own gate enforces it.",
|
||||
Fields: map[string]string{
|
||||
"CompositeQuery.fillGaps": "FillGaps is used to fill the gaps in the time series data",
|
||||
"CompositeQuery.unit": "Unit for the time series data shown in the graph\nThis is used in alerts to format the value and threshold",
|
||||
"O11ySavedViewOut.data": "Data holds the view.",
|
||||
"O11ySavedViewOut.status": "Status is \"success\".",
|
||||
"O11ySavedViewRef.viewId": "ViewID is the view's id.",
|
||||
"QueryEnvelope.spec": "Spec is the deferred decoding of the query if any.",
|
||||
"QueryEnvelope.type": "Type is the type of the query.",
|
||||
"SavedView.extraData": "ExtraData is JSON encoded data used by frontend to store additional data",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/fields/keys", zip.Doc{
|
||||
Description: "Returns the telemetry field keys matching the selector — the\nsignal's fields grouped by name, and whether the catalog is complete.\n\nCallers need the viewer role; the runtime's own gate enforces it.",
|
||||
Fields: map[string]string{
|
||||
"O11yFieldKeysIn.endUnixMilli": "EndUnixMilli is the window end as a unix millisecond epoch. Zero reads as\nunset.",
|
||||
"O11yFieldKeysIn.fieldContext": "FieldContext narrows the keys to one context — resource, scope, attribute,\nspan, log or metric.",
|
||||
"O11yFieldKeysIn.fieldDataType": "FieldDataType narrows the keys to one data type.",
|
||||
"O11yFieldKeysIn.limit": "Limit caps how many keys come back.",
|
||||
"O11yFieldKeysIn.metricName": "MetricName narrows the keys to those on one metric.",
|
||||
"O11yFieldKeysIn.metricNamespace": "MetricNamespace narrows the keys to one metric namespace.",
|
||||
"O11yFieldKeysIn.searchText": "SearchText narrows the keys to those containing it.",
|
||||
"O11yFieldKeysIn.signal": "Signal is the telemetry to read the fields of — traces, logs or metrics.",
|
||||
"O11yFieldKeysIn.source": "Source narrows the fields to one source within the signal.",
|
||||
"O11yFieldKeysIn.startUnixMilli": "StartUnixMilli is the window start as a unix millisecond epoch. Zero reads\nas unset.",
|
||||
"O11yFieldKeysOut.data": "Data holds the field keys grouped by name, and whether the catalog is\ncomplete.",
|
||||
"O11yFieldKeysOut.status": "Status is \"success\".",
|
||||
"TelemetryFieldKeySkipIndex.name": "Name is TelemetryFieldKey.Name not IndexName from Datastore",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/fields/values", zip.Doc{
|
||||
Description: "Returns the values one telemetry field has taken — string, bool,\nnumber and related values — and whether the value list is complete.\n\nCallers need the viewer role; the runtime's own gate enforces it.",
|
||||
Fields: map[string]string{
|
||||
"O11yFieldValuesIn.endUnixMilli": "EndUnixMilli is the window end as a unix millisecond epoch. Zero reads as\nunset.",
|
||||
"O11yFieldValuesIn.existingQuery": "ExistingQuery is the query the field appears in, so related values can be\nsuggested for it.",
|
||||
"O11yFieldValuesIn.fieldContext": "FieldContext narrows the field to one context.",
|
||||
"O11yFieldValuesIn.fieldDataType": "FieldDataType narrows the field to one data type.",
|
||||
"O11yFieldValuesIn.limit": "Limit caps how many values come back.",
|
||||
"O11yFieldValuesIn.metricName": "MetricName narrows the field to one metric.",
|
||||
"O11yFieldValuesIn.metricNamespace": "MetricNamespace narrows the field to one metric namespace.",
|
||||
"O11yFieldValuesIn.name": "Name is the field whose values to read.",
|
||||
"O11yFieldValuesIn.searchText": "SearchText narrows the values to those containing it.",
|
||||
"O11yFieldValuesIn.signal": "Signal is the telemetry to read the field of — traces, logs or metrics.",
|
||||
"O11yFieldValuesIn.source": "Source narrows the field to one source within the signal.",
|
||||
"O11yFieldValuesIn.startUnixMilli": "StartUnixMilli is the window start as a unix millisecond epoch. Zero reads\nas unset.",
|
||||
"O11yFieldValuesOut.data": "Data holds the values by data type, and whether the value list is complete.",
|
||||
"O11yFieldValuesOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/query_range", zip.Doc{
|
||||
Description: "Runs a Prometheus-style range query over metrics — the\nlegacy read that predates the v5 querier — and returns the matrix, vector or\nscalar the query resolved to.\n\nCallers need the viewer role; the runtime's own gate enforces it.",
|
||||
Fields: map[string]string{
|
||||
"O11yMetricsQueryRangeIn.end": "End is the window end, in the same form as Start, and not before it.\nRequired.",
|
||||
"O11yMetricsQueryRangeIn.query": "Query is the PromQL expression to evaluate. Required.",
|
||||
"O11yMetricsQueryRangeIn.start": "Start is the window start — a unix timestamp (seconds, with optional\nfraction) or an RFC 3339 time. Required.",
|
||||
"O11yMetricsQueryRangeIn.stats": "Stats, when \"all\", asks for query statistics alongside the result.",
|
||||
"O11yMetricsQueryRangeIn.step": "Step is the query resolution, e.g. 60s, 1m, 1h — a positive duration.\nRequired.",
|
||||
"O11yMetricsQueryRangeIn.timeout": "Timeout caps how long the query may run, e.g. 30s, 1m — a positive\nduration. Absent means the server default.",
|
||||
"O11yMetricsQueryRangeOut.data": "Data is the result — its result type, the value, and query stats when\nasked.",
|
||||
"O11yMetricsQueryRangeOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/auto_complete/attribute_values", zip.Doc{
|
||||
Description: "Reads the attribute-value request from the body rather\nthan off the query string — the spelling the newer builder uses to send its\nfilters alongside the request.\n\nCallers need the viewer role; the runtime's own gate enforces it.",
|
||||
Fields: map[string]string{
|
||||
"O11yAttributeValuesOut.data": "Data holds the values, split by data type.",
|
||||
"O11yAttributeValuesOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/explorer/views", zip.Doc{
|
||||
Description: "Saves a new explorer view for the caller's org and returns its\nid.\n\nCallers need the editor role; the runtime's own gate enforces it.",
|
||||
Fields: map[string]string{
|
||||
"CompositeQuery.fillGaps": "FillGaps is used to fill the gaps in the time series data",
|
||||
"CompositeQuery.unit": "Unit for the time series data shown in the graph\nThis is used in alerts to format the value and threshold",
|
||||
"O11ySavedViewCreateOut.data": "Data is the new view's id.",
|
||||
"O11ySavedViewCreateOut.status": "Status is \"success\".",
|
||||
"QueryEnvelope.spec": "Spec is the deferred decoding of the query if any.",
|
||||
"QueryEnvelope.type": "Type is the type of the query.",
|
||||
"SavedView.extraData": "ExtraData is JSON encoded data used by frontend to store additional data",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/query_range", zip.Doc{
|
||||
Description: "Executes a composite query over a time range: builder\nqueries over traces, logs and metrics, formulas, trace operators, PromQL and\nDatastore SQL, answering time series, scalars or raw records as the request\ntype asks.\n\nCallers need the viewer role; the runtime's own gate enforces it.",
|
||||
Fields: map[string]string{
|
||||
"CompositeQuery.queries": "Queries is the queries to use for the request.",
|
||||
"O11yQueryRangeOut.data": "Data is the query result — the result type, the result set, execution stats\nand any warning — as the engine rendered it.",
|
||||
"O11yQueryRangeOut.status": "Status is \"success\".",
|
||||
"QueryEnvelope.spec": "Spec is the deferred decoding of the query if any.",
|
||||
"QueryEnvelope.type": "Type is the type of the query.",
|
||||
"QueryRangeRequest.compositeQuery": "CompositeQuery is the composite query to use for the request.",
|
||||
"QueryRangeRequest.end": "End is the end time of the query in epoch milliseconds.",
|
||||
"QueryRangeRequest.noCache": "NoCache is a flag to disable caching for the request.",
|
||||
"QueryRangeRequest.requestType": "RequestType is the type of the request.",
|
||||
"QueryRangeRequest.schemaVersion": "SchemaVersion is the version of the schema to use for the request payload.",
|
||||
"QueryRangeRequest.start": "Start is the start time of the query in epoch milliseconds.",
|
||||
"QueryRangeRequest.variables": "Variables is the variables to use for the request.",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/query_range/format", zip.Doc{
|
||||
Description: "Parses a builder query and echoes it back normalized to the\nv3 shape — the endpoint the UI uses to canonicalize a query without running\nit.\n\nCallers need the viewer role; the runtime's own gate enforces it.",
|
||||
Fields: map[string]string{
|
||||
"CompositeQuery.fillGaps": "FillGaps is used to fill the gaps in the time series data",
|
||||
"CompositeQuery.unit": "Unit for the time series data shown in the graph\nThis is used in alerts to format the value and threshold",
|
||||
"O11yQueryRangeFormatOut.data": "Data is the query, normalized to the v3 shape.",
|
||||
"O11yQueryRangeFormatOut.status": "Status is \"success\".",
|
||||
"QueryEnvelope.spec": "Spec is the deferred decoding of the query if any.",
|
||||
"QueryEnvelope.type": "Type is the type of the query.",
|
||||
"QueryRangeParamsV3.step": "step is in seconds; used for prometheus queries",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/query_range/preview", zip.Doc{
|
||||
Description: "Validates a composite query and renders the Datastore\nstatements it would run WITHOUT executing it — a dry run for agentic and\ntooling use. verbose=false trades the rendered SQL and EXPLAIN for a\nlightweight per-query verdict.\n\nCallers need the viewer role; the runtime's own gate enforces it.",
|
||||
Fields: map[string]string{
|
||||
"CompositeQuery.queries": "Queries is the queries to use for the request.",
|
||||
"O11yQueryRangePreviewIn.QueryRangeRequest": "QueryRangeRequest is the composite query to render, embedded so the request\nbody stays the bare query the execute endpoint takes.",
|
||||
"O11yQueryRangePreviewIn.verbose": "Verbose selects the answer's depth. Empty or \"true\" renders the underlying\nDatastore SQL with EXPLAIN and granule analysis; \"false\" returns only the\nper-query valid/error/warnings verdict with no Datastore round trips.",
|
||||
"O11yQueryRangePreviewOut.data": "Data holds one preview per query, keyed by query name — the rendered\nstatements with optional EXPLAIN and granule analysis, or the per-query\nverdict.",
|
||||
"O11yQueryRangePreviewOut.status": "Status is \"success\".",
|
||||
"QueryEnvelope.spec": "Spec is the deferred decoding of the query if any.",
|
||||
"QueryEnvelope.type": "Type is the type of the query.",
|
||||
"QueryRangeRequest.compositeQuery": "CompositeQuery is the composite query to use for the request.",
|
||||
"QueryRangeRequest.end": "End is the end time of the query in epoch milliseconds.",
|
||||
"QueryRangeRequest.noCache": "NoCache is a flag to disable caching for the request.",
|
||||
"QueryRangeRequest.requestType": "RequestType is the type of the request.",
|
||||
"QueryRangeRequest.schemaVersion": "SchemaVersion is the version of the schema to use for the request payload.",
|
||||
"QueryRangeRequest.start": "Start is the start time of the query in epoch milliseconds.",
|
||||
"QueryRangeRequest.variables": "Variables is the variables to use for the request.",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/substitute_vars", zip.Doc{
|
||||
Description: "Substitutes a query's variables and returns the\nresolved request, without running it — what a dashboard does before it\nqueries.\n\nCallers need the viewer role; the runtime's own gate enforces it.",
|
||||
Fields: map[string]string{
|
||||
"CompositeQuery.queries": "Queries is the queries to use for the request.",
|
||||
"O11ySubstituteVarsOut.data": "Data is the resolved composite query, with its variables substituted.",
|
||||
"O11ySubstituteVarsOut.status": "Status is \"success\".",
|
||||
"QueryEnvelope.spec": "Spec is the deferred decoding of the query if any.",
|
||||
"QueryEnvelope.type": "Type is the type of the query.",
|
||||
"QueryRangeRequest.compositeQuery": "CompositeQuery is the composite query to use for the request.",
|
||||
"QueryRangeRequest.end": "End is the end time of the query in epoch milliseconds.",
|
||||
"QueryRangeRequest.noCache": "NoCache is a flag to disable caching for the request.",
|
||||
"QueryRangeRequest.requestType": "RequestType is the type of the request.",
|
||||
"QueryRangeRequest.schemaVersion": "SchemaVersion is the version of the schema to use for the request payload.",
|
||||
"QueryRangeRequest.start": "Start is the start time of the query in epoch milliseconds.",
|
||||
"QueryRangeRequest.variables": "Variables is the variables to use for the request.",
|
||||
},
|
||||
})
|
||||
zip.Describe("PUT /v1/o11y/explorer/views/:viewId", zip.Doc{
|
||||
Description: "Replaces one saved explorer view by id with the given view and\nechoes it back.\n\nCallers need the editor role; the runtime's own gate enforces it.",
|
||||
Fields: map[string]string{
|
||||
"CompositeQuery.fillGaps": "FillGaps is used to fill the gaps in the time series data",
|
||||
"CompositeQuery.unit": "Unit for the time series data shown in the graph\nThis is used in alerts to format the value and threshold",
|
||||
"O11ySavedViewOut.data": "Data holds the view.",
|
||||
"O11ySavedViewOut.status": "Status is \"success\".",
|
||||
"O11ySavedViewUpdateIn.SavedView": "SavedView is the new view content, embedded so the request body stays the\nbare view the create endpoint takes.",
|
||||
"O11ySavedViewUpdateIn.viewId": "ViewID is the id of the view to replace, taken from the URL.",
|
||||
"QueryEnvelope.spec": "Spec is the deferred decoding of the query if any.",
|
||||
"QueryEnvelope.type": "Type is the type of the query.",
|
||||
"SavedView.extraData": "ExtraData is JSON encoded data used by frontend to store additional data",
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
package o11y
|
||||
|
||||
// THE SEAM. One function, one place: every typed op in this package reaches the
|
||||
// o11y runtime through relay and through nothing else.
|
||||
//
|
||||
// It arrived here by collapse. The conversion grew face by face, and each face
|
||||
// carried its own copy of this function — relay (telemetry.go), relayAt
|
||||
// (logs.go), relayO11y (identity.go), relayAPM (apm.go), send (access.go) —
|
||||
// five bodies that differed only in which prefix constant they concatenated and
|
||||
// whether they took query parameters. Alongside them sat NINE names for one
|
||||
// value: identityPrefix, infraPrefix, integrationsPrefix, metricsRoot,
|
||||
// apmPrefix, accessPrefix, rulesAlertsPrefix and o11yRoot were all the string
|
||||
// "/v1/o11y", and prefix was "/v1/sentry". A name per place instead of a name
|
||||
// per value — so a change to the seam had five sites to find, and a change to
|
||||
// the root had eight.
|
||||
//
|
||||
// The fix is the obvious one once seen: the path a caller reaches is a VALUE,
|
||||
// so ops pass the whole public path (o11yRoot+"/logs") and relay concatenates
|
||||
// nothing. Which face an op belongs to is a fact about the file it lives in,
|
||||
// not a parameter of the transport.
|
||||
//
|
||||
// What relay is: it hands a typed op's call to the handler SetHandler
|
||||
// registered — the same value the delegation wildcard forwards to — so the
|
||||
// request runs the whole chain it always ran, in order. The auth middleware
|
||||
// resolves the same identity, the SAME ViewAccess/EditAccess/AdminAccess gate
|
||||
// the route has always had refuses the same callers, the audit record is
|
||||
// written where it always was, and the bytes it answers with are the bytes the
|
||||
// runtime wrote. There is no policy here: no tenant is resolved, no role is
|
||||
// checked, nothing is scoped. Those all happen where they already do, one layer
|
||||
// in. That is what keeps a typed op a NAMING of the wire rather than a second
|
||||
// implementation of it.
|
||||
//
|
||||
// Identity is PROPAGATED, never minted: the gateway's assertion about the
|
||||
// caller travels on as the same headers it arrived on (zip.CallerOf reads
|
||||
// exactly the set the call plane forwards). A context with no request behind it
|
||||
// — a command, an agent call — carries no assertion, so the runtime's gate
|
||||
// refuses it, which is the honest answer rather than an identity invented at
|
||||
// this hop.
|
||||
//
|
||||
// A non-2xx becomes an error carrying the runtime's own status and reason, so
|
||||
// the status a caller sees is the status the runtime chose.
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/zap-proto/zip"
|
||||
)
|
||||
|
||||
// The two public roots this package answers on, each spelled ONCE.
|
||||
//
|
||||
// - o11yRoot is the observability face: metrics, traces, logs, dashboards,
|
||||
// alerts, identity, access, infra, LLM observability — everything the
|
||||
// console calls.
|
||||
// - sentryRoot is the error-tracking face's own contract, kept separate
|
||||
// because a Sentry SDK reaches it with a DSN key rather than a session.
|
||||
const (
|
||||
o11yRoot = "/v1/o11y"
|
||||
sentryRoot = "/v1/sentry"
|
||||
)
|
||||
|
||||
// relay sends one typed op's call to the runtime at the FULL public path and
|
||||
// decodes the answer into the op's Out. Pass out == nil for the operations
|
||||
// whose answer is a 204 with no body.
|
||||
func relay(ctx context.Context, method, path string, params url.Values, body, out any) error {
|
||||
h := getHandler()
|
||||
if h == nil {
|
||||
return zip.Errorf(http.StatusServiceUnavailable, "o11y runtime not initialized")
|
||||
}
|
||||
|
||||
target := path
|
||||
if q := params.Encode(); q != "" {
|
||||
target += "?" + q
|
||||
}
|
||||
|
||||
payload := io.Reader(http.NoBody)
|
||||
if body != nil {
|
||||
b, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return zip.ErrBadRequest(err.Error())
|
||||
}
|
||||
payload = bytes.NewReader(b)
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, method, target, payload)
|
||||
if err != nil {
|
||||
return zip.ErrBadRequest(err.Error())
|
||||
}
|
||||
if body != nil {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
caller := zip.CallerOf(ctx)
|
||||
for header, value := range map[string]string{
|
||||
zip.HeaderOrg: caller.Org,
|
||||
zip.HeaderProject: caller.Project,
|
||||
zip.HeaderUser: caller.User,
|
||||
zip.HeaderUserName: caller.Name,
|
||||
zip.HeaderUserEmail: caller.Email,
|
||||
zip.HeaderUserOwner: caller.Owner,
|
||||
zip.HeaderRequestID: caller.RequestID,
|
||||
} {
|
||||
if value != "" {
|
||||
req.Header.Set(header, value)
|
||||
}
|
||||
}
|
||||
if caller.Admin {
|
||||
req.Header.Set(zip.HeaderUserAdmin, "true")
|
||||
}
|
||||
if caller.OrgAdmin {
|
||||
req.Header.Set(zip.HeaderUserOrgAdmin, "true")
|
||||
}
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code < http.StatusOK || rec.Code >= http.StatusMultipleChoices {
|
||||
code, reason := refusal(rec.Body.Bytes())
|
||||
return &zip.HTTPError{Status: rec.Code, Code: code, Msg: reason}
|
||||
}
|
||||
if out == nil {
|
||||
return nil
|
||||
}
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), out); err != nil {
|
||||
return zip.ErrInternal("cannot read the runtime's answer: " + err.Error())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// query builds the parameters an op sends from name/value pairs, dropping the
|
||||
// ones the caller left unset so an absent input stays absent instead of arriving
|
||||
// as an empty string — or as a zero page cap, which these faces have always read
|
||||
// as "no cap given". It is the ONE place a typed input is rendered onto the wire.
|
||||
func query(pairs ...any) url.Values {
|
||||
params := url.Values{}
|
||||
for i := 0; i+1 < len(pairs); i += 2 {
|
||||
name, _ := pairs[i].(string)
|
||||
switch value := pairs[i+1].(type) {
|
||||
case string:
|
||||
if value != "" {
|
||||
params.Set(name, value)
|
||||
}
|
||||
case int:
|
||||
if value != 0 {
|
||||
params.Set(name, strconv.Itoa(value))
|
||||
}
|
||||
case bool:
|
||||
if value {
|
||||
params.Set(name, "true")
|
||||
}
|
||||
}
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
// nanos renders a nanosecond epoch for the wire, or nothing when unset — the
|
||||
// runtime reads a missing or non-positive value as "use the default window", so
|
||||
// an absent input stays absent. It feeds query, which stays the ONE place
|
||||
// inputs are rendered onto the wire.
|
||||
func nanos(v int64) string {
|
||||
if v <= 0 {
|
||||
return ""
|
||||
}
|
||||
return strconv.FormatInt(v, 10)
|
||||
}
|
||||
|
||||
// refusal reads the runtime's refusal for its code and its reason. Both of the
|
||||
// shapes these faces answer with are handled — the runtime's own {error:{code,
|
||||
// message}} and the gate's {msg} — and anything else falls back to the body it
|
||||
// sent, so a reason is never invented and never lost.
|
||||
func refusal(body []byte) (code, reason string) {
|
||||
var refused struct {
|
||||
Error struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
} `json:"error"`
|
||||
Msg string `json:"msg"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &refused); err == nil {
|
||||
if refused.Error.Message != "" {
|
||||
return refused.Error.Code, refused.Error.Message
|
||||
}
|
||||
if refused.Msg != "" {
|
||||
return "", refused.Msg
|
||||
}
|
||||
}
|
||||
return "", strings.TrimSpace(string(body))
|
||||
}
|
||||
|
||||
// op names an operation for the document. Several routes carried an explicit
|
||||
// OpenAPI operation id on the runtime's own handler definitions; those ids are
|
||||
// preserved verbatim so an operation's name survives the move into the composed
|
||||
// document.
|
||||
func op(id string) zip.OpOption { return zip.WithOperationID(id) }
|
||||
+208
@@ -0,0 +1,208 @@
|
||||
package o11y_test
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/hanzoai/o11y"
|
||||
"github.com/zap-proto/zip"
|
||||
)
|
||||
|
||||
// THE CENSUS. This file is the arithmetic that the wildcard used to hide.
|
||||
//
|
||||
// While Mount ended in app.All("/v1/o11y/*", …) there was no way to tell a
|
||||
// converted route from an unconverted one from a MISSING one: the catch-all
|
||||
// answered all three the same. That is exactly how 83 typed ops shipped in
|
||||
// files nothing called — the package built, the suite passed, and every one of
|
||||
// those routes still fell through the wildcard. These tests are the replacement
|
||||
// for that catch-all's silence.
|
||||
|
||||
// registered is every non-HEAD, non-OPTIONS route the mount actually installed.
|
||||
func registered(t *testing.T, app *zip.App) map[string]bool {
|
||||
t.Helper()
|
||||
got := map[string]bool{}
|
||||
for _, r := range app.Fiber().GetRoutes(true) {
|
||||
if r.Method == http.MethodHead || r.Method == http.MethodOptions {
|
||||
continue
|
||||
}
|
||||
got[r.Method+" "+r.Path] = true
|
||||
}
|
||||
return got
|
||||
}
|
||||
|
||||
// assertRoutes proves a slice registered EXACTLY its own routes under prefix —
|
||||
// no more, no fewer, at the same methods. Each slice counts only its own doors,
|
||||
// so a slice that stops being mounted fails here rather than being quietly
|
||||
// answered by something else.
|
||||
func assertRoutes(t *testing.T, want map[string]bool, prefix string) {
|
||||
t.Helper()
|
||||
got := map[string]bool{}
|
||||
for route := range registered(t, mounted(t)) {
|
||||
_, path, _ := strings.Cut(route, " ")
|
||||
if path == prefix || strings.HasPrefix(path, prefix+"/") {
|
||||
got[route] = true
|
||||
}
|
||||
}
|
||||
for route := range want {
|
||||
if !got[route] {
|
||||
t.Errorf("%s is not registered", route)
|
||||
}
|
||||
}
|
||||
for route := range got {
|
||||
if !want[route] {
|
||||
t.Errorf("%s is registered but not in the census", route)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// THE WHOLE SURFACE, counted. 367 is the number of method+path pairs the o11y
|
||||
// runtime registers on its gorilla/mux tree (pkg/query-service/app/routes_*.go
|
||||
// and pkg/apiserver/o11yapiserver/*.go). Mount registers the same 367 — 353 as
|
||||
// typed ops that carry a named In, a named Out and their prose into the
|
||||
// document, 11 as named escape hatches, 3 as native service probes.
|
||||
//
|
||||
// If this number moves, one of two things happened and both need a human: a
|
||||
// route was added to the runtime and not named here (it would 404 in the
|
||||
// composed binary), or a route was named here that the runtime does not serve
|
||||
// (it would 404 at the runtime). Neither is visible without the count.
|
||||
func TestEveryRouteIsNamedAndCounted(t *testing.T) {
|
||||
app := mounted(t)
|
||||
all := registered(t, app)
|
||||
|
||||
const (
|
||||
wantTyped = 353 // typed ops: in the OpenAPI document and the MCP tool list
|
||||
wantHatches = 11 // mount.go mountHatches, each with its reason
|
||||
wantProbes = 3 // health.go livez/healthz/readyz, native with fall-through
|
||||
)
|
||||
if len(all) != wantTyped+wantHatches+wantProbes {
|
||||
t.Fatalf("mount registered %d routes, want %d", len(all), wantTyped+wantHatches+wantProbes)
|
||||
}
|
||||
|
||||
// The typed ops are exactly the operations zip published. Counting them off
|
||||
// the document rather than off the router is what makes this a payoff
|
||||
// measurement and not a tautology: an op that reaches the router but not the
|
||||
// document is the defect this whole conversion exists to remove.
|
||||
spec := app.OpenAPISpec()
|
||||
if got := len(app.MCPTools()); got != wantTyped {
|
||||
t.Fatalf("MCP tools = %d, want %d", got, wantTyped)
|
||||
}
|
||||
paths, ok := spec["paths"].(map[string]map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("OpenAPI paths has type %T", spec["paths"])
|
||||
}
|
||||
ops := 0
|
||||
for path, item := range paths {
|
||||
if !strings.HasPrefix(path, "/v1/o11y") && !strings.HasPrefix(path, "/v1/sentry") {
|
||||
t.Errorf("OpenAPI published %s, which is not o11y's surface", path)
|
||||
}
|
||||
for method := range item {
|
||||
switch method {
|
||||
case "get", "post", "put", "patch", "delete":
|
||||
ops++
|
||||
}
|
||||
}
|
||||
}
|
||||
if ops != wantTyped {
|
||||
t.Fatalf("OpenAPI operations = %d, want %d", ops, wantTyped)
|
||||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
func TestNoCatchAllRemains(t *testing.T) {
|
||||
for route := range registered(t, mounted(t)) {
|
||||
if strings.HasSuffix(route, "*") || strings.HasSuffix(route, "/+") {
|
||||
t.Errorf("%s is a catch-all", route)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A path under /v1/o11y that nothing named is a 404 — proof the catch-all is
|
||||
// gone. Under the wildcard this same request reached the runtime, which is why
|
||||
// an unmounted slice could go unnoticed.
|
||||
func TestUnnamedPathIsNotServed(t *testing.T) {
|
||||
app := mounted(t)
|
||||
served := false
|
||||
setRuntime(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
served = true
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
|
||||
status, _ := call(t, app, member(http.MethodGet, "/v1/o11y/no-such-thing", nil))
|
||||
if status != http.StatusNotFound {
|
||||
t.Fatalf("status=%d want 404", status)
|
||||
}
|
||||
if served {
|
||||
t.Fatal("an unnamed path still reached the runtime")
|
||||
}
|
||||
}
|
||||
|
||||
// setRuntime installs a stand-in runtime for the duration of the test.
|
||||
func setRuntime(t *testing.T, fn http.HandlerFunc) {
|
||||
t.Helper()
|
||||
o11y.SetHandler(fn)
|
||||
t.Cleanup(func() { o11y.SetHandler(nil) })
|
||||
}
|
||||
|
||||
// THE ELEVEN. Each hatch is registered at its exact method and path, and each
|
||||
// delegates with the path UNTOUCHED — a rewrite at this seam can only ever move
|
||||
// a request off its own route, and a mangler is invisible to the compiler, so
|
||||
// this is the only thing that catches one coming back.
|
||||
func TestHatchesDelegateVerbatim(t *testing.T) {
|
||||
hatches := []struct{ method, path string }{
|
||||
{http.MethodGet, "/v1/o11y/logs/livetail"},
|
||||
{http.MethodGet, "/v1/o11y/query_progress"},
|
||||
{http.MethodGet, "/ws/query_progress"},
|
||||
{http.MethodPost, "/v1/o11y/export_raw_data"},
|
||||
|
||||
{http.MethodGet, "/v1/o11y/complete/google"},
|
||||
{http.MethodGet, "/v1/o11y/complete/oidc"},
|
||||
{http.MethodPost, "/v1/o11y/complete/saml"},
|
||||
|
||||
{http.MethodPost, "/v1/o11y/api/6ba7b810-9dad-11d1-80b4-00c04fd430c8/envelope/"},
|
||||
{http.MethodPost, "/v1/o11y/api/6ba7b810-9dad-11d1-80b4-00c04fd430c8/store/"},
|
||||
{http.MethodPost, "/v1/sentry/6ba7b810-9dad-11d1-80b4-00c04fd430c8/envelope/"},
|
||||
{http.MethodPost, "/v1/sentry/6ba7b810-9dad-11d1-80b4-00c04fd430c8/store/"},
|
||||
}
|
||||
if len(hatches) != 11 {
|
||||
t.Fatalf("the census itself is wrong: %d", len(hatches))
|
||||
}
|
||||
|
||||
app := mounted(t)
|
||||
var saw string
|
||||
setRuntime(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
saw = r.URL.Path
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
|
||||
for _, h := range hatches {
|
||||
t.Run(h.method+" "+h.path, func(t *testing.T) {
|
||||
saw = ""
|
||||
status, body := call(t, app, member(h.method, h.path, strings.NewReader("{}")))
|
||||
if status != http.StatusOK {
|
||||
t.Fatalf("status=%d body=%s", status, body)
|
||||
}
|
||||
if saw != h.path {
|
||||
t.Fatalf("runtime received %q, want %q verbatim", saw, h.path)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// A hatch answers 503 rather than 404 while no runtime is registered — the same
|
||||
// honest "not initialized yet" the wildcard gave, kept now that each hatch is
|
||||
// its own route.
|
||||
func TestHatchWithoutRuntimeIs503(t *testing.T) {
|
||||
app := mounted(t)
|
||||
o11y.SetHandler(nil)
|
||||
resp, err := app.Fiber().Test(httptest.NewRequest(http.MethodGet, "/v1/o11y/logs/livetail", nil))
|
||||
if err != nil {
|
||||
t.Fatalf("Test: %v", err)
|
||||
}
|
||||
if resp.StatusCode != http.StatusServiceUnavailable {
|
||||
t.Fatalf("status=%d want 503", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
+2
-6
@@ -59,16 +59,12 @@ import (
|
||||
"github.com/zap-proto/zip"
|
||||
)
|
||||
|
||||
// rulesAlertsPrefix is the face's path root, spelled ONCE: the group the ops
|
||||
// register on, and the first segment rulesAlertsRelay rebuilds a call onto.
|
||||
const rulesAlertsPrefix = "/v1/o11y"
|
||||
|
||||
// mountRulesAlerts registers the rules & alerting face's typed ops on the
|
||||
// native router. Collection routes sit beside their parameterised siblings
|
||||
// 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 := app.Group(rulesAlertsPrefix)
|
||||
g := app.Group(o11yRoot)
|
||||
|
||||
// alert rules (runtime gate per op: see each op's comment)
|
||||
zip.Get(g, "/rules", listRules, zip.WithOperationID("ListRules"))
|
||||
@@ -765,7 +761,7 @@ func rulesAlertsRelay(ctx context.Context, method, path string, params url.Value
|
||||
return zip.Errorf(http.StatusServiceUnavailable, "o11y runtime not initialized")
|
||||
}
|
||||
|
||||
target := rulesAlertsPrefix + path
|
||||
target := o11yRoot + path
|
||||
if q := params.Encode(); q != "" {
|
||||
target += "?" + q
|
||||
}
|
||||
|
||||
@@ -1,131 +0,0 @@
|
||||
// Code generated by zipdoc; DO NOT EDIT.
|
||||
|
||||
package o11y
|
||||
|
||||
import (
|
||||
"github.com/zap-proto/zip"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// alert rules
|
||||
zip.Describe("GET /v1/o11y/rules", zip.Doc{
|
||||
Description: "Lists all alert rules with their current evaluation state. Viewer gate.",
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/rules/:id", zip.Doc{
|
||||
Description: "Returns one alert rule with its evaluation state, by id. Viewer gate.",
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/rules", zip.Doc{
|
||||
Description: "Creates a new alert rule and answers with the stored rule. Editor gate.",
|
||||
})
|
||||
zip.Describe("PUT /v1/o11y/rules/:id", zip.Doc{
|
||||
Description: "Replaces an alert rule's definition, by id. Editor gate.",
|
||||
})
|
||||
zip.Describe("DELETE /v1/o11y/rules/:id", zip.Doc{
|
||||
Description: "Removes an alert rule, by id. Editor gate.",
|
||||
})
|
||||
zip.Describe("PATCH /v1/o11y/rules/:id", zip.Doc{
|
||||
Description: "Applies a partial update to an alert rule, by id, answering with the\nstored rule — the common toggle for enabling or muting a rule. Editor gate.",
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/rules/test", zip.Doc{
|
||||
Description: "Fires a test notification for a rule definition without saving it,\nanswering with how many series would alert. Editor gate.",
|
||||
})
|
||||
|
||||
// planned maintenance / downtime schedules
|
||||
zip.Describe("GET /v1/o11y/downtime_schedules", zip.Doc{
|
||||
Description: "Lists all planned maintenance windows, optionally narrowed to the active\nones or the recurring ones. Viewer gate.",
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/downtime_schedules/:id", zip.Doc{
|
||||
Description: "Returns one planned maintenance window, by id. Viewer gate.",
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/downtime_schedules", zip.Doc{
|
||||
Description: "Creates a planned maintenance window, answering with the stored\nschedule. Editor gate.",
|
||||
})
|
||||
zip.Describe("PUT /v1/o11y/downtime_schedules/:id", zip.Doc{
|
||||
Description: "Replaces a planned maintenance window, by id. Editor gate.",
|
||||
})
|
||||
zip.Describe("DELETE /v1/o11y/downtime_schedules/:id", zip.Doc{
|
||||
Description: "Removes a planned maintenance window, by id. Editor gate.",
|
||||
})
|
||||
|
||||
// rule state history — v2 (GET, query parameters)
|
||||
zip.Describe("GET /v1/o11y/rules/:id/history/stats", zip.Doc{
|
||||
Description: "Returns trigger and resolution statistics for a rule over the selected\ntime range, current window against the prior one. Viewer gate.",
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/rules/:id/history/timeline", zip.Doc{
|
||||
Description: "Returns paginated timeline entries for a rule's state transitions,\nfilterable by state and a label expression, cursor-paginated. Viewer gate.",
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/rules/:id/history/top_contributors", zip.Doc{
|
||||
Description: "Returns the label combinations that contributed most to a rule firing\nover the selected range. Viewer gate.",
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/rules/:id/history/filter_keys", zip.Doc{
|
||||
Description: "Returns the distinct label keys present in a rule's history entries over\nthe selected range, for building history filters. Viewer gate.",
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/rules/:id/history/filter_values", zip.Doc{
|
||||
Description: "Returns the distinct values a given label key has taken across a rule's\nhistory entries. Viewer gate.",
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/rules/:id/history/overall_status", zip.Doc{
|
||||
Description: "Returns the overall firing/inactive intervals for a rule over the\nselected range. Viewer gate.",
|
||||
})
|
||||
|
||||
// rule state history — v1 (POST, JSON body) + legacy test-fire
|
||||
zip.Describe("POST /v1/o11y/testRule", zip.Doc{
|
||||
Description: "Fires a test notification for the posted rule definition and answers\nwith how many series alerted and a status message. The legacy path; prefer\n/rules/test. Editor gate.",
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/rules/:id/history/stats", zip.Doc{
|
||||
Description: "Returns trigger and resolution statistics for a rule, current window\nagainst the prior one, for the posted query range. Viewer gate.",
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/rules/:id/history/timeline", zip.Doc{
|
||||
Description: "Returns a rule's state-transition timeline for the posted query range,\neach entry carrying its related-logs or related-traces link. Viewer gate.",
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/rules/:id/history/top_contributors", zip.Doc{
|
||||
Description: "Returns the label combinations that contributed most to a rule firing,\nfor the posted query range. Viewer gate.",
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/rules/:id/history/overall_status", zip.Doc{
|
||||
Description: "Returns the overall firing/inactive windows for a rule, for the posted\nquery range. Viewer gate.",
|
||||
})
|
||||
|
||||
// notification channels
|
||||
zip.Describe("GET /v1/o11y/channels", zip.Doc{
|
||||
Description: "Lists the org's notification channels. Viewer gate.",
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/channels/:id", zip.Doc{
|
||||
Description: "Returns one notification channel, by id. Viewer gate.",
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/channels", zip.Doc{
|
||||
Description: "Creates a notification channel, answering with the stored channel. Admin gate.",
|
||||
})
|
||||
zip.Describe("PUT /v1/o11y/channels/:id", zip.Doc{
|
||||
Description: "Replaces a notification channel's receiver, by id. Admin gate.",
|
||||
})
|
||||
zip.Describe("DELETE /v1/o11y/channels/:id", zip.Doc{
|
||||
Description: "Removes a notification channel, by id. Admin gate.",
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/channels/test", zip.Doc{
|
||||
Description: "Sends a test notification to the posted receiver. Editor gate.",
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/testChannel", zip.Doc{
|
||||
Description: "Sends a test notification to the posted receiver. The legacy path;\nprefer /channels/test. Editor gate.",
|
||||
})
|
||||
|
||||
// route policies
|
||||
zip.Describe("GET /v1/o11y/route_policies", zip.Doc{
|
||||
Description: "Lists the org's route policies. Viewer gate.",
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/route_policies/:id", zip.Doc{
|
||||
Description: "Returns one route policy, by id. Viewer gate.",
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/route_policies", zip.Doc{
|
||||
Description: "Creates a route policy, answering with the stored policy. Admin gate.",
|
||||
})
|
||||
zip.Describe("PUT /v1/o11y/route_policies/:id", zip.Doc{
|
||||
Description: "Replaces a route policy, by id, answering with the stored policy. Admin gate.",
|
||||
})
|
||||
zip.Describe("DELETE /v1/o11y/route_policies/:id", zip.Doc{
|
||||
Description: "Removes a route policy, by id. Admin gate.",
|
||||
})
|
||||
|
||||
// alerts
|
||||
zip.Describe("GET /v1/o11y/alerts", zip.Doc{
|
||||
Description: "Returns the org's current alerts. Viewer gate.",
|
||||
})
|
||||
}
|
||||
+22
-22
@@ -56,11 +56,11 @@ import (
|
||||
)
|
||||
|
||||
// mountSentryErrors registers the eighteen typed ops: the sentry product face on
|
||||
// the /v1/sentry group (relay), and the error-tracking and legacy exceptions
|
||||
// faces on the /v1/o11y group (relayAt). prefix and o11yRoot are telemetry.go's
|
||||
// and logs.go's roots, spelled once each.
|
||||
// the sentryRoot group, and the error-tracking and legacy exceptions faces on
|
||||
// the o11yRoot group. Both roots and the seam they relay through are spelled
|
||||
// once each, in relay.go.
|
||||
func mountSentryErrors(app *zip.App) {
|
||||
gsentry := app.Group(prefix)
|
||||
gsentry := app.Group(sentryRoot)
|
||||
zip.Get(gsentry, "/projects", sentryListProjects)
|
||||
zip.Post(gsentry, "/projects", sentryCreateProject)
|
||||
zip.Get(gsentry, "/projects/:id", sentryGetProject)
|
||||
@@ -91,7 +91,7 @@ func mountSentryErrors(app *zip.App) {
|
||||
// Callers need the viewer role; the runtime's own gate enforces it.
|
||||
func sentryListProjects(ctx context.Context, _ *struct{}) (*O11ySentryProjectsOut, error) {
|
||||
out := new(O11ySentryProjectsOut)
|
||||
return out, relay(ctx, http.MethodGet, "/projects", nil, nil, out)
|
||||
return out, relay(ctx, http.MethodGet, sentryRoot+"/projects", nil, nil, out)
|
||||
}
|
||||
|
||||
// sentryCreateProject creates a Sentry project under the caller's org and
|
||||
@@ -101,7 +101,7 @@ func sentryListProjects(ctx context.Context, _ *struct{}) (*O11ySentryProjectsOu
|
||||
// Callers need the editor role; the runtime's own gate enforces it.
|
||||
func sentryCreateProject(ctx context.Context, in *O11ySentryPostableProject) (*O11ySentryProjectOut, error) {
|
||||
out := new(O11ySentryProjectOut)
|
||||
return out, relay(ctx, http.MethodPost, "/projects", nil, in, out)
|
||||
return out, relay(ctx, http.MethodPost, sentryRoot+"/projects", nil, in, out)
|
||||
}
|
||||
|
||||
// sentryGetProject returns one Sentry project of the caller's org, DSN included.
|
||||
@@ -109,7 +109,7 @@ func sentryCreateProject(ctx context.Context, in *O11ySentryPostableProject) (*O
|
||||
// Callers need the viewer role; the runtime's own gate enforces it.
|
||||
func sentryGetProject(ctx context.Context, in *O11ySentryProjectRef) (*O11ySentryProjectOut, error) {
|
||||
out := new(O11ySentryProjectOut)
|
||||
return out, relay(ctx, http.MethodGet, "/projects/"+in.ID, nil, nil, out)
|
||||
return out, relay(ctx, http.MethodGet, sentryRoot+"/projects/"+in.ID, nil, nil, out)
|
||||
}
|
||||
|
||||
// sentryDeleteProject deletes one Sentry project of the caller's org. Its DSN
|
||||
@@ -120,7 +120,7 @@ func sentryGetProject(ctx context.Context, in *O11ySentryProjectRef) (*O11ySentr
|
||||
func sentryDeleteProject(ctx context.Context, in *O11ySentryProjectRef) (*struct{}, error) {
|
||||
// The runtime answers 204 with a {"status":"success"} body; it is swallowed
|
||||
// so the op answers a clean 204 No Content, its declared contract.
|
||||
if err := relay(ctx, http.MethodDelete, "/projects/"+in.ID, nil, nil, &struct{}{}); err != nil {
|
||||
if err := relay(ctx, http.MethodDelete, sentryRoot+"/projects/"+in.ID, nil, nil, &struct{}{}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, nil
|
||||
@@ -133,7 +133,7 @@ func sentryDeleteProject(ctx context.Context, in *O11ySentryProjectRef) (*struct
|
||||
// Callers need the editor role; the runtime's own gate enforces it.
|
||||
func sentryRotateProjectKey(ctx context.Context, in *O11ySentryProjectRef) (*O11ySentryProjectOut, error) {
|
||||
out := new(O11ySentryProjectOut)
|
||||
return out, relay(ctx, http.MethodPost, "/projects/"+in.ID+"/keys/rotate", nil, nil, out)
|
||||
return out, relay(ctx, http.MethodPost, sentryRoot+"/projects/"+in.ID+"/keys/rotate", nil, nil, out)
|
||||
}
|
||||
|
||||
// ── sentry issues ───────────────────────────────────────────────────────────
|
||||
@@ -145,7 +145,7 @@ func sentryRotateProjectKey(ctx context.Context, in *O11ySentryProjectRef) (*O11
|
||||
// Callers need the viewer role; the runtime's own gate enforces it.
|
||||
func sentryListIssues(ctx context.Context, in *O11ySentryIssuesIn) (*O11yErrorIssuesOut, error) {
|
||||
out := new(O11yErrorIssuesOut)
|
||||
return out, relay(ctx, http.MethodGet, "/issues", query(
|
||||
return out, relay(ctx, http.MethodGet, sentryRoot+"/issues", query(
|
||||
"status", in.Status,
|
||||
"level", in.Level,
|
||||
"environment", in.Environment,
|
||||
@@ -165,7 +165,7 @@ func sentryListIssues(ctx context.Context, in *O11ySentryIssuesIn) (*O11yErrorIs
|
||||
// Callers need the viewer role; the runtime's own gate enforces it.
|
||||
func sentryGetIssue(ctx context.Context, in *O11ySentryIssueRef) (*O11yErrorGettableIssueOut, error) {
|
||||
out := new(O11yErrorGettableIssueOut)
|
||||
return out, relay(ctx, http.MethodGet, "/issues/"+in.ID, nil, nil, out)
|
||||
return out, relay(ctx, http.MethodGet, sentryRoot+"/issues/"+in.ID, nil, nil, out)
|
||||
}
|
||||
|
||||
// sentryUpdateIssue changes an issue's lifecycle — resolve, ignore, reopen or
|
||||
@@ -174,7 +174,7 @@ func sentryGetIssue(ctx context.Context, in *O11ySentryIssueRef) (*O11yErrorGett
|
||||
// Callers need the editor role; the runtime's own gate enforces it.
|
||||
func sentryUpdateIssue(ctx context.Context, in *O11ySentryUpdateIssueIn) (*O11yErrorIssueOut, error) {
|
||||
out := new(O11yErrorIssueOut)
|
||||
return out, relay(ctx, http.MethodPut, "/issues/"+in.ID, nil, O11yIssueUpdate{Status: in.Status, Assignee: in.Assignee}, out)
|
||||
return out, relay(ctx, http.MethodPut, sentryRoot+"/issues/"+in.ID, nil, O11yIssueUpdate{Status: in.Status, Assignee: in.Assignee}, out)
|
||||
}
|
||||
|
||||
// sentryIssueEvents lists one issue's captured occurrences, scoped to a project
|
||||
@@ -184,7 +184,7 @@ func sentryUpdateIssue(ctx context.Context, in *O11ySentryUpdateIssueIn) (*O11yE
|
||||
// Callers need the viewer role; the runtime's own gate enforces it.
|
||||
func sentryIssueEvents(ctx context.Context, in *O11ySentryIssueEventsIn) (*O11ySentryIssueEventsOut, error) {
|
||||
out := new(O11ySentryIssueEventsOut)
|
||||
return out, relay(ctx, http.MethodGet, "/issues/"+in.ID+"/events", query(
|
||||
return out, relay(ctx, http.MethodGet, sentryRoot+"/issues/"+in.ID+"/events", query(
|
||||
"project", in.Project,
|
||||
"limit", in.Limit,
|
||||
), nil, out)
|
||||
@@ -195,7 +195,7 @@ func sentryIssueEvents(ctx context.Context, in *O11ySentryIssueEventsIn) (*O11yS
|
||||
// Callers need the viewer role; the runtime's own gate enforces it.
|
||||
func sentryGetEvent(ctx context.Context, in *O11ySentryEventRef) (*O11ySentryEventOut, error) {
|
||||
out := new(O11ySentryEventOut)
|
||||
return out, relay(ctx, http.MethodGet, "/events/"+in.ID, query(
|
||||
return out, relay(ctx, http.MethodGet, sentryRoot+"/events/"+in.ID, query(
|
||||
"project", in.Project,
|
||||
), nil, out)
|
||||
}
|
||||
@@ -208,7 +208,7 @@ func sentryGetEvent(ctx context.Context, in *O11ySentryEventRef) (*O11ySentryEve
|
||||
// Callers need the viewer role; the runtime's own gate enforces it.
|
||||
func errorListIssues(ctx context.Context, in *O11yErrorIssuesIn) (*O11yErrorIssuesOut, error) {
|
||||
out := new(O11yErrorIssuesOut)
|
||||
return out, relayAt(ctx, http.MethodGet, "/errortracking/issues", query(
|
||||
return out, relay(ctx, http.MethodGet, o11yRoot+"/errortracking/issues", query(
|
||||
"status", in.Status,
|
||||
"level", in.Level,
|
||||
"environment", in.Environment,
|
||||
@@ -225,7 +225,7 @@ func errorListIssues(ctx context.Context, in *O11yErrorIssuesIn) (*O11yErrorIssu
|
||||
// Callers need the viewer role; the runtime's own gate enforces it.
|
||||
func errorGetIssue(ctx context.Context, in *O11yErrorIssueRef) (*O11yErrorGettableIssueOut, error) {
|
||||
out := new(O11yErrorGettableIssueOut)
|
||||
return out, relayAt(ctx, http.MethodGet, "/errortracking/issues/"+in.ID, nil, nil, out)
|
||||
return out, relay(ctx, http.MethodGet, o11yRoot+"/errortracking/issues/"+in.ID, nil, nil, out)
|
||||
}
|
||||
|
||||
// errorUpdateIssue changes an issue's lifecycle — resolve, ignore, reopen or
|
||||
@@ -234,7 +234,7 @@ func errorGetIssue(ctx context.Context, in *O11yErrorIssueRef) (*O11yErrorGettab
|
||||
// Callers need the editor role; the runtime's own gate enforces it.
|
||||
func errorUpdateIssue(ctx context.Context, in *O11yErrorUpdateIssueIn) (*O11yErrorIssueOut, error) {
|
||||
out := new(O11yErrorIssueOut)
|
||||
return out, relayAt(ctx, http.MethodPost, "/errortracking/issues/"+in.ID, nil, O11yIssueUpdate{Status: in.Status, Assignee: in.Assignee}, out)
|
||||
return out, relay(ctx, http.MethodPost, o11yRoot+"/errortracking/issues/"+in.ID, nil, O11yIssueUpdate{Status: in.Status, Assignee: in.Assignee}, out)
|
||||
}
|
||||
|
||||
// ── legacy exceptions reads ─────────────────────────────────────────────────
|
||||
@@ -246,7 +246,7 @@ func errorUpdateIssue(ctx context.Context, in *O11yErrorUpdateIssueIn) (*O11yErr
|
||||
// Callers need the viewer role; the runtime's own gate enforces it.
|
||||
func errorsList(ctx context.Context, in *O11yErrorsListIn) (*O11yErrorsList, error) {
|
||||
out := new(O11yErrorsList)
|
||||
return out, relayAt(ctx, http.MethodPost, "/listErrors", nil, in, out)
|
||||
return out, relay(ctx, http.MethodPost, o11yRoot+"/listErrors", nil, in, out)
|
||||
}
|
||||
|
||||
// errorsCount counts the grouped exceptions in the query window for the caller's
|
||||
@@ -255,7 +255,7 @@ func errorsList(ctx context.Context, in *O11yErrorsListIn) (*O11yErrorsList, err
|
||||
// Callers need the viewer role; the runtime's own gate enforces it.
|
||||
func errorsCount(ctx context.Context, in *O11yErrorsCountIn) (*O11yErrorCount, error) {
|
||||
out := new(O11yErrorCount)
|
||||
return out, relayAt(ctx, http.MethodPost, "/countErrors", nil, in, out)
|
||||
return out, relay(ctx, http.MethodPost, o11yRoot+"/countErrors", nil, in, out)
|
||||
}
|
||||
|
||||
// errorFromErrorID returns one exception instance and the span it happened on,
|
||||
@@ -264,7 +264,7 @@ func errorsCount(ctx context.Context, in *O11yErrorsCountIn) (*O11yErrorCount, e
|
||||
// Callers need the viewer role; the runtime's own gate enforces it.
|
||||
func errorFromErrorID(ctx context.Context, in *O11yErrorLookupIn) (*O11yErrorWithSpan, error) {
|
||||
out := new(O11yErrorWithSpan)
|
||||
return out, relayAt(ctx, http.MethodGet, "/errorFromErrorID", query(
|
||||
return out, relay(ctx, http.MethodGet, o11yRoot+"/errorFromErrorID", query(
|
||||
"timestamp", in.Timestamp,
|
||||
"groupID", in.GroupID,
|
||||
"errorID", in.ErrorID,
|
||||
@@ -277,7 +277,7 @@ func errorFromErrorID(ctx context.Context, in *O11yErrorLookupIn) (*O11yErrorWit
|
||||
// Callers need the viewer role; the runtime's own gate enforces it.
|
||||
func errorFromGroupID(ctx context.Context, in *O11yErrorLookupIn) (*O11yErrorWithSpan, error) {
|
||||
out := new(O11yErrorWithSpan)
|
||||
return out, relayAt(ctx, http.MethodGet, "/errorFromGroupID", query(
|
||||
return out, relay(ctx, http.MethodGet, o11yRoot+"/errorFromGroupID", query(
|
||||
"timestamp", in.Timestamp,
|
||||
"groupID", in.GroupID,
|
||||
"errorID", in.ErrorID,
|
||||
@@ -291,7 +291,7 @@ func errorFromGroupID(ctx context.Context, in *O11yErrorLookupIn) (*O11yErrorWit
|
||||
// Callers need the viewer role; the runtime's own gate enforces it.
|
||||
func nextPrevErrorIDs(ctx context.Context, in *O11yErrorLookupIn) (*O11yNextPrevErrorIDs, error) {
|
||||
out := new(O11yNextPrevErrorIDs)
|
||||
return out, relayAt(ctx, http.MethodGet, "/nextPrevErrorIDs", query(
|
||||
return out, relay(ctx, http.MethodGet, o11yRoot+"/nextPrevErrorIDs", query(
|
||||
"timestamp", in.Timestamp,
|
||||
"groupID", in.GroupID,
|
||||
"errorID", in.ErrorID,
|
||||
|
||||
@@ -1,507 +0,0 @@
|
||||
// Code generated by zipdoc; DO NOT EDIT.
|
||||
|
||||
package o11y
|
||||
|
||||
import (
|
||||
"github.com/zap-proto/zip"
|
||||
)
|
||||
|
||||
func init() {
|
||||
zip.Describe("DELETE /v1/sentry/projects/:id", zip.Doc{
|
||||
Description: "Deletes one Sentry project of the caller's org. Its DSN\nstops resolving immediately, so ingest for that id fails closed exactly as an\nunknown project does; retained events are not touched. Answers 204.\n\nCallers need the editor role; the runtime's own gate enforces it.",
|
||||
Fields: map[string]string{
|
||||
"O11ySentryProjectRef.id": "ID is the project id.",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/errorFromErrorID", zip.Doc{
|
||||
Description: "Returns one exception instance and the span it happened on,\nby its error id within a group at a timestamp.\n\nCallers need the viewer role; the runtime's own gate enforces it.",
|
||||
Fields: map[string]string{
|
||||
"O11yErrorLookupIn.errorID": "ErrorID is the exception instance id. Required by errorFromErrorID and\nnextPrevErrorIDs; unused by errorFromGroupID.",
|
||||
"O11yErrorLookupIn.groupID": "GroupID is the exception group the instance belongs to. Required.",
|
||||
"O11yErrorLookupIn.timestamp": "Timestamp is the instance's time as a nanosecond epoch spelled as a\nstring. Required.",
|
||||
"O11yErrorWithSpan.errorId": "ErrorID is the exception instance id.",
|
||||
"O11yErrorWithSpan.exceptionEscaped": "ExceptionEscaped marks an exception that escaped its span uncaught.",
|
||||
"O11yErrorWithSpan.exceptionMessage": "ExceptionMsg is the exception's message.",
|
||||
"O11yErrorWithSpan.exceptionStacktrace": "ExceptionStacktrace is the captured stack trace.",
|
||||
"O11yErrorWithSpan.exceptionType": "ExceptionType is the exception's type.",
|
||||
"O11yErrorWithSpan.groupID": "GroupID is the exception group it belongs to.",
|
||||
"O11yErrorWithSpan.serviceName": "ServiceName is the service that reported it.",
|
||||
"O11yErrorWithSpan.spanID": "SpanID is the span it happened on.",
|
||||
"O11yErrorWithSpan.timestamp": "Timestamp is when it happened.",
|
||||
"O11yErrorWithSpan.traceID": "TraceID is the trace the span belonged to.",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/errorFromGroupID", zip.Doc{
|
||||
Description: "Returns the representative exception instance of a group at a\ntimestamp, and the span it happened on.\n\nCallers need the viewer role; the runtime's own gate enforces it.",
|
||||
Fields: map[string]string{
|
||||
"O11yErrorLookupIn.errorID": "ErrorID is the exception instance id. Required by errorFromErrorID and\nnextPrevErrorIDs; unused by errorFromGroupID.",
|
||||
"O11yErrorLookupIn.groupID": "GroupID is the exception group the instance belongs to. Required.",
|
||||
"O11yErrorLookupIn.timestamp": "Timestamp is the instance's time as a nanosecond epoch spelled as a\nstring. Required.",
|
||||
"O11yErrorWithSpan.errorId": "ErrorID is the exception instance id.",
|
||||
"O11yErrorWithSpan.exceptionEscaped": "ExceptionEscaped marks an exception that escaped its span uncaught.",
|
||||
"O11yErrorWithSpan.exceptionMessage": "ExceptionMsg is the exception's message.",
|
||||
"O11yErrorWithSpan.exceptionStacktrace": "ExceptionStacktrace is the captured stack trace.",
|
||||
"O11yErrorWithSpan.exceptionType": "ExceptionType is the exception's type.",
|
||||
"O11yErrorWithSpan.groupID": "GroupID is the exception group it belongs to.",
|
||||
"O11yErrorWithSpan.serviceName": "ServiceName is the service that reported it.",
|
||||
"O11yErrorWithSpan.spanID": "SpanID is the span it happened on.",
|
||||
"O11yErrorWithSpan.timestamp": "Timestamp is when it happened.",
|
||||
"O11yErrorWithSpan.traceID": "TraceID is the trace the span belonged to.",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/errortracking/issues", zip.Doc{
|
||||
Description: "Lists the caller's org's grouped error issues (by\nfingerprint) with status, level, counts and first/last-seen.\n\nCallers need the viewer role; the runtime's own gate enforces it.",
|
||||
Fields: map[string]string{
|
||||
"O11yErrorIssue.assignee": "Assignee is who the issue is assigned to.",
|
||||
"O11yErrorIssue.count": "Count is how many occurrences have landed on the issue.",
|
||||
"O11yErrorIssue.createdAt": "CreatedAt is when the issue was first recorded.",
|
||||
"O11yErrorIssue.culprit": "Culprit is where it came from — the function or route blamed for it.",
|
||||
"O11yErrorIssue.environment": "Environment is the deployment the issue was seen in.",
|
||||
"O11yErrorIssue.fingerprint": "Fingerprint is the grouping key that puts like errors in one issue.",
|
||||
"O11yErrorIssue.firstSeen": "FirstSeen is when the earliest occurrence was recorded.",
|
||||
"O11yErrorIssue.id": "ID is the issue id.",
|
||||
"O11yErrorIssue.lastSeen": "LastSeen is when the latest was.",
|
||||
"O11yErrorIssue.level": "Level is the issue's severity, e.g. error, warning, info.",
|
||||
"O11yErrorIssue.platform": "Platform is the reporting runtime, e.g. go, python, javascript.",
|
||||
"O11yErrorIssue.regressed": "Regressed marks an issue that reopened after being resolved.",
|
||||
"O11yErrorIssue.release": "Release is the version that produced it.",
|
||||
"O11yErrorIssue.resolvedAt": "ResolvedAt is when the issue was resolved, if it is.",
|
||||
"O11yErrorIssue.serviceName": "ServiceName is the service that reported it.",
|
||||
"O11yErrorIssue.status": "Status is the lifecycle state: unresolved, resolved or ignored.",
|
||||
"O11yErrorIssue.type": "Type is the exception type.",
|
||||
"O11yErrorIssue.updatedAt": "UpdatedAt is when the issue last changed.",
|
||||
"O11yErrorIssue.value": "Value is the exception value.",
|
||||
"O11yErrorIssues.items": "Items are the issues.",
|
||||
"O11yErrorIssues.limit": "Limit is the page cap that was applied.",
|
||||
"O11yErrorIssues.offset": "Offset is how many were skipped.",
|
||||
"O11yErrorIssues.total": "Total is how many matched the filter.",
|
||||
"O11yErrorIssuesIn.environment": "Environment narrows to one deployment environment.",
|
||||
"O11yErrorIssuesIn.level": "Level narrows to one severity, e.g. error, warning, info.",
|
||||
"O11yErrorIssuesIn.limit": "Limit caps how many issues come back. Zero means the default.",
|
||||
"O11yErrorIssuesIn.offset": "Offset is how many issues to skip. Zero starts at the first.",
|
||||
"O11yErrorIssuesIn.query": "Query narrows to issues whose text contains it.",
|
||||
"O11yErrorIssuesIn.serviceName": "ServiceName narrows to one reporting service.",
|
||||
"O11yErrorIssuesIn.sort": "Sort orders the page, e.g. lastSeen, firstSeen, count.",
|
||||
"O11yErrorIssuesIn.status": "Status narrows to one lifecycle state: unresolved, resolved or ignored.",
|
||||
"O11yErrorIssuesOut.data": "Data holds the issues.",
|
||||
"O11yErrorIssuesOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/errortracking/issues/:id", zip.Doc{
|
||||
Description: "Returns one grouped issue with its latest occurrence sample.\n\nCallers need the viewer role; the runtime's own gate enforces it.",
|
||||
Fields: map[string]string{
|
||||
"O11yErrorGettableIssue.issue": "Issue is the lifecycle row.",
|
||||
"O11yErrorGettableIssue.latestEvent": "LatestEvent is the most recent occurrence that landed on the issue.",
|
||||
"O11yErrorGettableIssueOut.data": "Data is the issue and its latest occurrence.",
|
||||
"O11yErrorGettableIssueOut.status": "Status is \"success\".",
|
||||
"O11yErrorIssue.assignee": "Assignee is who the issue is assigned to.",
|
||||
"O11yErrorIssue.count": "Count is how many occurrences have landed on the issue.",
|
||||
"O11yErrorIssue.createdAt": "CreatedAt is when the issue was first recorded.",
|
||||
"O11yErrorIssue.culprit": "Culprit is where it came from — the function or route blamed for it.",
|
||||
"O11yErrorIssue.environment": "Environment is the deployment the issue was seen in.",
|
||||
"O11yErrorIssue.fingerprint": "Fingerprint is the grouping key that puts like errors in one issue.",
|
||||
"O11yErrorIssue.firstSeen": "FirstSeen is when the earliest occurrence was recorded.",
|
||||
"O11yErrorIssue.id": "ID is the issue id.",
|
||||
"O11yErrorIssue.lastSeen": "LastSeen is when the latest was.",
|
||||
"O11yErrorIssue.level": "Level is the issue's severity, e.g. error, warning, info.",
|
||||
"O11yErrorIssue.platform": "Platform is the reporting runtime, e.g. go, python, javascript.",
|
||||
"O11yErrorIssue.regressed": "Regressed marks an issue that reopened after being resolved.",
|
||||
"O11yErrorIssue.release": "Release is the version that produced it.",
|
||||
"O11yErrorIssue.resolvedAt": "ResolvedAt is when the issue was resolved, if it is.",
|
||||
"O11yErrorIssue.serviceName": "ServiceName is the service that reported it.",
|
||||
"O11yErrorIssue.status": "Status is the lifecycle state: unresolved, resolved or ignored.",
|
||||
"O11yErrorIssue.type": "Type is the exception type.",
|
||||
"O11yErrorIssue.updatedAt": "UpdatedAt is when the issue last changed.",
|
||||
"O11yErrorIssue.value": "Value is the exception value.",
|
||||
"O11yErrorIssueRef.id": "ID is the issue id.",
|
||||
"O11yEventUser.email": "Email is that user's email.",
|
||||
"O11yEventUser.id": "ID identifies the affected end user.",
|
||||
"O11yEventUser.ipAddress": "IP is that user's address.",
|
||||
"O11yEventUser.username": "Username is that user's name.",
|
||||
"O11yOccurrence.culprit": "Culprit is where it came from.",
|
||||
"O11yOccurrence.environment": "Environment is the deployment it happened in.",
|
||||
"O11yOccurrence.eventId": "EventID is the occurrence's id.",
|
||||
"O11yOccurrence.fingerprint": "Fingerprint is the grouping key it was bucketed by.",
|
||||
"O11yOccurrence.frames": "Frames are the stack, innermost first.",
|
||||
"O11yOccurrence.level": "Level is its severity, e.g. error, warning, info.",
|
||||
"O11yOccurrence.platform": "Platform is the reporting runtime.",
|
||||
"O11yOccurrence.release": "Release is the version that produced it.",
|
||||
"O11yOccurrence.serverName": "ServerName is the host that reported it.",
|
||||
"O11yOccurrence.serviceName": "ServiceName is the service that reported it.",
|
||||
"O11yOccurrence.spanId": "SpanID is the span it belonged to.",
|
||||
"O11yOccurrence.tags": "Tags are the reporter's own key/value labels.",
|
||||
"O11yOccurrence.timestamp": "Timestamp is when the error happened.",
|
||||
"O11yOccurrence.traceId": "TraceID is the trace it belonged to.",
|
||||
"O11yOccurrence.transaction": "Transaction is the operation it happened in.",
|
||||
"O11yOccurrence.type": "Type is the exception type.",
|
||||
"O11yOccurrence.user": "User is the affected end-user context, when the reporter attached one.",
|
||||
"O11yOccurrence.value": "Value is the exception value.",
|
||||
"O11yOccurrenceFrame.absPath": "AbsPath is the file's absolute path.",
|
||||
"O11yOccurrenceFrame.colno": "Colno is the column number.",
|
||||
"O11yOccurrenceFrame.filename": "Filename is the file it is in.",
|
||||
"O11yOccurrenceFrame.function": "Function is the function the frame is in.",
|
||||
"O11yOccurrenceFrame.inApp": "InApp marks a frame in the reporting application's own code.",
|
||||
"O11yOccurrenceFrame.lineno": "Lineno is the line number.",
|
||||
"O11yOccurrenceFrame.module": "Module is the module the function is in.",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/o11y/nextPrevErrorIDs", zip.Doc{
|
||||
Description: "Returns the ids of the exception instances immediately after\nand before a given one within its group — the paging cursor the error detail\nview walks.\n\nCallers need the viewer role; the runtime's own gate enforces it.",
|
||||
Fields: map[string]string{
|
||||
"O11yErrorLookupIn.errorID": "ErrorID is the exception instance id. Required by errorFromErrorID and\nnextPrevErrorIDs; unused by errorFromGroupID.",
|
||||
"O11yErrorLookupIn.groupID": "GroupID is the exception group the instance belongs to. Required.",
|
||||
"O11yErrorLookupIn.timestamp": "Timestamp is the instance's time as a nanosecond epoch spelled as a\nstring. Required.",
|
||||
"O11yNextPrevErrorIDs.groupID": "GroupID is the group both belong to.",
|
||||
"O11yNextPrevErrorIDs.nextErrorID": "NextErrorID is the id of the instance immediately after this one.",
|
||||
"O11yNextPrevErrorIDs.nextTimestamp": "NextTimestamp is that instance's time.",
|
||||
"O11yNextPrevErrorIDs.prevErrorID": "PrevErrorID is the id of the instance immediately before this one.",
|
||||
"O11yNextPrevErrorIDs.prevTimestamp": "PrevTimestamp is that instance's time.",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/sentry/events/:id", zip.Doc{
|
||||
Description: "Returns one captured error event of a project, by its id.\n\nCallers need the viewer role; the runtime's own gate enforces it.",
|
||||
Fields: map[string]string{
|
||||
"O11yEvent.culprit": "Culprit is where it came from — the function or route blamed for it.",
|
||||
"O11yEvent.environment": "Environment is the deployment it happened in.",
|
||||
"O11yEvent.eventId": "EventID is its own id.",
|
||||
"O11yEvent.fingerprint": "Fingerprint is the grouping key that puts like errors in one issue.",
|
||||
"O11yEvent.frames": "Frames are the stack, innermost first.",
|
||||
"O11yEvent.handled": "Handled says whether the application caught it.",
|
||||
"O11yEvent.level": "Level is its severity, e.g. error, warning, info.",
|
||||
"O11yEvent.message": "Message is the human-readable message.",
|
||||
"O11yEvent.orgId": "OrgID is the org that owns it.",
|
||||
"O11yEvent.platform": "Platform is the reporting runtime, e.g. go, python, javascript.",
|
||||
"O11yEvent.projectId": "ProjectID is the project it was captured for.",
|
||||
"O11yEvent.receivedAt": "ReceivedAt is when it arrived here.",
|
||||
"O11yEvent.release": "Release is the version that produced it.",
|
||||
"O11yEvent.serverName": "ServerName is the host that reported it.",
|
||||
"O11yEvent.serviceName": "ServiceName is the service that reported it.",
|
||||
"O11yEvent.spanId": "SpanID is the span it belonged to.",
|
||||
"O11yEvent.tags": "Tags are the reporter's own key/value labels.",
|
||||
"O11yEvent.timestamp": "Timestamp is when the error happened, as the reporter recorded it.",
|
||||
"O11yEvent.traceId": "TraceID is the trace it belonged to.",
|
||||
"O11yEvent.transaction": "Transaction is the operation it happened in.",
|
||||
"O11yEvent.type": "Type is the exception type.",
|
||||
"O11yEvent.userEmail": "UserEmail is that user's email, when attached.",
|
||||
"O11yEvent.userId": "UserID identifies the affected end user, when the reporter attached one.",
|
||||
"O11yEvent.userIp": "UserIP is that user's address, when attached.",
|
||||
"O11yEvent.value": "Value is the exception value.",
|
||||
"O11yFrame.column": "Column is the column number.",
|
||||
"O11yFrame.file": "File is the file it is in.",
|
||||
"O11yFrame.function": "Function is the function the frame is in.",
|
||||
"O11yFrame.line": "Line is the line number.",
|
||||
"O11yFrame.own": "Own marks a frame in the reporting application's own code rather than in a\ndependency or the runtime.",
|
||||
"O11ySentryEventOut.data": "Data is the event.",
|
||||
"O11ySentryEventOut.status": "Status is \"success\".",
|
||||
"O11ySentryEventRef.id": "ID is the event id.",
|
||||
"O11ySentryEventRef.project": "Project is the project the event belongs to, by its id. Required.",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/sentry/issues", zip.Doc{
|
||||
Description: "Lists the caller's org's grouped error issues, optionally\nnarrowed to one project and one time window, and filtered by status, level,\nenvironment, service, a free-text query and a sort.\n\nCallers need the viewer role; the runtime's own gate enforces it.",
|
||||
Fields: map[string]string{
|
||||
"O11yErrorIssue.assignee": "Assignee is who the issue is assigned to.",
|
||||
"O11yErrorIssue.count": "Count is how many occurrences have landed on the issue.",
|
||||
"O11yErrorIssue.createdAt": "CreatedAt is when the issue was first recorded.",
|
||||
"O11yErrorIssue.culprit": "Culprit is where it came from — the function or route blamed for it.",
|
||||
"O11yErrorIssue.environment": "Environment is the deployment the issue was seen in.",
|
||||
"O11yErrorIssue.fingerprint": "Fingerprint is the grouping key that puts like errors in one issue.",
|
||||
"O11yErrorIssue.firstSeen": "FirstSeen is when the earliest occurrence was recorded.",
|
||||
"O11yErrorIssue.id": "ID is the issue id.",
|
||||
"O11yErrorIssue.lastSeen": "LastSeen is when the latest was.",
|
||||
"O11yErrorIssue.level": "Level is the issue's severity, e.g. error, warning, info.",
|
||||
"O11yErrorIssue.platform": "Platform is the reporting runtime, e.g. go, python, javascript.",
|
||||
"O11yErrorIssue.regressed": "Regressed marks an issue that reopened after being resolved.",
|
||||
"O11yErrorIssue.release": "Release is the version that produced it.",
|
||||
"O11yErrorIssue.resolvedAt": "ResolvedAt is when the issue was resolved, if it is.",
|
||||
"O11yErrorIssue.serviceName": "ServiceName is the service that reported it.",
|
||||
"O11yErrorIssue.status": "Status is the lifecycle state: unresolved, resolved or ignored.",
|
||||
"O11yErrorIssue.type": "Type is the exception type.",
|
||||
"O11yErrorIssue.updatedAt": "UpdatedAt is when the issue last changed.",
|
||||
"O11yErrorIssue.value": "Value is the exception value.",
|
||||
"O11yErrorIssues.items": "Items are the issues.",
|
||||
"O11yErrorIssues.limit": "Limit is the page cap that was applied.",
|
||||
"O11yErrorIssues.offset": "Offset is how many were skipped.",
|
||||
"O11yErrorIssues.total": "Total is how many matched the filter.",
|
||||
"O11yErrorIssuesOut.data": "Data holds the issues.",
|
||||
"O11yErrorIssuesOut.status": "Status is \"success\".",
|
||||
"O11ySentryIssuesIn.environment": "Environment narrows to one deployment environment.",
|
||||
"O11ySentryIssuesIn.level": "Level narrows to one severity, e.g. error, warning, info.",
|
||||
"O11ySentryIssuesIn.limit": "Limit caps how many issues come back. Zero means the default.",
|
||||
"O11ySentryIssuesIn.offset": "Offset is how many issues to skip. Zero starts at the first.",
|
||||
"O11ySentryIssuesIn.period": "Period is the window to read, relative to now — 1h, 24h, 7d, 14d, 30d.",
|
||||
"O11ySentryIssuesIn.project": "Project narrows the org's issues to one project, by its id.",
|
||||
"O11ySentryIssuesIn.query": "Query narrows to issues whose text contains it.",
|
||||
"O11ySentryIssuesIn.serviceName": "ServiceName narrows to one reporting service.",
|
||||
"O11ySentryIssuesIn.sort": "Sort orders the page, e.g. lastSeen, firstSeen, count.",
|
||||
"O11ySentryIssuesIn.status": "Status narrows to one lifecycle state: unresolved, resolved or ignored.",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/sentry/issues/:id", zip.Doc{
|
||||
Description: "Returns one grouped issue of the caller's org with its latest\noccurrence sample.\n\nCallers need the viewer role; the runtime's own gate enforces it.",
|
||||
Fields: map[string]string{
|
||||
"O11yErrorGettableIssue.issue": "Issue is the lifecycle row.",
|
||||
"O11yErrorGettableIssue.latestEvent": "LatestEvent is the most recent occurrence that landed on the issue.",
|
||||
"O11yErrorGettableIssueOut.data": "Data is the issue and its latest occurrence.",
|
||||
"O11yErrorGettableIssueOut.status": "Status is \"success\".",
|
||||
"O11yErrorIssue.assignee": "Assignee is who the issue is assigned to.",
|
||||
"O11yErrorIssue.count": "Count is how many occurrences have landed on the issue.",
|
||||
"O11yErrorIssue.createdAt": "CreatedAt is when the issue was first recorded.",
|
||||
"O11yErrorIssue.culprit": "Culprit is where it came from — the function or route blamed for it.",
|
||||
"O11yErrorIssue.environment": "Environment is the deployment the issue was seen in.",
|
||||
"O11yErrorIssue.fingerprint": "Fingerprint is the grouping key that puts like errors in one issue.",
|
||||
"O11yErrorIssue.firstSeen": "FirstSeen is when the earliest occurrence was recorded.",
|
||||
"O11yErrorIssue.id": "ID is the issue id.",
|
||||
"O11yErrorIssue.lastSeen": "LastSeen is when the latest was.",
|
||||
"O11yErrorIssue.level": "Level is the issue's severity, e.g. error, warning, info.",
|
||||
"O11yErrorIssue.platform": "Platform is the reporting runtime, e.g. go, python, javascript.",
|
||||
"O11yErrorIssue.regressed": "Regressed marks an issue that reopened after being resolved.",
|
||||
"O11yErrorIssue.release": "Release is the version that produced it.",
|
||||
"O11yErrorIssue.resolvedAt": "ResolvedAt is when the issue was resolved, if it is.",
|
||||
"O11yErrorIssue.serviceName": "ServiceName is the service that reported it.",
|
||||
"O11yErrorIssue.status": "Status is the lifecycle state: unresolved, resolved or ignored.",
|
||||
"O11yErrorIssue.type": "Type is the exception type.",
|
||||
"O11yErrorIssue.updatedAt": "UpdatedAt is when the issue last changed.",
|
||||
"O11yErrorIssue.value": "Value is the exception value.",
|
||||
"O11yEventUser.email": "Email is that user's email.",
|
||||
"O11yEventUser.id": "ID identifies the affected end user.",
|
||||
"O11yEventUser.ipAddress": "IP is that user's address.",
|
||||
"O11yEventUser.username": "Username is that user's name.",
|
||||
"O11yOccurrence.culprit": "Culprit is where it came from.",
|
||||
"O11yOccurrence.environment": "Environment is the deployment it happened in.",
|
||||
"O11yOccurrence.eventId": "EventID is the occurrence's id.",
|
||||
"O11yOccurrence.fingerprint": "Fingerprint is the grouping key it was bucketed by.",
|
||||
"O11yOccurrence.frames": "Frames are the stack, innermost first.",
|
||||
"O11yOccurrence.level": "Level is its severity, e.g. error, warning, info.",
|
||||
"O11yOccurrence.platform": "Platform is the reporting runtime.",
|
||||
"O11yOccurrence.release": "Release is the version that produced it.",
|
||||
"O11yOccurrence.serverName": "ServerName is the host that reported it.",
|
||||
"O11yOccurrence.serviceName": "ServiceName is the service that reported it.",
|
||||
"O11yOccurrence.spanId": "SpanID is the span it belonged to.",
|
||||
"O11yOccurrence.tags": "Tags are the reporter's own key/value labels.",
|
||||
"O11yOccurrence.timestamp": "Timestamp is when the error happened.",
|
||||
"O11yOccurrence.traceId": "TraceID is the trace it belonged to.",
|
||||
"O11yOccurrence.transaction": "Transaction is the operation it happened in.",
|
||||
"O11yOccurrence.type": "Type is the exception type.",
|
||||
"O11yOccurrence.user": "User is the affected end-user context, when the reporter attached one.",
|
||||
"O11yOccurrence.value": "Value is the exception value.",
|
||||
"O11yOccurrenceFrame.absPath": "AbsPath is the file's absolute path.",
|
||||
"O11yOccurrenceFrame.colno": "Colno is the column number.",
|
||||
"O11yOccurrenceFrame.filename": "Filename is the file it is in.",
|
||||
"O11yOccurrenceFrame.function": "Function is the function the frame is in.",
|
||||
"O11yOccurrenceFrame.inApp": "InApp marks a frame in the reporting application's own code.",
|
||||
"O11yOccurrenceFrame.lineno": "Lineno is the line number.",
|
||||
"O11yOccurrenceFrame.module": "Module is the module the function is in.",
|
||||
"O11ySentryIssueRef.id": "ID is the issue id.",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/sentry/issues/:id/events", zip.Doc{
|
||||
Description: "Lists one issue's captured occurrences, scoped to a project\n— a project is an isolation unit, so the caller declares which project's\noccurrences to read.\n\nCallers need the viewer role; the runtime's own gate enforces it.",
|
||||
Fields: map[string]string{
|
||||
"O11yEvent.culprit": "Culprit is where it came from — the function or route blamed for it.",
|
||||
"O11yEvent.environment": "Environment is the deployment it happened in.",
|
||||
"O11yEvent.eventId": "EventID is its own id.",
|
||||
"O11yEvent.fingerprint": "Fingerprint is the grouping key that puts like errors in one issue.",
|
||||
"O11yEvent.frames": "Frames are the stack, innermost first.",
|
||||
"O11yEvent.handled": "Handled says whether the application caught it.",
|
||||
"O11yEvent.level": "Level is its severity, e.g. error, warning, info.",
|
||||
"O11yEvent.message": "Message is the human-readable message.",
|
||||
"O11yEvent.orgId": "OrgID is the org that owns it.",
|
||||
"O11yEvent.platform": "Platform is the reporting runtime, e.g. go, python, javascript.",
|
||||
"O11yEvent.projectId": "ProjectID is the project it was captured for.",
|
||||
"O11yEvent.receivedAt": "ReceivedAt is when it arrived here.",
|
||||
"O11yEvent.release": "Release is the version that produced it.",
|
||||
"O11yEvent.serverName": "ServerName is the host that reported it.",
|
||||
"O11yEvent.serviceName": "ServiceName is the service that reported it.",
|
||||
"O11yEvent.spanId": "SpanID is the span it belonged to.",
|
||||
"O11yEvent.tags": "Tags are the reporter's own key/value labels.",
|
||||
"O11yEvent.timestamp": "Timestamp is when the error happened, as the reporter recorded it.",
|
||||
"O11yEvent.traceId": "TraceID is the trace it belonged to.",
|
||||
"O11yEvent.transaction": "Transaction is the operation it happened in.",
|
||||
"O11yEvent.type": "Type is the exception type.",
|
||||
"O11yEvent.userEmail": "UserEmail is that user's email, when attached.",
|
||||
"O11yEvent.userId": "UserID identifies the affected end user, when the reporter attached one.",
|
||||
"O11yEvent.userIp": "UserIP is that user's address, when attached.",
|
||||
"O11yEvent.value": "Value is the exception value.",
|
||||
"O11yEvents.items": "Items are the events, newest first.",
|
||||
"O11yFrame.column": "Column is the column number.",
|
||||
"O11yFrame.file": "File is the file it is in.",
|
||||
"O11yFrame.function": "Function is the function the frame is in.",
|
||||
"O11yFrame.line": "Line is the line number.",
|
||||
"O11yFrame.own": "Own marks a frame in the reporting application's own code rather than in a\ndependency or the runtime.",
|
||||
"O11ySentryIssueEventsIn.id": "ID is the issue id.",
|
||||
"O11ySentryIssueEventsIn.limit": "Limit caps how many occurrences come back. Zero means the default.",
|
||||
"O11ySentryIssueEventsIn.project": "Project is the project whose occurrences to read, by its id. Required.",
|
||||
"O11ySentryIssueEventsOut.data": "Data holds the occurrences.",
|
||||
"O11ySentryIssueEventsOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/sentry/projects", zip.Doc{
|
||||
Description: "Lists the caller's org's Sentry projects, each with its\nfreshly-derived DSN.\n\nCallers need the viewer role; the runtime's own gate enforces it.",
|
||||
Fields: map[string]string{
|
||||
"O11ySentryProject.createdAt": "CreatedAt is when the project was created.",
|
||||
"O11ySentryProject.dsn": "DSN is the project's freshly-derived ingest DSN.",
|
||||
"O11ySentryProject.id": "ID is the project id.",
|
||||
"O11ySentryProject.name": "Name is the project's display name.",
|
||||
"O11ySentryProject.platform": "Platform is the reporting runtime, e.g. go, python, javascript.",
|
||||
"O11ySentryProject.slug": "Slug is the project's short name.",
|
||||
"O11ySentryProject.status": "Status is the project's lifecycle state: active or disabled.",
|
||||
"O11ySentryProject.updatedAt": "UpdatedAt is when the project last changed.",
|
||||
"O11ySentryProjects.items": "Items are the projects.",
|
||||
"O11ySentryProjects.total": "Total is how many the org has.",
|
||||
"O11ySentryProjectsOut.data": "Data holds the projects.",
|
||||
"O11ySentryProjectsOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("GET /v1/sentry/projects/:id", zip.Doc{
|
||||
Description: "Returns one Sentry project of the caller's org, DSN included.\n\nCallers need the viewer role; the runtime's own gate enforces it.",
|
||||
Fields: map[string]string{
|
||||
"O11ySentryProject.createdAt": "CreatedAt is when the project was created.",
|
||||
"O11ySentryProject.dsn": "DSN is the project's freshly-derived ingest DSN.",
|
||||
"O11ySentryProject.id": "ID is the project id.",
|
||||
"O11ySentryProject.name": "Name is the project's display name.",
|
||||
"O11ySentryProject.platform": "Platform is the reporting runtime, e.g. go, python, javascript.",
|
||||
"O11ySentryProject.slug": "Slug is the project's short name.",
|
||||
"O11ySentryProject.status": "Status is the project's lifecycle state: active or disabled.",
|
||||
"O11ySentryProject.updatedAt": "UpdatedAt is when the project last changed.",
|
||||
"O11ySentryProjectOut.data": "Data is the project.",
|
||||
"O11ySentryProjectOut.status": "Status is \"success\".",
|
||||
"O11ySentryProjectRef.id": "ID is the project id.",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/countErrors", zip.Doc{
|
||||
Description: "Counts the grouped exceptions in the query window for the caller's\norg.\n\nCallers need the viewer role; the runtime's own gate enforces it.",
|
||||
Fields: map[string]string{
|
||||
"O11yErrorsCountIn.end": "End is the window end, as a nanosecond epoch spelled as a string.",
|
||||
"O11yErrorsCountIn.exceptionType": "ExceptionType narrows to one exception type.",
|
||||
"O11yErrorsCountIn.serviceName": "ServiceName narrows to one reporting service.",
|
||||
"O11yErrorsCountIn.start": "Start is the window start, as a nanosecond epoch spelled as a string.",
|
||||
"O11yErrorsCountIn.tags": "Tags narrow the scan to spans carrying the given tag values.",
|
||||
"O11yTagQuery.boolValues": "BoolValues are the boolean values to test against.",
|
||||
"O11yTagQuery.key": "Key is the tag to test.",
|
||||
"O11yTagQuery.numberValues": "NumberValues are the numeric values to test against.",
|
||||
"O11yTagQuery.operator": "Operator is the comparison, e.g. in, nin, contains, exists.",
|
||||
"O11yTagQuery.stringValues": "StringValues are the string values to test against.",
|
||||
"O11yTagQuery.tagType": "TagType is where the tag lives, e.g. ResourceAttribute, SpanAttribute.",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/errortracking/issues/:id", zip.Doc{
|
||||
Description: "Changes an issue's lifecycle — resolve, ignore, reopen or\nassign — and returns the updated issue. Fields left unset are left unchanged.\n\nCallers need the editor role; the runtime's own gate enforces it.",
|
||||
Fields: map[string]string{
|
||||
"O11yErrorIssue.assignee": "Assignee is who the issue is assigned to.",
|
||||
"O11yErrorIssue.count": "Count is how many occurrences have landed on the issue.",
|
||||
"O11yErrorIssue.createdAt": "CreatedAt is when the issue was first recorded.",
|
||||
"O11yErrorIssue.culprit": "Culprit is where it came from — the function or route blamed for it.",
|
||||
"O11yErrorIssue.environment": "Environment is the deployment the issue was seen in.",
|
||||
"O11yErrorIssue.fingerprint": "Fingerprint is the grouping key that puts like errors in one issue.",
|
||||
"O11yErrorIssue.firstSeen": "FirstSeen is when the earliest occurrence was recorded.",
|
||||
"O11yErrorIssue.id": "ID is the issue id.",
|
||||
"O11yErrorIssue.lastSeen": "LastSeen is when the latest was.",
|
||||
"O11yErrorIssue.level": "Level is the issue's severity, e.g. error, warning, info.",
|
||||
"O11yErrorIssue.platform": "Platform is the reporting runtime, e.g. go, python, javascript.",
|
||||
"O11yErrorIssue.regressed": "Regressed marks an issue that reopened after being resolved.",
|
||||
"O11yErrorIssue.release": "Release is the version that produced it.",
|
||||
"O11yErrorIssue.resolvedAt": "ResolvedAt is when the issue was resolved, if it is.",
|
||||
"O11yErrorIssue.serviceName": "ServiceName is the service that reported it.",
|
||||
"O11yErrorIssue.status": "Status is the lifecycle state: unresolved, resolved or ignored.",
|
||||
"O11yErrorIssue.type": "Type is the exception type.",
|
||||
"O11yErrorIssue.updatedAt": "UpdatedAt is when the issue last changed.",
|
||||
"O11yErrorIssue.value": "Value is the exception value.",
|
||||
"O11yErrorIssueOut.data": "Data is the issue.",
|
||||
"O11yErrorIssueOut.status": "Status is \"success\".",
|
||||
"O11yErrorUpdateIssueIn.assignee": "Assignee is who the issue is assigned to.",
|
||||
"O11yErrorUpdateIssueIn.id": "ID is the issue id.",
|
||||
"O11yErrorUpdateIssueIn.status": "Status is the new lifecycle state: unresolved, resolved or ignored.",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/o11y/listErrors", zip.Doc{
|
||||
Description: "Lists the grouped exceptions in the query window — each an\nexception type with its message, count, service and first/last-seen — for the\ncaller's org.\n\nCallers need the viewer role; the runtime's own gate enforces it.",
|
||||
Fields: map[string]string{
|
||||
"O11yErrorsListIn.end": "End is the window end, as a nanosecond epoch spelled as a string.",
|
||||
"O11yErrorsListIn.exceptionType": "ExceptionType narrows to one exception type.",
|
||||
"O11yErrorsListIn.limit": "Limit caps how many exception groups come back. Required, non-zero.",
|
||||
"O11yErrorsListIn.offset": "Offset is how many groups to skip.",
|
||||
"O11yErrorsListIn.order": "Order is the direction: ascending or descending.",
|
||||
"O11yErrorsListIn.orderParam": "OrderParam is the column to order by, e.g. exceptionCount, lastSeen.",
|
||||
"O11yErrorsListIn.serviceName": "ServiceName narrows to one reporting service.",
|
||||
"O11yErrorsListIn.start": "Start is the window start, as a nanosecond epoch spelled as a string.",
|
||||
"O11yErrorsListIn.tags": "Tags narrow the scan to spans carrying the given tag values.",
|
||||
"O11yListError.exceptionCount": "ExceptionCount is how many instances the group holds in the window.",
|
||||
"O11yListError.exceptionMessage": "ExceptionMsg is its message.",
|
||||
"O11yListError.exceptionType": "ExceptionType is the exception's type.",
|
||||
"O11yListError.firstSeen": "FirstSeen is when the earliest was.",
|
||||
"O11yListError.groupID": "GroupID is the group's id.",
|
||||
"O11yListError.lastSeen": "LastSeen is when the latest instance was recorded.",
|
||||
"O11yListError.serviceName": "ServiceName is the service that reported them.",
|
||||
"O11yTagQuery.boolValues": "BoolValues are the boolean values to test against.",
|
||||
"O11yTagQuery.key": "Key is the tag to test.",
|
||||
"O11yTagQuery.numberValues": "NumberValues are the numeric values to test against.",
|
||||
"O11yTagQuery.operator": "Operator is the comparison, e.g. in, nin, contains, exists.",
|
||||
"O11yTagQuery.stringValues": "StringValues are the string values to test against.",
|
||||
"O11yTagQuery.tagType": "TagType is where the tag lives, e.g. ResourceAttribute, SpanAttribute.",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/sentry/projects", zip.Doc{
|
||||
Description: "Creates a Sentry project under the caller's org and\nreturns it, DSN included. Only the name, and optionally a slug and platform,\nare the caller's to set; the org, id and key are server-assigned.\n\nCallers need the editor role; the runtime's own gate enforces it.",
|
||||
Fields: map[string]string{
|
||||
"O11ySentryPostableProject.name": "Name is the project's display name. Required.",
|
||||
"O11ySentryPostableProject.platform": "Platform is the reporting runtime, e.g. go, python, javascript.",
|
||||
"O11ySentryPostableProject.slug": "Slug is the project's short name. Server-assigned from Name when empty.",
|
||||
"O11ySentryProject.createdAt": "CreatedAt is when the project was created.",
|
||||
"O11ySentryProject.dsn": "DSN is the project's freshly-derived ingest DSN.",
|
||||
"O11ySentryProject.id": "ID is the project id.",
|
||||
"O11ySentryProject.name": "Name is the project's display name.",
|
||||
"O11ySentryProject.platform": "Platform is the reporting runtime, e.g. go, python, javascript.",
|
||||
"O11ySentryProject.slug": "Slug is the project's short name.",
|
||||
"O11ySentryProject.status": "Status is the project's lifecycle state: active or disabled.",
|
||||
"O11ySentryProject.updatedAt": "UpdatedAt is when the project last changed.",
|
||||
"O11ySentryProjectOut.data": "Data is the project.",
|
||||
"O11ySentryProjectOut.status": "Status is \"success\".",
|
||||
},
|
||||
})
|
||||
zip.Describe("POST /v1/sentry/projects/:id/keys/rotate", zip.Doc{
|
||||
Description: "Rotates a project's DSN key — bumping its rotation\nwatermark so keys below it stop verifying — and returns the project with its\nnew DSN.\n\nCallers need the editor role; the runtime's own gate enforces it.",
|
||||
Fields: map[string]string{
|
||||
"O11ySentryProject.createdAt": "CreatedAt is when the project was created.",
|
||||
"O11ySentryProject.dsn": "DSN is the project's freshly-derived ingest DSN.",
|
||||
"O11ySentryProject.id": "ID is the project id.",
|
||||
"O11ySentryProject.name": "Name is the project's display name.",
|
||||
"O11ySentryProject.platform": "Platform is the reporting runtime, e.g. go, python, javascript.",
|
||||
"O11ySentryProject.slug": "Slug is the project's short name.",
|
||||
"O11ySentryProject.status": "Status is the project's lifecycle state: active or disabled.",
|
||||
"O11ySentryProject.updatedAt": "UpdatedAt is when the project last changed.",
|
||||
"O11ySentryProjectOut.data": "Data is the project.",
|
||||
"O11ySentryProjectOut.status": "Status is \"success\".",
|
||||
"O11ySentryProjectRef.id": "ID is the project id.",
|
||||
},
|
||||
})
|
||||
zip.Describe("PUT /v1/sentry/issues/:id", zip.Doc{
|
||||
Description: "Changes an issue's lifecycle — resolve, ignore, reopen or\nassign — and returns the updated issue. Fields left unset are left unchanged.\n\nCallers need the editor role; the runtime's own gate enforces it.",
|
||||
Fields: map[string]string{
|
||||
"O11yErrorIssue.assignee": "Assignee is who the issue is assigned to.",
|
||||
"O11yErrorIssue.count": "Count is how many occurrences have landed on the issue.",
|
||||
"O11yErrorIssue.createdAt": "CreatedAt is when the issue was first recorded.",
|
||||
"O11yErrorIssue.culprit": "Culprit is where it came from — the function or route blamed for it.",
|
||||
"O11yErrorIssue.environment": "Environment is the deployment the issue was seen in.",
|
||||
"O11yErrorIssue.fingerprint": "Fingerprint is the grouping key that puts like errors in one issue.",
|
||||
"O11yErrorIssue.firstSeen": "FirstSeen is when the earliest occurrence was recorded.",
|
||||
"O11yErrorIssue.id": "ID is the issue id.",
|
||||
"O11yErrorIssue.lastSeen": "LastSeen is when the latest was.",
|
||||
"O11yErrorIssue.level": "Level is the issue's severity, e.g. error, warning, info.",
|
||||
"O11yErrorIssue.platform": "Platform is the reporting runtime, e.g. go, python, javascript.",
|
||||
"O11yErrorIssue.regressed": "Regressed marks an issue that reopened after being resolved.",
|
||||
"O11yErrorIssue.release": "Release is the version that produced it.",
|
||||
"O11yErrorIssue.resolvedAt": "ResolvedAt is when the issue was resolved, if it is.",
|
||||
"O11yErrorIssue.serviceName": "ServiceName is the service that reported it.",
|
||||
"O11yErrorIssue.status": "Status is the lifecycle state: unresolved, resolved or ignored.",
|
||||
"O11yErrorIssue.type": "Type is the exception type.",
|
||||
"O11yErrorIssue.updatedAt": "UpdatedAt is when the issue last changed.",
|
||||
"O11yErrorIssue.value": "Value is the exception value.",
|
||||
"O11yErrorIssueOut.data": "Data is the issue.",
|
||||
"O11yErrorIssueOut.status": "Status is \"success\".",
|
||||
"O11ySentryUpdateIssueIn.assignee": "Assignee is who the issue is assigned to.",
|
||||
"O11ySentryUpdateIssueIn.id": "ID is the issue id.",
|
||||
"O11ySentryUpdateIssueIn.status": "Status is the new lifecycle state: unresolved, resolved or ignored.",
|
||||
},
|
||||
})
|
||||
}
|
||||
+214
@@ -0,0 +1,214 @@
|
||||
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)
|
||||
|
||||
zip.Get(g, "/span_mapper_groups", spanMapperGroups, op("ListSpanMapperGroups"))
|
||||
zip.Post(g, "/span_mapper_groups", spanMapperGroupCreate, op("CreateSpanMapperGroup"), zip.WithStatus(http.StatusCreated))
|
||||
zip.Patch(g, "/span_mapper_groups/:groupId", spanMapperGroupUpdate, op("UpdateSpanMapperGroup"), zip.WithStatus(http.StatusNoContent))
|
||||
zip.Delete(g, "/span_mapper_groups/:groupId", spanMapperGroupDelete, op("DeleteSpanMapperGroup"), zip.WithStatus(http.StatusNoContent))
|
||||
|
||||
zip.Get(g, "/span_mapper_groups/:groupId/span_mappers", spanMappers, op("ListSpanMappers"))
|
||||
zip.Post(g, "/span_mapper_groups/:groupId/span_mappers", spanMapperCreate, op("CreateSpanMapper"), zip.WithStatus(http.StatusCreated))
|
||||
zip.Patch(g, "/span_mapper_groups/:groupId/span_mappers/:mapperId", spanMapperUpdate, op("UpdateSpanMapper"), zip.WithStatus(http.StatusNoContent))
|
||||
zip.Delete(g, "/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, http.MethodGet, o11yRoot+"/span_mapper_groups", 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, http.MethodPost, o11yRoot+"/span_mapper_groups", 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, http.MethodPatch, groupPath(in.GroupID), 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, http.MethodDelete, groupPath(in.GroupID), 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, http.MethodGet, groupPath(in.GroupID)+"/span_mappers", 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, http.MethodPost, groupPath(in.GroupID)+"/span_mappers", 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, http.MethodPatch, mapperPath(in.GroupID, in.MapperID), 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, http.MethodDelete, mapperPath(in.GroupID, in.MapperID), 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"`
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
package o11y_test
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hanzoai/o11y/pkg/types"
|
||||
"github.com/hanzoai/o11y/pkg/types/spantypes"
|
||||
"github.com/hanzoai/o11y/pkg/valuer"
|
||||
)
|
||||
|
||||
// THE WIRE PROOF for the span-mapper face. Same harness as every other slice.
|
||||
// The four writes answer 204 with no body, so those cases assert the STATUS the
|
||||
// op declared as well as the path it reached — a declared status that does not
|
||||
// match the wire is the one lie a generated client cannot recover from.
|
||||
|
||||
func fullGroup() *spantypes.SpanMapperGroup {
|
||||
at := time.Date(2026, 7, 31, 12, 0, 0, 0, time.UTC)
|
||||
return &spantypes.SpanMapperGroup{
|
||||
ID: valuer.MustNewUUID("6ba7b810-9dad-11d1-80b4-00c04fd430c8"),
|
||||
OrgID: valuer.MustNewUUID("6ba7b811-9dad-11d1-80b4-00c04fd430c8"),
|
||||
Name: "promote-k8s",
|
||||
Condition: spantypes.SpanMapperGroupCondition{
|
||||
Attributes: []string{"k8s.pod.name"},
|
||||
Resource: []string{"service.name"},
|
||||
},
|
||||
Enabled: true,
|
||||
TimeAuditable: types.TimeAuditable{CreatedAt: at, UpdatedAt: at},
|
||||
UserAuditable: types.UserAuditable{CreatedBy: "z", UpdatedBy: "z"},
|
||||
}
|
||||
}
|
||||
|
||||
func fullMapper() *spantypes.SpanMapper {
|
||||
at := time.Date(2026, 7, 31, 12, 0, 0, 0, time.UTC)
|
||||
return &spantypes.SpanMapper{
|
||||
ID: valuer.MustNewUUID("6ba7b812-9dad-11d1-80b4-00c04fd430c8"),
|
||||
GroupID: valuer.MustNewUUID("6ba7b810-9dad-11d1-80b4-00c04fd430c8"),
|
||||
Name: "pod-name",
|
||||
FieldContext: spantypes.FieldContextSpanAttribute,
|
||||
Enabled: true,
|
||||
TimeAuditable: types.TimeAuditable{CreatedAt: at, UpdatedAt: at},
|
||||
UserAuditable: types.UserAuditable{CreatedBy: "z", UpdatedBy: "z"},
|
||||
}
|
||||
}
|
||||
|
||||
func TestSpanMapperReadsAnswerTheRuntimeAnswer(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name, path string
|
||||
payload any
|
||||
}{
|
||||
{"groups", "/v1/o11y/span_mapper_groups",
|
||||
spantypes.NewGettableSpanMapperGroups([]*spantypes.SpanMapperGroup{fullGroup()})},
|
||||
{"mappers", "/v1/o11y/span_mapper_groups/g-1/span_mappers",
|
||||
spantypes.NewGettableSpanMappers([]*spantypes.SpanMapper{fullMapper()})},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
app := mounted(t)
|
||||
want := rendered(t, http.StatusOK, tc.payload)
|
||||
asked := logsRuntime(t, http.StatusOK, want)
|
||||
|
||||
status, got := call(t, app, member(http.MethodGet, tc.path, nil))
|
||||
if status != http.StatusOK {
|
||||
t.Fatalf("status=%d body=%s", status, got)
|
||||
}
|
||||
if string(got) != string(want) {
|
||||
t.Fatalf("op answered %s, runtime wrote %s", got, want)
|
||||
}
|
||||
if r := *asked; r.Method != http.MethodGet || r.URL.Path != tc.path {
|
||||
t.Fatalf("runtime asked %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// The enabled filter reaches the runtime as a query parameter, and an UNSET
|
||||
// filter stays absent rather than arriving as "false" — the runtime has always
|
||||
// read a missing parameter as "all groups", so sending false would narrow a
|
||||
// listing the caller did not narrow.
|
||||
func TestSpanMapperGroupFilterStaysAbsentWhenUnset(t *testing.T) {
|
||||
payload := spantypes.NewGettableSpanMapperGroups([]*spantypes.SpanMapperGroup{fullGroup()})
|
||||
|
||||
for _, tc := range []struct{ target, want string }{
|
||||
{"/v1/o11y/span_mapper_groups", ""},
|
||||
{"/v1/o11y/span_mapper_groups?enabled=true", "enabled=true"},
|
||||
} {
|
||||
t.Run(tc.target, func(t *testing.T) {
|
||||
app := mounted(t)
|
||||
asked := logsRuntime(t, http.StatusOK, rendered(t, http.StatusOK, payload))
|
||||
call(t, app, member(http.MethodGet, tc.target, nil))
|
||||
if got := (*asked).URL.RawQuery; got != tc.want {
|
||||
t.Fatalf("runtime saw query %q, want %q", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSpanMapperCreatesAnswer201(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name, path string
|
||||
payload any
|
||||
body string
|
||||
}{
|
||||
{"group", "/v1/o11y/span_mapper_groups", fullGroup(),
|
||||
`{"name":"promote-k8s","condition":{"attributes":["k8s.pod.name"],"resource":["service.name"]},"enabled":true}`},
|
||||
{"mapper", "/v1/o11y/span_mapper_groups/g-1/span_mappers", fullMapper(),
|
||||
`{"name":"pod-name","fieldContext":"span","config":{},"enabled":true}`},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
app := mounted(t)
|
||||
want := rendered(t, http.StatusCreated, tc.payload)
|
||||
asked := logsRuntime(t, http.StatusCreated, want)
|
||||
|
||||
status, got := call(t, app, member(http.MethodPost, tc.path, strings.NewReader(tc.body)))
|
||||
if status != http.StatusCreated {
|
||||
t.Fatalf("status=%d want 201, body=%s", status, got)
|
||||
}
|
||||
if string(got) != string(want) {
|
||||
t.Fatalf("op answered %s, runtime wrote %s", got, want)
|
||||
}
|
||||
if r := *asked; r.Method != http.MethodPost || r.URL.Path != tc.path {
|
||||
t.Fatalf("runtime asked %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// The four writes answer 204. Their ops declare it, and each must reach its own
|
||||
// path with the two ids on VERBATIM.
|
||||
func TestSpanMapperWritesAnswer204(t *testing.T) {
|
||||
for _, tc := range []struct{ method, path, body string }{
|
||||
{http.MethodPatch, "/v1/o11y/span_mapper_groups/g.1", `{"enabled":false}`},
|
||||
{http.MethodDelete, "/v1/o11y/span_mapper_groups/g.1", ""},
|
||||
{http.MethodPatch, "/v1/o11y/span_mapper_groups/g.1/span_mappers/m.1", `{"enabled":false}`},
|
||||
{http.MethodDelete, "/v1/o11y/span_mapper_groups/g.1/span_mappers/m.1", ""},
|
||||
} {
|
||||
t.Run(tc.method+" "+tc.path, func(t *testing.T) {
|
||||
app := mounted(t)
|
||||
asked := logsRuntime(t, http.StatusNoContent, nil)
|
||||
|
||||
var req *http.Request
|
||||
if tc.body == "" {
|
||||
req = member(tc.method, tc.path, nil)
|
||||
} else {
|
||||
req = member(tc.method, tc.path, strings.NewReader(tc.body))
|
||||
}
|
||||
status, got := call(t, app, req)
|
||||
if status != http.StatusNoContent {
|
||||
t.Fatalf("status=%d want 204, body=%s", status, got)
|
||||
}
|
||||
if r := *asked; r.Method != tc.method || r.URL.Path != tc.path {
|
||||
t.Fatalf("runtime asked %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSpanMapperRoutesAreTheSameEight(t *testing.T) {
|
||||
want := map[string]bool{
|
||||
"GET /v1/o11y/span_mapper_groups": true,
|
||||
"POST /v1/o11y/span_mapper_groups": true,
|
||||
"PATCH /v1/o11y/span_mapper_groups/:groupId": true,
|
||||
"DELETE /v1/o11y/span_mapper_groups/:groupId": true,
|
||||
"GET /v1/o11y/span_mapper_groups/:groupId/span_mappers": true,
|
||||
"POST /v1/o11y/span_mapper_groups/:groupId/span_mappers": true,
|
||||
"PATCH /v1/o11y/span_mapper_groups/:groupId/span_mappers/:mapperId": true,
|
||||
"DELETE /v1/o11y/span_mapper_groups/:groupId/span_mappers/:mapperId": true,
|
||||
}
|
||||
if len(want) != 8 {
|
||||
t.Fatalf("the census itself is wrong: %d", len(want))
|
||||
}
|
||||
assertRoutes(t, want, "/v1/o11y/span_mapper_groups")
|
||||
}
|
||||
+6
-141
@@ -26,29 +26,18 @@ package o11y
|
||||
//go:generate go run github.com/zap-proto/zip/cmd/zipdoc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/zap-proto/zip"
|
||||
)
|
||||
|
||||
// prefix is the product face's path root, spelled ONCE: the group the ops
|
||||
// register on, and the first segment relay rebuilds a call onto.
|
||||
const prefix = "/v1/sentry"
|
||||
|
||||
// mountTelemetry registers the five typed telemetry ops on the native router.
|
||||
// Collection routes register before the parameterised one so an id can never
|
||||
// shadow a collection.
|
||||
func mountTelemetry(app *zip.App) {
|
||||
g := app.Group(prefix)
|
||||
g := app.Group(sentryRoot)
|
||||
zip.Post(g, "/discover", discover)
|
||||
zip.Get(g, "/logs", logs)
|
||||
zip.Get(g, "/traces", traces)
|
||||
@@ -67,14 +56,14 @@ func mountTelemetry(app *zip.App) {
|
||||
// rather than as data.
|
||||
func discover(ctx context.Context, in *O11yDiscoverIn) (*O11yDiscoverOut, error) {
|
||||
out := new(O11yDiscoverOut)
|
||||
return out, relay(ctx, http.MethodPost, "/discover", nil, in, out)
|
||||
return out, relay(ctx, http.MethodPost, sentryRoot+"/discover", nil, in, out)
|
||||
}
|
||||
|
||||
// logs lists a project's captured error events, newest first, optionally
|
||||
// narrowed to those whose message or exception text contains a search string.
|
||||
func logs(ctx context.Context, in *O11yLogsIn) (*O11yLogsOut, error) {
|
||||
out := new(O11yLogsOut)
|
||||
return out, relay(ctx, http.MethodGet, "/logs", query(
|
||||
return out, relay(ctx, http.MethodGet, sentryRoot+"/logs", query(
|
||||
"project", in.Project,
|
||||
"query", in.Query,
|
||||
"period", in.Period,
|
||||
@@ -87,7 +76,7 @@ func logs(ctx context.Context, in *O11yLogsIn) (*O11yLogsOut, error) {
|
||||
// message seen — the entry point for "which requests are failing".
|
||||
func traces(ctx context.Context, in *O11yTracesIn) (*O11yTracesOut, error) {
|
||||
out := new(O11yTracesOut)
|
||||
return out, relay(ctx, http.MethodGet, "/traces", query(
|
||||
return out, relay(ctx, http.MethodGet, sentryRoot+"/traces", query(
|
||||
"project", in.Project,
|
||||
"period", in.Period,
|
||||
"limit", in.Limit,
|
||||
@@ -103,7 +92,7 @@ func trace(ctx context.Context, in *O11yTraceIn) (*O11yTraceOut, error) {
|
||||
// runtime a different id than the caller named — for the one id spelling
|
||||
// that matters, an escaped slash, that is the difference between the answer
|
||||
// the face has always given and a new one.
|
||||
return out, relay(ctx, http.MethodGet, "/traces/"+in.ID, query(
|
||||
return out, relay(ctx, http.MethodGet, sentryRoot+"/traces/"+in.ID, query(
|
||||
"project", in.Project,
|
||||
), nil, out)
|
||||
}
|
||||
@@ -112,7 +101,7 @@ func trace(ctx context.Context, in *O11yTraceIn) (*O11yTraceOut, error) {
|
||||
// the requested period, counting the events in it.
|
||||
func stats(ctx context.Context, in *O11yStatsIn) (*O11yStatsOut, error) {
|
||||
out := new(O11yStatsOut)
|
||||
return out, relay(ctx, http.MethodGet, "/stats", query(
|
||||
return out, relay(ctx, http.MethodGet, sentryRoot+"/stats", query(
|
||||
"project", in.Project,
|
||||
"field", in.Field,
|
||||
"period", in.Period,
|
||||
@@ -371,127 +360,3 @@ type O11yFrame struct {
|
||||
}
|
||||
|
||||
// ── the seam ──────────────────────────────────────────────────────────────────
|
||||
|
||||
// relay hands a typed op's call to the o11y runtime and decodes the answer into
|
||||
// the op's Out.
|
||||
//
|
||||
// It is what keeps these five ops a NAMING of the wire rather than a second
|
||||
// implementation of it. The handler it calls is the one SetHandler registered —
|
||||
// the same value the delegation wildcard forwards to — so the request runs the
|
||||
// whole chain it always ran, in order, and the bytes it answers with are the
|
||||
// bytes the runtime wrote. There is no policy here: no tenant is resolved, no
|
||||
// role is checked, nothing is scoped. Those all happen where they already do,
|
||||
// one layer in.
|
||||
//
|
||||
// Identity is PROPAGATED, never minted: the gateway's assertion about the caller
|
||||
// travels on as the same headers it arrived on (zip.CallerOf reads exactly the
|
||||
// set the call plane forwards). A context with no request behind it — a command,
|
||||
// an agent call — carries no assertion, so the runtime's gate refuses it, which
|
||||
// is the honest answer rather than an identity invented at this hop.
|
||||
//
|
||||
// A non-2xx becomes an error carrying the runtime's own status and reason, so
|
||||
// the status a caller sees is the status the runtime chose.
|
||||
func relay(ctx context.Context, method, path string, params url.Values, body, out any) error {
|
||||
h := getHandler()
|
||||
if h == nil {
|
||||
return zip.Errorf(http.StatusServiceUnavailable, "o11y runtime not initialized")
|
||||
}
|
||||
|
||||
target := prefix + path
|
||||
if q := params.Encode(); q != "" {
|
||||
target += "?" + q
|
||||
}
|
||||
|
||||
payload := io.Reader(http.NoBody)
|
||||
if body != nil {
|
||||
b, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return zip.ErrBadRequest(err.Error())
|
||||
}
|
||||
payload = bytes.NewReader(b)
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, method, target, payload)
|
||||
if err != nil {
|
||||
return zip.ErrBadRequest(err.Error())
|
||||
}
|
||||
if body != nil {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
caller := zip.CallerOf(ctx)
|
||||
for header, value := range map[string]string{
|
||||
zip.HeaderOrg: caller.Org,
|
||||
zip.HeaderProject: caller.Project,
|
||||
zip.HeaderUser: caller.User,
|
||||
zip.HeaderUserName: caller.Name,
|
||||
zip.HeaderUserEmail: caller.Email,
|
||||
zip.HeaderUserOwner: caller.Owner,
|
||||
zip.HeaderRequestID: caller.RequestID,
|
||||
} {
|
||||
if value != "" {
|
||||
req.Header.Set(header, value)
|
||||
}
|
||||
}
|
||||
if caller.Admin {
|
||||
req.Header.Set(zip.HeaderUserAdmin, "true")
|
||||
}
|
||||
if caller.OrgAdmin {
|
||||
req.Header.Set(zip.HeaderUserOrgAdmin, "true")
|
||||
}
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code < http.StatusOK || rec.Code >= http.StatusMultipleChoices {
|
||||
code, reason := refusal(rec.Body.Bytes())
|
||||
return &zip.HTTPError{Status: rec.Code, Code: code, Msg: reason}
|
||||
}
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), out); err != nil {
|
||||
return zip.ErrInternal("cannot read the runtime's answer: " + err.Error())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// query builds the parameters an op sends from name/value pairs, dropping the
|
||||
// ones the caller left unset so an absent input stays absent instead of arriving
|
||||
// as an empty string — or as a zero page cap, which this face has always read as
|
||||
// "no cap given". It is the ONE place a typed input is rendered onto the wire.
|
||||
func query(pairs ...any) url.Values {
|
||||
params := url.Values{}
|
||||
for i := 0; i+1 < len(pairs); i += 2 {
|
||||
name, _ := pairs[i].(string)
|
||||
switch value := pairs[i+1].(type) {
|
||||
case string:
|
||||
if value != "" {
|
||||
params.Set(name, value)
|
||||
}
|
||||
case int:
|
||||
if value != 0 {
|
||||
params.Set(name, strconv.Itoa(value))
|
||||
}
|
||||
}
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
// refusal reads the runtime's refusal for its code and its reason. Both of the
|
||||
// shapes this face answers with are handled — the runtime's own {error:{code,
|
||||
// message}} and the gate's {msg} — and anything else falls back to the body it
|
||||
// sent, so a reason is never invented and never lost.
|
||||
func refusal(body []byte) (code, reason string) {
|
||||
var refused struct {
|
||||
Error struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
} `json:"error"`
|
||||
Msg string `json:"msg"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &refused); err == nil {
|
||||
if refused.Error.Message != "" {
|
||||
return refused.Error.Code, refused.Error.Message
|
||||
}
|
||||
if refused.Msg != "" {
|
||||
return "", refused.Msg
|
||||
}
|
||||
}
|
||||
return "", strings.TrimSpace(string(body))
|
||||
}
|
||||
|
||||
+39
-32
@@ -329,6 +329,15 @@ func TestTelemetryRoutesAreTheSameFive(t *testing.T) {
|
||||
"GET /v1/sentry/traces": true,
|
||||
"GET /v1/sentry/traces/:id": true,
|
||||
"GET /v1/sentry/stats": true,
|
||||
|
||||
// The two ingest doors are NAMED hatches (mount.go mountHatches), and they
|
||||
// land in this census because it counts what is registered. They were not
|
||||
// here before for a reason worth recording: the composed binary only ever
|
||||
// wildcarded /v1/o11y/*, so these two /v1/sentry paths reached NOTHING —
|
||||
// a Sentry SDK pointed at the clean root got a 404. Naming every route is
|
||||
// what surfaced it.
|
||||
"POST /v1/sentry/:project/envelope/": true,
|
||||
"POST /v1/sentry/:project/store/": true,
|
||||
}
|
||||
got := map[string]bool{}
|
||||
for _, r := range app.Fiber().GetRoutes(true) {
|
||||
@@ -355,40 +364,38 @@ func TestTelemetryRoutesAreTheSameFive(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// The REST of the product face is untouched: projects, issues, events and the
|
||||
// two ingest doors still reach the runtime through the host's wildcard, and the
|
||||
// five typed paths take precedence over it however the host ordered its mounts.
|
||||
func TestTheRestOfTheFaceStillReachesTheRuntime(t *testing.T) {
|
||||
app := zip.New(zip.Config{DisableStartupMessage: true})
|
||||
// The host registers its /v1/sentry wildcard BEFORE the module mounts, which
|
||||
// is the order the composed binary uses.
|
||||
app.All("/v1/sentry/*", zip.AdaptNetHTTP(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = io.WriteString(w, `{"door":"wildcard","path":"`+r.URL.Path+`"}`)
|
||||
})))
|
||||
if err := o11y.Mount(app, cloud.Deps{}); err != nil {
|
||||
t.Fatalf("Mount: %v", err)
|
||||
}
|
||||
runtime(t, map[string]any{"items": []*sentrytypes.Event{}})
|
||||
// The ingest door reaches the runtime through its OWN named route, path
|
||||
// untouched, while the typed reads next to it dispatch as ops.
|
||||
//
|
||||
// This test used to install a host wildcard and assert the ingest paths fell
|
||||
// into it. That framing hid a live defect: the composed binary's only wildcard
|
||||
// was /v1/o11y/*, so nothing in the real deployment ever answered
|
||||
// /v1/sentry/<project>/envelope/ — the test passed because the test itself had
|
||||
// registered the door. Naming every route removed both the wildcard and the
|
||||
// illusion.
|
||||
func TestTheIngestDoorIsANamedHatch(t *testing.T) {
|
||||
app := mounted(t)
|
||||
|
||||
// projects, issues, issues/:id/events and events/:id LEFT this list: they are
|
||||
// typed ops in sentryerrors.go and now dispatch ahead of the wildcard. They
|
||||
// only sampled as "wildcarded" because mountSentryErrors was never called —
|
||||
// this list was encoding the dark-slice defect, not guarding against it.
|
||||
// The envelope path stays: it is the Sentry SDK's own wire (that /api/-style
|
||||
// suffix is the SDK's, received verbatim), a genuine escape hatch.
|
||||
for _, target := range []string{
|
||||
"/v1/sentry/6ba7b810-9dad-11d1-80b4-00c04fd430c8/envelope/",
|
||||
} {
|
||||
_, body := call(t, app, member(http.MethodGet, target, nil))
|
||||
if !strings.Contains(string(body), `"door":"wildcard"`) {
|
||||
t.Errorf("%s no longer reaches the runtime through the wildcard: %s", target, body)
|
||||
}
|
||||
var saw string
|
||||
o11y.SetHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
saw = r.URL.Path
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = io.WriteString(w, `{"id":"e1"}`)
|
||||
}))
|
||||
t.Cleanup(func() { o11y.SetHandler(nil) })
|
||||
|
||||
const target = "/v1/sentry/6ba7b810-9dad-11d1-80b4-00c04fd430c8/envelope/"
|
||||
if status, body := call(t, app, member(http.MethodPost, target, strings.NewReader("{}"))); status != http.StatusOK {
|
||||
t.Fatalf("status=%d body=%s", status, body)
|
||||
}
|
||||
// ...and the typed paths win over that wildcard.
|
||||
_, body := call(t, app, member(http.MethodGet, "/v1/sentry/logs?project=p1", nil))
|
||||
if strings.Contains(string(body), `"door":"wildcard"`) {
|
||||
t.Fatalf("the typed op did not take precedence: %s", body)
|
||||
if saw != target {
|
||||
t.Fatalf("runtime received %q, want %q verbatim", saw, target)
|
||||
}
|
||||
|
||||
// ...and the typed reads next to it still dispatch as ops.
|
||||
runtime(t, map[string]any{"items": []*sentrytypes.Event{}})
|
||||
if status, body := call(t, app, member(http.MethodGet, "/v1/sentry/logs?project=p1", nil)); status != http.StatusOK {
|
||||
t.Fatalf("the typed op did not answer: status=%d %s", status, body)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+354
@@ -0,0 +1,354 @@
|
||||
package o11y
|
||||
|
||||
// The TRACE-FUNNELS product face — the funnel definitions themselves, and the
|
||||
// twelve analytics reads over them — as TYPED ops.
|
||||
//
|
||||
// These were eighteen 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. Typing them is what
|
||||
// puts the eighteen operations in the document and therefore in every projection
|
||||
// built from it.
|
||||
//
|
||||
// TWO ANALYTICS FAMILIES, and they are not duplicates of each other. Six routes
|
||||
// analyse a SAVED funnel, named by its id in the path, with only a window in the
|
||||
// body. Six more analyse an AD-HOC funnel the caller describes inline — the
|
||||
// steps travel in the body and nothing is stored — which is what the funnel
|
||||
// builder calls while a funnel is still being drafted. They compute the same six
|
||||
// things, so their answers share a type, and their inputs differ exactly where
|
||||
// the funnel comes from. The runtime spells them as separate handler pairs
|
||||
// (handleX / handleXWithPayload); here they are separate ops for the same
|
||||
// reason, and the naming says which is which.
|
||||
//
|
||||
// THE WIRE DOES NOT MOVE, and the way it does not move is that these ops do not
|
||||
// re-implement anything. Each hands the call to the SAME runtime handler the
|
||||
// wildcard delegates to (relay.go), so identity resolution, the org gate, the
|
||||
// role check (viewer on the reads, editor on the writes) and the success
|
||||
// envelope are all still the runtime's, executed in the order they always were.
|
||||
// What is new here is the TYPE and the prose that goes with it.
|
||||
|
||||
//go:generate go run github.com/zap-proto/zip/cmd/zipdoc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
tf "github.com/hanzoai/o11y/pkg/types/tracefunneltypes"
|
||||
"github.com/zap-proto/zip"
|
||||
)
|
||||
|
||||
// mountTraceFunnels registers the eighteen typed trace-funnel ops. The literal
|
||||
// /list, /new, /steps/update and /analytics/* routes register ahead of the
|
||||
// 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 := app.Group(o11yRoot + "/trace-funnels")
|
||||
|
||||
zip.Post(g, "/new", funnelCreate, op("CreateTraceFunnel"))
|
||||
zip.Get(g, "/list", funnelList, op("ListTraceFunnels"))
|
||||
zip.Put(g, "/steps/update", funnelStepsUpdate, op("UpdateTraceFunnelSteps"))
|
||||
|
||||
// The ad-hoc family: the funnel travels in the body, nothing is stored.
|
||||
zip.Post(g, "/analytics/validate", draftFunnelValidate, op("ValidateDraftFunnelTraces"))
|
||||
zip.Post(g, "/analytics/overview", draftFunnelOverview, op("GetDraftFunnelOverview"))
|
||||
zip.Post(g, "/analytics/steps", draftFunnelSteps, op("GetDraftFunnelStepMetrics"))
|
||||
zip.Post(g, "/analytics/steps/overview", draftFunnelStepOverview, op("GetDraftFunnelStepOverview"))
|
||||
zip.Post(g, "/analytics/slow-traces", draftFunnelSlowTraces, op("GetDraftFunnelSlowTraces"))
|
||||
zip.Post(g, "/analytics/error-traces", draftFunnelErrorTraces, op("GetDraftFunnelErrorTraces"))
|
||||
|
||||
zip.Get(g, "/:funnel_id", funnelGet, op("GetTraceFunnel"))
|
||||
zip.Put(g, "/:funnel_id", funnelUpdate, op("UpdateTraceFunnel"))
|
||||
zip.Delete(g, "/:funnel_id", funnelDelete, op("DeleteTraceFunnel"))
|
||||
|
||||
// The saved family: the funnel is named by the path, the body is the window.
|
||||
zip.Post(g, "/:funnel_id/analytics/validate", funnelValidate, op("ValidateTraceFunnelTraces"))
|
||||
zip.Post(g, "/:funnel_id/analytics/overview", funnelOverview, op("GetTraceFunnelOverview"))
|
||||
zip.Post(g, "/:funnel_id/analytics/steps", funnelSteps, op("GetTraceFunnelStepMetrics"))
|
||||
zip.Post(g, "/:funnel_id/analytics/steps/overview", funnelStepOverview, op("GetTraceFunnelStepOverview"))
|
||||
zip.Post(g, "/:funnel_id/analytics/slow-traces", funnelSlowTraces, op("GetTraceFunnelSlowTraces"))
|
||||
zip.Post(g, "/:funnel_id/analytics/error-traces", funnelErrorTraces, op("GetTraceFunnelErrorTraces"))
|
||||
}
|
||||
|
||||
// ── the funnel itself ─────────────────────────────────────────────────────────
|
||||
|
||||
// funnelCreate creates an empty funnel with a name, answering the funnel it
|
||||
// created. Steps are added afterwards with the steps update.
|
||||
//
|
||||
// Callers need the editor role; the runtime's own gate enforces it.
|
||||
func funnelCreate(ctx context.Context, in *O11yFunnelCreateIn) (*O11yFunnelOut, error) {
|
||||
out := new(O11yFunnelOut)
|
||||
return out, relay(ctx, http.MethodPost, o11yRoot+"/trace-funnels/new", nil, in, out)
|
||||
}
|
||||
|
||||
// funnelList lists the caller's org's funnels, each with its steps and who last
|
||||
// touched it.
|
||||
//
|
||||
// Callers need the viewer role; the runtime's own gate enforces it.
|
||||
func funnelList(ctx context.Context, _ *struct{}) (*O11yFunnelsOut, error) {
|
||||
out := new(O11yFunnelsOut)
|
||||
return out, relay(ctx, http.MethodGet, o11yRoot+"/trace-funnels/list", nil, nil, out)
|
||||
}
|
||||
|
||||
// funnelStepsUpdate replaces a funnel's steps — the funnel is named in the body
|
||||
// rather than the path — and answers the funnel as it now stands. A name or
|
||||
// description sent alongside is applied too; an empty one leaves it as it was.
|
||||
//
|
||||
// Callers need the editor role; the runtime's own gate enforces it.
|
||||
func funnelStepsUpdate(ctx context.Context, in *O11yFunnelStepsUpdateIn) (*O11yFunnelOut, error) {
|
||||
out := new(O11yFunnelOut)
|
||||
return out, relay(ctx, http.MethodPut, o11yRoot+"/trace-funnels/steps/update", nil, in, out)
|
||||
}
|
||||
|
||||
// funnelGet returns one funnel with its steps.
|
||||
//
|
||||
// Callers need the viewer role; the runtime's own gate enforces it.
|
||||
func funnelGet(ctx context.Context, in *O11yFunnelRef) (*O11yFunnelOut, error) {
|
||||
out := new(O11yFunnelOut)
|
||||
return out, relay(ctx, http.MethodGet, funnelPath(in.FunnelID), nil, nil, out)
|
||||
}
|
||||
|
||||
// funnelUpdate renames a funnel or rewrites its description, answering the
|
||||
// funnel as it now stands.
|
||||
//
|
||||
// Callers need the editor role; the runtime's own gate enforces it.
|
||||
func funnelUpdate(ctx context.Context, in *O11yFunnelUpdateIn) (*O11yFunnelOut, error) {
|
||||
out := new(O11yFunnelOut)
|
||||
return out, relay(ctx, http.MethodPut, funnelPath(in.FunnelID), nil, in, out)
|
||||
}
|
||||
|
||||
// funnelDelete deletes a funnel. The answer carries no data — the runtime
|
||||
// acknowledges with the success envelope alone, which is what this Out says.
|
||||
//
|
||||
// Callers need the editor role; the runtime's own gate enforces it.
|
||||
func funnelDelete(ctx context.Context, in *O11yFunnelRef) (*O11yFunnelDeleteOut, error) {
|
||||
out := new(O11yFunnelDeleteOut)
|
||||
return out, relay(ctx, http.MethodDelete, funnelPath(in.FunnelID), nil, nil, out)
|
||||
}
|
||||
|
||||
// ── analytics over a SAVED funnel ─────────────────────────────────────────────
|
||||
|
||||
// funnelValidate lists the traces that match a saved funnel over a window — the
|
||||
// read that answers "is this funnel finding anything at all".
|
||||
func funnelValidate(ctx context.Context, in *O11yFunnelWindowIn) (*O11yFunnelRowsOut, error) {
|
||||
return funnelAnalytics(ctx, in.FunnelID, "validate", in.TimeRange)
|
||||
}
|
||||
|
||||
// funnelOverview returns a saved funnel's conversion overview over a window:
|
||||
// how many entered, how many converted, the rate and the latency.
|
||||
func funnelOverview(ctx context.Context, in *O11yFunnelStepWindowIn) (*O11yFunnelRowsOut, error) {
|
||||
return funnelAnalytics(ctx, in.FunnelID, "overview", in.StepTransitionRequest)
|
||||
}
|
||||
|
||||
// funnelSteps returns a saved funnel's per-step metrics over a window — the
|
||||
// counts and latencies at each step, in step order.
|
||||
func funnelSteps(ctx context.Context, in *O11yFunnelWindowIn) (*O11yFunnelRowsOut, error) {
|
||||
return funnelAnalytics(ctx, in.FunnelID, "steps", in.TimeRange)
|
||||
}
|
||||
|
||||
// funnelStepOverview returns the conversion between two named steps of a saved
|
||||
// funnel — the step-to-step drill-down behind the overview.
|
||||
func funnelStepOverview(ctx context.Context, in *O11yFunnelStepWindowIn) (*O11yFunnelRowsOut, error) {
|
||||
return funnelAnalytics(ctx, in.FunnelID, "steps/overview", in.StepTransitionRequest)
|
||||
}
|
||||
|
||||
// funnelSlowTraces returns the slowest traces through a step transition of a
|
||||
// saved funnel — the entry point for "why is this step slow".
|
||||
func funnelSlowTraces(ctx context.Context, in *O11yFunnelStepWindowIn) (*O11yFunnelRowsOut, error) {
|
||||
return funnelAnalytics(ctx, in.FunnelID, "slow-traces", in.StepTransitionRequest)
|
||||
}
|
||||
|
||||
// funnelErrorTraces returns the errored traces through a step transition of a
|
||||
// saved funnel — the entry point for "why is this step failing".
|
||||
func funnelErrorTraces(ctx context.Context, in *O11yFunnelStepWindowIn) (*O11yFunnelRowsOut, error) {
|
||||
return funnelAnalytics(ctx, in.FunnelID, "error-traces", in.StepTransitionRequest)
|
||||
}
|
||||
|
||||
// ── analytics over an AD-HOC funnel ───────────────────────────────────────────
|
||||
|
||||
// draftFunnelValidate lists the traces that match a funnel described inline —
|
||||
// the builder's "try this" before anything is saved.
|
||||
func draftFunnelValidate(ctx context.Context, in *O11yDraftFunnelIn) (*O11yFunnelRowsOut, error) {
|
||||
return draftAnalytics(ctx, "validate", in)
|
||||
}
|
||||
|
||||
// draftFunnelOverview returns the conversion overview of a funnel described
|
||||
// inline.
|
||||
func draftFunnelOverview(ctx context.Context, in *O11yDraftFunnelIn) (*O11yFunnelRowsOut, error) {
|
||||
return draftAnalytics(ctx, "overview", in)
|
||||
}
|
||||
|
||||
// draftFunnelSteps returns the per-step metrics of a funnel described inline.
|
||||
func draftFunnelSteps(ctx context.Context, in *O11yDraftFunnelIn) (*O11yFunnelRowsOut, error) {
|
||||
return draftAnalytics(ctx, "steps", in)
|
||||
}
|
||||
|
||||
// draftFunnelStepOverview returns the conversion between two steps of a funnel
|
||||
// described inline.
|
||||
func draftFunnelStepOverview(ctx context.Context, in *O11yDraftFunnelIn) (*O11yFunnelRowsOut, error) {
|
||||
return draftAnalytics(ctx, "steps/overview", in)
|
||||
}
|
||||
|
||||
// draftFunnelSlowTraces returns the slowest traces through a step transition of
|
||||
// a funnel described inline.
|
||||
func draftFunnelSlowTraces(ctx context.Context, in *O11yDraftFunnelIn) (*O11yFunnelRowsOut, error) {
|
||||
return draftAnalytics(ctx, "slow-traces", in)
|
||||
}
|
||||
|
||||
// draftFunnelErrorTraces returns the errored traces through a step transition of
|
||||
// a funnel described inline.
|
||||
func draftFunnelErrorTraces(ctx context.Context, in *O11yDraftFunnelIn) (*O11yFunnelRowsOut, error) {
|
||||
return draftAnalytics(ctx, "error-traces", in)
|
||||
}
|
||||
|
||||
// ── the two shapes of the same call ───────────────────────────────────────────
|
||||
|
||||
// funnelPath is the one place a funnel id becomes a path. The id goes on
|
||||
// VERBATIM, as the segment the router matched: it arrived percent-encoded or it
|
||||
// did not, and re-encoding it here would hand the runtime a different id than
|
||||
// the caller named.
|
||||
func funnelPath(id string) string { return o11yRoot + "/trace-funnels/" + id }
|
||||
|
||||
// funnelAnalytics runs one of the six reads against a saved funnel. Twelve ops
|
||||
// and two call shapes, because the twelve differ only in which of the six
|
||||
// computations they name and where the funnel came from.
|
||||
func funnelAnalytics(ctx context.Context, id, view string, body any) (*O11yFunnelRowsOut, error) {
|
||||
out := new(O11yFunnelRowsOut)
|
||||
return out, relay(ctx, http.MethodPost, funnelPath(id)+"/analytics/"+view, nil, body, out)
|
||||
}
|
||||
|
||||
// draftAnalytics runs one of the six reads against a funnel described inline.
|
||||
func draftAnalytics(ctx context.Context, view string, in *O11yDraftFunnelIn) (*O11yFunnelRowsOut, error) {
|
||||
out := new(O11yFunnelRowsOut)
|
||||
return out, relay(ctx, http.MethodPost, o11yRoot+"/trace-funnels/analytics/"+view, nil, in, out)
|
||||
}
|
||||
|
||||
// ── inputs ────────────────────────────────────────────────────────────────────
|
||||
|
||||
// O11yFunnelRef names one saved funnel.
|
||||
type O11yFunnelRef struct {
|
||||
// FunnelID is the funnel's id. Required.
|
||||
FunnelID string `json:"-" url:"funnel_id" validate:"required"`
|
||||
}
|
||||
|
||||
// O11yFunnelCreateIn describes the funnel to create.
|
||||
type O11yFunnelCreateIn struct {
|
||||
// Name is the funnel's name.
|
||||
Name string `json:"funnel_name"`
|
||||
// Timestamp is when the funnel was created, as a millisecond epoch. Zero
|
||||
// takes the runtime's own clock.
|
||||
Timestamp int64 `json:"timestamp,omitempty"`
|
||||
}
|
||||
|
||||
// O11yFunnelUpdateIn renames a funnel or rewrites its description. The id
|
||||
// travels in the path; the runtime reads the rest from the body.
|
||||
type O11yFunnelUpdateIn struct {
|
||||
// FunnelID is the funnel to update. Required.
|
||||
FunnelID string `json:"-" url:"funnel_id" validate:"required"`
|
||||
// Name replaces the funnel's name. Empty leaves it as it was.
|
||||
Name string `json:"funnel_name,omitempty"`
|
||||
// Description replaces the funnel's description. Empty leaves it as it was.
|
||||
Description string `json:"description,omitempty"`
|
||||
// Timestamp is when the change was made, as a millisecond epoch.
|
||||
Timestamp int64 `json:"timestamp,omitempty"`
|
||||
}
|
||||
|
||||
// O11yFunnelStepsUpdateIn replaces a funnel's steps. The funnel is named in the
|
||||
// BODY here, not the path — that is the route the console has always called and
|
||||
// the shape is preserved rather than tidied.
|
||||
type O11yFunnelStepsUpdateIn struct {
|
||||
// FunnelID is the funnel to update.
|
||||
FunnelID string `json:"funnel_id"`
|
||||
// Steps are the funnel's steps, in order. At least two are needed before
|
||||
// any analytics read will answer.
|
||||
Steps []*tf.FunnelStep `json:"steps"`
|
||||
// Name replaces the funnel's name. Empty leaves it as it was.
|
||||
Name string `json:"funnel_name,omitempty"`
|
||||
// Description replaces the funnel's description. Empty leaves it as it was.
|
||||
Description string `json:"description,omitempty"`
|
||||
// Timestamp is when the change was made, as a millisecond epoch.
|
||||
Timestamp int64 `json:"timestamp,omitempty"`
|
||||
}
|
||||
|
||||
// O11yFunnelWindowIn names a saved funnel and the window to read it over.
|
||||
type O11yFunnelWindowIn struct {
|
||||
// FunnelID is the funnel to analyse. Required.
|
||||
FunnelID string `json:"-" url:"funnel_id" validate:"required"`
|
||||
|
||||
tf.TimeRange
|
||||
}
|
||||
|
||||
// O11yFunnelStepWindowIn names a saved funnel, the window, and which pair of
|
||||
// steps the transition runs between.
|
||||
type O11yFunnelStepWindowIn struct {
|
||||
// FunnelID is the funnel to analyse. Required.
|
||||
FunnelID string `json:"-" url:"funnel_id" validate:"required"`
|
||||
|
||||
tf.StepTransitionRequest
|
||||
}
|
||||
|
||||
// O11yDraftFunnelIn is a funnel described inline, with the window and the step
|
||||
// pair to analyse. Nothing here is stored.
|
||||
//
|
||||
// NOTHING IS MARKED REQUIRED, deliberately. The runtime decodes this body and
|
||||
// then applies its OWN rules — "funnel must have at least 2 steps" on some
|
||||
// reads, nothing at all on others — and a validate:"required" here would refuse
|
||||
// the call earlier, with a different status and a different message than the
|
||||
// face has always given. A typed op names the wire; it does not tighten it.
|
||||
type O11yDraftFunnelIn struct {
|
||||
// Steps are the funnel's steps, in order. At least two are needed.
|
||||
Steps []*tf.FunnelStep `json:"steps"`
|
||||
// StartTime is the start of the window, as a millisecond epoch.
|
||||
StartTime int64 `json:"start_time,omitempty"`
|
||||
// EndTime is the end of the window, as a millisecond epoch.
|
||||
EndTime int64 `json:"end_time,omitempty"`
|
||||
// StepStart is the step the transition runs from, 1-based. Ignored by the
|
||||
// reads that span the whole funnel.
|
||||
StepStart int64 `json:"step_start,omitempty"`
|
||||
// StepEnd is the step the transition runs to, 1-based.
|
||||
StepEnd int64 `json:"step_end,omitempty"`
|
||||
}
|
||||
|
||||
// ── answers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
// O11yFunnelOut is one funnel.
|
||||
type O11yFunnelOut struct {
|
||||
// Status is "success".
|
||||
Status string `json:"status"`
|
||||
// Data is the funnel with its steps.
|
||||
Data tf.GettableFunnel `json:"data"`
|
||||
}
|
||||
|
||||
// O11yFunnelsOut is the caller's org's funnels.
|
||||
type O11yFunnelsOut struct {
|
||||
// Status is "success".
|
||||
Status string `json:"status"`
|
||||
// Data are the funnels.
|
||||
Data []tf.GettableFunnel `json:"data"`
|
||||
}
|
||||
|
||||
// O11yFunnelDeleteOut acknowledges a delete. The runtime answers the success
|
||||
// envelope and NOTHING else — its data field is omitempty and a delete carries
|
||||
// no payload — so this type has no data field either. Declaring one would put a
|
||||
// "data": null in the document that the wire has never sent.
|
||||
type O11yFunnelDeleteOut struct {
|
||||
// Status is "success".
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
// O11yFunnelRowsOut is what all twelve analytics reads answer: a table of rows,
|
||||
// each a timestamp and an open object of columns. The columns differ per read —
|
||||
// that is the runtime's query shape, not a fixed schema — so the row is an open
|
||||
// object and naming its keys here would be a fiction.
|
||||
type O11yFunnelRowsOut struct {
|
||||
// Status is "success".
|
||||
Status string `json:"status"`
|
||||
// Data are the rows.
|
||||
Data []O11yFunnelRow `json:"data"`
|
||||
}
|
||||
|
||||
// O11yFunnelRow is one row of an analytics answer.
|
||||
type O11yFunnelRow struct {
|
||||
// Timestamp is the row's time.
|
||||
Timestamp string `json:"timestamp"`
|
||||
// Data are the row's columns, keyed by column name.
|
||||
Data map[string]any `json:"data"`
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
package o11y_test
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
v3 "github.com/hanzoai/o11y/pkg/query-service/model/v3"
|
||||
tf "github.com/hanzoai/o11y/pkg/types/tracefunneltypes"
|
||||
"github.com/hanzoai/o11y/pkg/valuer"
|
||||
)
|
||||
|
||||
// THE WIRE PROOF for the trace-funnels face. Same harness as every other slice:
|
||||
// the bytes the RUNTIME writes go in, the op's answer comes out, and they must
|
||||
// be the same bytes — plus the method and path the op asked the runtime for.
|
||||
|
||||
// fullFunnel is one funnel with every field populated, so a dropped or renamed
|
||||
// field cannot hide behind a zero value.
|
||||
func fullFunnel() tf.GettableFunnel {
|
||||
return tf.GettableFunnel{
|
||||
FunnelID: "6ba7b810-9dad-11d1-80b4-00c04fd430c8", FunnelName: "checkout",
|
||||
Description: "cart → pay → done", CreatedAt: 1_760_000_000_000, CreatedBy: "z",
|
||||
UpdatedAt: 1_760_000_009_000, UpdatedBy: "z", OrgID: "maxpower",
|
||||
UserEmail: "[email protected]",
|
||||
Steps: []*tf.FunnelStep{{
|
||||
ID: valuer.MustNewUUID("6ba7b811-9dad-11d1-80b4-00c04fd430c8"),
|
||||
Name: "cart", Description: "add to cart", Order: 1,
|
||||
ServiceName: "web", SpanName: "POST /cart",
|
||||
Filters: &v3.FilterSet{Operator: "AND"},
|
||||
LatencyPointer: "start", LatencyType: "p99", HasErrors: false,
|
||||
}},
|
||||
}
|
||||
}
|
||||
|
||||
func TestFunnelReadsAnswerTheRuntimeAnswer(t *testing.T) {
|
||||
one := fullFunnel()
|
||||
for _, tc := range []struct {
|
||||
name, method, path, target string
|
||||
payload any
|
||||
body string
|
||||
}{
|
||||
{"create", http.MethodPost, "/v1/o11y/trace-funnels/new", "/v1/o11y/trace-funnels/new",
|
||||
one, `{"funnel_name":"checkout","timestamp":1760000000000}`},
|
||||
{"list", http.MethodGet, "/v1/o11y/trace-funnels/list", "/v1/o11y/trace-funnels/list",
|
||||
[]tf.GettableFunnel{one}, ""},
|
||||
{"get", http.MethodGet, "/v1/o11y/trace-funnels/f-1", "/v1/o11y/trace-funnels/f-1",
|
||||
one, ""},
|
||||
{"update", http.MethodPut, "/v1/o11y/trace-funnels/f-1", "/v1/o11y/trace-funnels/f-1",
|
||||
one, `{"funnel_name":"checkout v2"}`},
|
||||
{"steps", http.MethodPut, "/v1/o11y/trace-funnels/steps/update", "/v1/o11y/trace-funnels/steps/update",
|
||||
one, `{"funnel_id":"f-1","steps":[]}`},
|
||||
{"delete", http.MethodDelete, "/v1/o11y/trace-funnels/f-1", "/v1/o11y/trace-funnels/f-1",
|
||||
nil, ""},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
app := mounted(t)
|
||||
want := rendered(t, http.StatusOK, tc.payload)
|
||||
asked := logsRuntime(t, http.StatusOK, want)
|
||||
|
||||
var body *strings.Reader
|
||||
if tc.body != "" {
|
||||
body = strings.NewReader(tc.body)
|
||||
}
|
||||
var req *http.Request
|
||||
if body == nil {
|
||||
req = member(tc.method, tc.path, nil)
|
||||
} else {
|
||||
req = member(tc.method, tc.path, body)
|
||||
}
|
||||
status, got := call(t, app, req)
|
||||
if status != http.StatusOK {
|
||||
t.Fatalf("status=%d body=%s", status, got)
|
||||
}
|
||||
if string(got) != string(want) {
|
||||
t.Fatalf("op answered %s, runtime wrote %s", got, want)
|
||||
}
|
||||
if r := *asked; r.Method != tc.method || r.URL.Path != tc.target {
|
||||
t.Fatalf("runtime asked %s %s, want %s %s", r.Method, r.URL.Path, tc.method, tc.target)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// The twelve analytics reads all answer the same table, and each must reach its
|
||||
// OWN path. The saved family carries the funnel id on the path; the ad-hoc
|
||||
// family carries the funnel in the body and must NOT invent an id segment.
|
||||
func TestFunnelAnalyticsReachTheirOwnPaths(t *testing.T) {
|
||||
rows := []v3.Row{{Data: map[string]any{"conversion_rate": 0.42, "total": float64(100)}}}
|
||||
views := []string{"validate", "overview", "steps", "steps/overview", "slow-traces", "error-traces"}
|
||||
|
||||
for _, view := range views {
|
||||
for _, saved := range []bool{true, false} {
|
||||
path := "/v1/o11y/trace-funnels/analytics/" + view
|
||||
if saved {
|
||||
path = "/v1/o11y/trace-funnels/f-1/analytics/" + view
|
||||
}
|
||||
t.Run(path, func(t *testing.T) {
|
||||
app := mounted(t)
|
||||
want := enveloped(t, rows)
|
||||
asked := logsRuntime(t, http.StatusOK, want)
|
||||
|
||||
status, got := call(t, app, member(http.MethodPost, path,
|
||||
strings.NewReader(`{"start_time":1,"end_time":2,"step_start":1,"step_end":2,"steps":[]}`)))
|
||||
if status != http.StatusOK {
|
||||
t.Fatalf("status=%d body=%s", status, got)
|
||||
}
|
||||
if string(got) != string(want) {
|
||||
t.Fatalf("op answered %s, runtime wrote %s", got, want)
|
||||
}
|
||||
if r := *asked; r.Method != http.MethodPost || r.URL.Path != path {
|
||||
t.Fatalf("runtime asked %s %s, want POST %s", r.Method, r.URL.Path, path)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
if len(views) != 6 {
|
||||
t.Fatalf("the census itself is wrong: %d", len(views))
|
||||
}
|
||||
}
|
||||
|
||||
// A funnel id is a path segment the router matched, and it goes on VERBATIM.
|
||||
func TestFunnelIDGoesOnVerbatim(t *testing.T) {
|
||||
app := mounted(t)
|
||||
asked := logsRuntime(t, http.StatusOK, rendered(t, http.StatusOK, fullFunnel()))
|
||||
|
||||
call(t, app, member(http.MethodGet, "/v1/o11y/trace-funnels/a.b-c_d", nil))
|
||||
if got := (*asked).URL.Path; got != "/v1/o11y/trace-funnels/a.b-c_d" {
|
||||
t.Fatalf("runtime saw %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
// The literal routes must beat the :funnel_id parameter — /list is a listing,
|
||||
// not a funnel named "list". The mux tree gets this from registration order;
|
||||
// here it is asserted, because a Fiber tree that ever changed its precedence
|
||||
// rule would otherwise fail silently at a customer.
|
||||
func TestFunnelLiteralsBeatTheParameter(t *testing.T) {
|
||||
for _, path := range []string{
|
||||
"/v1/o11y/trace-funnels/list",
|
||||
"/v1/o11y/trace-funnels/analytics/overview",
|
||||
} {
|
||||
t.Run(path, func(t *testing.T) {
|
||||
app := mounted(t)
|
||||
asked := logsRuntime(t, http.StatusOK, enveloped(t, []v3.Row{}))
|
||||
method := http.MethodGet
|
||||
var body *strings.Reader
|
||||
if strings.Contains(path, "analytics") {
|
||||
method, body = http.MethodPost, strings.NewReader("{}")
|
||||
}
|
||||
if body == nil {
|
||||
call(t, app, member(method, path, nil))
|
||||
} else {
|
||||
call(t, app, member(method, path, body))
|
||||
}
|
||||
if *asked == nil {
|
||||
t.Fatal("the runtime was never asked — the literal lost to the parameter")
|
||||
}
|
||||
if got := (*asked).URL.Path; got != path {
|
||||
t.Fatalf("runtime saw %q, want %q", got, path)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTraceFunnelRoutesAreTheSameEighteen(t *testing.T) {
|
||||
want := map[string]bool{
|
||||
"POST /v1/o11y/trace-funnels/new": true,
|
||||
"GET /v1/o11y/trace-funnels/list": true,
|
||||
"PUT /v1/o11y/trace-funnels/steps/update": true,
|
||||
"GET /v1/o11y/trace-funnels/:funnel_id": true,
|
||||
"PUT /v1/o11y/trace-funnels/:funnel_id": true,
|
||||
"DELETE /v1/o11y/trace-funnels/:funnel_id": true,
|
||||
}
|
||||
for _, view := range []string{"validate", "overview", "steps", "steps/overview", "slow-traces", "error-traces"} {
|
||||
want["POST /v1/o11y/trace-funnels/analytics/"+view] = true
|
||||
want["POST /v1/o11y/trace-funnels/:funnel_id/analytics/"+view] = true
|
||||
}
|
||||
if len(want) != 18 {
|
||||
t.Fatalf("the census itself is wrong: %d", len(want))
|
||||
}
|
||||
assertRoutes(t, want, "/v1/o11y/trace-funnels")
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
package o11y
|
||||
|
||||
// The TRACES product face — one trace's spans, the trace field catalog and its
|
||||
// tuning write, and the three trace-detail views (waterfall, flamegraph, span
|
||||
// aggregations) — as TYPED ops.
|
||||
//
|
||||
// These were six 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. Distributed tracing is the
|
||||
// product's headline read, and until now a customer could not reach a single
|
||||
// trace from anything but a hand-written HTTP call. Typing them is what puts the
|
||||
// six operations in the document and therefore in every projection built from it.
|
||||
//
|
||||
// THE WIRE DOES NOT MOVE, and the way it does not move is that these ops do not
|
||||
// re-implement the reads. Each hands the call to the SAME runtime handler the
|
||||
// wildcard delegates to (relay.go), so identity resolution, the org gate, the
|
||||
// role check (viewer on the reads, editor on the field write — the exact
|
||||
// ViewAccess/EditAccess gates the runtime has always run), the audit record and
|
||||
// the success envelope are all still the runtime's, executed in the order they
|
||||
// always were. What is new here is the TYPE — the In the caller may send and the
|
||||
// Out they get back — and the prose that goes with it.
|
||||
//
|
||||
// TWO ENVELOPES, HONESTLY NAMED. The trace-detail views come from the
|
||||
// apiserver, which answers {status, data}; the three query-service reads answer
|
||||
// the bare value. The Out types below say which is which rather than papering
|
||||
// over the difference, because the document has to be true of the wire.
|
||||
|
||||
//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"
|
||||
)
|
||||
|
||||
// mountTraces registers the six typed traces ops on the native router. The
|
||||
// literal /traces/fields routes register ahead of the parameterised trace read
|
||||
// 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 := app.Group(o11yRoot)
|
||||
zip.Get(g, "/traces/fields", traceFields, op("GetTraceFields"))
|
||||
zip.Post(g, "/traces/fields", traceFieldUpdate, op("UpdateTraceField"))
|
||||
zip.Get(g, "/traces/:traceId", traceSpans, op("SearchTraces"))
|
||||
zip.Post(g, "/traces/:traceId/waterfall", traceWaterfall, op("GetWaterfallV4"))
|
||||
zip.Post(g, "/traces/:traceId/flamegraph", traceFlamegraph, op("GetFlamegraph"))
|
||||
zip.Post(g, "/traces/:traceId/aggregations", traceAggregations, op("GetTraceAggregations"))
|
||||
}
|
||||
|
||||
// ── the six operations ────────────────────────────────────────────────────────
|
||||
|
||||
// traceFields returns the trace field catalog: the span fields already selected
|
||||
// as indexed columns, and the interesting ones seen in the data that could be.
|
||||
//
|
||||
// Callers need the viewer role; the runtime's own gate enforces it.
|
||||
func traceFields(ctx context.Context, _ *struct{}) (*O11yFieldCatalogOut, error) {
|
||||
out := new(O11yFieldCatalogOut)
|
||||
return out, relay(ctx, http.MethodGet, o11yRoot+"/traces/fields", nil, nil, out)
|
||||
}
|
||||
|
||||
// traceFieldUpdate changes how one span field is stored — selects or deselects
|
||||
// it as a materialized column and tunes its index — and echoes the setting back.
|
||||
//
|
||||
// Callers need the editor role; the runtime's own gate enforces it.
|
||||
func traceFieldUpdate(ctx context.Context, in *O11yFieldSetting) (*O11yFieldSetting, error) {
|
||||
out := new(O11yFieldSetting)
|
||||
return out, relay(ctx, http.MethodPost, o11yRoot+"/traces/fields", nil, in, out)
|
||||
}
|
||||
|
||||
// traceSpans returns one trace's spans as a column/row table, optionally
|
||||
// centred on a span and walked a fixed number of levels up and down from it —
|
||||
// the read the trace explorer opens a trace with.
|
||||
//
|
||||
// Callers need the viewer role; the runtime's own gate enforces it.
|
||||
func traceSpans(ctx context.Context, in *O11yTraceSpansIn) (*O11yTraceSpansOut, error) {
|
||||
out := new(O11yTraceSpansOut)
|
||||
// The trace id goes on VERBATIM, as the segment the router matched: it
|
||||
// arrived percent-encoded or it did not, and re-encoding it here would hand
|
||||
// the runtime a different id than the caller named.
|
||||
return out, relay(ctx, http.MethodGet, o11yRoot+"/traces/"+in.TraceID, query(
|
||||
"spanId", in.SpanID,
|
||||
"levelUp", in.LevelUp,
|
||||
"levelDown", in.LevelDown,
|
||||
"spanRenderLimit", in.SpanRenderLimit,
|
||||
), nil, out)
|
||||
}
|
||||
|
||||
// traceWaterfall returns a trace's waterfall: every span when the trace is
|
||||
// small enough, a capped window around the selected span when it is not, with
|
||||
// the uncollapsed subtrees the caller asked to keep open.
|
||||
//
|
||||
// Callers need the viewer role; the runtime's own gate enforces it.
|
||||
func traceWaterfall(ctx context.Context, in *O11yTraceWaterfallIn) (*O11yTraceWaterfallOut, error) {
|
||||
out := new(O11yTraceWaterfallOut)
|
||||
return out, relay(ctx, http.MethodPost, o11yRoot+"/traces/"+in.TraceID+"/waterfall", nil, in.PostableWaterfall, out)
|
||||
}
|
||||
|
||||
// traceFlamegraph returns a trace's flamegraph: spans bucketed by depth level,
|
||||
// each level ordered as it is drawn, around the selected span.
|
||||
//
|
||||
// Callers need the viewer role; the runtime's own gate enforces it.
|
||||
func traceFlamegraph(ctx context.Context, in *O11yTraceFlamegraphIn) (*O11yTraceFlamegraphOut, error) {
|
||||
out := new(O11yTraceFlamegraphOut)
|
||||
return out, relay(ctx, http.MethodPost, o11yRoot+"/traces/"+in.TraceID+"/flamegraph", nil, in.PostableFlamegraph, out)
|
||||
}
|
||||
|
||||
// traceAggregations computes span aggregations over one trace — span count,
|
||||
// duration or share of execution time — grouped by the resource field each
|
||||
// aggregation names.
|
||||
//
|
||||
// Callers need the viewer role; the runtime's own gate enforces it.
|
||||
func traceAggregations(ctx context.Context, in *O11yTraceAggregationsIn) (*O11yTraceAggregationsOut, error) {
|
||||
out := new(O11yTraceAggregationsOut)
|
||||
return out, relay(ctx, http.MethodPost, o11yRoot+"/traces/"+in.TraceID+"/aggregations", nil, in.PostableTraceAggregations, out)
|
||||
}
|
||||
|
||||
// ── inputs ────────────────────────────────────────────────────────────────────
|
||||
|
||||
// O11yTraceSpansIn names the trace to read and how much of it to walk.
|
||||
type O11yTraceSpansIn struct {
|
||||
// TraceID is the trace to read. Required.
|
||||
TraceID string `json:"-" url:"traceId" validate:"required"`
|
||||
// SpanID centres the read on one span. Empty reads from the trace roots.
|
||||
SpanID string `json:"-" url:"spanId"`
|
||||
// LevelUp is how many ancestor levels above the selected span to include.
|
||||
LevelUp int `json:"-" url:"levelUp"`
|
||||
// LevelDown is how many descendant levels below the selected span to
|
||||
// include.
|
||||
LevelDown int `json:"-" url:"levelDown"`
|
||||
// SpanRenderLimit caps how many spans come back. Zero takes the runtime's
|
||||
// own default.
|
||||
SpanRenderLimit int `json:"-" url:"spanRenderLimit"`
|
||||
}
|
||||
|
||||
// O11yTraceWaterfallIn names the trace and the waterfall's view state. The
|
||||
// body is the runtime's own PostableWaterfall, embedded rather than restated so
|
||||
// the two cannot drift.
|
||||
type O11yTraceWaterfallIn struct {
|
||||
// TraceID is the trace to draw. Required.
|
||||
TraceID string `json:"-" url:"traceId" validate:"required"`
|
||||
|
||||
spantypes.PostableWaterfall
|
||||
}
|
||||
|
||||
// O11yTraceFlamegraphIn names the trace and the flamegraph's view state.
|
||||
type O11yTraceFlamegraphIn struct {
|
||||
// TraceID is the trace to draw. Required.
|
||||
TraceID string `json:"-" url:"traceId" validate:"required"`
|
||||
|
||||
spantypes.PostableFlamegraph
|
||||
}
|
||||
|
||||
// O11yTraceAggregationsIn names the trace and the aggregations to compute.
|
||||
type O11yTraceAggregationsIn struct {
|
||||
// TraceID is the trace to aggregate. Required.
|
||||
TraceID string `json:"-" url:"traceId" validate:"required"`
|
||||
|
||||
spantypes.PostableTraceAggregations
|
||||
}
|
||||
|
||||
// ── answers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
// O11yTraceSpansOut is one trace's spans as the explorer reads them: a set of
|
||||
// windows, each a column list and the rows under it. It is an ARRAY on the wire
|
||||
// — the runtime writes the bare value with no envelope — and the type says so
|
||||
// rather than wrapping it into an object the wire does not have.
|
||||
type O11yTraceSpansOut []O11yTraceSpanWindow
|
||||
|
||||
// O11yTraceSpanWindow is one contiguous window of a trace's spans.
|
||||
type O11yTraceSpanWindow struct {
|
||||
// StartTimestampMillis is when the window opens.
|
||||
StartTimestampMillis uint64 `json:"startTimestampMillis"`
|
||||
// EndTimestampMillis is when it closes.
|
||||
EndTimestampMillis uint64 `json:"endTimestampMillis"`
|
||||
// Columns names the fields each row carries, in row order.
|
||||
Columns []string `json:"columns"`
|
||||
// Events are the rows, each positionally matching Columns.
|
||||
Events [][]any `json:"events"`
|
||||
// IsSubTree says the window is a subtree of the trace rather than the whole
|
||||
// of it.
|
||||
IsSubTree bool `json:"isSubTree"`
|
||||
}
|
||||
|
||||
// O11yTraceWaterfallOut is a trace's waterfall view.
|
||||
type O11yTraceWaterfallOut struct {
|
||||
// Status is "success".
|
||||
Status string `json:"status"`
|
||||
// Data holds the waterfall.
|
||||
Data spantypes.GettableWaterfallTrace `json:"data"`
|
||||
}
|
||||
|
||||
// O11yTraceFlamegraphOut is a trace's flamegraph view.
|
||||
type O11yTraceFlamegraphOut struct {
|
||||
// Status is "success".
|
||||
Status string `json:"status"`
|
||||
// Data holds the flamegraph.
|
||||
Data spantypes.GettableFlamegraphTrace `json:"data"`
|
||||
}
|
||||
|
||||
// O11yTraceAggregationsOut is a trace's computed span aggregations.
|
||||
type O11yTraceAggregationsOut struct {
|
||||
// Status is "success".
|
||||
Status string `json:"status"`
|
||||
// Data holds one result per aggregation asked for.
|
||||
Data spantypes.GettableTraceAggregations `json:"data"`
|
||||
}
|
||||
+270
@@ -0,0 +1,270 @@
|
||||
package o11y_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/hanzoai/o11y/pkg/query-service/model"
|
||||
"github.com/hanzoai/o11y/pkg/types/spantypes"
|
||||
"github.com/hanzoai/o11y/pkg/types/telemetrytypes"
|
||||
)
|
||||
|
||||
// THE WIRE PROOF for the traces face — logs_test.go's discipline applied to the
|
||||
// six typed traces ops. Each case takes the bytes the RUNTIME writes, through
|
||||
// the same construction the real handler uses (WriteJSON's bare marshal for the
|
||||
// query-service reads, render.Success's {status,data} for the apiserver detail
|
||||
// views), hands them to the op, and demands the op answered the same bytes and
|
||||
// asked the runtime for the same method and path. A field the port failed to
|
||||
// name, or named with a different tag, shows up here as a diff.
|
||||
//
|
||||
// The helpers (mounted, call, member, logsRuntime, bare, mustJSON) are
|
||||
// telemetry_test.go's and logs_test.go's; every face is proved with one harness.
|
||||
|
||||
// fullTraceWindow is one window of a trace's spans with every field populated,
|
||||
// so a dropped or renamed field cannot hide behind a zero value.
|
||||
func fullTraceWindow() []model.SearchSpansResult {
|
||||
return []model.SearchSpansResult{{
|
||||
StartTimestampMillis: 1_760_000_000_000,
|
||||
EndTimestampMillis: 1_760_000_009_000,
|
||||
Columns: []string{"timestamp", "spanID", "name", "durationNano"},
|
||||
Events: [][]any{
|
||||
{"2026-07-31T12:00:00Z", "s1", "GET /v1/thing", float64(1500000)},
|
||||
{"2026-07-31T12:00:01Z", "s2", "db.query", float64(430000)},
|
||||
},
|
||||
IsSubTree: true,
|
||||
}}
|
||||
}
|
||||
|
||||
func TestTraceSpansAnswerIsTheRuntimeAnswer(t *testing.T) {
|
||||
app := mounted(t)
|
||||
want := bare(t, fullTraceWindow())
|
||||
asked := logsRuntime(t, http.StatusOK, want)
|
||||
|
||||
status, got := call(t, app, member(http.MethodGet,
|
||||
"/v1/o11y/traces/tr-1?spanId=s1&levelUp=2&levelDown=3&spanRenderLimit=500", nil))
|
||||
if status != http.StatusOK {
|
||||
t.Fatalf("status=%d body=%s", status, got)
|
||||
}
|
||||
if string(got) != string(want) {
|
||||
t.Fatalf("op answered %s, runtime wrote %s", got, want)
|
||||
}
|
||||
r := *asked
|
||||
if r.Method != http.MethodGet || r.URL.Path != "/v1/o11y/traces/tr-1" {
|
||||
t.Fatalf("runtime asked %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
q := r.URL.Query()
|
||||
for name, value := range map[string]string{
|
||||
"spanId": "s1", "levelUp": "2", "levelDown": "3", "spanRenderLimit": "500",
|
||||
} {
|
||||
if q.Get(name) != value {
|
||||
t.Fatalf("query %s=%q want %q (full %q)", name, q.Get(name), value, r.URL.RawQuery)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A trace id is a path segment the router matched, and it goes on VERBATIM.
|
||||
// Re-encoding it here would hand the runtime a different id than the caller
|
||||
// named — the difference between the answer this face has always given and a
|
||||
// new one.
|
||||
func TestTraceIDGoesOnVerbatim(t *testing.T) {
|
||||
app := mounted(t)
|
||||
asked := logsRuntime(t, http.StatusOK, bare(t, fullTraceWindow()))
|
||||
|
||||
call(t, app, member(http.MethodGet, "/v1/o11y/traces/a.b-c_d", nil))
|
||||
if got := (*asked).URL.Path; got != "/v1/o11y/traces/a.b-c_d" {
|
||||
t.Fatalf("runtime saw %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTraceFieldCatalogAnswerIsTheRuntimeAnswer(t *testing.T) {
|
||||
app := mounted(t)
|
||||
want := bare(t, model.GetFieldsResponse{
|
||||
Selected: []model.Field{{Name: "service.name", DataType: "string", Type: "resource"}},
|
||||
Interesting: []model.Field{
|
||||
{Name: "http.status_code", DataType: "int64", Type: "tag"},
|
||||
{Name: "db.system", DataType: "string", Type: "tag"},
|
||||
},
|
||||
})
|
||||
asked := logsRuntime(t, http.StatusOK, want)
|
||||
|
||||
status, got := call(t, app, member(http.MethodGet, "/v1/o11y/traces/fields", nil))
|
||||
if status != http.StatusOK {
|
||||
t.Fatalf("status=%d body=%s", status, got)
|
||||
}
|
||||
if string(got) != string(want) {
|
||||
t.Fatalf("op answered %s, runtime wrote %s", got, want)
|
||||
}
|
||||
if p := (*asked).URL.Path; p != "/v1/o11y/traces/fields" {
|
||||
t.Fatalf("runtime asked %q — the literal must beat the :traceId param", p)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTraceFieldUpdateEchoesTheSetting(t *testing.T) {
|
||||
app := mounted(t)
|
||||
setting := model.UpdateField{
|
||||
Name: "http.status_code", DataType: "int64", Type: "tag",
|
||||
Selected: true, IndexType: "minmax", IndexGranularity: 4,
|
||||
}
|
||||
want := bare(t, setting)
|
||||
asked := logsRuntime(t, http.StatusOK, want)
|
||||
|
||||
status, got := call(t, app, member(http.MethodPost, "/v1/o11y/traces/fields",
|
||||
strings.NewReader(mustJSON(t, setting))))
|
||||
if status != http.StatusOK {
|
||||
t.Fatalf("status=%d body=%s", status, got)
|
||||
}
|
||||
if string(got) != string(want) {
|
||||
t.Fatalf("op answered %s, runtime wrote %s", got, want)
|
||||
}
|
||||
if r := *asked; r.Method != http.MethodPost || r.URL.Path != "/v1/o11y/traces/fields" {
|
||||
t.Fatalf("runtime asked %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
}
|
||||
|
||||
// The three detail views come off the apiserver, which answers the
|
||||
// {status,data} envelope. Each proves both halves: the answer is the runtime's
|
||||
// bytes, and the trace id reached the runtime on the path rather than in the
|
||||
// body.
|
||||
func TestTraceDetailViewsAnswerTheRuntimeAnswer(t *testing.T) {
|
||||
waterfall := spantypes.GettableWaterfallTrace{
|
||||
StartTimestampMillis: 1_760_000_000_000, EndTimestampMillis: 1_760_000_009_000,
|
||||
RootServiceName: "api", RootServiceEntryPoint: "GET /v1/thing",
|
||||
TotalSpansCount: 2, TotalErrorSpansCount: 1,
|
||||
Spans: []*spantypes.WaterfallSpan{{
|
||||
SpanID: "s1", Name: "GET /v1/thing", DurationNano: 1500000,
|
||||
HasError: true, KindString: "SPAN_KIND_SERVER", TraceID: "tr-1",
|
||||
Attributes: map[string]any{"http.method": "GET"},
|
||||
Resource: map[string]string{"service.name": "api"},
|
||||
References: []spantypes.OtelSpanRef{},
|
||||
Events: []spantypes.Event{},
|
||||
}},
|
||||
HasMissingSpans: false, UncollapsedSpans: []string{"s1"}, HasMore: false,
|
||||
}
|
||||
flame := spantypes.GettableFlamegraphTrace{
|
||||
Spans: [][]*spantypes.FlamegraphSpan{{{
|
||||
SpanID: "s1", ParentSpanID: "", Timestamp: 1_760_000_000_000,
|
||||
DurationNano: 1500000, HasError: true, Name: "GET /v1/thing", Level: 0,
|
||||
Events: []spantypes.Event{},
|
||||
Attributes: map[string]any{"http.method": "GET"},
|
||||
Resource: map[string]string{"service.name": "api"},
|
||||
}}},
|
||||
StartTimestampMillis: 1_760_000_000_000, EndTimestampMillis: 1_760_000_009_000,
|
||||
HasMore: false,
|
||||
}
|
||||
|
||||
for _, tc := range []struct {
|
||||
view string
|
||||
payload any
|
||||
body string
|
||||
}{
|
||||
{"waterfall", waterfall, `{"selectedSpanId":"s1","uncollapsedSpans":["s1"]}`},
|
||||
{"flamegraph", flame, `{"selectedSpanId":"s1"}`},
|
||||
} {
|
||||
t.Run(tc.view, func(t *testing.T) {
|
||||
app := mounted(t)
|
||||
want := rendered(t, http.StatusOK, tc.payload)
|
||||
asked := logsRuntime(t, http.StatusOK, want)
|
||||
|
||||
status, got := call(t, app, member(http.MethodPost,
|
||||
"/v1/o11y/traces/tr-1/"+tc.view, strings.NewReader(tc.body)))
|
||||
if status != http.StatusOK {
|
||||
t.Fatalf("status=%d body=%s", status, got)
|
||||
}
|
||||
if string(got) != string(want) {
|
||||
t.Fatalf("op answered %s, runtime wrote %s", got, want)
|
||||
}
|
||||
r := *asked
|
||||
if r.Method != http.MethodPost || r.URL.Path != "/v1/o11y/traces/tr-1/"+tc.view {
|
||||
t.Fatalf("runtime asked %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TWO ENCODERS, ONE VALUE — a divergence this test PINS rather than hides.
|
||||
//
|
||||
// The runtime renders with jsoniter (pkg/http/render, pkg/query-service/app);
|
||||
// zip renders a typed op's Out with the stdlib encoder. jsoniter does not
|
||||
// implement Go 1.24's `,omitzero`, so for any type that uses it — here
|
||||
// telemetrytypes.TelemetryFieldKey's signal/fieldContext/fieldDataType, and
|
||||
// sixteen other fields across pkg/types — the runtime WRITES the zero value and
|
||||
// a typed op OMITS the key. The value is the same; the bytes are not.
|
||||
//
|
||||
// This is not new with the traces face: every already-shipped op whose Out
|
||||
// reaches TelemetryFieldKey (querycore's field catalog, rules, the v5 builder)
|
||||
// has had it since it was typed. It is recorded here, on the one op that makes
|
||||
// it visible, so the next person finds a test instead of a surprise. The fix is
|
||||
// ONE encoder across both halves, which is a change to the runtime's rendering
|
||||
// and belongs in its own pass.
|
||||
func TestTraceAggregationsCarryTheRuntimeValue(t *testing.T) {
|
||||
app := mounted(t)
|
||||
aggs := spantypes.GettableTraceAggregations{
|
||||
Aggregations: []spantypes.SpanAggregationResult{{
|
||||
Field: telemetrytypes.TelemetryFieldKey{Name: "service.name", FieldContext: telemetrytypes.FieldContextResource},
|
||||
Aggregation: spantypes.SpanAggregationSpanCount,
|
||||
Value: map[string]uint64{"api": 2},
|
||||
}},
|
||||
}
|
||||
want := rendered(t, http.StatusOK, aggs)
|
||||
asked := logsRuntime(t, http.StatusOK, want)
|
||||
|
||||
status, got := call(t, app, member(http.MethodPost, "/v1/o11y/traces/tr-1/aggregations",
|
||||
strings.NewReader(`{"aggregations":[{"field":{"name":"service.name","fieldContext":"resource"},"aggregation":"span_count"}]}`)))
|
||||
if status != http.StatusOK {
|
||||
t.Fatalf("status=%d body=%s", status, got)
|
||||
}
|
||||
// Equal as VALUES: every field the runtime set survives the op.
|
||||
var runtimeValue, opValue map[string]any
|
||||
if err := json.Unmarshal(want, &runtimeValue); err != nil {
|
||||
t.Fatalf("runtime bytes: %v", err)
|
||||
}
|
||||
if err := json.Unmarshal(got, &opValue); err != nil {
|
||||
t.Fatalf("op bytes: %v", err)
|
||||
}
|
||||
stripZeroStrings(runtimeValue)
|
||||
if !reflect.DeepEqual(runtimeValue, opValue) {
|
||||
t.Fatalf("op answered %s, runtime wrote %s", got, want)
|
||||
}
|
||||
if r := *asked; r.Method != http.MethodPost || r.URL.Path != "/v1/o11y/traces/tr-1/aggregations" {
|
||||
t.Fatalf("runtime asked %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
}
|
||||
|
||||
// stripZeroStrings removes the empty-string keys jsoniter writes for `omitzero`
|
||||
// fields and the stdlib encoder leaves out. It is the exact shape of the
|
||||
// divergence above, spelled once.
|
||||
func stripZeroStrings(v map[string]any) {
|
||||
for k, val := range v {
|
||||
switch t := val.(type) {
|
||||
case string:
|
||||
if t == "" {
|
||||
delete(v, k)
|
||||
}
|
||||
case map[string]any:
|
||||
stripZeroStrings(t)
|
||||
case []any:
|
||||
for _, e := range t {
|
||||
if m, ok := e.(map[string]any); ok {
|
||||
stripZeroStrings(m)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The six routes are the six the runtime registers — no more, no fewer, at the
|
||||
// same methods. Route-table arithmetic is what caught the dark slices; it is
|
||||
// cheap and it is the only thing that catches a mount that never ran.
|
||||
func TestTracesRoutesAreTheSameSix(t *testing.T) {
|
||||
assertRoutes(t, map[string]bool{
|
||||
"GET /v1/o11y/traces/fields": true,
|
||||
"POST /v1/o11y/traces/fields": true,
|
||||
"GET /v1/o11y/traces/:traceId": true,
|
||||
"POST /v1/o11y/traces/:traceId/waterfall": true,
|
||||
"POST /v1/o11y/traces/:traceId/flamegraph": true,
|
||||
"POST /v1/o11y/traces/:traceId/aggregations": true,
|
||||
}, "/v1/o11y/traces")
|
||||
}
|
||||
+5266
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user