Skip to content

Observability

ArchiPy uses OpenTelemetry for traces, metrics, and logs. Providers are built from BaseConfig.OTEL (pydantic-settings) — not from OTEL_* environment-variable autoconfiguration.

Enable the relevant extras, set OTEL__IS_ENABLED=true, and call AppUtils.create_fastapi_app / create_grpc_app (or use the tracing/metrics decorators in workers and business logic).

Installation

Extra Purpose
archipy[otel] SDK, OTLP exporters, B3 propagator, httpx/requests, threading, system metrics
archipy[otel,fastapi] FastAPI native telemetry (FastAPI 0.142.2+)
archipy[otel-grpc] gRPC server/client contrib interceptors
archipy[otel-sqlalchemy] SQLAlchemy instrumentation (covers Postgres/MySQL/SQLite via ORM)
archipy[otel-redis] Redis instrumentation
archipy[otel-kafka] Confluent Kafka instrumentation
archipy[otel-scylladb] Cassandra/ScyllaDB driver instrumentation
archipy[otel-minio] Botocore (MinIO/S3) instrumentation
archipy[otel-pydantic-ai] Pydantic AI agent, model request and tool spans plus token usage metrics
uv add "archipy[otel]"
uv add "archipy[otel,fastapi,otel-grpc]"
uv add "archipy[otel,otel-sqlalchemy,otel-redis,otel-kafka,otel-scylladb,otel-minio]"

Configuration

Configure OpenTelemetry through BaseConfig.OTEL (env prefix OTEL__):

# .env
OTEL__IS_ENABLED=true
OTEL__SERVICE_NAME=my-service
OTEL__OTLP_ENDPOINT=http://localhost:4317
OTEL__PROTOCOL=grpc
OTEL__TRACES_ENABLED=true
OTEL__METRICS_ENABLED=true
OTEL__METRICS_EXPORTER=otlp
OTEL__METRICS_PULL_HOST=0.0.0.0
OTEL__METRICS_PULL_PORT=8200
OTEL__SYSTEM_METRICS_ENABLED=true
OTEL__LOGS_ENABLED=true
OTEL__LOGS_EXPORTER=console
OTEL__TRACES_SAMPLE_RATIO=0.1
OTEL__FASTAPI_EXCLUDED_URLS=health,docs,redoc,openapi.json
import logging

from archipy.configs.base_config import BaseConfig

logger = logging.getLogger(__name__)


class AppConfig(BaseConfig):
    """Application configuration with OpenTelemetry enabled."""


config = AppConfig()
BaseConfig.set_global(config)
logger.info("OTel enabled=%s endpoint=%s", config.OTEL.IS_ENABLED, config.OTEL.OTLP_ENDPOINT)

Note: ArchiPy builds TracerProvider / MeterProvider / LoggerProvider programmatically from OpentelemetryConfig; the SDK's own OTEL_* autoconfiguration is not used. Set USE_STANDARD_ENV to let the standard variables fill in unset settings (see Standard OTEL_* variables).

Key fields

Field Default Description
IS_ENABLED false Master switch
OTLP_ENDPOINT http://localhost:4317 Default OTLP collector URL
TRACES_ENDPOINT None Optional per-signal OTLP traces URL override
METRICS_ENDPOINT None Optional per-signal OTLP metrics URL override
LOGS_ENDPOINT None Optional per-signal OTLP logs URL override
PROTOCOL grpc grpc or http/protobuf
TRACES_SAMPLER parentbased_traceidratio always_on, always_off, traceidratio or the parentbased_* variants
TRACES_SAMPLE_RATIO 0.1 Ratio of the traceidratio and parentbased_traceidratio samplers
PROPAGATORS ["tracecontext", "baggage"] Context propagators; also b3, b3multi; none disables them
OTLP_COMPRESSION none gzip cuts realistic payloads to about half or less at some CPU cost; worth it when the collector is across a network
SPAN_BATCH_* / LOG_BATCH_* queue 4096, batch 512 SCHEDULE_DELAY_MS (spans 5000, logs 1000), EXPORT_TIMEOUT_MS (30000), MAX_QUEUE_SIZE, MAX_EXPORT_BATCH_SIZE of the batch processors. Keep a batch under the 4 MiB message limit gRPC receivers apply to the decompressed message: 512 spans with 25 attributes encode to about 0.75 MiB, with a 4 KB stack trace each to about 2.5 MiB, and 1024 of those already pass 4 MiB. Batch size barely changes the gzip ratio: on realistic spans gzip cut the bytes of a gRPC export to about 0.4 of the uncompressed size (200 spans, 25 attributes and a 4 KB stack trace each, measured through a proxy). A batch size left unset follows a lowered queue size; one set above its queue size is rejected
SERVICE_NAME / SERVICE_VERSION None Resource service.name and service.version
SERVICE_INSTANCE_ID generated UUID Resource service.instance.id
RESOURCE_DETECTORS ["service_instance", "host", "os"] Detectors that add service.instance.id, host.*, os.*; process adds process.* including the full command line, so it is opt-in
METRICS_DURATION_BUCKETS_S 5 ms to 10 s Bucket boundaries in seconds of the duration histograms
USE_STANDARD_ENV false Fill unset settings from the standard OTEL_* variables (see below)
METRIC_EXPORT_INTERVAL_MS 60000 Periodic OTLP metric export interval
METRICS_EXPORTER otlp Unique exporter: otlp, pull, or pushgateway
METRICS_PULL_HOST 0.0.0.0 Pull scrape bind host (unauthenticated)
METRICS_PULL_PORT 8200 Pull scrape bind port (/metrics)
METRICS_PUSHGATEWAY_URL None Pushgateway URL (required for pushgateway)
METRICS_PUSHGATEWAY_JOB None Pushgateway job (defaults to SERVICE_NAME)
METRICS_PUSHGATEWAY_INTERVAL_SECONDS 60 Push interval when exporter is pushgateway
METRICS_PUSHGATEWAY_TIMEOUT_SECONDS 10 HTTP timeout for push/delete
METRICS_PUSHGATEWAY_GROUPING_KEY {} Extra grouping labels (max 16 entries); instance defaults to the pod (see below)
METRICS_PUSHGATEWAY_DELETE_ON_SHUTDOWN true Delete grouping key on graceful shutdown
SYSTEM_METRICS_ENABLED true Process/system metrics instrumentor
INSTRUMENTATION_SKIP_DEP_CHECK ["sqlalchemy"] Libraries instrumented even when newer than the instrumentor's declared range
LOGS_ENABLED true Enable log export when OTel is on
LOGS_EXPORTER console Unique logs exporter: console or otlp
FASTAPI_EXCLUDED_URLS None Comma-separated URL patterns skipped by FastAPI
FASTAPI_OPERATION_SPANS_ENABLED true FastAPI child spans (dependencies, endpoint, serialization, background tasks)
FASTAPI_LOGS_ENABLED false FastAPI log records for validation failures and unhandled exceptions (includes stack traces)
RESOURCE_ATTRIBUTES {} Extra OTel resource attributes
LOGS_LEVEL INFO Minimum level for the root-logger handler

Pushgateway and several replicas

A Pushgateway push replaces its whole group, so replicas that share a group overwrite each other's metrics. ArchiPy therefore adds an instance label to the grouping key unless you set one: POD_NAME, else HOSTNAME (or the host name). On Kubernetes the hostname is the pod name unless the pod uses hostNetwork; set POD_NAME explicitly with the downward API:

env:
  - name: POD_NAME
    valueFrom:
      fieldRef:
        fieldPath: metadata.name

On plain Docker the hostname is the container ID, because the container name is not visible inside the container.

Warning

Pushgateway keeps the last push of a group until it is deleted. A graceful shutdown deletes the pod's group (METRICS_PUSHGATEWAY_DELETE_ON_SHUTDOWN), but a crashed pod leaves a stale group, and pod names change on every restart. Clean stale groups up, for example with a CronJob that deletes groups that have not pushed recently.

Standard OTEL_* variables

With OTEL__USE_STANDARD_ENV=true, the standard variables fill in every setting that ArchiPy's own config leaves unset. A field set in code, in an OTEL__* variable or in TOML always wins. An invalid value is logged and ignored, and so are values that are valid alone but invalid together (for example a batch size above the queue size).

Variable Sets
OTEL_SERVICE_NAME SERVICE_NAME
OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_EXPORTER_OTLP_{TRACES,METRICS,LOGS}_ENDPOINT OTLP_ENDPOINT, TRACES_ENDPOINT, METRICS_ENDPOINT, LOGS_ENDPOINT
OTEL_EXPORTER_OTLP_PROTOCOL (grpc, http/protobuf) PROTOCOL
OTEL_EXPORTER_OTLP_HEADERS (k=v,k=v) OTLP_HEADERS
OTEL_EXPORTER_OTLP_TIMEOUT (seconds, as the Python SDK reads it) TIMEOUT
OTEL_EXPORTER_OTLP_COMPRESSION (none, gzip) OTLP_COMPRESSION
OTEL_TRACES_SAMPLER, OTEL_TRACES_SAMPLER_ARG TRACES_SAMPLER, TRACES_SAMPLE_RATIO (the Jaeger and X-Ray samplers are not supported)
OTEL_PROPAGATORS PROPAGATORS
OTEL_BSP_*, OTEL_BLRP_* SPAN_BATCH_*, LOG_BATCH_*
OTEL_METRIC_EXPORT_INTERVAL METRIC_EXPORT_INTERVAL_MS
OTEL_SDK_DISABLED=true Keeps OpenTelemetry off, whatever IS_ENABLED says

OTEL_RESOURCE_ATTRIBUTES and OTEL_SERVICE_NAME also reach the resource without the switch, because the SDK's Resource.create reads them; explicit RESOURCE_ATTRIBUTES and SERVICE_NAME override them.

Metrics exporters

Exactly one metrics exporter when METRICS_ENABLED is true — set via METRICS_EXPORTER (otlp | pull | pushgateway):

Mode METRICS_EXPORTER Behavior
OTLP (default) otlp Periodic export to the OTLP collector
Pull scrape pull Dedicated HTTP server on METRICS_PULL_HOST:METRICS_PULL_PORT/metrics
Pushgateway pushgateway Daemon thread pushes Prometheus text to Pushgateway (no local scrape)

otlp and pushgateway both push from the process, but to different wire targets: OTLP collector vs Prometheus Pushgateway.

Pull scrape is a standalone process-wide HTTP server started from OtelUtils.init_otel_if_needed — not a FastAPI /metrics route. Works for FastAPI, gRPC, and workers. If the scrape port cannot bind, ArchiPy logs a warning and keeps traces/logs/metrics providers running.

# Pull-only (no collector)
OTEL__METRICS_ENABLED=true
OTEL__METRICS_EXPORTER=pull
OTEL__METRICS_PULL_HOST=0.0.0.0
OTEL__METRICS_PULL_PORT=8200
# Prometheus Pushgateway (no local /metrics server)
OTEL__METRICS_ENABLED=true
OTEL__METRICS_EXPORTER=pushgateway
OTEL__METRICS_PUSHGATEWAY_URL=http://pushgateway.monitoring:9091
OTEL__METRICS_PUSHGATEWAY_JOB=my-service
OTEL__METRICS_PUSHGATEWAY_INTERVAL_SECONDS=60

OtelUtils.metrics_registry() returns the Prometheus CollectorRegistry when exporter is pull or pushgateway.

Logs exporters

Exactly one logs exporter when LOGS_ENABLED is true — set via LOGS_EXPORTER (console | otlp):

Mode LOGS_EXPORTER Behavior
Console (default) console INFO/DEBUG → stdout; WARNING+ → stderr
OTLP otlp Batch export to the OTLP collector
# OTLP log export
OTEL__LOGS_ENABLED=true
OTEL__LOGS_EXPORTER=otlp

Warning (scrape security): The pull scrape endpoint is unauthenticated. Default bind 0.0.0.0 is for cluster scrape. Prefer 127.0.0.1 plus a sidecar/proxy when the network is not locked down. Do not expose scrape on the public internet.

Warning (prefork): The parent process holds the scrape port. With gunicorn preload + fork, workers cannot share one scrape port — use a unique port per worker or disable preload.

Note (HTTP endpoints): With PROTOCOL=http/protobuf, each signal needs its own path (/v1/traces, /v1/metrics, /v1/logs). When OTLP_ENDPOINT has no path (e.g. http://localhost:4318), ArchiPy appends the correct /v1/{signal} suffix automatically. Set TRACES_ENDPOINT / METRICS_ENDPOINT / LOGS_ENDPOINT to override a single signal. gRPC mode uses one shared endpoint and needs no path.

Warning (logs cost): When LOGS_ENABLED=true, a LoggingHandler is attached to the root logger at LOGS_LEVEL. With LOGS_EXPORTER=otlp, every matching record from every library (httpx, sqlalchemy, urllib3, …) is exported to OTLP — prefer WARNING in production. console (default) writes locally only. Records from opentelemetry.* loggers are filtered out to avoid exporter feedback loops.

Note (provider globals): ArchiPy sets the global tracer/meter provider once (_globals_set). If another library already installed a global provider first, FastAPI/gRPC/Temporal interceptors (global) and ArchiPy decorators (owned provider) may emit to different backends — and ArchiPy OTLP/pull exporter configuration is ignored for an adopted MeterProvider. Initialize ArchiPy OTel early via AppUtils or OtelUtils.init_otel_if_needed.

Note (instrument names): The OTel SDK lowercases instrument names (TestMethod → testmethod). Prefer lowercase dotted names in dashboards.


Initialization Order

Call OtelUtils.init_otel_if_needed(config) at bootstrap — before your DI container builds adapters — right after BaseConfig.set_global(config). Init also installs the W3C TraceContext + Baggage textmap propagator (traceparent / tracestate / baggage) so another library cannot silently replace the global propagator first.

import logging

from archipy.configs.base_config import BaseConfig
from archipy.helpers.utils.otel_utils import OtelUtils

logger = logging.getLogger(__name__)


class AppConfig(BaseConfig):
    """Application configuration with OpenTelemetry enabled."""


config = AppConfig()
BaseConfig.set_global(config)
OtelUtils.init_otel_if_needed(config)
logger.info("OTel initialized before adapter construction")

# Only now build the DI container / adapters

The call is idempotent and thread-safe — AppUtils.create_fastapi_app / create_grpc_app invoke it again safely.

Warning: If adapters are constructed before OTel initialization, some telemetry is lost permanently:

  • SQLAlchemy — engines created before SQLAlchemyInstrumentor runs are never traced (only future create_engine calls are wrapped).
  • Confluent Kafka — producers/consumers instantiated before instrumentation stay unwrapped.
  • Cassandra/ScyllaDB — sessions created early miss span wrapping.
  • Logs — records emitted before init bypass the OTLP logging handler.

Tracers and meters obtained via OtelUtils.get_tracer / get_meter before init recover automatically (the global proxy provider resolves once providers are set), as do Redis, requests, httpx, and httpx2 clients — their instrumentors patch at class level.


Auto-instrumentation via AppUtils

FastAPI

AppUtils.create_fastapi_app passes FastAPIUtils.build_otel_telemetry_config(config) to FastAPI(telemetry=...). That initializes providers (idempotent) and enables FastAPI's built-in OpenTelemetry support (requires archipy[otel,fastapi], FastAPI 0.142.2+). Traces and metrics use ArchiPy's providers; OTLP auto-configuration is always off. FastAPI log records (validation failures and unhandled exceptions, with stack traces) are opt-in via OTEL__FASTAPI_LOGS_ENABLED=true and use ArchiPy's logger provider. The HTTP duration metric is http.server.request.duration (seconds). If you build the app yourself, pass the same config to the constructor:

app = FastAPI(telemetry=FastAPIUtils.build_otel_telemetry_config(config))
import logging

from archipy.configs.base_config import BaseConfig
from archipy.helpers.utils.app_utils import AppUtils

logger = logging.getLogger(__name__)

config = BaseConfig.global_config()
app = AppUtils.create_fastapi_app(config)
logger.info("FastAPI app created with OTel auto-instrumentation")

gRPC

AppUtils.create_grpc_app / create_async_grpc_app install OpenTelemetry server interceptors when OTel is enabled (requires archipy[otel-grpc] for traces):

  • Traces — contrib server_interceptor / aio_server_interceptor when TRACES_ENABLED
  • Metrics — the OpenTelemetry rpc.server.call.duration histogram (seconds, same buckets as FastAPI's http.server.request.duration) and an ArchiPy rpc.server.active_requests up-down counter (in-flight calls; the specification does not define it) when METRICS_ENABLED (works in metrics-only mode; no handler decorators required). The duration histogram follows the RPC semantic conventions: rpc.system.name (grpc), the fully qualified rpc.method (package.Service/Method) and the rpc.response.status_code name the call finished with (OK, NOT_FOUND, CANCELLED, ...). error.type is set to the status code only for the codes the conventions treat as server failures (UNKNOWN, DEADLINE_EXCEEDED, UNIMPLEMENTED, INTERNAL, UNAVAILABLE, DATA_LOSS); handled caller errors such as NOT_FOUND or RESOURCE_EXHAUSTED and client cancellations carry a status code but no error.type. A handler that raises without a status is recorded as UNKNOWN, which is what gRPC answers.

Renamed in this release

The histogram used to be rpc.server.request.duration with rpc.system, rpc.service, a short rpc.method, rpc.grpc.status_code and a custom status attribute. Update dashboards and alerts to the names above.

Order becomes: OTel traces (if any) → OTel metrics (if any) → exception interceptor → rate-limit (if enabled) → custom interceptors.

import logging

from archipy.configs.base_config import BaseConfig
from archipy.helpers.utils.app_utils import AppUtils

logger = logging.getLogger(__name__)

config = BaseConfig.global_config()
server = AppUtils.create_grpc_app(config)
logger.info("gRPC server created with OTel interceptor")

Library instrumentors

On first OtelUtils.init_otel_if_needed, ArchiPy best-effort instruments installed contrib packages (SQLAlchemy, Redis, Confluent Kafka, Cassandra, Botocore, httpx, httpx2, requests, threading) when the matching otel-* extras are present. Instrumentors whose target library is not installed (e.g. httpx when only httpx2 is present) are skipped silently. Elasticsearch needs no instrumentor — the elasticsearch client emits OTel spans natively. SystemMetricsInstrumentor runs only when METRICS_ENABLED and SYSTEM_METRICS_ENABLED are both true.

Version ranges

Each instrumentor declares the library versions it supports. When a library is installed outside that range, ArchiPy logs a warning (no spans will be emitted) instead of staying silent. A library that is simply not installed is skipped at debug level.

opentelemetry-instrumentation-sqlalchemy still declares sqlalchemy<2.1.0, while ArchiPy requires SQLAlchemy 2.1. The cap sits in the instrumentor's instruments extra, which is not a hard dependency, so installs resolve without error and the instrumentor only refuses at runtime. To keep SQLAlchemy spans, OTEL__INSTRUMENTATION_SKIP_DEP_CHECK (default ["sqlalchemy"]) lists the libraries that are instrumented anyway; ArchiPy logs one warning naming the mismatch. Set it to [] to turn the bypass off. Remove a key once its instrumentor supports the installed version: make check-instrumentor-compat resolves the instrumentor's instruments extra together with ArchiPy's own SQLAlchemy bound and reports when upstream has caught up (a weekly CI job runs it too).

Pydantic AI

With archipy[otel-pydantic-ai] installed, OtelUtils.init_otel_if_needed calls Agent.instrument_all with ArchiPy's tracer and meter providers, so agent runs, model requests and tool calls become spans and token usage becomes a metric. An agent the application instrumented itself is left alone. Settings live in BaseConfig.PYDANTIC_AI:

Field Default Description
INSTRUMENTATION_ENABLED true Instrument Pydantic AI agents when OpenTelemetry is enabled
INSTRUMENTATION_INCLUDE_CONTENT true Prompts, model output and tool arguments in span attributes (turn off for personal data)
INSTRUMENTATION_INCLUDE_BINARY_CONTENT false Binary content (images, audio, documents) in span attributes
INSTRUMENTATION_INCLUDE_MODEL_REQUEST_PARAMETERS true Model request parameters (temperature, max tokens, ...) in model request spans
INSTRUMENTATION_VERSION 6 Span and attribute format version (5 or 6)
INSTRUMENTATION_USE_AGGREGATED_USAGE_ATTRIBUTE_NAMES true Name the agent run usage attributes gen_ai.aggregated_usage.*

Client gRPC

For outbound gRPC clients, attach contrib interceptors explicitly:

import logging

import grpc

from archipy.helpers.utils.otel_utils import OtelUtils

logger = logging.getLogger(__name__)

channel = grpc.intercept_channel(
    grpc.insecure_channel("localhost:50051"),
    *OtelUtils.grpc_client_interceptors(),
)
logger.info("gRPC client channel wrapped with OTel interceptors")

For async clients use OtelUtils.async_grpc_client_interceptors().


Tracing Decorators

Every decorator span records code.function.name (the fully qualified name of the traced function), and a span for a failed call also carries error.type (the BaseError.code, or the fully qualified exception class) next to the recorded exception event.

For code outside FastAPI/gRPC — workers, schedulers, domain logic — use decorators from archipy.helpers.decorators:

Decorator Purpose
@trace_span Sync child span
@trace_span Async child span
@trace_root Sync root span (entry point)
@trace_root Async root span
@trace_class Wrap public methods of a class with spans
import logging

from archipy.helpers.decorators import trace_span, trace_root, trace_span

logger = logging.getLogger(__name__)


@trace_root(name="process_order")
def process_order(order_id: int) -> dict[str, int | float]:
    """Process a single order end-to-end.

    Args:
        order_id: The order to process.

    Returns:
        A summary of the processing result.
    """
    items = fetch_order_items(order_id)
    total = calculate_total(items)
    return {"order_id": order_id, "total": total}


@trace_span(name="fetch_order_items", capture_args=["order_id"])
def fetch_order_items(order_id: int) -> list[dict[str, float]]:
    """Load order items from the database.

    Args:
        order_id: The order to load items for.

    Returns:
        List of order item dicts.
    """
    logger.debug("Fetching items for order %d", order_id)
    return [{"price": 10.0}]


@trace_span(name="calculate_total")
def calculate_total(items: list[dict[str, float]]) -> float:
    """Sum item prices.

    Args:
        items: List of order item dicts.

    Returns:
        Total order value.
    """
    return sum(item["price"] for item in items)

Decorators no-op when OTEL.IS_ENABLED is false. On exceptions they set span status via OtelUtils.status_for_exception (BaseError with HTTP status below 500 leaves status UNSET per the OTel spec for handled client errors; other exceptions become ERROR).

Warning (capture_args): Prefer non-sensitive identifiers (user_id, order_id). Names matching a denylist (password, token, secret, authorization, api_key, …) are recorded as ***. The denylist is substring-based and case-insensitive — still avoid capturing free-form blobs that may embed secrets.


Metrics Decorators

Decorator Purpose
@measure_duration Sync duration histogram
@measure_duration Async duration histogram
@count_calls Sync call counter
@count_calls Async call counter
from archipy.helpers.decorators import count_calls, measure_duration


@measure_duration(attributes={"layer": "logic"})
@count_calls()
def process_payment(amount: float) -> None:
    """Charge a payment amount.

    Args:
        amount: Amount to charge.
    """
    ...

Instruments default to {module}.{qualname}.duration / .calls and record a status attribute (ok / error / cancelled for async cancel). Duration histograms use explicit second buckets and a description. status aligns with tracing: BaseError with HTTP status below 500 records as ok (handled client error), not error. A call that ends in error or cancelled also carries the standard error.type (the BaseError.code, or the fully qualified exception class); a call recorded as ok carries none. No-op when OTel or METRICS_ENABLED is off.


Exception Capture

archipy.helpers.errors.capture_exception always logs locally. When OTel is enabled and a recording span is active, it records the exception on the current span and sets span status — it does not send to Sentry or Elastic APM:

from archipy.helpers.errors import capture_exception
from archipy.models.errors import InternalError

try:
    raise InternalError()
except InternalError as exc:
    capture_exception(exc)
    raise

Temporal

Temporal metrics and traces reuse the global OTel config:

  • Metrics: set TEMPORAL__ENABLE_METRICS=true and OTEL__IS_ENABLED=true with OTEL__METRICS_ENABLED=true. The adapter builds a Temporal Runtime with OpenTelemetryConfig pointing at OTEL.OTLP_ENDPOINT / PROTOCOL / OTLP_HEADERS. With OTEL__METRICS_EXPORTER=pull the Temporal SDK serves its own /metrics on TEMPORAL__METRICS_PULL_PORT; with pushgateway it still exports over OTLP because the SDK cannot push.
  • Traces: when OTEL.IS_ENABLED and TRACES_ENABLED, the Temporal client attaches temporalio.contrib.opentelemetry.TracingInterceptor after OtelUtils.init_otel_if_needed.

See Temporal adapter for a full example.


Known Gaps

Area Status
Kafka aio otel-kafka covers Confluent Kafka sync instrumentation; async Kafka paths may not be instrumented
SMTP / email No OpenTelemetry instrumentation for smtplib / the email adapter

See Also