Use the OpenTelemetry Collector as a single ingestion gateway to send logs, metrics and traces to Observe in one shot. Includes a full config, receiver/exporter guidance, and the pitfalls you will hit.
Pointing applications straight at the observability platform is the simplest setup — until you have scale. Dozens of services each pushing data to the platform means configuring a token, an endpoint, retries and backpressure for every single one; change the ingestion URL and every service needs a config update. Worse, logs, metrics and traces often come from different libraries with inconsistent fields, formats and sampling policies, so the platform receives a mess it has to normalize downstream.
The OpenTelemetry Collector is a middle layer: applications send data only to a local Collector, which handles processing, filtering, batching and exporting in one place. The application side only worries about one thing — send to the Collector — and everything else is the Collector's job. This also means you can swap the backend (change vendors, or add a second one) by editing a single exporter, without touching any application. In short, the Collector turns a fan-out of fragile app-to-platform connections into one managed pipeline.
Start with a single-node Collector running in Docker. Two receivers are defined here: otlp for SDK-instrumented applications and filelog for tailing raw log files:
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
filelog:
include: [ /var/log/app/*.log ]
start_at: end
processors:
batch:
timeout: 5s
send_batch_size: 8192
memory_limiter:
check_interval: 1s
limit_mib: 512
exporters:
otlphttp:
endpoint: https://ob.example.com/v1/traces
headers:
Authorization: "Bearer <token>"
service:
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, batch]
exporters: [otlphttp]
A quick note on the pieces: receivers define where data comes in, processors transform it, exporters define where it goes out, and service.pipelines wires them together. The batch processor groups small packets into larger ones to cut the number of network round-trips, and memory_limiter keeps the Collector from exhausting its own memory under load. You can add more processors — for example a filter to drop debug logs before they leave the host, which saves ingest volume and money.
Observability data comes in three signals, with separate export endpoints:
/v1/traces, distributed tracing/v1/metrics, metrics/v1/logs, logsIn service.pipelines, create one pipeline per signal. Receivers can share the same OTLP port, or you can dedicate different ones per signal if you want independent buffering and retry behavior. A common pattern is one OTLP receiver feeding three pipelines, each with its own exporter endpoint. Splitting the signals is worthwhile because each has different cardinality and volume, and you'll often want different batch sizes for logs (high volume) versus traces (low volume but latency-sensitive).
After the application adds the OpenTelemetry SDK, point its OTLP endpoint at the local Collector (localhost:4317). You no longer care about the platform's URL and token — those are configured once, in the Collector. This is the real payoff: onboarding a new service becomes "add the SDK and set one environment variable," and the platform credentials never leave the Collector host.
For Java, inject them via environment variables:
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
OTEL_SERVICE_NAME=order-service
memory_limiter: under high concurrency the Collector's memory grows fast; without a limit it OOMs and drops everything in flight. Set limit_mib to a fraction of the host's memory and spike_limit_mib for bursts.send_batch_size too small: tiny batches mean frequent requests that slow things down; around 8192 is stable for most workloads. Too large, and you add latency waiting for the batch to fill.Authorization: Bearer <token>, not a URL parameter. Getting this wrong is the most common cause of "data isn't showing up."Once connected, verify all three signals landed with a single query in the Observe console:
SELECT source, count(*) FROM observability
WHERE ts > now() - 15m GROUP BY source
All three present means onboarding is complete. If a signal is missing, check its pipeline in the Collector config first — the receiver, processor and exporter must all be wired in service.pipelines, which is easy to forget.