otelio ships three built-in export targets — otlp (SigNoz / any OTLP-gRPC collector),
otlp-http (any OTLP/HTTP-protobuf collector) and azure (App Insights) — selected with
the OTELIO_TARGET env var. If you need a different backend (Loki, a vendor SaaS endpoint
with custom auth, a console exporter, a vendor SDK, …) you can register your own exporter
without modifying otelio and select it the same way: OTELIO_TARGET=<your-name>.
Public API reference and the built-in config live in the README. This doc is the "I need a backend otelio doesn't ship" companion.
- Built-in targets
- How it works
- Register your exporter
- Select it from the environment
- Full example — OTLP/HTTP with custom auth headers
- Overriding a built-in target
- Gotchas
otelio ships three export targets out of the box. You select one with OTELIO_TARGET;
no registration needed. All cover traces and logs.
OTELIO_TARGET |
Backend | Transport | Reads from | Extra dependency |
|---|---|---|---|---|
otlp (default) |
Any OTLP collector — SigNoz, Grafana, Jaeger, … | OTLP / gRPC | OTEL_EXPORTER_OTLP_ENDPOINT (default http://localhost:4317) |
bundled |
otlp-http |
Any OTLP collector over HTTP | OTLP / HTTP-protobuf | OTEL_EXPORTER_OTLP_ENDPOINT (default http://localhost:4318, /v1/traces + /v1/logs appended) |
bundled |
azure |
Azure Application Insights | Azure Monitor SDK | APPLICATIONINSIGHTS_CONNECTION_STRING |
azure-monitor-opentelemetry-exporter |
The rest of this doc is only needed when you want a target these three don't cover (Loki, a vendor SaaS endpoint needing custom auth headers, a console exporter, a vendor SDK, …).
Internally, otelio keeps a registry mapping a target name to an exporter factory.
The three built-ins above (otlp, otlp-http and azure) are just pre-registered entries. When
init_otelio runs, it looks up OTELIO_TARGET in that registry and calls the factory to
build the exporter.
A factory is any callable that takes the resolved Settings
and returns an exporter:
TraceExporterFactory = Callable[[Settings], SpanExporter]
LogExporterFactory = Callable[[Settings], LogRecordExporter]Receiving Settings means your factory can read s.otlp_endpoint, s.azure_conn_str,
s.service_name, s.environment, etc. — the same resolved config the built-ins use.
Naming note. otelio names its surface after the signal —
traceandlog— so the pair stays consistent. The trace exporter's OTel type is stillSpanExporter(the SDK's class name), which is why the factory's return annotation saysSpanExporter.
Pass your custom exporters straight to init_otelio through two optional list params,
trace_exporters and log_exporters. Each is a list of {"name": ..., "factory": ...}
objects. otelio registers them before resolving OTELIO_TARGET, so there's no separate
registration step and no import-ordering to think about.
# main.py
from otelio import Settings, init_otelio
# import your backend's exporters — these are NOT otelio deps, they're yours
import os
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter
# A vendor SaaS OTLP/HTTP endpoint that needs an API-key header — the built-in
# `otlp-http` target doesn't expose custom headers, so register a tuned variant.
_HEADERS = {"x-api-key": os.environ["VENDOR_API_KEY"]}
def build_saas_traces(s: Settings):
return OTLPSpanExporter(endpoint=f"{s.otlp_endpoint}/v1/traces", headers=_HEADERS)
def build_saas_logs(s: Settings):
return OTLPLogExporter(endpoint=f"{s.otlp_endpoint}/v1/logs", headers=_HEADERS)
init_otelio(
"my-service",
"1.0.0",
trace_exporters=[{"name": "otlp-saas", "factory": build_saas_traces}],
log_exporters=[{"name": "otlp-saas", "factory": build_saas_logs}],
)You picked the name otlp-saas. It's case-insensitive (stored lowercased), and it's what
you'll set OTELIO_TARGET to. Each list can hold several entries if you want multiple
targets selectable by env.
Register a trace and/or a log factory under your name.
OTELIO_TARGETselects the exporters for both signals at once; if a chosen target has no factory for a given signal, otelio raises a clearValueErrorlisting the known targets.To send the two signals to different backends, set
OTELIO_TRACE_TARGETand/orOTELIO_LOG_TARGET— each overridesOTELIO_TARGETfor that one signal. In that case a name only needs a factory for the signal it's selected for (e.g. a logs-only target needs no trace factory).
The object shape is typed — import TraceExporterEntry / LogExporterEntry from otelio
if you want your editor to check it.
Nothing in your code changes per environment — only the env var:
# .env
OTELIO_TARGET=otlp-saas
OTEL_EXPORTER_OTLP_ENDPOINT=https://otlp.vendor.exampleLocal can stay on the built-in gRPC otlp, staging can use your otlp-saas, and so on —
purely by flipping OTELIO_TARGET.
A complete, runnable shape. Plain OTLP/HTTP is built in as otlp-http; this variant adds an
auth header the built-in factory doesn't expose — the common reason to register your own.
# main.py
import os
from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from otelio import Settings, init_otelio
_HEADERS = {"x-api-key": os.environ["VENDOR_API_KEY"]}
def _traces(s: Settings):
return OTLPSpanExporter(endpoint=f"{s.otlp_endpoint}/v1/traces", headers=_HEADERS)
def _logs(s: Settings):
return OTLPLogExporter(endpoint=f"{s.otlp_endpoint}/v1/logs", headers=_HEADERS)
init_otelio(
"my-service",
"1.0.0",
trace_exporters=[{"name": "otlp-saas", "factory": _traces}],
log_exporters=[{"name": "otlp-saas", "factory": _logs}],
)# .env
OTELIO_TARGET=otlp-saas
OTEL_EXPORTER_OTLP_ENDPOINT=https://otlp.vendor.exampleThe OTLP/HTTP exporter is already an otelio dependency, so there's nothing extra to install — and you're exporting with custom auth and zero changes to otelio.
Registering under an existing name (otlp, otlp-http or azure) replaces that built-in — useful
for tweaking exporter options (timeouts, headers, compression) the built-in factory doesn't
expose. Just reuse the name:
from grpc import Compression
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from otelio import Settings, init_otelio
def _gzip_traces(s: Settings):
return OTLPSpanExporter(endpoint=s.otlp_endpoint, compression=Compression.Gzip)
init_otelio(
"my-service",
"1.0.0",
trace_exporters=[{"name": "otlp", "factory": _gzip_traces}], # OTELIO_TARGET=otlp now uses gzip
)The last factory registered for a name wins.
- The name must be registered.
OTELIO_TARGET=<name>must match a built-in (otlp/otlp-http/azure) or a name you passed toinit_otelio, or it raisesValueError: No trace exporter registered for …. - Register both signals. One name needs both a trace factory and a log factory; otelio
builds an exporter for each signal from the same
OTELIO_TARGET. - The factory's deps are yours. otelio bundles the SDKs for
otlpandotlp-http, and pulls inazurevia the[azure]extra. Whatever exporter you import in your factory, add it to your project's dependencies. - Build, don't wrap. Return a bare exporter from the factory — otelio wraps it in the
appropriate processor (
BatchSpanProcessor/BatchLogRecordProcessor) itself. Don't return a processor. - Keep factories cheap and side-effect-free. They run once during
init_otelio. Read fromSettings(or your own env vars) inside the factory; don't do network I/O there. OTELIO_CONSOLEis independent. It adds a console span exporter on top of whatever target you select (see the README); it doesn't replace it.