OpenTelemetry Swift Sampling
What is sampling?
Sampling restricts the amount of spans 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.
OpenTelemetry Swift SDK provides head-based sampling where the sampling decision is made at the beginning of a trace.
Built-in samplers
OpenTelemetry Swift provides several built-in samplers:
AlwaysOnSampler
Samples every trace. Useful for development but use with caution in production:
import OpenTelemetrySdk
let sampler = Samplers.alwaysOn
let tracerProvider = TracerProviderBuilder()
.with(sampler: sampler)
.build()
AlwaysOffSampler
Samples no traces. Useful for completely disabling tracing:
let sampler = Samplers.alwaysOff
let tracerProvider = TracerProviderBuilder()
.with(sampler: sampler)
.build()
TraceIdRatioBased
Samples a fraction of traces based on the trace ID:
// Sample 10% of traces
let sampler = Samplers.traceIdRatio(ratio: 0.1)
let tracerProvider = TracerProviderBuilder()
.with(sampler: sampler)
.build()
ParentBased
Respects the parent span's sampling decision. If there's no parent, uses the provided root sampler:
// ParentBased with 10% sampling for root spans
let sampler = Samplers.parentBased(root: Samplers.traceIdRatio(ratio: 0.1))
let tracerProvider = TracerProviderBuilder()
.with(sampler: sampler)
.build()
Configuration examples
Development configuration
Sample all traces during development:
func configureDevelopmentTracing() {
let tracerProvider = TracerProviderBuilder()
.with(sampler: Samplers.alwaysOn)
.add(spanProcessor: SimpleSpanProcessor(spanExporter: StdoutExporter()))
.build()
OpenTelemetry.registerTracerProvider(tracerProvider: tracerProvider)
}
Production configuration
Use ratio-based sampling in production:
func configureProductionTracing(dsn: String) {
// Sample 10% of traces
let sampler = Samplers.parentBased(root: Samplers.traceIdRatio(ratio: 0.1))
let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
let channel = ClientConnection
.usingPlatformAppropriateTLS(for: group)
.connect(host: "api.uptrace.dev", port: 4317)
let otlpConfig = OtlpConfiguration(
timeout: OtlpConfiguration.DefaultTimeoutInterval,
headers: [("uptrace-dsn", dsn)]
)
let traceExporter = OtlpTraceExporter(channel: channel, config: otlpConfig)
let spanProcessor = BatchSpanProcessor(spanExporter: traceExporter)
let tracerProvider = TracerProviderBuilder()
.with(sampler: sampler)
.add(spanProcessor: spanProcessor)
.with(resource: DefaultResources().get())
.build()
OpenTelemetry.registerTracerProvider(tracerProvider: tracerProvider)
}
Environment-based configuration
Choose sampler based on environment:
func configureTracing() {
let environment = ProcessInfo.processInfo.environment["ENVIRONMENT"] ?? "development"
let sampleRate = Double(ProcessInfo.processInfo.environment["OTEL_SAMPLE_RATE"] ?? "1.0") ?? 1.0
let sampler: Sampler
switch environment {
case "production":
sampler = Samplers.parentBased(root: Samplers.traceIdRatio(ratio: sampleRate))
case "staging":
sampler = Samplers.parentBased(root: Samplers.traceIdRatio(ratio: 0.5))
default:
sampler = Samplers.alwaysOn
}
let tracerProvider = TracerProviderBuilder()
.with(sampler: sampler)
// ... add processors
.build()
OpenTelemetry.registerTracerProvider(tracerProvider: tracerProvider)
}
Custom samplers
Create custom samplers for advanced use cases:
import OpenTelemetrySdk
class CustomSampler: Sampler {
var description: String {
return "CustomSampler"
}
func shouldSample(
parentContext: SpanContext?,
traceId: TraceId,
name: String,
kind: SpanKind,
attributes: [String: AttributeValue],
links: [SpanData.Link]
) -> SamplingResult {
// Always sample error spans
if name.contains("error") || name.contains("exception") {
return SamplingResult(decision: .recordAndSample)
}
// Always sample slow operations (if detected by name)
if name.contains("slow") {
return SamplingResult(decision: .recordAndSample)
}
// Sample 10% of everything else
let hash = traceId.rawHigherLong ^ traceId.rawLowerLong
let threshold = UInt64(0.1 * Double(UInt64.max))
if hash < threshold {
return SamplingResult(decision: .recordAndSample)
}
return SamplingResult(decision: .drop)
}
}
// Usage
let sampler = CustomSampler()
let tracerProvider = TracerProviderBuilder()
.with(sampler: sampler)
.build()
Attribute-based sampling
Sample based on span attributes:
class AttributeBasedSampler: Sampler {
private let baseSampler: Sampler
private let alwaysSampleAttributes: [String: String]
init(baseSampler: Sampler, alwaysSampleAttributes: [String: String]) {
self.baseSampler = baseSampler
self.alwaysSampleAttributes = alwaysSampleAttributes
}
var description: String {
return "AttributeBasedSampler(base: \(baseSampler.description))"
}
func shouldSample(
parentContext: SpanContext?,
traceId: TraceId,
name: String,
kind: SpanKind,
attributes: [String: AttributeValue],
links: [SpanData.Link]
) -> SamplingResult {
// Check if any attribute matches our always-sample criteria
for (key, value) in alwaysSampleAttributes {
if let attrValue = attributes[key],
case .string(let stringValue) = attrValue,
stringValue == value {
return SamplingResult(decision: .recordAndSample)
}
}
// Fall back to base sampler
return baseSampler.shouldSample(
parentContext: parentContext,
traceId: traceId,
name: name,
kind: kind,
attributes: attributes,
links: links
)
}
}
// Usage: Always sample premium users, sample 5% of others
let sampler = AttributeBasedSampler(
baseSampler: Samplers.traceIdRatio(ratio: 0.05),
alwaysSampleAttributes: ["user.tier": "premium"]
)
Error-biased sampling
Always sample errors while applying ratio to successes:
class ErrorBiasedSampler: Sampler {
private let successRatio: Double
init(successRatio: Double) {
self.successRatio = successRatio
}
var description: String {
return "ErrorBiasedSampler(successRatio: \(successRatio))"
}
func shouldSample(
parentContext: SpanContext?,
traceId: TraceId,
name: String,
kind: SpanKind,
attributes: [String: AttributeValue],
links: [SpanData.Link]
) -> SamplingResult {
// Respect parent sampling decision
if let parentContext = parentContext, parentContext.traceFlags.sampled {
return SamplingResult(decision: .recordAndSample)
}
// For root spans, apply ratio sampling
let hash = traceId.rawHigherLong ^ traceId.rawLowerLong
let threshold = UInt64(successRatio * Double(UInt64.max))
if hash < threshold {
return SamplingResult(decision: .recordAndSample)
}
return SamplingResult(decision: .drop)
}
}
Performance optimization
Use isRecording to skip expensive operations on non-sampled spans:
func handleRequest() {
let tracer = OpenTelemetry.instance.tracerProvider.get(
instrumentationName: "MyApp",
instrumentationVersion: "1.0.0"
)
let span = tracer.spanBuilder(spanName: "handle-request").startSpan()
defer { span.end() }
// Only add expensive attributes if span is being recorded
if span.isRecording {
span.setAttribute(key: "request.body", value: serializeRequestBody())
span.setAttribute(key: "user.details", value: fetchUserDetails())
}
// Process request...
}
Sampling recommendations
Mobile/iOS applications
- Use lower sampling rates (1-5%) to reduce data usage
- Consider sampling based on network type (WiFi vs cellular)
- Always sample error traces
func configureMobileSampling() {
let sampler: Sampler
#if DEBUG
sampler = Samplers.alwaysOn
#else
// Low sample rate for production mobile apps
sampler = Samplers.parentBased(root: Samplers.traceIdRatio(ratio: 0.01))
#endif
let tracerProvider = TracerProviderBuilder()
.with(sampler: sampler)
.build()
OpenTelemetry.registerTracerProvider(tracerProvider: tracerProvider)
}
Server-side Swift
- Use parent-based sampling to respect upstream decisions
- Start with 10% and adjust based on traffic
- Use consistent sampling across services
func configureServerSampling() {
// Recommended: 10% sampling with parent respect
let sampler = Samplers.parentBased(root: Samplers.traceIdRatio(ratio: 0.1))
let tracerProvider = TracerProviderBuilder()
.with(sampler: sampler)
.build()
OpenTelemetry.registerTracerProvider(tracerProvider: tracerProvider)
}
Best practices
- Start conservative - Begin with 1-5% sampling and increase based on needs
- Use parent-based sampling - Ensures consistent decisions across distributed traces
- Always sample errors - Create custom samplers that always capture error traces
- Monitor sampling rates - Track your actual sampling to ensure meaningful data
- Environment-specific rates - Higher rates in development, lower in production