Skip to content

OpenTelemetry Collector

SideSeat includes a built-in OpenTelemetry collector optimized for local AI development workflows. It receives OTLP traces via HTTP and gRPC, stores them locally (DuckDB + SQLite), and provides real-time streaming via SSE.

  • OTLP-compatible: Receives traces via standard OpenTelemetry protocol (HTTP JSON/Protobuf, gRPC)
  • Framework detection: Automatically detects Strands, LangGraph, Vercel AI SDK, Google ADK, Claude Agent SDK, and other AI frameworks
  • GenAI field extraction: Extracts token usage, model info, and other GenAI-specific fields
  • Bounded memory: Configurable buffer limits prevent memory exhaustion
  • FIFO storage: Automatic cleanup when storage limits are reached
  • Real-time streaming: SSE endpoint for live trace updates
  • Efficient storage: DuckDB + SQLite with indexed columns for fast queries
EndpointMethodContent-TypeDescription
/otel/{project_id}/v1/tracesPOSTapplication/jsonOTLP JSON traces
/otel/{project_id}/v1/tracesPOSTapplication/x-protobufOTLP Protobuf traces
localhost:4317gRPCProtobufOTLP gRPC endpoint
EndpointMethodDescription
/api/v1/project/{project_id}/otel/tracesGETList traces with filtering
/api/v1/project/{project_id}/otel/traces/filter-optionsGETGet available filter options
/api/v1/project/{project_id}/otel/traces/{trace_id}GETGet single trace details
/api/v1/project/{project_id}/otel/tracesDELETEDelete traces (batch) — JSON body {"trace_ids": ["..."]}
/api/v1/project/{project_id}/otel/traces/{trace_id}/spansGETGet spans for a trace
/api/v1/project/{project_id}/otel/spansGETQuery spans with GenAI fields
/api/v1/project/{project_id}/otel/traces/{trace_id}/spans/{span_id}GETGet span detail with events
/api/v1/project/{project_id}/otel/traces/{trace_id}/spans/{span_id}/messagesGETGet normalized span messages
/api/v1/project/{project_id}/otel/sessionsGETList sessions with filtering
/api/v1/project/{project_id}/otel/sessions/{session_id}GETGet single session details
/api/v1/project/{project_id}/otel/sessionsDELETEDelete sessions (batch)
/api/v1/project/{project_id}/otel/sessions/filter-optionsGETGet available filter options
EndpointMethodDescription
/api/v1/project/{project_id}/otel/sseGETSSE stream of trace events

All OTel settings are under the otel key in your config file:

{
"otel": {
"grpc": {
"enabled": true,
"port": 4317
},
"retention": {
"max_age_minutes": 10080,
"max_spans": 5000000
}
}
}

See Config Manager for the full configuration reference.

from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
# Configure exporter to send to SideSeat
exporter = OTLPSpanExporter(endpoint="http://localhost:5388/otel/default/v1/traces")
provider = TracerProvider()
provider.add_span_processor(BatchSpanProcessor(exporter))
trace.set_tracer_provider(provider)
# Create traces
tracer = trace.get_tracer(__name__)
with tracer.start_as_current_span("my-agent-operation"):
# Your agent code here
pass
from strands import Agent
from strands.models import BedrockModel
from strands.telemetry import StrandsTelemetry
# Configure telemetry to export to SideSeat
telemetry = StrandsTelemetry()
telemetry.setup_otlp_exporter(endpoint="http://localhost:5388/otel/default/v1/traces")
# Create agent with optional trace attributes
model = BedrockModel(model_id="us.anthropic.claude-sonnet-4-5-20250929-v1:0")
agent = Agent(
name="my-agent",
model=model,
trace_attributes={
"session.id": "my-session-123",
"user.id": "user-456",
},
)
# Don't forget to flush telemetry before exit
# telemetry.tracer_provider.force_flush()
const { NodeTracerProvider } = require('@opentelemetry/sdk-trace-node');
const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-http');
const { BatchSpanProcessor } = require('@opentelemetry/sdk-trace-base');
const exporter = new OTLPTraceExporter({
url: 'http://localhost:5388/otel/default/v1/traces',
});
const provider = new NodeTracerProvider();
provider.addSpanProcessor(new BatchSpanProcessor(exporter));
provider.register();

For higher throughput, use the gRPC endpoint:

from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
exporter = OTLPSpanExporter(endpoint="localhost:4317", insecure=True)

SideSeat automatically detects and normalizes spans from popular AI frameworks:

Detection uses the span name, span attributes, and resource attributes (notably service.name). The instrumentation scope name is not consulted.

FrameworkDetection MethodExtracted Fields
Strandsservice.name, gen_ai.system, span nameCycle ID, agent info
Vercel AI SDKai.* attributesModel, tokens, telemetry
LangGraphlanggraph.* attributes, span nameNode, edge, state
LangChainlangchain.* / langsmith.* attributesChain type, run ID
CrewAIcrew_* attributes, service.nameCrew, agent, task
AutoGenautogen.* attributes, span nameAgent name, chat round
Google ADKgoogle.adk.* / gcp.vertex.agent.* attributesAgent name, model
OpenAI Agentsopenai.agents.*, service.nameAgent name, model
Claude Agent SDKclaude_code.* span names, service.nameModel, tokens, messages
Microsoft Agent FrameworkGenAI semantic conventionsModel, tokens, tool calls
Google Vertex AIvertexai.* span namesModel, tokens, tool calls
OpenInferenceAttribute prefixSession ID, user ID
Generic GenAIgen_ai.* attributesModel, tokens, system

The collector extracts and normalizes GenAI-specific fields:

FieldDescription
gen_ai_systemAI provider (openai, anthropic, etc.)
gen_ai_request_modelRequested model name
gen_ai_response_modelActual model used
gen_ai_operation_nameOperation type (chat, completion)
gen_ai_agent_nameAgent name (for agent frameworks)
gen_ai_tool_nameTool name (for tool calls)
gen_ai_usage_input_tokensInput/prompt tokens
gen_ai_usage_output_tokensOutput/completion tokens
gen_ai_usage_total_tokensTotal tokens (computed if not provided)
gen_ai_usage_cache_read_tokensCache read tokens (Anthropic)
gen_ai_usage_cache_write_tokensCache write tokens (Anthropic)
gen_ai_usage_reasoning_tokensReasoning tokens
gen_ai_cost_input / gen_ai_cost_output / gen_ai_cost_totalComputed cost, USD
gen_ai_server_ttft_msTime to first token (TTFT)
gen_ai_server_request_duration_msTotal request duration
session_idSession/conversation ID

These are the normalized storage field names. The trace and span query responses return shorter aliases for the same values — input_tokens, output_tokens, total_tokens, cache_read_tokens, cache_write_tokens, reasoning_tokens, input_cost, output_cost, total_cost, model, agent_name, duration_ms.

Span events (messages, tool calls, choices) are automatically categorized:

Event TypeRoleDescription
user_messageuserUser input messages
assistant_messageassistantModel responses
system_messagesystemSystem prompts
tool_callassistantTool/function calls
tool_resulttoolTool execution results
choiceassistantCompletion choices with finish_reason

Message content is not returned on the span record itself. Use the dedicated messages endpoints (/traces/{id}/messages, /spans/{trace_id}/{span_id}/messages) for normalized message content, or ?include_raw_span=true for the untouched OTLP span with its events and attributes. Span records carry only the truncated input_preview and output_preview strings for list display.

Trace data is stored locally with DuckDB for analytics and SQLite for app metadata. Full span data is preserved as JSON for complete access to all fields.

Storage is managed with optional retention limits:

  • Time-based: If retention.max_age_minutes is set, data older than that is deleted. No default (disabled unless configured).
  • Volume-based: retention.max_spans limits the number of stored spans. Default: 5,000,000. Oldest spans are deleted first.

Subscribe to span events via Server-Sent Events:

const eventSource = new EventSource(
'http://localhost:5388/api/v1/project/default/otel/sse'
);
eventSource.addEventListener('span', (event) => {
const payload = JSON.parse(event.data);
console.log('Span event:', payload);
});
  • Maximum connections: 100 (configurable)
  • Connection timeout: 1 hour (configurable)
  • Keepalive interval: 30 seconds (configurable)

Use the filters query parameter on list endpoints to filter by attributes and fields. Pass a JSON array of filter objects (URL-encoded).

Filters are a JSON array passed in the filters query parameter. Each entry is tagged by type and carries a column, an operator, and (except for null) a value. Operators are SQL-shaped literals, not names — "=", not "eq".

typeOperatorsValue
string=, contains, starts_with, ends_withstring
number=, >, <, >=, <=number
datetime>, <, >=, <=RFC 3339 string
string_optionsany of, none ofarray of strings
boolean=, <>boolean
nullis null, is not nullomitted

Each endpoint allows its own set of columns and rejects the rest with INVALID_FILTER_COLUMN. Notably the timestamp column differs: /traces and /sessions use start_time / end_time, while /spans also accepts timestamp_start / timestamp_end.

Terminal window
# Traces slower than 1s
curl -G "http://localhost:5388/api/v1/project/default/otel/traces" \
--data-urlencode 'filters=[{"type":"number","column":"duration_ms","operator":">","value":1000}]'
# Traces from either framework, since a given date
curl -G "http://localhost:5388/api/v1/project/default/otel/traces" \
--data-urlencode 'filters=[
{"type":"string_options","column":"framework","operator":"any of","value":["StrandsAgents","ClaudeAgentSDK"]},
{"type":"datetime","column":"start_time","operator":">=","value":"2026-01-01T00:00:00Z"}
]'
# Spans that belong to a session
curl -G "http://localhost:5388/api/v1/project/default/otel/spans" \
--data-urlencode 'filters=[{"type":"null","column":"session_id","operator":"is not null"}]'
Terminal window
curl http://localhost:5388/api/v1/project/default/otel/traces
Terminal window
# Traces where environment = production
curl -G "http://localhost:5388/api/v1/project/default/otel/traces" \
--data-urlencode 'filters=[{"type":"string","column":"environment","operator":"=","value":"production"}]'

Entries in the array are combined with AND.

Terminal window
curl -G "http://localhost:5388/api/v1/project/default/otel/traces" \
--data-urlencode 'filters=[
{"type":"string","column":"environment","operator":"=","value":"production"},
{"type":"string","column":"user_id","operator":"=","value":"user-123"}
]'
Terminal window
curl http://localhost:5388/api/v1/project/default/otel/traces/abc123def456

Discover available filter values for building UI dropdowns:

Terminal window
curl http://localhost:5388/api/v1/project/default/otel/traces/filter-options

The response is a single options map from filterable column name to the values present, each with an occurrence count:

{
"options": {
"trace_name": [
{ "value": "invoke_agent Strands Agents", "count": 87 },
{ "value": "chat global.anthropic.claude-haiku-4-5", "count": 104 }
],
"environment": [{ "value": "production", "count": 42 }],
"session_id": [],
"user_id": [],
"tags": []
}
}
  1. Check OTel is enabled: "otel": { "enabled": true }
  2. Verify endpoint URL matches your exporter configuration
  3. Check server logs for ingestion errors

Ingestion buffers are sized with environment variables. There is no otel.ingestion section in the config file — unknown keys there are silently ignored, so setting them has no effect.

Terminal window
# In-memory ingest buffer per topic, in bytes (default: 104857600 = 100 MB)
SIDESEAT_TOPIC_BUFFER_SIZE=5242880
# Max queued messages per topic channel (default: 100000)
SIDESEAT_TOPIC_CHANNEL_CAPACITY=500
  1. Reduce retention.max_age_minutes for a shorter retention period
  2. The local database will be cleaned up automatically based on retention settings