-
Notifications
You must be signed in to change notification settings - Fork 772
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
Support IConfiguration
for configuration settings
#2980
Comments
https://github.com/open-telemetry/opentelemetry-dotnet/tree/main/src/OpenTelemetry.Extensions.Hosting I think this is covered when using OTel.Extensions.Hosting package. (JaegerExporter currently support this, but not really documented in its readme file). Please see if Extensions.Hosting package addresses this. (Note that this package is not yet stable.) |
Unfortunately, it doesn't seem that does what I'm hoping. It does look like it nicely can attach things to the hosting environment, but it doesn't connect anything to A good way to find out if it can be connected to |
I do see the calls to {
"Jaeger": {
"ServiceName": "jaeger-test",
"AgentHost": "localhost",
"AgentPort": 6831,
"Endpoint": "http://localhost:14268",
"Protocol": "UdpCompactThrift"
}
} Sort of like a JSON serialized version of the object. However, that doesn't merge in what might be specified in the environment. Imagine appsettings.json like: {
"OTEL_EXPORTER_JAEGER_AGENT_HOST": "localhost"
} Then appsettings.Production.json might be: {
"OTEL_EXPORTER_JAEGER_AGENT_HOST": "production-jaeger"
} Finally, at deployment, a common set of environment variables may be added to all pods going to a given cluster.
It may be that the JSON files aren't included so it's only that one override environment variable. It may be that the environment variable isn't provided but only the JSON files. The scenario is that they all use the same, standardized variable names (for environment and for config) so:
|
Maybe I'm not getting your specific question but I'm relying on the WebApplicationBuilder to read the configs from the usual providers: builder.Services.AddOpenTelemetryTracing(traceProvider =>
{
traceProvider
.AddSource(OpenTelemetryExtensions.ServiceName)
.SetResourceBuilder(
ResourceBuilder.CreateDefault()
.AddService(serviceName: OpenTelemetryExtensions.ServiceName, serviceVersion: OpenTelemetryExtensions.ServiceVersion))
.AddHttpClientInstrumentation()
.AddAspNetCoreInstrumentation()
.AddJaegerExporter(exporter =>
{
exporter.AgentHost = builder.Configuration["JaegerConfiguration:AgentHost"];
exporter.AgentPort = Convert.ToInt32(builder.Configuration["JaegerConfiguration:AgentPort"]);
});
});``` |
@codeaphex That part where you're manually reading info and passing it to the Jaeger exporter? There is already code to read that directly from the environment. Since config can include environment variables, it seems reasonable to allow those settings to come from config, not just environment. If that was done, you wouldn't need to do that manual wire-up at all; it'd happen for you, just like it would right now if you used environment variables explicitly. |
@cijothomas This really doesn't have anything to do with the hosting library. You can bind builder.Services.Configure<JaegerExporterOptions>(builder.Configuration.GetSection("Jaeger"));
builder.Services.Configure<OtlpExporterOptions>(builder.Configuration.GetSection("Otlp"));
builder.Services.Configure<ZipkinExporterOptions>(builder.Configuration.GetSection("Zipkin")); There are a bunch of ways to do it. Essentially all the SDK exporters will ask for their options class instance through the The issue is how that binding happens. If you set an environment variable like this The problem is the OTel standard key "OTEL_EXPORTER_JAEGER_AGENT_HOST" will map somewhere very specific using the .NET Options/Configuration pathing structure and where it maps probably doesn't match the user's binding. Essentially the issue is the OTel spec environment variable keys are not friendly to work well with .NET IConfiguration. @tillig Am I understanding this correctly? |
Well, sort of. It's almost the inverse of that.
The OTel standard environment variable works just fine with configuration. It does not get mapped weird because it doesn't have double-underscores anywhere. I put an example earlier showing how configuration could look. I'll put it here again for clarity: I want to be able to have a default in {
"OTEL_EXPORTER_JAEGER_AGENT_HOST": "localhost"
} I want to be able to override that default for my production environment, so {
"OTEL_EXPORTER_JAEGER_AGENT_HOST": "production-jaeger"
} There may be a need in a deployment to override that. That's how configuration hierarchy works! Instead of using a .NET specific override, I can be language-agnostic by inserting an environment variable in the Kubernetes pod definition on a global level as part of my CI/CD pipeline. I won't have to know it's a .NET container being deployed.
.NET
The real key is I don't want the .NET specific stuff leaking out to the environment variables. Being language agnostic and using the standard keys helps unify things across polyglot systems. I also don't want folks confused where ".NET configuration uses one set of keys but the environment variable override is totally different." There's already a standard, I want to use the standard - in configuration and in the environment. Finally, from a testability standpoint, it's pretty hard to set up unit tests for things that are glued to the environment. I see some of the hoops that get jumped through to make that happen and it's more like integration tests than unit tests. With var data = new Dictionary<string, string>
{
{ "OTEL_EXPORTER_JAEGER_AGENT_HOST", "value" },
};
var config = new ConfigurationBuilder().AddInMemoryCollection(data).Build(); No set/update/reset of variables, no need to ensure process isolation due to environment corruption, etc. And, again, the default fallback already includes/supports environment, so even if folks don't have JSON files, the var config = new ConfigurationBuilder.AddEnvironmentVariables().Build(); Then unify on reading environment from config rather than literally directly from the hardcoded environment. It'd also give the added benefit, internally, of not having to manually support things like |
Couldn't you just use different environment variables that play nice with your particular configuration layout? For example, instead of |
@CodeBlanch I could, but that breaks this:
We have a standard deployment template that works across things that aren't .NET. I don't want .NET specific stuff leaking out when there is already a documented standard. |
Would having some duplicates in there that are .NET specific cause harm to the other platforms? |
I'm more worried about confusion and precedent. "Why is it configured twice? I'm a Java person, let me PR this unnecessary duplicate out of there." "These two values can diverge. Which takes precedence? On which platforms?" "We need to update this value. I'm not a .NET person and only put a PR in to update the one that every other platform uses. This other non-.NET dev approved it. Uh oh, diverging values!" "Hey, the .NET folks get their own language specific variables jammed in here, let's all put our own stuff in there!" Think in terms of hundreds of devs in an org, not all of them super-senior folks. I'm trying to make things as standard and "pit of success" as possible. If it was two or three services, or if I could trust folks to be more diligent in these areas, I'd probably care a lot less. Removing duplicates, having standard options/places to set things, using a standard I can point out in the OTel docs and having that "just work" with .NET config and not be different/special - that's the big value. |
For sure any kind of shared-single-template-to-rule-them-all setup is going to suffer these kind of human issues. But are there any technical blockers? Just trying to prioritize this and understand the need. Here's another workaround I messed with this morning... // In startup
builder.Services.ConfigureOptions<ConfigureJaegerExporterOptions>();
// Somewhere
internal sealed class ConfigureJaegerExporterOptions : IConfigureOptions<JaegerExporterOptions>
{
private readonly IConfiguration configuration;
public ConfigureJaegerExporterOptions(IConfiguration configuration)
{
this.configuration = configuration;
}
public void Configure(JaegerExporterOptions options)
{
options.AgentHost = this.configuration.GetValue<string>("OTEL_EXPORTER_JAEGER_AGENT_HOST");
// TODO: Map other standard OTEL envvar keys onto options instance
}
} What that workaround does is just apply the Could OTel .NET take these environment variables from IConfiguration automatically? The challenge here is how does that work for .NET Framework? Is it the common case that .NET Framework users have |
Are there technical blockers? No, I have extension methods and configurators that already basically do what your workaround/experimentation is doing: /// <summary>
/// Environment variable keys defined by the OpenTelemetry specification:
/// <c>https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/sdk-environment-variables.md</c>.
/// </summary>
public static class OpenTelemetryEnvironmentVariable
{
/// <summary>
/// Specifies the Jaeger transport protocol (<c>http/thrift.binary</c>, <c>grpc</c>, <c>udp/thrift.compact</c>, <c>udp/thrift.binary</c>).
/// </summary>
public const string JaegerExporterProtocol = "OTEL_EXPORTER_JAEGER_PROTOCOL";
/// <summary>
/// Hostname of the Jaeger agent when using UDP (<c>udp/thrift.compact</c> or <c>udp/thrift.binary</c>) transports.
/// </summary>
public const string JaegerExporterAgentHost = "OTEL_EXPORTER_JAEGER_AGENT_HOST";
/// <summary>
/// Port of the Jaeger agent when using UDP (<c>udp/thrift.compact</c> or <c>udp/thrift.binary</c>) transports.
/// </summary>
public const string JaegerExporterAgentPort = "OTEL_EXPORTER_JAEGER_AGENT_PORT";
/// <summary>
/// Full URL of the Jaeger HTTP or gRPC endpoint.
/// </summary>
public const string JaegerExporterEndpoint = "OTEL_EXPORTER_JAEGER_ENDPOINT";
}
/// <summary>
/// Sets up <see cref="JaegerExporterOptions"/> based on environment variables
/// or configuration keys defined by the OpenTelemetry SDK:
/// <c>https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/sdk-environment-variables.md</c>.
/// </summary>
/// <seealso cref="OpenTelemetryEnvironmentVariable"/>
public class JaegerExporterConfigurator : IConfigureOptions<JaegerExporterOptions>
{
private readonly IConfiguration _configuration;
/// <summary>
/// Initializes a new instance of the <see cref="JaegerExporterConfigurator"/> class.
/// </summary>
/// <param name="configuration">
/// The <see cref="IConfiguration"/> with the environment variable values
/// for the Jaeger exporter.
/// </param>
public JaegerExporterConfigurator(IConfiguration configuration)
{
this._configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
}
/// <inheritdoc/>
public void Configure(JaegerExporterOptions options)
{
ArgumentNullException.ThrowIfNull(options);
if (this._configuration.TryGetValue<string>(OpenTelemetryEnvironmentVariable.JaegerExporterProtocol, out var protocol))
{
options.Protocol = JaegerExportProtocolMap.Map(protocol);
}
if (this._configuration.TryGetValue<string>(OpenTelemetryEnvironmentVariable.JaegerExporterAgentHost, out var agentHost))
{
options.AgentHost = agentHost;
}
if (this._configuration.TryGetValue<int>(OpenTelemetryEnvironmentVariable.JaegerExporterAgentPort, out var agentPort))
{
options.AgentPort = agentPort;
}
if (this._configuration.TryGetValue<Uri>(OpenTelemetryEnvironmentVariable.JaegerExporterEndpoint, out var endpoint))
{
if (endpoint == null || !endpoint.IsAbsoluteUri)
{
throw new InvalidOperationException($"Jaeger endpoint must be expressed as an absolute URI. Value '{endpoint}' is not absolute.");
}
options.Endpoint = endpoint;
}
}
} The challenges I ran into were:
For the .NET Framework folks who don't necessarily have public class JaegerExporterOptions
{
public JaegerExporterOptions():
this(new ConfigurationBuilder().AddEnvironmentVariables().Build())
{
}
public JaegerExporterOptions(IConfiguration config)
{
this.Protocol = config["OTEL_EXPORTER_JAEGER_PROTOCOL"].Get<string>();
}
} Obviously I've simplified some error checking there but the concept is that Microsoft.Extensions.Configuration does support .NET 4.6.2+ so it's an option for any reasonably supported .NET desktop framework. There's no need to hardcode right to the environment. (I also haven't tried this from a usage perspective. It may be that as a constructor parameter it's not the best; but I also wonder if having the constructor do the initialization directly rather than using a factory method or something is all that great. A reasonable first step that would help is to have publicly accessible constants classes for environment variable names defined by the OTel spec. They're used internally, they're documented part of the spec, it doesn't seem terribly unreasonable to have them as part of the public API. I think the Finally, I think removing the hard tie to the environment in constructors would be good. If there needs to be a way to get stuff from the environment "by default," a static factory method might be a better way to go. public static JaegerOptions FromEnvironment()
{
var config = new ConfigurationBuilder().AddEnvironmentVariables().Build();
return FromConfiguration(config);
}
public static JaegerOptions FromConfiguration(IConfiguration config)
{
var options = new JaegerOptions();
var configurator = new ConfigureJaegerExporterOptions(config);
configurator.Configure(options);
return options;
} Then people could pick from It doesn't tie anyone to the environment, it doesn't use custom .NET config settings, it still all works for .NET 4.6.2+, it doesn't require anyone actually be using |
None of that works sadly 😢 Options (meaning the Options API) is very strict and requires a public parameter-less constructor which is invoked internally by the pipeline. You can take over the factory used internally, but I think there are easier ways to make this work. I'll try after #3533 and report back what I find! |
This works perfectly as of 1.4.0-beta.3. Thanks! |
Feature Request
Allow the OpenTelemetry environment variables/settings to also be provided by
IConfiguration
instead of directly from the environment.Is your feature request related to a problem?
The OpenTelemetry spec has a set of conventional environment variables to configure various aspects of the system. The current implementation of the libraries here read those values directly from the environment.
It's a common pattern in .NET Core to include environment variables in
IConfiguration
. This allows for merging some default values from JSON with environment settings to make a complete set of values. Being able to provide this merged configuration to the systems that read settings rather than having them read directly from environment would allow for a wider variety of configuration opportunities.Further, it's much easier to set up tests (e.g., ASP.NET
TestHost
integration tests) if you don't have to change environment variables.Describe the solution you'd like:
I'd like to be able to pass
IConfiguration
into anywhere configuration values are read from the environment. For example, in theJaegerConfigurationOptions
class the constructor reads directly from the environment to set things up. Having a constructor that takesIConfiguration
would be helpful and would allow for shared logic to read the keys. Something like...Describe alternatives you've considered.
The only alternative is to build all the duplicate parsing logic myself since our systems hold things like "service name" in JSON config rather than environment variables.
The text was updated successfully, but these errors were encountered: