OpenTelemetry Collector Configuration Tutorial

OpenTelemetry Collector receives, processes, and exports telemetry data (traces, metrics, logs) from applications. This flow of receiving and forwarding telemetry data is described in more detail in OpenTelemetry Collector ingestion. Configure receivers (OTLP, Prometheus), processors (batch, sampling), and exporters (OTLP, cloud providers) using YAML. Default ports: 4317 (gRPC), 4318 (HTTP). Validate with otelcol-contrib validate --config=config.yaml.

Collector architecture

text
[Your App] → [Receivers] → [Processors] → [Exporters] → [Backend]

For what each stage does, see how the Collector works. What matters when writing the config is the wiring: components are declared in their own top-level blocks and activated in service.pipelines. Declaring a receiver does not start it.

One declaration can serve several pipelines — a single otlp receiver feeds traces, metrics, and logs at once — and pipelines of the same signal can run side by side with different processing.

Quick reference

The parts you will reach for on day one.

Essential components to remember

  • otlp receiver for modern apps
  • batch processor for performance
  • memory_limiter processor for stability
  • debug exporter for testing

Default ports

  • 4317: OTLP gRPC
  • 4318: OTLP HTTP
  • 8888: Collector metrics
  • 8889: Prometheus exporter (if configured)

Useful commands

Validate before you restart anything — a bad config makes the Collector exit rather than fall back.

bash
# Validate config without starting the Collector
otelcol-contrib validate --config=config.yaml

# Run with debug logging
otelcol-contrib --config=config.yaml --set=service.telemetry.logs.level=debug

# Read the Collector's own metrics
curl http://localhost:8888/metrics

Basic configuration structure

Every Collector config follows this YAML structure:

yaml
# collector-config.yaml
receivers:
  # How to receive data

processors:
  # How to process data (optional)

exporters:
  # Where to send data

service:
  pipelines:
    # Connect everything together

First configuration

Let's start with a minimal setup that receives OTLP data and exports it to the console:

yaml
# basic-config.yaml
receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
      http:
        endpoint: 0.0.0.0:4318

exporters:
  debug:
    verbosity: detailed

service:
  pipelines:
    traces:
      receivers: [otlp]
      exporters: [debug]
    metrics:
      receivers: [otlp]
      exporters: [debug]
    logs:
      receivers: [otlp]
      exporters: [debug]

Run it:

bash
otelcol-contrib --config=basic-config.yaml

This config:

  • Accepts OTLP data on standard ports (4317 for gRPC, 4318 for HTTP)
  • Prints all received data to the console
  • Handles traces, metrics, and logs separately

Configuration example

Here's a more practical setup that you might use in production:

yaml
# production-config.yaml
receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
      http:
        endpoint: 0.0.0.0:4318

  # Scrape Prometheus metrics
  prometheus:
    config:
      scrape_configs:
        - job_name: 'my-service'
          static_configs:
            - targets: ['localhost:8080']

processors:
  # Protect against OOM
  memory_limiter:
    limit_mib: 512
    spike_limit_mib: 128
    check_interval: 1s

  # Sample traces to reduce volume
  probabilistic_sampler:
    sampling_percentage: 10.0

  # Add resource attributes
  resource:
    attributes:
      - key: environment
        value: production
        action: upsert
      - key: service.version
        from_attribute: app.version
        action: insert

  # Batch data for efficiency
  batch:
    timeout: 1s
    send_batch_size: 1024

exporters:
  # Export via OTLP to any compatible backend
  otlp/backend:
    endpoint: your-backend:4317
    tls:
      insecure: true

  # Export to Prometheus
  prometheus:
    endpoint: "0.0.0.0:8889"

  # Export to Uptrace
  otlp/uptrace:
    endpoint: https://api.uptrace.dev:4317
    headers:
      "uptrace-dsn": "${UPTRACE_DSN}"

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [memory_limiter, probabilistic_sampler, resource, batch]
      exporters: [otlp/backend, otlp/uptrace]

    metrics:
      receivers: [otlp, prometheus]
      processors: [memory_limiter, resource, batch]
      exporters: [prometheus, otlp/uptrace]

    logs:
      receivers: [otlp]
      processors: [memory_limiter, resource, batch]
      exporters: [otlp/uptrace]

Configuration patterns

Three patterns cover most of what teams actually change after the first config works.

Pattern 1: multi-environment setup

Use different configs for dev/staging/prod:

yaml
# Use environment variables for flexibility
exporters:
  otlp:
    endpoint: ${env:OTLP_ENDPOINT}

processors:
  probabilistic_sampler:
    sampling_percentage: ${env:SAMPLING_RATE:-100.0}  # Default 100% if not set

Pattern 2: data enrichment

Add context to your telemetry:

yaml
processors:
  resource:
    attributes:
      - key: k8s.cluster.name
        value: ${env:K8S_CLUSTER_NAME}
        action: upsert
      - key: deployment.environment
        value: ${env:ENVIRONMENT}
        action: upsert

  transform:
    trace_statements:
      - context: span
        statements:
          - set(attributes["custom.field"], "processed-by-collector")

Pattern 3: data filtering

Remove unwanted data using OTTL conditions:

yaml
processors:
  filter:
    error_mode: ignore
    traces:
      span:
        - 'attributes["http.route"] == "/health"'
        - 'name == "GET /metrics"'
    metrics:
      metric:
        - 'name == "unwanted_metric"'

Receiver

Receivers are how data gets in. Most pipelines need exactly one — otlp — plus another only when you are collecting from something that cannot send OTLP itself.

OTLP receiver (most common)

Accepts data from any OpenTelemetry SDK. Bind to 0.0.0.0 inside containers, or the Collector will only accept connections from itself.

yaml
receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
        # Optional: TLS configuration
        tls:
          cert_file: /path/to/cert.pem
          key_file: /path/to/key.pem
      http:
        endpoint: 0.0.0.0:4318
        cors:
          allowed_origins: ["*"]  # Be more restrictive in production

Prometheus receiver

Scrapes existing /metrics endpoints, so services already exposing Prometheus metrics need no code changes. See Prometheus integration for the full setup, and host metrics for CPU, memory, and disk.

yaml
receivers:
  prometheus:
    config:
      scrape_configs:
        - job_name: 'my-app'
          scrape_interval: 30s
          static_configs:
            - targets: ['app:8080']
          metrics_path: /metrics

Filelog receiver (for logs)

Tails log files and parses them into OTLP records — the usual route when your language's logs SDK is not stable yet.

yaml
receivers:
  filelog:
    include: [/var/log/myapp/*.log]
    operators:
      - type: json_parser
        timestamp:
          parse_from: attributes.timestamp
          layout: '%Y-%m-%d %H:%M:%S'

Processor

Processors run in the order you list them in the pipeline. Two belong in every production config: memory_limiter first, batch last.

Batch processor (essential for performance)

Groups records before export. Without it, every span becomes its own request.

yaml
processors:
  batch:
    timeout: 1s           # How long to wait before sending
    send_batch_size: 512  # Send when this many items collected
    send_batch_max_size: 1024  # Never exceed this size

Sampling processors

probabilistic_sampler drops a fixed share cheaply. tail_sampling waits for the whole trace and can keep exactly the errors and slow requests — at the cost of buffering every span in memory until it decides. See sampling for the trade-off and the replica constraint.

yaml
processors:
  # Sample 10% of traces
  probabilistic_sampler:
    sampling_percentage: 10.0

  # More sophisticated sampling
  tail_sampling:
    decision_wait: 10s
    policies:
      - name: error-traces
        type: status_code
        status_code: {status_codes: [ERROR]}
      - name: slow-traces
        type: latency
        latency: {threshold_ms: 1000}
      - name: random-sample
        type: probabilistic
        probabilistic: {sampling_percentage: 1.0}

Exporter

Exporters send processed data to one or more backends. For complete exporter configuration including all available backends, authentication, and production patterns, see OpenTelemetry Collector Exporters.

Cloud provider exporters

For sending to a managed backend that does not accept OTLP directly.

yaml
exporters:
  # Google Cloud
  googlecloud:
    project: my-gcp-project

  # AWS X-Ray
  awsxray:
    region: us-west-2

  # Azure Monitor
  azuremonitor:
    connection_string: ${env:APPLICATIONINSIGHTS_CONNECTION_STRING}

File exporter (for debugging)

Writes raw telemetry to disk, which is the fastest way to confirm what actually reached the Collector.

yaml
exporters:
  file:
    path: /tmp/otel-data.json
    rotation:
      max_megabytes: 100
      max_days: 7
      max_backups: 3

Environment variables and secrets

Keep sensitive data out of your config files:

yaml
# In your config
exporters:
  otlp/backend:
    endpoint: ${BACKEND_ENDPOINT}
    headers:
      authorization: "Bearer ${API_TOKEN}"
bash
# In your environment
export BACKEND_ENDPOINT="https://api.example.com"
export API_TOKEN="your-secret-token"

Docker deployment

Here's a complete Docker setup:

dockerfile
# Dockerfile
FROM otel/opentelemetry-collector-contrib:latest
COPY collector-config.yaml /etc/otelcol-contrib/config.yaml
EXPOSE 4317 4318 8889
yaml
# docker-compose.yml
services:
  otel-collector:
    build: .
    ports:
      - "4317:4317"   # OTLP gRPC
      - "4318:4318"   # OTLP HTTP
      - "8889:8889"   # Prometheus metrics
    environment:
      - OTLP_ENDPOINT=otelcol-gateway:4317
    volumes:
      - ./logs:/var/log

This setup works well for local development and simple deployments. In production environments, the Collector is typically deployed using orchestration platforms like Kubernetes, where you can manage scaling, configuration, and service discovery more effectively. For example, you can monitor Kubernetes with OpenTelemetry Collector to collect telemetry from your cluster.

Testing your configuration

Three checks, in order: does the config parse, is the Collector running, does data get through.

1. Validate syntax

bash
otelcol-contrib validate --config=your-config.yaml

2. Check what's running

The Collector reports on itself. otelcol_receiver_accepted_spans and otelcol_exporter_sent_spans tell you whether data is entering and leaving.

bash
# The collector exposes metrics about itself
curl http://localhost:8888/metrics

3. Send test data

A single span over OTLP/HTTP, no SDK involved — it isolates the Collector from your application.

bash
# Send a test trace using curl
curl -X POST http://localhost:4318/v1/traces \
  -H "Content-Type: application/json" \
  -d '{
    "resourceSpans": [{
      "resource": {"attributes": [{"key": "service.name", "value": {"stringValue": "test-service"}}]},
      "scopeSpans": [{
        "spans": [{
          "traceId": "5b8aa5a2d2c872e8321cf37308d69df2",
          "spanId": "051581bf3cb55c13",
          "name": "test-span",
          "kind": "SPAN_KIND_CLIENT",
          "startTimeUnixNano": "1640995200000000000",
          "endTimeUnixNano": "1640995200100000000"
        }]
      }]
    }]
  }'

Common troubleshooting

For the full pipeline-by-pipeline procedure, see OpenTelemetry troubleshooting.

Configuration not loading

  • Check YAML syntax (indentation matters!)
  • Verify file permissions
  • Look for typos in component names

Data not flowing

  • Check that receivers are listening on the right ports
  • Verify pipeline connections (receivers → processors → exporters)
  • Look at Collector logs: otelcol-contrib --config=config.yaml --set=service.telemetry.logs.level=debug

Performance issues

  • Add batch processor if missing
  • Reduce sampling rates
  • Check memory_limiter processor configuration

Memory usage growing

Add memory_limiter as the first processor in every pipeline so it can reject data before the rest of the chain allocates for it.

yaml
processors:
  memory_limiter:
    limit_mib: 512
    spike_limit_mib: 128

Advanced use cases

Multi-pipeline setup

Pipelines of the same type can run side by side with different processing. A common use is keeping every error trace while sampling the rest.

yaml
service:
  pipelines:
    # High-priority traces (errors) with no sampling
    traces/errors:
      receivers: [otlp]
      processors: [filter/errors, batch]
      exporters: [otlp/backend]

    # Normal traces with sampling
    traces/sampled:
      receivers: [otlp]
      processors: [filter/normal, probabilistic_sampler, batch]
      exporters: [otlp/backend]

Data routing by attributes

The routing processor is deprecated. Use the routing connector, which routes to pipelines rather than straight to exporters — so you can still run processors after the routing decision. It is declared under connectors: and appears as an exporter in the incoming pipeline and as a receiver in each destination.

yaml
connectors:
  routing:
    default_pipelines: [traces/other]
    table:
      - context: resource
        condition: attributes["service.name"] == "frontend"
        pipelines: [traces/frontend]

service:
  pipelines:
    traces/in:
      receivers: [otlp]
      processors: [batch]
      exporters: [routing]

    traces/frontend:
      receivers: [routing]
      exporters: [otlp/frontend]

    traces/other:
      receivers: [routing]
      exporters: [otlp/backend]

What's next?