Skip to content

.NET

.NET OpenTelemetry SDK

This guide covers how to instrument .NET applications with OpenTelemetry to send traces to Sematext Tracing.

Auto-instrumentation automatically instruments popular frameworks and libraries without requiring code changes.

1. Install the .NET Automatic Instrumentation

For Windows:

# Download the latest release
$url = "https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/releases/latest/download/opentelemetry-dotnet-instrumentation-windows.zip"
Invoke-WebRequest -Uri $url -OutFile "otel-dotnet-auto.zip"
Expand-Archive -Path "otel-dotnet-auto.zip" -DestinationPath "C:\otel-dotnet-auto"

For Linux:

# Download the latest release
curl -L -o opentelemetry-dotnet-instrumentation-linux.tar.gz \
  https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/releases/latest/download/opentelemetry-dotnet-instrumentation-linux-glibc.tar.gz

# Extract
mkdir -p /opt/opentelemetry-dotnet-instrumentation
tar -xzf opentelemetry-dotnet-instrumentation-linux.tar.gz -C /opt/opentelemetry-dotnet-instrumentation

2. Set Environment Variables and Run

Windows:

$env:OTEL_SERVICE_NAME="your-service-name"
$env:OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4338"
$env:OTEL_EXPORTER_OTLP_PROTOCOL="http/protobuf"
$env:OTEL_DOTNET_AUTO_HOME="C:\otel-dotnet-auto"
$env:CORECLR_ENABLE_PROFILING="1"
$env:CORECLR_PROFILER="{918728DD-259F-4A6A-AC2B-B85E1B658318}"
$env:CORECLR_PROFILER_PATH="$env:OTEL_DOTNET_AUTO_HOME\win-x64\OpenTelemetry.AutoInstrumentation.Native.dll"

dotnet run

Linux:

export OTEL_SERVICE_NAME=your-service-name
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4338
export OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
export OTEL_DOTNET_AUTO_HOME=/opt/opentelemetry-dotnet-instrumentation
export CORECLR_ENABLE_PROFILING=1
export CORECLR_PROFILER={918728DD-259F-4A6A-AC2B-B85E1B658318}
export CORECLR_PROFILER_PATH=$OTEL_DOTNET_AUTO_HOME/linux-x64/OpenTelemetry.AutoInstrumentation.Native.so

dotnet run

Manual Instrumentation (Advanced)

For more control over instrumentation, you can manually configure OpenTelemetry.

1. Install NuGet Packages

<PackageReference Include="OpenTelemetry" Version="1.6.0" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.6.0" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.6.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.5.1-beta.1" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.5.1-beta.1" />

2. Configure in Program.cs (ASP.NET Core)

using OpenTelemetry.Trace;
using OpenTelemetry.Resources;
using OpenTelemetry.Exporter;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container
builder.Services.AddControllers();

// Configure OpenTelemetry
builder.Services.AddOpenTelemetry()
    .ConfigureResource(resource => resource
        .AddService("your-service-name", "1.0.0")
        .AddAttributes(new Dictionary<string, object>
        {
            ["deployment.environment"] = "production",
            ["service.team"] = "backend"
        }))
    .WithTracing(tracing => tracing
        .AddAspNetCoreInstrumentation(options =>
        {
            options.RecordException = true;
            options.Filter = httpContext =>
            {
                // Filter out health check endpoints
                return !httpContext.Request.Path.StartsWithSegments("/health");
            };
        })
        .AddHttpClientInstrumentation(options =>
        {
            options.RecordException = true;
        })
        .AddSource("YourApplication")
        .AddOtlpExporter(options =>
        {
            options.Endpoint = new Uri("http://localhost:4338");
            options.Protocol = OtlpExportProtocol.HttpProtobuf;
        }));

var app = builder.Build();

// Configure the HTTP request pipeline
app.UseRouting();
app.MapControllers();

app.Run();

3. Manual Span Creation

using System.Diagnostics;
using OpenTelemetry;
using OpenTelemetry.Trace;

public class UserService
{
    private static readonly ActivitySource ActivitySource = new("YourApplication");
    
    public async Task<User> GetUserAsync(string userId)
    {
        using var activity = ActivitySource.StartActivity("get-user");
        activity?.SetTag("user.id", userId);
        activity?.SetTag("operation.type", "database-query");
        
        try
        {
            // Your business logic here
            var user = await FetchUserFromDatabaseAsync(userId);
            
            activity?.SetTag("user.found", user != null);
            activity?.SetStatus(ActivityStatusCode.Ok);
            
            return user;
        }
        catch (Exception ex)
        {
            activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
            activity?.RecordException(ex);
            throw;
        }
    }
    
    private async Task<User> FetchUserFromDatabaseAsync(string userId)
    {
        using var activity = ActivitySource.StartActivity("fetch-user-db");
        activity?.SetTag("user.id", userId);
        activity?.SetTag("db.operation", "SELECT");
        activity?.SetTag("db.table", "users");
        
        try
        {
            // Simulate database call
            await Task.Delay(100);
            
            return new User { Id = userId, Name = "John Doe" };
        }
        catch (Exception ex)
        {
            activity?.RecordException(ex);
            throw;
        }
    }
}

public class User
{
    public string Id { get; set; }
    public string Name { get; set; }
}

ASP.NET Core Integration

Controller Example

using Microsoft.AspNetCore.Mvc;
using System.Diagnostics;

[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
    private static readonly ActivitySource ActivitySource = new("YourApplication");
    private readonly UserService _userService;
    
    public UsersController(UserService userService)
    {
        _userService = userService;
    }
    
    [HttpGet("{id}")]
    public async Task<ActionResult<User>> GetUser(string id)
    {
        using var activity = ActivitySource.StartActivity("get-user-controller");
        activity?.SetTag("user.id", id);
        activity?.SetTag("http.method", Request.Method);
        activity?.SetTag("http.route", "/api/users/{id}");
        
        try
        {
            var user = await _userService.GetUserAsync(id);
            
            if (user == null)
            {
                activity?.SetTag("user.found", false);
                return NotFound();
            }
            
            activity?.SetTag("user.found", true);
            return Ok(user);
        }
        catch (Exception ex)
        {
            activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
            activity?.RecordException(ex);
            return StatusCode(500, "Internal server error");
        }
    }
}

Middleware for Custom Instrumentation

public class TracingMiddleware
{
    private readonly RequestDelegate _next;
    private static readonly ActivitySource ActivitySource = new("YourApplication.Middleware");
    
    public TracingMiddleware(RequestDelegate next)
    {
        _next = next;
    }
    
    public async Task InvokeAsync(HttpContext context)
    {
        using var activity = ActivitySource.StartActivity("custom-middleware");
        activity?.SetTag("http.path", context.Request.Path);
        activity?.SetTag("http.method", context.Request.Method);
        
        try
        {
            await _next(context);
            
            activity?.SetTag("http.status_code", context.Response.StatusCode);
            
            if (context.Response.StatusCode >= 400)
            {
                activity?.SetStatus(ActivityStatusCode.Error, $"HTTP {context.Response.StatusCode}");
            }
        }
        catch (Exception ex)
        {
            activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
            activity?.RecordException(ex);
            throw;
        }
    }
}

// Register in Program.cs
app.UseMiddleware<TracingMiddleware>();

Database Instrumentation

Entity Framework Core

<PackageReference Include="OpenTelemetry.Instrumentation.EntityFrameworkCore" Version="1.0.0-beta.7" />
builder.Services.AddOpenTelemetry()
    .WithTracing(tracing => tracing
        .AddEntityFrameworkCoreInstrumentation(options =>
        {
            options.SetDbStatementForText = true;
            options.SetDbStatementForStoredProcedure = true;
        })
        // ... other configuration
    );

SQL Client

<PackageReference Include="OpenTelemetry.Instrumentation.SqlClient" Version="1.5.1-beta.1" />
builder.Services.AddOpenTelemetry()
    .WithTracing(tracing => tracing
        .AddSqlClientInstrumentation(options =>
        {
            options.SetDbStatementForText = true;
            options.RecordException = true;
        })
        // ... other configuration
    );

Redis

<PackageReference Include="OpenTelemetry.Instrumentation.StackExchangeRedis" Version="1.0.0-rc9.10" />
using StackExchange.Redis;

// In Program.cs
var redis = ConnectionMultiplexer.Connect("localhost:6379");
builder.Services.AddSingleton<IConnectionMultiplexer>(redis);

builder.Services.AddOpenTelemetry()
    .WithTracing(tracing => tracing
        .AddRedisInstrumentation(redis)
        // ... other configuration
    );

Console Application

Program.cs for Console App

using OpenTelemetry;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
using System.Diagnostics;

// Configure OpenTelemetry
using var tracerProvider = Sdk.CreateTracerProviderBuilder()
    .AddSource("ConsoleApp")
    .ConfigureResource(resource => resource
        .AddService("console-app", "1.0.0"))
    .AddOtlpExporter(options =>
    {
        options.Endpoint = new Uri("http://localhost:4338");
        options.Protocol = OtlpExportProtocol.HttpProtobuf;
    })
    .Build();

var activitySource = new ActivitySource("ConsoleApp");

// Your application logic
using var activity = activitySource.StartActivity("main-operation");
activity?.SetTag("operation.type", "batch-processing");

try
{
    await DoWorkAsync();
    activity?.SetStatus(ActivityStatusCode.Ok);
}
catch (Exception ex)
{
    activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
    activity?.RecordException(ex);
    throw;
}

async Task DoWorkAsync()
{
    using var workActivity = activitySource.StartActivity("do-work");
    workActivity?.SetTag("work.type", "data-processing");
    
    // Simulate work
    await Task.Delay(1000);
    
    workActivity?.SetTag("work.completed", true);
}

Worker Service

Background Service with Tracing

using OpenTelemetry;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
using System.Diagnostics;

public class TracedWorkerService : BackgroundService
{
    private static readonly ActivitySource ActivitySource = new("WorkerService");
    private readonly ILogger<TracedWorkerService> _logger;
    
    public TracedWorkerService(ILogger<TracedWorkerService> logger)
    {
        _logger = logger;
    }
    
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            using var activity = ActivitySource.StartActivity("worker-iteration");
            activity?.SetTag("iteration.timestamp", DateTimeOffset.UtcNow.ToString());
            
            try
            {
                await ProcessBatchAsync();
                
                activity?.SetStatus(ActivityStatusCode.Ok);
                await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
            }
            catch (Exception ex)
            {
                activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
                activity?.RecordException(ex);
                _logger.LogError(ex, "Error in worker service");
            }
        }
    }
    
    private async Task ProcessBatchAsync()
    {
        using var activity = ActivitySource.StartActivity("process-batch");
        
        // Your batch processing logic
        await Task.Delay(100);
        
        activity?.SetTag("batch.processed", true);
    }
}

// In Program.cs
var builder = Host.CreateApplicationBuilder(args);

builder.Services.AddOpenTelemetry()
    .ConfigureResource(resource => resource
        .AddService("worker-service"))
    .WithTracing(tracing => tracing
        .AddSource("WorkerService")
        .AddOtlpExporter(options =>
        {
            options.Endpoint = new Uri("http://localhost:4338");
            options.Protocol = OtlpExportProtocol.HttpProtobuf;
        }));

builder.Services.AddHostedService<TracedWorkerService>();

var host = builder.Build();
host.Run();

Configuration Options

appsettings.json Configuration

{
  "OpenTelemetry": {
    "ServiceName": "my-dotnet-service",
    "ServiceVersion": "1.0.0",
    "Otlp": {
      "Endpoint": "http://localhost:4338",
      "Protocol": "HttpProtobuf"
    },
    "Sampling": {
      "Type": "TraceIdRatioBased",
      "Ratio": 0.1
    }
  }
}

Using Configuration

builder.Services.AddOpenTelemetry()
    .ConfigureResource(resource => resource
        .AddService(
            serviceName: builder.Configuration["OpenTelemetry:ServiceName"] ?? "unknown-service",
            serviceVersion: builder.Configuration["OpenTelemetry:ServiceVersion"]))
    .WithTracing(tracing => tracing
        .AddAspNetCoreInstrumentation()
        .AddHttpClientInstrumentation()
        .AddOtlpExporter(options =>
        {
            options.Endpoint = new Uri(builder.Configuration["OpenTelemetry:Otlp:Endpoint"] ?? "http://localhost:4338");
            options.Protocol = Enum.Parse<OtlpExportProtocol>(
                builder.Configuration["OpenTelemetry:Otlp:Protocol"] ?? "HttpProtobuf");
        }));

Troubleshooting

Debug Configuration

using OpenTelemetry.Trace;

builder.Services.AddOpenTelemetry()
    .WithTracing(tracing => tracing
        .AddConsoleExporter() // Add console output for debugging
        .AddOtlpExporter()
        // ... other configuration
    );

Common Issues

No Traces Appearing - Verify the Sematext Agent is running - Check OTLP endpoint configuration - Ensure ActivitySource names match between creation and registration

Performance Issues

builder.Services.AddOpenTelemetry()
    .WithTracing(tracing => tracing
        .SetSampler(new TraceIdRatioBasedSampler(0.1)) // 10% sampling
        // ... other configuration
    );

Verification

using System.Diagnostics;

public class TracingTest
{
    private static readonly ActivitySource ActivitySource = new("TestApp");
    
    public static void TestTracing()
    {
        using var activity = ActivitySource.StartActivity("test-span");
        activity?.SetTag("test.attribute", "test-value");
        activity?.SetTag("test.number", 42);
        
        Console.WriteLine("Test span created");
    }
}

Best Practices

Activity Source Management

// Create a single ActivitySource per assembly/module
public static class Telemetry
{
    public static readonly ActivitySource ActivitySource = new("YourApplication");
}

// Use it throughout your application
using var activity = Telemetry.ActivitySource.StartActivity("operation-name");

Exception Handling

try
{
    // Business logic
}
catch (Exception ex)
{
    activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
    activity?.RecordException(ex);
    throw; // Re-throw to maintain exception flow
}

Resource Attributes

.ConfigureResource(resource => resource
    .AddService("my-service", "1.0.0")
    .AddAttributes(new Dictionary<string, object>
    {
        ["deployment.environment"] = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "unknown",
        ["service.team"] = "backend",
        ["service.component"] = "api"
    }))

Next Steps