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

OpenTelemetry APM

Uptrace is a OpenTelemetry backend 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 with 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?