Auto-instrumentation is easy but misses business context; manual spans are precise but costly. Java/Python startup commands, manual span patterns, attribute naming and how to verify your traces actually work end to end.
OpenTelemetry auto-instrumentation rewrites bytecode or hooks the interpreter at runtime via an injected agent, giving you HTTP requests, database calls and message-queue spans with zero code changes. It covers framework-level spans: inbound requests, outbound calls and SQL execution.
What it cannot cover is business context. Auto-instrumentation knows you called POST /orders, but not the order amount, the user ID, or which coupon was applied. Those are exactly the dimensions you miss when troubleshooting. It can tell you which call chain is slow, but not which kind of order is slow.
One thing auto-instrumentation gives you for free is context propagation: the trace and span IDs travel in headers (W3C traceparent), so a trace stitches together across every instrumented service. If a hop in the middle is not instrumented, the trace breaks there — the spans before and after look like two unrelated traces. When you see orphan spans in the console, look for the uninstrumented hop or a message queue that drops propagation headers.
For a Java service, one extra line in the launch command gets you a full trace:
java -javaagent:opentelemetry-javaagent.jar \
-Dotel.service.name=order-service \
-Dotel.exporter.otlp.endpoint=http://collector:4317 \
-Dotel.traces.exporter=otlp \
-jar order-service.jar
For Python, wrap the launch command with opentelemetry-instrument, and use opentelemetry-bootstrap to install the right instrumentor for each library:
opentelemetry-bootstrap -a install
opentelemetry-instrument \
--traces_exporter otlp \
--exporter_otlp_endpoint http://collector:4317 \
python app.py
After it starts, send one request and check the console for a complete trace. If the trace is not there, fix that first — the problem is almost always on the collection or network side, not in your code.
Do not wrap every bit of logic in a span. Reach for manual instrumentation in three places only:
transaction_id.user.id and order.amount to existing spans so you can filter by business conditions during an investigation.Adding attributes to a span in Java:
Span span = tracer.spanBuilder("process-order").startSpan();
span.setAttribute("order.amount", 99.5);
span.setAttribute("user.id", uid);
try (Scope scope = span.makeCurrent()) {
// business logic
} catch (Exception e) {
span.recordException(e);
span.setStatus(StatusCode.ERROR);
throw e;
} finally {
span.end();
}
The same pattern is more concise in Python thanks to the context manager:
from opentelemetry import trace
tracer = trace.get_tracer(__name__)
with tracer.start_as_current_span("process-order") as span:
span.set_attribute("order.amount", 99.5)
span.set_attribute("user.id", uid)
try:
process_order()
except Exception as e:
span.record_exception(e)
span.set_status(trace.StatusCode.ERROR)
raise
Do not skip recordException and setStatus(ERROR): alerting and sampling policies rely on span status codes. If you fail to mark errors, tail sampling may discard exactly the trace you need during an outage.
A common question is what belongs in a span attribute versus a log line or a span event. The rule of thumb: attributes are for dimensions you filter and aggregate on (order.amount, user.id, service.version); span events are for points in time within a span, such as a cache miss or a retry decision; and logs are for rich, unstructured text you need to read, not aggregate. Keep attributes small and typed, put transient moments in events, and leave prose to logs. Overloading spans with dozens of attributes makes traces hard to read and bloats storage without helping anyone find the problem faster.
Keep attribute names dotted and lowercase (order.amount, not orderAmount), and agree on a shared attribute list up front so every service uses the same names. After integration, verify in the console by trace id: is the span tree complete, are the business attributes present, and are error spans marked? Pick one failing request and confirm you can drill from the alert down to the exception stack. Only then is the integration truly done.
A useful smoke test: run the same failing request twice and confirm the two traces have identical structure. If an attribute value differs between runs where it should not, you have a bug in your instrumentation, not your service. And enable sampling early — even 100% for a single service in staging — so you learn how sampling interacts with your manual spans before production.