Application Tracing in .NET for Performance Monitoring

Published February 10, 2024

portrait of Vadim Korolik.

by Vadim Korolik

Introduction

Application tracing is a critical aspect of software development and maintenance, especially in complex .NET environments. It involves monitoring and recording application activities, providing insights into application behavior and performance. This guide will delve into the essential tools and techniques for effective application tracing in .NET, ensuring that developers and administrators can efficiently troubleshoot and optimize their applications.

The Role of Tracing in .NET Applications

In .NET, application tracing provides a window into the running state of applications, helping identify bottlenecks, errors, and performance issues. It’s not only about finding problems; tracing also offers valuable data for optimizing and refining application functionality.

Utilizing Built-in .NET Tracing Capabilities

.NET Framework and .NET Core offer built-in tracing capabilities that are robust and easy to implement. Let’s explore a basic example of how to implement tracing in a .NET application:

1using System.Diagnostics;
2
3Trace.Listeners.Add(new TextWriterTraceListener("logfile.log"));
4Trace.AutoFlush = true;
5Trace.WriteLine("Starting application tracing");
6
7// Your application logic here
8
9Trace.WriteLine("Ending application tracing");

This code snippet demonstrates how to set up a simple trace listener that writes trace output to a file. This is fundamental for any .NET application requiring basic logging and tracing capabilities.

Advanced Tracing with DiagnosticSource in .NET Core

For more advanced scenarios, especially in .NET Core, System.Diagnostics.DiagnosticSource provides a powerful way to collect rich telemetry data. Here’s an example:

1using System.Diagnostics;
2
3var source = new DiagnosticListener("MyApplicationSource");
4
5if (source.IsEnabled("StartRequest"))
6{
7 source.Write("StartRequest", new { RequestId = Guid.NewGuid(), Timestamp = DateTime.UtcNow });
8}
9
10// Application logic here
11
12if (source.IsEnabled("EndRequest"))
13{
14 source.Write("EndRequest", new { RequestId = Guid.NewGuid(), Timestamp = DateTime.UtcNow });
15}

This code creates a DiagnosticListener that emits custom events, making it a versatile tool for complex tracing requirements.

Leveraging Third-Party Tools for Tracing

Application Insights for Comprehensive Telemetry

Application Insights, a feature of Azure Monitor, is an extensible Application Performance Management (APM) service for developers. It can be easily integrated into .NET applications:

1using Microsoft.ApplicationInsights;
2using Microsoft.ApplicationInsights.Extensibility;
3
4var telemetryConfiguration = TelemetryConfiguration.CreateDefault();
5telemetryConfiguration.InstrumentationKey = "your_instrumentation_key_here";
6var telemetryClient = new TelemetryClient(telemetryConfiguration);
7
8telemetryClient.TrackTrace("Application trace message");

This snippet shows how to send trace messages to Application Insights, which provides analytics and actionable insights on application performance and usage.

LaunchDarkly for Open Source Telemetry via OpenTelemetry

Setting up OpenTelemetry tracing for a .NET application involves a few key steps. OpenTelemetry is a set of APIs, libraries, agents, and instrumentation that allow you to create and manage telemetry data (metrics, logs, and traces) for your applications. Here’s how you can set up tracing in a .NET application:

  1. Install Necessary NuGet Packages

First, you need to add the OpenTelemetry packages to your project. You can do this via the NuGet package manager. The primary package you’ll need is OpenTelemetry. Depending on the specific needs of your application, you may also need exporters (like Zipkin, Jaeger, etc.) and instrumentation for specific libraries.

$# Copy code
$dotnet add package OpenTelemetry
$dotnet add package OpenTelemetry.Exporter.Console
$dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol
$dotnet add package OpenTelemetry.Instrumentation.AspNetCore
  1. Configure Services in Startup.cs

In the Startup.cs file of your .NET application, you need to configure the OpenTelemetry services. This includes setting up the tracing pipeline with any necessary exporters and instrumentations.

Here is an example code block for setting up a basic OpenTelemetry tracing with a console exporter and ASP.NET Core instrumentation:

1using OpenTelemetry.Trace;
2
3public class Startup
4{
5public void ConfigureServices(IServiceCollection services)
6{
7// Other service configurations ...
8
9 // Configure OpenTelemetry Tracing
10 services.AddOpenTelemetryTracing(builder =>
11 {
12 builder
13 .AddAspNetCoreInstrumentation()
14 .AddConsoleExporter(); // Using console exporter for demonstration
15 });
16 }
17
18 public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
19 {
20 // Existing configuration code...
21
22 // Ensure to use the appropriate middleware if needed
23 app.UseOpenTelemetry();
24 }
25}
  1. Instrumenting Your Code

To create custom traces or to add additional information to the automatic traces, you can use the OpenTelemetry API in your application code:

1using System.Diagnostics;
2using OpenTelemetry.Trace;
3
4public class MyService
5{
6private static readonly ActivitySource ActivitySource = new ActivitySource("highlight-dot-net-example");
7
8 public void DoWork()
9 {
10 using (var activity = ActivitySource.StartActivity("DoingWork"))
11 {
12 // Your logic here
13 // You can add custom tags or events to 'activity' as needed
14 }
15 }
16}

The configuration and code above set up a basic tracing pipeline for a .NET application. This will set up a console exporter to debug traces to the console log, but you can change to an OTLP exporter to send traces to a remote collector.

  1. Update the code block to send traces to highlight
1using System.Diagnostics;
2using OpenTelemetry;
3using OpenTelemetry.Exporter;
4using OpenTelemetry.Logs;
5using OpenTelemetry.Metrics;
6using OpenTelemetry.Resources;
7using OpenTelemetry.Trace;
8using Serilog.Context;
9using Serilog.Core;
10using Serilog.Events;
11using Serilog.Sinks.OpenTelemetry;
12
13namespace dotnet;
14
15public class HighlightTraceProcessor : BaseProcessor<Activity>
16{
17 public override void OnStart(Activity data)
18 {
19 var ctx = HighlightConfig.GetHighlightContext();
20 foreach (var entry in ctx)
21 {
22 data.SetTag(entry.Key, entry.Value);
23 }
24
25 base.OnStart(data);
26 }
27}
28
29public class HighlightLogProcessor : BaseProcessor<LogRecord>
30{
31 public override void OnStart(LogRecord data)
32 {
33 var ctx = HighlightConfig.GetHighlightContext();
34 var attributes = ctx.Select(entry => new KeyValuePair<string, object?>(entry.Key, entry.Value)).ToList();
35 if (data.Attributes != null)
36 {
37 attributes = attributes.Concat(data.Attributes).ToList();
38 }
39
40 data.Attributes = attributes;
41 base.OnStart(data);
42 }
43}
44
45public class HighlightLogEnricher : ILogEventEnricher
46{
47 public void Enrich(LogEvent logEvent, ILogEventPropertyFactory pf)
48 {
49 var ctx = HighlightConfig.GetHighlightContext();
50 foreach (var entry in ctx)
51 {
52 logEvent.AddOrUpdateProperty(pf.CreateProperty(entry.Key, entry.Value));
53 }
54 }
55}
56
57public class HighlightConfig
58{
59 // Replace with the highlight endpoint. For highlight.io cloud, use https://otel.highlight.io:4318
60 public static readonly String OtlpEndpoint = "https://otel.highlight.io:4318";
61
62 // Replace with your project ID and service name.
63 public static readonly String ProjectId = "<YOUR_PROJECT_ID>";
64 // This must match the ServiceName used by the ActivitySource to ensure traces are sent.
65 public static readonly String ServiceName = "highlight-dot-net-example";
66
67 public static readonly String TracesEndpoint = OtlpEndpoint + "/v1/traces";
68 public static readonly String LogsEndpoint = OtlpEndpoint + "/v1/logs";
69 public static readonly String MetricsEndpoint = OtlpEndpoint + "/v1/metrics";
70
71 public static readonly OtlpProtocol Protocol = OtlpProtocol.HttpProtobuf;
72 public static readonly OtlpExportProtocol ExportProtocol = OtlpExportProtocol.HttpProtobuf;
73 public static readonly String HighlightHeader = "x-highlight-request";
74
75 public static readonly Dictionary<string, object> ResourceAttributes = new()
76 {
77 ["highlight.project_id"] = ProjectId,
78 ["service.name"] = ServiceName,
79 };
80
81 public static Dictionary<string, string> GetHighlightContext()
82 {
83 var ctx = new Dictionary<string, string>
84 {
85 { "highlight.project_id", ProjectId },
86 };
87
88 var headerValue = Baggage.GetBaggage(HighlightHeader);
89 if (headerValue == null) return ctx;
90
91 var parts = headerValue.Split("/");
92 if (parts.Length < 2) return ctx;
93
94 ctx["highlight.session_id"] = parts[0];
95 ctx["highlight.trace_id"] = parts[1];
96 return ctx;
97 }
98
99 public static void EnrichWithHttpRequest(Activity activity, HttpRequest httpRequest)
100 {
101 var headerValues = httpRequest.Headers[HighlightHeader];
102 if (headerValues.Count < 1) return;
103 var headerValue = headerValues[0];
104 if (headerValue == null) return;
105 var parts = headerValue.Split("/");
106 if (parts?.Length < 2) return;
107 activity.SetTag("highlight.session_id", parts?[0]);
108 activity.SetTag("highlight.trace_id", parts?[1]);
109 Baggage.SetBaggage(new KeyValuePair<string, string>[]
110 {
111 new(HighlightHeader, headerValue)
112 });
113 }
114
115 public static void Configure(WebApplicationBuilder builder)
116 {
117 builder.Logging.AddOpenTelemetry(options =>
118 {
119 options
120 .SetResourceBuilder(ResourceBuilder.CreateDefault().AddAttributes(ResourceAttributes))
121 .AddProcessor(new HighlightLogProcessor())
122 .AddOtlpExporter(exporterOptions =>
123 {
124 exporterOptions.Endpoint = new Uri(LogsEndpoint);
125 exporterOptions.Protocol = ExportProtocol;
126 });
127 });
128
129 builder.Services.AddOpenTelemetry()
130 .ConfigureResource(resource => resource.AddAttributes(ResourceAttributes))
131 .WithTracing(tracing => tracing
132 .AddSource(ServiceName)
133 .AddProcessor(new HighlightTraceProcessor())
134 .AddAspNetCoreInstrumentation(options =>
135 {
136 options.RecordException = true;
137 options.EnrichWithHttpRequest = EnrichWithHttpRequest;
138 })
139 .AddOtlpExporter(options =>
140 {
141 options.Endpoint = new Uri(TracesEndpoint);
142 options.Protocol = ExportProtocol;
143 }))
144 .WithMetrics(metrics => metrics
145 .AddMeter(ServiceName)
146 .AddAspNetCoreInstrumentation()
147 .AddOtlpExporter(options =>
148 {
149 options.Endpoint = new Uri(MetricsEndpoint);
150 options.Protocol = ExportProtocol;
151 }));
152 }
153}

Get started today with our OpenTelemetry instrumentation for .NET that gives you flexibility with your data destination.

NLog for Flexible and Structured Logging

NLog is a versatile logging tool for .NET, allowing for structured logging, which is crucial in modern application tracing. Here’s a basic setup:

1var config = new NLog.Config.LoggingConfiguration();
2var logfile = new NLog.Targets.FileTarget("logfile") { FileName = "file.txt" };
3var logconsole = new NLog.Targets.ConsoleTarget("logconsole");
4
5config.AddRule(LogLevel.Info, LogLevel.Fatal, logconsole);
6config.AddRule(LogLevel.Debug, LogLevel.Fatal, logfile);
7
8NLog.LogManager.Configuration = config;

This configuration sets up NLog to log messages to both a file and the console, providing a flexible approach to logging.

Conclusion

Effective tracing in .NET applications is key to understanding and improving application behavior and performance. This guide has introduced various tools and techniques, from basic built-in .NET tracing to advanced tools like Application Insights and NLog. By choosing the right combination of these tools, developers can gain valuable insights and maintain robust, high-performance applications.

Explore these tracing techniques in your .NET projects. Share your experiences and insights in the comments below. For more in-depth information, check out our additional resources.