Distributed Tracing with OpenTelemetry
This guide explains how to configure and use distributed tracing in vLLM Semantic Router for enhanced observability and debugging capabilities.
Overview
vLLM Semantic Router implements comprehensive distributed tracing using OpenTelemetry, providing fine-grained visibility into the request processing pipeline. Tracing helps you:
- Debug Production Issues: Trace individual requests through the entire routing pipeline
- Optimize Performance: Identify bottlenecks in classification, caching, and routing
- Monitor Security: Track PII detection and jailbreak prevention operations
- Analyze Decisions: Understand routing logic and reasoning mode selection
- Correlate Services: Connect traces across the router and vLLM backends
Architecture
Trace Hierarchy
A typical request trace follows this structure:
semantic_router.request.received [root span]
├─ semantic_router.classification
├─ semantic_router.security.pii_detection
├─ semantic_router.security.jailbreak_detection
├─ semantic_router.cache.lookup
├─ semantic_router.routing.decision
├─ semantic_router.backend.selection
├─ semantic_router.system_prompt.injection
└─ semantic_router.upstream.request
Span Attributes
Each span includes rich attributes following OpenInference conventions for LLM observability:
Request Metadata:
request.id- Unique request identifieruser.id- User identifier (if available)http.method- HTTP methodhttp.path- Request path
Model Information:
model.name- Selected model namerouting.original_model- Original requested modelrouting.selected_model- Model selected by router
Classification:
category.name- Classified categoryclassifier.type- Classifier implementationclassification.time_ms- Classification duration
Security:
pii.detected- Whether PII was foundpii.types- Types of PII detectedjailbreak.detected- Whether jailbreak attempt detectedsecurity.action- Action taken (blocked, allowed)
Routing:
routing.strategy- Routing strategy (auto, specified)routing.reason- Reason for routing decisionreasoning.enabled- Whether reasoning mode enabledreasoning.effort- Reasoning effort level
Performance:
cache.hit- Cache hit/miss statuscache.lookup_time_ms- Cache lookup durationprocessing.time_ms- Total processing time
Configuration
Basic Configuration
Add the observability.tracing section to your config.yaml:
observability:
tracing:
enabled: true
provider: "opentelemetry"
exporter:
type: "stdout" # or "otlp"
endpoint: "localhost:4317"
insecure: true
sampling:
type: "always_on" # or "probabilistic"
rate: 1.0
resource:
service_name: "vllm-semantic-router"
service_version: "v0.1.0"
deployment_environment: "production"
Configuration Options
Exporter Types
stdout - Print traces to console (development)
exporter:
type: "stdout"
otlp - Export to OTLP-compatible backend (production)
exporter:
type: "otlp"
endpoint: "jaeger:4317" # Jaeger, Tempo, Datadog, etc.
insecure: true # Use false with TLS in production
Sampling Strategies
always_on - Sample all requests (development/debugging)
sampling:
type: "always_on"
always_off - Disable sampling (emergency performance)
sampling:
type: "always_off"
probabilistic - Sample a percentage of requests (production)
sampling:
type: "probabilistic"
rate: 0.1 # Sample 10% of requests
Environment-Specific Configurations
Development
observability:
tracing:
enabled: true
provider: "opentelemetry"
exporter:
type: "stdout"
sampling:
type: "always_on"
resource:
service_name: "vllm-semantic-router-dev"
deployment_environment: "development"
Production
observability:
tracing:
enabled: true
provider: "opentelemetry"
exporter:
type: "otlp"
endpoint: "tempo:4317"
insecure: false # Use TLS
sampling:
type: "probabilistic"
rate: 0.1 # 10% sampling
resource:
service_name: "vllm-semantic-router"
service_version: "v0.1.0"
deployment_environment: "production"
Deployment
With Jaeger
- Start Jaeger (all-in-one for testing):
docker run -d --name jaeger \
-p 4317:4317 \
-p 16686:16686 \
jaegertracing/all-in-one:latest
- Configure Router:
observability:
tracing:
enabled: true
exporter:
type: "otlp"
endpoint: "localhost:4317"
insecure: true
sampling:
type: "probabilistic"
rate: 0.1
- Access Jaeger UI: http://localhost:16686
With Grafana Tempo
- Configure Tempo (tempo.yaml):
server:
http_listen_port: 3200
distributor:
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
storage:
trace:
backend: local
local:
path: /tmp/tempo/traces
- Start Tempo:
docker run -d --name tempo \
-p 4317:4317 \
-p 3200:3200 \
-v $(pwd)/tempo.yaml:/etc/tempo.yaml \
grafana/tempo:latest \
-config.file=/etc/tempo.yaml
- Configure Router:
observability:
tracing:
enabled: true
exporter:
type: "otlp"
endpoint: "tempo:4317"
insecure: true
Kubernetes Deployment
apiVersion: v1
kind: ConfigMap
metadata:
name: router-config
data:
config.yaml: |
observability:
tracing:
enabled: true
exporter:
type: "otlp"
endpoint: "jaeger-collector.observability.svc:4317"
insecure: false
sampling:
type: "probabilistic"
rate: 0.1
resource:
service_name: "vllm-semantic-router"
deployment_environment: "production"
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: semantic-router
spec:
template:
spec:
containers:
- name: router
image: vllm-semantic-router:latest
env:
- name: CONFIG_PATH
value: /config/config.yaml
volumeMounts:
- name: config
mountPath: /config
volumes:
- name: config
configMap:
name: router-config
Usage Examples
Viewing Traces
Console Output (stdout exporter)
{
"Name": "semantic_router.classification",
"SpanContext": {
"TraceID": "abc123...",
"SpanID": "def456..."
},
"Attributes": [
{
"Key": "category.name",
"Value": "math"
},
{
"Key": "classification.time_ms",
"Value": 45
}
],
"Duration": 45000000
}