Why OpenTelemetry for .NET
.NET has had built-in tracing primitives (System.Diagnostics.Activity) for years; OpenTelemetry .NET is the standard SDK that turns those activities into OTLP spans. With the OTel .NET SDK and the contrib instrumentations, ASP.NET Core requests, HttpClient calls, Entity Framework queries, gRPC calls, and SqlClient queries all become spans without any custom middleware. The same SDK exports metrics and logs in the same wire format — OTLP — so everything reaches Telemtra on a single endpoint.
Install NuGet packages
dotnet add package OpenTelemetry.Extensions.Hosting
dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol
dotnet add package OpenTelemetry.Instrumentation.AspNetCore
dotnet add package OpenTelemetry.Instrumentation.Http
dotnet add package OpenTelemetry.Instrumentation.EntityFrameworkCore --prerelease
dotnet add package OpenTelemetry.Instrumentation.Runtime Where this goes in your project
For a typical ASP.NET Core 8 project, the changes are: NuGet references in the .csproj, a service registration block in Program.cs, and environment variables. No new files needed.
my-service/
├── MyService.csproj # adds OpenTelemetry.* NuGet packages
├── Program.cs # add the AddOpenTelemetry() block from the next section
├── appsettings.json # unchanged (optional: read endpoint from config)
└── deploy/env # TELEMTRA_API_KEY and ASPNETCORE_ENVIRONMENT 1. Add packages via dotnet add package (the snippets in the install section, or via your IDE).
2. Add the registration block to Program.cs before builder.Build(). The exact block is in the next section — paste it after var builder = WebApplication.CreateBuilder(args);.
3. Environment variables, set via launchSettings.json for local development or your hosting provider for production:
TELEMTRA_API_KEY=tk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
ASPNETCORE_ENVIRONMENT=Production That is it. No new files, no source changes outside Program.cs. The OpenTelemetry hosted service starts with the host and shuts down cleanly during graceful shutdown, which flushes pending spans.
Register OpenTelemetry in Program.cs
The using directives matter: the metrics-side AddAspNetCoreInstrumentation() lives in OpenTelemetry.Metrics, and the logs-side AddOtlpExporter() lives in OpenTelemetry.Logs. Miss either namespace and the file will not compile.
For OtlpExportProtocol.HttpProtobuf with per-signal AddOtlpExporter(...), the SDK does not append /v1/traces, /v1/metrics, or /v1/logs automatically — you must include the signal path in each Endpoint.
using OpenTelemetry;
using OpenTelemetry.Exporter;
using OpenTelemetry.Logs;
using OpenTelemetry.Metrics;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
var builder = WebApplication.CreateBuilder(args);
var baseEndpoint = "https://ingest.telemtra.io";
var apiKey = Environment.GetEnvironmentVariable("TELEMTRA_API_KEY")!;
var headers = $"x-api-key={apiKey}";
builder.Services.AddOpenTelemetry()
.ConfigureResource(r => r
.AddService("checkout-api")
.AddAttributes(new Dictionary<string, object>
{
["deployment.environment"] = builder.Environment.EnvironmentName
}))
.WithTracing(t => t
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddEntityFrameworkCoreInstrumentation()
.AddSource("checkout-api")
.AddOtlpExporter(o =>
{
o.Endpoint = new Uri($"{baseEndpoint}/v1/traces");
o.Protocol = OtlpExportProtocol.HttpProtobuf;
o.Headers = headers;
}))
.WithMetrics(m => m
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddRuntimeInstrumentation()
.AddMeter("checkout-api")
.AddOtlpExporter(o =>
{
o.Endpoint = new Uri($"{baseEndpoint}/v1/metrics");
o.Protocol = OtlpExportProtocol.HttpProtobuf;
o.Headers = headers;
}));
builder.Logging.AddOpenTelemetry(o =>
{
o.SetResourceBuilder(ResourceBuilder.CreateDefault()
.AddService("checkout-api"));
o.AddOtlpExporter(otlp =>
{
otlp.Endpoint = new Uri($"{baseEndpoint}/v1/logs");
otlp.Protocol = OtlpExportProtocol.HttpProtobuf;
otlp.Headers = headers;
});
});
var app = builder.Build(); ASP.NET Core
AddAspNetCoreInstrumentation() wraps every incoming request in a span with route template, status code, and exception. Routes like /orders/{id} stay templated so URL cardinality does not explode.
HttpClient
AddHttpClientInstrumentation() covers every HttpClient registered through IHttpClientFactory. Outbound calls produce client spans and propagate W3C trace context so downstream services nest under the originating request.
Entity Framework Core
Each DbContext command becomes a span with the SQL text (optionally hashed for sensitive workloads). In Telemtra you can see exactly which slow query pushed a request past p95.
Manual spans via ActivitySource
private static readonly ActivitySource Activity = new("checkout-api");
public async Task SubmitOrder(string orderId, long totalCents)
{
using var activity = Activity.StartActivity("checkout.submit_order");
activity?.SetTag("order.id", orderId);
activity?.SetTag("order.total_cents", totalCents);
try
{
await ChargeCard(orderId, totalCents);
await WriteLedger(orderId);
await SendReceipt(orderId);
}
catch (Exception ex)
{
activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
throw;
}
} Metrics
private static readonly Meter Meter = new("checkout-api");
private static readonly Counter<long> OrdersTotal =
Meter.CreateCounter<long>("orders_total");
private static readonly Histogram<double> CheckoutLatencyMs =
Meter.CreateHistogram<double>("checkout_latency_ms");
OrdersTotal.Add(1, new("plan", "team"));
CheckoutLatencyMs.Record(348); Logs
The OpenTelemetry logging provider attaches to ILogger so every logger.LogInformation call becomes an OTLP log record. Inside an active span, trace and span IDs are attached automatically.
logger.LogInformation("order accepted {OrderId} {TotalCents}", orderId, totalCents); Troubleshooting
- CS1929:
MeterProviderBuilderdoes not containAddAspNetCoreInstrumentation. Addusing OpenTelemetry.Metrics;— the metrics overload lives in that namespace. - CS1929:
OpenTelemetryLoggerOptionsdoes not containAddOtlpExporter. Addusing OpenTelemetry.Logs;— the logs overload lives in that namespace. - 404 or empty responses from the endpoint. With
HttpProtobufand per-signalAddOtlpExporter(...), the SDK does not append/v1/traces,/v1/metrics,/v1/logs. Set each exporter'sEndpointto the full signal URL. - 403 from Telemtra. The header is
x-api-key, lowercase. Seto.Headers = "x-api-key=YOUR_KEY". - EF Core spans missing. The EF Core instrumentation is pre-release; include
--prereleaseondotnet add package. - Logs not arriving. Confirm
builder.Logging.AddOpenTelemetryis called and the OTLP exporter uses the/v1/logsendpoint with the same headers as traces. - Runtime metrics missing. Add
OpenTelemetry.Instrumentation.Runtimeand register it with.AddRuntimeInstrumentation().
For protocol details see What is OTLP?. For a Collector sidecar, follow the Collector quickstart. New to Telemtra? The quickstart shows the minimum Program.cs registration. Create a Telemtra workspace to get an API key.
OpenTelemetry .NET FAQ
Does OpenTelemetry .NET work with ASP.NET Core 6, 7, 8, and 9?
Yes. OpenTelemetry.Extensions.Hosting integrates with the generic host on every supported .NET version. ASP.NET Core requests, HttpClient calls, Entity Framework queries, and gRPC calls become spans through the matching OpenTelemetry.Instrumentation.* NuGet packages.
Do I need a separate Telemtra SDK or NuGet package?
No. Telemtra accepts standard OpenTelemetry over OTLP. Use OpenTelemetry.Exporter.OpenTelemetryProtocol with OtlpExportProtocol.HttpProtobuf, point the endpoint at https://ingest.telemtra.io, and set the x-api-key header.
Does System.Diagnostics.Activity work with OpenTelemetry .NET?
Yes. .NET has had Activity-based tracing for years; OpenTelemetry .NET maps each ActivitySource to spans. Use ActivitySource.StartActivity for manual spans and they will export over OTLP alongside the auto-instrumented ones.
Are ILogger logs sent to OpenTelemetry?
Yes if you call builder.Logging.AddOpenTelemetry(o => o.AddOtlpExporter(...)). Every logger.LogInformation / LogWarning / LogError becomes an OTLP log record. Inside an active span, trace and span IDs are attached automatically so logs link back to their trace in Telemtra.
What about Entity Framework Core spans?
Add OpenTelemetry.Instrumentation.EntityFrameworkCore (currently --prerelease) and register it with .AddEntityFrameworkCoreInstrumentation(). Each EF Core query becomes a span with the SQL command text (truncated by default) and database connection details.
Further reading
- Telemtra quickstart — minimum .NET config and snippets for every other supported stack.
- opentelemetry.io — .NET documentation
- opentelemetry.io — .NET instrumentation guide
- github.com/open-telemetry/opentelemetry-dotnet — SDK source
- github.com/open-telemetry/opentelemetry-dotnet-contrib — contrib instrumentations
- OpenTelemetry semantic conventions