Exporting OTLP Metrics to the Collector

Getting aggregated metrics out of a Python process and into the OpenTelemetry Collector comes down to pairing an OTLPMetricExporter with a PeriodicExportingMetricReader and matching that to a Collector receiver. This guide is part of the Python Metrics and Instrumentation guide and extends the OpenTelemetry Metrics SDK section, which covers the full provider lifecycle; here the focus is purely the export hop and the Collector wiring on the other side.

OTLP metric export path The PeriodicExportingMetricReader collects in the Python process and the OTLP gRPC exporter sends to the Collector OTLP receiver on port 4317, which forwards to a metrics backend. Python process Periodic reader OTLP exporter Collector OTLP receiver :4317 gRPC Backend store gRPC export
The export hop: reader and exporter in-process, OTLP receiver and pipeline in the Collector.

Prerequisites

Install the SDK and the gRPC OTLP exporter with pinned ranges.

pip install \
  "opentelemetry-sdk>=1.30.0,<2.0.0" \
  "opentelemetry-exporter-otlp-proto-grpc>=1.30.0,<2.0.0"

Environment variables the exporter honors out of the box:

export OTEL_EXPORTER_OTLP_ENDPOINT="otel-collector:4317"
export OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE="delta"
export OTEL_METRIC_EXPORT_INTERVAL="15000"     # milliseconds
export OTEL_EXPORTER_OTLP_CERTIFICATE="/etc/otel/ca.pem"  # for TLS

Implementation

Step 1 — Construct the exporter against the Collector endpoint. The gRPC exporter takes a bare host:port, not a URL. Keep insecure=False for any non-local network and rely on TLS credentials; use insecure=True only against a trusted local Collector.

from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter

exporter = OTLPMetricExporter(
    endpoint="otel-collector:4317",   # host:port, no scheme
    insecure=False,                   # keep TLS on in production
    timeout=10,                       # seconds per export attempt
    headers=(("x-tenant", "team-checkout"),),  # optional auth/routing headers
)

Step 2 — Set a temporality preference. Delta keeps payloads small and suits backends that recompute rates per interval; cumulative re-states totals and survives dropped exports. Choose per instrument kind.

from opentelemetry.sdk.metrics import Counter, Histogram, ObservableCounter
from opentelemetry.sdk.metrics.export import AggregationTemporality

preferred = {
    Counter: AggregationTemporality.DELTA,
    Histogram: AggregationTemporality.DELTA,
    ObservableCounter: AggregationTemporality.CUMULATIVE,
}
exporter = OTLPMetricExporter(endpoint="otel-collector:4317", preferred_temporality=preferred)

Step 3 — Wrap the exporter in a periodic reader and register the provider. The reader owns the export interval and a per-export timeout; the provider owns the reader.

from opentelemetry import metrics
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader

reader = PeriodicExportingMetricReader(
    exporter,
    export_interval_millis=15000,
    export_timeout_millis=10000,
)
metrics.set_meter_provider(MeterProvider(metric_readers=[reader]))

The interval and timeout interact. export_interval_millis is how often the reader collects every instrument and ships a payload; export_timeout_millis is the deadline for that whole collect-and-send cycle. The timeout must be comfortably smaller than the interval, or a slow export overlaps the next one and the reader falls behind. A 15-second interval with a 10-second timeout leaves headroom for a retry. When the Collector is briefly unreachable the gRPC exporter retries with backoff inside the timeout window and then drops that cycle's batch; under delta temporality the dropped window is lost, while under cumulative the next successful export re-states the running total, which is one more reason the temporality choice matters for reliability and not just payload size.

The reader runs export on its own background thread, woken every interval, and it does not maintain a queue of pending batches across cycles. This is the crucial backpressure property: if a cycle takes longer than the interval the reader simply starts the next collection late rather than letting work pile up, so a struggling Collector slows the cadence instead of growing unbounded memory in the process. Within a single cycle the gRPC exporter handles transient failures itself. A StatusCode.UNAVAILABLE or DEADLINE_EXCEEDED is treated as retryable and retried with exponential backoff, starting near one second and roughly doubling, until either the export succeeds or the cumulative time approaches export_timeout_millis, at which point the batch is abandoned. Non-retryable status codes such as INVALID_ARGUMENT fail immediately without retry, because resending an identical malformed payload cannot help. Because the abandoned batch is never re-queued, the timeout is effectively your data-loss budget per outage: a longer timeout buys more in-cycle retries against a flapping Collector but eats into the interval headroom, so size the two together rather than independently.

Step 4 — Enable the matching receiver on the Collector. The receiver listens on 4317 for gRPC; a pipeline routes metrics from the receiver through a batch processor to an exporter. This snippet logs metrics so you can confirm arrival before adding a real backend.

# otel-collector-config.yaml
receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317

processors:
  batch:
    timeout: 10s

exporters:
  debug:
    verbosity: detailed

service:
  pipelines:
    metrics:
      receivers: [otlp]
      processors: [batch]
      exporters: [debug]

Securing the channel

In production the exporter should authenticate the Collector and encrypt the link. Leave insecure at its default and pass a CA certificate so the gRPC channel verifies the server, then attach credentials or headers for tenant routing if your Collector fronts multiple teams. Reserve insecure=True for a Collector running on the same host or inside the same pod network namespace, where the traffic never crosses an untrusted boundary. Prefer the OTEL_EXPORTER_OTLP_ENDPOINT and OTEL_EXPORTER_OTLP_CERTIFICATE environment variables over hardcoded values so the same image runs unchanged across environments and the deployment system controls routing. When the Collector sits behind a load balancer, point the endpoint at the balancer and let it terminate or pass through TLS; the exporter multiplexes many exports over one long-lived gRPC connection, so keepalive on the balancer matters more than connection count.

Headers are the mechanism for both authentication and multitenancy. The headers argument takes a tuple of key-value pairs that are attached as gRPC metadata on every export; a managed backend that accepts OTLP directly usually wants an authorization or vendor-specific API-key header here, and a shared Collector uses a routing header like x-tenant to fan traffic to the right pipeline. Headers can equally be supplied out of band through OTEL_EXPORTER_OTLP_HEADERS as a comma-separated key=value list, which keeps secrets out of the image. When you provide explicit TLS credentials, build them from the CA bundle and pass them as credentials, which takes precedence over the environment certificate path.

import grpc
from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter

with open("/etc/otel/ca.pem", "rb") as fh:
    creds = grpc.ssl_channel_credentials(root_certificates=fh.read())

exporter = OTLPMetricExporter(
    endpoint="otel-collector:4317",
    credentials=creds,                         # verifies the server cert
    headers=(("authorization", "Bearer ${OTLP_TOKEN}"),),
    timeout=10,
)

Expected Output:

INFO opentelemetry.sdk.metrics.export MetricExporter started; endpoint=otel-collector:4317 tls=enabled

Configuration options

Setting Where Default Notes
endpoint exporter / OTEL_EXPORTER_OTLP_ENDPOINT localhost:4317 gRPC uses host:port, no scheme
insecure exporter False True disables TLS; local only
timeout exporter 10 s per-export deadline
headers exporter / OTEL_EXPORTER_OTLP_HEADERS none auth, tenant routing
preferred_temporality exporter / ..._TEMPORALITY_PREFERENCE cumulative delta vs cumulative per kind
export_interval_millis reader / OTEL_METRIC_EXPORT_INTERVAL 60000 collection cadence
export_timeout_millis reader / OTEL_METRIC_EXPORT_TIMEOUT 30000 whole-collection deadline
Collector endpoint receiver YAML 0.0.0.0:4317 must match exporter port

Verification

Run the Python process for one interval, then check the Collector's debug output. A successful delta export prints a resource block, a scope, and data points.

Expected Output (Collector debug exporter):

2026-06-19T10:14:32Z info  MetricsExporter  {"kind": "exporter", "data_type": "metrics", "name": "debug", "resource metrics": 1, "metrics": 2, "data points": 2}
Metric #0
Descriptor:
     -> Name: http.server.request.count
     -> Unit: {request}
     -> DataType: Sum
     -> IsMonotonic: true
     -> AggregationTemporality: Delta
NumberDataPoints #0
Data point attributes:
     -> http.route: Str(/checkout)
Value: 50

To verify without a Collector, point the exporter's endpoint at a closed port and confirm the SDK logs a retry rather than crashing:

Expected Output (exporter retry log):

WARNING opentelemetry.exporter.otlp.proto.grpc.exporter Transient error StatusCode.UNAVAILABLE encountered while exporting metrics to otel-collector:4317, retrying in 1s.

Common mistakes

StatusCode.UNAVAILABLE on every export. The exporter cannot reach the Collector. Root cause is usually a scheme in the gRPC endpoint (http://otel-collector:4317) or a port mismatch. Remediation: use a bare host:port and confirm the Collector OTLP gRPC receiver listens on the same port.

Metrics arrive but rates look doubled or halved. The exporter's temporality preference disagrees with what the backend expects. Remediation: align preferred_temporality (or OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE) with the backend; use cumulative for Prometheus-style stores and delta for per-interval gateways.

Final window missing after deploys. A rolling restart kills the process before the next interval. Remediation: call provider.shutdown() (or force_flush()) in your shutdown handler so the reader flushes the pending collection before exit.

Exports silently stop keeping up under load. Symptom: gaps in dashboards with no error logs, and the export-cycle duration creeping toward the interval. Root cause is an export_timeout_millis set equal to or larger than export_interval_millis, so a slow Collector lets one cycle run into the next and the reader perpetually starts late. Remediation: keep the timeout comfortably below the interval (for example 10s timeout against a 15s interval), and if cycles are genuinely slow, lengthen the interval rather than the timeout so retries fit inside one cycle.

Frequently Asked Questions

What endpoint format does the gRPC metric exporter expect?

The gRPC OTLPMetricExporter takes a host:port endpoint such as otel-collector:4317 without an http scheme. The HTTP exporter, by contrast, expects a full URL with a path. Mixing the two formats is the most common cause of connection failures.

How do I enable TLS to the Collector?

Leave insecure unset or False and supply a certificate through the credentials argument or the OTEL_EXPORTER_OTLP_CERTIFICATE environment variable. Use insecure=True only on a trusted local network, since it disables transport encryption entirely.

What export interval should I choose?

Fifteen to sixty seconds suits most services. Shorter intervals raise network and Collector load without improving dashboards that aggregate over minutes, while very long intervals delay alerting and risk losing the final window if a process crashes.

What happens when the Collector is down during an export?

The gRPC exporter retries the failed batch with exponential backoff bounded by the export timeout, then drops that batch. The reader does not queue across cycles, so under delta temporality the dropped window is lost and under cumulative the next successful export re-states the running total.