Skip to content

Python custom OpenTelemetry instrumentation

Auto-instrumentation captures framework, database, and HTTP spans. Use the OpenTelemetry API to add spans and metrics for your own business logic — a batch job, a cache decision, a domain-specific counter. The API calls attach to the provider that opentelemetry-instrument or your programmatic setup already configured, so custom telemetry exports to Osuite through the same pipeline.

Get a tracer once per module, then wrap work in a span with start_as_current_span. The context manager sets the span as current, so any auto-instrumented calls inside it — and any child spans you create — nest underneath.

from opentelemetry import trace
tracer = trace.get_tracer("myapp.orders")
def process_order(order):
with tracer.start_as_current_span("process_order") as span:
span.set_attribute("order.id", order.id)
span.set_attribute("order.item_count", len(order.items))
charge(order)

Set attributes to record the values you will filter and group by in Osuite. Use dotted, lowercase names.

To trace an entire function, use the tracer as a decorator. The span name defaults to the wrapped function’s name.

@tracer.start_as_current_span("reconcile_ledger")
def reconcile_ledger():
...

Mark a span as failed and attach the exception so it appears in error tracking. record_exception stores the stack trace; set_status marks the span as an error.

from opentelemetry.trace import Status, StatusCode
with tracer.start_as_current_span("charge") as span:
try:
charge(order)
except PaymentError as exc:
span.record_exception(exc)
span.set_status(Status(StatusCode.ERROR))
raise

Events are timestamped annotations on the current span — useful for marking a milestone without creating a child span.

span = trace.get_current_span()
span.add_event("cache.miss", {"cache.key": key})

Get a meter once per module, then create instruments at module scope and record to them in your code. Osuite reads these in the Explore metrics view.

from opentelemetry import metrics
meter = metrics.get_meter("myapp.orders")
orders_processed = meter.create_counter(
"orders.processed",
unit="1",
description="Number of orders processed",
)
order_value = meter.create_histogram(
"orders.value",
unit="USD",
description="Monetary value per order",
)
def process_order(order):
orders_processed.add(1, {"order.type": order.type})
order_value.record(order.total, {"order.type": order.type})
  • Counter — a value that only increases (requests handled, jobs completed). Use create_up_down_counter when it can also decrease, such as an in-flight gauge you increment and decrement.
  • Histogram — a distribution of recorded values (durations, sizes, amounts). Osuite computes percentiles from it.

For a value you read rather than accumulate — a queue depth, a pool size — register an observable gauge with a callback. The SDK invokes the callback each collection interval.

def queue_depth(options):
yield metrics.Observation(get_queue_depth(), {"queue": "orders"})
meter.create_observable_gauge(
"orders.queue.depth",
callbacks=[queue_depth],
unit="1",
description="Pending orders in the queue",
)

Logs written through the standard logging module inside an active span carry that span’s trace and span IDs automatically when the OpenTelemetry logging bridge is enabled (OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED=true, set in the overview). No extra code is needed — log as usual and the records line up with the trace in Osuite.

What you should see

Exercise the instrumented code path. Your custom spans appear nested in the request or task trace in APM, with the attributes you set. Custom metrics appear in the Explore metrics view under the instrument names you chose within a minute of the first recording.

No data in Osuite after a few minutes? Work through these checks.

  • Endpoint — confirm the exporter targets exactly ingest.<region>.osuite.io:443 for your region, over TLS.
  • Token — confirm the ingest header carries a valid <your-ingest-token> and has not been rotated.
  • Pipeline — confirm the signal you expect (traces, logs, or metrics) is wired into an active pipeline with the otlp/osuite exporter attached.
  • Export errors — check the application or Collector logs for OTLP export failures (auth, DNS, TLS, connection refused).
  • Network — confirm the host has outbound access to the ingest endpoint on port 443.
  • Timing — allow up to a minute for the first data to appear before assuming a failure.

Still stuck? Ask the Investigation Agent or contact support.