Drop-in OpenTelemetry setup for .NET — tracing, metrics and logging over OTLP, configured through code or configuration.
builder.Services.AddTelemetry(builder.Configuration);{
"Telemetry": {
"Endpoint": "http://localhost:4318",
"ServiceName": "my-api"
}
}That's the whole setup — traces, metrics and logs are exported via OTLP.
- Features
- Requirements
- Installation
- Quick Start
- Configuration
- Code Configuration
- Using outside the Generic Host
- Samples
- Running Locally with a Backend
- Sample Backend Configurations
- Contributing
- Report a Bug
- One-call setup — tracing, metrics and logging via a single
AddTelemetry(), fromappsettings.jsonor code - Works on any .NET — ASP.NET Core, WPF, console and more, exporting over OTLP to any compatible backend
- Auto-instrumentation —
HttpClient, ASP.NET Core and .NET runtime traced and measured automatically, each toggleable - Extensible — add your own sources, meters and databases when you need them
Every app type works — ASP.NET Core, WPF, WinForms, console, worker services, MAUI/WinUI, UWP. The package ships three targets and NuGet picks the best match for your target framework; the app type plays no role:
| Your target framework | Build you get | Bundles ASP.NET Core instrumentation |
|---|---|---|
net10.0 — including net10.0-windows (WPF/WinForms) |
net10.0 |
✅ |
net8.0, net9.0 — including their -windows variants |
net8.0 |
✅ |
| .NET Framework 4.6.1+, .NET Standard 2.0 consumers | netstandard2.0 |
❌ |
The last column is only about the optional
ASP.NET Core instrumentation
dependency, which traces incoming HTTP requests and is therefore meaningless
outside a web app. It is left out of the netstandard2.0 build so .NET Framework
clients don't drag in an ASP.NET Core dependency they can't use. Everything else
— OTLP export, tracing, metrics, logging, HttpClient and runtime
instrumentation — is identical in all three builds.
A WPF app on
net10.0-windowstherefore gets the fullnet10.0build. Only WPF on .NET Framework falls back tonetstandard2.0.
You also need an OTLP-compatible backend to receive the telemetry (collector, Jaeger, OpenObserve, SigNoz, the .NET Aspire Dashboard, …). See Running Locally with a Backend.
dotnet add package OpenTelemetryExtension.ConfigurationThe NuGet package is the recommended path, but it isn't the only one:
- As source / project reference or a plain copy — the library is just two
files. Either clone
OpenTelemetryExtension.Configurationinto your repository and add a<ProjectReference>to it, or drop the two files straight into your project (no package, no reference) and own them outright:TelemetryServiceCollectionExtensions.csandTelemetryOptions.cs. Handy when you want to tweak the defaults or step through the setup code. - As a git submodule — pin the source at a specific commit and reference the
project from your solution:
then add a
git submodule add https://github.com/thorstenalpers/OpenTelemetryExtension.Configuration.git external/OpenTelemetryExtension.Configuration
<ProjectReference>toexternal/OpenTelemetryExtension.Configuration/src/OpenTelemetryExtension.Configuration/OpenTelemetryExtension.Configuration.csproj. - As a fork — fork the repository and adapt it to your needs. Optionally, if your changes are generally useful, open a pull request back upstream. See Contributing.
In every case the API is identical — AddTelemetry(...) works the same whether
the type comes from a NuGet package or your own copy of the source.
builder.Services.AddTelemetry(builder.Configuration);{
"Telemetry": {
"Endpoint": "http://localhost:4318",
"ServiceName": "my-api"
}
}That's it — tracing, metrics and logging are exported via OTLP.
You need an OTLP-compatible backend listening at
Endpoint. No backend yet? See Running Locally with a Backend for one-command setups.
All keys live under the Telemetry section. Only Endpoint is required; everything
else has a default and telemetry is on out of the box. Bind from appsettings.json or set
the same properties in the AddTelemetry(o => …) callback.
| Key | Description | Type | Default | Example |
|---|---|---|---|---|
Endpoint |
OTLP endpoint for traces, metrics and logs. | Uri |
— (required) | https://otel.example.com:4317 |
Protocol |
Transport protocol: HttpProtobuf (4318) or Grpc (4317). |
string |
HttpProtobuf |
Grpc |
Headers |
OTLP exporter headers, e.g. for auth. Format: k1=v1,k2=v2. |
string |
"" |
x-otlp-api-key=secret |
Enabled |
Master switch; false registers no OpenTelemetry at all. |
bool |
true |
false |
ServiceName |
Logical service name shown in the backend. | string? |
null |
orders-api |
ResourceAttributes |
Extra resource attributes. | object |
{} |
{ "deployment.environment": "production" } |
EnableTracing |
Distributed tracing. | bool |
true |
false |
EnableMetrics |
Metrics collection. | bool |
true |
false |
EnableLogging |
Log export via OTLP. | bool |
true |
false |
SampleRatio |
Trace sample fraction — 1.0 = all, 0.1 = 10% (ParentBased(TraceIdRatioBased)). |
double |
1.0 |
0.1 |
EnableAspNetCoreInstrumentation |
Incoming ASP.NET Core requests (net8.0/net10.0 builds; no-op on netstandard2.0). | bool |
true |
false |
EnableHttpClientInstrumentation |
Outgoing HttpClient requests. |
bool |
true |
false |
EnableRuntimeInstrumentation |
.NET runtime metrics (GC, memory, thread pool). | bool |
true |
false |
AdditionalTracingSources |
Extra ActivitySource names to collect. |
string[] |
[] |
[ "Npgsql", "MyApp" ] |
AdditionalMeters |
Extra Meter names to collect. |
string[] |
[] |
[ "MyApp.Orders" ] |
RecordExceptions |
Record exceptions with stack traces on spans. | bool |
true |
false |
ExcludedPaths |
Paths excluded from tracing (per-segment prefix match). | string[] |
[ "/health" ] |
[ "/health", "/ready" ] |
IncludeScopes |
Include log scopes in exported records. | bool |
true |
false |
IncludeFormattedMessage |
Include the formatted message in exported records. | bool |
true |
false |
ConfigureTracing |
Code only. Register extra tracing — see Code configuration. | Action<TracerProviderBuilder> |
null |
t => t.AddSource("MyApp") |
ConfigureMetrics |
Code only. Register extra metrics. | Action<MeterProviderBuilder> |
null |
m => m.AddMeter("MyApp") |
ConfigureLogging |
Code only. Tweak the logging pipeline. | Action<ILoggingBuilder> |
null |
l => l.AddConsole() |
💡 See
docs/appsettings.Example.jsonfor a complete profile with every key set to a realistic, non-default value.
Endpoint & protocol — give Endpoint the base URL, not a signal path:
HttpProtobuf(default, port4318) — the per-signal path (/v1/traces,/v1/metrics,/v1/logs) is appended for you.Grpc(port4317) — every signal goes to the base endpoint as-is.
Endpoint is validated at registration and AddTelemetry() throws when it is
missing. The one exception is Enabled: false: validation is skipped, nothing is
registered, and the call becomes a no-op — so you can switch telemetry off in an
environment without also having to supply an endpoint.
The section defaults to Telemetry, but you can bind any section by passing its name:
builder.Services.AddTelemetry(builder.Configuration, "MyTelemetry");
// or together with a code callback:
builder.Services.AddTelemetry(builder.Configuration, o => { /* ... */ }, "MyTelemetry");{
"MyTelemetry": {
"Endpoint": "http://localhost:4318",
"ServiceName": "my-api"
}
}Configure entirely in code instead of appsettings.json:
builder.Services.AddTelemetry(o =>
{
o.Endpoint = new Uri("http://localhost:4318");
o.ServiceName = "my-api";
o.ResourceAttributes = new() { ["deployment.environment"] = "production" };
o.SampleRatio = 0.1;
// Code-only: register additional instrumentation
o.ConfigureTracing = tracing => tracing.AddSource("MyApp");
o.ConfigureMetrics = metrics => metrics.AddMeter("MyApp");
o.ConfigureLogging = logging => logging.AddConsole();
});Or bind appsettings.json first and layer code-only options on top — both
sources are combined, and bound values can still be overridden in the callback:
builder.Services.AddTelemetry(builder.Configuration, o =>
{
// Everything from appsettings.json is already bound here.
o.ConfigureTracing = tracing => tracing.AddSource("MyApp");
o.ConfigureMetrics = metrics => metrics.AddMeter("MyApp");
o.ConfigureLogging = logging => logging.AddConsole();
});The three callbacks are the extension points for your own telemetry. The
built-in instrumentation (ASP.NET Core, HttpClient, .NET runtime) is wired up
automatically; these hooks let you add the signals your application emits itself,
plus anything that needs an extra NuGet package — such as databases.
| Hook | Builder | Used to register |
|---|---|---|
ConfigureTracing |
TracerProviderBuilder |
Activity Sources via AddSource("Name") |
ConfigureMetrics |
MeterProviderBuilder |
Meters via AddMeter("Name") |
ConfigureLogging |
ILoggingBuilder |
extra logging providers, filters, etc. |
What is a Meter?
A Meter
(from System.Diagnostics.Metrics) is the factory you create instruments
(counters, histograms, gauges) from. Each Meter has a name, and OpenTelemetry
only collects metrics from meters you have explicitly registered with
AddMeter("That.Name"). Without that call, your custom metrics are never exported.
// 1. Create a Meter and an instrument somewhere in your app
private static readonly Meter Meter = new("MyApp.Orders");
private static readonly Counter<long> OrdersPlaced = Meter.CreateCounter<long>("orders.placed");
// ... later
OrdersPlaced.Add(1);
// 2. Register the meter's name so it gets exported
o.ConfigureMetrics = metrics => metrics.AddMeter("MyApp.Orders");What is a Source?
An ActivitySource
is the tracing equivalent: it creates Activity objects (= spans). Register its
name with AddSource("MyApp") so your custom spans are sampled and exported.
private static readonly ActivitySource Activity = new("MyApp");
using var span = Activity.StartActivity("ProcessOrder");
// ... work being traced
o.ConfigureTracing = tracing => tracing.AddSource("MyApp");The string passed to
AddMeter/AddSourcemust exactly match the name you gave theMeter/ActivitySource— that name is how OpenTelemetry routes the data.
Database instrumentation is not built in — it depends entirely on your
driver, so it is added through the ConfigureTracing hook. This keeps the
package free of database-specific dependencies; you only pull in what you use.
There are two kinds of database instrumentation:
| Kind | Examples | How to enable |
|---|---|---|
Package-based — ships an Add…Instrumentation() extension |
SQL Server (OpenTelemetry.Instrumentation.SqlClient), EF Core (OpenTelemetry.Instrumentation.EntityFrameworkCore) |
install the package + call it in ConfigureTracing |
Source-based — the driver already emits an ActivitySource |
Npgsql (Npgsql), MySqlConnector (MySqlConnector), Oracle (Oracle.ManagedDataAccess.Core) |
register the source name — code or config, no extra package |
ConfigureTracing is a single delegate, so chain everything in one assignment:
// dotnet add package OpenTelemetry.Instrumentation.SqlClient
// dotnet add package OpenTelemetry.Instrumentation.EntityFrameworkCore
o.ConfigureTracing = t => t
.AddSqlClientInstrumentation()
.AddEntityFrameworkCoreInstrumentation()
.AddSource("Npgsql") // PostgreSQL
.AddSource("MySqlConnector"); // MySQLNo code for source-based drivers: the second row of the table above needs no
C# at all — AdditionalTracingSources does the same job from appsettings.json:
{
"Telemetry": {
"Endpoint": "http://localhost:4318",
"AdditionalTracingSources": [ "Npgsql", "MyApp" ],
"AdditionalMeters": [ "MyApp.Orders" ]
}
}Package-based instrumentation (SQL Server, EF Core) still needs the one-line
ConfigureTracingcall above, because it requires its NuGet package — a config string alone can't pull in a dependency.
Toggling SQL instrumentation from appsettings.json
Because EnableSqlClientInstrumentation is not part of TelemetryOptions (the
package is optional), you can add it as a custom key and read it in the callback:
appsettings.json:
{
"Telemetry": {
"Endpoint": "http://localhost:4318",
"ServiceName": "my-api",
"RecordExceptions": true,
"EnableSqlClientInstrumentation": true // custom key
}
}Program.cs:
builder.Services.AddTelemetry(builder.Configuration, opt =>
opt.ConfigureTracing = tracing =>
{
if (builder.Configuration.GetValue<bool>("Telemetry:EnableSqlClientInstrumentation"))
{
// NuGet: OpenTelemetry.Instrumentation.SqlClient
tracing.AddSqlClientInstrumentation(sql => sql.RecordException = opt.RecordExceptions);
}
});This keeps the on/off switch in config while the package dependency stays explicit in code.
AddTelemetry() works with any IServiceCollection — ASP.NET Core, WPF,
WinForms, console, MAUI/WinUI, UWP, worker services, etc.
With the Generic Host (recommended for desktop/console — Host.CreateApplicationBuilder()),
the providers start and flush automatically:
var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddTelemetry(builder.Configuration);
using var host = builder.Build();
await host.RunAsync(); // telemetry starts here and flushes on shutdownWithout a host (e.g. a bare ServiceCollection in UWP or a minimal app),
build the provider and dispose it on exit so buffered telemetry is flushed:
var services = new ServiceCollection();
services.AddTelemetry(o =>
{
o.Endpoint = new Uri("http://localhost:4318");
o.ServiceName = "my-desktop-app";
});
var provider = services.BuildServiceProvider();
// ... app runs ...
provider.Dispose(); // flushes traces, metrics and logsASP.NET Core instrumentation is in the
net8.0andnet10.0builds only — which a desktop or console app targetingnet8.0/net10.0(or their-windowsvariants) does get. Only on thenetstandard2.0build (.NET Framework) is it absent, and settingEnableAspNetCoreInstrumentationthere is a harmless no-op. See Requirements.
Two runnable samples live under src/:
| Sample | Project | Demonstrates |
|---|---|---|
| Web API | …Sample.WebApi |
ASP.NET Core minimal API configured from appsettings.json, ready-to-run backend profiles, EF Core and opt-in SQL instrumentation. |
| WPF | …Sample.Wpf |
Desktop app wiring AddTelemetry() through the Generic Host, emitting a custom ActivitySource/Meter and an HttpClient span on a button click. |
The Web API sample drives the backend walkthrough below; the WPF sample exports
to http://localhost:4318 by default — point it at any of the backends here.
The Web API sample ships
ready-to-run configurations for several popular backends (the three below are
documented in full; more start scripts live in infrastructure/). Each backend has:
- an infrastructure start script (Docker Compose or Helm) in
infrastructure/, - a launch profile that selects the matching
appsettings.<env>.json, - a UI where the exported traces, metrics and logs show up.
- Start the backend infrastructure — run the script for your backend (see table).
- Docker scripts live in
infrastructure/dockerand need Docker. - Helm scripts live in
infrastructure/helmand need a local Kubernetes cluster (e.g. k3s in WSL2).
- Docker scripts live in
- Run the sample with the matching profile:
Or pick the profile from the run dropdown in Visual Studio / Rider.
cd src/OpenTelemetryExtension.Configuration.Sample.WebApi dotnet run --launch-profile "Start Aspire"
- Generate traffic — the app opens Swagger at
https://localhost:5073/swagger; call an endpoint. - Open the backend UI (see table) to inspect the telemetry.
| Backend | Start infrastructure | Launch profile | Backend UI |
|---|---|---|---|
| .NET Aspire Dashboard | docker/docker-install-aspire-dashboard.cmd (or helm/helm-install-aspire-dashboard.cmd) |
Start Aspire |
http://localhost:31888 |
| Jaeger | docker/docker-install-jaeger.cmd |
Start Jaeger |
http://localhost:16686 |
| OpenObserve | helm/helm-install-openobserve.cmd (or docker/docker-install-openobserve.cmd) |
Start OpenObserve Http / Start OpenObserve Grpc |
http://localhost:30117 (admin@web.de/admin) |
| SigNoz | helm/helm-install-signoz.cmd |
Start SigNoz |
http://localhost:30111 (admin@web.de) |
| Grafana / Loki | docker/docker-install-loki.cmd |
Start Loki |
http://localhost:3000 (admin/admin) |
| OpenSearch Dashboards | docker/docker-install-opensearch.cmd |
Start OpenSearch |
http://localhost:5601 |
(Paths are relative to infrastructure/. The first three are
documented in full below; the rest work the same way — start the script, pick the
profile.)
Tip — viewing logs in the Aspire Dashboard: after starting the app with the
Start Aspireprofile, open http://localhost:31888, then go to the Structured (logs), Traces or Metrics tab. Data appears as soon as you hit a Swagger endpoint.
These are the exact appsettings.<env>.json files used by the sample's launch profiles.
The dashboard requires an API key on the OTLP endpoint (x-otlp-api-key). The
gRPC endpoint is exposed on NodePort 31889 (Helm) or host port 31889 (Docker).
{
"Telemetry": {
"Protocol": "Grpc",
"Endpoint": "http://localhost:31889",
"Headers": "x-otlp-api-key=aspire"
}
}Traces, metrics and logs from the sample app shown live in the Aspire Dashboard UI:
{
"Telemetry": {
"Protocol": "Grpc",
"Endpoint": "http://localhost:4317"
}
}Traces from the sample app shown in the Jaeger UI:
{
"Telemetry": {
"Protocol": "HttpProtobuf",
"Endpoint": "http://localhost:30117/api/default",
"Headers": "Authorization=Basic YWRtaW5Ad2ViLmRlOmFkbWlu,stream-name=default"
}
}The same telemetry explored in the OpenObserve UI:
Contributions are welcome — bug fixes, new instrumentation options, documentation improvements and ideas alike. For anything beyond a small fix, please open an issue first so we can agree on the approach before you invest time in a pull request.
The basic workflow is: fork the repo, branch off main (feature/<name> or
fix/<name>), make your change, add or update tests for any public API change,
run the unit tests, and open a PR against main. Build and test locally with:
dotnet build OpenTelemetryExtension.slnx -c Release
dotnet test src/OpenTelemetryExtension.Configuration.Tests -c ReleaseFull details — code style, the integration-test stack, and the release process — are in CONTRIBUTING.md. Note that versioning and release notes are handled separately at release time, so you don't need to touch them in a feature PR.
Found something broken or behaving unexpectedly? Please
open an issue.
To help reproduce it quickly, include the package version, your target framework
(e.g. net10.0 or netstandard2.0), the relevant Telemetry configuration, the
OTLP backend you export to, and what you expected to happen versus what actually
did. A minimal repro or stack trace speeds things up a lot.
For security-sensitive reports, please follow SECURITY.md instead of opening a public issue.



