OpenTelemetry JavaScript Sampling
What is sampling?
Sampling is a process that restricts the amount of spans 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.
JavaScript sampling overview
OpenTelemetry JavaScript 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. You can configure a sampler on the tracer provider using the sampler option.
Built-in samplers
The JavaScript SDK provides several built-in samplers for common use cases.
AlwaysOnSampler
Samples every trace. Useful for development environments but use with caution in production with significant traffic:
const { NodeTracerProvider } = require('@opentelemetry/sdk-trace-node');
const { AlwaysOnSampler } = require('@opentelemetry/sdk-trace-base');
const provider = new NodeTracerProvider({
sampler: new AlwaysOnSampler(),
});
provider.register();
AlwaysOffSampler
Samples no traces. Useful for completely disabling tracing:
const { NodeTracerProvider } = require('@opentelemetry/sdk-trace-node');
const { AlwaysOffSampler } = require('@opentelemetry/sdk-trace-base');
const provider = new NodeTracerProvider({
sampler: new AlwaysOffSampler(),
});
provider.register();
TraceIdRatioBasedSampler
Samples a fraction of traces based on the trace ID. The fraction should be between 0.0 and 1.0:
const { NodeTracerProvider } = require('@opentelemetry/sdk-trace-node');
const { TraceIdRatioBasedSampler } = require('@opentelemetry/sdk-trace-base');
// Sample 10% of traces
const provider = new NodeTracerProvider({
sampler: new TraceIdRatioBasedSampler(0.1),
});
provider.register();
ParentBasedSampler
A sampler decorator that behaves differently based on the parent of the span. If the span has no parent, the root sampler is used to make the sampling decision. If the span has a parent, the sampler follows the parent's sampling decision:
const { NodeTracerProvider } = require('@opentelemetry/sdk-trace-node');
const {
ParentBasedSampler,
TraceIdRatioBasedSampler,
AlwaysOnSampler,
AlwaysOffSampler,
} = require('@opentelemetry/sdk-trace-base');
// ParentBased with TraceIdRatioBased root sampler
const provider = new NodeTracerProvider({
sampler: new ParentBasedSampler({
root: new TraceIdRatioBasedSampler(0.1),
}),
});
provider.register();
The ParentBasedSampler accepts additional configuration options:
const { NodeTracerProvider } = require('@opentelemetry/sdk-trace-node');
const {
ParentBasedSampler,
TraceIdRatioBasedSampler,
AlwaysOnSampler,
AlwaysOffSampler,
} = require('@opentelemetry/sdk-trace-base');
const provider = new NodeTracerProvider({
sampler: new ParentBasedSampler({
// Sampler for root spans (no parent)
root: new TraceIdRatioBasedSampler(0.5),
// Sampler when remote parent is sampled
remoteParentSampled: new AlwaysOnSampler(),
// Sampler when remote parent is not sampled
remoteParentNotSampled: new AlwaysOffSampler(),
// Sampler when local parent is sampled
localParentSampled: new AlwaysOnSampler(),
// Sampler when local parent is not sampled
localParentNotSampled: new AlwaysOffSampler(),
}),
});
provider.register();
Configuration options
Environment variables
You can configure sampling using environment variables:
# TraceIdRatioBased sampler with 10% sampling
export OTEL_TRACES_SAMPLER="traceidratio"
export OTEL_TRACES_SAMPLER_ARG="0.1"
# ParentBased with TraceIdRatioBased
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"
# ParentBased with AlwaysOn (default)
export OTEL_TRACES_SAMPLER="parentbased_always_on"
Programmatic configuration
const { NodeTracerProvider } = require('@opentelemetry/sdk-trace-node');
const { BatchSpanProcessor } = require('@opentelemetry/sdk-trace-base');
const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-http');
const { resourceFromAttributes } = require('@opentelemetry/resources');
const { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } = require('@opentelemetry/semantic-conventions');
const {
ParentBasedSampler,
TraceIdRatioBasedSampler,
AlwaysOnSampler,
} = require('@opentelemetry/sdk-trace-base');
function setupTracing() {
// Create OTLP exporter
const exporter = new OTLPTraceExporter({
url: 'https://api.uptrace.dev/v1/traces',
headers: {
'uptrace-dsn': process.env.UPTRACE_DSN,
},
});
// Configure sampler based on environment
let sampler;
const env = process.env.NODE_ENV;
switch (env) {
case 'development':
sampler = new AlwaysOnSampler();
break;
case 'production':
// 10% sampling for production
sampler = new ParentBasedSampler({
root: new TraceIdRatioBasedSampler(0.1),
});
break;
default:
// 25% sampling for staging/test
sampler = new ParentBasedSampler({
root: new TraceIdRatioBasedSampler(0.25),
});
}
// Create tracer provider
const provider = new NodeTracerProvider({
resource: resourceFromAttributes({
[ATTR_SERVICE_NAME]: 'my-service',
[ATTR_SERVICE_VERSION]: '1.0.0',
}),
sampler: sampler,
spanProcessors: [new BatchSpanProcessor(exporter)],
});
provider.register();
return () => provider.shutdown();
}
module.exports = { setupTracing };
Custom samplers
Create custom sampling logic by implementing the Sampler interface:
const { NodeTracerProvider } = require('@opentelemetry/sdk-trace-node');
const { SamplingDecision } = require('@opentelemetry/api');
// Custom sampler that samples based on span attributes
class PathBasedSampler {
shouldSample(context, traceId, spanName, spanKind, attributes, links) {
const httpTarget = attributes['http.target'] || attributes['url.path'];
// Always sample API endpoints
if (httpTarget && httpTarget.startsWith('/api/')) {
return {
decision: SamplingDecision.RECORD_AND_SAMPLED,
};
}
// Never sample health check endpoints
if (httpTarget === '/health' || httpTarget === '/healthz' || httpTarget === '/ready') {
return {
decision: SamplingDecision.NOT_RECORD,
};
}
// Sample 10% of other requests
const threshold = 0.1;
const random = Math.random();
return {
decision:
random < threshold
? SamplingDecision.RECORD_AND_SAMPLED
: SamplingDecision.NOT_RECORD,
};
}
toString() {
return 'PathBasedSampler';
}
}
const provider = new NodeTracerProvider({
sampler: new PathBasedSampler(),
});
provider.register();
Attribute-based sampling
const { SamplingDecision } = require('@opentelemetry/api');
const { TraceIdRatioBasedSampler, AlwaysOnSampler } = require('@opentelemetry/sdk-trace-base');
class AttributeBasedSampler {
constructor() {
this.highPrioritySampler = new AlwaysOnSampler();
this.defaultSampler = new TraceIdRatioBasedSampler(0.05); // 5% default
}
shouldSample(context, traceId, spanName, spanKind, attributes, links) {
// Always sample spans with errors
if (attributes['error'] === true) {
return this.highPrioritySampler.shouldSample(
context,
traceId,
spanName,
spanKind,
attributes,
links
);
}
// Always sample critical operations
if (
spanName.includes('critical') ||
spanName.includes('payment') ||
spanName.includes('auth')
) {
return this.highPrioritySampler.shouldSample(
context,
traceId,
spanName,
spanKind,
attributes,
links
);
}
// Check for high priority attributes
const httpRoute = attributes['http.route'];
if (httpRoute) {
// Always sample admin routes
if (httpRoute.startsWith('/admin')) {
return this.highPrioritySampler.shouldSample(
context,
traceId,
spanName,
spanKind,
attributes,
links
);
}
}
// Check user tier
const userTier = attributes['user.tier'];
if (userTier === 'premium') {
return this.highPrioritySampler.shouldSample(
context,
traceId,
spanName,
spanKind,
attributes,
links
);
}
// Check HTTP status code for errors
const statusCode = attributes['http.status_code'] || attributes['http.response.status_code'];
if (statusCode && statusCode >= 400) {
return this.highPrioritySampler.shouldSample(
context,
traceId,
spanName,
spanKind,
attributes,
links
);
}
// Use default sampler for other cases
return this.defaultSampler.shouldSample(
context,
traceId,
spanName,
spanKind,
attributes,
links
);
}
toString() {
return 'AttributeBasedSampler{admin=always,premium=always,errors=always,default=5%}';
}
}
Rate-limiting sampler
const { SamplingDecision } = require('@opentelemetry/api');
class RateLimitingSampler {
constructor(maxSamplesPerSecond) {
this.maxSamplesPerSecond = maxSamplesPerSecond;
this.lastSecond = 0;
this.currentCount = 0;
}
shouldSample(context, traceId, spanName, spanKind, attributes, links) {
const now = Math.floor(Date.now() / 1000);
if (now !== this.lastSecond) {
this.lastSecond = now;
this.currentCount = 0;
}
if (this.currentCount < this.maxSamplesPerSecond) {
this.currentCount++;
return {
decision: SamplingDecision.RECORD_AND_SAMPLED,
};
}
return {
decision: SamplingDecision.NOT_RECORD,
};
}
toString() {
return `RateLimitingSampler{${this.maxSamplesPerSecond} samples/second}`;
}
}
// Usage: limit to 100 samples per second
const provider = new NodeTracerProvider({
sampler: new RateLimitingSampler(100),
});
Production deployment examples
Express.js server with sampling
const express = require('express');
const { NodeTracerProvider } = require('@opentelemetry/sdk-trace-node');
const { BatchSpanProcessor } = require('@opentelemetry/sdk-trace-base');
const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-http');
const { resourceFromAttributes } = require('@opentelemetry/resources');
const { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION, ATTR_DEPLOYMENT_ENVIRONMENT } = require('@opentelemetry/semantic-conventions');
const { registerInstrumentations } = require('@opentelemetry/instrumentation');
const { HttpInstrumentation } = require('@opentelemetry/instrumentation-http');
const { ExpressInstrumentation } = require('@opentelemetry/instrumentation-express');
const { trace } = require('@opentelemetry/api');
const {
ParentBasedSampler,
TraceIdRatioBasedSampler,
AlwaysOnSampler,
} = require('@opentelemetry/sdk-trace-base');
// Setup tracing before importing other modules
function setupTracing() {
const exporter = new OTLPTraceExporter({
url: 'https://api.uptrace.dev/v1/traces',
headers: {
'uptrace-dsn': process.env.UPTRACE_DSN,
},
});
// Configure sampling based on environment
let sampler;
switch (process.env.NODE_ENV) {
case 'development':
sampler = new AlwaysOnSampler();
break;
case 'production':
sampler = new ParentBasedSampler({
root: new TraceIdRatioBasedSampler(0.1), // 10% sampling
});
break;
default:
sampler = new ParentBasedSampler({
root: new TraceIdRatioBasedSampler(0.25), // 25% sampling
});
}
const provider = new NodeTracerProvider({
resource: resourceFromAttributes({
[ATTR_SERVICE_NAME]: 'my-express-server',
[ATTR_SERVICE_VERSION]: '1.0.0',
[ATTR_DEPLOYMENT_ENVIRONMENT]: process.env.NODE_ENV || 'development',
}),
sampler: sampler,
spanProcessors: [
new BatchSpanProcessor(exporter, {
maxQueueSize: 2048,
maxExportBatchSize: 512,
scheduledDelayMillis: 5000,
exportTimeoutMillis: 30000,
}),
],
});
provider.register();
registerInstrumentations({
instrumentations: [new HttpInstrumentation(), new ExpressInstrumentation()],
});
return () => provider.shutdown();
}
const shutdown = setupTracing();
// Create Express app
const app = express();
const tracer = trace.getTracer('my-express-server');
app.get('/api/users/:id', async (req, res) => {
const span = trace.getActiveSpan();
// Add attributes only if span is recording
if (span && span.isRecording()) {
span.setAttribute('user.id', req.params.id);
span.setAttribute('http.route', '/api/users/:id');
}
// Simulate database call
const user = await getUserById(req.params.id);
res.json(user);
});
app.get('/health', (req, res) => {
res.json({ status: 'healthy' });
});
async function getUserById(id) {
return tracer.startActiveSpan('getUserById', async (span) => {
try {
// Simulate database query
await new Promise((resolve) => setTimeout(resolve, 50));
if (span.isRecording()) {
span.setAttribute('db.system', 'postgresql');
span.setAttribute('db.operation', 'SELECT');
}
return { id, name: 'John Doe', email: 'john@example.com' };
} finally {
span.end();
}
});
}
const port = process.env.PORT || 3000;
app.listen(port, () => {
console.log(`Server running on port ${port}`);
});
// Graceful shutdown
process.on('SIGTERM', async () => {
await shutdown();
process.exit(0);
});
Browser application with sampling
import { WebTracerProvider } from '@opentelemetry/sdk-trace-web';
import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { resourceFromAttributes } from '@opentelemetry/resources';
import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from '@opentelemetry/semantic-conventions';
import { ZoneContextManager } from '@opentelemetry/context-zone';
import { registerInstrumentations } from '@opentelemetry/instrumentation';
import { FetchInstrumentation } from '@opentelemetry/instrumentation-fetch';
import { XMLHttpRequestInstrumentation } from '@opentelemetry/instrumentation-xml-http-request';
import {
ParentBasedSampler,
TraceIdRatioBasedSampler,
} from '@opentelemetry/sdk-trace-base';
function setupBrowserTracing() {
const exporter = new OTLPTraceExporter({
url: 'https://api.uptrace.dev/v1/traces',
headers: {
'uptrace-dsn': 'YOUR_UPTRACE_DSN',
},
});
// Use lower sampling rate for browser to reduce data volume
const sampler = new ParentBasedSampler({
root: new TraceIdRatioBasedSampler(0.05), // 5% sampling for browser
});
const provider = new WebTracerProvider({
resource: resourceFromAttributes({
[ATTR_SERVICE_NAME]: 'my-browser-app',
[ATTR_SERVICE_VERSION]: '1.0.0',
}),
sampler: sampler,
spanProcessors: [
new BatchSpanProcessor(exporter, {
maxQueueSize: 100,
maxExportBatchSize: 10,
scheduledDelayMillis: 500,
exportTimeoutMillis: 30000,
}),
],
});
provider.register({
contextManager: new ZoneContextManager(),
});
registerInstrumentations({
instrumentations: [
new FetchInstrumentation({
propagateTraceHeaderCorsUrls: [/your-api-domain\.com/],
clearTimingResources: true,
}),
new XMLHttpRequestInstrumentation({
propagateTraceHeaderCorsUrls: [/your-api-domain\.com/],
}),
],
});
}
setupBrowserTracing();
Composite samplers (experimental)
The OpenTelemetry JavaScript SDK provides experimental composite samplers for more complex sampling scenarios:
const {
createCompositeSampler,
createComposableAlwaysOffSampler,
createComposableAlwaysOnSampler,
createComposableParentThresholdSampler,
createComposableTraceIDRatioBasedSampler,
} = require('@opentelemetry/sampler-composite');
// Never sample
const neverSampler = createCompositeSampler(createComposableAlwaysOffSampler());
// Always sample
const alwaysSampler = createCompositeSampler(createComposableAlwaysOnSampler());
// Follow the parent, or sample with 30% probability for root spans
const parentBasedSampler = createCompositeSampler(
createComposableParentThresholdSampler(createComposableTraceIDRatioBasedSampler(0.3))
);
Best practices
Performance optimization
When implementing sampling, consider these performance best practices:
- Check span recording status: Use
span.isRecording()before adding expensive attributes or operations - Use batch processing: Always use
BatchSpanProcessorinstead ofSimpleSpanProcessorin production - Configure appropriate batch sizes: Balance memory usage and export frequency based on your traffic patterns
- Avoid expensive operations in samplers: Keep
shouldSampleimplementations fast and simple
Production considerations
For production deployments:
- Start conservative: Begin with lower sampling rates (1-5%) and increase gradually based on your observability needs
- Use ParentBasedSampler: Ensures consistent sampling decisions across service boundaries
- Monitor sampling effectiveness: Track sampling rates and ensure you're capturing enough data for meaningful analysis
- Consider costs: Balance observability value with storage and processing costs
- Use environment-based configuration: Configure different sampling rates for development, staging, and production
- Implement fallback strategies: Ensure your application continues to function even if sampling configuration fails
Checking if a span is recording
To avoid unnecessary work when a span is not being sampled:
const { trace } = require('@opentelemetry/api');
function processRequest(req) {
const span = trace.getActiveSpan();
// Only add expensive attributes if the span is recording
if (span && span.isRecording()) {
span.setAttribute('request.body.size', JSON.stringify(req.body).length);
span.setAttribute('request.headers', JSON.stringify(req.headers));
}
// Process the request...
}