Distributed Tracing
This topic describes distributed tracing for .NET applications.
Previous versions of Steeltoe offered either an implementation of OpenCensus or shortcuts for enabling distributed tracing with OpenTelemetry to integrate with the Tanzu platform. Steeltoe 3 has a strong dependency on OpenTelemetry packages, but with the breaking changes in the latest versions of OpenTelemetry, Steeltoe couldn't adapt without resorting to reflection. While we do that in other places, reflection is slow, and instrumentation is performance-sensitive.
OpenTelemetry has evolved to the point that only a few lines of code are needed. So instead of providing a Steeltoe component, the guidance below is offered to accomplish the same. The benefit of this approach is greater flexibility: when OpenTelemetry changes, there's no need to wait for a new Steeltoe version before using it.
Steeltoe continues to directly offer an option for log correlation. The remainder of this topic provides direction for developers looking to achieve the same outcomes Steeltoe has previously provided more directly.
About distributed tracing
As the name implies, distributed tracing is a way of tracing requests through distributed systems. Distributed tracing is typically accomplished by instrumenting components of the system to allow recognizing and passing along metadata that is specific to a particular action or user request, and using another backend system to reconstruct the flow of the request through that metadata.
In the parlance of distributed tracing, a "span" is the basic unit of work. For example, sending an HTTP request creates a new span, as does sending a response. Each span is identified by a unique ID and contains the ID for the "trace" it is part of. Spans also have other data like descriptions, key-value annotations, the ID of the span that initiated the execution flow, and process IDs. Spans are started and stopped, and they keep track of their timing information. After you create a span, you must stop it at some point in the future. A set of spans form a tree-like structure called a "trace." For example, a trace might be formed by a POST request that adds an item to a shopping cart, which results in calling several backend services.
Log correlation
Log correlation refers to the process of taking log entries from disparate systems and bringing them together using some matching criteria (such as a distributed trace ID). The process is easier when important pieces of data are logged in the same format across different systems (such as .NET and Java apps communicating with each other).
Steeltoe provides the class TracingLogProcessor
, which is an IDynamicMessageProcessor
for correlating logs. The processor is built for use with a Steeltoe Dynamic Logging provider.
It enriches log entries with correlation data using the same trace format popularized by Spring Cloud Sleuth,
that include [<ApplicationName>,<TraceId>,<SpanId>,<ParentSpanId>,<IsAllDataRequested>]
.
Consider this pair of log entries from the Steeltoe Management sample applications:
info: System.Net.Http.HttpClient.ActuatorApiClient.LogicalHandler[100]
[ActuatorWeb,44ed2fe24a051bda2d1a56815448e9fb,8d51b985e3f0fd81,0000000000000000,true] Start processing HTTP request GET http://localhost:5140/weatherForecast?fromDate=2024-12-19&days=1
dbug: Microsoft.EntityFrameworkCore.Database.Command[20104]
[ActuatorApi,44ed2fe24a051bda2d1a56815448e9fb,c32846ff227bed40,f315823f4c554816,true] Created DbCommand for 'ExecuteReader' (1ms).
Log correlation is easiest with a tool such as Splunk, SumoLogic, or DataDog. (This is not an endorsement of any tool, only a pointer to some popular options.)
Using TracingLogProcessor
To use the processor:
Add a reference to the
Steeltoe.Management.Tracing
NuGet package.Register the processor from
Program.cs
:using Steeltoe.Management.Tracing; var builder = WebApplication.CreateBuilder(args); builder.Services.AddTracingLogProcessor();
Note
The AddTracingLogProcessor()
extension method also ensures that implementations of IApplicationInstanceInfo
and IDynamicLoggerProvider
have been registered.
If you want to customize either of these or use non-default implementations, call their extension methods before calling AddTracingLogProcessor
.
OpenTelemetry
To use OpenTelemetry, start by adding a reference to the OpenTelemetry.Extensions.Hosting
NuGet package.
Depending on your specific application needs, other package references will probably be necessary.
This package provides access to OpenTelemetryBuilder
, which is the main entrypoint to OpenTelemetry.
Add OpenTelemetry Tracing
To use tracing with OpenTelemetry, add the following to your Program.cs
:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOpenTelemetry().WithTracing();
Note
At this point, not much has changed for the application. The following sections will expand this line to add instrumentation and exporting of traces.
Sampler configuration
OpenTelemetry Provides the Sampler
abstraction for configuring when traces should be recorded.
The simplest options are AlwaysOnSampler
and AlwaysOffSampler
, with their names describing exactly which traces will be recorded.
As a replacement for what Steeltoe used to provide for using these samplers, set the environment variable OTEL_TRACES_SAMPLER
to:
always_on
always_off
Tip
OpenTelemetry is generally built to follow the options pattern. There are more ways to configure options than demonstrated in this topic; these are just examples to help you get started.
Set Application Name
To use the Steeltoe name for your application with OpenTelemetry, call SetResourceBuilder
and pass in a value from the registered IApplicationInstanceInfo
:
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
using Steeltoe.Common;
builder.Services.ConfigureOpenTelemetryTracerProvider((serviceProvider, tracing) =>
{
var appInfo = serviceProvider.GetRequiredService<IApplicationInstanceInfo>();
tracing.SetResourceBuilder(ResourceBuilder.CreateDefault().AddService(appInfo.ApplicationName!));
});
The above example assumes you are already using some other Steeltoe component that adds IApplicationInstanceInfo
to the IoC container. If that is not the case, follow these steps to register the default implementation:
Add a NuGet package reference to
Steeltoe.Common
.Call
AddApplicationInstanceInfo
.using Steeltoe.Common.Extensions; var builder = WebApplication.CreateBuilder(args); builder.Services.AddApplicationInstanceInfo();
Instrumenting applications
To maximize the benefit of collecting distributed traces, ideally, you will have participation from the core components and frameworks of your application and some third-party components. Some packages in the .NET ecosystem automatically support OpenTelemetry; others can be supported by the collection of instrumentation libraries. Steeltoe previously configured the instrumentation libraries for ASP.NET Core and HttpClient.
ASP.NET Core
To instrument requests coming into the application through ASP.NET Core:
Add a reference to the
OpenTelemetry.Instrumentation.AspNetCore
NuGet package.Add the instrumentation to the
TracerProviderBuilder
by updating the existing call from the earlier example to:using OpenTelemetry.Trace; builder.Services.AddOpenTelemetry().WithTracing(tracing => tracing.AddAspNetCoreInstrumentation());
To replicate the Steeltoe setting
IngressIgnorePattern
(a Regex pattern describing which incoming requests to ignore), configure theAspNetCoreTraceInstrumentationOptions
:using System.Text.RegularExpressions; using OpenTelemetry.Instrumentation.AspNetCore; builder.Services.Configure<AspNetCoreTraceInstrumentationOptions>(options => { const string defaultIngressIgnorePattern = @"/actuator/.*|/cloudfoundryapplication/.*|.*\.png|.*\.css|.*\.js|.*\.html|/favicon.ico|.*\.gif"; Regex ingressPathMatcher = new(defaultIngressIgnorePattern, RegexOptions.Compiled | RegexOptions.CultureInvariant, TimeSpan.FromSeconds(1)); options.Filter += httpContext => !ingressPathMatcher.IsMatch(httpContext.Request.Path); });
Alternatively, rather than using a regular expression, you can list the paths to ignore in the Filter property (Filter
is a Func<HttpContext, bool>?
):
using OpenTelemetry.Instrumentation.AspNetCore;
builder.Services.Configure<AspNetCoreTraceInstrumentationOptions>(options =>
{
options.Filter += httpContext =>
!httpContext.Request.Path.StartsWithSegments("/actuator", StringComparison.OrdinalIgnoreCase) &&
!httpContext.Request.Path.StartsWithSegments("/cloudfoundryapplication", StringComparison.OrdinalIgnoreCase);
});
Note
By default, the ASP.NET Core instrumentation does not filter out any requests. The alternative approach described can quickly prove unwieldy if there are many patterns to ignore, such as when listing many file types.
To learn more about ASP.NET Core instrumentation for OpenTelemetry see the OpenTelemetry documentation.
HttpClient
To instrument requests leaving the application through HttpClient
:
Add a reference to the
OpenTelemetry.Instrumentation.Http
NuGet package.Add the instrumentation to the
TracerProviderBuilder
by updating the existing call from above to:using OpenTelemetry.Trace; builder.Services.AddOpenTelemetry().WithTracing(tracing => tracing.AddHttpClientInstrumentation());
To replicate the Steeltoe setting
EgressIgnorePattern
(a Regex pattern describing which outgoing HTTP requests to ignore), configure theHttpClientTraceInstrumentationOptions
:using OpenTelemetry.Instrumentation.Http; using System.Text.RegularExpressions; builder.Services.Configure<HttpClientTraceInstrumentationOptions>(options => { const string defaultEgressIgnorePattern = "/api/v2/spans|/v2/apps/.*/permissions"; Regex egressPathMatcher = new(defaultEgressIgnorePattern, RegexOptions.Compiled | RegexOptions.CultureInvariant, TimeSpan.FromSeconds(1)); options.FilterHttpRequestMessage += httpRequestMessage => !egressPathMatcher.IsMatch(httpRequestMessage.RequestUri?.PathAndQuery ?? string.Empty); });
To learn more about HttpClient instrumentation for OpenTelemetry see the OpenTelemetry documentation.
Propagating Trace Context
By default, OpenTelemetry uses the W3C trace context for propagating traces. Some systems like Cloud Foundry may still be configured for the Zipkin standard of B3 propagation.
To use B3 propagation:
Add a reference to the
OpenTelemetry.Extensions.Propagators
NuGet package.Let the compiler know that the
B3Propagator
should come from the package reference you just added (rather than the deprecated class found inOpenTelemetry.Context.Propagation
):using B3Propagator = OpenTelemetry.Extensions.Propagators.B3Propagator;
Register a
CompositeTextMapPropagator
that includes theB3Propagator
andBaggagePropagator
:using OpenTelemetry; using OpenTelemetry.Context.Propagation; using OpenTelemetry.Trace; builder.Services.ConfigureOpenTelemetryTracerProvider((_, _) => { List<TextMapPropagator> propagators = [ new B3Propagator(), new BaggagePropagator() ]; Sdk.SetDefaultTextMapPropagator(new CompositeTextMapPropagator(propagators)); });
Exporting Distributed Traces
Previous versions of Steeltoe could automatically configure several different trace exporters, including:
- Zipkin
- OpenTelemetryProtocol (OTLP)
- Jaeger. The Jaeger exporter has been deprecated in favor of OTLP, which was only minimally configured by Steeltoe. For more information, see the OTLP exporter documentation.
Zipkin Server
To use the Zipkin Exporter:
Add a reference to the
OpenTelemetry.Exporter.Zipkin
NuGet package.Use the extension method
AddZipkinExporter
by updating the existing call from above to:builder.Services.AddOpenTelemetry().WithTracing(tracing => tracing.AddZipkinExporter());
The Zipkin options class ZipkinExporterOptions
works nearly the same as Steeltoe settings with the same names in previous releases:
using OpenTelemetry.Exporter;
builder.Services.Configure<ZipkinExporterOptions>(options =>
{
options.Endpoint = new Uri("http://localhost:9411");
options.MaxPayloadSizeInBytes = 4096;
options.UseShortTraceIds = true;
});
Tip
The Zipkin endpoint can also be set with the environment variable OTEL_EXPORTER_ZIPKIN_ENDPOINT
.