Guidance for OpenTelemetry Protocol (OTLP) logging and observability in .NET. USE FOR: OTLP log export, OpenTelemetry traces and metrics, distributed tracing with Activity API, configuring OTel collectors, correlating logs with traces, custom metrics and instruments. DO NOT USE FOR: Serilog-specific sinks and enrichers (use serilog), NLog-specific targets and routing (use nlog), Microsoft.Extensions.Logging abstractions (use extensions-logging).
Guidance for OpenTelemetry Protocol (OTLP) logging and observability in .NET. USE FOR: OTLP log export, OpenTelemetry traces and metrics, distributed tracing with Activity API, configuring OTel collectors, correlating logs with traces, custom metrics and instruments. DO NOT USE FOR: Serilog-specific sinks and enrichers (use serilog), NLog-specific targets and routing (use nlog), Microsoft.Extensions.Logging abstractions (use extensions-logging).
license
MIT
metadata
{"displayName":"OTLP Logging and Observability","author":"Tyler-R-Kendrick","version":"1.0.0"}
OpenTelemetry (OTel) is the vendor-neutral observability standard for collecting logs, traces, and metrics from distributed systems. In .NET, the and packages provide first-class integration with the generic host and ASP.NET Core. OTLP (OpenTelemetry Protocol) is the wire format used to export telemetry data to backends like Jaeger, Zipkin, Grafana Tempo, Azure Monitor, Datadog, and the OpenTelemetry Collector.
OpenTelemetry.Extensions.Hosting
OpenTelemetry.Exporter.OpenTelemetryProtocol
The three pillars of observability -- logs, traces, and metrics -- are configured independently but share a common Resource that identifies the service. By correlating log entries with trace and span IDs, developers can navigate from a log message to the exact distributed trace that produced it.
Configuring All Three Pillars
Set up logging, tracing, and metrics in a single place using the OpenTelemetry builder.
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using OpenTelemetry;
using OpenTelemetry.Exporter;
using OpenTelemetry.Logs;
using OpenTelemetry.Metrics;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
var builder = WebApplication.CreateBuilder(args);
var serviceName = "OrderApi";
var serviceVersion = "1.0.0";
var resourceBuilder = ResourceBuilder.CreateDefault()
.AddService(
serviceName: serviceName,
serviceVersion: serviceVersion)
.AddAttributes(new Dictionary<string, object>
{
["deployment.environment"] =
builder.Environment.EnvironmentName
});
// Tracing
builder.Services.AddOpenTelemetry()
.ConfigureResource(r => r.AddService(serviceName, serviceVersion))
.WithTracing(tracing => tracing
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddEntityFrameworkCoreInstrumentation()
.AddOtlpExporter(options =>
{
options.Endpoint = new Uri("http://collector:4317");
options.Protocol = OtlpExportProtocol.Grpc;
}))
.WithMetrics(metrics => metrics
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddRuntimeInstrumentation()
.AddMeter("MyApp.Orders")
.AddOtlpExporter(options =>
{
options.Endpoint = new Uri("http://collector:4317");
options.Protocol = OtlpExportProtocol.Grpc;
}));
// Logging
builder.Logging.AddOpenTelemetry(logging =>
{
logging.SetResourceBuilder(resourceBuilder);
logging.IncludeFormattedMessage = true;
logging.IncludeScopes = true;
logging.AddOtlpExporter(options =>
{
options.Endpoint = new Uri("http://collector:4317");
options.Protocol = OtlpExportProtocol.Grpc;
});
});
var app = builder.Build();
app.MapGet("/", () => "Hello");
app.Run();
Distributed Tracing with Activity API
.NET uses System.Diagnostics.Activity as the tracing primitive. Create custom spans for business operations.
When OpenTelemetry logging is configured with tracing, log entries automatically include TraceId and SpanId, enabling navigation from a log entry to its distributed trace.
using Microsoft.Extensions.Logging;
using System.Diagnostics;
namespaceMyApp.Services;
publicclassPaymentService
{
privatestaticreadonly ActivitySource ActivitySource =
new("MyApp.Payments");
privatereadonly ILogger<PaymentService> _logger;
publicPaymentService(ILogger<PaymentService> logger)
{
_logger = logger;
}
publicasync Task ChargeAsync(string orderId, decimal amount)
{
usingvar activity = ActivitySource.StartActivity(
"ChargePayment");
// This log entry automatically includes:// TraceId: from Activity.Current.TraceId// SpanId: from Activity.Current.SpanId
_logger.LogInformation(
"Charging {Amount:C} for order {OrderId}",
amount, orderId);
// Add structured event to the span
activity?.AddEvent(new ActivityEvent(
"PaymentCharged",
tags: new ActivityTagsCollection
{
{ "payment.amount", amount },
{ "payment.currency", "USD" }
}));
await Task.CompletedTask;
}
}
Environment-Based Configuration
Configure OTLP export via environment variables for container deployments.
using Microsoft.AspNetCore.Builder;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
var builder = WebApplication.CreateBuilder(args);
// Reads OTEL_* environment variables automatically
builder.Services.AddOpenTelemetry()
.ConfigureResource(r => r
.AddService(builder.Environment.ApplicationName)
.AddEnvironmentVariableDetector())
.WithTracing(tracing => tracing
.AddAspNetCoreInstrumentation()
.AddOtlpExporter()); // Uses OTEL_EXPORTER_OTLP_ENDPOINTvar app = builder.Build();
app.Run();
OpenTelemetry Signal Types
Signal
.NET API
OTel Package
Use Case
Traces
System.Diagnostics.Activity
OpenTelemetry.Exporter.OpenTelemetryProtocol
Request flow across services
Metrics
System.Diagnostics.Metrics
OpenTelemetry.Exporter.OpenTelemetryProtocol
Counters, histograms, gauges
Logs
Microsoft.Extensions.Logging
OpenTelemetry.Exporter.OpenTelemetryProtocol
Structured log events
Instrumentation Libraries
Library
Package
What It Captures
ASP.NET Core
OpenTelemetry.Instrumentation.AspNetCore
HTTP server spans, request metrics
HttpClient
OpenTelemetry.Instrumentation.Http
Outbound HTTP client spans
EF Core
OpenTelemetry.Instrumentation.EntityFrameworkCore
Database query spans
SQL Client
OpenTelemetry.Instrumentation.SqlClient
SQL Server query spans
Runtime
OpenTelemetry.Instrumentation.Runtime
GC, threadpool, assembly metrics
gRPC
OpenTelemetry.Instrumentation.GrpcNetClient
gRPC client call spans
Best Practices
Configure all three signals (traces, metrics, logs) together with a shared Resource so the observability backend can correlate data from the same service instance.
Use semantic conventions for span names and attributes (e.g., http.request.method, db.system, order.id) so observability tools can provide automatic dashboards and alerts.
Set an appropriate sampling rate in production using parentbased_traceidratio (e.g., 10% via OTEL_TRACES_SAMPLER_ARG=0.1) to reduce storage costs while maintaining statistical significance.
Add ActivitySource.StartActivity for business-critical operations (order processing, payment charging, inventory updates) to create custom spans that appear in the trace timeline alongside framework spans.
Use the OTEL_* environment variables for configuration in containerized deployments, as the OpenTelemetry SDK reads them automatically without code changes.
Include IncludeFormattedMessage = true and IncludeScopes = true in the logging exporter configuration so log messages in the backend are human-readable and include scope properties.
Register custom Meter names in AddMeter("MyApp.Orders") on the metrics builder; meters not registered are silently ignored, which is a common configuration mistake.
Export to an OpenTelemetry Collector rather than directly to backends, so you can fan out to multiple destinations, apply transformations, and change backends without redeploying the application.
Check activity is not null before calling SetTag or AddEvent because StartActivity returns null when no listener (sampler) is active for the source, which is expected behavior.
Record error details on spans using activity?.SetStatus(ActivityStatusCode.Error, exception.Message) and activity?.RecordException(exception) so error rates and stack traces are visible in the tracing backend.