Documentation
¶
Overview ¶
Package audit emits one event per terminal gateway response — including 401s, 403s, and 429s — to configured sinks (stdout, webhook).
Index ¶
Constants ¶
const ( OutcomeDelivered = "delivered" OutcomeRetried = "retried" // an attempt failed and will be tried again OutcomeDeadLettered = "dead_lettered" // delivery abandoned, written to the dead-letter file OutcomeDropped = "dropped" // abandoned with nowhere to put it, or the buffer was full )
Delivery outcomes reported to the observer. Audit is the single exit door, so the interesting number is not how many events were written but how many were not: a sink that quietly drops is indistinguishable from a quiet system.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Event ¶
type Event struct {
Time time.Time `json:"time"`
Principal string `json:"principal,omitempty"` // subject, or "" when auth is disabled
Issuer string `json:"issuer,omitempty"`
Method string `json:"method"` // MCP method, e.g. "tools/call"
Name string `json:"name,omitempty"` // namespaced tool/prompt name
Upstream string `json:"upstream,omitempty"` // routed upstream id
// Direction is set only on the reverse path — "server_initiated", for the
// sampling and elicitation requests an upstream makes of the caller's
// client. Its absence means the ordinary client-to-upstream direction, so
// every event fold emitted before this field existed reads unchanged.
Direction string `json:"direction,omitempty"`
// Tenant is the group the principal resolved to, when tenancy is
// configured. Empty means no tenant matched — which is not an error, just
// a caller governed by the gateway-wide rules.
Tenant string `json:"tenant,omitempty"`
// Decision is "allow" | "deny" for policy-gated invocations; for
// oauth/token events it carries the exchange outcome verbatim
// ("minted", "replayed", "invalid_grant", ...) so a detected replay is
// alertable on a structured field.
Decision string `json:"decision,omitempty"`
RuleID string `json:"ruleId,omitempty"`
// MissingScopes names the scopes a denial said would have satisfied a
// rule that otherwise granted the invocation. Present only on denials
// where scopes were the sole obstacle, so a trail can distinguish "not
// authorized" from "not authorized yet" without re-deriving it.
MissingScopes []string `json:"missingScopes,omitempty"` // matching policy rule
Outcome Outcome `json:"outcome"`
Error string `json:"error,omitempty"`
LatencyMs int64 `json:"latencyMs"`
// UpstreamCalls is how many upstream invocations this one request cost.
// It is not always 1: a federated list fans out to every upstream, so
// this is where a cheap-looking client request shows its real price.
UpstreamCalls int `json:"upstreamCalls,omitempty"`
// ItemsServed is how many tools/prompts/resources a list returned after
// per-principal policy filtering — the size of the surface this caller
// was actually handed, which is what lands in a model's context.
ItemsServed int `json:"itemsServed,omitempty"`
// ItemsCapped counts list items a policy rule's maxItems dropped —
// distinct from items policy made invisible, because the remedies differ:
// one is a grant the caller does not have, the other a bound the operator
// set. The result's _meta says the same thing to the client.
ItemsCapped int `json:"itemsCapped,omitempty"`
// HookOutcome is what the external decision hook said: "allow", "deny",
// or "error". Present only when a hook inspected this request. The error
// case is the one worth alerting on: with onError "allow" it means the
// call proceeded uninspected, which a fail-open deployment would
// otherwise have no record of.
HookOutcome string `json:"hookOutcome,omitempty"`
// Usage carries counters an upstream published in its result `_meta`,
// verbatim. fold never synthesizes these; an absent field means the
// upstream reported nothing, not that nothing was consumed.
Usage map[string]any `json:"usage,omitempty"`
}
Event is one audit record.
type Logger ¶
type Logger struct {
// contains filtered or unexported fields
}
Logger fans events out to sinks. A nil *Logger drops everything.
func New ¶
New builds a logger from config. Absent config → no audit emission.
A sink that cannot be constructed — a file path that will not open — is reported and skipped rather than failing the gateway: losing one destination should not take the endpoint down, and the error is visible at startup.
func (*Logger) Close ¶ added in v1.9.0
func (l *Logger) Close()
Close releases sinks that hold resources (open files, delivery workers). Buffered events are flushed on a bounded best-effort basis; a shutdown that waits indefinitely on an unreachable webhook is worse than a lost tail.
func (*Logger) StartupErrors ¶ added in v1.9.0
StartupErrors reports sinks that could not be constructed, so the caller can log them. Empty when every configured sink was built.
type Observer ¶ added in v1.9.0
Observer is told the fate of events, by sink type and outcome (delivered, retried, dead_lettered, dropped). The gateway turns this into metrics: audit is the single exit door, so an event that never arrives has to be countable somewhere, or a silent sink is indistinguishable from a silent system.
type Option ¶ added in v1.9.0
type Option func(*Logger)
Option configures a Logger at construction.
func WithObserver ¶ added in v1.9.0
WithObserver reports delivery outcomes.
func WithPanicHook ¶ added in v1.11.0
WithPanicHook routes recovered delivery-worker panics into the caller's panic accounting, so this package's recoveries alert exactly like the gateway's own (fold_panics_total, the "panic recovered" log line). Absent, a recovered panic still writes stderr — never silent, merely unaggregated.
type Outcome ¶
type Outcome string
Outcome classifies how a request terminated.
const ( OutcomeOK Outcome = "ok" OutcomeError Outcome = "error" OutcomeDenied Outcome = "denied" OutcomeRateLimited Outcome = "rate_limited" OutcomeUnauthenticated Outcome = "unauthenticated" OutcomeUpstreamDown Outcome = "upstream_down" OutcomeForbidden Outcome = "forbidden" // host/origin rejected (DNS rebinding) // OutcomeBudgetExhausted is distinct from OutcomeRateLimited because the // remedy differs: a rate limit clears in seconds, an exhausted budget not // until the period rolls over. Collapsing them would make "we are being // throttled" and "we have spent the month" the same line in a SIEM. OutcomeBudgetExhausted Outcome = "budget_exhausted" // OutcomeWarned is a finding rather than a request outcome: fold noticed // something an operator should see and did not change what it served. // Definition drift is the first of these — see // docs/design-definition-pinning.md. It is deliberately not an error: the // request succeeded, and an SLO that counted findings as failures would // page someone for a working federation. OutcomeWarned Outcome = "warned" // OutcomeHookDenied is the external decision hook refusing a call, kept // distinct from OutcomeDenied because an operator reading the trail needs // to know whether their policy or their inspector said no — the remedies // live in different systems, and often in different teams. OutcomeHookDenied Outcome = "hook_denied" )
Outcomes mirror the terminal responses the gateway can produce; every audit event carries exactly one.