Skip to content

OpenTelemetry

Joinery.Dovetail.OpenTelemetry exposes OpenTelemetry’s configuration surface — the top-level IOpenTelemetryBuilder, the resource builder, and the tracer, meter and logger provider builders — as ordinary Dovetail joints. Any library can contribute an activity source, a meter, a log processor, or a resource attribute without taking a direct dependency on how the host wires up OpenTelemetry. Coming from LaunchPad.Telemetry? Its parity check maps each of its conventions to a joint here.

An application turns OpenTelemetry on by referencing Joinery.Dovetail.OpenTelemetry along with Joinery.Dovetail.Hosting. Joinery.Dovetail.Sdk adds Joinery.Dovetail.OpenTelemetry to every project that references OpenTelemetry or OpenTelemetry.Extensions.Hosting, and to every file-based app (dotnet run app.cs) along with OpenTelemetry.Extensions.Hosting; set DovetailEnableOpenTelemetry to false to leave OpenTelemetry to the application.

Joinery.Dovetail.Hosting then emits joints into the application that add OpenTelemetry to its services, apply every OpenTelemetry joint, and start it. OpenTelemetry only listens to activity sources and meters once its providers are resolved, which OpenTelemetry.Extensions.Hosting does from a hosted service, so what an application needs depends on whether its host runs hosted services:

HostRuns hosted servicesNeeds OpenTelemetry.Extensions.HostingStarted by
Generic host: workers, ASP.NET Coreyesyesits hosted service
Uno Platform (Uno.Extensions.Hosting)once started: NavigateAsync<TShell>() starts the host, ConfigureDovetail() doesn’tnoDovetailOpenTelemetryUnoStartJoint, once the host is built
.NET MAUI (MauiAppBuilder)nonoDovetailOpenTelemetryMauiStartJoint, once the app is built
Blazor WebAssemblyfrom .NET 11noDovetailOpenTelemetryWebAssemblyStartJoint, once the host is built (and on .NET 11 the hosted service too, when referenced)
  • With OpenTelemetry.Extensions.Hosting, whatever the host, DovetailOpenTelemetryJoint calls services.AddOpenTelemetry(). On .NET 11 it also traces ASP.NET Core’s Microsoft.AspNetCore activity source.
  • Without it, DovetailOpenTelemetryServicesJoint adds OpenTelemetry to the services directly, for the hosts that don’t need it.
  • A start joint resolves the meter, tracer and logger providers right after the host is built, as the hosted service does when the host starts, so OpenTelemetry runs on those hosts without a package the application wouldn’t otherwise use.
  • Only an application whose hosts all need OpenTelemetry.Extensions.Hosting (the generic host, with no Uno, MAUI or WebAssembly host) and that doesn’t reference it fails to compile, with an #error (CS1029) that says to add it. Packages that build on it, such as the Azure Monitor distro or .NET Aspire’s service defaults, bring it along, so they count.

OpenTelemetry runs in the browser too: in single-threaded Blazor WebAssembly its batch export processor and periodic metric reader run on tasks instead of threads.

Only applications and test projects get the joint, as an internal class: a library’s copy would be exported to every application that references it. A test project that imports its application’s joints gets the application’s copy as well as its own, and only the first one to run applies OpenTelemetry.

Every [DovetailExport] joint, and anything registered with a DovetailContextBuilder method such as ConfigureTracerProvider(...), is then applied with nothing else to call:

var host = await Host
.CreateApplicationBuilder(args)
.ConfigureDovetail(dovetail => dovetail.ConfigureTracerProvider((_, tracing) => tracing.AddConsoleExporter()));

Joinery.Dovetail.OpenTelemetry declares one <DovetailJoint>, OpenTelemetry, for OpenTelemetry.IOpenTelemetryBuilder: IOpenTelemetryJoint, its OpenTelemetryJointDelegate, the ConfigureOpenTelemetry(...) builder method and the ApplyOpenTelemetry extension that the emitted joint calls. Narrower interfaces derive from IOpenTelemetryJoint, each for one of OpenTelemetry’s builders:

Joint interfaceBuilderDovetailContextBuilder method
IOpenTelemetryJointOpenTelemetry.IOpenTelemetryBuilderConfigureOpenTelemetry
IResourceBuilderJointOpenTelemetry.Resources.ResourceBuilderConfigureResourceBuilder
ITracerProviderJointOpenTelemetry.Trace.TracerProviderBuilderConfigureTracerProvider
IMeterProviderBuilderJointOpenTelemetry.Metrics.MeterProviderBuilderConfigureMeterProvider
ILoggerProviderBuilderJointOpenTelemetry.Logs.LoggerProviderBuilderConfigureLoggerProvider

Since they’re all OpenTelemetry joints, they run together in the executor’s usual priority order. A library that only wants to register an activity source implements ITracerProviderJoint without knowing anything about WithMetrics, WithLogging or the resource — the same narrow, composable extension point the rest of Dovetail follows.

A library adds a System.Diagnostics.ActivitySource to tracing by implementing ITracerProviderJoint and marking the class [DovetailExport], the same pattern used for any other discovered Dovetail joint:

using System.Diagnostics;
using Joinery.Dovetail;
using Joinery.Dovetail.Attributes;
using Joinery.Dovetail.OpenTelemetry.Joints;
using OpenTelemetry.Trace;
namespace MyLibrary;
[DovetailExport]
public class MyLibraryTracing : ITracerProviderJoint
{
public static readonly ActivitySource ActivitySource = new("MyLibrary");
public void Register(IDovetailContext context, TracerProviderBuilder builder) =>
builder.AddSource(ActivitySource.Name);
}

Joinery.Dovetail.OpenTelemetry only opens the extension points — it does not ship any exporters or instrumentation. In particular, it does not include:

  • The console exporter, the OTLP exporter, or any other exporter package.
  • ASP.NET Core, HttpClient, or other instrumentation packages.
  • Any default sampler, resource attribute, or processor configuration of its own. The OpenTelemetry SDK’s default resource still applies, and already has the telemetry SDK attributes and the environment variable detector.

Consumers add exporters and instrumentation the same way they add anything else to a provider: by writing their own ITracerProviderJoint/IMeterProviderBuilderJoint/ILoggerProviderBuilderJoint implementation (or a plain Configure{Kind}(...) delegate) that calls the relevant AddConsoleExporter(), AddOtlpExporter(), AddAspNetCoreInstrumentation(), etc. This keeps Joinery.Dovetail.OpenTelemetry itself free of exporter dependencies, which also keeps it friendly to trimming/AOT — see Non-Goals in the OpenTelemetry design doc for the full rationale.