# OpenTelemetry Java Tracing API

> Instrument Java services with the OpenTelemetry Tracing API, create spans, record errors, and send distributed traces to Uptrace.

![undefined](/devicon/java-original.svg)This document teaches you how to use the OpenTelemetry Java Tracing API.

## Prerequisites

<partial path="otel-prereq-java">



</partial>

## Installation

[OpenTelemetry Java](https://github.com/open-telemetry/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](/opentelemetry/distributed-tracing).

Add the following dependencies to your project:

<code-group>

```xml [Maven]
<dependency>
    <groupId>io.opentelemetry</groupId>
    <artifactId>opentelemetry-api</artifactId>
</dependency>
```

```gradle [Gradle]
implementation("io.opentelemetry:opentelemetry-api")
```

</code-group>

## 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](/opentelemetry/distributed-tracing#spans):

```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](/opentelemetry/distributed-tracing#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](/opentelemetry/distributed-tracing#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](/opentelemetry/distributed-tracing#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](/opentelemetry/distributed-tracing#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](/opentelemetry/distributed-tracing#span-kind):

```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](/opentelemetry/distributed-tracing#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](/get/opentelemetry-java/propagation):

```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](/opentelemetry/distributed-tracing#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](/opentelemetry/distributed-tracing#semantic-attributes) 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](/opentelemetry/distributed-tracing#status-code) 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](/opentelemetry/distributed-tracing#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"
));
```

## Adding span links

You can add [links](/opentelemetry/distributed-tracing#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](/opentelemetry/distributed-tracing#span-name) 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();
        }
    }
}
```

## What's next?

- [Get started](/get/opentelemetry-java)
- [Learn about OpenTelemetry Java Metrics API](/get/opentelemetry-java/metrics)
- [Learn about OpenTelemetry Java Logs integration](/get/opentelemetry-java/logs)
- [Learn about OpenTelemetry Java Resource detectors](/get/opentelemetry-java/resources)
- [Learn about OpenTelemetry Java Sampling](/get/opentelemetry-java/sampling)
- [Learn about OpenTelemetry Context Propagation in Java](/get/opentelemetry-java/propagation)
- [OpenTelemetry Spring Boot](/guides/opentelemetry-spring-boot)
- [OpenTelemetry Tomcat](/guides/opentelemetry-tomcat)
