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:

xml Maven
<dependency>
    <groupId>io.opentelemetry</groupId>
    <artifactId>opentelemetry-api</artifactId>
</dependency>
gradle Gradle
implementation("io.opentelemetry:opentelemetry-api")

Quickstart

Step 1. Let's instrument the following function:

java
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:

java
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:

java
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:

java
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:

java
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:

java
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:

java
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().

java
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:

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:

java
// 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.

java
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:

java
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:

java
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:

java
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

java
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:

java
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

java
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:

java
// 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:

java
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"
));

You can add links to show relationships between operations in different traces:

java
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:

java
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:

java
if (span.isRecording()) {
    String expensiveValue = calculateExpensiveValue();
    span.setAttribute("expensive.attribute", expensiveValue);
}

Span naming

Use descriptive, low-cardinality span names:

java
// 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

java
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

java
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

java
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

java
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 Overview

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.

What's next?