Documentation
¶
Overview ¶
Package metrics provides token usage metrics collection and aggregation.
Index ¶
- type Accumulator
- func (a *Accumulator) Clear()
- func (a *Accumulator) PromptUsageSnapshot() map[string]ToolStat
- func (a *Accumulator) Query(duration time.Duration) TimeSeriesResponse
- func (a *Accumulator) Record(serverName string, inputTokens, outputTokens int)
- func (a *Accumulator) RecordFormatSavings(serverName string, originalTokens, formattedTokens int)
- func (a *Accumulator) RecordPromptGet(name string)
- func (a *Accumulator) RecordReplica(serverName string, replicaID, inputTokens, outputTokens int)
- func (a *Accumulator) RecordReplicaWithClient(serverName string, replicaID int, clientID string, ...)
- func (a *Accumulator) RecordToolCall(serverName, toolName string)
- func (a *Accumulator) RecordToolCallUsage(serverName, toolName string, inputTokens, outputTokens int)
- func (a *Accumulator) ReplaySnapshot(serverName string, ts time.Time, inputTokens, outputTokens int64)
- func (a *Accumulator) Restore(perServer map[string]TokenCounts)
- func (a *Accumulator) RestorePromptUsage(perSkill map[string]ToolStat)
- func (a *Accumulator) RestoreToolUsage(perServer map[string]map[string]ToolStat)
- func (a *Accumulator) Snapshot() TokenUsage
- func (a *Accumulator) StartedAt() time.Time
- func (a *Accumulator) ToolUsageSnapshot() map[string]map[string]ToolStat
- type DataPoint
- type FormatSavings
- type Observer
- type TimeSeriesResponse
- type TokenCounts
- type TokenUsage
- type ToolStat
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Accumulator ¶
type Accumulator struct {
// contains filtered or unexported fields
}
Accumulator collects token usage metrics with thread-safe operations. Session totals use atomic counters. Historical data is stored in a ring buffer of pre-aggregated 1-minute time buckets.
func NewAccumulator ¶
func NewAccumulator(maxDataPoints int) *Accumulator
NewAccumulator creates a metrics accumulator with the given ring buffer capacity. Each slot holds one minute of aggregated data, so 10000 slots ≈ ~7 days.
func (*Accumulator) Clear ¶
func (a *Accumulator) Clear()
Clear resets all metrics — session totals, per-server totals, and history.
func (*Accumulator) PromptUsageSnapshot ¶
func (a *Accumulator) PromptUsageSnapshot() map[string]ToolStat
PromptUsageSnapshot returns a deep copy of the per-skill prompts/get call counters. Empty (nil) when no prompt has been served yet. Reuses the ToolStat value shape so the persistence and API layers share one type.
func (*Accumulator) Query ¶
func (a *Accumulator) Query(duration time.Duration) TimeSeriesResponse
Query returns historical time-series data for the given duration. For ranges > 6h, data points are downsampled to hourly buckets.
func (*Accumulator) Record ¶
func (a *Accumulator) Record(serverName string, inputTokens, outputTokens int)
Record adds a token usage observation from a tool call. Equivalent to RecordReplica with replicaID=-1 (i.e. do not attribute to a replica).
func (*Accumulator) RecordFormatSavings ¶
func (a *Accumulator) RecordFormatSavings(serverName string, originalTokens, formattedTokens int)
RecordFormatSavings records token counts before and after format conversion. Normal token usage tracking is handled separately by the ToolCallObserver; this method only tracks the format savings delta.
func (*Accumulator) RecordPromptGet ¶
func (a *Accumulator) RecordPromptGet(name string)
RecordPromptGet increments the call counter for a single skill (prompt) served via prompts/get and stamps the last-called timestamp. Powers the Skills Library "Never used" facet. An empty name is a no-op so callers without attribution can invoke unconditionally.
Kept parallel to RecordToolCall rather than reusing it: routing prompt serving through the tool-usage map would surface synthetic entries in Tools Audit Mode.
func (*Accumulator) RecordReplica ¶
func (a *Accumulator) RecordReplica(serverName string, replicaID, inputTokens, outputTokens int)
RecordReplica adds a token usage observation attributed to a specific replica. Per-server aggregates are updated in all cases. Pass replicaID < 0 to skip the per-replica update (used for servers that are not part of a replica set).
func (*Accumulator) RecordReplicaWithClient ¶
func (a *Accumulator) RecordReplicaWithClient(serverName string, replicaID int, clientID string, inputTokens, outputTokens int)
RecordReplicaWithClient is the client-aware variant of RecordReplica. It updates the per-client token counters in addition to session, per-server, and per-replica aggregates. An empty clientID skips the per-client update, matching the replicaID < 0 convention so callers without attribution can continue to use the same code path.
func (*Accumulator) RecordToolCall ¶
func (a *Accumulator) RecordToolCall(serverName, toolName string)
RecordToolCall increments per-(server, tool) call counters and stamps the last-called timestamp. Used by pkg/optimize's unused_tool heuristic.
An empty serverName or toolName is a no-op so callers without per-tool attribution (legacy ToolCallObserver path) can invoke unconditionally.
func (*Accumulator) RecordToolCallUsage ¶
func (a *Accumulator) RecordToolCallUsage(serverName, toolName string, inputTokens, outputTokens int)
RecordToolCallUsage is RecordToolCall plus the call's token counts: one bucket lookup increments the call counter, stamps the last-called timestamp, and adds input/output tokens, so the observer's hot path touches the tool-usage map once per call.
An empty serverName or toolName is a no-op so callers without per-tool attribution (legacy ToolCallObserver path) can invoke unconditionally.
func (*Accumulator) ReplaySnapshot ¶
func (a *Accumulator) ReplaySnapshot(serverName string, ts time.Time, inputTokens, outputTokens int64)
ReplaySnapshot adds a historical observation to the time-series ring buffers (aggregate + per-server) without touching cumulative counters. Used by telemetry.MetricsFlusher.SeedFromFile to rehydrate per-minute bucket history from each persisted Diff line — the chart shows pre-restart activity continuously alongside live data instead of resetting to a single post-restart point.
Cumulative counters are restored separately via Restore. Calling both with the same source data reproduces the on-disk state.
ts is bucketed to the minute via the same key the live Record path uses, so chronological replay produces one bucket per flush minute and live observations after replay continue advancing the same ring naturally.
func (*Accumulator) Restore ¶
func (a *Accumulator) Restore(perServer map[string]TokenCounts)
Restore replaces per-server token totals with the supplied map and recomputes session totals as the sum across all servers (matching the invariant Record/RecordReplica maintains). Used on daemon startup to repopulate cumulative counters from a persisted metrics.jsonl file.
Existing per-server counters are overwritten for any server present in the map; servers absent from the map retain their current state. Replicas and format-savings counters are not restored — those carry no on-disk equivalent in the snapshot format. Time-series ring buckets are populated separately via ReplaySnapshot.
func (*Accumulator) RestorePromptUsage ¶
func (a *Accumulator) RestorePromptUsage(perSkill map[string]ToolStat)
RestorePromptUsage seeds per-skill prompts/get counters from a persisted snapshot so usage history survives a gateway restart. Mirrors RestoreToolUsage: max-wins per counter (an existing in-memory value is kept when it already exceeds the restored one), entries with no recorded calls are skipped, and an empty map is a no-op.
func (*Accumulator) RestoreToolUsage ¶
func (a *Accumulator) RestoreToolUsage(perServer map[string]map[string]ToolStat)
RestoreToolUsage seeds per-(server, tool) call counters from a persisted snapshot so Audit Mode's usage history survives a gateway restart. Called on startup by telemetry.MetricsFlusher.SeedFromFile before the gateway serves traffic, so the counters it re-creates are the same *toolUsage buckets RecordToolCall increments afterward — live calls continue from the restored count rather than starting at zero.
Tool-call attribution flows through the same observer for direct and code-mode calls (Gateway.CallTool → HandleToolsCall → Observer → RecordToolCall), so a restored snapshot reflects both equally.
Restore is max-wins per counter — calls and tokens alike: an existing in-memory value is kept when it already exceeds the restored one (defensive against a seed racing late initialization; the counters are monotonic between resets, so max-wins never double-counts). Entries with no recorded calls are skipped so the snapshot stays sparse. An empty map is a no-op.
func (*Accumulator) Snapshot ¶
func (a *Accumulator) Snapshot() TokenUsage
Snapshot returns the current token usage summary.
func (*Accumulator) StartedAt ¶
func (a *Accumulator) StartedAt() time.Time
StartedAt returns the wall-clock time the accumulator was created. Clear does not reset this value — the start-of-observation window stays anchored to the gateway lifetime, which is what pkg/optimize uses to gate "<24h of data" findings.
func (*Accumulator) ToolUsageSnapshot ¶
func (a *Accumulator) ToolUsageSnapshot() map[string]map[string]ToolStat
ToolUsageSnapshot returns a deep copy of the per-(server, tool) call counters. Empty when no per-tool calls have been recorded (typical for gateways still on the legacy ToolCallObserver path).
type DataPoint ¶
type DataPoint struct {
Timestamp time.Time `json:"timestamp"`
InputTokens int64 `json:"input_tokens"`
OutputTokens int64 `json:"output_tokens"`
TotalTokens int64 `json:"total_tokens"`
}
DataPoint is a single time-series data point with token counts.
type FormatSavings ¶
type FormatSavings struct {
OriginalTokens int64 `json:"original_tokens"`
FormattedTokens int64 `json:"formatted_tokens"`
SavedTokens int64 `json:"saved_tokens"`
SavingsPercent float64 `json:"savings_percent"`
}
FormatSavings tracks token savings from output formatting.
type Observer ¶
type Observer struct {
// contains filtered or unexported fields
}
Observer implements mcp.ToolCallObserver and mcp.ClientObserver by counting tokens and recording them into an Accumulator.
func NewObserver ¶
func NewObserver(counter token.Counter, accumulator *Accumulator) *Observer
NewObserver creates a ToolCallObserver that counts tokens and records metrics.
func (*Observer) ObservePromptGet ¶
func (o *Observer) ObservePromptGet(obs mcp.PromptGetObservation)
ObservePromptGet records that a registry skill was served via prompts/get, incrementing its cumulative count and last-used timestamp in the parallel prompt-usage namespace. The token path does not apply: prompts are static content, not tool calls.
func (*Observer) ObserveToolCall ¶
func (o *Observer) ObserveToolCall(serverName string, replicaID int, arguments map[string]any, result *mcp.ToolCallResult)
ObserveToolCall counts input/output tokens and records them.
func (*Observer) ObserveToolCallWithClient ¶
func (o *Observer) ObserveToolCallWithClient(_ context.Context, obs mcp.ToolCallObservation) mcp.ToolCallSummary
ObserveToolCallWithClient is the ClientObserver entry point. It records the same tokens as ObserveToolCall, additionally attributes them to the supplied client, and returns a summary the gateway uses to populate OTel GenAI semantic span attributes without re-counting tokens.
type TimeSeriesResponse ¶
type TimeSeriesResponse struct {
Range string `json:"range"`
Interval string `json:"interval"`
Points []DataPoint `json:"data_points"`
PerServer map[string][]DataPoint `json:"per_server"`
}
TimeSeriesResponse is returned by the historical metrics endpoint.
type TokenCounts ¶
type TokenCounts struct {
InputTokens int64 `json:"input_tokens"`
OutputTokens int64 `json:"output_tokens"`
TotalTokens int64 `json:"total_tokens"`
}
TokenCounts holds input/output/total token counts.
type TokenUsage ¶
type TokenUsage struct {
Session TokenCounts `json:"session"`
PerServer map[string]TokenCounts `json:"per_server"`
PerReplica map[string]map[int]TokenCounts `json:"per_replica,omitempty"`
// PerClient groups token usage by the originating MCP client (for example
// "claude-code", "cursor"). The field is omitempty so consumers built
// before per-client attribution shipped continue to see the same JSON
// shape. Future per-user / per-team dimensions land as sibling fields
// (per_user, per_team) under this same shape rather than reshaping
// per_client.
PerClient map[string]TokenCounts `json:"per_client,omitempty"`
FormatSavings FormatSavings `json:"format_savings"`
}
TokenUsage is the top-level token usage snapshot returned by the API.
type ToolStat ¶
type ToolStat struct {
Calls int64 `json:"calls"`
LastCalledAt time.Time `json:"last_called_at,omitempty"`
InputTokens int64 `json:"input_tokens,omitempty"`
OutputTokens int64 `json:"output_tokens,omitempty"`
}
ToolStat is the snapshot shape for per-(server, tool) call tracking. Used by pkg/optimize to detect tools that have not seen any calls inside a freshness window and to attribute observed spend per tool. Calls is the cumulative count since the accumulator was created or last cleared; LastCalledAt is the wall-clock time the most recent call was recorded, or the zero value when no calls have been recorded. InputTokens/OutputTokens are the cumulative token counts of the tool's own calls. The token fields are omitempty so persisted lines written before per-tool attribution stay byte-identical.