Point-to-point reporting is a maintenance burden. This guide onboards logs, metrics and traces to Observe with a single Collector config, covering agent vs gateway deployment, tail sampling and correlation checks.
Having each application report straight to the backend looks simple but has real downsides. Every language needs its own SDK, and each SDK has its own quirks; changing the backend address or rotating a token means touching every application in the fleet; there is no buffering or retry when the network hiccups, so a brief outage turns into silently dropped telemetry; and sampling policies end up scattered across a dozen services where nobody can see or reason about them. OpenTelemetry Collector collapses all of that into a single ingestion layer built on a receive-process-export pipeline. Applications just send; policy lives in one place.
You can run the Collector two ways. As an agent on each host, it collects locally, batches writes, and offloads protocol translation from the backend — the right choice when you have many small services packed onto shared machines. As a centralized gateway, it owns authentication, sampling, and routing for the whole fleet. A single gateway is enough while you are small; at scale, layer agents in front of a gateway so each host ships to a local collector and the gateway fans out to your backends.
The Collector ships as a single static binary with no runtime to install. Run it under systemd or in a container and point it at the config file, otel-collector-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
batch:
timeout: 1s
send_batch_size: 1024
exporters:
otlphttp:
endpoint: https://ob.example.com/otlp
headers:
Authorization: "Bearer <ingestion-token>"
service:
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, batch]
exporters: [otlphttp]
metrics:
receivers: [otlp]
processors: [memory_limiter, batch]
exporters: [otlphttp]
logs:
receivers: [otlp]
processors: [memory_limiter, batch]
exporters: [otlphttp]
Start it with:
otelcol --config otel-collector-config.yaml
A few details worth knowing: the otlp receiver listens on both gRPC (4317) and HTTP (4318) so every SDK can reach it; memory_limiter stops a runaway collector from taking down the host; batch amortizes network round-trips by grouping spans before export. The exporter sends OTLP over HTTP to Observe with the bearer token in the header, so the token never sits inside your application code. If you prefer containers, the official otel/opentelemetry-collector image works out of the box — mount the config file and expose ports 4317 and 4318.
For Java, the javaagent is the least-effort path — no code changes, just startup flags:
java -javaagent:opentelemetry-javaagent.jar \
-Dotel.traces.exporter=otlp \
-Dotel.metrics.exporter=otlp \
-Dotel.logs.exporter=otlp \
-Dotel.exporter.otlp.endpoint=http://collector:4317 \
-Dotel.service.name=order-service \
-jar app.jar
Go, Python, and Node.js work the same way: install the SDK, set OTEL_EXPORTER_OTLP_ENDPOINT to the collector, and you are done. Two rules worth following: keep service.name consistent across a service's instances, since it is what determines grouping in the console, and set service.version so you can tell which release a trace came from when you diff before and after a deploy.
Full capture in production is expensive, and most of it is noise. The Collector supports layered sampling:
A sensible default: keep every error trace and sample normal traffic at 1%. On the logs side, use a processor to drop DEBUG and trace level lines, redact sensitive fields such as phone numbers or tokens before they leave your network, and strip stack traces you do not need. This cuts storage and keeps PII out of the index entirely.
Fire a few erroring requests, then look up a trace by trace id in the console and confirm that logs, metrics, and traces correlate through that id. Then stop one downstream service and confirm the alert fires. Pass both checks and the integration is done — correlation is the whole point of sending three signals through one pipeline, and if it is broken you want to know now rather than during an incident. Once it works, pin your sampling and filtering decisions in the config file so they are version-controlled and reviewable.