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:
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:
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:
// 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:
// 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:
# 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
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:
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
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
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
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
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);
}
}
}