Expert guidance for configuring and deploying the OpenTelemetry Collector. Use when setting up a Collector pipeline, configuring receivers, exporters, or processors, deploying a Collector to Kubernetes or Docker, or forwarding telemetry to Dash0. Triggers on requests involving collector, pipeline, OTLP receiver, exporter, or Dash0 collector setup.
100
100%
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Advisory
Suggest reviewing before use
Instrument .NET applications to generate traces, logs, and metrics for deep insights into behavior and performance.
Download and run the auto-instrumentation install script:
curl -L -O https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/releases/latest/download/otel-dotnet-auto-install.sh
./otel-dotnet-auto-install.sh
. $HOME/.otel-dotnet-auto/instrument.shNote: This script is not supported on Apple Silicon. For Windows, use the PowerShell guide.
All environment variables that control the SDK behavior:
| Variable | Required | Default | Description |
|---|---|---|---|
OTEL_SERVICE_NAME | Yes | unknown_service | Identifies your service in telemetry data |
OTEL_TRACES_EXPORTER | Yes | none | Must set to otlp to export traces |
OTEL_METRICS_EXPORTER | No | none | Set to otlp to export metrics |
OTEL_LOGS_EXPORTER | No | none | Set to otlp to export logs |
OTEL_EXPORTER_OTLP_ENDPOINT | Yes | http://localhost:4318 | OTLP collector endpoint |
OTEL_EXPORTER_OTLP_HEADERS | No | - | Headers for authentication (e.g., Authorization=Bearer TOKEN) |
OTEL_EXPORTER_OTLP_PROTOCOL | No | http/protobuf | Protocol: grpc, http/protobuf, or http/json |
OTEL_RESOURCE_ATTRIBUTES | No | - | Additional resource attributes (e.g., deployment.environment=production) |
Critical: Without OTEL_TRACES_EXPORTER=otlp, the SDK defaults to none and no telemetry is exported.
https://<region>.your-platform.comorder-api, checkout-service)The SDK is activated by sourcing the instrument script after installation:
. $HOME/.otel-dotnet-auto/instrument.shThis sets the necessary .NET profiler environment variables that enable auto-instrumentation at runtime.
export OTEL_SERVICE_NAME="my-service"This step is required - without it, no telemetry is sent:
# Required for traces
export OTEL_TRACES_EXPORTER="otlp"
# Optional: also export metrics and logs
export OTEL_METRICS_EXPORTER="otlp"
export OTEL_LOGS_EXPORTER="otlp"export OTEL_EXPORTER_OTLP_ENDPOINT="https://<OTLP_ENDPOINT>"
export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer YOUR_AUTH_TOKEN"
export OTEL_EXPORTER_OTLP_PROTOCOL="http/protobuf"export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer YOUR_AUTH_TOKEN,Dash0-Dataset=my-dataset"# Activate auto-instrumentation
. $HOME/.otel-dotnet-auto/instrument.sh
# Service identification
export OTEL_SERVICE_NAME="my-service"
# Enable exporters (required!)
export OTEL_TRACES_EXPORTER="otlp"
export OTEL_METRICS_EXPORTER="otlp"
export OTEL_LOGS_EXPORTER="otlp"
# Configure endpoint
export OTEL_EXPORTER_OTLP_ENDPOINT="https://<OTLP_ENDPOINT>"
export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer YOUR_AUTH_TOKEN"
export OTEL_EXPORTER_OTLP_PROTOCOL="http/protobuf"
dotnet runAdd environment variables to your Properties/launchSettings.json:
{
"profiles": {
"MyApp": {
"commandName": "Project",
"environmentVariables": {
"OTEL_SERVICE_NAME": "my-service",
"OTEL_TRACES_EXPORTER": "otlp",
"OTEL_METRICS_EXPORTER": "otlp",
"OTEL_LOGS_EXPORTER": "otlp",
"OTEL_EXPORTER_OTLP_ENDPOINT": "https://<OTLP_ENDPOINT>",
"OTEL_EXPORTER_OTLP_HEADERS": "Authorization=Bearer YOUR_AUTH_TOKEN",
"OTEL_EXPORTER_OTLP_PROTOCOL": "http/protobuf"
}
}
}
}Note: You must still source the instrument script before running dotnet run.
For development without a collector, use the console exporter to see telemetry in your terminal:
. $HOME/.otel-dotnet-auto/instrument.sh
export OTEL_SERVICE_NAME="my-service"
export OTEL_TRACES_EXPORTER="console"
export OTEL_METRICS_EXPORTER="console"
export OTEL_LOGS_EXPORTER="console"
dotnet runThis prints spans, metrics, and logs directly to stdout—useful for verifying instrumentation works before configuring a remote backend.
If you set OTEL_TRACES_EXPORTER=otlp but have no collector running, you will see connection errors.
This is expected behavior.
Options:
console exporter during development (recommended for quick testing)Set service.name, service.version, and deployment.environment.name for every deployment.
See resource attributes for the full list of required and recommended attributes.
See Kubernetes deployment for pod metadata injection, resource attributes, and Dash0 Kubernetes Operator guidance.
The auto-instrumentation package automatically instruments:
| Category | Libraries |
|---|---|
| HTTP | ASP.NET Core, HttpClient |
| Database | SqlClient, Entity Framework Core |
| gRPC | Grpc.Net.Client |
| Messaging | MassTransit |
| Logging | ILogger (Microsoft.Extensions.Logging) |
| Runtime | .NET Runtime metrics, process metrics |
Refer to OpenTelemetry documentation for the complete list.
Add business context to auto-instrumented traces using System.Diagnostics.ActivitySource and Activity, the .NET native tracing API that OpenTelemetry bridges:
using System.Diagnostics;
public class OrderService
{
private static readonly ActivitySource Source = new("MyService");
public async Task<Order> ProcessOrder(Order order)
{
using var activity = Source.StartActivity("order.process");
try
{
activity?.SetTag("order.id", order.Id);
activity?.SetTag("order.total", order.Total);
var result = await SaveOrder(order);
return result;
}
catch (Exception ex)
{
activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
// ILogger message templates do not support dots in parameter names,
// so use BeginScope to set the exception.* and trace context attributes.
using (logger.BeginScope(new Dictionary<string, object>
{
["trace_id"] = activity?.TraceId.ToString() ?? "",
["span_id"] = activity?.SpanId.ToString() ?? "",
["exception.type"] = ex.GetType().FullName!,
["exception.message"] = ex.Message,
["exception.stacktrace"] = ex.ToString(),
}))
{
logger.LogError("order.process.failed");
}
throw;
}
}
}Auto-instrumentation creates spans you do not control directly (e.g., the SERVER span for an HTTP request).
To enrich these spans with business context or set their status, retrieve the active activity from the current context.
See adding attributes to auto-instrumented spans for when to use this pattern.
.NET uses System.Diagnostics.Activity instead of spans.
Activity.Current returns the active activity (span) on the current thread:
using System.Diagnostics;
[HttpPost("/api/orders")]
public IActionResult CreateOrder([FromBody] OrderRequest request)
{
Activity.Current?.SetTag("order.id", request.OrderId);
Activity.Current?.SetTag("tenant.id", request.TenantId);
// ... handler logic
}Activity.Current returns null if no activity is active.
Always use null-conditional (?.) when calling methods on the result.
See span status code for the full rules. This section shows how to apply them in .NET.
ERRORThe second argument to SetStatus is the status message.
It must contain the exception type and a short explanation — enough to understand the failure without opening the full trace.
// BAD: no status message
activity?.SetStatus(ActivityStatusCode.Error);
// BAD: generic message with no diagnostic value
activity?.SetStatus(ActivityStatusCode.Error, "something went wrong");
// GOOD: specific message with exception type and context
activity?.SetStatus(ActivityStatusCode.Error, $"{ex.GetType().Name}: {ex.Message}");Do not include stack traces in the status message.
Record those in a log record with exception.stacktrace instead.
// BAD: stack trace in the status message
activity?.SetStatus(ActivityStatusCode.Error, ex.ToString());
// GOOD: short message only
activity?.SetStatus(ActivityStatusCode.Error, ex.Message);OK only for confirmed successSet status to OK when application logic has explicitly verified the operation succeeded.
Leave status UNSET if the code simply did not encounter an error.
// GOOD: explicit confirmation from downstream
var response = await httpClient.GetAsync(url);
if (response.IsSuccessStatusCode)
{
activity?.SetStatus(ActivityStatusCode.Ok);
}
// BAD: setting OK speculatively
activity?.SetStatus(ActivityStatusCode.Ok);
return await SomeMethodAsync(); // might still fail after this pointConfigure your logging framework to serialize exceptions into a single structured field so that stack traces do not break the one-line-per-record contract. See logs for general guidance on structured logging and exception stack traces.
Serilog with Serilog.Formatting.Compact produces single-line JSON output with exceptions serialized into a structured field.
using Serilog;
using Serilog.Formatting.Compact;
Log.Logger = new LoggerConfiguration()
.WriteTo.Console(new CompactJsonFormatter())
.CreateLogger();
try
{
ProcessOrder(order);
}
catch (Exception ex)
{
Log.Error(ex, "order.failed {@OrderId}", order.Id);
}The CompactJsonFormatter serializes the exception (including its stack trace) into an "x" field as a single escaped string.
ASP.NET Core's built-in console logger supports JSON output starting from .NET 5.
builder.Logging.AddJsonConsole();try
{
ProcessOrder(order);
}
catch (Exception ex)
{
logger.LogError(ex, "order.failed, OrderId={OrderId}", order.Id);
}The JSON console formatter serializes exceptions into a structured field, keeping each log record on a single line.
The .NET auto-instrumentation (instrument.sh) registers a shutdown hook automatically.
When the process receives SIGTERM or exits normally, the hook flushes all pending spans, metrics, and log records before termination.
No additional code is needed for the auto-instrumented setup.
If you use the NuGet SDK packages (programmatic setup), the ASP.NET Core host shuts down registered providers when the application stops. For non-host applications (console apps, workers), dispose the providers explicitly:
var tracerProvider = Sdk.CreateTracerProviderBuilder()
.AddOtlpExporter()
.Build();
// On shutdown:
tracerProvider?.Dispose();Dispose() calls Shutdown() internally, which flushes pending batches and releases resources.
Check exporters are enabled:
echo $OTEL_TRACES_EXPORTER # Should be "otlp" or "console", not emptyThe SDK defaults OTEL_TRACES_EXPORTER to none, which silently discards all telemetry.
Verify the instrument script was sourced:
echo $CORECLR_ENABLE_PROFILING # Should be "1"This means the SDK is working but cannot reach the collector:
OTEL_TRACES_EXPORTER=consoleOTEL_EXPORTER_OTLP_ENDPOINT is correctThe install script does not support Apple Silicon (arm64 macOS). Use a Linux or Windows environment, or run inside a container for local development on Apple Silicon.
Usually means OTEL_TRACES_EXPORTER (or metrics/logs) is not set.
Set it explicitly:
export OTEL_TRACES_EXPORTER="otlp"