All posts
Engineering

Run an OpenTelemetry Collector with TracePath

A collector buffers, redacts and samples telemetry between your apps and your backend. Here is a minimal OTLP config that forwards to TracePath.

An OpenTelemetry Collector is a standalone process that receives OTLP from your applications, optionally reshapes it, and forwards it somewhere else. You do not need one to use TracePath: every OpenTelemetry SDK can export straight to https://ingest.tracepath.dev/api/otel. You want one once you need buffering across deploys, a single place to strip personal data, tail-based sampling, or one egress point through a firewall.

This guide covers when a collector earns its place, the smallest configuration that forwards every signal to TracePath, how to run it as a service, and the two processors that justify the extra hop.

Key takeaways

  • The Collector is optional. Start with SDK-direct export and add a collector when you have a concrete reason.
  • The whole TracePath integration is one endpoint and one header: https://ingest.tracepath.dev/api/otel and Authorization: Bearer <project_token>.
  • TracePath speaks OTLP over HTTP only. Your collector can still accept gRPC from your apps; only the hop to TracePath must be otlphttp.
  • Keep exported batches well under the 10 MB body limit, and leave retry_on_failure on so a 503 is retried rather than dropped.
  • Tail sampling is the one processor that genuinely changes your bill, and the one with a real deployment constraint attached.

Do you actually need a collector?

Exporting straight from the SDK is fine, and often better, when:

  • You run a handful of services and each one already batches its own exports.
  • Your platform has no place to run a long-lived sidecar (serverless functions, Cloudflare Workers, managed PaaS containers).
  • You are still in the "is anything arriving at all?" phase. One less moving part is one less thing to misconfigure.

A collector earns its keep when at least one of these is true:

ReasonWhat the collector gives you
Deploy churnA queue that survives a rolling restart of your app, so spans are not lost while pods cycle
ComplianceOne place to redact attributes before anything leaves your network
Cost controlTail-based sampling: decide what to keep after you know how the trace ended
Network policyA single egress host, so app containers need no outbound internet access
Fan-outThe same pipeline exporting to TracePath and to an archive bucket during a migration
Non-OTLP sourcesReceivers for Prometheus scrape targets, syslog, journald, Kafka, and so on

If none of those apply yet, skip this article, point your SDK at the ingest endpoint, and come back when one does.

The minimal configuration

Use a distribution that carries the components below. The upstream otelcol-contrib release does; so do the vendor-neutral builds you can produce with the OpenTelemetry Collector Builder.

# /etc/otelcol/config.yaml
receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
      http:
        endpoint: 0.0.0.0:4318
 
processors:
  memory_limiter:
    check_interval: 1s
    limit_mib: 512
    spike_limit_mib: 128
  batch:
    timeout: 5s
    send_batch_size: 512
    send_batch_max_size: 1024
 
exporters:
  otlphttp/tracepath:
    endpoint: https://ingest.tracepath.dev/api/otel
    headers:
      Authorization: "Bearer ${env:TRACEPATH_TOKEN}"
    compression: gzip
    sending_queue:
      enabled: true
      queue_size: 5000
    retry_on_failure:
      enabled: true
      initial_interval: 5s
      max_interval: 30s
      max_elapsed_time: 5m
 
extensions:
  health_check:
    endpoint: 127.0.0.1:13133
 
service:
  extensions: [health_check]
  pipelines:
    traces:
      receivers: [otlp]
      processors: [memory_limiter, batch]
      exporters: [otlphttp/tracepath]
    metrics:
      receivers: [otlp]
      processors: [memory_limiter, batch]
      exporters: [otlphttp/tracepath]
    logs:
      receivers: [otlp]
      processors: [memory_limiter, batch]
      exporters: [otlphttp/tracepath]

Five details in there are worth knowing rather than copying:

The exporter endpoint has no signal path on it. The otlphttp exporter appends /v1/traces, /v1/metrics and /v1/logs itself, so one endpoint line covers all three pipelines. If you set traces_endpoint explicitly, you must write the full path yourself.

memory_limiter goes first, batch goes last. The limiter only protects you if it sees data before anything else buffers it. The batcher only helps if nothing after it re-splits the batch.

Batch size is a body-size decision. TracePath rejects an OTLP body over 10 MB with 413, applied to the decompressed payload, so gzip does not buy you headroom. OTLP exporters treat 4xx as permanent and drop the batch rather than retry it, which makes an oversized export silent data loss. send_batch_max_size: 1024 keeps you far below the ceiling for normal span sizes; lower it if your spans carry large attributes.

retry_on_failure is what makes quota backpressure harmless. When a project is over its monthly ingest allowance, or ingest is momentarily saturated, TracePath answers 503 with a Retry-After header instead of failing outright. That status is retryable, and the collector backs off and resends.

The token is an environment variable, not a literal. ${env:TRACEPATH_TOKEN} is resolved at startup. Keep the value in a systemd EnvironmentFile, a Kubernetes Secret, or your platform's secret store, and keep it out of the config file you commit.

Running it as a service

On a Linux host, a unit file and an environment file are the whole deployment:

# /etc/systemd/system/otelcol.service
[Unit]
Description=OpenTelemetry Collector
After=network-online.target
 
[Service]
EnvironmentFile=/etc/otelcol/otelcol.env
ExecStart=/usr/bin/otelcol-contrib --config=/etc/otelcol/config.yaml
Restart=on-failure
RestartSec=5s
User=otelcol
 
[Install]
WantedBy=multi-user.target
# /etc/otelcol/otelcol.env  (chmod 600, owned by root)
TRACEPATH_TOKEN=your-project-token

Then point your applications at the collector instead of at TracePath:

OTEL_EXPORTER_OTLP_ENDPOINT=http://collector.internal:4318
OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf

Note that the Authorization header disappears from your application config entirely. The project token now lives in exactly one place, which is most of the reason a network-policy team wanted the collector to begin with.

Verifying the first batch

Check the collector is alive, then check the data landed:

curl -fsS http://127.0.0.1:13133/          # collector health
curl -fsS http://127.0.0.1:8888/metrics \  # its own internal counters
  | grep -E 'otelcol_exporter_(sent|send_failed)_spans'

otelcol_exporter_sent_spans climbing with otelcol_exporter_send_failed_spans flat means the hop to TracePath is working. If sends are failing, the collector logs the HTTP status: 401 means the token header is wrong (the value must literally begin with Bearer ), 413 means your batches are too large, and a connection error usually means the exporter is still configured for gRPC.

Then open the project in the dashboard. Traces become endpoints and spans, metrics become dashboard widgets, and logs land on the Logs page already linked to the trace that produced them.

Redacting before egress

The single best reason to run a collector is that it gives you one place to enforce what must never leave the building. An attributes processor is enough for known keys:

processors:
  attributes/scrub:
    actions:
      - key: http.request.header.authorization
        action: delete
      - key: user.email
        action: hash
      - key: db.statement
        action: update
        value: "[redacted]"

Add it to each pipeline before batch. delete drops the attribute, hash replaces the value with a digest so you can still count distinct users without storing who they are, and update substitutes a constant.

For values that appear inside otherwise legitimate fields, such as a card number inside a log body, pattern-based redaction is a better fit than an allowlist of keys. Whichever you use, test it against a real payload rather than a synthetic one. The attribute that leaks is always the one nobody knew was being set.

Tail sampling

Head sampling, the kind your SDK does, decides at the first span whether to keep a trace, which means it decides before it knows whether the request failed. Tail sampling waits until the trace is complete. That lets you keep everything interesting and throw away most of the rest:

processors:
  tail_sampling:
    decision_wait: 10s
    num_traces: 50000
    expected_new_traces_per_sec: 500
    policies:
      - name: keep-errors
        type: status_code
        status_code: { status_codes: [ERROR] }
      - name: keep-slow
        type: latency
        latency: { threshold_ms: 1000 }
      - name: sample-the-rest
        type: probabilistic
        probabilistic: { sampling_percentage: 10 }

Policies are evaluated as an OR: a trace is kept if any policy says keep. So this configuration keeps every errored trace, every trace over a second, and a tenth of everything else.

Two constraints come with it. First, decision_wait is how long the collector holds spans in memory before deciding, and num_traces caps how many it holds at once; both multiply into your memory ceiling, which is why memory_limiter is not optional here. Second, and more consequential: every span of a trace must reach the same collector instance, or the sampler judges a fragment. One collector is fine. More than one means putting a load-balancing exporter in front, routing on trace ID, so each trace lands consistently on one backend.

Replace tail_sampling in the pipeline where batch used to be, and keep batch after it:

    traces:
      receivers: [otlp]
      processors: [memory_limiter, tail_sampling, batch]
      exporters: [otlphttp/tracepath]

Sampling is the lever that decides whether a busy service fits inside a plan's monthly ingest allowance. TracePath meters the bytes it receives, so a trace dropped at the collector is a trace you are not billed for. The allowances and what happens at the edge of one are on the pricing page.

Where the TracePath OTel Agent fits

The TracePath OTel Agent is a different tool and it is easy to confuse with this. It is a small pre-configured collector that scrapes host metrics, CPU, memory, disk, filesystem and network, from a machine every 60 seconds and ships them to your project. It binds only loopback ports and it is not a gateway: it does not accept OTLP from your applications.

Use both if you want both things. The agent gives you the infrastructure view of each host in the fleet; the collector above carries your application telemetry. They can point at the same project, and the host metrics show up next to the services running on that host. The agent's install and configuration options are in the documentation.

For a Kubernetes cluster, neither of these is the right shape. Run the Collector as a DaemonSet with the hostmetrics and Kubernetes receivers instead, so you get node metrics, pod metrics, container logs, and cluster state from one deployment.

FAQ

Does TracePath support OTLP over gRPC? No. TracePath accepts OTLP over HTTP only, Protobuf or JSON. Set OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf explicitly: several SDKs default to gRPC when the variable is unset, and the resulting failure is silent. Your collector can still expose a gRPC receiver to your own apps; only the exporter to TracePath must be otlphttp.

Do I need a separate collector per signal? No. One collector, one exporter and three pipelines cover traces, metrics and logs, all with the same project token. Each signal still needs its own exporter wired into the application SDK, which is the most common reason the Logs page stays empty while traces arrive fine.

Should I run the collector as an agent on every host or as a gateway? Both patterns work. A per-host agent collects local sources and keeps the network hop short; a central gateway is where tail sampling and redaction belong, because those want to see everything. Large deployments usually run both: agents forwarding to a gateway, gateway forwarding to TracePath.

What happens if my collector cannot reach TracePath? The exporter retries with backoff and the sending queue holds data meanwhile. An in-memory queue is lost if the process restarts; the file storage extension makes it persistent across restarts, at the cost of writing to disk.

Does a collector lock me into TracePath? No, and neither does the SDK. Your instrumentation is plain OpenTelemetry, and the TracePath-specific part is one endpoint and one header. Changing backends, or exporting to two at once during an evaluation, is an exporter edit.

Next

Subscribe

Get new engineering posts in your inbox

This opens your mail client with a short message to [email protected]. Prefer no email at all? Use the RSS feed.