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

Instrument Your Services with OpenTelemetry: Logs, Metrics, and Traces in One Go

A step-by-step guide to shipping logs, metrics, and traces into OBSERVE via OpenTelemetry — Collector config, automatic instrumentation, trace/log correlation, and the pitfalls to avoid.

Why OpenTelemetry

The worst part of adopting an observability platform is vendor lock-in: adopt vendor A's SDK today and you rewrite all your instrumentation when you switch to vendor B. OpenTelemetry (OTel) is a vendor-neutral standard that defines three signal types — logs, metrics, and traces — plus a unified collection and wire format. OBSERVE speaks the OTLP protocol natively, so adopting OTel gets you all three signals at once, and switching backends later only means changing one export endpoint. Your instrumentation code stays untouched.

One more reason OTel matters: it is where the ecosystem is heading. Kubernetes, the major language runtimes, and the big cloud providers all converge on OTLP. Betting on OTel now means your instrumentation keeps working as your stack evolves, whether you self-host or move to a managed backend later.

Step 1: Deploy the Collector

Use the OTel Collector as a unified gateway. Applications push data to a local or nearby Collector, which forwards it to OBSERVE. This keeps application-side configuration minimal and makes it easy to collect logs from middleware too.

receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
      http:
        endpoint: 0.0.0.0:4318
  filelog:
    include: [/var/log/app/*.log]
    operators:
      - type: json_parser
processors:
  batch:
    timeout: 5s
    send_batch_size: 1024
exporters:
  otlphttp:
    endpoint: https://ob.jjhub.cn/otlp
    headers:
      Authorization: Bearer <your-token>
service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [batch]
      exporters: [otlphttp]
    metrics:
      receivers: [otlp]
      processors: [batch]
      exporters: [otlphttp]
    logs:
      receivers: [otlp, filelog]
      processors: [batch]
      exporters: [otlphttp]

Note the batch processor: it buffers and compresses data before export, which matters a lot when you are pushing thousands of spans per second. Without it, every span becomes its own HTTP round trip, and both your network and OBSERVE's ingest suffer.

Step 2: Automatic instrumentation

For Java, attach the agent to capture HTTP, database, and message-queue traces with zero code changes:

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

Python, Go, and Node.js have equivalent agents or SDKs. Reach for manual instrumentation only when you need business-specific spans — for example, wrapping order placement or payment deduction in their own span and attaching the order ID as an attribute, so a trace is searchable by business key, not just by service name.

Step 3: Correlate logs and traces

The single most useful tracing technique is injecting trace_id into logs. In Java, logback's MDC does this automatically:

<encoder>
  <pattern>%d %-5level [%X{trace_id}] %logger{36} - %msg%n</pattern>
</encoder>

Now any log line in OBSERVE jumps straight to its full trace, and vice versa — no more switching back and forth between two pages during an incident. If you use a different logging framework, the principle is the same: put trace_id and span_id into the structured log context, and OBSERVE wires the links for you.

Verification and common pitfalls

After wiring everything up, confirm all three signals are actually arriving: search a log line, query a metric, and open a flame graph. Then watch for three pitfalls:

  • Clock skew: drift between containers and hosts scrambles trace ordering. Sync with NTP or chrony.
  • Sampling: high-QPS services should sample by ratio (otel.traces.sampler=traceidratio, start at 0.1). Full capture is expensive to store and rarely worth it.
  • Auth header: the Collector-to-OBSERVE exporter must carry the token, or you will get 401s on the export path.

Choosing between agent and SDK

The agent (Java, .NET, Node.js) is the right default: it covers HTTP, database drivers, and message queues without touching your code, and it is easy to upgrade by swapping the agent file. The SDK (Python, Go, Rust) gives you more control — useful when you need to capture business logic inside your own functions, or when the agent's auto-instrumentation is too broad and you want to trim noisy spans. A practical middle ground is to start with the agent, then add manual spans only where the automatic coverage leaves gaps in the business path.

Going further

Once the basics are in, two upgrades pay off quickly. First, add resource attributes — service version, environment, region — so you can slice dashboards by deployment. Second, add a Sampling processor in the Collector to keep interesting traces (errors, slow requests) at 100% while downsampling the noise. Both are config-only changes; no application redeploy required. A third upgrade worth planning: send traces to OBSERVE over gRPC instead of HTTP to cut per-span overhead, and enable compression on the exporter. The Collector handles both with a two-line change to the exporter block.