Manual instrumentation of .NET applications with OpenTelemetry
This guide will walk you using the OpenTelemetry SDK to collect metrics and traces from a simple ASP.NET Core application in .NET 6.
Prerequisites
To follow this guide, you will need to have Visual Studio Code or Visual Studio 2022 installed and the .NET 6 SDK.
If these haven’t been installed yet, choose an IDE and install the most recent stable SDK version:
Scaffold New Project
First, we will create a new ASP.NET Core project.
Run the following in a terminal:
dotnet new web -o HelloWorldApi
Or using Visual Studio 2022, create a new project and select the “ASP.NET Core Empty” template.
Open the new project - the Program.cs
file will look like this:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();
Now we have a minimal ASP.NET Core API setup and ready to instrument using the OpenTelemetry SDK.
OpenTelemetry SDK Installation
For this example, we’ll be using the following packages:
OpenTelemetry.Extensions.Hosting
- Adds OpenTelemetry hooks to dependency injection
OpenTelemetry.Instrumentation.AspNetCore
- Collects metrics and traces from ASP.NET Core
OpenTelemetry.Instrumentation.Runtime
- .NET runtime metrics, like garbage collection statistics
OpenTelemetry.Exporter.Console
- Outputs collected metrics and traces to stdout
Run the following to install the four packages:
dotnet add package OpenTelemetry.Extensions.Hosting --version 1.5.1
dotnet add package OpenTelemetry.Instrumentation.AspNetCore --version 1.5.0-beta.1
dotnet add package OpenTelemetry.Instrumentation.Runtime --version 1.5.0
dotnet add package OpenTelemetry.Exporter.Console --version 1.5.1
If installing using Visual Studio 2022, be sure to select “Include prelease” in the NuGet Package Manager.
OpenTelemetry Tracing Setup
To setup tracing, modify Program.cs
to look like:
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOpenTelemetry()
.ConfigureResource(resource => resource.AddService(serviceName: builder.Environment.ApplicationName))
.WithTracing(tracing => tracing.AddAspNetCoreInstrumentation()
.AddConsoleExporter());
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();
These additions:
- Configure the service name, stored as
service.name
on traces - Enable trace collection from ASP.NET Core
- Output any traces collected to stdout
Run the application via your IDE or this terminal command:
dotnet run
Open a browser or curl the application and you will see trace data written to the terminal.
OpenTelemetry Metrics Setup
To collect metrics in addition to traces, once more modify Program.cs
. This time it should look like:
using OpenTelemetry.Metrics;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOpenTelemetry()
.ConfigureResource(resource => resource.AddService(serviceName: builder.Environment.ApplicationName))
.WithTracing(tracing => tracing.AddAspNetCoreInstrumentation()
.AddConsoleExporter())
.WithMetrics(metrics => metrics.AddAspNetCoreInstrumentation()
.AddRuntimeInstrumentation()
.AddConsoleExporter());
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();
Notice this looks very similar to enabling collection of traces. The new lines:
- Enable metrics collection from ASP.NET Core
- Enable metrics collection from the .NET runtime
- Output any metrics collected to stdout
Run the application again - this time metrics and traces will be written to the terminal.
Adding Recommended OpenTelemetry Resource Attributes
OpenTelemetry resource attributes are added to logs, traces, and metrics emitted by your application. These are standardized attributes that makes telemetry data much easier.
Our sample already sets the service.name
attribute in the ConfigureResource
call. We’ll be adding four more recommended attributes. Read more about OpenTelemetry resource attributes.
After modifying our ConfigureResource
call to set the additional attributes, Program.cs
now looks like:
using OpenTelemetry.Metrics;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
using System.Reflection;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOpenTelemetry()
.ConfigureResource(resource => resource.AddService(
serviceNamespace: "demo-namespace",
serviceName: builder.Environment.ApplicationName,
serviceVersion: Assembly.GetEntryAssembly()?.GetName().Version?.ToString(),
serviceInstanceId: Environment.MachineName
).AddAttributes(new Dictionary<string, object>
{
{ "deployment.environment", builder.Environment.EnvironmentName }
}))
.WithTracing(tracing => tracing.AddAspNetCoreInstrumentation()
.AddConsoleExporter())
.WithMetrics(metrics => metrics.AddAspNetCoreInstrumentation()
.AddRuntimeInstrumentation()
.AddConsoleExporter());
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();
Now if you run the application, these new attributes will be included with metric and trace data written to the terminal.
Sending OpenTelemetry Data to Grafana
Now we’ve seen metrics and traces written to stdout, let’s extend the application to send this telemetry data to Grafana.
Note: If you use Grafana Cloud, follow the OpenTelemetry Integration, which creates a Grafana Agent configuration for you.
Before changing any code, follow the steps in this article to setup the Grafana Agent:
Once the Agent is setup locally and listening for OpenTelemetry data, we will install a new exporter.
Once more, run this command in your terminal is use the NuGet Packager Manager in Visual Studio:
dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol --version 1.5.1
The only change needed to add a call to AddOtlpExporter
after each call AddConsoleExporter
, resulting in this Program.cs
:
using OpenTelemetry.Metrics;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOpenTelemetry()
.ConfigureResource(resource => resource.AddService(
serviceNamespace: "demo-namespace",
serviceName: builder.Environment.ApplicationName,
serviceVersion: Assembly.GetEntryAssembly()?.GetName().Version?.ToString(),
serviceInstanceId: Environment.MachineName
).AddAttributes(new Dictionary<string, object>
{
{ "deployment.environment", builder.Environment.EnvironmentName }
}))
.WithTracing(tracing => tracing.AddAspNetCoreInstrumentation()
.AddConsoleExporter()
.AddOtlpExporter())
.WithMetrics(metrics => metrics.AddAspNetCoreInstrumentation()
.AddRuntimeInstrumentation()
.AddConsoleExporter()
.AddOtlpExporter());
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();
Note: In a production scenario, you’d remove the call to
AddConsoleExporter
. We’re leaving it here for demo purposes.
The Grafana Agent is already setup to listen on the default ports the OTLP (OpenTelemetry Protocol) exporter connects over - no additional configuration is required.
If the Agent is not running locally with the default gRPC endpoint (localhost:4317), you may change the OTLP endpoint by specificing a hostname and port like so:
.AddOtlpExporter(config => config.Endpoint = new Uri("http://host:port"))
Now when you run this latest version, you should still see metrics and traces written to stdout. You will see also see new metrics and traces written to the Grafana instance the Agent has been configured to write to.