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

Zero-Code Tracing: A Practical Guide to OpenTelemetry Auto-Instrumentation

Adopt distributed tracing without touching business code using OpenTelemetry auto-instrumentation, with full Java and Python config, production sampling, containerized deployment, and troubleshooting of common pitfalls.

Why auto-instrumentation

The biggest blocker to distributed tracing isn't technical — it's the cost of instrumentation. Adding spans by hand means code changes, reviews and regressions, and plenty of teams give up at exactly this step. OpenTelemetry's auto-instrumentation gets you there without touching a single line of code.

Auto-instrumentation works by injecting at runtime: Java rewrites bytecode at class-load time through an Agent, Python monkey-patches the standard library and framework entry points. It intercepts the boundaries of middleware and clients rather than your business logic, so it's zero-intrusion while still carrying the cross-process trace context.

This guide assumes you already have an OBSERVE endpoint and token. If not, create a project in the console's onboarding wizard first.

Java as the example

Auto-instrumentation for Java works through a Java Agent. Download the OTel Java Agent jar and add one line to your startup command:

java -javaagent:/opt/otel/opentelemetry-javaagent.jar   -Dotel.service.name=order-service   -Dotel.traces.exporter=otlp   -Dotel.exporter.otlp.endpoint=https://otlp.jjhub.cn:4317   -Dotel.exporter.otlp.headers="Authorization=Bearer <token>"   -jar app.jar

Four parameters do the work: service.name groups the service in the topology; traces.exporter picks the exporter; otlp.endpoint points at OBSERVE's OTLP receiver; headers carry the auth. The agent intercepts Spring MVC, the MySQL driver, Redis, Kafka, HTTP clients and other common components, generating spans automatically.

Other languages

The mechanism is the same, only the carrier differs:

  • Go: there's no Agent, so lean on the ready-made middleware in go.opentelemetry.io/contrib/instrumentation for net/http, gin and gRPC — usually a one-import change.
  • Python: launch with the opentelemetry-instrument command; Flask, Django and Celery need no changes.
  • Node.js: one require of @opentelemetry/auto-instrumentations-node.

For Python:

pip install opentelemetry-distro opentelemetry-exporter-otlp
opentelemetry-bootstrap -a install
OTEL_SERVICE_NAME=user-service OTEL_EXPORTER_OTLP_ENDPOINT=https://otlp.jjhub.cn:4317 opentelemetry-instrument python app.py

Two things before production

Auto-instrumentation collects everything by default, which brings two costs in production: span volume explodes and storage bills rise, and high-frequency components (like a span per DB query) can slow requests down. Do these two things together:

Tune sampling. Add to the Agent flags:

-Dotel.traces.sampler=parentbased_traceidratio -Dotel.traces.sampler.arg=0.1

This keeps 10% of traces. When a parent span is sampled, the whole trace is kept, so you never get a half trace.

Containerize the Agent. On Kubernetes, mount the Agent jar into the Pod via an initContainer rather than baking it into the image, so Agent upgrades don't require rebuilding images. The key is a shared volume that mounts the agent directory into the main container's /opt/otel. Alternatively, run a per-node OTel Collector as a DaemonSet and point every Agent at it — that centralizes batching and lets you rotate tokens in one place instead of across every workload.

Add logs and metrics while you're at it

Once tracing works, push logs and metrics over OTLP as well, so you don't run three separate collectors for three signals. For the Java Agent, add two flags:

-Dotel.logs.exporter=otlp -Dotel.metrics.exporter=otlp

Now one Agent reports all three signals with a consistent service.name. In OBSERVE you can click from a span straight to the matching log lines and metric dashboards, instead of jumping between three tools during an incident.

Three checks after wiring up

Connected doesn't mean working. Verify in three steps:

  1. On the tracing page, filter by service.name and check the new service appears — that confirms data is flowing. If it doesn't show up within a minute, read the Agent logs for exporter errors before touching your application.
  2. Open a full trace and confirm parent-child relationships across services are correct and duration fields (like DB query time) are populated.
  3. Trigger a real request and check the sampling rate matches expectations, and watch your own service's CPU and P99 latency to make sure instrumentation isn't visibly dragging it.

Common pitfalls

Four frequent issues. First, an exporter/protocol mismatch throws unimplemented — try swapping the endpoint from 4318 (HTTP) to 4317 (gRPC) or vice versa. Second, forgetting to open the OTLP port in the firewall or security group inside containers. Third, leaving service.name unset, which dumps every service under unknown_service and crams the topology into one blob. Fourth, hosts with unsynchronized clocks make cross-service spans look inverted — tracing is sensitive to clock skew, so run NTP on every host.

For private protocols or in-house RPC that auto-instrumentation can't see, fill the gaps with manual spans. Auto first, manual second — that order gets you a complete trace map on day one.