From f191e846d37283e82272f6daa95df10aec4edd7b Mon Sep 17 00:00:00 2001 From: Mikel Blanchard Date: Wed, 12 Oct 2022 09:56:07 -0700 Subject: [PATCH 1/2] [SDK + Jaeger] Support loading environment variables from IConfiguration in Traces & Metrics (#3720) * Support retrieval of environment variables through IConfiguration in SDK. * Update Jaeger to load environment variables through IConfiguration. * Warning fix. * CHANGELOG patch. * Bug fixes. * Warning cleanup. * Code review. --- build/Common.props | 3 +- .../CHANGELOG.md | 4 + .../JaegerExporterHelperExtensions.cs | 11 +- .../JaegerExporterOptions.cs | 39 +++-- .../JaegerExporterProtocolParser.cs | 2 +- .../OpenTelemetry.Exporter.Jaeger.csproj | 2 +- src/OpenTelemetry/CHANGELOG.md | 4 + ...viderBuilderServiceCollectionExtensions.cs | 42 +++++ .../Internal/ConfigurationExtensions.cs | 164 ++++++++++++++++++ .../Internal/EnvironmentVariableHelper.cs | 19 ++ .../Builder/MeterProviderBuilderBase.cs | 4 +- ...rProviderBuilderServiceCollectionHelper.cs | 4 +- src/OpenTelemetry/OpenTelemetry.csproj | 2 + .../Builder/TracerProviderBuilderBase.cs | 4 +- ...rProviderBuilderServiceCollectionHelper.cs | 4 +- .../JaegerExporterOptionsTests.cs | 25 +++ .../HostingMeterExtensionTests.cs | 46 ++++- .../HostingTracerExtensionTests.cs | 45 +++++ .../MeterProviderBuilderExtensionsTests.cs | 55 ++++++ .../TracerProviderBuilderExtensionsTest.cs | 55 ++++++ 20 files changed, 504 insertions(+), 30 deletions(-) create mode 100644 src/OpenTelemetry/Internal/Builder/ProviderBuilderServiceCollectionExtensions.cs create mode 100644 src/OpenTelemetry/Internal/ConfigurationExtensions.cs diff --git a/build/Common.props b/build/Common.props index deed26cd9c..e7a089f2b9 100644 --- a/build/Common.props +++ b/build/Common.props @@ -31,10 +31,11 @@ [2.1.1,6.0) [3.3.3] [17.3.0] + [3.1.0,) [2.1.0,) [3.1.0,) [3.1.0,) - [3.1.0,) + [5.0.0,) [1.0.0,2.0) [1.0.0,2.0) [0.12.1,0.13) diff --git a/src/OpenTelemetry.Exporter.Jaeger/CHANGELOG.md b/src/OpenTelemetry.Exporter.Jaeger/CHANGELOG.md index 041b2df43e..f60481649c 100644 --- a/src/OpenTelemetry.Exporter.Jaeger/CHANGELOG.md +++ b/src/OpenTelemetry.Exporter.Jaeger/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +* Added support for loading environment variables from `IConfiguration` when + using the `AddJaegerExporter` extension + ([#3720](https://github.com/open-telemetry/opentelemetry-dotnet/pull/3720)) + ## 1.4.0-beta.1 Released 2022-Sep-29 diff --git a/src/OpenTelemetry.Exporter.Jaeger/JaegerExporterHelperExtensions.cs b/src/OpenTelemetry.Exporter.Jaeger/JaegerExporterHelperExtensions.cs index c9da743562..6a6bd83efc 100644 --- a/src/OpenTelemetry.Exporter.Jaeger/JaegerExporterHelperExtensions.cs +++ b/src/OpenTelemetry.Exporter.Jaeger/JaegerExporterHelperExtensions.cs @@ -62,10 +62,15 @@ public static TracerProviderBuilder AddJaegerExporter(this TracerProviderBuilder name ??= Options.DefaultName; - if (configure != null) + builder.ConfigureServices(services => { - builder.ConfigureServices(services => services.Configure(name, configure)); - } + if (configure != null) + { + services.Configure(name, configure); + } + + services.RegisterOptionsFactory(configuration => new JaegerExporterOptions(configuration)); + }); return builder.ConfigureBuilder((sp, builder) => { diff --git a/src/OpenTelemetry.Exporter.Jaeger/JaegerExporterOptions.cs b/src/OpenTelemetry.Exporter.Jaeger/JaegerExporterOptions.cs index b992048d33..f2946388b9 100644 --- a/src/OpenTelemetry.Exporter.Jaeger/JaegerExporterOptions.cs +++ b/src/OpenTelemetry.Exporter.Jaeger/JaegerExporterOptions.cs @@ -17,6 +17,7 @@ using System; using System.Diagnostics; using System.Net.Http; +using Microsoft.Extensions.Configuration; using OpenTelemetry.Internal; using OpenTelemetry.Trace; @@ -43,37 +44,45 @@ public class JaegerExporterOptions internal static readonly Func DefaultHttpClientFactory = () => new HttpClient(); + /// + /// Initializes a new instance of the class. + /// public JaegerExporterOptions() + : this(new ConfigurationBuilder().AddEnvironmentVariables().Build()) { - if (EnvironmentVariableHelper.LoadString(OTelProtocolEnvVarKey, out string protocolEnvVar)) + } + + internal JaegerExporterOptions(IConfiguration configuration) + { + if (configuration.TryGetValue( + OTelProtocolEnvVarKey, + JaegerExporterProtocolParser.TryParse, + out var protocol)) { - if (JaegerExporterProtocolParser.TryParse(protocolEnvVar, out var protocol)) - { - this.Protocol = protocol; - } - else - { - throw new FormatException($"{OTelProtocolEnvVarKey} environment variable has an invalid value: '{protocolEnvVar}'"); - } + this.Protocol = protocol; } - if (EnvironmentVariableHelper.LoadString(OTelAgentHostEnvVarKey, out string agentHostEnvVar)) + if (configuration.TryGetStringValue(OTelAgentHostEnvVarKey, out var agentHost)) { - this.AgentHost = agentHostEnvVar; + this.AgentHost = agentHost; } - if (EnvironmentVariableHelper.LoadNumeric(OTelAgentPortEnvVarKey, out int agentPortEnvVar)) + if (configuration.TryGetIntValue(OTelAgentPortEnvVarKey, out var agentPort)) { - this.AgentPort = agentPortEnvVar; + this.AgentPort = agentPort; } - if (EnvironmentVariableHelper.LoadString(OTelEndpointEnvVarKey, out string endpointEnvVar) - && Uri.TryCreate(endpointEnvVar, UriKind.Absolute, out Uri endpoint)) + if (configuration.TryGetUriValue(OTelEndpointEnvVarKey, out var endpoint)) { this.Endpoint = endpoint; } } + /// + /// Gets or sets the to use when + /// communicating to Jaeger. Default value: . + /// public JaegerExportProtocol Protocol { get; set; } = JaegerExportProtocol.UdpCompactThrift; /// diff --git a/src/OpenTelemetry.Exporter.Jaeger/JaegerExporterProtocolParser.cs b/src/OpenTelemetry.Exporter.Jaeger/JaegerExporterProtocolParser.cs index 52fb109350..2a77013554 100644 --- a/src/OpenTelemetry.Exporter.Jaeger/JaegerExporterProtocolParser.cs +++ b/src/OpenTelemetry.Exporter.Jaeger/JaegerExporterProtocolParser.cs @@ -20,7 +20,7 @@ internal static class JaegerExporterProtocolParser { public static bool TryParse(string value, out JaegerExportProtocol result) { - switch (value?.Trim()) + switch (value.Trim().ToLower()) { case "udp/thrift.compact": result = JaegerExportProtocol.UdpCompactThrift; diff --git a/src/OpenTelemetry.Exporter.Jaeger/OpenTelemetry.Exporter.Jaeger.csproj b/src/OpenTelemetry.Exporter.Jaeger/OpenTelemetry.Exporter.Jaeger.csproj index abd411f0df..2720fd1a2b 100644 --- a/src/OpenTelemetry.Exporter.Jaeger/OpenTelemetry.Exporter.Jaeger.csproj +++ b/src/OpenTelemetry.Exporter.Jaeger/OpenTelemetry.Exporter.Jaeger.csproj @@ -19,7 +19,7 @@ - + diff --git a/src/OpenTelemetry/CHANGELOG.md b/src/OpenTelemetry/CHANGELOG.md index 417e252b80..4584473b9d 100644 --- a/src/OpenTelemetry/CHANGELOG.md +++ b/src/OpenTelemetry/CHANGELOG.md @@ -7,6 +7,10 @@ `BatchLogRecordExportProcessor.OnEnd` is fired. ([#3731](https://github.com/open-telemetry/opentelemetry-dotnet/pull/3731)) +* Added support for loading environment variables from `IConfiguration` when + using `TracerProviderBuilder` or `MeterProviderBuilder` + ([#3720](https://github.com/open-telemetry/opentelemetry-dotnet/pull/3720)) + ## 1.4.0-beta.1 Released 2022-Sep-29 diff --git a/src/OpenTelemetry/Internal/Builder/ProviderBuilderServiceCollectionExtensions.cs b/src/OpenTelemetry/Internal/Builder/ProviderBuilderServiceCollectionExtensions.cs new file mode 100644 index 0000000000..ab136d3a4f --- /dev/null +++ b/src/OpenTelemetry/Internal/Builder/ProviderBuilderServiceCollectionExtensions.cs @@ -0,0 +1,42 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#nullable enable + +using System.Diagnostics; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.Extensions.DependencyInjection; + +internal static class ProviderBuilderServiceCollectionExtensions +{ + public static IServiceCollection AddOpenTelemetryProviderBuilderServices(this IServiceCollection services) + { + Debug.Assert(services != null, "services was null"); + + services.AddOptions(); + + // Note: When using a host builder IConfiguration is automatically + // registered and this registration will no-op. This only runs for + // Sdk.Create* style or when manually creating a ServiceCollection. The + // point of this registration is to make IConfiguration available in + // those cases. + services!.TryAddSingleton(sp => new ConfigurationBuilder().AddEnvironmentVariables().Build()); + + return services!; + } +} diff --git a/src/OpenTelemetry/Internal/ConfigurationExtensions.cs b/src/OpenTelemetry/Internal/ConfigurationExtensions.cs new file mode 100644 index 0000000000..dc22bbf673 --- /dev/null +++ b/src/OpenTelemetry/Internal/ConfigurationExtensions.cs @@ -0,0 +1,164 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#nullable enable + +using System; +using System.Collections.Generic; +using System.Diagnostics; +#if !NETFRAMEWORK && !NETSTANDARD2_0 +using System.Diagnostics.CodeAnalysis; +#endif +using System.Globalization; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; + +namespace OpenTelemetry.Internal; + +internal static class ConfigurationExtensions +{ + public delegate bool TryParseFunc( + string value, +#if !NETFRAMEWORK && !NETSTANDARD2_0 + [NotNullWhen(true)] +#endif + out T? parsedValue); + + public static bool TryGetStringValue( + this IConfiguration configuration, + string key, +#if !NETFRAMEWORK && !NETSTANDARD2_0 + [NotNullWhen(true)] +#endif + out string? value) + { + value = configuration.GetValue(key, null); + + return !string.IsNullOrWhiteSpace(value); + } + + public static bool TryGetUriValue( + this IConfiguration configuration, + string key, +#if !NETFRAMEWORK && !NETSTANDARD2_0 + [NotNullWhen(true)] +#endif + out Uri? value) + { + if (!configuration.TryGetStringValue(key, out var stringValue)) + { + value = null; + return false; + } + + if (!Uri.TryCreate(stringValue, UriKind.Absolute, out value)) + { + throw new FormatException($"{key} environment variable has an invalid value: '{stringValue}'"); + } + + return true; + } + + public static bool TryGetIntValue( + this IConfiguration configuration, + string key, + out int value) + { + if (!configuration.TryGetStringValue(key, out var stringValue)) + { + value = default; + return false; + } + + if (!int.TryParse(stringValue, NumberStyles.None, CultureInfo.InvariantCulture, out value)) + { + throw new FormatException($"{key} environment variable has an invalid value: '{stringValue}'"); + } + + return true; + } + + public static bool TryGetValue( + this IConfiguration configuration, + string key, + TryParseFunc tryParseFunc, +#if !NETFRAMEWORK && !NETSTANDARD2_0 + [NotNullWhen(true)] +#endif + out T? value) + { + if (!configuration.TryGetStringValue(key, out var stringValue)) + { + value = default; + return false; + } + + if (!tryParseFunc(stringValue!, out value)) + { + throw new FormatException($"{key} environment variable has an invalid value: '{stringValue}'"); + } + + return true; + } + + public static IServiceCollection RegisterOptionsFactory( + this IServiceCollection services, + Func optionsFactoryFunc) + where T : class + { + Debug.Assert(services != null, "services was null"); + Debug.Assert(optionsFactoryFunc != null, "optionsFactoryFunc was null"); + + services!.TryAddSingleton>(sp => + { + return new DelegatingOptionsFactory( + optionsFactoryFunc!, + sp.GetRequiredService(), + sp.GetServices>(), + sp.GetServices>(), + sp.GetServices>()); + }); + + return services!; + } + + private sealed class DelegatingOptionsFactory : OptionsFactory + where T : class + { + private readonly Func optionsFactoryFunc; + private readonly IConfiguration configuration; + + public DelegatingOptionsFactory( + Func optionsFactoryFunc, + IConfiguration configuration, + IEnumerable> setups, + IEnumerable> postConfigures, + IEnumerable> validations) + : base(setups, postConfigures, validations) + { + Debug.Assert(optionsFactoryFunc != null, "optionsFactoryFunc was null"); + Debug.Assert(configuration != null, "configuration was null"); + + this.optionsFactoryFunc = optionsFactoryFunc!; + this.configuration = configuration!; + } + + protected override T CreateInstance(string name) + => this.optionsFactoryFunc(this.configuration); + } +} diff --git a/src/OpenTelemetry/Internal/EnvironmentVariableHelper.cs b/src/OpenTelemetry/Internal/EnvironmentVariableHelper.cs index f6ea978359..6a745e4ad1 100644 --- a/src/OpenTelemetry/Internal/EnvironmentVariableHelper.cs +++ b/src/OpenTelemetry/Internal/EnvironmentVariableHelper.cs @@ -76,6 +76,25 @@ public static bool LoadNumeric(string envVarKey, out int result) return false; } + return LoadNumeric(envVarKey, value, out result); + } + + /// + /// Reads an environment variable and parses is as a + /// + /// numeric value - a non-negative decimal integer. + /// + /// The name of the environment variable. + /// The value of the environment variable. + /// The parsed value of the environment variable. + /// + /// Returns true when a non-empty value was read; otherwise, false. + /// + /// + /// Thrown when failed to parse the non-empty value. + /// + public static bool LoadNumeric(string envVarKey, string value, out int result) + { if (!int.TryParse(value, NumberStyles.None, CultureInfo.InvariantCulture, out result)) { throw new FormatException($"{envVarKey} environment variable has an invalid value: '{value}'"); diff --git a/src/OpenTelemetry/Metrics/Builder/MeterProviderBuilderBase.cs b/src/OpenTelemetry/Metrics/Builder/MeterProviderBuilderBase.cs index 186926a579..80482d7b5c 100644 --- a/src/OpenTelemetry/Metrics/Builder/MeterProviderBuilderBase.cs +++ b/src/OpenTelemetry/Metrics/Builder/MeterProviderBuilderBase.cs @@ -52,7 +52,7 @@ internal MeterProviderBuilderBase(IServiceCollection services) { Guard.ThrowIfNull(services); - services.AddOptions(); + services.AddOpenTelemetryProviderBuilderServices(); services.TryAddSingleton(sp => new MeterProviderSdk(sp, ownsServiceProvider: false)); this.services = services; @@ -65,7 +65,7 @@ protected MeterProviderBuilderBase() { var services = new ServiceCollection(); - services.AddOptions(); + services.AddOpenTelemetryProviderBuilderServices(); this.services = services; this.ownsServices = true; diff --git a/src/OpenTelemetry/Metrics/Builder/MeterProviderBuilderServiceCollectionHelper.cs b/src/OpenTelemetry/Metrics/Builder/MeterProviderBuilderServiceCollectionHelper.cs index e53f412828..779e7f017f 100644 --- a/src/OpenTelemetry/Metrics/Builder/MeterProviderBuilderServiceCollectionHelper.cs +++ b/src/OpenTelemetry/Metrics/Builder/MeterProviderBuilderServiceCollectionHelper.cs @@ -42,7 +42,7 @@ internal static class MeterProviderBuilderServiceCollectionHelper Debug.Assert(services != null, "services was null"); Debug.Assert(configure != null, "configure was null"); - return services.AddSingleton(new ConfigureMeterProviderBuilderStateCallbackRegistration(configure!)); + return services!.AddSingleton(new ConfigureMeterProviderBuilderStateCallbackRegistration(configure!)); } internal static void InvokeRegisteredConfigureStateCallbacks( @@ -52,7 +52,7 @@ internal static class MeterProviderBuilderServiceCollectionHelper Debug.Assert(serviceProvider != null, "serviceProvider was null"); Debug.Assert(state != null, "state was null"); - var callbackRegistrations = serviceProvider.GetServices(); + var callbackRegistrations = serviceProvider!.GetServices(); foreach (var callbackRegistration in callbackRegistrations) { diff --git a/src/OpenTelemetry/OpenTelemetry.csproj b/src/OpenTelemetry/OpenTelemetry.csproj index 542fcc9e40..c05c686b45 100644 --- a/src/OpenTelemetry/OpenTelemetry.csproj +++ b/src/OpenTelemetry/OpenTelemetry.csproj @@ -17,8 +17,10 @@ + + diff --git a/src/OpenTelemetry/Trace/Builder/TracerProviderBuilderBase.cs b/src/OpenTelemetry/Trace/Builder/TracerProviderBuilderBase.cs index a747395308..223f07e798 100644 --- a/src/OpenTelemetry/Trace/Builder/TracerProviderBuilderBase.cs +++ b/src/OpenTelemetry/Trace/Builder/TracerProviderBuilderBase.cs @@ -51,7 +51,7 @@ internal TracerProviderBuilderBase(IServiceCollection services) { Guard.ThrowIfNull(services); - services.AddOptions(); + services.AddOpenTelemetryProviderBuilderServices(); services.TryAddSingleton(sp => new TracerProviderSdk(sp, ownsServiceProvider: false)); this.services = services; @@ -64,7 +64,7 @@ protected TracerProviderBuilderBase() { var services = new ServiceCollection(); - services.AddOptions(); + services.AddOpenTelemetryProviderBuilderServices(); this.services = services; this.ownsServices = true; diff --git a/src/OpenTelemetry/Trace/Builder/TracerProviderBuilderServiceCollectionHelper.cs b/src/OpenTelemetry/Trace/Builder/TracerProviderBuilderServiceCollectionHelper.cs index ae68e0881b..5de52f1a49 100644 --- a/src/OpenTelemetry/Trace/Builder/TracerProviderBuilderServiceCollectionHelper.cs +++ b/src/OpenTelemetry/Trace/Builder/TracerProviderBuilderServiceCollectionHelper.cs @@ -42,7 +42,7 @@ internal static class TracerProviderBuilderServiceCollectionHelper Debug.Assert(services != null, "services was null"); Debug.Assert(configure != null, "configure was null"); - return services.AddSingleton(new ConfigureTracerProviderBuilderStateCallbackRegistration(configure!)); + return services!.AddSingleton(new ConfigureTracerProviderBuilderStateCallbackRegistration(configure!)); } internal static void InvokeRegisteredConfigureStateCallbacks( @@ -52,7 +52,7 @@ internal static class TracerProviderBuilderServiceCollectionHelper Debug.Assert(serviceProvider != null, "serviceProvider was null"); Debug.Assert(state != null, "state was null"); - var callbackRegistrations = serviceProvider.GetServices(); + var callbackRegistrations = serviceProvider!.GetServices(); foreach (var callbackRegistration in callbackRegistrations) { diff --git a/test/OpenTelemetry.Exporter.Jaeger.Tests/JaegerExporterOptionsTests.cs b/test/OpenTelemetry.Exporter.Jaeger.Tests/JaegerExporterOptionsTests.cs index 33143faf16..42b0548be6 100644 --- a/test/OpenTelemetry.Exporter.Jaeger.Tests/JaegerExporterOptionsTests.cs +++ b/test/OpenTelemetry.Exporter.Jaeger.Tests/JaegerExporterOptionsTests.cs @@ -15,6 +15,8 @@ // using System; +using System.Collections.Generic; +using Microsoft.Extensions.Configuration; using Xunit; namespace OpenTelemetry.Exporter.Jaeger.Tests @@ -93,6 +95,29 @@ public void JaegerExporterOptions_EnvironmentVariableNames() Assert.Equal("OTEL_EXPORTER_JAEGER_ENDPOINT", JaegerExporterOptions.OTelEndpointEnvVarKey); } + [Fact] + public void JaegerExporterOptions_FromConfigurationTest() + { + var values = new Dictionary() + { + [JaegerExporterOptions.OTelProtocolEnvVarKey] = "http/thrift.binary", + [JaegerExporterOptions.OTelAgentHostEnvVarKey] = "jaeger-host", + [JaegerExporterOptions.OTelAgentPortEnvVarKey] = "123", + [JaegerExporterOptions.OTelEndpointEnvVarKey] = "http://custom-endpoint:12345", + }; + + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(values) + .Build(); + + var options = new JaegerExporterOptions(configuration); + + Assert.Equal("jaeger-host", options.AgentHost); + Assert.Equal(123, options.AgentPort); + Assert.Equal(JaegerExportProtocol.HttpBinaryThrift, options.Protocol); + Assert.Equal(new Uri("http://custom-endpoint:12345"), options.Endpoint); + } + private static void ClearEnvVars() { Environment.SetEnvironmentVariable(JaegerExporterOptions.OTelProtocolEnvVarKey, null); diff --git a/test/OpenTelemetry.Extensions.Hosting.Tests/HostingMeterExtensionTests.cs b/test/OpenTelemetry.Extensions.Hosting.Tests/HostingMeterExtensionTests.cs index f4acf44b4e..4553ae3392 100644 --- a/test/OpenTelemetry.Extensions.Hosting.Tests/HostingMeterExtensionTests.cs +++ b/test/OpenTelemetry.Extensions.Hosting.Tests/HostingMeterExtensionTests.cs @@ -15,13 +15,14 @@ // using System; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using OpenTelemetry.Metrics; using Xunit; -using static OpenTelemetry.Extensions.Hosting.Tests.HostingTracerExtensionTests; namespace OpenTelemetry.Extensions.Hosting.Tests { @@ -195,6 +196,49 @@ public void AddOpenTelemetryMetrics_MultipleCallsConfigureSingleProvider() Assert.Single(providers); } + [Fact] + public async Task AddOpenTelemetryMetrics_HostConfigurationHonoredTest() + { + bool configureBuilderCalled = false; + + var builder = new HostBuilder() + .ConfigureAppConfiguration(builder => + { + builder.AddInMemoryCollection(new Dictionary + { + ["TEST_KEY"] = "TEST_KEY_VALUE", + }); + }) + .ConfigureServices(services => + { + services.AddOpenTelemetryMetrics(builder => + { + builder.ConfigureBuilder((sp, builder) => + { + configureBuilderCalled = true; + + var configuration = sp.GetRequiredService(); + + var testKeyValue = configuration.GetValue("TEST_KEY", null); + + Assert.Equal("TEST_KEY_VALUE", testKeyValue); + }); + }); + }); + + var host = builder.Build(); + + Assert.False(configureBuilderCalled); + + await host.StartAsync(); + + Assert.True(configureBuilderCalled); + + await host.StopAsync(); + + host.Dispose(); + } + private static MeterProviderBuilder AddMyFeature(MeterProviderBuilder meterProviderBuilder) { meterProviderBuilder.ConfigureServices(services => services.AddSingleton()); diff --git a/test/OpenTelemetry.Extensions.Hosting.Tests/HostingTracerExtensionTests.cs b/test/OpenTelemetry.Extensions.Hosting.Tests/HostingTracerExtensionTests.cs index 131e903813..22432e3f4a 100644 --- a/test/OpenTelemetry.Extensions.Hosting.Tests/HostingTracerExtensionTests.cs +++ b/test/OpenTelemetry.Extensions.Hosting.Tests/HostingTracerExtensionTests.cs @@ -15,9 +15,11 @@ // using System; +using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading.Tasks; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using OpenTelemetry.Trace; @@ -198,6 +200,49 @@ public void AddOpenTelemetryTracing_MultipleCallsConfigureSingleProvider() Assert.Single(providers); } + [Fact] + public async Task AddOpenTelemetryTracing_HostConfigurationHonoredTest() + { + bool configureBuilderCalled = false; + + var builder = new HostBuilder() + .ConfigureAppConfiguration(builder => + { + builder.AddInMemoryCollection(new Dictionary + { + ["TEST_KEY"] = "TEST_KEY_VALUE", + }); + }) + .ConfigureServices(services => + { + services.AddOpenTelemetryTracing(builder => + { + builder.ConfigureBuilder((sp, builder) => + { + configureBuilderCalled = true; + + var configuration = sp.GetRequiredService(); + + var testKeyValue = configuration.GetValue("TEST_KEY", null); + + Assert.Equal("TEST_KEY_VALUE", testKeyValue); + }); + }); + }); + + var host = builder.Build(); + + Assert.False(configureBuilderCalled); + + await host.StartAsync(); + + Assert.True(configureBuilderCalled); + + await host.StopAsync(); + + host.Dispose(); + } + private static TracerProviderBuilder AddMyFeature(TracerProviderBuilder tracerProviderBuilder) { tracerProviderBuilder.ConfigureServices(services => services.AddSingleton()); diff --git a/test/OpenTelemetry.Tests/Metrics/MeterProviderBuilderExtensionsTests.cs b/test/OpenTelemetry.Tests/Metrics/MeterProviderBuilderExtensionsTests.cs index b4c96f2cc7..7691f22f34 100644 --- a/test/OpenTelemetry.Tests/Metrics/MeterProviderBuilderExtensionsTests.cs +++ b/test/OpenTelemetry.Tests/Metrics/MeterProviderBuilderExtensionsTests.cs @@ -17,6 +17,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using OpenTelemetry.Resources; @@ -199,6 +200,60 @@ public void SetAndConfigureResourceTest() Assert.Contains(provider.Resource.Attributes, kvp => kvp.Key == "key2" && (string)kvp.Value == "value2"); } + [Fact] + public void ConfigureBuilderIConfigurationAvailableTest() + { + Environment.SetEnvironmentVariable("TEST_KEY", "TEST_KEY_VALUE"); + + bool configureBuilderCalled = false; + + using var provider = Sdk.CreateMeterProviderBuilder() + .ConfigureBuilder((sp, builder) => + { + var configuration = sp.GetRequiredService(); + + configureBuilderCalled = true; + + var testKeyValue = configuration.GetValue("TEST_KEY", null); + + Assert.Equal("TEST_KEY_VALUE", testKeyValue); + }) + .Build(); + + Assert.True(configureBuilderCalled); + + Environment.SetEnvironmentVariable("TEST_KEY", null); + } + + [Fact] + public void ConfigureBuilderIConfigurationModifiableTest() + { + bool configureBuilderCalled = false; + + using var provider = Sdk.CreateMeterProviderBuilder() + .ConfigureServices(services => + { + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary { ["TEST_KEY_2"] = "TEST_KEY_2_VALUE" }) + .Build(); + + services.AddSingleton(configuration); + }) + .ConfigureBuilder((sp, builder) => + { + var configuration = sp.GetRequiredService(); + + configureBuilderCalled = true; + + var testKey2Value = configuration.GetValue("TEST_KEY_2", null); + + Assert.Equal("TEST_KEY_2_VALUE", testKey2Value); + }) + .Build(); + + Assert.True(configureBuilderCalled); + } + private static void RunBuilderServiceLifecycleTest( MeterProviderBuilder builder, Func buildFunc, diff --git a/test/OpenTelemetry.Tests/Trace/TracerProviderBuilderExtensionsTest.cs b/test/OpenTelemetry.Tests/Trace/TracerProviderBuilderExtensionsTest.cs index 383e691d35..34cc5a8e3e 100644 --- a/test/OpenTelemetry.Tests/Trace/TracerProviderBuilderExtensionsTest.cs +++ b/test/OpenTelemetry.Tests/Trace/TracerProviderBuilderExtensionsTest.cs @@ -18,6 +18,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using OpenTelemetry.Resources; @@ -393,6 +394,60 @@ public void AddExporterNamedOptionsTest() Assert.Equal(1, namedOptionsConfigureInvocations); } + [Fact] + public void ConfigureBuilderIConfigurationAvailableTest() + { + Environment.SetEnvironmentVariable("TEST_KEY", "TEST_KEY_VALUE"); + + bool configureBuilderCalled = false; + + using var provider = Sdk.CreateTracerProviderBuilder() + .ConfigureBuilder((sp, builder) => + { + var configuration = sp.GetRequiredService(); + + configureBuilderCalled = true; + + var testKeyValue = configuration.GetValue("TEST_KEY", null); + + Assert.Equal("TEST_KEY_VALUE", testKeyValue); + }) + .Build(); + + Assert.True(configureBuilderCalled); + + Environment.SetEnvironmentVariable("TEST_KEY", null); + } + + [Fact] + public void ConfigureBuilderIConfigurationModifiableTest() + { + bool configureBuilderCalled = false; + + using var provider = Sdk.CreateTracerProviderBuilder() + .ConfigureServices(services => + { + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary { ["TEST_KEY_2"] = "TEST_KEY_2_VALUE" }) + .Build(); + + services.AddSingleton(configuration); + }) + .ConfigureBuilder((sp, builder) => + { + var configuration = sp.GetRequiredService(); + + configureBuilderCalled = true; + + var testKey2Value = configuration.GetValue("TEST_KEY_2", null); + + Assert.Equal("TEST_KEY_2_VALUE", testKey2Value); + }) + .Build(); + + Assert.True(configureBuilderCalled); + } + private static void RunBuilderServiceLifecycleTest( TracerProviderBuilder builder, Func buildFunc, From e8af2a036da475fba5546f6382995f2c3a4639f4 Mon Sep 17 00:00:00 2001 From: Mikel Blanchard Date: Wed, 12 Oct 2022 10:52:48 -0700 Subject: [PATCH 2/2] [Zipkin] Support loading envvars from IConfiguration (#3759) * Support loading envvars from IConfiguration in Zipkin exporter. * CHANGELOG patch. --- src/OpenTelemetry.Exporter.Zipkin/CHANGELOG.md | 4 ++++ .../OpenTelemetry.Exporter.Zipkin.csproj | 2 +- .../ZipkinExporterHelperExtensions.cs | 11 ++++++++--- .../ZipkinExporterOptions.cs | 8 +++++++- .../ZipkinExporterTests.cs | 18 ++++++++++++++++++ 5 files changed, 38 insertions(+), 5 deletions(-) diff --git a/src/OpenTelemetry.Exporter.Zipkin/CHANGELOG.md b/src/OpenTelemetry.Exporter.Zipkin/CHANGELOG.md index 352288c42a..eb922b12f2 100644 --- a/src/OpenTelemetry.Exporter.Zipkin/CHANGELOG.md +++ b/src/OpenTelemetry.Exporter.Zipkin/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +* Added support for loading environment variables from `IConfiguration` when + using the `AddZipkinExporter` extension + ([#3759](https://github.com/open-telemetry/opentelemetry-dotnet/pull/3759)) + ## 1.4.0-beta.1 Released 2022-Sep-29 diff --git a/src/OpenTelemetry.Exporter.Zipkin/OpenTelemetry.Exporter.Zipkin.csproj b/src/OpenTelemetry.Exporter.Zipkin/OpenTelemetry.Exporter.Zipkin.csproj index 0aee4bd633..c5e14a100d 100644 --- a/src/OpenTelemetry.Exporter.Zipkin/OpenTelemetry.Exporter.Zipkin.csproj +++ b/src/OpenTelemetry.Exporter.Zipkin/OpenTelemetry.Exporter.Zipkin.csproj @@ -14,7 +14,7 @@ - + diff --git a/src/OpenTelemetry.Exporter.Zipkin/ZipkinExporterHelperExtensions.cs b/src/OpenTelemetry.Exporter.Zipkin/ZipkinExporterHelperExtensions.cs index 261cecd1a6..93a4301487 100644 --- a/src/OpenTelemetry.Exporter.Zipkin/ZipkinExporterHelperExtensions.cs +++ b/src/OpenTelemetry.Exporter.Zipkin/ZipkinExporterHelperExtensions.cs @@ -62,10 +62,15 @@ public static TracerProviderBuilder AddZipkinExporter(this TracerProviderBuilder name ??= Options.DefaultName; - if (configure != null) + builder.ConfigureServices(services => { - builder.ConfigureServices(services => services.Configure(name, configure)); - } + if (configure != null) + { + services.Configure(name, configure); + } + + services.RegisterOptionsFactory(configuration => new ZipkinExporterOptions(configuration)); + }); return builder.ConfigureBuilder((sp, builder) => { diff --git a/src/OpenTelemetry.Exporter.Zipkin/ZipkinExporterOptions.cs b/src/OpenTelemetry.Exporter.Zipkin/ZipkinExporterOptions.cs index 9c354574a7..d51eb007b8 100644 --- a/src/OpenTelemetry.Exporter.Zipkin/ZipkinExporterOptions.cs +++ b/src/OpenTelemetry.Exporter.Zipkin/ZipkinExporterOptions.cs @@ -17,6 +17,7 @@ using System; using System.Diagnostics; using System.Net.Http; +using Microsoft.Extensions.Configuration; using OpenTelemetry.Internal; using OpenTelemetry.Trace; @@ -44,8 +45,13 @@ public sealed class ZipkinExporterOptions /// Initializes zipkin endpoint. /// public ZipkinExporterOptions() + : this(new ConfigurationBuilder().AddEnvironmentVariables().Build()) { - if (EnvironmentVariableHelper.LoadUri(ZipkinEndpointEnvVar, out Uri endpoint)) + } + + internal ZipkinExporterOptions(IConfiguration configuration) + { + if (configuration.TryGetUriValue(ZipkinEndpointEnvVar, out var endpoint)) { this.Endpoint = endpoint; } diff --git a/test/OpenTelemetry.Exporter.Zipkin.Tests/ZipkinExporterTests.cs b/test/OpenTelemetry.Exporter.Zipkin.Tests/ZipkinExporterTests.cs index 4cd2d97d2e..3c19c2c106 100644 --- a/test/OpenTelemetry.Exporter.Zipkin.Tests/ZipkinExporterTests.cs +++ b/test/OpenTelemetry.Exporter.Zipkin.Tests/ZipkinExporterTests.cs @@ -23,6 +23,7 @@ using System.Net; using System.Net.Http; using System.Text; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using OpenTelemetry.Exporter.Zipkin.Implementation; using OpenTelemetry.Resources; @@ -207,6 +208,23 @@ public void ErrorGettingUriFromEnvVarSetsDefaultEndpointValue() } } + [Fact] + public void EndpointConfigurationUsingIConfiguration() + { + var values = new Dictionary() + { + [ZipkinExporterOptions.ZipkinEndpointEnvVar] = "http://custom-endpoint:12345", + }; + + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(values) + .Build(); + + var options = new ZipkinExporterOptions(configuration); + + Assert.Equal(new Uri("http://custom-endpoint:12345"), options.Endpoint); + } + [Fact] public void UserHttpFactoryCalled() {