Documentation
¶
Overview ¶
Package metrics provides OpenTelemetry metrics instrumentation for generative AI operations.
This package simplifies the process of tracking token usage and tool calls across AI model interactions. It provides a unified metrics interface that works with any AI model provider (Claude, Gemini, OpenAI, etc.) using OpenTelemetry for observability.
Overview ¶
The metrics package offers the following key features:
- Token usage tracking (prompt and completion tokens)
- Tool call counting with model and tool name dimensions
- Automatic attribute enrichment from execution context (repository, turn, etc.)
- Graceful degradation when metric creation fails
- Thread-safe operations
Basic Usage ¶
Create a GenAI metrics instance and record token usage:
// Create metrics instance with a unified meter name
m := metrics.NewGenAI("chainguard.ai.agents")
// Record token usage for a model interaction
m.RecordTokens(ctx, "claude-3-sonnet", 150, 250)
// Record a tool call
m.RecordToolCall(ctx, "claude-3-sonnet", "read_file")
Attribute Enrichment ¶
Contextual attributes (reconciler_type, repository, turn, etc.) are automatically extracted from the execution context propagated via context.Context using agenttrace.GetExecutionContext. No manual enricher setup is needed.
Graceful Degradation ¶
The package handles metric creation failures gracefully. If a counter cannot be created, a warning is logged and a no-op counter is used instead:
// Even if OpenTelemetry is not configured, this won't panic
m := metrics.NewGenAI("chainguard.ai.agents")
m.RecordTokens(ctx, "claude-3-sonnet", 100, 200) // Safe to call
Thread Safety ¶
All functions and methods in this package are thread-safe. The GenAI struct can be safely shared across goroutines for concurrent metric recording.
Integration with AI Executors ¶
This package is designed to work with various AI executor implementations:
Claude executor example:
m := metrics.NewGenAI("chainguard.ai.agents")
// After each Claude API call
m.RecordTokens(ctx, "claude-3-sonnet",
response.Usage.InputTokens,
response.Usage.OutputTokens)
for _, toolUse := range response.Content {
if toolUse.Type == "tool_use" {
m.RecordToolCall(ctx, "claude-3-sonnet", toolUse.Name)
}
}
Google Gemini executor example:
m := metrics.NewGenAI("chainguard.ai.agents")
// After each Gemini API call
m.RecordTokens(ctx, "gemini-pro",
response.UsageMetadata.PromptTokenCount,
response.UsageMetadata.CandidatesTokenCount)
Metric Names ¶
The package emits the following OpenTelemetry metrics:
Custom metrics:
- genai.token.prompt: Counter of prompt tokens used (unit: {tokens})
- genai.token.completion: Counter of completion tokens used (unit: {tokens})
- genai.tool.calls: Counter of tool invocations (unit: {calls})
OpenTelemetry GenAI semantic convention metric:
- gen_ai.client.token.usage: Histogram of token counts with gen_ai.token.type dimension (unit: {token})
All metrics include a "model" attribute. Tool call metrics also include a "tool" attribute. The semconv histogram also includes gen_ai.operation.name and gen_ai.request.model attributes. Callers should pass gen_ai.provider.name via the attrs parameter.
Custom Attributes ¶
Additional attributes can be passed to recording methods:
m.RecordTokens(ctx, "claude-3-sonnet", 100, 200,
attribute.String("agent", "code-reviewer"),
attribute.Int("turn", 3))
m.RecordToolCall(ctx, "claude-3-sonnet", "edit_file",
attribute.String("file_type", "go"))
Index ¶
- type GenAI
- func (m *GenAI) RecordAPIRequest(ctx context.Context, model, responseCode string, attrs ...attribute.KeyValue)
- func (m *GenAI) RecordCacheTokens(ctx context.Context, model string, cacheRead, cacheCreation int64, ...)
- func (m *GenAI) RecordTokens(ctx context.Context, model string, promptTokens, completionTokens int64, ...)
- func (m *GenAI) RecordToolCall(ctx context.Context, model, toolName string, attrs ...attribute.KeyValue)
- func (m *GenAI) RecordTurns(ctx context.Context, model string, turns int, limitExceeded bool, ...)
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type GenAI ¶
type GenAI struct {
// contains filtered or unexported fields
}
GenAI provides OpenTelemetry metrics for generative AI operations. It includes counters for token usage (prompt and completion), tool calls, and prompt cache metrics, with support for graceful degradation if metric creation fails.
Metrics are emitted in both custom format (genai.token.prompt, genai.token.completion) and OpenTelemetry GenAI semantic conventions (gen_ai.client.token.usage histogram with gen_ai.token.type dimension). See: https://opentelemetry.io/docs/specs/semconv/gen-ai/gen-ai-metrics/
Example (MultipleModels) ¶
ExampleGenAI_multipleModels demonstrates tracking metrics across different models.
package main
import (
"context"
"fmt"
"chainguard.dev/driftlessaf/agents/metrics"
)
func main() {
ctx := context.Background()
// Use a single metrics instance for all models
// The model name serves as a dimension to differentiate between them
m := metrics.NewGenAI("chainguard.ai.agents")
// Track Claude usage
m.RecordTokens(ctx, "claude-3-sonnet", 150, 250)
m.RecordToolCall(ctx, "claude-3-sonnet", "read_file")
// Track Gemini usage
m.RecordTokens(ctx, "gemini-pro", 100, 300)
m.RecordToolCall(ctx, "gemini-pro", "search_codebase")
// Track GPT usage
m.RecordTokens(ctx, "gpt-4", 200, 400)
fmt.Println("Multi-model metrics recorded")
}
Output: Multi-model metrics recorded
func NewGenAI ¶
NewGenAI creates a new GenAI metrics instance with the specified meter name. Uses graceful degradation: if any metric counter fails to initialize, logs a warning and uses a no-op counter instead of failing entirely.
The meterName should be unified across all agent executors (e.g., "chainguard.ai.agents") with the model name serving as a dimension on the recorded metrics to differentiate between different models (Claude, Gemini, etc.).
Example ¶
ExampleNewGenAI demonstrates creating a new GenAI metrics instance.
package main
import (
"fmt"
"chainguard.dev/driftlessaf/agents/metrics"
)
func main() {
// Create a metrics instance with a unified meter name
// The meter name should be consistent across all agent executors
m := metrics.NewGenAI("chainguard.ai.agents")
// The metrics instance is ready to use
fmt.Printf("Metrics instance created: %T\n", m)
}
Output: Metrics instance created: *metrics.GenAI
func (*GenAI) RecordAPIRequest ¶ added in v0.7.0
func (m *GenAI) RecordAPIRequest(ctx context.Context, model, responseCode string, attrs ...attribute.KeyValue)
RecordAPIRequest counts a single LLM API attempt. Each retry inside an in-process retry loop should call this once with the responseCode it observed — successes pass "200", rate-limit failures pass "429", overloaded failures pass "529", and errors that don't carry an HTTP status pass "unknown". This shape mirrors serviceruntime.googleapis.com/api/request_count's response_code label so PromQL on this metric reads the same as MQL on the GCP-side equivalent.
Example ¶
ExampleGenAI_RecordAPIRequest demonstrates counting LLM API attempts by response code.
package main
import (
"context"
"fmt"
"chainguard.dev/driftlessaf/agents/metrics"
"go.opentelemetry.io/otel/attribute"
)
func main() {
ctx := context.Background()
m := metrics.NewGenAI("chainguard.ai.agents")
// Record a successful API call
m.RecordAPIRequest(ctx, "gemini-2.5-flash", "200")
// Record a rate-limited API call with provider attribution
m.RecordAPIRequest(ctx, "gemini-2.5-flash", "429",
attribute.String("gen_ai.provider.name", "gcp.vertex_ai"))
// Record an error without an HTTP status
m.RecordAPIRequest(ctx, "claude-sonnet-4", "unknown")
fmt.Println("API request metrics recorded")
}
Output: API request metrics recorded
func (*GenAI) RecordCacheTokens ¶ added in v0.2.0
func (m *GenAI) RecordCacheTokens(ctx context.Context, model string, cacheRead, cacheCreation int64, attrs ...attribute.KeyValue)
RecordCacheTokens records Anthropic prompt cache token usage on the existing prompt tokens counter using gen_ai.token.type dimension values "cache_read" and "cache_creation". This aligns with the OpenTelemetry GenAI semantic conventions pattern from gen_ai.client.token.usage (see #35633).
cacheRead: tokens served from cache (cheap — 0.1x base input price). cacheCreation: tokens written to cache (1.25x base input price, amortized over reads).
A healthy caching setup shows high cache_read and low/zero cache_creation after the first turn.
func (*GenAI) RecordTokens ¶
func (m *GenAI) RecordTokens(ctx context.Context, model string, promptTokens, completionTokens int64, attrs ...attribute.KeyValue)
RecordTokens records prompt and completion token usage. Enriches attributes from the execution context propagated via context.Context.
Example ¶
ExampleGenAI_RecordTokens demonstrates recording token usage.
package main
import (
"context"
"fmt"
"chainguard.dev/driftlessaf/agents/metrics"
"go.opentelemetry.io/otel/attribute"
)
func main() {
ctx := context.Background()
m := metrics.NewGenAI("chainguard.ai.agents")
// Record token usage from an AI model response
// Parameters: context, model name, prompt tokens, completion tokens
m.RecordTokens(ctx, "claude-3-sonnet", 150, 250)
// Record with additional custom attributes
m.RecordTokens(ctx, "gemini-pro", 100, 200,
attribute.String("agent", "code-reviewer"),
attribute.Int("turn", 1))
fmt.Println("Token metrics recorded")
}
Output: Token metrics recorded
func (*GenAI) RecordToolCall ¶
func (m *GenAI) RecordToolCall(ctx context.Context, model, toolName string, attrs ...attribute.KeyValue)
RecordToolCall records a tool invocation. Enriches attributes from the execution context propagated via context.Context.
Example ¶
ExampleGenAI_RecordToolCall demonstrates recording tool invocations.
package main
import (
"context"
"fmt"
"chainguard.dev/driftlessaf/agents/metrics"
"go.opentelemetry.io/otel/attribute"
)
func main() {
ctx := context.Background()
m := metrics.NewGenAI("chainguard.ai.agents")
// Record a tool call with model and tool name
m.RecordToolCall(ctx, "claude-3-sonnet", "read_file")
// Record with additional custom attributes
m.RecordToolCall(ctx, "claude-3-sonnet", "edit_file",
attribute.String("file_type", "go"),
attribute.String("operation", "modify"))
fmt.Println("Tool call metrics recorded")
}
Output: Tool call metrics recorded
func (*GenAI) RecordTurns ¶ added in v0.3.0
func (m *GenAI) RecordTurns(ctx context.Context, model string, turns int, limitExceeded bool, attrs ...attribute.KeyValue)
RecordTurns records the number of LLM round-trips consumed by an agent execution. It always records the turns histogram and, when limitExceeded is true, also increments the turn_limit_exceeded counter. Call this on every exit path of the executor conversation loop so p50/p95/p99 distributions are observable per service.