Instrument code with the standard opentelemetry API. Elide
implements it: there is no SDK, exporter, or opentelemetry-instrument wrapper to add, and libraries that already
use the API work unchanged.
# app.py
import logging
from opentelemetry import trace
tracer = trace.get_tracer("checkout")
with tracer.start_as_current_span("charge") as span:
span.set_attribute("order.id", 42)
logging.warning("card declined, retrying")OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 elide run app.pyThe API package
| Your environment | import opentelemetry resolves to |
|---|---|
No opentelemetry-api installed | Elide’s vendored copy (opentelemetry-api 1.45.1) |
opentelemetry-api installed | Your copy, backed by Elide |
Elide installs its tracer, meter, and logger providers when your code first imports opentelemetry.trace,
opentelemetry.metrics, or opentelemetry._logs. Nothing is imported before your code does, and the import order
among those modules does not matter. The vendored copy
answers importlib.metadata like an installed wheel, so OTEL_PROPAGATORS, OTEL_PYTHON_CONTEXT, and the
OTEL_PYTHON_*_PROVIDER variables work as usual. When telemetry is not configured, the API returns its no-op
implementations.
Traces
Tracers, spans, links, events, and status follow the API. Context is the API’s contextvars context, so it follows
with blocks, threads that copy the context, and asyncio tasks.
from opentelemetry import trace
tracer = trace.get_tracer("jobs")
@tracer.start_as_current_span("import rows")
def import_rows(rows):
trace.get_current_span().set_attribute("rows", len(rows))A span started with no active Python span is parented to the request Elide is serving on that thread, if any. Passing
an explicit context= starts a root span instead.
Metrics
from opentelemetry import metrics
pending = []
meter = metrics.get_meter("checkout")
orders = meter.create_counter("orders", unit="{order}")
latency = meter.create_histogram(
"charge.duration", unit="s", explicit_bucket_boundaries_advisory=[0.01, 0.05, 0.1, 0.5, 1]
)
def queue_depth(options):
yield metrics.Observation(len(pending))
meter.create_observable_gauge("queue.depth", callbacks=[queue_depth])
orders.add(1, {"region": "eu"})
latency.record(0.042)All synchronous and observable instruments are supported, and callbacks may be generators. Callbacks run at each collection.
Logging
With logs.console on (the default), records that a logging handler would print are also exported as log records,
with the active span’s ids. What logging prints is unchanged, and records no handler would emit are not exported.
logging field | Log record |
|---|---|
| Logger name | Instrumentation scope |
levelno | Severity: DEBUG 5, INFO 9, WARNING 13, ERROR 17, CRITICAL 21 |
getMessage() | Body |
pathname, funcName, lineno | code.file.path, code.function.name, code.line.number |
exc_info | exception.type, exception.message, exception.stacktrace |
The logs API works too; records keep structured bodies (dict, list, bytes):
from opentelemetry._logs import get_logger, LogRecord, SeverityNumber
get_logger("checkout").emit(
LogRecord(severity_number=SeverityNumber.WARN, body={"event": "retry", "attempt": 2})
)An uncaught exception is exported as an error with its Python traceback; see Errors.
Using an OpenTelemetry SDK
An application can still set its own providers, from opentelemetry-sdk or opentelemetry-instrument:
| Event | Result |
|---|---|
| The application sets a tracer, meter, or logger provider | It takes over that signal and Elide prints a warning. Tracers, meters, loggers, and observable callbacks created earlier forward to it. |
OTEL_ (or _METER_, _LOGGER_) names a provider | Elide never installs its own for that signal. |
elide. | Elide unsets its providers; the application’s take over without warnings. |
An OpenTelemetry SDK set its own tracer provider; Elide's Python tracer provider steps asideElide’s runtime telemetry keeps recording either way.
elide.telemetry
Controls the API does not cover. Stubs ship with Elide’s Python type stubs; every function is safe to call when telemetry is not configured.
import elide.telemetry
from opentelemetry import trace
with trace.get_tracer("jobs").start_as_current_span("nightly"):
print(elide.telemetry.current_span_context()["traceparent"])
elide.telemetry.flush()
print(elide.telemetry.status()["spans"])| Function | Returns | Description |
|---|---|---|
active() | bool | Whether telemetry is configured for this process. |
flush(timeout_ | bool | Exports everything recorded so far, waiting up to timeout_millis. Whether every destination accepted it. |
status() | TelemetryStatus | Approximate, process-wide pipeline counters; see below. |
current_span_context() | CurrentSpanContext | None | The active span’s trace_id, span_id, trace_flags, and traceparent, or None outside a valid span. |
release() | bool | Stops serving as the global OpenTelemetry implementation, so a provider set afterwards owns every signal. Elide’s runtime instrumentation keeps recording. False when already released or never installed. |
class TelemetryStatus(TypedDict):
active: bool
released: bool
stepped_aside: list[str] # "trace", "metrics", "logs"
spans: SpanCounters # started, unsampled, ended, live, exported, dropped, table_full, reclaimed,
# handed_off, exit_dropped
logs: LogCounters # emitted, filtered, exported, dropped, handed_off, exit_dropped
metrics: MetricCounters # handed_off, exit_dropped
export_failures: int
exports_abandoned: int
class CurrentSpanContext(TypedDict):
trace_id: str
span_id: str
trace_flags: int
traceparent: strLimits
- Requests served by Elide do not set the Python
contextvarscontext; spans parent to the request through Elide instead, as described in Traces. - Python HTTP clients do not get client spans or
traceparent; inject it withopentelemetry.propagate.inject(headers).