Java SDK

Full observability for JVM services — HTTP, databases, gRPC, messaging, and logging captured automatically via OpenTelemetry

Installation

Add the dependency to your pom.xml:

pom.xml
<dependency>
  <groupId>io.obtrace</groupId>
  <artifactId>obtrace-sdk-java</artifactId>
  <version>1.0.0</version>
</dependency>

Or with Gradle:

build.gradle
implementation 'io.obtrace:obtrace-sdk-java:1.0.0'

Minimum Setup

Main.java
import io.obtrace.sdk.core.ObtraceClient;
import io.obtrace.sdk.model.ObtraceConfig;
 
public class Main {
    public static void main(String[] args) {
        try (var client = new ObtraceClient(ObtraceConfig.builder()
                .apiKey(System.getenv("OBTRACE_API_KEY"))
                .serviceName("checkout-api")
                .build())) {
            client.log("info", "service.started", null);
        }
    }
}

Two fields are required: apiKey and serviceName. Everything else has defaults.

ObtraceClient implements AutoCloseable, so use try-with-resources to ensure the internal executor shuts down cleanly. The default config also registers a JVM shutdown hook that calls flush() before exit. Disable it with .registerShutdownHook(false) on the builder if you manage lifecycle yourself.

Built on OpenTelemetry

The Java SDK uses the OpenTelemetry Java SDK to export telemetry to the Obtrace ingest endpoint. For Spring Boot applications, the obtrace Spring Boot starter provides full auto-instrumentation of HTTP, databases, messaging, and gRPC with zero manual configuration.

What's Captured Automatically

After creating an ObtraceClient, the SDK configures OpenTelemetry and enables available instrumentations. You don't need to write any extra code for this:

WhatHowNeeds code?
Loggingjava.util.logging, Logback, and Log4j2 are captured and mapped to Obtrace levelsNo
Inbound HTTPSpring Boot requests are auto-instrumented via the OTel starterNo (Spring Boot)
Outbound HTTPjava.net.http.HttpClient via getHttpClient()Use getHttpClient()
JDBC / HibernateDatabase queries instrumented with spans via OTelNo (Spring Boot)
gRPCClient and server calls instrumented via OTelNo (Spring Boot)
Kafka / RabbitMQMessage production and consumption instrumentedNo (Spring Boot)
RedisLettuce and Jedis clients instrumentedNo (Spring Boot)
Shutdown flushA JVM shutdown hook flushes pending telemetry before exit (disable with .registerShutdownHook(false))No

Instrumented HTTP Client

The SDK provides an instrumented java.net.http.HttpClient that automatically creates spans for outbound HTTP calls and injects trace propagation headers:

var httpClient = client.getHttpClient();
var req = HttpRequest.newBuilder()
    .uri(URI.create("https://api.stripe.com/v1/charges"))
    .POST(HttpRequest.BodyPublishers.ofString(body))
    .build();
var resp = httpClient.send(req, HttpResponse.BodyHandlers.ofString());

Each call through getHttpClient() emits a span with method, URL, status code, and duration. The traceparent header is injected automatically.

Optional: Custom Telemetry

The automatic log capture covers basic observability. For business-specific events, use the SDK methods directly.

Logging

Use client.log(level, message, ctx) to record events you need to search for later. Good candidates:

  • Caught exceptions and error conditions
  • Business events (order placed, payment failed, user signed up)
  • Audit trail entries (permission changed, API key rotated)
  • State transitions (circuit breaker opened, cache eviction)
import io.obtrace.sdk.model.ObtraceContext;
 
var ctx = ObtraceContext.builder()
    .attr("order_id", orderId)
    .attr("provider", "stripe")
    .attr("decline_code", resp.getDeclineCode())
    .build();
client.log("error", "payment.declined", ctx);
 
client.log("info", "user.signup", null);

The level parameter accepts: debug, info, warn, error, fatal. Messages are truncated at 32KB.

The ctx parameter is optional (pass null to skip). When provided, it attaches structured attributes to the log entry. Use attr() on the builder for custom key-value pairs, and the built-in fields (traceId, spanId, method, endpoint, statusCode) to correlate logs with traces.

Metrics

Use client.metric(name, value, unit, ctx) to record measurements you want to graph or alert on. Good candidates:

  • Latency measurements (how long did this take?)
  • Counters (how many orders per minute?)
  • Gauges (current queue depth, active connections, heap usage)
  • Business KPIs (revenue per checkout, items per cart)
client.metric("http.server.duration", 142.5, "ms", ObtraceContext.builder()
    .method("POST")
    .endpoint("/api/checkout")
    .build());
 
client.metric("checkout.revenue", 49.99, "USD", null);
 
client.metric("queue.depth", workQueue.size(), "1", null);

The unit parameter follows OTLP conventions: "ms" for milliseconds, "By" for bytes, "1" for dimensionless values (counts, ratios), or a currency code.

Tracing / Spans

Use client.span(...) to track a unit of work and its duration. Returns a String[] of {traceId, spanId} so you can propagate context to downstream services. Good candidates:

  • HTTP handler execution (request in, response out)
  • Database queries
  • External API calls
  • Background job processing
  • Any operation where you need to see where time was spent
import java.util.Map;
 
String[] ids = client.span("db.query", null, null, null, "", Map.of(
    "db.system", "postgresql",
    "db.statement", "SELECT * FROM orders WHERE id = ?"
));
 
client.span("http.client POST /payments", ids[0], null, null, "", Map.of(
    "http.method", "POST",
    "http.url", "https://api.stripe.com/v1/charges"
));

Pass null for traceId and spanId to auto-generate them. Pass an existing traceId (from ids[0]) to group spans into the same trace. The statusCode uses OTLP conventions: 0 = unset, 1 = OK, 2 = error.

The 7-argument overload also accepts parentSpanId to build span hierarchies within a single service.

Propagating Trace Context

When using getHttpClient(), trace context is injected automatically. For other HTTP clients, inject manually:

String[] ids = client.span("http.client GET /inventory", null, null, null, "", Map.of(
    "http.method", "GET",
    "http.url", "https://inventory.internal/stock"
));
 
Map<String, String> headers = client.injectPropagation(null, ids[0], ids[1], null);
HttpRequest req = HttpRequest.newBuilder()
    .uri(URI.create("https://inventory.internal/stock"))
    .header("traceparent", headers.get("traceparent"))
    .GET()
    .build();

Framework Integration

Spring Boot

For Spring Boot applications, the obtrace Spring Boot starter auto-configures OpenTelemetry with full instrumentation of HTTP requests, JDBC, Hibernate, gRPC, Kafka, RabbitMQ, and Redis. Register the client as a Spring bean and use SpringObtraceFilter to instrument request lifecycle:

ObtraceBootstrap.java
import io.obtrace.sdk.core.ObtraceClient;
import io.obtrace.sdk.model.ObtraceConfig;
import io.obtrace.sdk.framework.SpringObtraceFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
 
@Configuration
public class ObtraceBootstrap {
    @Bean
    public ObtraceClient obtraceClient() {
        return new ObtraceClient(ObtraceConfig.builder()
            .apiKey(System.getenv("OBTRACE_API_KEY"))
            .serviceName("checkout-api")
            .env(System.getenv("OBTRACE_ENV"))
            .serviceVersion(System.getenv("OBTRACE_SERVICE_VERSION"))
            .build());
    }
 
    @Bean
    public SpringObtraceFilter obtraceFilter(ObtraceClient client) {
        return new SpringObtraceFilter(client);
    }
}

Then call afterRequest from a servlet filter or HandlerInterceptor:

ObtraceInterceptor.java
import io.obtrace.sdk.framework.SpringObtraceFilter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
 
import java.time.Instant;
 
@Component
public class ObtraceInterceptor implements HandlerInterceptor {
    private final SpringObtraceFilter filter;
    private static final ThreadLocal<Instant> START = new ThreadLocal<>();
 
    public ObtraceInterceptor(SpringObtraceFilter filter) {
        this.filter = filter;
    }
 
    @Override
    public boolean preHandle(HttpServletRequest req, HttpServletResponse res, Object handler) {
        START.set(Instant.now());
        return true;
    }
 
    @Override
    public void afterCompletion(HttpServletRequest req, HttpServletResponse res, Object handler, Exception ex) {
        filter.afterRequest(req.getMethod(), req.getRequestURI(), res.getStatus(), START.get());
        START.remove();
    }
}

Your controllers can still call client.log(), client.metric(), and client.span() directly for business-specific telemetry. The interceptor handles the per-request baseline.

Configuration Reference

All config is set through ObtraceConfig.builder():

Builder methodTypeDefaultDescription
.apiKey()StringRequired. Your Obtrace API key.
.serviceName()StringRequired. Stable name for this service.
.serviceVersion()String"0.0.0"Deployment version (git SHA, semver, date).
.tenantId()StringnullScoped ingest identity.
.projectId()StringnullScoped ingest identity.
.appId()StringnullScoped ingest identity.
.env()StringnullEnvironment name (prod, staging, dev).
.requestTimeoutMs()int5000HTTP timeout per OTLP request in milliseconds.
.maxQueueSize()int1000Max queued telemetry items before oldest are dropped.
.flushTimeoutMs()int30000Max time allowed for a single flush cycle.
.validateSemanticMetrics()booleanfalseWhen true + debug, warns on non-canonical metric names.
.debug()booleanfalseEnables SDK diagnostic output to stderr.
.registerShutdownHook()booleantrueRegisters a JVM shutdown hook that flushes on exit.

Validation Checklist

After integrating the SDK, verify these before shipping to production:

  • serviceName, env, and serviceVersion are set and survive restarts and deploys
  • At least one request path emits both a log and a span
  • ObtraceClient is closed on shutdown (try-with-resources, Spring @PreDestroy, or shutdown hook)
  • No 401 or 403 errors appear during OTLP submission (check with .debug(true))
  • Metrics use correct units (ms, By, 1) not free-form strings
  • Outbound HTTP calls use getHttpClient() or injectPropagation if the downstream service also reports to Obtrace
  • Spring interceptor is registered in WebMvcConfigurer.addInterceptors if using Spring

See also: Semantic Metrics

On this page