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

Add support for OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE environment variable #4667

Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
// limitations under the License.
// </copyright>

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using OpenTelemetry.Exporter;
Expand All @@ -26,6 +27,8 @@ namespace OpenTelemetry.Metrics
/// </summary>
public static class OtlpMetricExporterExtensions
{
internal const string OtlpMetricExporterTemporalityPreferenceEnvVarKey = "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE";

/// <summary>
/// Adds <see cref="OtlpMetricExporter"/> to the <see cref="MeterProviderBuilder"/> using default options.
/// </summary>
Expand Down Expand Up @@ -69,6 +72,17 @@ public static MeterProviderBuilder AddOtlpExporter(this MeterProviderBuilder bui
}

OtlpExporterOptions.RegisterOtlpExporterOptionsFactory(services);

services.AddOptions<MetricReaderOptions>(finalOptionsName).Configure<IConfiguration>(
(readerOptions, config) =>
{
var otlpTemporalityPreference = config[OtlpMetricExporterTemporalityPreferenceEnvVarKey];
if (!string.IsNullOrWhiteSpace(otlpTemporalityPreference)
&& Enum.TryParse<MetricReaderTemporalityPreference>(otlpTemporalityPreference, ignoreCase: true, out var enumValue))
{
readerOptions.TemporalityPreference = enumValue;
}
});
});

return builder.AddReader(sp =>
Expand Down Expand Up @@ -135,6 +149,17 @@ public static MeterProviderBuilder AddOtlpExporter(this MeterProviderBuilder bui
builder.ConfigureServices(services =>
{
OtlpExporterOptions.RegisterOtlpExporterOptionsFactory(services);

services.AddOptions<MetricReaderOptions>(name).Configure<IConfiguration>(
(readerOptions, config) =>
{
var otlpTemporalityPreference = config[OtlpMetricExporterTemporalityPreferenceEnvVarKey];
if (!string.IsNullOrWhiteSpace(otlpTemporalityPreference)
&& Enum.TryParse<MetricReaderTemporalityPreference>(otlpTemporalityPreference, ignoreCase: true, out var enumValue))
{
readerOptions.TemporalityPreference = enumValue;
}
});
});

return builder.AddReader(sp =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
// </copyright>

using System.Diagnostics.Metrics;
using System.Reflection;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation;
using OpenTelemetry.Metrics;
Expand Down Expand Up @@ -704,6 +706,50 @@ public void TestHistogramToOtlpMetric(string name, string description, string un
Assert.Empty(dataPoint.Exemplars);
}

[Theory]
[InlineData("cumulative", MetricReaderTemporalityPreference.Cumulative)]
[InlineData("Cumulative", MetricReaderTemporalityPreference.Cumulative)]
[InlineData("CUMULATIVE", MetricReaderTemporalityPreference.Cumulative)]
[InlineData("delta", MetricReaderTemporalityPreference.Delta)]
[InlineData("Delta", MetricReaderTemporalityPreference.Delta)]
[InlineData("DELTA", MetricReaderTemporalityPreference.Delta)]
public void TestTemporalityPreferenceConfiguration(string configValue, MetricReaderTemporalityPreference expectedTemporality)
{
var configData = new Dictionary<string, string> { ["OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE"] = configValue };
Copy link
Member

Choose a reason for hiding this comment

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

nit: Use the constant OtlpMetricExporterExtensions.OtlpMetricExporterTemporalityPreferenceEnvVarKey here (I'm assuming the tests can see internals)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This was done on purpose. If someone accidentally changes the environment variable key name in the src file, then our tests should complain about it. If I use the const string defined in the src file then our tests would simply pass even when the value of the key is changed.

Copy link
Member

Choose a reason for hiding this comment

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

I don't necessarily disagree with your logic, but it does break with all other envvar tests that I can recall seeing. Ex:

Environment.SetEnvironmentVariable(OtlpExporterOptions.EndpointEnvVarName, "http://test:8888");
Environment.SetEnvironmentVariable(OtlpExporterOptions.HeadersEnvVarName, "A=2,B=3");
Environment.SetEnvironmentVariable(OtlpExporterOptions.TimeoutEnvVarName, "2000");
Environment.SetEnvironmentVariable(OtlpExporterOptions.ProtocolEnvVarName, "http/protobuf");

I would probably go with the constant to be consistent and follow DRY principle. If someone does have a valid reason to change it they should only have to do it in one spot.

But I made this a "nit" comment because I figured you might disagree 🤣 Up to you!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think we should think about what do we want to test? If we want to test, that a user a can configure something by providing a value for a particular string in the config, then our tests shouldn't be using the same const as the src file. Our tests would be too dynamic for their own good. 😄

var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(configData)
.Build();

// Check for both the code paths:
// 1. The final extension method which accepts `Action<OtlpExporterOptions>`.
// 2. The final extension method which accepts `Action<OtlpExporterOptions, MetricReaderOptions>`.

// Test 1st code path
using var meterProvider1 = Sdk.CreateMeterProviderBuilder()
.ConfigureServices(services => services.AddSingleton<IConfiguration>(configuration))
.AddOtlpExporter() // This would in turn call the extension method which accepts `Action<OtlpExporterOptions>`
.Build();

var assembly = typeof(Sdk).Assembly;
var type = assembly.GetType("OpenTelemetry.Metrics.MeterProviderSdk");
var fieldInfo = type.GetField("reader", BindingFlags.Instance | BindingFlags.NonPublic);
var reader = fieldInfo.GetValue(meterProvider1) as MetricReader;
var temporality = reader.TemporalityPreference;

Assert.Equal(expectedTemporality, temporality);

// Test 2nd code path
using var meterProvider2 = Sdk.CreateMeterProviderBuilder()
.ConfigureServices(services => services.AddSingleton<IConfiguration>(configuration))
.AddOtlpExporter((_, _) => { }) // This would in turn call the extension method which accepts `Action<OtlpExporterOptions, MetricReaderOptions>`
.Build();

reader = fieldInfo.GetValue(meterProvider2) as MetricReader;
temporality = reader.TemporalityPreference;

Assert.Equal(expectedTemporality, temporality);
}

private static IEnumerable<KeyValuePair<string, object>> ToAttributes(object[] keysValues)
{
var keys = keysValues?.Where((_, index) => index % 2 == 0).ToArray();
Expand Down