Use a single OpenTelemetry Collector to receive logs, metrics and traces, normalize and scrub them, then forward to Observe — instead of running a separate agent per signal. Includes a full config and troubleshooting checklist.
The classic onboarding mistake is running three agents: Filebeat for logs, node_exporter for metrics and a Jaeger agent for traces. Three configs to maintain, three upgrades, three places to troubleshoot — and the operational cost grows linearly with the number of services. Worse, the three agents produce different formats, timestamps and label conventions, so you end up re-aligning everything at the backend anyway.
The OpenTelemetry Collector unifies all three signals in one component: receivers accept data, processors clean, scrub and sample it, and exporters ship it out. Editing a single YAML changes the whole pipeline. It's fully open source and vendor-neutral, so you aren't locked to any backend — if you switch backends later, you swap the exporter and leave the collection side untouched.
A side note: OpenTelemetry is a CNCF open standard and has become the de facto standard for observability data collection in recent years, supported by the major language SDKs and cloud vendors. Choosing it for your ingestion layer decouples collection from the backend — the team maintains one collection config, and the backend choice can evolve independently.
Install the Collector first. Using the Linux binary:
wget https://github.com/open-telemetry/opentelemetry-collector-releases/releases/latest/download/otelcol_linux_amd64.tar.gz
tar -xzf otelcol_linux_amd64.tar.gz
sudo mv otelcol /usr/local/bin/
Then write config.yaml. This configuration receives all three signals and forwards them to Observe:
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
prometheus:
config:
scrape_configs:
- job_name: "node"
static_configs: [{ targets: ["localhost:9100"] }]
processors:
batch:
timeout: 5s
send_batch_size: 1024
attributes:
actions:
- key: "card_number"
action: "delete" # scrub sensitive fields
- key: "env"
action: "upsert"
value: "prod"
exporters:
otlphttp:
endpoint: "https://ob.example.com/otel"
headers: { "Authorization": "Bearer <your-ingestion-token>" }
service:
pipelines:
traces: { receivers: [otlp], processors: [batch, attributes], exporters: [otlphttp] }
metrics: { receivers: [otlp, prometheus], processors: [batch, attributes], exporters: [otlphttp] }
logs: { receivers: [otlp, filelog], processors: [batch, attributes], exporters: [otlphttp] }
Key points, one by one:
start_at: end means it only picks up new lines, avoiding a full replay of historical files on first startcard_number are deleted outright, and labels like env: prod are added uniformly, fixing naming inconsistencies across servicesThe app doesn't need to know Collector details — it just sends data to localhost:4317/4318. For Spring Boot, attach the OpenTelemetry Java agent:
java -javaagent:opentelemetry-javaagent.jar \
-Dotel.traces.exporter=otlp \
-Dotel.metrics.exporter=otlp \
-Dotel.logs.exporter=otlp \
-Dotel.exporter.otlp.endpoint=http://localhost:4317 \
-jar app.jar
Other languages follow the same pattern: opentelemetry-distro for Python, the otel SDK for Go, @opentelemetry/sdk-node for Node.js — all share one convention, "send to the local OTLP port", while the Collector handles everything downstream.
Sending logs over OTLP requires a Logback otel appender, but a simpler route is to keep writing local files and let the Collector's filelog receiver tail them — zero application changes, all collection logic centralized in the Collector. We recommend starting with the latter: minimal change, easiest rollback.
Separate production, staging and test environments by namespace or an env label rather than standing up a separate Collector for each. The Collector itself can be deployed per environment (one for production, one for staging), but it should always attach the env label on export so the backend can filter by environment. Keep service naming consistent too: use a stable identifier for service.name and avoid baking in version numbers or hostnames, or a single logical service will splinter into a dozen nodes in your traces.
On Kubernetes, deploy the Collector as a DaemonSet so it runs on every node: the filelog receiver tails logs written to disk by Pods, the otlp receiver accepts application traffic, and workloads discover the local Collector via the node address. This avoids cross-node network hops and spreads collection load evenly.
The defaults run fine, but tune two things for production: the batch processor's send_batch_size and the exporter's queue length. At high log volume, batching dramatically cuts the number of uploads and CPU overhead.
endpoint and Authorization header, then look for 4xx/5xx in the Collector logsstart_at and the batch settingsattributes runs before the exporterOnce connected, logs, metrics and traces should appear in a single service view in the Observe console, all searchable with the same SQL-like query language.