Migrating from raw OpenTelemetry SDK to obskit¶
The OpenTelemetry Python SDK is powerful but requires significant boilerplate to configure correctly for production. obskit wraps the SDK with sensible defaults, auto-instrumentation detection, and first-class integration with structured logging and Prometheus metrics — while keeping full OTel compatibility.
Why Migrate?¶
Raw OTel setup: ~60 lines of boilerplate¶
# What you write today with raw OTel
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.sdk.resources import Resource, SERVICE_NAME
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.trace.sampling import TraceIdRatioBased
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor
from opentelemetry.instrumentation.redis import RedisInstrumentor
from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor
resource = Resource.create({
SERVICE_NAME: "order-service",
"service.version": "1.2.3",
"deployment.environment": "production",
})
sampler = TraceIdRatioBased(0.1)
provider = TracerProvider(resource=resource, sampler=sampler)
exporter = OTLPSpanExporter(
endpoint="http://tempo:4317",
insecure=True,
)
provider.add_span_processor(BatchSpanProcessor(exporter))
trace.set_tracer_provider(provider)
# Auto-instrumentation
FastAPIInstrumentor().instrument()
SQLAlchemyInstrumentor().instrument()
RedisInstrumentor().instrument()
HTTPXClientInstrumentor().instrument()
tracer = trace.get_tracer("order-service")
With obskit: 3 lines¶
from obskit.tracing import setup_tracing
setup_tracing(
exporter_endpoint="http://tempo:4317",
sample_rate=0.1,
# auto-detects FastAPI, SQLAlchemy, Redis, httpx, etc.
)
Installation¶
pip install "obskit[otlp]"
# For auto-instrumentation of specific frameworks:
pip install opentelemetry-instrumentation-fastapi
pip install opentelemetry-instrumentation-sqlalchemy
pip install opentelemetry-instrumentation-redis
pip install opentelemetry-instrumentation-httpx
TracerProvider Setup → setup_tracing()¶
Before¶
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.sdk.resources import Resource, SERVICE_NAME
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
resource = Resource.create({SERVICE_NAME: "my-service"})
provider = TracerProvider(resource=resource)
provider.add_span_processor(
BatchSpanProcessor(OTLPSpanExporter(endpoint="http://tempo:4317"))
)
trace.set_tracer_provider(provider)
After¶
from obskit.tracing import setup_tracing
setup_tracing(exporter_endpoint="http://tempo:4317")
You can still pass custom OTel resource attributes:
setup_tracing(
exporter_endpoint="http://tempo:4317",
sample_rate=0.1,
resource_attributes={
"k8s.pod.name": os.environ.get("POD_NAME", "unknown"),
"k8s.namespace.name": os.environ.get("K8S_NAMESPACE", "default"),
},
)
Manual Span Creation¶
Before — tracer.start_as_current_span¶
from opentelemetry import trace
tracer = trace.get_tracer(__name__)
def process_order(order_id: str) -> None:
with tracer.start_as_current_span(
"process_order",
attributes={"order.id": order_id, "order.service": "payment"},
) as span:
result = charge_card(order_id)
span.set_attribute("charge.status", result.status)
if result.failed:
span.set_status(trace.StatusCode.ERROR, result.error_message)
After — trace_span¶
from obskit.tracing import trace_span
def process_order(order_id: str) -> None:
with trace_span(
"process_order",
attributes={"order.id": order_id, "order.service": "payment"},
) as span:
result = charge_card(order_id)
span.set_attribute("charge.status", result.status)
Async spans¶
from obskit.tracing import async_trace_span
async def fetch_inventory(sku: str) -> dict:
async with async_trace_span("fetch_inventory", attributes={"sku": sku}):
return await db.get_inventory(sku)
The trace_span and async_trace_span functions automatically:
- Set
error=trueand record the exception on the span when an exception propagates. - Extract and propagate W3C TraceContext headers.
- Inject the
trace_idinto the current structlog context (trace-log correlation).
Auto-Instrumentation¶
obskit auto-detects installed OTel instrumentation packages and applies them.
Before — manual instrumentor registration¶
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor
from opentelemetry.instrumentation.redis import RedisInstrumentor
FastAPIInstrumentor().instrument(app=app)
SQLAlchemyInstrumentor().instrument()
RedisInstrumentor().instrument()
After — automatic detection¶
from obskit.tracing import setup_tracing
# setup_tracing detects and applies all installed instrumentors
setup_tracing(exporter_endpoint="http://tempo:4317")
# Or specify explicitly:
setup_tracing(
exporter_endpoint="http://tempo:4317",
instrument=["fastapi", "sqlalchemy", "redis"],
)
Keeping your existing instrumentors¶
If you have custom instrumentor configuration, you can skip auto-detection and apply
instrumentors yourself after calling setup_tracing():
from obskit.tracing import setup_tracing
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
# Setup provider only, no auto-instrumentation
setup_tracing(exporter_endpoint="http://tempo:4317", instrument=[])
# Apply your instrumentors with custom config
FastAPIInstrumentor().instrument(
app=app,
excluded_urls="/health,/metrics",
http_capture_headers_server_request=["X-Request-ID"],
)
OTLP Exporter Configuration Migration¶
Environment variables (no code change required)¶
# Before (raw OTel env vars still work)
export OTEL_EXPORTER_OTLP_ENDPOINT=http://tempo:4317
export OTEL_SERVICE_NAME=order-service
export OTEL_TRACES_SAMPLER=parentbased_traceidratio
export OTEL_TRACES_SAMPLER_ARG=0.1
# After (obskit env vars — preferred)
export OBSKIT_OTLP_ENDPOINT=http://tempo:4317
export OBSKIT_SERVICE_NAME=order-service
export OBSKIT_TRACE_SAMPLE_RATE=0.1
Note
obskit reads both OTEL_* and OBSKIT_* environment variables.
OBSKIT_* takes precedence when both are set.
Trace-Log Correlation¶
One of obskit's biggest advantages over raw OTel is automatic trace-log correlation.
When setup_tracing() is called, obskit installs a structlog processor that injects
trace_id and span_id into every log record emitted during an active span.
Before — manual correlation¶
import structlog
from opentelemetry import trace
logger = structlog.get_logger()
def process_order(order_id: str) -> None:
span = trace.get_current_span()
ctx = span.get_span_context()
log = logger.bind(
trace_id=format(ctx.trace_id, "032x") if ctx.is_valid else None,
span_id=format(ctx.span_id, "016x") if ctx.is_valid else None,
)
log.info("processing_order", order_id=order_id)
After — automatic correlation¶
from obskit.logging import get_logger
logger = get_logger(__name__)
def process_order(order_id: str) -> None:
# trace_id and span_id are injected automatically
logger.info("processing_order", order_id=order_id)
Output:
{
"event": "processing_order",
"order_id": "ord-123",
"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
"span_id": "00f067aa0ba902b7",
"level": "info",
"timestamp": "2025-01-15T10:30:45.123456Z"
}
W3C Baggage Propagation¶
obskit exposes a clean API for W3C Baggage, replacing verbose OTel SDK calls.
Before¶
from opentelemetry import baggage, context
ctx = baggage.set_baggage("tenant_id", "acme-corp")
context.attach(ctx)
tenant = baggage.get_baggage("tenant_id")
After¶
from obskit.tracing import set_baggage, get_baggage, clear_baggage
set_baggage("tenant_id", "acme-corp")
tenant = get_baggage("tenant_id") # "acme-corp"
clear_baggage("tenant_id")
Shutdown¶
# Before
from opentelemetry.sdk.trace import TracerProvider
provider: TracerProvider = trace.get_tracer_provider()
provider.shutdown()
# After
from obskit.tracing import shutdown_tracing
shutdown_tracing()
Migration Checklist¶
- [ ] Install
obskit[otlp]and remove manual OTel setup code - [ ] Replace
TracerProviderboilerplate withsetup_tracing() - [ ] Replace
tracer.start_as_current_span()withtrace_span()/async_trace_span() - [ ] Remove manual instrumentor calls (or keep them with
instrument=[]) - [ ] Switch from
OTEL_*env vars toOBSKIT_*(or keep both — both are read) - [ ] Replace manual trace-log correlation with
get_logger()from obskit - [ ] Replace
baggage.set_baggage()withset_baggage()from obskit.tracing - [ ] Replace
provider.shutdown()withshutdown_tracing() - [ ] Verify spans appear in your tracing backend (Grafana Tempo, Jaeger, etc.)