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:

swift
import OpenTelemetrySdk

let sampler = Samplers.alwaysOn

let tracerProvider = TracerProviderBuilder()
    .with(sampler: sampler)
    .build()

AlwaysOffSampler

Samples no traces. Useful for completely disabling tracing:

swift
let sampler = Samplers.alwaysOff

let tracerProvider = TracerProviderBuilder()
    .with(sampler: sampler)
    .build()

TraceIdRatioBased

Samples a fraction of traces based on the trace ID:

swift
// 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:

swift
// 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:

swift
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:

swift
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:

swift
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:

swift
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:

swift
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:

swift
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:

swift
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
swift
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
swift
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

  1. Start conservative - Begin with 1-5% sampling and increase based on needs
  2. Use parent-based sampling - Ensures consistent decisions across distributed traces
  3. Always sample errors - Create custom samplers that always capture error traces
  4. Monitor sampling rates - Track your actual sampling to ensure meaningful data
  5. Environment-specific rates - Higher rates in development, lower in production

What's next?