OpenTelemetry Java Tracing API
This document teaches you how to use the OpenTelemetry Java Tracing API. To learn how to install and configure the OpenTelemetry Java SDK, see Getting started with OpenTelemetry Java.
Installation
OpenTelemetry Java is the Java implementation of OpenTelemetry. It provides the OpenTelemetry Tracing API which you can use to instrument your application with OpenTelemetry tracing.
Add the following dependencies to your project:
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-api</artifactId>
</dependency>
Quickstart
Step 1. Let's instrument the following function:
public User insertUser(User user) throws SQLException {
String sql = "INSERT INTO users (email, name) VALUES (?, ?)";
try (PreparedStatement stmt = connection.prepareStatement(sql)) {
stmt.setString(1, user.getEmail());
stmt.setString(2, user.getName());
stmt.executeUpdate();
}
return user;
}
Step 2. Wrap the operation with a span:
import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.context.Scope;
private static final Tracer tracer = GlobalOpenTelemetry.getTracer("com.example.myapp", "1.0.0");
public User insertUser(User user) throws SQLException {
Span span = tracer.spanBuilder("insert-user").startSpan();
try (Scope scope = span.makeCurrent()) {
String sql = "INSERT INTO users (email, name) VALUES (?, ?)";
try (PreparedStatement stmt = connection.prepareStatement(sql)) {
stmt.setString(1, user.getEmail());
stmt.setString(2, user.getName());
stmt.executeUpdate();
}
return user;
} finally {
span.end();
}
}
Step 3. Record exceptions and set status code:
import io.opentelemetry.api.trace.StatusCode;
public User insertUser(User user) throws SQLException {
Span span = tracer.spanBuilder("insert-user").startSpan();
try (Scope scope = span.makeCurrent()) {
String sql = "INSERT INTO users (email, name) VALUES (?, ?)";
try (PreparedStatement stmt = connection.prepareStatement(sql)) {
stmt.setString(1, user.getEmail());
stmt.setString(2, user.getName());
stmt.executeUpdate();
}
return user;
} catch (SQLException e) {
span.recordException(e);
span.setStatus(StatusCode.ERROR, e.getMessage());
throw e;
} finally {
span.end();
}
}
Step 4. Record contextual information with attributes:
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.common.AttributeKey;
public User insertUser(User user) throws SQLException {
Span span = tracer.spanBuilder("insert-user").startSpan();
try (Scope scope = span.makeCurrent()) {
String sql = "INSERT INTO users (email, name) VALUES (?, ?)";
try (PreparedStatement stmt = connection.prepareStatement(sql)) {
stmt.setString(1, user.getEmail());
stmt.setString(2, user.getName());
stmt.executeUpdate();
}
if (span.isRecording()) {
span.setAttribute("enduser.id", user.getId());
span.setAttribute("enduser.email", user.getEmail());
}
return user;
} catch (SQLException e) {
span.recordException(e);
span.setStatus(StatusCode.ERROR, e.getMessage());
throw e;
} finally {
span.end();
}
}
That's it! The operation is now fully instrumented with proper error handling and contextual information.
Tracer
To start creating spans, you need a tracer. You can create a tracer by providing the name and version of the library or application doing the instrumentation:
import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.api.trace.Tracer;
Tracer tracer = GlobalOpenTelemetry.getTracer("com.example.myapp", "1.0.0");
You can have as many tracers as you want, but usually you need only one tracer per application or library. Later, you can use tracer names to identify the instrumentation that produces the spans.
Creating spans
Once you have a tracer, creating spans is straightforward:
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.SpanKind;
import io.opentelemetry.context.Scope;
// Create a span with name "operation-name" and kind="server"
Span span = tracer.spanBuilder("operation-name")
.setSpanKind(SpanKind.SERVER)
.startSpan();
try (Scope scope = span.makeCurrent()) {
// Your business logic here
doSomeWork();
} finally {
// End the span when the operation is done
span.end();
}
Span kinds
Specify the type of span using span kinds:
import io.opentelemetry.api.trace.SpanKind;
// For incoming requests (server-side)
Span serverSpan = tracer.spanBuilder("handle-request")
.setSpanKind(SpanKind.SERVER)
.startSpan();
// For outgoing requests (client-side)
Span clientSpan = tracer.spanBuilder("http-request")
.setSpanKind(SpanKind.CLIENT)
.startSpan();
// For async operations (producer/consumer)
Span producerSpan = tracer.spanBuilder("publish-message")
.setSpanKind(SpanKind.PRODUCER)
.startSpan();
Span consumerSpan = tracer.spanBuilder("process-message")
.setSpanKind(SpanKind.CONSUMER)
.startSpan();
// For internal operations (default)
Span internalSpan = tracer.spanBuilder("internal-operation")
.setSpanKind(SpanKind.INTERNAL)
.startSpan();
Context
OpenTelemetry stores the active span in a context. The context is stored in thread-local storage and is automatically propagated when you use makeCurrent().
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.context.Context;
import io.opentelemetry.context.Scope;
// Get the currently active span
Span currentSpan = Span.current();
// Get the span from a context
Span spanFromContext = Span.fromContext(Context.current());
// Manually activate a span
Span span = tracer.spanBuilder("my-span").startSpan();
try (Scope scope = span.makeCurrent()) {
// Span is now active in this scope
doWork();
} finally {
span.end();
}
Context propagation
When starting child spans, the parent context is automatically used. For distributed tracing across services, see OpenTelemetry Trace Context Propagation in Java:
// Parent span
Span parentSpan = tracer.spanBuilder("parent-operation").startSpan();
try (Scope parentScope = parentSpan.makeCurrent()) {
// Child span - automatically becomes a child of the parent
Span childSpan = tracer.spanBuilder("child-operation").startSpan();
try (Scope childScope = childSpan.makeCurrent()) {
// Perform child work
doChildWork();
} finally {
childSpan.end();
}
} finally {
parentSpan.end();
}
To explicitly set a parent context:
// Create a child span with explicit parent
Span childSpan = tracer.spanBuilder("child-operation")
.setParent(Context.current().with(parentSpan))
.startSpan();
Adding span attributes
To record contextual information, you can annotate spans with attributes. For example, an HTTP endpoint may have attributes such as http.method = GET and http.route = /projects/:id.
import io.opentelemetry.api.common.AttributeKey;
// Check if span is being recorded to avoid expensive computations
if (span.isRecording()) {
span.setAttribute("http.method", "GET");
span.setAttribute("http.route", "/projects/:id");
span.setAttribute("http.status_code", 200);
span.setAttribute("user.id", userId);
}
Setting multiple attributes
You can set multiple attributes efficiently by calling setAttribute multiple times:
import io.opentelemetry.api.common.AttributeKey;
if (span.isRecording()) {
span.setAttribute("http.method", "POST");
span.setAttribute("http.route", "/api/users");
span.setAttribute("http.status_code", 201L);
span.setAttribute("user.role", "admin");
}
Semantic conventions
Use semantic conventions for consistent attribute naming:
import io.opentelemetry.semconv.trace.attributes.SemanticAttributes;
span.setAttribute(SemanticAttributes.HTTP_METHOD, "GET");
span.setAttribute(SemanticAttributes.HTTP_ROUTE, "/projects/:id");
span.setAttribute(SemanticAttributes.HTTP_STATUS_CODE, 200L);
Setting status code
Use status codes to indicate the outcome of operations:
import io.opentelemetry.api.trace.StatusCode;
// Successful operation (you typically don't need to set this)
span.setStatus(StatusCode.OK);
// Operation with error
span.setStatus(StatusCode.ERROR, "Database connection failed");
// Unset status (let the system decide)
span.setStatus(StatusCode.UNSET);
Status with exception handling
try {
performDatabaseOperation();
span.setStatus(StatusCode.OK);
} catch (Exception e) {
span.setStatus(StatusCode.ERROR, e.getMessage());
span.recordException(e);
throw e;
}
Recording exceptions
OpenTelemetry provides a convenient method to record exceptions:
try {
riskyOperation();
} catch (Exception e) {
// Record the exception
span.recordException(e);
// Also mark span as failed
span.setStatus(StatusCode.ERROR, e.getMessage());
throw e; // Re-throw if needed
}
Exception with additional context
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.common.AttributeKey;
try {
connectToDatabase(host, port);
} catch (SQLException e) {
span.recordException(e, Attributes.of(
AttributeKey.stringKey("db.host"), host,
AttributeKey.longKey("db.port"), (long) port,
AttributeKey.longKey("retry.count"), retryCount
));
span.setStatus(StatusCode.ERROR, "Database connection failed");
throw e;
}
Adding span events
You can annotate spans with events that represent significant moments during the span's lifetime:
// Simple event
span.addEvent("cache.miss");
// Event with attributes
span.addEvent("user.login", Attributes.of(
AttributeKey.stringKey("user.id"), "12345",
AttributeKey.stringKey("user.role"), "admin",
AttributeKey.stringKey("login.method"), "oauth"
));
// Event with custom timestamp (epoch nanos)
long timestampNanos = System.nanoTime();
span.addEvent("database.query.start", Attributes.of(
AttributeKey.stringKey("db.statement"), "SELECT * FROM users WHERE id = ?",
AttributeKey.stringKey("db.operation"), "SELECT"
), timestampNanos, java.util.concurrent.TimeUnit.NANOSECONDS);
Logging with events
Events are particularly useful for structured logging:
span.addEvent("log", Attributes.of(
AttributeKey.stringKey("log.severity"), "error",
AttributeKey.stringKey("log.message"), "User not found",
AttributeKey.stringKey("user.id"), "123",
AttributeKey.stringKey("error.code"), "USER_NOT_FOUND"
));
Adding span links
You can add links to show relationships between operations in different traces:
import io.opentelemetry.api.trace.SpanContext;
// Create a span with links to other operations
Span span = tracer.spanBuilder("batch-process")
.addLink(relatedSpanContext)
.addLink(anotherSpanContext, Attributes.of(
AttributeKey.stringKey("link.type"), "caused_by"
))
.startSpan();
Best practices
Error handling pattern
Use proper exception handling patterns:
public void processOrder(Order order) {
Span span = tracer.spanBuilder("process-order").startSpan();
try (Scope scope = span.makeCurrent()) {
if (span.isRecording()) {
span.setAttribute("order.id", order.getId());
span.setAttribute("order.total", order.getTotal());
}
// Process the order
validateOrder(order);
chargePayment(order);
sendConfirmation(order);
span.setStatus(StatusCode.OK);
span.setAttribute("operation.result", "success");
} catch (Exception e) {
span.recordException(e);
span.setStatus(StatusCode.ERROR, e.getMessage());
span.setAttribute("operation.result", "failure");
throw e;
} finally {
span.end();
}
}
Attribute optimization
Only set attributes when the span is being recorded:
if (span.isRecording()) {
String expensiveValue = calculateExpensiveValue();
span.setAttribute("expensive.attribute", expensiveValue);
}
Span naming
Use descriptive, low-cardinality span names:
// Good: Low cardinality
Span span = tracer.spanBuilder("GET /users/{id}").startSpan();
// Bad: High cardinality (includes variable user ID)
Span span = tracer.spanBuilder("GET /users/" + userId).startSpan();
Integration examples
HTTP client instrumentation
import io.opentelemetry.api.trace.SpanKind;
import io.opentelemetry.api.trace.StatusCode;
import io.opentelemetry.semconv.trace.attributes.SemanticAttributes;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public HttpResponse<String> makeHttpRequest(String url, String method) throws Exception {
Span span = tracer.spanBuilder("HTTP " + method)
.setSpanKind(SpanKind.CLIENT)
.startSpan();
try (Scope scope = span.makeCurrent()) {
if (span.isRecording()) {
span.setAttribute(SemanticAttributes.HTTP_METHOD, method);
span.setAttribute(SemanticAttributes.HTTP_URL, url);
}
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.method(method, HttpRequest.BodyPublishers.noBody())
.build();
HttpResponse<String> response = httpClient.send(request,
HttpResponse.BodyHandlers.ofString());
if (span.isRecording()) {
span.setAttribute(SemanticAttributes.HTTP_STATUS_CODE, (long) response.statusCode());
}
if (response.statusCode() >= 400) {
span.setStatus(StatusCode.ERROR, "HTTP " + response.statusCode());
} else {
span.setStatus(StatusCode.OK);
}
return response;
} catch (Exception e) {
span.recordException(e);
span.setStatus(StatusCode.ERROR, "HTTP request failed");
throw e;
} finally {
span.end();
}
}
Database query instrumentation
import io.opentelemetry.api.trace.SpanKind;
import io.opentelemetry.api.trace.StatusCode;
import io.opentelemetry.semconv.trace.attributes.SemanticAttributes;
public List<User> executeQuery(String query) throws SQLException {
Span span = tracer.spanBuilder("db.query")
.setSpanKind(SpanKind.CLIENT)
.startSpan();
try (Scope scope = span.makeCurrent()) {
if (span.isRecording()) {
span.setAttribute(SemanticAttributes.DB_SYSTEM, "postgresql");
span.setAttribute(SemanticAttributes.DB_STATEMENT, query);
span.setAttribute(SemanticAttributes.DB_NAME, "mydb");
}
long startTime = System.nanoTime();
try (PreparedStatement stmt = connection.prepareStatement(query);
ResultSet rs = stmt.executeQuery()) {
List<User> results = new ArrayList<>();
while (rs.next()) {
results.add(mapUser(rs));
}
long duration = System.nanoTime() - startTime;
if (span.isRecording()) {
span.setAttribute("db.rows_affected", results.size());
span.setAttribute("db.duration_ms", duration / 1_000_000.0);
}
span.setStatus(StatusCode.OK);
return results;
}
} catch (SQLException e) {
span.recordException(e);
span.setStatus(StatusCode.ERROR, "Database query failed");
throw e;
} finally {
span.end();
}
}
Async operation instrumentation
import java.util.concurrent.CompletableFuture;
import io.opentelemetry.context.Context;
public CompletableFuture<String> asyncOperation() {
Span span = tracer.spanBuilder("async-operation")
.setSpanKind(SpanKind.INTERNAL)
.startSpan();
// Capture the current context to propagate to async code
Context context = Context.current().with(span);
return CompletableFuture.supplyAsync(() -> {
// Restore context in async thread
try (Scope scope = context.makeCurrent()) {
if (span.isRecording()) {
span.setAttribute("operation.type", "async");
}
// Simulate async work
Thread.sleep(100);
span.addEvent("async.work.completed");
span.setStatus(StatusCode.OK);
return "success";
} catch (Exception e) {
span.recordException(e);
span.setStatus(StatusCode.ERROR, e.getMessage());
throw new RuntimeException(e);
} finally {
span.end();
}
});
}
Complete example
package com.example;
import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.SpanKind;
import io.opentelemetry.api.trace.StatusCode;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.context.Scope;
public class TracingExample {
private static final Tracer tracer = GlobalOpenTelemetry.getTracer(
"com.example.myapp", "1.0.0");
public static void main(String[] args) {
// Create a root span for the main operation
Span mainSpan = tracer.spanBuilder("main-operation")
.setSpanKind(SpanKind.INTERNAL)
.startSpan();
try (Scope mainScope = mainSpan.makeCurrent()) {
mainSpan.setAttribute("app.version", "1.0.0");
// Process user request
processUserRequest("user-123");
mainSpan.setStatus(StatusCode.OK);
} catch (Exception e) {
mainSpan.recordException(e);
mainSpan.setStatus(StatusCode.ERROR, e.getMessage());
} finally {
mainSpan.end();
}
}
private static void processUserRequest(String userId) {
Span span = tracer.spanBuilder("process-user-request")
.setSpanKind(SpanKind.INTERNAL)
.startSpan();
try (Scope scope = span.makeCurrent()) {
if (span.isRecording()) {
span.setAttribute("user.id", userId);
}
// Validate user
validateUser(userId);
// Fetch user data
fetchUserData(userId);
span.addEvent("request.processed");
span.setStatus(StatusCode.OK);
} catch (Exception e) {
span.recordException(e);
span.setStatus(StatusCode.ERROR, e.getMessage());
throw e;
} finally {
span.end();
}
}
private static void validateUser(String userId) {
Span span = tracer.spanBuilder("validate-user").startSpan();
try (Scope scope = span.makeCurrent()) {
// Validation logic
if (userId == null || userId.isEmpty()) {
throw new IllegalArgumentException("User ID is required");
}
span.addEvent("validation.passed");
} finally {
span.end();
}
}
private static void fetchUserData(String userId) {
Span span = tracer.spanBuilder("fetch-user-data")
.setSpanKind(SpanKind.CLIENT)
.startSpan();
try (Scope scope = span.makeCurrent()) {
if (span.isRecording()) {
span.setAttribute("db.system", "postgresql");
span.setAttribute("db.operation", "SELECT");
}
// Simulate database fetch
Thread.sleep(50);
span.addEvent("data.fetched", Attributes.of(
AttributeKey.longKey("rows.returned"), 1L
));
} catch (InterruptedException e) {
span.recordException(e);
Thread.currentThread().interrupt();
} finally {
span.end();
}
}
}
OpenTelemetry APM
Uptrace is an open source APM for OpenTelemetry that supports distributed tracing, metrics, and logs. You can use it to monitor applications and troubleshoot issues.

Uptrace comes with an intuitive query builder, rich dashboards, alerting rules, notifications, and integrations for most languages and frameworks.
Uptrace can process billions of spans and metrics on a single server and allows you to monitor your applications at 10x lower cost.
In just a few minutes, you can try Uptrace by visiting the cloud demo (no login required) or running it locally with Docker. The source code is available on GitHub.