The OpenTelemetry Java Agent lets you instrument your Java applications for distributed tracing and metrics without touching your application code.
Let’s see it in action. Imagine a simple Spring Boot application with a REST controller and a call to an external HTTP service.
@RestController
@Slf4j
public class MyController {
private final RestTemplate restTemplate;
public MyController(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
@GetMapping("/greet/{name}")
public String greet(@PathVariable String name) {
log.info("Received request for name: {}", name);
String externalServiceUrl = "http://localhost:8081/external/" + name;
String response = restTemplate.getForObject(externalServiceUrl, String.class);
log.info("Response from external service: {}", response);
return "Hello, " + name + "! External service said: " + response;
}
}
And a simple RestTemplate bean configuration:
@Configuration
public class AppConfig {
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
With the OpenTelemetry Java Agent attached, when we run this application and hit the /greet/World endpoint, we’ll automatically get traces showing the incoming HTTP request, the RestTemplate call to localhost:8081, and potentially even the handler on localhost:8081 if it’s also instrumented. No try-catch blocks, no manual span creation.
Here’s how it works internally: the agent is a Java instrumentation agent. When the Java Virtual Machine (JVM) starts, you tell it to load this agent using the -javaagent flag. The agent then intercepts class loading. Before a class is loaded into the JVM, the agent can modify its bytecode. For common libraries and frameworks (like Spring, Netty, JDBC, HTTP clients), the agent has pre-built "instrumentations" – specific bytecode modifications that inject OpenTelemetry API calls. These injected calls create spans, record attributes, and capture exceptions, all automatically.
The primary problem this solves is the friction of adding observability to existing, or even new, applications. Manually instrumenting every outgoing HTTP request, every database query, every incoming request, and managing the context propagation across them is tedious, error-prone, and requires significant code changes. The agent removes this burden by providing a zero-code solution for common observability signals.
You control what gets instrumented and where the data goes primarily through configuration. You can enable or disable specific instrumentations (e.g., only trace HTTP clients, or exclude certain URLs). You configure the exporter to send data to your OpenTelemetry Collector or directly to a backend like Jaeger or Prometheus.
A key configuration aspect is OTEL_INSTRUMENTATION_ENABLED. This environment variable, or a system property, allows you to control which instrumentation packages are active. For example, to disable the httpclient instrumentation, you’d set OTEL_INSTRUMENTATION_ENABLED=false for the io.opentelemetry.java.httpclient package. This is crucial for fine-tuning performance or troubleshooting unexpected behavior.
The agent works by leveraging the Java Instrumentation API. When a class is loaded, the agent receives a callback. It can then use libraries like Byte Buddy to transform the class’s bytecode. For instance, when a RestTemplate object’s execute method is about to be called, the agent inserts code that starts a new span, records the HTTP method and URL as attributes, makes the original execute call, and then, regardless of success or failure, ends the span and records any exceptions.
Most people know you can turn instrumentations on or off, but they often overlook the detailed configuration for specific libraries. For example, you can configure the JDBC instrumentation to capture SQL statements and their parameters. This is done via properties like otel.jdbc.statement.parameter.collection.enabled=true. However, be mindful that capturing all parameters can significantly increase trace verbosity and overhead, so it’s often enabled selectively or disabled by default for performance reasons.
The next hurdle you’ll encounter is understanding and configuring context propagation, especially when dealing with asynchronous operations or custom network protocols.