This repository has been archived by the owner on Oct 30, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 25
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add OpenTelemetry tracing hook (#332)
This can either be released with the main hook feature, or at a later date. If later, then we should re-target to main once hooks is merged. --------- Co-authored-by: Todd Anderson <[email protected]>
- Loading branch information
1 parent
138f6d7
commit 5932385
Showing
5 changed files
with
399 additions
and
2 deletions.
There are no files selected for viewing
192 changes: 192 additions & 0 deletions
192
src/LaunchDarkly.ServerSdk/Integrations/OpenTelemetry/TracingHook.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,192 @@ | ||
using System.Collections.Immutable; | ||
using System.Diagnostics; | ||
using LaunchDarkly.Sdk.Internal; | ||
using LaunchDarkly.Sdk.Server.Hooks; | ||
|
||
namespace LaunchDarkly.Sdk.Server.Integrations.OpenTelemetry | ||
{ | ||
|
||
using SeriesData = ImmutableDictionary<string, object>; | ||
|
||
/// <summary> | ||
/// TracingHookBuilder creates a <see cref="TracingHook"/>s. | ||
/// </summary> | ||
public class TracingHookBuilder | ||
{ | ||
private bool _createActivities; | ||
private bool _includeVariant; | ||
|
||
internal TracingHookBuilder() | ||
{ | ||
_createActivities = false; | ||
_includeVariant = false; | ||
} | ||
|
||
/// <summary> | ||
/// The TracingHook will create <see cref="Activity"/>s for flag evaluations. | ||
/// The activities will be children of the current activity, if one exists, or root activities. | ||
/// Disabled by default. | ||
/// | ||
/// NOTE: This is an experimental option; it may be removed and behavior is | ||
/// subject to change within minor versions. | ||
/// </summary> | ||
/// <param name="createActivities">true to create activities, false otherwise</param> | ||
/// <returns>this builder</returns> | ||
public TracingHookBuilder CreateActivities(bool createActivities = true) | ||
{ | ||
_createActivities = createActivities; | ||
return this; | ||
} | ||
|
||
/// <summary> | ||
/// The TracingHook will include the flag variant in the current activity, if one exists. | ||
/// The variant representation is a JSON string. Disabled by default. | ||
/// </summary> | ||
/// <param name="includeVariant">true to include variants, false otherwise</param> | ||
/// <returns>this builder</returns> | ||
public TracingHookBuilder IncludeVariant(bool includeVariant = true) | ||
{ | ||
_includeVariant = includeVariant; | ||
return this; | ||
} | ||
|
||
/// <summary> | ||
/// Builds the <see cref="TracingHook"/> with the configured options. | ||
/// | ||
/// The hook may be passed into the SDK's Hook configuration as-is. | ||
/// </summary> | ||
/// <returns>the new hook</returns> | ||
public TracingHook Build() | ||
{ | ||
return new TracingHook(new TracingHook.Options(_createActivities, _includeVariant)); | ||
} | ||
} | ||
|
||
/// <summary> | ||
/// TracingHook is a <see cref="Hook"/> that adds tracing capabilities to the LaunchDarkly SDK for feature flag | ||
/// evaluations. | ||
/// </summary> | ||
public class TracingHook : Hook | ||
{ | ||
|
||
/// <summary> | ||
/// Used to create new activities if the TracingHook is configured to create them. | ||
/// </summary> | ||
private static readonly ActivitySource Source = new ActivitySource("LaunchDarkly.ServerSdk", | ||
AssemblyVersions.GetAssemblyVersionForType(typeof(LdClient)).ToString()); | ||
|
||
/// <summary> | ||
/// Returns the name of the ActivitySource that the TracingHook uses to generate Activities. | ||
/// </summary> | ||
public static string ActivitySourceName => Source.Name; | ||
|
||
private static class SemanticAttributes | ||
{ | ||
public const string EventName = "feature_flag"; | ||
public const string FeatureFlagKey = "feature_flag.key"; | ||
public const string FeatureFlagProviderName = "feature_flag.provider_name"; | ||
public const string FeatureFlagVariant = "feature_flag.variant"; | ||
public const string FeatureFlagContextKeyAttributeName = "feature_flag.context.key"; | ||
} | ||
|
||
internal struct Options | ||
{ | ||
public bool CreateActivities { get; } | ||
public bool IncludeVariant { get; } | ||
|
||
public Options(bool createActivities, bool includeVariant) | ||
{ | ||
CreateActivities = createActivities; | ||
IncludeVariant = includeVariant; | ||
} | ||
} | ||
|
||
private readonly Options _options; | ||
|
||
private const string ActivityFieldKey = "evalActivity"; | ||
|
||
internal TracingHook(Options options) : base("LaunchDarkly Tracing Hook") | ||
{ | ||
_options = options; | ||
} | ||
|
||
/// <summary> | ||
/// Returns a <see cref="TracingHookBuilder"/> which can be used to create a <see cref="TracingHook"/>. | ||
/// | ||
/// </summary> | ||
/// <returns>the builder</returns> | ||
public static TracingHookBuilder Builder() => new TracingHookBuilder(); | ||
|
||
/// <summary> | ||
/// Returns the default TracingHook. By default, the hook will attach an event to the current activity. | ||
/// | ||
/// To change the configuration, see <see cref="Builder"/>. | ||
/// </summary> | ||
/// <returns></returns> | ||
public static TracingHook Default() => Builder().Build(); | ||
|
||
/// <summary> | ||
/// Optionally creates a new Activity for the evaluation of a feature flag. | ||
/// </summary> | ||
/// <param name="context">the evaluation parameters</param> | ||
/// <param name="data">the series data</param> | ||
/// <returns>unchanged data if CreateActivities is disabled, or data containing a reference to the created activity</returns> | ||
public override SeriesData BeforeEvaluation(EvaluationSeriesContext context, SeriesData data) | ||
{ | ||
if (!_options.CreateActivities) return data; | ||
|
||
var attrs = new ActivityTagsCollection | ||
{ | ||
{ SemanticAttributes.FeatureFlagKey, context.FlagKey }, | ||
{ SemanticAttributes.FeatureFlagContextKeyAttributeName, context.Context.FullyQualifiedKey } | ||
}; | ||
|
||
// If there is a parent activity, then our new activity should be a child of it. | ||
// Otherwise, our new activity will be a root activity. | ||
var parentContext = Activity.Current?.Context ?? new ActivityContext(); | ||
|
||
// This is an internal activity because LaunchDarkly SDK usage is an internal operation of an application. | ||
var activity = Source.StartActivity( context.Method, ActivityKind.Internal, parentContext, attrs); | ||
return new SeriesDataBuilder(data).Set(ActivityFieldKey, activity).Build(); | ||
} | ||
|
||
/// <summary> | ||
/// Ends the activity created in BeforeEvaluation, if it exists. Adds the feature flag key, provider name, and context key | ||
/// to the existing activity. If IncludeVariant is enabled, also adds the variant. | ||
/// </summary> | ||
/// <param name="context">the evaluation parameters</param> | ||
/// <param name="data">the series data</param> | ||
/// <param name="detail">the evaluation details</param> | ||
/// <returns></returns> | ||
public override SeriesData AfterEvaluation(EvaluationSeriesContext context, SeriesData data, EvaluationDetail<LdValue> detail) | ||
{ | ||
if (_options.CreateActivities && data.TryGetValue(ActivityFieldKey, out var value)) | ||
{ | ||
try | ||
{ | ||
var activity = (Activity) value; | ||
activity?.Stop(); | ||
} | ||
catch (System.InvalidCastException) | ||
{ | ||
// This should never happen, but if it does, don't crash the application. | ||
} | ||
} | ||
|
||
var attributes = new ActivityTagsCollection | ||
{ | ||
{SemanticAttributes.FeatureFlagKey, context.FlagKey}, | ||
{SemanticAttributes.FeatureFlagProviderName, "LaunchDarkly"}, | ||
{SemanticAttributes.FeatureFlagContextKeyAttributeName, context.Context.FullyQualifiedKey}, | ||
}; | ||
|
||
if (_options.IncludeVariant) | ||
{ | ||
attributes.Add(SemanticAttributes.FeatureFlagVariant, detail.Value.ToJsonString()); | ||
} | ||
|
||
Activity.Current?.AddEvent(new ActivityEvent(name: SemanticAttributes.EventName, tags: attributes)); | ||
return data; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
45 changes: 45 additions & 0 deletions
45
test/LaunchDarkly.ServerSdk.Tests/Integrations/OpenTelemetry/TracingHook.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
using LaunchDarkly.Sdk.Server.Hooks; | ||
using Xunit; | ||
|
||
namespace LaunchDarkly.Sdk.Server.Integrations.OpenTelemetry | ||
{ | ||
public class TestTracingHook | ||
{ | ||
[Fact] | ||
public void CanConstructTracingHook() | ||
{ | ||
var hook = TracingHook.Default(); | ||
Assert.NotNull(hook); | ||
Assert.Equal("LaunchDarkly Tracing Hook", hook.Metadata.Name); | ||
} | ||
|
||
[Fact] | ||
public void CanRetrieveActivitySourceName() | ||
{ | ||
Assert.NotEmpty(TracingHook.ActivitySourceName); | ||
} | ||
|
||
[Theory] | ||
[InlineData(false, false)] | ||
[InlineData(false, true)] | ||
[InlineData(true, false)] | ||
[InlineData(true, true)] | ||
public void ConfigurationOptionsDoNotThrowExceptions(bool includeVariant, bool createSpans) | ||
{ | ||
var hook = TracingHook.Builder() | ||
.IncludeVariant(includeVariant) | ||
.CreateActivities(createSpans) | ||
.Build(); | ||
var context = new EvaluationSeriesContext("foo", Context.New("bar"), LdValue.Null, "testMethod"); | ||
var data = hook.BeforeEvaluation(context, new SeriesDataBuilder().Build()); | ||
hook.AfterEvaluation(context, data, new EvaluationDetail<LdValue>()); | ||
} | ||
|
||
[Fact] | ||
public void CallingDisposeDoesNotThrowException() | ||
{ | ||
var hook = TracingHook.Default(); | ||
hook.Dispose(); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.