Instrument applications with the OpenTelemetry SDK and prove the telemetry is good by validating it against a local Kopai backend. Use when setting up observability, adding tracing/logging/metrics, deciding what to instrument or which attributes to add, retrofitting OTel into an existing codebase, threading context through call chains, configuring sampling, or when traces/logs/metrics aren't appearing after setup. Also use when users say things like "my traces aren't showing up", "I don't see any data", or "how do I add observability to my app". Do NOT use to investigate existing telemetry for a root cause (use root-cause-analysis), to build dashboards (use create-dashboard), or to instrument LLM and agent calls (use otel-genai-instrumentation).
—
—
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Low
Low-risk findings worth noting
| title | impact | tags |
|---|---|---|
| Python Instrumentation | HIGH | lang, python |
Set up OpenTelemetry SDK for Python applications with traces, logs, and metrics.
pip install opentelemetry-api opentelemetry-sdk opentelemetry-exporter-otlp-proto-httpimport os
import logging
from opentelemetry import trace, metrics
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
from opentelemetry.exporter.otlp.proto.http.metric_exporter import OTLPMetricExporter
from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter
# Configuration from environment
ENDPOINT = os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4318")
SERVICE_NAME = os.getenv("OTEL_SERVICE_NAME", "my-service")
# Create resource
resource = Resource.create({"service.name": SERVICE_NAME})
# Traces
trace_provider = TracerProvider(resource=resource)
trace_provider.add_span_processor(
BatchSpanProcessor(OTLPSpanExporter(endpoint=f"{ENDPOINT}/v1/traces"))
)
trace.set_tracer_provider(trace_provider)
# Metrics
meter_provider = MeterProvider(
resource=resource,
metric_readers=[PeriodicExportingMetricReader(
OTLPMetricExporter(endpoint=f"{ENDPOINT}/v1/metrics")
)]
)
metrics.set_meter_provider(meter_provider)
# Logs
logger_provider = LoggerProvider(resource=resource)
logger_provider.add_log_record_processor(
BatchLogRecordProcessor(OTLPLogExporter(endpoint=f"{ENDPOINT}/v1/logs"))
)
handler = LoggingHandler(logger_provider=logger_provider)
logging.getLogger().addHandler(handler)Nothing flushes on its own. Without this, short scripts and SIGTERM'd servers lose their
last batch — see validate-shutdown.md.
import atexit, signal, sys
def shutdown(*_):
trace_provider.shutdown() # flush order: traces, metrics, logs
meter_provider.shutdown()
logger_provider.shutdown()
atexit.register(shutdown)
signal.signal(signal.SIGTERM, lambda *_: (shutdown(), sys.exit(0)))pip install opentelemetry-distro
opentelemetry-bootstrap -a install
opentelemetry-instrument python app.pyopentelemetry-bootstrap -a install inspects your installed packages and pulls the
matching instrumentation libraries — Flask, Django, requests, psycopg, SQLAlchemy,
redis and so on. Re-run it after adding dependencies, or their calls stay invisible.
from opentelemetry import trace
tracer = trace.get_tracer("checkout-service")
# attributes on the span you already have
span = trace.get_current_span()
span.set_attribute("user.id", user.id)
span.set_attribute("user.type", user.tier)
# a span of your own
with tracer.start_as_current_span("process-payment") as span:
span.set_attribute("payment.amount", order.total)contextvars propagate through async/await and within a thread automatically. They do
not cross a ThreadPoolExecutor boundary or a multiprocessing fork — spans created
there become orphans:
from opentelemetry import context as otel_context
ctx = otel_context.get_current()
def run():
token = otel_context.attach(ctx)
try:
process_item(item)
finally:
otel_context.detach(token)
executor.submit(run)See context-propagation.md.
SDK setup is step 3 of six. It gets bytes flowing; it does not make the telemetry good.
instrument-spans.mdinstrument-attributes.mdinstrument-errors.mddrive-traffic.mdvalidate-traces.mdConfirm before moving on: the SDK starts before any application code that could
create a span, and shutdown flushes on SIGTERM (validate-shutdown.md). Both fail
silently.
references