# Customizing OpenTelemetry .NET SDK for Tracing ## TracerProvider As shown in the [Getting Started - ASP.NET Core Application](../getting-started-aspnetcore/README.md) and [Getting Started - Console Application](../getting-started-console/README.md) docs, OpenTelemetry tracing is managed by a [`TracerProvider`](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/sdk.md#tracer-provider) instance configured using the `TracerProviderBuilder` API. `TracerProviderBuilder` exposes various methods to configure the provider (ex: `SetSampler`, `AddProcessor`, etc.) which are explained in subsequent sections of this document. It is also common for library authors to target `TracerProviderBuilder` for extension methods which help configure SDK plug-in components. ## Building a TracerProvider There are two different ways to create a `TracerProviderBuilder`. ### Using OpenTelemetry.Extensions.Hosting For [ASP.NET Core](https://learn.microsoft.com/aspnet/core/fundamentals/host/web-host) and [.NET Generic](https://learn.microsoft.com/dotnet/core/extensions/generic-host) host users, helper extensions are provided in the [OpenTelemetry.Extensions.Hosting](../../../src/OpenTelemetry.Extensions.Hosting/README.md) package to simplify configuration and management of the `TracerProvider`. ```csharp using OpenTelemetry.Trace; var appBuilder = WebApplication.CreateBuilder(args); appBuilder.Services.AddOpenTelemetry() .WithTracing(builder => builder.AddConsoleExporter()); ``` > [!NOTE] > The [AddOpenTelemetry](../../../src/OpenTelemetry.Extensions.Hosting/README.md#extension-method-reference) extension automatically starts and stops the `TracerProvider` with the host. ### Using Sdk.CreateTracerProviderBuilder `Sdk.CreateTracerProviderBuilder()` is provided on all runtimes to create `TracerProvider`s when either hosting is not available or multiple providers are required. Call `Sdk.CreateTracerProviderBuilder()` to obtain a builder and then call `Build()` once configuration is done to retrieve the `TracerProvider` instance. > [!NOTE] > Once built changes to `TracerProvider` configuration are not allowed, with the exception of adding more processors. In most cases a single `TracerProvider` is created at the application startup, and is disposed when application shuts down. The snippet below shows how to build a basic `TracerProvider` and dispose it at the end of the application. This will create a provider with default configuration, and is not particularly useful. The subsequent sections shows how to build a more useful provider. ```csharp using OpenTelemetry; using OpenTelemetry.Trace; var tracerProvider = Sdk.CreateTracerProviderBuilder().Build(); // .... // Dispose at application shutdown tracerProvider.Dispose() ``` ## TracerProvider configuration `TracerProvider` holds the tracing configuration, which includes the following: 1. The list of `ActivitySource`s (aka `Tracer`s) from which traces are collected. 2. The list of instrumentations enabled via [InstrumentationLibrary](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/glossary.md#instrumentation-library). 3. The list of [Processors](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/sdk.md#span-processor), including exporting processors which exports traces to [Exporters](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/sdk.md#span-exporter) 4. The [Resource](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/resource/sdk.md) associated with the traces. 5. The [Sampler](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/sdk.md#sampler) to be used. ### Activity Source `ActivitySource` denotes a [`Tracer`](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/api.md#tracer), which is used to create activities. The SDK follows an explicit opt-in model for listening to activity sources. i.e, by default, it listens to no sources. Every activity source which produce telemetry must be explicitly added to the tracer provider to start collecting traces from them. `AddSource` method on `TracerProviderBuilder` can be used to add a `ActivitySource` to the provider. The name of the `ActivitySource` (case-insensitive) must be the argument to this method. Multiple `AddSource` can be called to add more than one source. It also supports wildcard subscription model as well. It is not possible to add sources *after* the provider is built, by calling the `Build()` method on the `TracerProviderBuilder`. The snippet below shows how to add activity sources to the provider. ```csharp using OpenTelemetry; using OpenTelemetry.Trace; var tracerProvider = Sdk.CreateTracerProviderBuilder() // The following subscribes to activities from Activity Source // named "MyCompany.MyProduct.MyLibrary" only. .AddSource("MyCompany.MyProduct.MyLibrary") // The following subscribes to activities from all Activity Sources // whose name starts with "AbcCompany.XyzProduct.". .AddSource("AbcCompany.XyzProduct.*") .Build(); ``` See [Program.cs](./Program.cs) for complete example. > [!NOTE] > A common mistake while configuring `TracerProvider` is forgetting to add all `ActivitySources` to the provider. It is recommended to leverage the wild card subscription model where it makes sense. For example, if your application is expecting to enable tracing from a number of libraries from a company "Abc", the you can use `AddSource("Abc.*")` to enable all sources whose name starts with "Abc.". ### Instrumentation While the OpenTelemetry API can be used to instrument any library manually, [Instrumentation Libraries](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/overview.md#instrumentation-libraries) are available for a lot of commonly used libraries. Such instrumentations can be added to the `TracerProvider`. It is not required to attach the instrumentation to the provider, unless the life cycle of the instrumentation must be managed by the provider. If the instrumentation must be activated/shutdown/disposed along with the provider, then the instrumentation must be added to the provider. Typically, the instrumentation libraries provide extension methods on `TracerProviderBuilder` to allow adding them to the `TracerProvider`. Please refer to corresponding documentation of the instrumentation library to know the exact method name. Follow [this](../extending-the-sdk/README.md#instrumentation-library) document to learn about the instrumentation libraries shipped from this repo and writing custom instrumentation libraries. ### Processors & Exporters [Processors](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/sdk.md#span-processor) expose hooks for start and end processing of `Activity` instances. If no processors are configured then traces are simply dropped by the SDK. The `AddProcessor` method on `TracerProviderBuilder` is provided to add a processor to the SDK pipeline. There can be any number of processors added to the provider and they are invoked in the same order as they are added. Unlike `Sampler` and `Resource`, processors can be added to the provider even *after* it is built. [Exporters](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/sdk.md#span-exporter) expose hooks for exporting batches of completed `Activity` instances (a batch may contain a single or many records) and are called by processors. Two base processor classes `SimpleExportProcessor` & `BatchExportProcessor` are provided to support invoking exporters through the processor pipeline and implement the standard behaviors prescribed by the OpenTelemetry specification. > [!NOTE] > The SDK only ever invokes processors and has no direct knowledge of any registered exporters. #### Processor Configuration The snippet below shows how to add processors to the provider before and after it is built. ```csharp using OpenTelemetry; using OpenTelemetry.Trace; var tracerProvider = Sdk.CreateTracerProviderBuilder() .AddProcessor(new MyProcessor1()) .AddProcessor(new MyProcessor2())) .Build(); // Processors can be added to provider even after it is built. // Only those traces which are emitted after this line, will be sent to it. tracerProvider.AddProcessor(new MyProcessor3()); ``` > [!NOTE] > The order of processor registration is important. Each processor added is invoked in order by the SDK. For example if a simple exporting processor is added before an enrichment processor the exported data will not contain anything added by the enrichment because it happens after the export. <!-- This comment is to make sure the two notes above and below are not merged --> > [!NOTE] > A `TracerProvider` assumes ownership of **all** processors added to it. This means that the provider will call the `Shutdown` method on all registered processors when it is shutting down and call the `Dispose` method on all registered processors when it is disposed. If multiple providers are being set up in an application then separate instances of processors **MUST** be registered on each provider. Otherwise shutting down one provider will cause the shared processor(s) in other providers to be shut down as well which may lead to undesired results. Processors can be used for enriching, exporting, and/or filtering telemetry. To enrich telemetry, users may write custom processors overriding the `OnStart` and/or `OnEnd` methods (as needed) to implement custom logic to change the data before it is passed to the next processor in the pipeline. For exporting purposes, the SDK provides the following built-in processors: * [BatchExportProcessor<T>](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/sdk.md#batching-processor) : This is an exporting processor which batches the telemetry before sending to the configured exporter. The following environment variables can be used to override the default values of the `BatchExportActivityProcessorOptions`. | Environment variable | `BatchExportActivityProcessorOptions` property | | -------------------------------- | ---------------------------------------------- | | `OTEL_BSP_SCHEDULE_DELAY` | `ScheduledDelayMilliseconds` | | `OTEL_BSP_EXPORT_TIMEOUT` | `ExporterTimeoutMilliseconds` | | `OTEL_BSP_MAX_QUEUE_SIZE` | `MaxQueueSize` | | `OTEL_BSP_MAX_EXPORT_BATCH_SIZE` | `MaxExportBatchSizeEnvVarKey` | * [SimpleExportProcessor<T>](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/sdk.md#simple-processor) : This is an exporting processor which passes telemetry to the configured exporter immediately without any batching. > [!NOTE] > A special processor [CompositeProcessor<T>](../../../src/OpenTelemetry/CompositeProcessor.cs) is used by the SDK to chain multiple processors together and may be used as needed by users to define sub-pipelines. <!-- This comment is to make sure the two notes above and below are not merged --> > [!NOTE] > The processors shipped from this SDK are generic implementations and support tracing and logging by implementing `Activity` and `LogRecord` respectively. Follow [this](../extending-the-sdk/README.md#processor) document to learn about writing custom processors. #### Exporter Configuration The snippet below shows how to add export processors to the provider before it is built. ```csharp using OpenTelemetry; using OpenTelemetry.Trace; var tracerProvider = Sdk.CreateTracerProviderBuilder() .AddProcessor(new BatchActivityExportProcessor(new MyExporter1())) .AddProcessor(new SimpleActivityExportProcessor(new MyExporter2())) .Build(); ``` It is also common for exporters to provide their own extensions to simplify registration. The snippet below shows how to add the [OtlpExporter](../../../src/OpenTelemetry.Exporter.OpenTelemetryProtocol/README.md) to the provider before it is built. ```csharp using OpenTelemetry; using OpenTelemetry.Trace; var tracerProvider = Sdk.CreateTracerProviderBuilder() .AddOtlpExporter() .Build(); ``` Follow [this](../extending-the-sdk/README.md#exporter) document to learn about writing custom exporters. ### Resource [Resource](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/resource/sdk.md) is the immutable representation of the entity producing the telemetry. If no `Resource` is explicitly configured, the [default](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/resource/README.md#semantic-attributes-with-sdk-provided-default-value) is to use a resource indicating this [Service](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/resource/README.md#service) and [Telemetry SDK](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/resource/README.md#telemetry-sdk). The `ConfigureResource` method on `TracerProviderBuilder` can be used to configure the resource on the provider. `ConfigureResource` accepts an `Action` to configure the `ResourceBuilder`. Multiple calls to `ConfigureResource` can be made. When the provider is built, it builds the final `Resource` combining all the `ConfigureResource` calls. There can only be a single `Resource` associated with a provider. It is not possible to change the resource builder *after* the provider is built, by calling the `Build()` method on the `TracerProviderBuilder`. `ResourceBuilder` offers various methods to construct resource comprising of attributes from various sources. For example, `AddService()` adds [Service](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/resource/README.md#service) resource. `AddAttributes` can be used to add any additional attribute to the `Resource`. It also allows adding `ResourceDetector`s. It is recommended to model attributes that are static throughout the lifetime of the process as Resources, instead of adding them as attributes(tags) on each `Activity`. Follow [this](../../resources/README.md#resource-detector) document to learn about writing custom resource detectors. The snippet below shows configuring the `Resource` associated with the provider. ```csharp using OpenTelemetry; using OpenTelemetry.Resources; using OpenTelemetry.Trace; var tracerProvider = Sdk.CreateTracerProviderBuilder() .ConfigureResource(r => r.AddAttributes(new List<KeyValuePair<string, object>> { new KeyValuePair<string, object>("static-attribute1", "v1"), new KeyValuePair<string, object>("static-attribute2", "v2"), })) .ConfigureResource(resourceBuilder => resourceBuilder.AddService("service-name")) .Build(); ``` It is also possible to configure the `Resource` by using following environmental variables: | Environment variable | Description | | -------------------------- | -------------------------------------------------- | | `OTEL_RESOURCE_ATTRIBUTES` | Key-value pairs to be used as resource attributes. See the [Resource SDK specification](https://github.com/open-telemetry/opentelemetry-specification/blob/v1.5.0/specification/resource/sdk.md#specifying-resource-information-via-an-environment-variable) for more details. | | `OTEL_SERVICE_NAME` | Sets the value of the `service.name` resource attribute. If `service.name` is also provided in `OTEL_RESOURCE_ATTRIBUTES`, then `OTEL_SERVICE_NAME` takes precedence. | ### Samplers [Samplers](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/sdk.md#sampler) are used to control the noise and overhead introduced by OpenTelemetry by reducing the number of samples of traces collected and sent to the processors. If no sampler is explicitly configured, the default is to use `ParentBased(root=AlwaysOn)`. `SetSampler` method on `TracerProviderBuilder` can be used to set sampler. Only one sampler can be associated with a provider. If `SetSampler` is called multiple times, the last one wins. Also, it is not possible to change the sampler *after* the provider is built, by calling the `Build()` method on the `TracerProviderBuilder`. The snippet below shows configuring a custom sampler to the provider. ```csharp using OpenTelemetry; using OpenTelemetry.Trace; var tracerProvider = Sdk.CreateTracerProviderBuilder() .SetSampler(new TraceIdRatioBasedSampler(0.25)) .Build(); ``` If using `1.8.0-rc.1` or newer it is also possible to configure the sampler by using the following environmental variables: | Environment variable | Description | | -------------------------- | -------------------------------------------------- | | `OTEL_TRACES_SAMPLER` | Sampler to be used for traces. See the [General SDK Configuration specification](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/configuration/sdk-environment-variables.md#general-sdk-configuration) for more details. | | `OTEL_TRACES_SAMPLER_ARG` | String value to be used as the sampler argument. | The supported values for `OTEL_TRACES_SAMPLER` are: * `always_off` * `always_on` * `traceidratio` * `parentbased_always_on`, * `parentbased_always_off` * `parentbased_traceidratio` The options `traceidratio` and `parentbased_traceidratio` may have the sampler probability configured via the `OTEL_TRACES_SAMPLER_ARG` environment variable. Follow [this](../extending-the-sdk/README.md#sampler) document to learn about writing custom samplers. ## Context Propagation The OpenTelemetry API exposes a method to obtain the default propagator which is no-op, by default. This SDK replaces the no-op with a [composite propagator](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/context/api-propagators.md#composite-propagator) containing the Baggage Propagator and TraceContext propagator. This default propagator can be overridden with the below snippet. ```csharp using OpenTelemetry; Sdk.SetDefaultTextMapPropagator(new MyCustomPropagator()); ``` ## Dependency injection support > [!NOTE] > This information applies to the OpenTelemetry SDK version 1.4.0 and newer only. The SDK implementation of `TracerProviderBuilder` is backed by an `IServiceCollection` and supports a wide range of APIs to enable what is generally known as [dependency injection](https://learn.microsoft.com/dotnet/core/extensions/dependency-injection). ### Dependency injection examples For the below examples imagine a processor with this constructor: ```csharp public class MyCustomProcessor : BaseProcessor<Activity> { public MyCustomProcessor(MyCustomService myCustomService) { // Implementation not important } } ``` We want to inject `MyCustomService` dependency into our `MyCustomProcessor` instance. #### Using Sdk.CreateTracerProviderBuilder() To register `MyCustomProcessor` and `MyCustomService` we can use the `ConfigureServices` and `AddProcessor` methods: ```csharp using var tracerProvider = Sdk.CreateTracerProviderBuilder() .ConfigureServices(services => { services.AddSingleton<MyCustomService>(); }) .AddProcessor<MyCustomProcessor>() .Build(); ``` When using the `Sdk.CreateTracerProviderBuilder` method the `TracerProvider` owns its own `IServiceCollection`. It will only be able to see services registered into that collection. > [!NOTE] > It is important to correctly manage the lifecycle of the `TracerProvider`. See [Building a TracerProvider](#building-a-tracerprovider) for details. #### Using the OpenTelemetry.Extensions.Hosting package > [!NOTE] > If you are authoring an [ASP.NET Core application](https://learn.microsoft.com/aspnet/core/fundamentals/host/web-host) or using the [.NET Generic Host](https://learn.microsoft.com/dotnet/core/extensions/generic-host) the [OpenTelemetry.Extensions.Hosting](../../../src/OpenTelemetry.Extensions.Hosting/README.md) package is the recommended mechanism. ```csharp var appBuilder = WebApplication.CreateBuilder(args); appBuilder.Services.AddSingleton<MyCustomService>(); appBuilder.Services.AddOpenTelemetry() .WithTracing(builder => builder .AddProcessor<MyCustomProcessor>()); ``` When using the `AddOpenTelemetry` & `WithTracing` extension methods the `TracerProvider` does not own its `IServiceCollection` and instead registers into an existing collection (typically the collection used is the one managed by the application host). The `TracerProviderBuilder` will be able to access all services registered into that collection. For lifecycle management, the `AddOpenTelemetry` registers an [IHostedService](https://learn.microsoft.com/dotnet/api/microsoft.extensions.hosting.ihostedservice) which is used to automatically start the `TracerProvider` when the host starts and the host will automatically shutdown and dispose the `TracerProvider` when it is shutdown. > [!NOTE] > Multiple calls to `WithTracing` will configure the same `TracerProvider`. Only a single `TraceProvider` may exist in an `IServiceCollection` \ `IServiceProvider`. ### Dependency injection TracerProviderBuilder extension method reference * `AddInstrumentation<T>`: Adds instrumentation of type `T` into the `TracerProvider`. * `AddInstrumentation<T>(Func<IServiceProvider, T> instrumentationFactory)`: Adds instrumentation of type `T` into the `TracerProvider` using a factory function to create the instrumentation instance. * `AddProcessor<T>`: Adds a processor of type `T` (must derive from `BaseProcessor<Activity>`) into the `TracerProvider`. * `AddProcessor(Func<IServiceProvider, BaseProcessor<Activity>> implementationFactory)`: Adds a processor into the `TracerProvider` using a factory function to create the processor instance. * `ConfigureServices`: Registers a callback function for configuring the `IServiceCollection` used by the `TracerProviderBuilder`. > [!NOTE] > `ConfigureServices` may only be called before the `IServiceProvider` has been created after which point services can no longer be added. * `SetSampler<T>`: Register type `T` (must derive from `Sampler`) as the sampler for the `TracerProvider`. * `SetSampler(Func<IServiceProvider, Sampler> implementationFactory)`: Adds a sampler into the `TracerProvider` using a factory function to create the sampler instance. > [!NOTE] > The factory functions accepting `IServiceProvider` may always be used regardless of how the SDK is initialized. When using an external service collection (ex: `appBuilder.Services.AddOpenTelemetry()`), as is common in ASP.NET Core hosts, the `IServiceProvider` will be the instance shared and managed by the host. When using "Sdk.Create" functions, as is common in .NET Framework hosts, the provider creates its own `IServiceCollection` and will build an `IServiceProvider` from it to make available to extensions. ## Configuration files and environment variables > [!NOTE] > This information applies to the OpenTelemetry SDK version 1.4.0 and newer only. The OpenTelemetry .NET SDK integrates with the standard [configuration](https://learn.microsoft.com/dotnet/core/extensions/configuration) and [options](https://learn.microsoft.com/dotnet/core/extensions/options) patterns provided by .NET. The configuration pattern supports building a composited view of settings from external sources and the options pattern helps use those settings to configure features by binding to simple classes. ### How to set up configuration The following sections describe how to set up configuration based on the host and OpenTelemetry API being used. #### Using .NET hosts with the OpenTelemetry.Extensions.Hosting package [ASP.NET Core](https://learn.microsoft.com/aspnet/core/fundamentals/host/web-host) and [.NET Generic](https://learn.microsoft.com/dotnet/core/extensions/generic-host) host users using the [OpenTelemetry.Extensions.Hosting](../../../src/OpenTelemetry.Extensions.Hosting/README.md) package do not need to do anything extra to enable `IConfiguration` support. The OpenTelemetry SDK will automatically use whatever `IConfiguration` has been supplied by the host. The host by default will load environment variables, command-line arguments, and config files. See [Configuration in .NET](https://learn.microsoft.com/dotnet/core/extensions/configuration) for details. #### Using Sdk.CreateTracerProviderBuilder directly By default the `Sdk.CreateTracerProviderBuilder` API will create an `IConfiguration` from environment variables. The following example shows how to customize the `IConfiguration` used by `Sdk.CreateTracerProviderBuilder` for cases where additional sources beyond environment variables are required. ```csharp // Build configuration from sources. Order is important. var configuration = new ConfigurationBuilder() .AddJsonFile("./myOTelSettings.json") .AddEnvironmentVariables() .AddCommandLine(args) .Build(); // Set up a TracerProvider using the configuration. var provider = Sdk.CreateTracerProviderBuilder() .ConfigureServices(services => services.AddSingleton<IConfiguration>(configuration)) .Build(); ``` ### OpenTelemetry Specification environment variables The [OpenTelemetry Specification](https://github.com/open-telemetry/opentelemetry-specification) defines [specific environment variables](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/configuration/sdk-environment-variables.md) which may be used to configure SDK implementations. The OpenTelemetry .NET SDK will look for the environment variables defined in the specification using `IConfiguration` which means in addition to environment variables users may also manage these settings via the command-line, configuration files, or any other source registered with the .NET configuration engine. This provides greater flexibility than what the specification defines. > [!NOTE] > Not all of the environment variables defined in the specification are supported. Consult the individual project README files for details on specific environment variable support. As an example the OpenTelemetry Specification defines the `OTEL_SERVICE_NAME` environment variable which may be used to configure the service name emitted on telemetry by the SDK. A traditional environment variable is set using a command like `set OTEL_SERVICE_NAME=MyService` on Windows or `export OTEL_SERVICE_NAME=MyService` on Linux. That works as expected but the OpenTelemetry .NET SDK is actually looking for the `OTEL_SERVICE_NAME` key in `IConfiguration` which means it may also be configured in any configuration source registered with the `IConfigurationBuilder` used to create the final configuration for the host. Below are two examples of configuring the `OTEL_SERVICE_NAME` setting beyond environment variables. * Using appsettings.json: ```json { "OTEL_SERVICE_NAME": "MyService" } ``` * Using command-line: ```sh dotnet run --OTEL_SERVICE_NAME "MyService" ``` > [!NOTE] > The [.NET Configuration](https://learn.microsoft.com/dotnet/core/extensions/configuration) pattern is hierarchical meaning the order of registered configuration sources controls which value will seen by the SDK when it is defined in multiple sources. ### Using the .NET Options pattern to configure the SDK Options are typically simple classes containing only properties with public "getters" and "setters" (aka POCOs) and have "Options" at the end of the class name. These options classes are primarily used when interacting with the `TracerProviderBuilder` to control settings and features of the different SDK components. Options classes can always be configured through code but users typically want to control key settings through configuration. The following example shows how to configure `OtlpExporterOptions` by binding to an `IConfiguration` section. Json config file (usually appsettings.json): ```json { "OpenTelemetry": { "Otlp": { "Endpoint": "http://localhost:4317" } } } ``` Code: ```csharp var appBuilder = WebApplication.CreateBuilder(args); appBuilder.Services.Configure<OtlpExporterOptions>( appBuilder.Configuration.GetSection("OpenTelemetry:Otlp")); appBuilder.Services.AddOpenTelemetry() .WithTracing(builder => builder.AddOtlpExporter()); ``` The OpenTelemetry .NET SDK supports running multiple `TracerProvider`s inside the same application and it also supports registering multiple similar components such as exporters into a single `TracerProvider`. In order to allow users to target configuration at specific components a "name" parameter is typically supported on configuration extensions to control the options instance used for the component being registered. The below example shows how to configure two `OtlpExporter` instances inside a single `TracerProvider` sending to different ports. Json config file (usually appsettings.json): ```json { "OpenTelemetry": { "OtlpPrimary": { "Endpoint": "http://localhost:4317" }, "OtlpSecondary": { "Endpoint": "http://localhost:4327" }, } } ``` Code: ```csharp var appBuilder = WebApplication.CreateBuilder(args); appBuilder.Services.Configure<OtlpExporterOptions>( "OtlpPrimary", appBuilder.Configuration.GetSection("OpenTelemetry:OtlpPrimary")); appBuilder.Services.Configure<OtlpExporterOptions>( "OtlpSecondary", appBuilder.Configuration.GetSection("OpenTelemetry:OtlpSecondary")); appBuilder.Services.AddOpenTelemetry() .WithTracing(builder => builder .AddOtlpExporter(name: "OtlpPrimary", configure: null) .AddOtlpExporter(name: "OtlpSecondary", configure: null)); ```