This walkthrough shows how to add observability to your .NET application using the OpenTelemetry .NET libraries and tools.
For details on how to assemble the base OTLP endpoint URL, see Export with OTLP. The URL should end in /api/v2/otlp
.
The access token for ingesting traces, logs, and metrics can be generated under Access Tokens.
Export with OTLP has more details on the format and the necessary access scopes.
For .NET, OpenTelemetry supports automatic and manual instrumentation (or a combination of both).
It's a good idea to start with automatic instrumentation and add manual instrumentation if the automatic approach doesn't work or doesn't provide enough information.
.NET automatic instrumentation can be configured either during development or later after deployment.
It is currently not possible to enrich automatically instrumented services with host-relevant information. To achieve this, you'd need to switch to manual instrumentation.
Install OpenTelemetry.Extensions.Hosting
.
dotnet add package OpenTelemetry.Extensions.Hosting
Install the appropriate instrumentation library for your .NET framework (full list available here).
dotnet add package OpenTelemetry.Instrumentation.[FRAMEWORK_NAME]
In addition to the instrumentation setup above, you also need to configure the relevant export parameters with environment variables. This includes the endpoint URL, the authentication token, and the temporality preference for metrics.
OTEL_EXPORTER_OTLP_ENDPOINT=[URL]OTEL_EXPORTER_OTLP_HEADERS="Authorization=Api-Token [TOKEN]"OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE=delta
The setup steps slightly differ depending on whether you instrument a plain .NET application or an ASP.NET application.
Install the following packages.
dotnet add package Microsoft.Extensions.Loggingdotnet add package OpenTelemetry.Extensions.Hostingdotnet add package OpenTelemetrydotnet add package OpenTelemetry.Apidotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol
Add the following using
statements to the startup class, which bootstraps your application.
using OpenTelemetry;using OpenTelemetry.Trace;using OpenTelemetry.Exporter;using OpenTelemetry.Metrics;using OpenTelemetry.Logs;using OpenTelemetry.Resources;using OpenTelemetry.Context.Propagation;using System.Diagnostics;using System.Diagnostics.Metrics;using Microsoft.Extensions.Logging;
Add these fields to your startup class, with the first two containing the access details, if you are using the OTLP export.
private static string DT_API_URL = ""; // TODO: Provide your SaaS/Managed URL hereprivate static string DT_API_TOKEN = ""; // TODO: Provide the OpenTelemetry-scoped access token hereprivate const string activitySource = "Dynatrace.DotNetApp.Sample"; // TODO: Provide a descriptive name for your application herepublic static readonly ActivitySource MyActivitySource = new ActivitySource(activitySource);private static ILoggerFactory loggerFactoryOT;
Instead of hardcoding the URL and token, you might also consider reading them from storage specific to your application framework (for example, environment variables or framework secrets).
Add the initOpenTelemetry
method to your startup class and invoke it as early as possible during your application startup. This initializes OpenTelemetry for the Dynatrace backend and creates default tracer and meter providers.
private static void initOpenTelemetry(IServiceCollection services){List<KeyValuePair<string, object>> dt_metadata = new List<KeyValuePair<string, object>>();foreach (string name in new string[] {"dt_metadata_e617c525669e072eebe3d0f08212e8f2.properties","/var/lib/dynatrace/enrichment/dt_metadata.properties","/var/lib/dynatrace/enrichment/dt_host_metadata.properties"}) {try {foreach (string line in System.IO.File.ReadAllLines(name.StartsWith("/var") ? name : System.IO.File.ReadAllText(name))) {var keyvalue = line.Split("=");dt_metadata.Add( new KeyValuePair<string, object>(keyvalue[0], keyvalue[1]));}}catch { }}Action<ResourceBuilder> configureResource = r => r.AddService(serviceName: "dotnet-quickstart") //TODO Replace with the name of your application.AddAttributes(dt_metadata);services.AddOpenTelemetry().ConfigureResource(configureResource).WithTracing(builder => {builder.SetSampler(new AlwaysOnSampler()).AddSource(MyActivitySource.Name).AddOtlpExporter(options =>{options.Endpoint = new Uri(Environment.GetEnvironmentVariable("DT_API_URL")+ "/v1/traces");options.Protocol = OpenTelemetry.Exporter.OtlpExportProtocol.HttpProtobuf;options.Headers = $"Authorization=Api-Token {Environment.GetEnvironmentVariable("DT_API_TOKEN")}";});}).WithMetrics(builder => {builder.AddMeter("my-meter").AddOtlpExporter((OtlpExporterOptions exporterOptions, MetricReaderOptions readerOptions) =>{exporterOptions.Endpoint = new Uri(Environment.GetEnvironmentVariable("DT_API_URL")+ "/v1/metrics");exporterOptions.Headers = $"Authorization=Api-Token {Environment.GetEnvironmentVariable("DT_API_TOKEN")}";exporterOptions.Protocol = OpenTelemetry.Exporter.OtlpExportProtocol.HttpProtobuf;readerOptions.TemporalityPreference = MetricReaderTemporalityPreference.Delta;});});var resourceBuilder = ResourceBuilder.CreateDefault();configureResource!(resourceBuilder);loggerFactoryOT = LoggerFactory.Create(builder => {builder.AddOpenTelemetry(options => {options.SetResourceBuilder(resourceBuilder).AddOtlpExporter(options => {options.Endpoint = new Uri(Environment.GetEnvironmentVariable("DT_API_URL")+ "/v1/logs");options.Headers = $"Authorization=Api-Token {Environment.GetEnvironmentVariable("DT_API_TOKEN")}";options.ExportProcessorType = OpenTelemetry.ExportProcessorType.Batch;options.Protocol = OtlpExportProtocol.HttpProtobuf;});}).AddConsole();});Sdk.CreateTracerProviderBuilder().SetSampler(new AlwaysOnSampler()).AddSource(MyActivitySource.Name).ConfigureResource(configureResource);// add-logging}
The file read operations, parsing the dt_metadata
files in the example code, attempt to read the OneAgent data files to enrich the OTLP request and ensure that all relevant topology information is available within Dynatrace.
Using MyActivitySource
from the setup step, we can now start new activities (traces):
using var activity = Startup.MyActivitySource.StartActivity("Call to /myendpoint", ActivityKind.Consumer, parentContext.ActivityContext);activity?.SetTag("http.method", "GET");activity?.SetTag("net.protocol.version", "1.1");
In the above code, we:
The activity will be automatically set as the current and active span until the execution flow leaves the current method scope. Subsequent activities will automatically become child spans.
To instantiate new metric instruments, we first need a meter object.
private static readonly Meter meter = new Meter("my-meter", "1.0.0"); //TODO Replace with the name of your meter
With meter
, we can now create individual instruments, such as a counter.
private static readonly Counter<long> counter = meter.CreateCounter<long>("request_counter");
We can now invoke the Add()
method of counter
to record new values with our counter and save additional attributes (for example, action.type
).
counter.Add(1, new("ip", "an ip address here"), new("some other key", "some other value"));
With the loggerFactoryOT
variable, we initialized under Setup, we can now create individual logger instances, which will pass logged information straight to the configured OpenTelemetry endpoint at Dynatrace.
var logger = loggerFactoryOT.CreateLogger<Startup>();services.AddSingleton<ILoggerFactory>(loggerFactoryOT);services.AddSingleton(logger);logger.LogInformation(eventId: 123, "Log line");
Context propagation is particularly important when network calls (for example, REST) are involved.
If you are using automatic instrumentation and your networking libraries are covered by automatic instrumentation, this will be automatically taken care of by the instrumentation libraries. Otherwise, your code needs to take this into account.
In the following example, we assume that we have received a network call via System.Web.HttpRequest
and we define a CompositeTextMapPropagator
instance to fetch the context information from the HTTP headers. We then pass that instance to Extract()
, returning the context object, which allows us to continue the previous trace with our spans.
private CompositeTextMapPropagator propagator = new CompositeTextMapPropagator(new TextMapPropagator[] {new TraceContextPropagator(),new BaggagePropagator(),});private static readonly Func<HttpRequest, string, IEnumerable<string>> valueGetter = (request, name) => request.Headers[name];var parentContext = propagator.Extract(default, HttpContext.Request, valueGetter);using var activity = MyActivitySource.StartActivity("my-span", ActivityKind.Consumer, parentContext.ActivityContext);
In the following example, we send a REST request to another service and provide our existing context as part of the HTTP headers of our request.
To do so, we define a TextMapPropagator
instance, which adds the respective information. Once we have instantiated our REST object, we pass it, along with the context and the setter instance, to Inject()
, which will add the necessary headers to the request.
private CompositeTextMapPropagator propagator = new CompositeTextMapPropagator(new TextMapPropagator[] {new TraceContextPropagator(),new BaggagePropagator()});private static Action<HttpRequestMessage, string, string> _headerValueSetter => (request, name, value) => {request.Headers.Remove(name);request.Headers.Add(name, value);};propagator.Inject(new PropagationContext(activity!.Context, Baggage.Current), request, _headerValueSetter);
While Dynatrace automatically captures all OpenTelemetry attributes, only attribute values specified in the allowlist are stored and displayed in the Dynatrace web UI. This prevents accidental storage of personal data, so you can meet your privacy requirements and control the amount of monitoring data stored.
To view your custom attributes, you need to allow them in the Dynatrace web UI first. To learn how to configure attribute storage and masking, see Attribute redaction.
Once you have finished the instrumentation of your application, perform a couple of test actions to create and send demo traces, metrics, and logs and verify that they were correctly ingested into Dynatrace.
To do that for traces, go to Distributed Traces and select the Ingested traces tab. If you use OneAgent, select PurePaths instead.
For metrics and logs, go to Metrics or Logs.