From 165b6f4ca427abbfce8d0a381e48ce13565c7298 Mon Sep 17 00:00:00 2001 From: Mikel Blanchard Date: Thu, 10 Nov 2022 09:44:29 -0800 Subject: [PATCH] [Repo] TracerProviderBuilder dependency injection docs updates (#3869) * Added content for the "Guidance for library authors" section. * Code review. * Spellcheck! * Code review. * Code review. * Code review. * Bug fix. * Lint. * Tweaks. Co-authored-by: Cijo Thomas Co-authored-by: Utkarsh Umesan Pillai --- docs/trace/customizing-the-sdk/README.md | 13 +- docs/trace/extending-the-sdk/README.md | 279 ++++++++++++++++++++++- 2 files changed, 281 insertions(+), 11 deletions(-) diff --git a/docs/trace/customizing-the-sdk/README.md b/docs/trace/customizing-the-sdk/README.md index 5c76b5c5b..6da254f7c 100644 --- a/docs/trace/customizing-the-sdk/README.md +++ b/docs/trace/customizing-the-sdk/README.md @@ -378,17 +378,15 @@ For the below examples imagine an exporter with this constructor: ```csharp public class MyCustomExporter : BaseExporter { - public MyCustomExporter( - ILogger logger, - MyCustomService myCustomService) + public MyCustomExporter(MyCustomService myCustomService) { // Implementation not important } } ``` -We want to inject the `ILogger` and `MyCustomService` -dependencies into our `MyCustomExporter` instance. +We want to inject `MyCustomService` dependency into our `MyCustomExporter` +instance. #### Using Sdk.CreateTracerProviderBuilder() @@ -399,7 +397,6 @@ To register `MyCustomExporter` and `MyCustomService` we can use the using var tracerProvider = Sdk.CreateTracerProviderBuilder() .ConfigureServices(services => { - services.AddLogging(); services.AddSingleton(); }) .AddExporter(ExportProcessorType.Batch) @@ -486,10 +483,6 @@ shutdown. `IServiceCollection` during `ConfigureBuilder` because the `IServiceProvider` has already been created. -### Guidance for library authors - -// TODO: Add details here - ## Configuration files and environment variables // TODO: Add details here diff --git a/docs/trace/extending-the-sdk/README.md b/docs/trace/extending-the-sdk/README.md index d9324d218..fd3085551 100644 --- a/docs/trace/extending-the-sdk/README.md +++ b/docs/trace/extending-the-sdk/README.md @@ -1,10 +1,13 @@ # Extending the OpenTelemetry .NET SDK +Quick links: + * [Building your own exporter](#exporter) * [Building your own instrumentation library](#instrumentation-library) * [Building your own processor](#processor) * [Building your own sampler](#sampler) * [Building your own resource detector](#resource-detector) +* [Registration extension method guidance for library authors](#registration-extension-method-guidance-for-library-authors) * [References](#references) ## Exporter @@ -62,7 +65,9 @@ A demo exporter which simply writes activity name to the console is shown Apart from the exporter itself, you should also provide extension methods as shown [here](./MyExporterExtensions.cs). This allows users to add the Exporter -to the `TracerProvider` as shown in the example [here](./Program.cs). +to the `TracerProvider` as shown in the example [here](./Program.cs). See +[here](#registration-extension-method-guidance-for-library-authors) for more +detailed extension method guidance. ### Exporting Activity Status @@ -334,6 +339,278 @@ Custom resource detectors can be implemented: A demo ResourceDetector is shown [here](./MyResourceDetector.cs). +## Registration extension method guidance for library authors + +**Note** This information applies to the OpenTelemetry SDK version 1.4.0 and +newer only. + +Library authors are encouraged to provide extension methods users may call to +register custom OpenTelemetry components into their `TracerProvider`s. These +extension methods can target either the `TracerProviderBuilder` or the +`IServiceCollection` classes. Both of these patterns are described below. + +When providing registration extensions: + +* **DO** support the [.NET Options + pattern](https://learn.microsoft.com/dotnet/core/extensions/options) and + **DO** support [named + options](https://learn.microsoft.com/dotnet/core/extensions/options#named-options-support-using-iconfigurenamedoptions). + The Options pattern allows users to bind + [configuration](https://learn.microsoft.com/dotnet/core/extensions/configuration) + to options classes and provides extension points for working with instances as + they are created. Multiple providers may exist in the same application for a + single configuration and multiple components (for example exporters) may exist + in the same provider. Named options help users target configuration to + specific components. + + * Use the + [Configure](https://learn.microsoft.com/dotnet/api/microsoft.extensions.dependencyinjection.optionsservicecollectionextensions.configure#microsoft-extensions-dependencyinjection-optionsservicecollectionextensions-configure-1(microsoft-extensions-dependencyinjection-iservicecollection-system-string-system-action((-0)))) + extension to register configuration callbacks for a given name. + + * Use the + [IOptionsMonitor<T>.Get](https://learn.microsoft.com/dotnet/api/microsoft.extensions.options.ioptionsmonitor-1.get) + method to access options class instances by name. + +* **DO** throw exceptions for issues that prevent the component being registered + from starting. The OpenTelemetry SDK is allowed to crash if it cannot be + started. It **MUST NOT** crash once running. + +### TracerProviderBuilder extension methods + +When registering pipeline components (for example samplers, exporters, or +resource detectors) it is recommended to use the `TracerProviderBuilder` as the +target type for registration extension methods. These extensions will be highly +discoverable for users interacting with the `TracerProviderBuilder` in their IDE +of choice. + +The following example shows how to register a custom exporter with named options +support using a `TracerProviderBuilder` extension. + +```csharp +using System.Diagnostics; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; +using MyLibrary; +using OpenTelemetry; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; + +namespace OpenTelemetry.Trace +{ + public static class MyLibraryTracerProviderBuilderRegistrationExtensions + { + public static TracerProviderBuilder AddMyLibraryExporter( + this TracerProviderBuilder builder, + string? name = null, + Action? configureExporterOptions = null, + Action? configureBatchProcessorOptions = null) + { + ArgumentNullException.ThrowIfNull(builder); + + // Support named options. + name ??= Options.DefaultName; + + builder.ConfigureServices(services => + { + if (configureExporterOptions != null) + { + // Support configuration through Options API. + services.Configure(name, configureExporterOptions); + } + + if (configureBatchProcessorOptions != null) + { + // Support configuration through Options API. + services.Configure( + name, + o => configureBatchProcessorOptions(o.BatchExportProcessorOptions)); + } + + // Register custom service as a singleton. + services.TryAddSingleton(); + }); + + builder.ConfigureBuilder((sp, builder) => + { + // Retrieve MyExporterOptions instance using name. + var options = sp.GetRequiredService>().Get(name); + + // Retrieve MyCustomService singleton. + var myCustomService = sp.GetRequiredService(); + + // Registers MyCustomExporter with a batch processor. + builder.AddExporter( + ExportProcessorType.Batch, + new MyCustomExporter(options, myCustomService), + name, + configure: null); + }); + + // Return builder for call chaining. + return builder; + } + } +} + +namespace MyLibrary +{ + // Options class can be bound to IConfiguration or configured by code. + public class MyExporterOptions + { + public Uri? IngestionUri { get; set; } + } + + internal sealed class MyCustomExporter : BaseExporter + { + public MyCustomExporter( + MyExporterOptions options, + MyCustomService myCustomService) + { + // Implementation not shown. + } + + public override ExportResult Export(in Batch batch) + { + // Implementation not shown. + + return ExportResult.Success; + } + } + + internal sealed class MyCustomService + { + // Implementation not shown. + } +} +``` + +When providing `TracerProviderBuilder` registration extensions: + +* **DO** Use the `OpenTelemetry.Trace` namespace for `TracerProviderBuilder` + registration extensions to help with discoverability. + +* **DO** Return the `TracerProviderBuilder` passed in to support call chaining + of registration methods. + +* **DO** Use the `TracerProviderBuilder.ConfigureServices` extension method to + register dependent services. + +* **DO** Use the `TracerProviderBuilder.ConfigureBuilder` extension method to + peform configuration once the final `IServiceProvider` is available. + +### IServiceCollection extension methods + +When registering instrumentation or listening to telemetry in a library +providing other features it is recommended to use the `IServiceCollection` as +the target type for registration extension methods. + +The following example shows how a library might enable tracing and metric +support using an `IServiceCollection` extension by calling +`ConfigureOpenTelemetryTracing`. + +```csharp +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; +using MyLibrary; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; + +namespace Microsoft.Extensions.DependencyInjection +{ + public static class MyLibraryServiceCollectionRegistrationExtensions + { + public static IServiceCollection AddMyLibrary( + this IServiceCollection services, + string? name = null, + Action? configure = null) + { + ArgumentNullException.ThrowIfNull(services); + + // Register library services. + services.TryAddSingleton(); + + // Support named options. + name ??= Options.Options.DefaultName; + + if (configure != null) + { + // Support configuration through Options API. + services.Configure(name, configure); + } + + // Configure OpenTelemetry tracing. + services.ConfigureOpenTelemetryTracing(builder => builder.ConfigureBuilder((sp, builder) => + { + var options = sp.GetRequiredService>().Get(name); + if (options.EnableTracing) + { + builder.AddSource("MyLibrary"); + } + })); + + // Configure OpenTelemetry metrics. + services.ConfigureOpenTelemetryMetrics(builder => builder.ConfigureBuilder((sp, builder) => + { + var options = sp.GetRequiredService>().Get(name); + if (options.EnableMetrics) + { + builder.AddMeter("MyLibrary"); + } + })); + + return services; + } + } +} + +namespace MyLibrary +{ + // Options class can be bound to IConfiguration or configured by code. + public class MyLibraryOptions + { + public bool EnableTracing { get; set; } + + public bool EnableMetrics { get; set; } + } + + internal sealed class MyLibraryService : IMyLibraryService + { + // Implementation not shown. + } + + public interface IMyLibraryService + { + // Implementation not shown. + } +} +``` + +The benefit to using the `IServiceCollection` style is users only need to call a +single `AddMyLibrary` extension to configure the library itself and optionally +turn on OpenTelemetry integration for multiple signals (tracing & metrics in +this case). + +**Note** `ConfigureOpenTelemetryTracing` does not automatically start +OpenTelemetry. The host is responsible for either calling +`AddOpenTelemetryTracing` in the +[OpenTelemetry.Extensions.Hosting](../../../src/OpenTelemetry.Extensions.Hosting/README.md) +package, calling `Build` when using the `Sdk.CreateTracerProviderBuilder` +method, or by accessing the `TracerProvider` from the `IServiceCollection` where +`ConfigureOpenTelemetryTracing` was performed. + +When providing `IServiceCollection` registration extensions: + +* **DO** Use the `Microsoft.Extensions.DependencyInjection` namespace for + `IServiceCollection` registration extensions to help with discoverability. + +* **DO** Return the `IServiceCollection` passed in to support call chaining of + registration methods. + +* **DO** Use the `IServiceCollection` directly to register dependent services. + +* **DO** Use the `TracerProviderBuilder.ConfigureBuilder` extension method to + peform configuration once the final `IServiceProvider` is available. + ## References * [Exporter