OpenTelemetry Sampling [.NET]

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.

.NET sampling implementation

OpenTelemetry .NET SDK provides built-in sampling capabilities through the Sampler class. The sampling decision is made at the beginning of a trace (head-based sampling) and affects the entire trace tree.

Built-in samplers

AlwaysOnSampler

Samples 100% of traces. Useful for development and debugging:

cs
using OpenTelemetry;
using OpenTelemetry.Trace;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddOpenTelemetry()
    .WithTracing(tracing => tracing
        .SetSampler(new AlwaysOnSampler())
        .AddAspNetCoreInstrumentation()
        .AddOtlpExporter()
    );

AlwaysOffSampler

Disables tracing entirely. Useful for temporarily turning off tracing:

cs
builder.Services.AddOpenTelemetry()
    .WithTracing(tracing => tracing
        .SetSampler(new AlwaysOffSampler())
        .AddAspNetCoreInstrumentation()
        .AddOtlpExporter()
    );

TraceIdRatioBasedSampler

Samples a percentage of traces based on the trace ID. The sampling rate is consistent across all services:

cs
// Sample 10% of traces
builder.Services.AddOpenTelemetry()
    .WithTracing(tracing => tracing
        .SetSampler(new TraceIdRatioBasedSampler(0.1)) // 10% sampling rate
        .AddAspNetCoreInstrumentation()
        .AddOtlpExporter()
    );

ParentBasedSampler

Respects the sampling decision of the parent span. If there's no parent, it uses a root sampler:

cs
// Parent-based sampler with 25% root sampling
var rootSampler = new TraceIdRatioBasedSampler(0.25);
var parentBasedSampler = new ParentBasedSampler(rootSampler);

builder.Services.AddOpenTelemetry()
    .WithTracing(tracing => tracing
        .SetSampler(parentBasedSampler)
        .AddAspNetCoreInstrumentation()
        .AddOtlpExporter()
    );

Configuration in .NET

Environment variable configuration

You can configure sampling using environment variables:

bash
# Set sampling ratio to 10%
OTEL_TRACES_SAMPLER=traceidratio
OTEL_TRACES_SAMPLER_ARG=0.1

# Always sample
OTEL_TRACES_SAMPLER=always_on

# Never sample
OTEL_TRACES_SAMPLER=always_off

# Parent-based sampling
OTEL_TRACES_SAMPLER=parentbased_traceidratio
OTEL_TRACES_SAMPLER_ARG=0.2

Programmatic configuration

cs
using OpenTelemetry;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddOpenTelemetry()
    .ConfigureResource(resource => resource
        .AddService("my-api", "1.0.0"))
    .WithTracing(tracing => tracing
        .SetSampler(new TraceIdRatioBasedSampler(0.1)) // 10% sampling
        .AddSource("MyApp")
        .AddAspNetCoreInstrumentation(options =>
        {
            options.RecordException = true;
        })
        .AddHttpClientInstrumentation()
        .AddOtlpExporter()
    );

Custom sampler implementation

You can create custom sampling logic by implementing the Sampler class:

cs
using OpenTelemetry.Trace;
using System.Diagnostics;

public class CustomSampler : Sampler
{
    private readonly double _samplingRatio;
    private readonly Random _random = new Random();

    public CustomSampler(double samplingRatio)
    {
        _samplingRatio = samplingRatio;
    }

    public override SamplingResult ShouldSample(in SamplingParameters samplingParameters)
    {
        // Always sample error traces
        var tags = samplingParameters.Tags;
        if (tags != null)
        {
            foreach (var tag in tags)
            {
                if (tag.Key == "error" && tag.Value?.ToString() == "true")
                {
                    return SamplingResult.Create(SamplingDecision.RecordAndSample);
                }
            }
        }

        // Sample high-priority operations
        var operationName = samplingParameters.Name;
        if (operationName.Contains("critical") || operationName.Contains("payment"))
        {
            return SamplingResult.Create(SamplingDecision.RecordAndSample);
        }

        // Use probability sampling for other traces
        if (_random.NextDouble() < _samplingRatio)
        {
            return SamplingResult.Create(SamplingDecision.RecordAndSample);
        }

        return SamplingResult.Create(SamplingDecision.Drop);
    }
}

// Register custom sampler
builder.Services.AddOpenTelemetry()
    .WithTracing(tracing => tracing
        .SetSampler(new CustomSampler(0.1))
        .AddAspNetCoreInstrumentation()
        .AddOtlpExporter()
    );

Advanced .NET sampling scenarios

Sampling based on request attributes

cs
public class AttributeBasedSampler : Sampler
{
    public override SamplingResult ShouldSample(in SamplingParameters samplingParameters)
    {
        var tags = samplingParameters.Tags;
        if (tags != null)
        {
            foreach (var tag in tags)
            {
                // Always sample requests to admin endpoints
                if (tag.Key == "http.route" && tag.Value?.ToString()?.Contains("/admin") == true)
                {
                    return SamplingResult.Create(SamplingDecision.RecordAndSample);
                }

                // Sample 50% of API requests
                if (tag.Key == "http.route" && tag.Value?.ToString()?.StartsWith("/api") == true)
                {
                    var traceId = samplingParameters.TraceId;
                    var hash = traceId.GetHashCode();
                    return Math.Abs(hash) % 100 < 50
                        ? SamplingResult.Create(SamplingDecision.RecordAndSample)
                        : SamplingResult.Create(SamplingDecision.Drop);
                }
            }
        }

        // Default: don't sample
        return SamplingResult.Create(SamplingDecision.Drop);
    }
}

Time-based sampling

cs
public class TimeBasedSampler : Sampler
{
    private readonly Dictionary<DateTime, int> _samplesPerMinute = new();
    private readonly int _maxSamplesPerMinute;
    private readonly object _lock = new object();

    public TimeBasedSampler(int maxSamplesPerMinute)
    {
        _maxSamplesPerMinute = maxSamplesPerMinute;
    }

    public override SamplingResult ShouldSample(in SamplingParameters samplingParameters)
    {
        lock (_lock)
        {
            var currentMinute = new DateTime(DateTime.UtcNow.Year, DateTime.UtcNow.Month,
                DateTime.UtcNow.Day, DateTime.UtcNow.Hour, DateTime.UtcNow.Minute, 0);

            if (!_samplesPerMinute.ContainsKey(currentMinute))
            {
                _samplesPerMinute[currentMinute] = 0;

                // Clean up old entries
                var keysToRemove = _samplesPerMinute.Keys
                    .Where(k => k < currentMinute.AddMinutes(-5))
                    .ToList();
                foreach (var key in keysToRemove)
                {
                    _samplesPerMinute.Remove(key);
                }
            }

            if (_samplesPerMinute[currentMinute] < _maxSamplesPerMinute)
            {
                _samplesPerMinute[currentMinute]++;
                return SamplingResult.Create(SamplingDecision.RecordAndSample);
            }

            return SamplingResult.Create(SamplingDecision.Drop);
        }
    }
}

// Usage: limit to 100 samples per minute
builder.Services.AddOpenTelemetry()
    .WithTracing(tracing => tracing
        .SetSampler(new TimeBasedSampler(100))
        .AddAspNetCoreInstrumentation()
        .AddOtlpExporter()
    );

.NET production deployment

ASP.NET Core application

cs
using OpenTelemetry;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;

var builder = WebApplication.CreateBuilder(args);

// Configure services
builder.Services.AddControllers();

// Configure OpenTelemetry with sampling
builder.Services.AddOpenTelemetry()
    .ConfigureResource(resource => resource
        .AddService("my-api", "1.0.0")
        .AddAttributes(new Dictionary<string, object>
        {
            ["deployment.environment"] = builder.Environment.EnvironmentName,
            ["service.instance.id"] = Environment.MachineName
        }))
    .WithTracing(tracing => tracing
        .SetSampler(GetSampler(builder.Environment))
        .AddSource("MyApi")
        .AddAspNetCoreInstrumentation(options =>
        {
            options.RecordException = true;
            options.Filter = httpContext =>
            {
                // Don't trace health checks
                return !httpContext.Request.Path.StartsWithSegments("/health");
            };
        })
        .AddHttpClientInstrumentation()
        .AddEntityFrameworkCoreInstrumentation()
        .AddOtlpExporter()
    );

var app = builder.Build();

static Sampler GetSampler(IWebHostEnvironment environment)
{
    return environment.IsDevelopment()
        ? new AlwaysOnSampler()  // Sample everything in dev
        : new TraceIdRatioBasedSampler(0.1);  // 10% in production
}

app.MapControllers();
app.Run();

Controller with manual span creation

cs
[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
    private static readonly ActivitySource ActivitySource = new("MyApi");
    private readonly IOrderService _orderService;

    public OrdersController(IOrderService orderService)
    {
        _orderService = orderService;
    }

    [HttpPost]
    public async Task<IActionResult> CreateOrder([FromBody] CreateOrderRequest request)
    {
        using var activity = ActivitySource.StartActivity("CreateOrder");
        activity?.SetTag("order.customer_id", request.CustomerId);
        activity?.SetTag("order.items_count", request.Items.Count);

        try
        {
            var order = await _orderService.CreateOrderAsync(request);
            activity?.SetTag("order.id", order.Id);
            activity?.SetStatus(ActivityStatusCode.Ok);

            return Ok(order);
        }
        catch (Exception ex)
        {
            activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
            activity?.RecordException(ex);
            throw;
        }
    }
}

Monitoring sampling in .NET

Check sampling ratio

cs
public class SamplingMetricsMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<SamplingMetricsMiddleware> _logger;
    private static long _totalRequests = 0;
    private static long _sampledRequests = 0;

    public SamplingMetricsMiddleware(RequestDelegate next, ILogger<SamplingMetricsMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        Interlocked.Increment(ref _totalRequests);

        var activity = Activity.Current;
        if (activity != null && activity.IsAllDataRequested)
        {
            Interlocked.Increment(ref _sampledRequests);
        }

        // Log sampling ratio every 1000 requests
        if (_totalRequests % 1000 == 0)
        {
            var ratio = (double)_sampledRequests / _totalRequests;
            _logger.LogInformation("Sampling ratio: {Ratio:P} ({Sampled}/{Total})",
                ratio, _sampledRequests, _totalRequests);
        }

        await _next(context);
    }
}

What's next?