Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SDK + Jaeger] Support loading environment variables from IConfiguration in Traces & Metrics #3720

Merged
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion build/Common.props
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,11 @@
<MicrosoftAspNetCoreHttpFeaturesPkgVer>[2.1.1,6.0)</MicrosoftAspNetCoreHttpFeaturesPkgVer>
<MicrosoftCodeAnalysisAnalyzersPkgVer>[3.3.3]</MicrosoftCodeAnalysisAnalyzersPkgVer>
<MicrosoftCodeCoveragePkgVer>[17.3.0]</MicrosoftCodeCoveragePkgVer>
<MicrosoftExtensionsConfigurationEnvironmentVariablesPkgVer>[3.1.0,)</MicrosoftExtensionsConfigurationEnvironmentVariablesPkgVer>
<MicrosoftExtensionsHostingAbstractionsPkgVer>[2.1.0,)</MicrosoftExtensionsHostingAbstractionsPkgVer>
<MicrosoftExtensionsLoggingPkgVer>[3.1.0,)</MicrosoftExtensionsLoggingPkgVer>
<MicrosoftExtensionsLoggingConfigurationPkgVer>[3.1.0,)</MicrosoftExtensionsLoggingConfigurationPkgVer>
<MicrosoftExtensionsOptionsPkgVer>[3.1.0,)</MicrosoftExtensionsOptionsPkgVer>
<MicrosoftExtensionsOptionsPkgVer>[5.0.0,)</MicrosoftExtensionsOptionsPkgVer>
Comment on lines 35 to +38
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes me wonder what the harm would be in bumping all the extension packages to [5.0.0,)?

Just poking around the dependencies of the Microsoft.Extensions.* packages themselves seems to suggest they're always bumped in unison.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have an issue with that. I'm not going to do it on this PR but we could bring it up on SIG?

<MicrosoftNETFrameworkReferenceAssembliesPkgVer>[1.0.0,2.0)</MicrosoftNETFrameworkReferenceAssembliesPkgVer>
<MicrosoftSourceLinkGitHubPkgVer>[1.0.0,2.0)</MicrosoftSourceLinkGitHubPkgVer>
<OpenTracingPkgVer>[0.12.1,0.13)</OpenTracingPkgVer>
Expand Down
4 changes: 4 additions & 0 deletions src/OpenTelemetry.Exporter.Jaeger/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
using System.Net.Http;
using System.Reflection;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
using OpenTelemetry.Exporter;
using OpenTelemetry.Internal;
Expand Down Expand Up @@ -62,10 +63,15 @@ public static TracerProviderBuilder AddJaegerExporter(

name ??= Options.DefaultName;

if (configure != null)
builder.ConfigureServices(services =>
{
builder.ConfigureServices(services => services.Configure(name, configure));
}
if (configure != null)
{
services.Configure(name, configure);
}

services.TryAddSingleton<IOptionsFactory<JaegerExporterOptions>, JaegerExporterOptions.JaegerExporterOptionsFactory>();
});

return builder.ConfigureBuilder((sp, builder) =>
{
Expand Down
71 changes: 60 additions & 11 deletions src/OpenTelemetry.Exporter.Jaeger/JaegerExporterOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,11 @@
// </copyright>

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Net.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;
using OpenTelemetry.Internal;
using OpenTelemetry.Trace;

Expand All @@ -43,37 +46,63 @@ public class JaegerExporterOptions

internal static readonly Func<HttpClient> DefaultHttpClientFactory = () => new HttpClient();

/// <summary>
/// Initializes a new instance of the <see cref="JaegerExporterOptions"/> class.
/// </summary>
public JaegerExporterOptions()
: this(new ConfigurationBuilder().AddEnvironmentVariables().Build())
{
}

internal JaegerExporterOptions(IConfiguration configuration)
{
if (EnvironmentVariableHelper.LoadString(OTelProtocolEnvVarKey, out string protocolEnvVar))
var protocol = configuration.GetValue<string>(OTelProtocolEnvVarKey, null);
if (!string.IsNullOrEmpty(protocol))
{
if (JaegerExporterProtocolParser.TryParse(protocolEnvVar, out var protocol))
if (JaegerExporterProtocolParser.TryParse(protocol, out var parsedProtocol))
{
this.Protocol = protocol;
this.Protocol = parsedProtocol;
}
else
{
throw new FormatException($"{OTelProtocolEnvVarKey} environment variable has an invalid value: '{protocolEnvVar}'");
throw new FormatException($"{OTelProtocolEnvVarKey} environment variable has an invalid value: '{protocol}'");
}
}

if (EnvironmentVariableHelper.LoadString(OTelAgentHostEnvVarKey, out string agentHostEnvVar))
var agentHost = configuration.GetValue<string>(OTelAgentHostEnvVarKey, null);
if (!string.IsNullOrEmpty(agentHost))
{
this.AgentHost = agentHostEnvVar;
this.AgentHost = agentHost;
}

if (EnvironmentVariableHelper.LoadNumeric(OTelAgentPortEnvVarKey, out int agentPortEnvVar))
var agentPort = configuration.GetValue<string>(OTelAgentPortEnvVarKey, null);
if (!string.IsNullOrEmpty(agentPort))
{
this.AgentPort = agentPortEnvVar;
if (EnvironmentVariableHelper.LoadNumeric(OTelAgentPortEnvVarKey, agentPort, out int parsedAgentPort))
pellared marked this conversation as resolved.
Show resolved Hide resolved
{
this.AgentPort = parsedAgentPort;
}
}

if (EnvironmentVariableHelper.LoadString(OTelEndpointEnvVarKey, out string endpointEnvVar)
&& Uri.TryCreate(endpointEnvVar, UriKind.Absolute, out Uri endpoint))
var endpoint = configuration.GetValue<string>(OTelEndpointEnvVarKey, null);
pellared marked this conversation as resolved.
Show resolved Hide resolved
if (!string.IsNullOrEmpty(endpoint))
{
this.Endpoint = endpoint;
if (Uri.TryCreate(endpoint, UriKind.Absolute, out Uri parsedEndpoint))
{
this.Endpoint = parsedEndpoint;
}
else
{
throw new FormatException($"{OTelEndpointEnvVarKey} environment variable has an invalid value: '{endpoint}'");
}
}
}

/// <summary>
/// Gets or sets the <see cref="JaegerExportProtocol"/> to use when
/// communicating to Jaeger. Default value: <see
/// cref="JaegerExportProtocol.UdpCompactThrift"/>.
/// </summary>
public JaegerExportProtocol Protocol { get; set; } = JaegerExportProtocol.UdpCompactThrift;

/// <summary>
Expand Down Expand Up @@ -129,5 +158,25 @@ public JaegerExporterOptions()
/// </list>
/// </remarks>
public Func<HttpClient> HttpClientFactory { get; set; } = DefaultHttpClientFactory;

internal sealed class JaegerExporterOptionsFactory : OptionsFactory<JaegerExporterOptions>
{
private readonly IConfiguration configuration;

public JaegerExporterOptionsFactory(
IConfiguration configuration,
IEnumerable<IConfigureOptions<JaegerExporterOptions>> setups,
IEnumerable<IPostConfigureOptions<JaegerExporterOptions>> postConfigures,
IEnumerable<IValidateOptions<JaegerExporterOptions>> validations)
: base(setups, postConfigures, validations)
{
Debug.Assert(configuration != null, "configuration was null");

this.configuration = configuration;
}

protected override JaegerExporterOptions CreateInstance(string name)
=> new(this.configuration);
}
}
}
4 changes: 4 additions & 0 deletions src/OpenTelemetry/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

* 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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// <copyright file="ProviderBuilderServiceCollectionExtensions.cs" company="OpenTelemetry Authors">
// 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.
// </copyright>

#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();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No warning from the missing null forgiving operator?

Suggested change
services.AddOptions();
services!.AddOptions();

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This class is internal and services is not decorated as nullable (NOT IServiceCollection? services) so really, it shouldn't warn. You would get a warning at the call site if you try to pass a null into it. For net6.0 & netstandard2.1 targets, it works fine. For net462 & netstandard2.0 it doesn't warn on this line but it does warn on the two de-references below this! That is all kinds of wrong 🤣 Nullable analysis just doesn't work well on anything before netstandard2.1. So a lot of these !s are there just to make the IDE happy for old targets. In contrib I did this. That turns off nullable warnings but then you have to have some newer target available to vet valid warnings. You get weirdness like this where it is adding targets just for analysis.


// 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<IConfiguration>(sp => new ConfigurationBuilder().AddEnvironmentVariables().Build());

return services!;
}
}
19 changes: 19 additions & 0 deletions src/OpenTelemetry/Internal/EnvironmentVariableHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,25 @@ public static bool LoadNumeric(string envVarKey, out int result)
return false;
}

return LoadNumeric(envVarKey, value, out result);
}

/// <summary>
/// Reads an environment variable and parses is as a
/// <a href="https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/sdk-environment-variables.md#numeric-value">
/// numeric value</a> - a non-negative decimal integer.
/// </summary>
/// <param name="envVarKey">The name of the environment variable.</param>
/// <param name="value">The value of the environment variable.</param>
/// <param name="result">The parsed value of the environment variable.</param>
/// <returns>
/// Returns <c>true</c> when a non-empty value was read; otherwise, <c>false</c>.
/// </returns>
/// <exception cref="FormatException">
/// Thrown when failed to parse the non-empty value.
/// </exception>
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}'");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ internal MeterProviderBuilderBase(IServiceCollection services)
{
Guard.ThrowIfNull(services);

services.AddOptions();
services.AddOpenTelemetryProviderBuilderServices();
services.TryAddSingleton<MeterProvider>(sp => new MeterProviderSdk(sp, ownsServiceProvider: false));

this.services = services;
Expand All @@ -65,7 +65,7 @@ protected MeterProviderBuilderBase()
{
var services = new ServiceCollection();

services.AddOptions();
services.AddOpenTelemetryProviderBuilderServices();

this.services = services;
this.ownsServices = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ internal static IServiceCollection RegisterConfigureStateCallback(
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(
Expand All @@ -52,7 +52,7 @@ internal static void InvokeRegisteredConfigureStateCallbacks(
Debug.Assert(serviceProvider != null, "serviceProvider was null");
Debug.Assert(state != null, "state was null");

var callbackRegistrations = serviceProvider.GetServices<ConfigureMeterProviderBuilderStateCallbackRegistration>();
var callbackRegistrations = serviceProvider!.GetServices<ConfigureMeterProviderBuilderStateCallbackRegistration>();

foreach (var callbackRegistration in callbackRegistrations)
{
Expand Down
2 changes: 2 additions & 0 deletions src/OpenTelemetry/OpenTelemetry.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@

<ItemGroup>
<PackageReference Include="System.Reflection.Emit.Lightweight" Version="$(SystemReflectionEmitLightweightPkgVer)" Condition="'$(TargetFramework)' != 'net6.0'" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="$(MicrosoftExtensionsConfigurationEnvironmentVariablesPkgVer)" />
Copy link
Member Author

@CodeBlanch CodeBlanch Oct 3, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is needed to initialize IConfiguration from environment variables when no IConfiguration is found. No one wants the dependency but OTel spec calls for a lot of environment variable behavior so I don't think it is unreasonable for SDK to have this. Anyone using the AspNetCore "meta" reference already has this.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm anticipating a future desire for an OpenTelemetry .NET Lite package... let's hope not 😆

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Less filling, tastes great.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just want to call out that bringing more and more dependencies increases the chances of an assembly version conflict which is especially annoying for .NET Auto-Instrumentation 😭

Reference: https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/main/docs/troubleshooting.md#assembly-version-conflicts

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alanwest and I kicked around some ideas/options, but we haven't been able to come up with anything really great for avoiding this. When Auto-Instrumentation copies in references, does it do so blindly? Or do you somehow try to reason out what is there prior to doing the copy?

<PackageReference Include="Microsoft.Extensions.Logging" Version="$(MicrosoftExtensionsLoggingPkgVer)" />
<PackageReference Include="Microsoft.Extensions.Logging.Configuration" Version="$(MicrosoftExtensionsLoggingConfigurationPkgVer)" />
<PackageReference Include="Microsoft.Extensions.Options" Version="$(MicrosoftExtensionsOptionsPkgVer)" />
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We already had Microsoft.Extensions.Options through Microsoft.Extensions.Logging.Configuration but now we ask for >= 5.0.0. The reason for that is to gain access to OptionsFactory.CreateInstance which allows us to initialize the instance created for options using IConfiguration + the custom environment variables defined in OTel spec.

</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ internal TracerProviderBuilderBase(IServiceCollection services)
{
Guard.ThrowIfNull(services);

services.AddOptions();
services.AddOpenTelemetryProviderBuilderServices();
services.TryAddSingleton<TracerProvider>(sp => new TracerProviderSdk(sp, ownsServiceProvider: false));

this.services = services;
Expand All @@ -64,7 +64,7 @@ protected TracerProviderBuilderBase()
{
var services = new ServiceCollection();

services.AddOptions();
services.AddOpenTelemetryProviderBuilderServices();

this.services = services;
this.ownsServices = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ internal static IServiceCollection RegisterConfigureStateCallback(
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(
Expand All @@ -52,7 +52,7 @@ internal static void InvokeRegisteredConfigureStateCallbacks(
Debug.Assert(serviceProvider != null, "serviceProvider was null");
Debug.Assert(state != null, "state was null");

var callbackRegistrations = serviceProvider.GetServices<ConfigureTracerProviderBuilderStateCallbackRegistration>();
var callbackRegistrations = serviceProvider!.GetServices<ConfigureTracerProviderBuilderStateCallbackRegistration>();

foreach (var callbackRegistration in callbackRegistrations)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
// </copyright>

using System;
using System.Collections.Generic;
using Microsoft.Extensions.Configuration;
using Xunit;

namespace OpenTelemetry.Exporter.Jaeger.Tests
Expand Down Expand Up @@ -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<string, string>()
{
[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);
Expand Down
Loading