← Back to blog
Guide 4 min read 炬鲸团队

Full-Stack OpenTelemetry: Logs, Metrics and Traces in One Pass

Configure the OpenTelemetry Collector once to ship logs, metrics and traces, with a working OTLP exporter config and the pitfalls that bite first-timers.

Why OpenTelemetry

The most common mistake when onboarding an observability platform is installing a separate agent for logs, metrics and traces — three configs, three exporters, and every change fights the others. Worse, the three datasets don't correlate: logs carry no trace_id, metrics carry no service dimension, and when something breaks you jump between three dashboards trying to line them up.

OpenTelemetry (OTel) fixes this by unifying collection: one SDK instruments the application, one Collector aggregates everything, and a single OTLP protocol ships it to the backend. Logs, metrics and traces share the same resource attributes and trace context, so they correlate out of the box.

Observe speaks OTLP natively, so the moment your data reaches the platform, logs, traces and metrics all show up together.

Concretely, a team that used to run Filebeat for logs, Prometheus for metrics and Jaeger for traces can replace all three with one Collector and one endpoint. Fewer moving parts means fewer places for a signal to silently drop, and one place to add the service dimension that ties all three together.

Step 1: Start a Collector

The Collector is the heart of OTel — it receives, processes and exports signals. Start one with Docker:

receivers:
  otlp:
    protocols:
      grpc: { endpoint: 0.0.0.0:4317 }
      http: { endpoint: 0.0.0.0:4318 }
processors:
  batch: { timeout: 5s }
exporters:
  otlphttp:
    endpoint: https://ob.example.com/otlp
    headers: { Authorization: "Bearer <token>" }
service:
  pipelines:
    traces:  { receivers: [otlp], processors: [batch], exporters: [otlphttp] }
    metrics: { receivers: [otlp], processors: [batch], exporters: [otlphttp] }
    logs:    { receivers: [otlp], processors: [batch], exporters: [otlphttp] }
docker run -v $PWD/otel-collector.yaml:/etc/otel/config.yaml   otel/opentelemetry-collector-contrib:0.90.0

The batch processor matters more than it looks: it accumulates signals and flushes them in batches, cutting network overhead and backend load. Don't delete it.

Step 2: Instrument the application

For Java, use the opentelemetry-javaagent — add a JVM flag at startup, no code changes needed:

java -javaagent:opentelemetry-javaagent.jar   -Dotel.service.name=order-service   -Dotel.exporter.otlp.endpoint=http://localhost:4317   -jar app.jar

For Go, instrument with the SDK directly or adopt the ready-made otelhttp and otelgrpc instrumentation libraries. The thing that matters most is propagating trace context across process boundaries — inject the traceparent header in HTTP clients and forward metadata in gRPC — or your traces break at every service hop.

Step 3: Tag your resources

Add resource attributes like service.name, env and cluster to all signals in one place, so search and alerts can filter by them. Stick to the OTel semantic conventions and keep field names consistent across teams:

processors:
  resource:
    attributes:
      - key: env
        value: prod
        action: upsert

Verify everything landed

After starting the Collector and the application, trigger a few requests and confirm all three signals arrived. In the Observe console, run a SQL-like log query and a trace search:

SELECT * FROM logs WHERE service = 'order-service' ORDER BY ts DESC LIMIT 50

If logs show up but traces don't, the problem is almost always context propagation — check that your HTTP and gRPC clients carry the trace headers. If traces exist but the flame graph looks shallow, confirm the SDK is actually recording spans: auto-instrumentation covers common libraries, but it won't know about your custom code unless you add spans there.

One more practical note: run one Collector per environment (dev, staging, prod), not one per service. A single Collector comfortably handles dozens of services, and centralizing it means you configure batching, sampling and redaction once instead of forty times. Keeping one Collector per environment also gives you a natural chokepoint to enforce consistent resource attributes and masking rules, so every service ships data that follows the same conventions without each team reimplementing them.

Pitfalls that bite first-timers

  • Broken traces — check that your HTTP/gRPC clients also inject trace context (otelhttp / otelgrpc).
  • Logs don't correlate with traces — make sure logs carry trace_id and span_id; you can add them in the log exporter.
  • Clock drift — enable NTP on every host, or cross-service timestamps will never line up.
  • Sampling drops the important traces — full collection is too expensive on high-traffic services. Use tail sampling to keep errors and slow requests instead of dropping a fixed percentage blindly.

Configure all three signals once, and every new service just reuses the same Collector and endpoint — no more one-off setup per service.