OpenTelemetry Sampling [Java]

What is sampling?

Sampling is a process that restricts the amount of traces that are generated by a system. In high-volume applications, collecting 100% of traces can be expensive and unnecessary. Sampling allows you to collect a representative subset of traces while reducing costs and performance overhead.

Java sampling

OpenTelemetry Java SDK provides head-based sampling capabilities where the sampling decision is made at the beginning of a trace. By default, the tracer provider uses a ParentBased sampler with the AlwaysOn sampler. A sampler can be set on the tracer provider using the setSampler() method.

Built-in samplers

AlwaysOn

Samples every trace. Useful for development environments but be careful in production with significant traffic:

java
import io.opentelemetry.sdk.trace.SdkTracerProvider;
import io.opentelemetry.sdk.trace.samplers.Sampler;

SdkTracerProvider tracerProvider = SdkTracerProvider.builder()
    .setSampler(Sampler.traceIdRatioBased(1.0))
    .build();

AlwaysOff

Samples no traces. Useful for completely disabling tracing:

java
SdkTracerProvider tracerProvider = SdkTracerProvider.builder()
    .setSampler(Sampler.traceIdRatioBased(0.0))
    .build();

TraceIdRatioBased

Samples a fraction of spans based on the trace ID. The ratio should be between 0.0 and 1.0:

java
// Sample 10% of traces
SdkTracerProvider tracerProvider = SdkTracerProvider.builder()
    .setSampler(Sampler.traceIdRatioBased(0.1))
    .build();

// Sample 50% of traces
SdkTracerProvider tracerProvider = SdkTracerProvider.builder()
    .setSampler(Sampler.traceIdRatioBased(0.5))
    .build();

ParentBased

A sampler decorator that behaves differently based on the parent of the span. If the span has no parent, the decorated sampler is used to make the sampling decision:

java
// ParentBased with TraceIdRatioBased root sampler
SdkTracerProvider tracerProvider = SdkTracerProvider.builder()
    .setSampler(Sampler.parentBased(Sampler.traceIdRatioBased(0.1)))
    .build();

// ParentBased with AlwaysOn root sampler (default behavior)
SdkTracerProvider tracerProvider = SdkTracerProvider.builder()
    .setSampler(Sampler.parentBased(Sampler.traceIdRatioBased(1.0)))
    .build();

Configuration in Java

Environment variable

You can configure sampling using environment variables:

bash
# TraceIdRatio sampler with 50% sampling
export OTEL_TRACES_SAMPLER="traceidratio"
export OTEL_TRACES_SAMPLER_ARG="0.5"

# ParentBased with TraceIdRatio
export OTEL_TRACES_SAMPLER="parentbased_traceidratio"
export OTEL_TRACES_SAMPLER_ARG="0.1"

# Always sample
export OTEL_TRACES_SAMPLER="always_on"

# Never sample
export OTEL_TRACES_SAMPLER="always_off"

Programmatic configuration

java
package com.example;

import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter;
import io.opentelemetry.sdk.OpenTelemetrySdk;
import io.opentelemetry.sdk.resources.Resource;
import io.opentelemetry.sdk.trace.SdkTracerProvider;
import io.opentelemetry.sdk.trace.export.BatchSpanProcessor;
import io.opentelemetry.sdk.trace.samplers.Sampler;
import io.opentelemetry.semconv.resource.attributes.ResourceAttributes;

public class TracingSetup {

    public static OpenTelemetry setupTracing() {
        // Create OTLP exporter
        OtlpGrpcSpanExporter spanExporter = OtlpGrpcSpanExporter.builder()
            .setEndpoint("https://api.uptrace.dev:4317")
            .addHeader("uptrace-dsn", System.getenv("UPTRACE_DSN"))
            .build();

        // Create resource
        Resource resource = Resource.getDefault()
            .merge(Resource.builder()
                .put(ResourceAttributes.SERVICE_NAME, "my-service")
                .put(ResourceAttributes.SERVICE_VERSION, "1.0.0")
                .build());

        // Configure sampler based on environment
        Sampler sampler;
        String env = System.getenv("JAVA_ENV");
        switch (env != null ? env : "default") {
            case "development":
                sampler = Sampler.traceIdRatioBased(1.0);
                break;
            case "production":
                sampler = Sampler.parentBased(Sampler.traceIdRatioBased(0.1)); // 10% sampling
                break;
            default:
                sampler = Sampler.parentBased(Sampler.traceIdRatioBased(0.25)); // 25% sampling
        }

        // Create tracer provider
        SdkTracerProvider tracerProvider = SdkTracerProvider.builder()
            .addSpanProcessor(BatchSpanProcessor.builder(spanExporter).build())
            .setResource(resource)
            .setSampler(sampler)
            .build();

        OpenTelemetry openTelemetry = OpenTelemetrySdk.builder()
            .setTracerProvider(tracerProvider)
            .build();

        // Add shutdown hook
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            tracerProvider.close();
        }));

        return openTelemetry;
    }
}

Custom sampler

Create custom sampling logic by implementing the Sampler interface:

java
package com.example;

import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.trace.SpanKind;
import io.opentelemetry.context.Context;
import io.opentelemetry.sdk.trace.data.LinkData;
import io.opentelemetry.sdk.trace.samplers.Sampler;
import io.opentelemetry.sdk.trace.samplers.SamplingDecision;
import io.opentelemetry.sdk.trace.samplers.SamplingResult;

import java.util.List;

public class CustomSampler implements Sampler {

    private final Sampler defaultSampler;

    public CustomSampler() {
        this.defaultSampler = Sampler.traceIdRatioBased(0.1); // 10% default sampling
    }

    @Override
    public SamplingResult shouldSample(
            Context parentContext,
            String traceId,
            String name,
            SpanKind spanKind,
            Attributes attributes,
            List<LinkData> parentLinks) {

        // Always sample critical operations
        if (name.contains("critical") ||
            name.contains("payment") ||
            name.contains("auth")) {
            return SamplingResult.create(SamplingDecision.RECORD_AND_SAMPLE);
        }

        // Don't sample health checks
        if (name.contains("health") || name.contains("ping")) {
            return SamplingResult.create(SamplingDecision.DROP);
        }

        // Use default sampler for other cases
        return defaultSampler.shouldSample(parentContext, traceId, name, spanKind, attributes, parentLinks);
    }

    @Override
    public String getDescription() {
        return "CustomSampler{critical=always,health=never,default=10%}";
    }
}

// Usage
public class Main {
    public static void main(String[] args) {
        SdkTracerProvider tracerProvider = SdkTracerProvider.builder()
            .setSampler(new CustomSampler())
            .build();

        OpenTelemetry openTelemetry = OpenTelemetrySdk.builder()
            .setTracerProvider(tracerProvider)
            .build();
    }
}

Advanced scenarios

Attribute-based sampling

java
package com.example;

import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.trace.SpanKind;
import io.opentelemetry.context.Context;
import io.opentelemetry.sdk.trace.data.LinkData;
import io.opentelemetry.sdk.trace.samplers.Sampler;
import io.opentelemetry.sdk.trace.samplers.SamplingDecision;
import io.opentelemetry.sdk.trace.samplers.SamplingResult;

import java.util.List;

public class AttributeBasedSampler implements Sampler {

    private final Sampler highPrioritySampler;
    private final Sampler defaultSampler;

    public AttributeBasedSampler() {
        this.highPrioritySampler = Sampler.traceIdRatioBased(1.0);
        this.defaultSampler = Sampler.traceIdRatioBased(0.05); // 5% default
    }

    @Override
    public SamplingResult shouldSample(
            Context parentContext,
            String traceId,
            String name,
            SpanKind spanKind,
            Attributes attributes,
            List<LinkData> parentLinks) {

        // Always sample admin routes
        String httpRoute = attributes.get(AttributeKey.stringKey("http.route"));
        if (httpRoute != null && httpRoute.startsWith("/admin")) {
            return highPrioritySampler.shouldSample(parentContext, traceId, name, spanKind, attributes, parentLinks);
        }

        // Always sample error responses
        Long statusCode = attributes.get(AttributeKey.longKey("http.status_code"));
        if (statusCode != null && statusCode >= 400) {
            return highPrioritySampler.shouldSample(parentContext, traceId, name, spanKind, attributes, parentLinks);
        }

        return defaultSampler.shouldSample(parentContext, traceId, name, spanKind, attributes, parentLinks);
    }

    @Override
    public String getDescription() {
        return "AttributeBasedSampler{admin=always,errors=always,default=5%}";
    }
}

Java production deployment

HTTP server with tracing

java
package com.example;

import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.context.Scope;
import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter;
import io.opentelemetry.sdk.OpenTelemetrySdk;
import io.opentelemetry.sdk.resources.Resource;
import io.opentelemetry.sdk.trace.SdkTracerProvider;
import io.opentelemetry.sdk.trace.export.BatchSpanProcessor;
import io.opentelemetry.sdk.trace.samplers.Sampler;
import io.opentelemetry.semconv.resource.attributes.ResourceAttributes;

import com.sun.net.httpserver.HttpServer;
import java.net.InetSocketAddress;

public class HttpServerExample {

    public static void main(String[] args) throws Exception {
        OpenTelemetry openTelemetry = setupTracing();
        Tracer tracer = openTelemetry.getTracer("my-http-server");

        HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0);
        server.createContext("/hello", exchange -> {
            Span span = tracer.spanBuilder("handle-request").startSpan();
            try (Scope scope = span.makeCurrent()) {

                if (span.isRecording()) {
                    span.setAttribute("http.method", exchange.getRequestMethod());
                    span.setAttribute("http.status_code", 200);
                }

                String response = "Hello, World!";
                exchange.sendResponseHeaders(200, response.length());
                exchange.getResponseBody().write(response.getBytes());
                exchange.getResponseBody().close();

            } finally {
                span.end();
            }
        });

        server.start();
        System.out.println("Server started on port 8080");
    }

    private static OpenTelemetry setupTracing() {
        OtlpGrpcSpanExporter spanExporter = OtlpGrpcSpanExporter.builder()
            .setEndpoint("https://api.uptrace.dev:4317")
            .addHeader("uptrace-dsn", System.getenv("UPTRACE_DSN"))
            .build();

        Resource resource = Resource.getDefault()
            .merge(Resource.builder()
                .put(ResourceAttributes.SERVICE_NAME, "my-http-server")
                .put(ResourceAttributes.SERVICE_VERSION, "1.0.0")
                .build());

        // Configure sampling based on environment
        Sampler sampler = "production".equals(System.getenv("ENVIRONMENT"))
            ? Sampler.parentBased(Sampler.traceIdRatioBased(0.1))
            : Sampler.traceIdRatioBased(1.0);

        SdkTracerProvider tracerProvider = SdkTracerProvider.builder()
            .addSpanProcessor(BatchSpanProcessor.builder(spanExporter).build())
            .setResource(resource)
            .setSampler(sampler)
            .build();

        return OpenTelemetrySdk.builder()
            .setTracerProvider(tracerProvider)
            .build();
    }
}

Database operations

java
public class UserService {

    private final Connection connection;
    private final Tracer tracer;

    public UserService(Connection connection, Tracer tracer) {
        this.connection = connection;
        this.tracer = tracer;
    }

    public void createUser(User user) throws SQLException {
        Span span = tracer.spanBuilder("create-user").startSpan();
        try (Scope scope = span.makeCurrent()) {

            if (span.isRecording()) {
                span.setAttribute("user.email", user.getEmail());
                span.setAttribute("operation.result", "success");
            }

            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();
            }

        } catch (SQLException e) {
            span.recordException(e);
            span.setStatus(StatusCode.ERROR, "Failed to create user");
            throw e;
        } finally {
            span.end();
        }
    }
}

Monitoring sampling

Statistics collector

java
public class SamplingStatsCollector implements SpanProcessor {

    private final AtomicLong totalSpans = new AtomicLong(0);
    private final AtomicLong sampledSpans = new AtomicLong(0);

    @Override
    public void onStart(Context parentContext, ReadWriteSpan span) {
        totalSpans.incrementAndGet();
        if (span.getSpanContext().isSampled()) {
            sampledSpans.incrementAndGet();
        }
    }

    @Override
    public boolean isStartRequired() {
        return true;
    }

    @Override
    public void onEnd(ReadableSpan span) {}

    @Override
    public boolean isEndRequired() {
        return false;
    }

    public void logStats() {
        long total = totalSpans.get();
        long sampled = sampledSpans.get();

        if (total > 0) {
            double rate = (double) sampled / total * 100;
            System.out.printf("Sampling rate: %.2f%% (%d/%d spans)%n", rate, sampled, total);
        }
    }
}

What's next?