Skip to content
This repository has been archived by the owner on Oct 30, 2024. It is now read-only.

Commit

Permalink
feat: add OpenTelemetry tracing hook (#332)
Browse files Browse the repository at this point in the history
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
cwaldren-ld and tanderson-ld authored Apr 17, 2024
1 parent 138f6d7 commit 5932385
Show file tree
Hide file tree
Showing 5 changed files with 399 additions and 2 deletions.
192 changes: 192 additions & 0 deletions src/LaunchDarkly.ServerSdk/Integrations/OpenTelemetry/TracingHook.cs
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;
}
}
}
5 changes: 3 additions & 2 deletions src/LaunchDarkly.ServerSdk/LaunchDarkly.ServerSdk.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,10 @@
<PackageReference Include="System.Collections.Immutable" Version="1.7.1" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'
or '$(TargetFramework)' == 'net462'">
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0' or '$(TargetFramework)' == 'net462'">
<PackageReference Include="System.Text.Json" Version="6.0.0" />
<PackageReference Include="System.Diagnostics.DiagnosticSource" Version="8.0.0" />

<!-- it's a built-in package in net6.0 -->
</ItemGroup>

Expand Down
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();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="YamlDotNet" Version="12.0.0" />
<PackageReference Include="OpenTelemetry" Version="1.7.0" />
<PackageReference Include="OpenTelemetry.Exporter.InMemory" Version="1.7.0" />


</ItemGroup>

<ItemGroup>
Expand Down
Loading

0 comments on commit 5932385

Please sign in to comment.