Modern cloud applications are complex distributed systems where a single user request might touch dozens of services. When something breaks, you need comprehensive observability to understand what happened. Traditional monitoring tools often lock you into a specific vendor or don't provide the full picture across your entire stack.
OpenTelemetry is the industry-standard, vendor-neutral framework for collecting telemetry data (traces, metrics, and logs) from your applications. It's backed by the Cloud Native Computing Foundation and has become the dominant observability framework in 2026.
Azure Application Insights has moved to OpenTelemetry-based implementations. Since 2023, the Azure Monitor OpenTelemetry Distro is now the recommended approach for new .NET applications.
Key point: You can use OpenTelemetry APIs and still send data to Application Insights - you get the best of both worlds.
Recommended approach (2026): Use OpenTelemetry instrumentation with Azure Monitor OpenTelemetry Distro. This gives you standard APIs with full Application Insights integration.
For ASP.NET Core applications sending to Azure Monitor:
dotnet add package Azure.Monitor.OpenTelemetry.AspNetCore
For other scenarios:
dotnet add package OpenTelemetry.Extensions.Hostingdotnet add package OpenTelemetry.Instrumentation.AspNetCoredotnet add package OpenTelemetry.Instrumentation.Http
var builder = WebApplication.CreateBuilder(args);// Add OpenTelemetry with Azure Monitorbuilder.Services.AddOpenTelemetry().UseAzureMonitor(options =>{options.ConnectionString = builder.Configuration["ApplicationInsights:ConnectionString"];});var app = builder.Build();
using System.Diagnostics;public class OrderService{private static readonly ActivitySource ActivitySource = new("MyApp.OrderService");public async Task<Order> ProcessOrder(string orderId){using var activity = ActivitySource.StartActivity("ProcessOrder");activity?.SetTag("order.id", orderId);// Your business logic hereawait Task.Delay(100);activity?.SetTag("order.status", "completed");return new Order { Id = orderId };}}// Register the activity source in Program.csbuilder.Services.AddOpenTelemetry().WithTracing(tracing => tracing.AddSource("MyApp.OrderService"));
High-cardinality data (e.g., user IDs in metric tags) can explode costs and impact performance.
❌ Bad example - High cardinality
// Don't use unique IDs as metric dimensionsmeter.CreateCounter<int>("orders.processed").Add(1, new KeyValuePair<string, object?>("user.id", userId)); // ❌ High cardinality!
✅ Good example - Low cardinality
// Use categories insteadmeter.CreateCounter<int>("orders.processed").Add(1, new KeyValuePair<string, object?>("user.tier", "premium")); // ✅ Low cardinality
Follow OpenTelemetry semantic conventions for consistent naming.
❌ Bad example - Custom naming
activity?.SetTag("userId", userId); // ❌ Non-standardactivity?.SetTag("dbQuery", query); // ❌ Non-standard
✅ Good example - Semantic conventions
activity?.SetTag("enduser.id", userId); // ✅ Standard conventionactivity?.SetTag("db.statement", query); // ✅ Standard convention
In .NET applications with dependency injection, let DI manage the lifecycle to ensure proper flushing.
❌ Bad example - Manual disposal
// Don't manually create and disposevar tracerProvider = Sdk.CreateTracerProviderBuilder().AddSource("MyApp").Build();// May not flush properly! ❌
✅ Good example - DI-managed
// Let DI handle lifecyclebuilder.Services.AddOpenTelemetry().WithTracing(tracing => tracing.AddSource("MyApp")); // ✅ Properly managed
You can send different telemetry to different backends:
builder.Services.AddOpenTelemetry().WithTracing(tracing => tracing.AddSource("MyApp").AddAzureMonitorTraceExporter() // Critical traces to App Insights.AddOtlpExporter()) // High-volume to self-hosted backend.WithMetrics(metrics => metrics.AddMeter("MyApp").AddAzureMonitorMetricExporter());