diff --git a/pkgs/telemetry/src/LaunchDarkly.ServerSdk.Telemetry.csproj b/pkgs/telemetry/src/LaunchDarkly.ServerSdk.Telemetry.csproj
index db6c783f..95ef789a 100644
--- a/pkgs/telemetry/src/LaunchDarkly.ServerSdk.Telemetry.csproj
+++ b/pkgs/telemetry/src/LaunchDarkly.ServerSdk.Telemetry.csproj
@@ -37,7 +37,7 @@
-
+
diff --git a/pkgs/telemetry/src/TracingHook.cs b/pkgs/telemetry/src/TracingHook.cs
index f33acd4c..81323cf0 100644
--- a/pkgs/telemetry/src/TracingHook.cs
+++ b/pkgs/telemetry/src/TracingHook.cs
@@ -16,11 +16,13 @@ public class TracingHookBuilder
{
private bool _createActivities;
private bool _includeVariant;
+ private string _environmentId;
internal TracingHookBuilder()
{
_createActivities = false;
_includeVariant = false;
+ _environmentId = null;
}
///
@@ -51,6 +53,22 @@ public TracingHookBuilder IncludeVariant(bool includeVariant = true)
return this;
}
+ ///
+ /// The environment ID associated with the SDK configuration. In typical usage the environment ID should not be
+ /// specified. The environment ID only needs to be manually specified if it cannot be retrieved from the SDK.
+ ///
+ /// This is not the same as the SDK key. The environment ID is equivalent to the client-side ID in the
+ /// LaunchDarkly UI and documentation.
+ ///
+ ///
+ /// The environment the SDK is configured to connect to.
+ /// this builder
+ public TracingHookBuilder EnvironmentId(string environmentId)
+ {
+ _environmentId = environmentId;
+ return this;
+ }
+
///
/// Builds the with the configured options.
///
@@ -59,7 +77,7 @@ public TracingHookBuilder IncludeVariant(bool includeVariant = true)
/// the new hook
public TracingHook Build()
{
- return new TracingHook(new TracingHook.Options(_createActivities, _includeVariant));
+ return new TracingHook(new TracingHook.Options(_createActivities, _includeVariant, _environmentId));
}
}
@@ -92,6 +110,7 @@ private static class SemanticAttributes
public const string FeatureFlagProviderName = "feature_flag.provider_name";
public const string FeatureFlagVariant = "feature_flag.variant";
public const string FeatureFlagContextKeyAttributeName = "feature_flag.context.key";
+ public const string FeatureFlagSetId = "feature_flag.set.id";
}
internal struct Options
@@ -99,10 +118,13 @@ internal struct Options
public bool CreateActivities { get; }
public bool IncludeVariant { get; }
- public Options(bool createActivities, bool includeVariant)
+ public string EnvironmentId { get; }
+
+ public Options(bool createActivities, bool includeVariant, string environmentId = null)
{
CreateActivities = createActivities;
IncludeVariant = includeVariant;
+ EnvironmentId = environmentId;
}
}
@@ -185,6 +207,15 @@ public override SeriesData AfterEvaluation(EvaluationSeriesContext context, Seri
{SemanticAttributes.FeatureFlagContextKeyAttributeName, context.Context.FullyQualifiedKey},
};
+ if (_options.EnvironmentId != null)
+ {
+ attributes[SemanticAttributes.FeatureFlagSetId] = _options.EnvironmentId;
+ }
+ else if (context.EnvironmentId != null)
+ {
+ attributes[SemanticAttributes.FeatureFlagSetId] = context.EnvironmentId;
+ }
+
if (_options.IncludeVariant)
{
attributes.Add(SemanticAttributes.FeatureFlagVariant, detail.Value.ToJsonString());
diff --git a/pkgs/telemetry/test/TracingHookTests.cs b/pkgs/telemetry/test/TracingHookTests.cs
index 912429a0..70c2402c 100644
--- a/pkgs/telemetry/test/TracingHookTests.cs
+++ b/pkgs/telemetry/test/TracingHookTests.cs
@@ -19,6 +19,13 @@ public void CanConstructTracingHook()
Assert.Equal("LaunchDarkly Tracing Hook", hook.Metadata.Name);
}
+ [Fact]
+ public void CanConstructTracingHookWithEnvironmentId()
+ {
+ var hook = TracingHook.Builder().EnvironmentId("env-123").Build();
+ Assert.NotNull(hook);
+ }
+
[Fact]
public void CanRetrieveActivitySourceName()
{
@@ -64,12 +71,14 @@ public void TracingHookCreatesRootSpans(bool createSpans)
var featureKey = "feature-key";
var context = Context.New("foo");
- var evalContext1 = new EvaluationSeriesContext(featureKey, context, LdValue.Of(true), "LdClient.BoolVariation");
+ var evalContext1 =
+ new EvaluationSeriesContext(featureKey, context, LdValue.Of(true), "LdClient.BoolVariation");
var data1 = hookUnderTest.BeforeEvaluation(evalContext1, new SeriesDataBuilder().Build());
hookUnderTest.AfterEvaluation(evalContext1, data1,
new EvaluationDetail(LdValue.Of(true), 0, EvaluationReason.FallthroughReason));
- var evalContext2 = new EvaluationSeriesContext(featureKey, context, LdValue.Of("default"), "LdClient.StringVariation");
+ var evalContext2 =
+ new EvaluationSeriesContext(featureKey, context, LdValue.Of("default"), "LdClient.StringVariation");
var data2 = hookUnderTest.BeforeEvaluation(evalContext2, new SeriesDataBuilder().Build());
hookUnderTest.AfterEvaluation(evalContext2, data2,
new EvaluationDetail(LdValue.Of("default"), 0, EvaluationReason.FallthroughReason));
@@ -117,12 +126,14 @@ public void TracingHookCreatesChildSpans(bool createSpans)
var rootActivity = testSource.StartActivity("root-activity");
- var evalContext1 = new EvaluationSeriesContext(featureKey, context, LdValue.Of(true), "LdClient.BoolVariation");
+ var evalContext1 =
+ new EvaluationSeriesContext(featureKey, context, LdValue.Of(true), "LdClient.BoolVariation");
var data1 = hookUnderTest.BeforeEvaluation(evalContext1, new SeriesDataBuilder().Build());
hookUnderTest.AfterEvaluation(evalContext1, data1,
new EvaluationDetail(LdValue.Of(true), 0, EvaluationReason.FallthroughReason));
- var evalContext2 = new EvaluationSeriesContext(featureKey, context, LdValue.Of("default"), "LdClient.StringVariation");
+ var evalContext2 =
+ new EvaluationSeriesContext(featureKey, context, LdValue.Of("default"), "LdClient.StringVariation");
var data2 = hookUnderTest.BeforeEvaluation(evalContext2, new SeriesDataBuilder().Build());
hookUnderTest.AfterEvaluation(evalContext2, data2,
new EvaluationDetail(LdValue.Of("default"), 0, EvaluationReason.FallthroughReason));
@@ -173,12 +184,14 @@ public void TracingHookIncludesVariant(bool includeVariant)
var rootActivity = testSource.StartActivity("root-activity");
- var evalContext1 = new EvaluationSeriesContext(featureKey, context, LdValue.Of(true), "LdClient.BoolVariation");
+ var evalContext1 =
+ new EvaluationSeriesContext(featureKey, context, LdValue.Of(true), "LdClient.BoolVariation");
var data1 = hookUnderTest.BeforeEvaluation(evalContext1, new SeriesDataBuilder().Build());
hookUnderTest.AfterEvaluation(evalContext1, data1,
new EvaluationDetail(LdValue.Of(true), 0, EvaluationReason.FallthroughReason));
- var evalContext2 = new EvaluationSeriesContext(featureKey, context, LdValue.Of("default"), "LdClient.StringVariation");
+ var evalContext2 =
+ new EvaluationSeriesContext(featureKey, context, LdValue.Of("default"), "LdClient.StringVariation");
var data2 = hookUnderTest.BeforeEvaluation(evalContext2, new SeriesDataBuilder().Build());
hookUnderTest.AfterEvaluation(evalContext2, data2,
new EvaluationDetail(LdValue.Of("default"), 0, EvaluationReason.FallthroughReason));
@@ -207,5 +220,123 @@ public void TracingHookIncludesVariant(bool includeVariant)
Assert.All(items, i => i.Events.All(e => e.Tags.All(kvp => kvp.Key != "feature_flag.variant")));
}
}
+
+
+ [Fact]
+ public void TracingHookIncludesEnvironmentIdWhenSpecified()
+ {
+ ICollection exportedItems = new Collection();
+
+ _ = OpenTelemetry.Sdk.CreateTracerProviderBuilder()
+ .AddSource("test-source")
+ .SetResourceBuilder(
+ ResourceBuilder.CreateDefault()
+ .AddService(serviceName: "test-source", serviceVersion: "1.0.0"))
+ .AddInMemoryExporter(exportedItems)
+ .Build();
+
+ var testSource = new ActivitySource("test-source", "1.0.0");
+
+ var hookUnderTest = TracingHook.Builder().EnvironmentId("env-123").Build();
+ var featureKey = "feature-key";
+ var context = Context.New("foo");
+
+ var rootActivity = testSource.StartActivity("root-activity");
+
+ var evalContext1 =
+ new EvaluationSeriesContext(featureKey, context, LdValue.Of(true), "LdClient.BoolVariation");
+ var data1 = hookUnderTest.BeforeEvaluation(evalContext1, new SeriesDataBuilder().Build());
+ hookUnderTest.AfterEvaluation(evalContext1, data1,
+ new EvaluationDetail(LdValue.Of(true), 0, EvaluationReason.FallthroughReason));
+
+ rootActivity.Stop();
+
+ var items = exportedItems.ToList();
+
+ Assert.Single(items);
+ Assert.Equal("root-activity", items[0].OperationName);
+
+ var events = items[0].Events;
+ Assert.Single(events.Where(e =>
+ e.Tags.Contains(new KeyValuePair("feature_flag.set.id", "env-123"))));
+ }
+
+ [Fact]
+ public void TracingHookUsesEnvironmentIdFromContext()
+ {
+ ICollection exportedItems = new Collection();
+
+ _ = OpenTelemetry.Sdk.CreateTracerProviderBuilder()
+ .AddSource("test-source")
+ .SetResourceBuilder(
+ ResourceBuilder.CreateDefault()
+ .AddService(serviceName: "test-source", serviceVersion: "1.0.0"))
+ .AddInMemoryExporter(exportedItems)
+ .Build();
+
+ var testSource = new ActivitySource("test-source", "1.0.0");
+
+ var hookUnderTest = TracingHook.Builder().Build();
+ var featureKey = "feature-key";
+ var context = Context.New("foo");
+
+ var rootActivity = testSource.StartActivity("root-activity");
+
+ var evalContext1 = new EvaluationSeriesContext(featureKey, context, LdValue.Of(true),
+ "LdClient.BoolVariation", "env-456");
+ var data1 = hookUnderTest.BeforeEvaluation(evalContext1, new SeriesDataBuilder().Build());
+ hookUnderTest.AfterEvaluation(evalContext1, data1,
+ new EvaluationDetail(LdValue.Of(true), 0, EvaluationReason.FallthroughReason));
+
+ rootActivity.Stop();
+
+ var items = exportedItems.ToList();
+
+ Assert.Single(items);
+ Assert.Equal("root-activity", items[0].OperationName);
+
+ var events = items[0].Events;
+ Assert.Single(events.Where(e =>
+ e.Tags.Contains(new KeyValuePair("feature_flag.set.id", "env-456"))));
+ }
+
+ [Fact]
+ public void TracingHookPrioritizesEnvironmentIdFromOptions()
+ {
+ ICollection exportedItems = new Collection();
+
+ _ = OpenTelemetry.Sdk.CreateTracerProviderBuilder()
+ .AddSource("test-source")
+ .SetResourceBuilder(
+ ResourceBuilder.CreateDefault()
+ .AddService(serviceName: "test-source", serviceVersion: "1.0.0"))
+ .AddInMemoryExporter(exportedItems)
+ .Build();
+
+ var testSource = new ActivitySource("test-source", "1.0.0");
+
+ var hookUnderTest = TracingHook.Builder().EnvironmentId("env-123").Build();
+ var featureKey = "feature-key";
+ var context = Context.New("foo");
+
+ var rootActivity = testSource.StartActivity("root-activity");
+
+ var evalContext1 = new EvaluationSeriesContext(featureKey, context, LdValue.Of(true),
+ "LdClient.BoolVariation", "env-456");
+ var data1 = hookUnderTest.BeforeEvaluation(evalContext1, new SeriesDataBuilder().Build());
+ hookUnderTest.AfterEvaluation(evalContext1, data1,
+ new EvaluationDetail(LdValue.Of(true), 0, EvaluationReason.FallthroughReason));
+
+ rootActivity.Stop();
+
+ var items = exportedItems.ToList();
+
+ Assert.Single(items);
+ Assert.Equal("root-activity", items[0].OperationName);
+
+ var events = items[0].Events;
+ Assert.Single(events.Where(e =>
+ e.Tags.Contains(new KeyValuePair("feature_flag.set.id", "env-123"))));
+ }
}
}