The right way to onboard polyglot services: use an OpenTelemetry Collector to centralize sampling, batching and export. Includes a complete configuration, per-language notes for Java, Go, Python and Node.js, and the pitfalls that trip up most teams.
There are two ways to onboard telemetry: send it straight from SDKs to the backend, or put an OpenTelemetry Collector in between. For a polyglot shop — Java here, Go there, a Python cron job and a Node gateway — the Collector wins for three reasons:
The Collector is a single binary you run once per host or per cluster; it accepts OTLP from every language and forwards it in one consistent stream.
Download the binary and point it at otel-collector-config.yaml:
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
processors:
batch:
timeout: 5s
send_batch_size: 1024
exporters:
otlphttp:
endpoint: https://ob.example.com/otlp
headers:
Authorization: Bearer <your-ingestion-token>
service:
pipelines:
traces:
receivers: [otlp]
processors: [batch]
exporters: [otlphttp]
logs:
receivers: [otlp]
processors: [batch]
exporters: [otlphttp]
metrics:
receivers: [otlp]
processors: [batch]
exporters: [otlphttp]
Applications then only need to send to 4317 (gRPC) or 4318 (HTTP) on localhost. The three pipelines — traces, logs, metrics — are declared separately, so you can enable them one at a time and, say, leave metrics off until you actually need them.
There are two common placements. Run one Collector per host (a systemd unit or a sidecar) and have apps target 127.0.0.1:4318; or run a small set of Collectors behind a load balancer and point every app at one stable address. For most teams, per-host is simpler and avoids a single point of failure — if one host's Collector dies, only that host's telemetry is affected.
In Kubernetes, the natural fit is a DaemonSet (one Collector per node) plus, optionally, a Deployment for gateway-style processing. The DaemonSet does the light work — receiving and batching — while the gateway handles heavier jobs like tail sampling and attribute scrubbing before forwarding to Observe. Start with just the DaemonSet; add the gateway later, when you need a centralized sampling policy across many services.
opentelemetry-javaagent. Add -javaagent:opentelemetry-javaagent.jar and set OTEL_EXPORTER_OTLP_ENDPOINT=http://127.0.0.1:4318. Zero code changes; it auto-instruments HTTP, DB and MQ calls.otelhttp, then start and end spans around each handler. Go's biggest trap is context: every span child must take the parent context.Context, or the trace silently breaks into pieces.opentelemetry-instrument for auto-injection, plus one line to instrument the framework: FlaskInstrumentor().instrument_app(app).NodeTracerProvider from @opentelemetry/sdk-trace-node and register it early, before routes are defined; then make sure the traceparent header is forwarded across HTTP calls.The single most important step: correlate logs with traces. Put trace_id into your log lines so you can jump from one log entry to the whole call chain. The Java agent injects the trace id into the MDC automatically — add %X{trace_id} to your logging.pattern and you're done. For Go and Python you'll need a few lines to write the current span's trace id into each log record.
traceparent header between services. Grep for it at each hop.tail_sampling and drop high-frequency, low-value requests like health checks.