View on GitHub

Lightspeed Core Stack

Lightspeed Core Stack

OpenTelemetry Tracing Design

   
Date 2026-04-08
Component lightspeed-stack
Authors Andrej Šimurka
Feature / Initiative LCORE-322
Spike LCORE-2655
Links Spike doc: docs/design/observability-opentelemetry/observability-opentelemetry-spike.md

What

Request tracing for Lightspeed Core using the OpenTelemetry Python SDK.

It provides:

When tracing is off (OTEL_SDK_DISABLED=true or exporter env not set), no spans are exported. Application-level manual span creation should remain a no-op when the SDK is disabled.

Why

Request tracing provides visibility into how requests flow through LCORE, enabling operators and developers to understand system behavior in production.

Without tracing, it is difficult to:

By introducing OpenTelemetry-based tracing, LCORE enables:

This improves observability, reduces time to diagnose issues, and aligns LCORE with modern cloud-native monitoring practices.

Requirements

R1 – Tracing support
LCORE shall support request tracing for all requests, producing telemetry compatible with OpenTelemetry.

R2 – Configuration
Tracing shall be configurable at deployment time. The effective tracing configuration shall be inspectable at runtime so operators can verify the running setup without hunting through separate deployment manifests (secret values shall be redacted).

R3 – Trace continuation
It shall be possible to continue an upstream trace when a calling service already started one, and to disable that behavior so LCORE starts a standalone trace per request. The configuration shall be documented.

R4 – Session grouping
LCORE shall group all traces for a user conversation into a session container, so that multiple user prompts / model turns within the same conversation can be correlated and analyzed together. Each session shall carry a unique conversation identifier, an anonymized user id and other relevant attributes.

R5 – LCORE-owned span tree
LCORE shall emit the prescribed multi-span trace per request from its own pipeline summary objects. LCORE shall not merge backend-exported spans into the trace.

R6 – Coverage
Tracing shall cover the full request lifecycle, including key stages such as request handling, LLM calls, RAG retrieval, conversation management, and shield moderation.

R7 – Semantic conventions and data handling
Spans and their attributes shall follow OpenTelemetry semantic conventions and avoid capturing sensitive or high-volume data.

R8 – Lifecycle management
Tracing shall be properly initialized and shut down with the application, ensuring all data is flushed on shutdown.

R9 – Resilience
Tracing failures must not impact request processing or user-facing behavior.

R10 – Documentation
The feature shall include documentation describing how to enable tracing, configure required environment variables, and verify correct behavior.

Use Cases

U1
As an SRE, I want LCORE to export traces to my OTLP endpoint, so that I can monitor and alert consistently with other services.

U2
As a platform engineer, I want upstream W3C trace context honored by default, with the option to disable it, so that gateway-started traces continue through LCORE when needed.

U3
As a developer, I want spans for RAG, LLM, tools, and shields, so that I can localize latency and errors without storing high volume data in the trace backend.

U4
As an administrator, I want tracing configurable at deploy time and the effective settings visible for inspection at runtime, so I can verify the running setup without hunting through separate deployment manifests.

U5
As an SRE, I want each pipeline step (retrieval, tool call, generation, etc.) as its own LCORE span with consistent naming and metadata, without depending on backend trace export or cross-service propagation.

U6
As a developer, I want remote and in-process backend integrations to produce the same trace shape from LCORE’s perspective.

Architecture

Chosen approach (spike decisions)

Spike decision Choice
1 — Configuration Environment-first (OTEL_*; no LCORE YAML block); /config scrapes env
2 — SDK initialization opentelemetry-instrument
3 — Inbound trace context Default W3C propagators; OTEL_PROPAGATORS=none to disable
4 — Outbound to backends LCORE-owned multi-span tree; no outbound propagation
5 — Export topology OTLP to a configurable endpoint only; collector deployment out of scope (operator choice)
6 — Span filtering Downstream in operator-managed collector or backend pipeline

Overview

Clients send requests to LCORE, which builds a structured span tree from internal pipeline summaries. External backends are not represented by their own exported spans. LCORE exports traces via OTLP to a configured trace backend for monitoring.

Tracing boundary

LCORE exports a single coherent trace per inbound request. External dependencies (inference backends, MCP servers, databases) are implementation details - their work is reflected only in LCORE-constructed step spans.

Caller ──(HTTP, optional traceparent/tracestate)──► LCORE FastAPI (root span)
                                                        │
                                                        ├─► validation, conversation management, shields
                                                        ├─► llm_inference
                                                        │       ├─► rag_retrieval
                                                        │       ├─► tool_execution
                                                        │       └─► response_generation
                                                        └─► conversation persistence, quota, etc.

LCORE: TracerProvider ──► OTLP exporter ──► (optional) Collector ──► trace backend

External backends: not merged into the LCORE span tree; optional separate OTel export

Configuration and SDK initialization

Spike Decision 1 (environment-first) and Decision 2 (opentelemetry-instrument).

All tracing configuration uses OTEL_* environment variables at process launch. LCORE defines no YAML block for tracing.

LCORE starts with opentelemetry-instrument, which initializes the SDK from OTEL_* before application code runs and auto-instruments supported libraries. The application does not construct or configure the SDK. Use OTEL_SDK_DISABLED=true as a process-wide kill switch.

/config visibility: GET /v1/config handler reads relevant OTEL_* variables and appends them under observability.otel (secrets redacted).

Inbound W3C trace context

Spike Decision 3 (default propagators).

Use standard OpenTelemetry propagators via OTEL_PROPAGATORS (default includes W3C tracecontext). FastAPI auto-instrumentation extracts traceparent on incoming requests. Applies to inbound LCORE HTTP requests only.

To disable inbound propagation, set OTEL_PROPAGATORS=none.

LCORE-owned span tree

Spike Decision 4 (LCORE-owned spans; no outbound propagation).

External backend interactions are implementation details from a tracing perspective. LCORE does not inject W3C trace context on outbound backend calls and does not merge backend-exported spans into the trace.

Instead, LCORE constructs the full span tree per request from internal pipeline summary objects - structures that accumulate timings, inputs/outputs, retrieved sources, and tool-call records as the request is handled. Each prescribed step becomes its own span (e.g. retrieval, each tool invocation, response generation).

with tracer.start_as_current_span("backend.inference") as span:
    span.set_attribute("backend.operation", "inference")
    span.set_attribute("llm.model.id", model_id)
    # ... invoke backend client ...
    span.add_event("llm.response.completed")
    span.set_attribute("llm.usage.input_tokens", ...)

Export topology

Spike Decision 5.

LCORE’s responsibility is only to export OTLP telemetry to the configured endpoint (OTEL_EXPORTER_OTLP_ENDPOINT and related OTEL_* variables).

What exists behind that endpoint is out of scope for this feature and is an infrastructure/operator decision. The endpoint may point directly to an OTLP-compatible backend (e.g. LangFuse) or to an OpenTelemetry Collector, which can perform fan-out, filtering, or export to additional destinations. Deployment and configuration of any collector or downstream telemetry infrastructure are managed outside of LCORE for now.

Span filtering

Spike Decision 6.

LCORE emits all spans defined in this specification. Filtering, sampling, scrubbing, or tail sampling is applied downstream in the collector or backend. LCORE does not provide per-span or per-span-group enable flags.

Span coverage

Recommended candidate spans, grouped by functional category. Each logical operation is represented by one parent span, with child spans for underlying pipeline steps—populated only from LCORE summary objects, not from backend-exported traces.

Shared inference pipeline

Covers core request handling and LLM processing (POST /v1/query, /streaming_query, /responses, /infer).

Span Place Description Key Attributes Key Events
MCP OAuth probe utils.mcp_oauth_probe.check_mcp_auth Validate MCP-related auth before backend calls mcp.auth.probe.ok mcp.auth.probe.finished
Quota gate utils.quota.check_tokens_available Enforce token quota before work quota.check.passed
Request validation Various validators Validate overrides & attachments request.attachments.count, llm.model.id, llm.provider.id validation.completed
Shield utils.shields.run_shield_moderation now (shields will be agent capabilities in the future) Apply input/output shields shield.result shield.rejected, pii.detected
llm.inference (parent) utils.agents.query.retrieve_agent_response; utils.agents.streaming.retrieve_agent_response_generator, agent_response_generator Orchestrate backend invoke and post-process span duration → response time llm.inference.started, llm.inference.completed
rag.retrieve (child) utils.agents.tool_processor.process_native_tool_result (FileSearchTool), summarize_file_search_result, rag_chunks_from_file_search_results Retrieve context for the turn rag.input; rag.sources.count, rag.sources[] rag.retrieval.completed
tool.execute (child) utils.agents.tool_processor.process_function_tool_call, process_native_tool_call, process_function_tool_result, process_native_tool_result; utils.agents.streaming.dispatch_stream_event Execute tools for the turn (one span per tool call) tool.calls.count, tool.calls.names tool.execution.completed
skill.activate (child) utils.pydantic_ai.build_agent (_skills_capability, _agent_capabilities) Skills selected for the turn skill.activations skill.activated
response.generate (child) utils.agents.query.build_turn_summary_from_agent_run, extract_agent_token_usage; utils.agents.streaming._process_token, dispatch_stream_event (AgentRunResultEvent) Generate assistant response llm.usage.input_tokens, llm.usage.output_tokens, llm.stream, llm.response llm.response.completed, turn.persisted

Streaming pipeline spans

For streaming endpoints (/streaming_query, /responses) and async tasks.

Span Place Description Key Attributes Key Events
SSE stream lifecycle Async generators in streaming_query.py / responses.py Bind stream to trace stream.conversation.id; span duration → response time stream.first_delta, stream.completed, stream.error
MCP tool in stream Stream parsers / MCP handlers Tool call visible in stream mcp.tool.name, mcp.args.byte.len, tool.calls.count, tool.calls.names mcp.tool.arguments.done, mcp.tool.result.received
Topic summary (background) utils.query.update_conversation_topic_summary Async topic summary topic.summary.task.started topic.summary.task.finished

Catalog, discovery, and MCP auth

Span Place Description Key Attributes Key Events
List toolgroups tools.tools_endpoint_handler List backend toolgroups toolgroups.count, backend.operation toolgroups.list.done
List tools per group tools.tools_endpoint_handler Tools in one toolgroup tools.toolgroup.id, tools.count, backend.operation tools.list.done
Get RAG rags.get_rag_endpoint_handler Single RAG metadata rags.rag.id, backend.operation
Get provider providers get handler Single provider providers.provider.id, backend.operation

Other discovery spans (trivial): List shields, models, providers, service info, effective config, MCP client options (attributes/events similar to above).

MCP server administration

Span Place Description Key Attributes Key Events
Register MCP server mcp_servers.register_mcp_server_handler Register dynamic MCP mcp.server.name, mcp.register.ok, backend.operation mcp.server.registered
List MCP servers mcp_servers.list_mcp_servers_handler List runtime MCP servers mcp.servers.count
Delete MCP server mcp_servers.delete_mcp_server_handler Unregister toolgroup mcp.server.name, mcp.delete.ok, backend.operation mcp.server.deleted

Conversations, feedback, RLS, A2A, misc

Span Place Description Key Attributes Key Events
Conversations CRUD Handlers & backend client calls DB + backend conversation APIs; session grouping conversation.id, conversation.items.count, session.invocation.count, session.transcript (anonymized), backend.operation conversation.db.query, conversation.backend.call
Feedback feedback module handlers Submit/query feedback feedback.operation, feedback.status.code, feedback.rating, feedback.comment feedback.conversation feedback.submitted
RLS infer rlsapi_v1 Render template / infer request rls.template.ok, llm.model.id, llm.provider.id rls.template.rendered
Stream interrupt stream_interrupt.* Cancel in-flight stream interrupt.request_id
A2A a2a endpoints Inbound agent requests a2a.rpc.method, a2a.request.id a2a.dispatch.start, a2a.dispatch.end
Authorized probe authorized.* Auth check authorized.ok

Health, metrics, and root endpoints are noisy and should not have manual spans, but FastAPI will still generate automatic spans. These can be filtered via OTEL_PYTHON_FASTAPI_EXCLUDED_URLS or dropped downstream.

Naming conventions

Prometheus metrics

LCORE continues to expose Prometheus-compatible metrics via /metrics. While OpenTelemetry tracing is introduced for spans, metrics remain on Prometheus.

Failure handling and sensitive data

Environment variables

All tracing SDK configuration uses standard OpenTelemetry environment variables at process launch.

Global kill switch: OTEL_SDK_DISABLED=true

Required for export (typical):

Common optional settings:

See the OpenTelemetry SDK environment variables reference.

Deployment

docker-compose.yaml (LCORE service) — set OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_SERVICE_NAME, OTEL_EXPORTER_OTLP_PROTOCOL; add headers, sampler, OTEL_SDK_DISABLED, etc. as needed via environment / env_file.

Containerfile (LCORE image)
ENTRYPOINT ["opentelemetry-instrument", "python3.12", "src/lightspeed_stack.py"]

Trigger mechanism

Tracing is active when the process starts with opentelemetry-instrument and a coherent set of OTEL_* values (unless OTEL_SDK_DISABLED=true). The SDK and propagators are fully configured from the environment at process launch; LCORE YAML plays no role.

Storage / data model changes

None. Traces are exported; LCORE does not persist span data in application databases.

Configuration

LCORE defines no YAML block for OpenTelemetry. All tracing settings are OTEL_* environment variables, set at deploy time. See Architecture → Environment variables.

/config response enrichment

When GET /v1/config returns the effective configuration, the handler shall append scraped OTEL_* values under observability.otel:

{
  "observability": {
    "otel": {
      "OTEL_EXPORTER_OTLP_ENDPOINT": "http://otel-collector:4318",
      "OTEL_EXPORTER_OTLP_PROTOCOL": "http/protobuf",
      "OTEL_SERVICE_NAME": "lightspeed-core",
      "OTEL_PROPAGATORS": "tracecontext,baggage",
      "OTEL_EXPORTER_OTLP_HEADERS": "[REDACTED]"
    }
  }
}

Values are read from the process environment at request time. Secret-bearing variables shall be redacted. There is no corresponding LCORE config model for tracing.

API changes

No required change to JSON requests/responses. The /config response gains observability.otel as described above.

Error handling

Security considerations

Migration / backwards compatibility

New dependencies

Implementation Suggestions

Key files and insertion points

File What to do
pyproject.toml Add OTel API, SDK, OTLP exporter, FastAPI instrumentor, propagators; pin versions per project policy.
src/app/endpoints/config.py Scrape OTEL_* env vars into observability.otel on /config response; redact secrets.
app/endpoints/*.py, utils/*.py Add manual spans around logical sections of request handlers.
Containerfile Add OTel packages; set ENTRYPOINT to ["opentelemetry-instrument", "python3.12", "src/lightspeed_stack.py"].
docker-compose.yaml environment / env_file: required OTEL_* exporter fields.

Open Questions

Epics

Appendix B: External references