Skip to content

Commit 5e3b1e1

Browse files
authored
feat: Add support for environment variable configuration. (#205)
## Summary Add support for common OTEL environment variables. The underlying OTEL libraries support most variables, but our default configuration would interfere with these specific ones without explicit support. ## How did you test this change? <!-- Frontend - Leave a screencast or a screenshot to visually describe the changes. --> ## Are there any deployment considerations? <!-- Backend - Do we need to consider migrations or backfilling data? -->
1 parent b215109 commit 5e3b1e1

File tree

5 files changed

+241
-8
lines changed

5 files changed

+241
-8
lines changed

sdk/@launchdarkly/observability-dotnet/src/LaunchDarkly.Observability/BaseBuilder.cs

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ public class BaseBuilder<TBuilder> where TBuilder : BaseBuilder<TBuilder>
2323
{
2424
private const string DefaultOtlpEndpoint = "https://otel.observability.app.launchdarkly.com:4318";
2525
private const string DefaultBackendUrl = "https://pub.observability.app.launchdarkly.com";
26-
private string _otlpEndpoint = DefaultOtlpEndpoint;
27-
private string _backendUrl = DefaultBackendUrl;
26+
private string _otlpEndpoint = string.Empty;
27+
private string _backendUrl = string.Empty;
2828
private string _serviceName = string.Empty;
2929
private string _environment = string.Empty;
3030
private string _serviceVersion = string.Empty;
@@ -42,14 +42,19 @@ protected BaseBuilder()
4242
/// For most configurations, the OTLP endpoint will not need to be set.
4343
/// </para>
4444
/// <para>
45+
/// If not explicitly set, set to null, or set to whitespace/empty string, the OTLP endpoint will be read from
46+
/// the OTEL_EXPORTER_OTLP_ENDPOINT environment variable. Values set with this method take precedence over the
47+
/// environment variable.
48+
/// </para>
49+
/// <para>
4550
/// Setting the endpoint to null will reset the builder value to the default.
4651
/// </para>
4752
/// </summary>
4853
/// <param name="otlpEndpoint">The OTLP exporter endpoint URL.</param>
4954
/// <returns>A reference to this builder.</returns>
5055
public TBuilder WithOtlpEndpoint(string otlpEndpoint)
5156
{
52-
_otlpEndpoint = otlpEndpoint ?? DefaultOtlpEndpoint;
57+
_otlpEndpoint = otlpEndpoint;
5358
return (TBuilder)this;
5459
}
5560

@@ -66,12 +71,17 @@ public TBuilder WithOtlpEndpoint(string otlpEndpoint)
6671
/// <returns>A reference to this builder.</returns>
6772
public TBuilder WithBackendUrl(string backendUrl)
6873
{
69-
_backendUrl = backendUrl ?? DefaultBackendUrl;
74+
_backendUrl = backendUrl;
7075
return (TBuilder)this;
7176
}
7277

7378
/// <summary>
7479
/// Set the service name.
80+
/// <para>
81+
/// If not explicitly set, set to null, or set to whitespace/empty string, the service name will be read from
82+
/// the OTEL_SERVICE_NAME environment variable. Values set with this method take precedence over the environment
83+
/// variable.
84+
/// </para>
7585
/// </summary>
7686
/// <param name="serviceName">The logical service name used in telemetry resource attributes.</param>
7787
/// <returns>A reference to this builder.</returns>
@@ -234,10 +244,22 @@ internal ObservabilityConfig BuildConfig(string sdkKey)
234244
"SDK key cannot be null when creating an ObservabilityConfig builder.");
235245
}
236246

237-
return new ObservabilityConfig(
238-
_otlpEndpoint,
239-
_backendUrl,
247+
var effectiveServiceName = EnvironmentHelper.GetValueOrEnvironment(
240248
_serviceName,
249+
EnvironmentVariables.OtelServiceName,
250+
string.Empty);
251+
252+
var effectiveOtlpEndpoint = EnvironmentHelper.GetValueOrEnvironment(
253+
_otlpEndpoint,
254+
EnvironmentVariables.OtelExporterOtlpEndpoint,
255+
DefaultOtlpEndpoint);
256+
257+
var effectiveBackendUrl = string.IsNullOrWhiteSpace(_backendUrl) ? DefaultBackendUrl : _backendUrl;
258+
259+
return new ObservabilityConfig(
260+
effectiveOtlpEndpoint,
261+
effectiveBackendUrl,
262+
effectiveServiceName,
241263
_environment,
242264
_serviceVersion,
243265
sdkKey,
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
using System;
2+
3+
namespace LaunchDarkly.Observability
4+
{
5+
/// <summary>
6+
/// Helper methods for working with environment variables.
7+
/// </summary>
8+
internal static class EnvironmentHelper
9+
{
10+
/// <summary>
11+
/// Returns the provided value if it's not null or empty, otherwise attempts to get the value from the specified environment variable.
12+
/// If the environment variable is also not set, returns the default value.
13+
/// </summary>
14+
/// <param name="value">The primary value to use.</param>
15+
/// <param name="environmentVariable">The name of the environment variable to check if the primary value is null or empty.</param>
16+
/// <param name="defaultValue">The default value to use if both the primary value and environment variable are not set.</param>
17+
/// <returns>The resolved value based on the precedence: primary value > environment variable > default value.</returns>
18+
public static string GetValueOrEnvironment(string value, string environmentVariable, string defaultValue = "")
19+
{
20+
if (!string.IsNullOrWhiteSpace(value))
21+
{
22+
return value;
23+
}
24+
25+
var envValue = Environment.GetEnvironmentVariable(environmentVariable);
26+
return !string.IsNullOrWhiteSpace(envValue) ? envValue : defaultValue;
27+
}
28+
}
29+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
namespace LaunchDarkly.Observability
2+
{
3+
/// <summary>
4+
/// Contains constants for environment variable names used by the Observability SDK.
5+
/// </summary>
6+
internal static class EnvironmentVariables
7+
{
8+
/// <summary>
9+
/// The OpenTelemetry standard environment variable for the service name.
10+
/// When not explicitly set via WithServiceName(), this environment variable will be used.
11+
/// </summary>
12+
public const string OtelServiceName = "OTEL_SERVICE_NAME";
13+
14+
/// <summary>
15+
/// The OpenTelemetry standard environment variable for the OTLP exporter endpoint.
16+
/// When not explicitly set via WithOtlpEndpoint(), this environment variable will be used.
17+
/// </summary>
18+
public const string OtelExporterOtlpEndpoint = "OTEL_EXPORTER_OTLP_ENDPOINT";
19+
}
20+
}

sdk/@launchdarkly/observability-dotnet/test/LaunchDarkly.Observability.Tests/ObservabilityConfigBuilderTests.cs

Lines changed: 113 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
using System;
2-
using LaunchDarkly.Observability;
32
using NUnit.Framework;
43
using OpenTelemetry.Logs;
54
using OpenTelemetry.Metrics;
@@ -10,6 +9,13 @@ namespace LaunchDarkly.Observability.Test
109
[TestFixture]
1110
public class ObservabilityConfigBuilderTests
1211
{
12+
[SetUp]
13+
public void SetUp()
14+
{
15+
Environment.SetEnvironmentVariable(EnvironmentVariables.OtelExporterOtlpEndpoint, null);
16+
Environment.SetEnvironmentVariable(EnvironmentVariables.OtelServiceName, null);
17+
}
18+
1319
[Test]
1420
public void Build_WithAllFields_SetsValues()
1521
{
@@ -256,5 +262,111 @@ public void WithExtendedConfigurations_NullActionsAreAccepted()
256262
Assert.That(config.ExtendedMeterConfiguration, Is.Null);
257263
});
258264
}
265+
266+
[Test]
267+
public void Build_UsesOtelServiceNameEnvironmentVariable_WhenServiceNameNotSet()
268+
{
269+
Environment.SetEnvironmentVariable(EnvironmentVariables.OtelServiceName, "service-from-env");
270+
271+
// Build config without setting service name explicitly
272+
var config = ObservabilityConfig.Builder().Build("sdk-key");
273+
274+
// Should use the environment variable value
275+
Assert.That(config.ServiceName, Is.EqualTo("service-from-env"));
276+
}
277+
278+
[Test]
279+
public void Build_PrefersExplicitServiceName_OverEnvironmentVariable()
280+
{
281+
Environment.SetEnvironmentVariable(EnvironmentVariables.OtelServiceName, "service-from-env");
282+
283+
// Build config with explicit service name
284+
var config = ObservabilityConfig.Builder()
285+
.WithServiceName("explicit-service")
286+
.Build("sdk-key");
287+
288+
// Should use the explicitly set value, not the environment variable
289+
Assert.That(config.ServiceName, Is.EqualTo("explicit-service"));
290+
}
291+
292+
[Test]
293+
public void Build_HandlesAbsentOtelServiceNameEnvironmentVariable()
294+
{
295+
Environment.SetEnvironmentVariable(EnvironmentVariables.OtelServiceName, null);
296+
297+
// Build config without setting service name
298+
var config = ObservabilityConfig.Builder().Build("sdk-key");
299+
300+
// Should use empty string as default
301+
Assert.That(config.ServiceName, Is.EqualTo(string.Empty));
302+
}
303+
304+
[Test]
305+
public void Build_WithServiceNameSetToNull_UsesEnvironmentVariable()
306+
{
307+
Environment.SetEnvironmentVariable(EnvironmentVariables.OtelServiceName, "service-from-env");
308+
309+
// Build config with service name explicitly set to null (which becomes empty string in WithServiceName)
310+
var config = ObservabilityConfig.Builder()
311+
.WithServiceName(null)
312+
.Build("sdk-key");
313+
314+
// Should use the environment variable since WithServiceName(null) sets it to empty string
315+
Assert.That(config.ServiceName, Is.EqualTo("service-from-env"));
316+
}
317+
318+
[Test]
319+
public void Build_UsesOtelExporterOtlpEndpointEnvironmentVariable_WhenOtlpEndpointNotSet()
320+
{
321+
Environment.SetEnvironmentVariable(EnvironmentVariables.OtelExporterOtlpEndpoint,
322+
"https://custom-otlp.example.com:4318");
323+
324+
// Build config without setting OTLP endpoint explicitly
325+
var config = ObservabilityConfig.Builder().Build("sdk-key");
326+
327+
// Should use the environment variable value
328+
Assert.That(config.OtlpEndpoint, Is.EqualTo("https://custom-otlp.example.com:4318"));
329+
}
330+
331+
[Test]
332+
public void Build_PrefersExplicitOtlpEndpoint_OverEnvironmentVariable()
333+
{
334+
Environment.SetEnvironmentVariable(EnvironmentVariables.OtelExporterOtlpEndpoint,
335+
"https://env-otlp.example.com:4318");
336+
337+
// Build config with explicit OTLP endpoint
338+
var config = ObservabilityConfig.Builder()
339+
.WithOtlpEndpoint("https://explicit-otlp.example.com:4318")
340+
.Build("sdk-key");
341+
342+
// Should use the explicitly set value, not the environment variable
343+
Assert.That(config.OtlpEndpoint, Is.EqualTo("https://explicit-otlp.example.com:4318"));
344+
}
345+
346+
[Test]
347+
public void Build_HandlesAbsentOtelExporterOtlpEndpointEnvironmentVariable()
348+
{
349+
// Clear the environment variable
350+
Environment.SetEnvironmentVariable(EnvironmentVariables.OtelExporterOtlpEndpoint, null);
351+
352+
// Build config without setting OTLP endpoint
353+
var config = ObservabilityConfig.Builder().Build("sdk-key");
354+
355+
// Should use default OTLP endpoint
356+
Assert.That(config.OtlpEndpoint, Is.EqualTo("https://otel.observability.app.launchdarkly.com:4318"));
357+
}
358+
359+
[Test]
360+
public void Build_WithOtlpEndpointSetToNull_UsesEnvironmentVariableIfSet()
361+
{
362+
Environment.SetEnvironmentVariable(EnvironmentVariables.OtelExporterOtlpEndpoint,
363+
"https://env-otlp.example.com:4318");
364+
365+
var config = ObservabilityConfig.Builder()
366+
.WithOtlpEndpoint(null)
367+
.Build("sdk-key");
368+
369+
Assert.That(config.OtlpEndpoint, Is.EqualTo("https://env-otlp.example.com:4318"));
370+
}
259371
}
260372
}

sdk/@launchdarkly/observability-dotnet/test/LaunchDarkly.Observability.Tests/ObservabilityPluginBuilderTests.cs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,5 +65,55 @@ public void Build_WithNullValues_HandlesNullsCorrectly()
6565

6666
Assert.That(plugin, Is.InstanceOf<ObservabilityPlugin>());
6767
}
68+
69+
[Test]
70+
public void Build_UsesOtelServiceNameEnvironmentVariable_WhenServiceNameNotSet()
71+
{
72+
// Save the original environment variable value
73+
var originalValue = Environment.GetEnvironmentVariable(EnvironmentVariables.OtelServiceName);
74+
75+
try
76+
{
77+
// Set the environment variable
78+
Environment.SetEnvironmentVariable(EnvironmentVariables.OtelServiceName, "plugin-service-from-env");
79+
80+
// Build plugin without setting service name explicitly
81+
var plugin = ObservabilityPlugin.Builder(_services).Build();
82+
83+
// Plugin should be created successfully and will use the env var internally
84+
Assert.That(plugin, Is.InstanceOf<ObservabilityPlugin>());
85+
}
86+
finally
87+
{
88+
// Restore the original environment variable value
89+
Environment.SetEnvironmentVariable(EnvironmentVariables.OtelServiceName, originalValue);
90+
}
91+
}
92+
93+
[Test]
94+
public void Build_PrefersExplicitServiceName_OverEnvironmentVariable()
95+
{
96+
// Save the original environment variable value
97+
var originalValue = Environment.GetEnvironmentVariable(EnvironmentVariables.OtelServiceName);
98+
99+
try
100+
{
101+
// Set the environment variable
102+
Environment.SetEnvironmentVariable(EnvironmentVariables.OtelServiceName, "plugin-service-from-env");
103+
104+
// Build plugin with explicit service name
105+
var plugin = ObservabilityPlugin.Builder(_services)
106+
.WithServiceName("explicit-plugin-service")
107+
.Build();
108+
109+
// Plugin should be created successfully and will use the explicit value internally
110+
Assert.That(plugin, Is.InstanceOf<ObservabilityPlugin>());
111+
}
112+
finally
113+
{
114+
// Restore the original environment variable value
115+
Environment.SetEnvironmentVariable(EnvironmentVariables.OtelServiceName, originalValue);
116+
}
117+
}
68118
}
69119
}

0 commit comments

Comments
 (0)