Adding OpenTelemetry to a .NET Core Application
.NET Core is widely used for building modern, cross-platform backend services and web applications. With performance, scalability, and reliability at stake, teams need real visibility into how their applications are running. This means going beyond simple logs—true observability requires metrics and traces.
OpenTelemetry (OTel) makes this possible. It's an open standard for collecting telemetry data—logs, metrics, and traces—in a consistent, vendor-neutral way. With OTel, you can track performance issues, monitor dependencies, and analyze runtime behavior across your entire stack.
In this guide, we’ll walk through how to instrument a .NET Core application with OTel. You’ll see how to capture metrics and trace data, and export that telemetry to SolarWinds® Observability SaaS for visualization and analysis.
Download and Run the Sample Application
To get started, we’ll use the Razor Pages movie listing app from Microsoft’s ASP.NET Core tutorials. This gives us a clean, functional project to work with.
First, download the sample application. You can also check out the official Microsoft Learn instructions. Make sure you have downloaded and installed the .NET 9.0 SDK.
For this basic demo, you will build and run the app locally. This means modifying the application to use SQLite.
Modifications for Local Demo (Use SQLite)
First, add this line <ItemGroup>
in RazorPagesMovie.csproj:
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.0-preview.3.24172.4" />
Next, modify Program.cs
to accommodate the SQLite case. The resulting file looks like this:
using Microsoft.EntityFrameworkCore;
using RazorPagesMovie.Data;
using RazorPagesMovie.Models;
var builder = WebApplication.CreateBuilder(args);
// Set environment to Development
builder.Environment.EnvironmentName = "Development";
builder.Services.AddRazorPages();
builder.Services.AddDbContext<RazorPagesMovieContext>(options =>
options.UseSqlite(builder.Configuration.GetConnectionString("RazorPagesMovieContext") ?? throw new InvalidOperationException("Connection string 'RazorPagesMovieContext' not found.")));
var app = builder.Build();
using (var scope = app.Services.CreateScope())
{
var services = scope.ServiceProvider;
SeedData.Initialize(services);
}
// Always use developer exception page in development
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapRazorPages();
app.Run();
Finally, modify appsettings.json
to point to the correct data source file:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"RazorPagesMovieContext": "Data Source=RazorPagesMovie.db"
}
}
With these file changes in place, go ahead and run the following commands:
rm -rf Migrations/
dotnet ef migrations add InitialCreate
dotnet ef database update
dotnet run
Now, when you open your browser to localhost:5000
, you’ll see the application running:
Configure the Application for OpenTelemetry
Before instrumenting the app, you need to add OTel-related dependencies to your project.
Add OpenTelemetry Packages
Use dotnet add package
to install the necessary OTel packages:
dotnet add package OpenTelemetry.Extensions.Hosting
dotnet add package OpenTelemetry.Instrumentation.AspNetCore
dotnet add package OpenTelemetry.Instrumentation.Runtime
dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol
dotnet add package OpenTelemetry.Extensions.Logging
dotnet add package OpenTelemetry.Instrumentation.Http
dotnet add package OpenTelemetry.Instrumentation.SqlClient
Next, modify Program.cs to set up the OTel services and configure the OTLP exporter to send data to SolarWinds Observability SaaS:
// Program.cs
using Microsoft.EntityFrameworkCore;
using OpenTelemetry;
using OpenTelemetry.Logs;
using OpenTelemetry.Metrics;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
using OpenTelemetry.Instrumentation.SqlClient;
using RazorPagesMovie.Data;
using RazorPagesMovie.Models;
var builder = WebApplication.CreateBuilder(args);
// Add OpenTelemetry configuration
builder.Configuration.AddJsonFile("appsettings.OpenTelemetry.json", optional: false, reloadOnChange: true);
// Set environment to Development
builder.Environment.EnvironmentName = "Development";
// Configure OpenTelemetry
var otelConfig = builder.Configuration.GetSection("OpenTelemetry");
var serviceName = otelConfig["ServiceName"] ?? "RazorPagesMovie";
var serviceVersion = otelConfig["ServiceVersion"] ?? "1.0.0";
var solarWindsEndpoint = otelConfig.GetSection("SolarWinds")["Endpoint"];
var solarWindsToken = otelConfig.GetSection("SolarWinds")["ApiToken"];
if (string.IsNullOrEmpty(solarWindsEndpoint) || string.IsNullOrEmpty(solarWindsToken))
{
throw new InvalidOperationException("SolarWinds endpoint and API token must be configured in appsettings.OpenTelemetry.json");
}
builder.Services.AddOpenTelemetry()
.ConfigureResource(resource => resource
.AddService(serviceName: serviceName, serviceVersion: serviceVersion))
.WithTracing(tracing => tracing
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddSqlClientInstrumentation()
.AddOtlpExporter(opts => {
opts.Endpoint = new Uri(solarWindsEndpoint);
opts.Headers = $"authorization=Bearer {solarWindsToken}";
}))
.WithMetrics(metrics => metrics
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddRuntimeInstrumentation()
.AddOtlpExporter(opts => {
opts.Endpoint = new Uri(solarWindsEndpoint);
opts.Headers = $"authorization=Bearer {solarWindsToken}";
}));
// Configure OpenTelemetry Logging
builder.Logging.AddOpenTelemetry(logging =>
{
logging.IncludeFormattedMessage = true;
logging.IncludeScopes = true;
logging.ParseStateValues = true;
logging.AddOtlpExporter(opts => {
opts.Endpoint = new Uri(solarWindsEndpoint);
opts.Headers = $"authorization=Bearer {solarWindsToken}";
});
});
builder.Services.AddRazorPages();
...
Next, configure your application with the credentials to send OTel data to SolarWinds Observability SaaS.
Get SolarWinds Observability SaaS OTel Endpoint and API Token
To find your OTel endpoint and create an API token for data ingestion, follow these steps.
Log in to SolarWinds Observability SaaS. Take note of the URL. It may look similar to this:
https://my.na-01.cloud.solarwinds.com/
The xx-yy
part of the URL (in the above example, that's na-01
) indicates the data center for your organization. You will need this when determining your OTel endpoint.
Navigate to Settings > API Tokens.
On the API Tokens page, click Create API Token.
Specify a name for your new API token. Select Ingestion as the token type. You can also add tags if you'd like. Click Create API Token.
Copy the resulting token value.
Configure OTel Export Settings
Next, create a new configuration file (appsettings.OpenTelemetry.json) to store the SolarWinds Observability SaaS endpoint and API token.
{
"OpenTelemetry": {
"ServiceName": "RazorPagesMovie",
"ServiceVersion": "1.0.0",
"SolarWinds": {
"Endpoint": "https://otel.collector.xx-yy.cloud.solarwinds.com:443",
"ApiToken": "YOUR_API_TOKEN_HERE"
}
}
}
Replace the xx-yy
in the endpoint with your data center code (such as na-01). Then, paste in the API token value that was just copied.
Lastly, update the project file (RazorPagesMovie.csproj) to include this configuration file:
<ItemGroup>
…
<None Update="appsettings.OpenTelemetry.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
Instrument the Application
With the application configured with OTel and ready to collect and send telemetry data to SolarWinds Observability SaaS, you're ready to instrument the application.
Logging Middleware and Telemetry Helpers
For convenience, create a class (Middleware/RequestLoggingMiddleware.cs
) so that you can log every request:
// Middleware/RequestLoggingMiddleware.cs
using System.Diagnostics;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
namespace RazorPagesMovie.Middleware;
public class RequestLoggingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<RequestLoggingMiddleware> _logger;
public RequestLoggingMiddleware(RequestDelegate next, ILogger<RequestLoggingMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
var sw = Stopwatch.StartNew();
try
{
await _next(context);
}
finally
{
sw.Stop();
var elapsed = sw.ElapsedMilliseconds;
var statusCode = context.Response?.StatusCode ?? 500;
var method = context.Request.Method;
var path = context.Request.Path;
_logger.LogInformation(
"Request {Method} {Path} completed with status code {StatusCode} in {ElapsedMilliseconds}ms",
method, path, statusCode, elapsed
);
}
}
}
Next, create a class (Telemetry/TelemetryHelper.cs
) to implement custom traces and metrics.
using System.Diagnostics;
using System.Diagnostics.Metrics;
using OpenTelemetry.Trace;
using Microsoft.Extensions.Configuration;
namespace RazorPagesMovie.Telemetry;
public static class TelemetryHelper
{
private static readonly ActivitySource ActivitySource;
private static readonly Meter Meter;
// Metrics
private static readonly Counter<long> MovieOperationCounter;
private static readonly Histogram<double> MovieOperationDuration;
static TelemetryHelper()
{
var configuration = new ConfigurationBuilder()
.AddJsonFile("appsettings.OpenTelemetry.json", optional: false)
.Build();
var serviceName = configuration.GetSection("OpenTelemetry")["ServiceName"] ?? "RazorPagesMovie";
ctivitySource = new ActivitySource(serviceName);
Meter = new Meter(serviceName);
MovieOperationCounter = Meter.CreateCounter<long>(
"movie_operations_total",
"operations",
"Total number of movie operations performed");
MovieOperationDuration = Meter.CreateHistogram<double>(
"movie_operation_duration_seconds",
"seconds",
"Duration of movie operations");
}
public static Activity? StartMovieOperation(string operation, int? movieId = null)
{
var activity = ActivitySource.StartActivity($"Movie.{operation}");
if (activity != null)
{
activity.SetTag("movie.operation", operation);
if (movieId.HasValue)
{
activity.SetTag("movie.id", movieId.Value);
}
}
return activity;
}
public static void RecordMovieOperation(string operation, double durationSeconds, int? movieId = null)
{
var tags = new KeyValuePair<string, object?>[]
{
new("operation", operation)
};
if (movieId.HasValue)
{
tags = tags.Append(new KeyValuePair<string, object?>("movie.id", movieId.Value)).ToArray();
}
MovieOperationCounter.Add(1, tags);
MovieOperationDuration.Record(durationSeconds, tags);
}
}
Back in Program.cs
, register your middleware and telemetry components.
// Program.cs
…
builder.Services.AddOpenTelemetry()
.ConfigureResource(resource => resource
.AddService(serviceName: serviceName, serviceVersion: serviceVersion))
.WithTracing(tracing => tracing
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddSqlClientInstrumentation()
.AddSource(serviceName)
.AddOtlpExporter(opts => {
opts.Endpoint = new Uri(solarWindsEndpoint);
opts.Headers = $"authorization=Bearer {solarWindsToken}";
}))
.WithMetrics(metrics => metrics
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddRuntimeInstrumentation()
.AddMeter(serviceName)
.AddOtlpExporter(opts => {
opts.Endpoint = new Uri(solarWindsEndpoint);
opts.Headers = $"authorization=Bearer {solarWindsToken}";
}));
…
app.UseMiddleware<RequestLoggingMiddleware>();
Set up Tracing and Metrics
Finally, throughout the application, add instrumentation code. For example, in the Create movie operation, you can capture request duration metrics.
// Pages/Movies/Create.cshtml.cs
public async Task<IActionResult> OnPostAsync()
{
var sw = Stopwatch.StartNew();
using var activity = TelemetryHelper.StartMovieOperation("Create");
try
{
if (!ModelState.IsValid)
{
return Page();
}
_context.Movie.Add(Movie);
await _context.SaveChangesAsync();
_logger.LogInformation("Created movie: {Title}", Movie.Title);
TelemetryHelper.RecordMovieOperation("Create", sw.Elapsed.TotalSeconds);
return RedirectToPage("./Index");
}
catch (Exception ex)
{
_logger.LogError(ex, "Error creating movie: {Title}", Movie.Title);
activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
throw;
}
}
Your application is now instrumented with OTel across all three pillars:
- Logging: HTTP requests (via
RequestLoggingMiddleware
) and movie operations (in page models) - Traces: HTTP/SQL operations (automatic) and movie operations (via
TelemetryHelper
) - Metrics: Runtime/HTTP metrics (automatic) and movie operation counts/durations (via
TelemetryHelper
)
All telemetry is exported to SolarWinds with consistent service naming and proper authentication. You're ready to start it up again.
dotnet run
This time, with the application running, try performing different operations and transactions to simulate activity and generate telemetry data.
Confirm Data Ingestion With SolarWinds Observability SaaS
To confirm that your application is successfully sending telemetry data to SolarWinds Observability SaaS, log in to your account and navigate to APM.
There, you should see your application listed.
Clicking the application brings you to an APM overview page for your application. You’ll see the presence of traces (50+) and metrics (4) just by looking at the top bar nav.
The Traces subpage shows the custom spans that you have instrumented for tracing in your application's Movie model.
When you click on the details for any individual trace, you’ll see detailed information about the times spent in each of the components throughout the action.
Similarly, you can visit the Metrics subpage for your application. From there, you can see metrics, such as the average response time.
Finally, to see log events captured with OTel and exported to SolarWinds Observability SaaS, navigate to the Logs section of the site.
On that page, there’s a comprehensive set of log events, from your request middleware, explicit logging statements in your Movie actions, and within your database operations.
You have successfully instrumented your .NET Core application with OTel, and your telemetry data is shipping to SolarWinds Observability SaaS for easier analysis and querying.
Wrap-up and Next Steps
We’ve shown how to instrument a .NET Core app with OpenTelemetry to capture logs, metrics, and traces. By using open standards, you can make your observability setup portable and extensible—there is no vendor lock-in.
If you’re looking for a powerful and user-friendly way to analyze and visualize your telemetry data, check out SolarWinds Observability SaaS. It’s built to work seamlessly with OTel and supports a wide range of application stacks, including .NET Core.