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")))); + } } }