Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
e7d40c7
Added metrics hook.
askpt Dec 31, 2024
46e5711
Adding the traces hook.
askpt Dec 31, 2024
ced2d34
Adding Metrics Hook tests.
askpt Dec 31, 2024
633859c
Adding tracing hook tests.
askpt Dec 31, 2024
6d4378f
Fix typos.
askpt Dec 31, 2024
8a32ca9
Simplify code.
askpt Dec 31, 2024
629b900
Cleanup tracing hook tests.
askpt Dec 31, 2024
766caa5
More test cleanup.
askpt Dec 31, 2024
703f418
Cleanup tests.
askpt Dec 31, 2024
729bd65
Adding empty span.
askpt Dec 31, 2024
7f5af18
Adding .ConfigureAwait
askpt Dec 31, 2024
c0b6cc0
Fix dotnet format.
askpt Dec 31, 2024
d0157ae
Removed comma
askpt Dec 31, 2024
902a102
Adding README information.
askpt Dec 31, 2024
60813c5
Merge branch 'main' into askpt/175-promote-otel-hooks
askpt Dec 31, 2024
6a3d911
Using the new API.
askpt Dec 31, 2024
52b47da
Fix subscribed Meter.
askpt Dec 31, 2024
8e1e884
Merge branch 'main' into askpt/175-promote-otel-hooks
askpt Jan 6, 2025
1a0c16d
Merge branch 'main' into askpt/175-promote-otel-hooks
askpt Jan 7, 2025
41c915d
Fix breaking change.
askpt Jan 7, 2025
4bf70d2
Merge branch 'main' into askpt/175-promote-otel-hooks
askpt Feb 26, 2025
7b6ef96
Merge branch 'main' into askpt/175-promote-otel-hooks
askpt Mar 5, 2025
066706d
Merge branch 'main' into askpt/175-promote-otel-hooks
askpt Apr 8, 2025
5cfb99b
Merge branch 'main' into askpt/175-promote-otel-hooks
askpt Apr 8, 2025
a39ebbd
Merge branch 'main' into askpt/175-promote-otel-hooks
askpt Apr 28, 2025
415de4d
feat: add System.Diagnostics.DiagnosticSource package reference and u…
askpt Apr 28, 2025
4a3bacf
Merge branch 'main' into askpt/175-promote-otel-hooks
askpt May 12, 2025
dd86dd5
Merge branch 'main' into askpt/175-promote-otel-hooks
askpt May 19, 2025
94a1fcb
refactor: Replace MetricsConstants with TelemetryConstants in Metrics…
askpt May 19, 2025
a8789f4
refactor: Enhance documentation for TracingHook methods and clarify e…
askpt May 19, 2025
d34ede3
fix: Update return statements in TracingHook to call base methods for…
askpt May 19, 2025
345a164
fix: Correct documentation and update tag keys in TracingHook tests f…
askpt May 19, 2025
bc38b0c
refactor: Optimize Meter initialization in MetricsHook constructor fo…
askpt May 20, 2025
cd9811e
fix: Correct indentation in README.md for OpenTelemetry OTLP exporter…
askpt May 20, 2025
ed68e95
Merge branch 'main' into askpt/175-promote-otel-hooks
askpt May 21, 2025
fe05709
Merge branch 'main' into askpt/175-promote-otel-hooks
askpt Jun 4, 2025
3309850
chore: remove unused usings from hook files
askpt Jun 4, 2025
904fd44
chore: remove variant assignment from MetricsHook
askpt Jun 4, 2025
0200319
feat: add additional tags for flag evaluation details in TracingHook
askpt Jun 23, 2025
ed543dd
feat: add value and reason tags to telemetry in TracingHook
askpt Jun 23, 2025
5309232
fix: update remarks for MetricsHook and TracingHook to clarify experi…
askpt Jun 23, 2025
4359a37
refactor: rename AfterAsync to FinallyAsync in TracingHook and update…
askpt Jun 23, 2025
8038a69
Merge branch 'main' into askpt/175-promote-otel-hooks
askpt Jun 23, 2025
d3cf9d2
feat: add TraceEnricherHook and corresponding tests for telemetry tra…
askpt Jun 25, 2025
b8b3436
refactor: remove ErrorAsync method from TraceEnricherHook to streamli…
askpt Jun 25, 2025
46fcf38
refactor: update event name in TraceEnricherHook and remove unused Tr…
askpt Jun 25, 2025
cc5fde8
Merge branch 'main' into askpt/175-promote-otel-hooks
askpt Jun 25, 2025
4a3d3bc
refactor: streamline event tagging in TraceEnricherHook by using Eval…
askpt Jun 25, 2025
fea94e9
refactor: update event name and enhance assertions in TraceEnricherHo…
askpt Jun 25, 2025
78044d1
refactor: rename variable for clarity in TraceEnricherHookTests
askpt Jun 25, 2025
1a7f62a
docs: improve README formatting and enhance Trace Enricher Hook descr…
askpt Jun 25, 2025
a0319e4
feat: integrate OpenTelemetry for tracing and metrics in ASP.NET Core…
askpt Jun 25, 2025
2b4306a
Merge branch 'main' into askpt/175-promote-otel-hooks
askpt Jun 27, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="$(MicrosoftExtensionsVersion)" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="$(MicrosoftExtensionsVersion)" />
<PackageVersion Include="System.Collections.Immutable" Version="$(MicrosoftExtensionsVersion)" />
<PackageVersion Include="System.Diagnostics.DiagnosticSource" Version="$(MicrosoftExtensionsVersion)" />
<PackageVersion Include="System.Threading.Channels" Version="$(MicrosoftExtensionsVersion)" />
<PackageVersion Include="System.ValueTuple" Version="4.6.1" />
</ItemGroup>
Expand All @@ -29,6 +30,8 @@
<PackageVersion Include="Microsoft.Extensions.Diagnostics.Testing" Version="9.3.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageVersion Include="NSubstitute" Version="5.3.0" />
<PackageVersion Include="OpenTelemetry" Version="1.11.2" />
<PackageVersion Include="OpenTelemetry.Exporter.InMemory" Version="1.11.2" />
<PackageVersion Include="Reqnroll.xUnit" Version="2.4.1" />
<PackageVersion Include="xunit" Version="2.9.3" />
<PackageVersion Include="xunit.runner.visualstudio" Version="2.8.2" />
Expand All @@ -39,4 +42,4 @@
<PackageVersion Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.3" />
</ItemGroup>

</Project>
</Project>
147 changes: 138 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,23 +79,23 @@ public async Task Example()

The [`samples/`](./samples) folder contains example applications demonstrating how to use OpenFeature in different .NET scenarios.

| Sample Name | Description |
|---------------------------------------------------|----------------------------------------------------------------|
| [AspNetCore](/samples/AspNetCore/README.md) | Feature flags in an ASP.NET Core Web API. |
| Sample Name | Description |
| ------------------------------------------- | ----------------------------------------- |
| [AspNetCore](/samples/AspNetCore/README.md) | Feature flags in an ASP.NET Core Web API. |

**Getting Started with a Sample:**

1. Navigate to the sample directory

```shell
cd samples/AspNetCore
```
```shell
cd samples/AspNetCore
```

2. Restore dependencies and run

```shell
dotnet run
```
```shell
dotnet run
```

Want to contribute a new sample? See our [CONTRIBUTING](CONTRIBUTING.md) guide!

Expand Down Expand Up @@ -534,6 +534,135 @@ services.AddOpenFeature(builder =>
});
```

### Trace Enricher Hook

The `TraceEnricherHook` enriches telemetry traces with additional information during the feature flag evaluation lifecycle. This hook adds relevant flag evaluation details as tags and events to the current `Activity` for tracing purposes.

For this hook to function correctly, an active span must be set in the current `Activity`, otherwise the hook will no-op.

Below are the tags added to the trace event:

| Tag Name | Description | Source |
| --------------------------- | ---------------------------------------------------------------------------- | ----------------------------- |
| feature_flag.key | The lookup key of the feature flag | Hook context flag key |
| feature_flag.provider.name | The name of the feature flag provider | Provider metadata |
| feature_flag.result.reason | The reason code which shows how a feature flag value was determined | Evaluation details |
| feature_flag.result.variant | A semantic identifier for an evaluated flag value | Evaluation details |
| feature_flag.result.value | The evaluated value of the feature flag | Evaluation details |
| feature_flag.context.id | The unique identifier for the flag evaluation context | Flag metadata (if available) |
| feature_flag.set.id | The identifier of the flag set to which the feature flag belongs | Flag metadata (if available) |
| feature_flag.version | The version of the ruleset used during the evaluation | Flag metadata (if available) |
| error.type | Describes a class of error the operation ended with | Evaluation details (if error) |
| error.message | A message explaining the nature of an error occurring during flag evaluation | Evaluation details (if error) |

#### Example

The following example demonstrates the use of the `TraceEnricherHook` with the `OpenFeature dotnet-sdk`. The traces are sent to a `jaeger` OTLP collector running at `localhost:4317`.

```csharp
using OpenFeature.Contrib.Providers.Flagd;
using OpenFeature.Hooks;
using OpenTelemetry.Exporter;
using OpenTelemetry.Resources;
using OpenTelemetry;
using OpenTelemetry.Trace;

namespace OpenFeatureTestApp
{
class Hello {
static void Main(string[] args) {

// set up the OpenTelemetry OTLP exporter
var tracerProvider = Sdk.CreateTracerProviderBuilder()
.AddSource("my-tracer")
.ConfigureResource(r => r.AddService("jaeger-test"))
.AddOtlpExporter(o =>
{
o.ExportProcessorType = ExportProcessorType.Simple;
})
.Build();

// add the TraceEnricherHook to the OpenFeature instance
OpenFeature.Api.Instance.AddHooks(new TraceEnricherHook());

var flagdProvider = new FlagdProvider(new Uri("http://localhost:8013"));

// Set the flagdProvider as the provider for the OpenFeature SDK
OpenFeature.Api.Instance.SetProvider(flagdProvider);

var client = OpenFeature.Api.Instance.GetClient("my-app");

var val = client.GetBooleanValueAsync("myBoolFlag", false, null);

// Print the value of the 'myBoolFlag' feature flag
System.Console.WriteLine(val.Result.ToString());
}
}
}
```

After running this example, you will be able to see the traces, including the events sent by the hook in your Jaeger UI.

### Metrics Hook

For this hook to function correctly a global `MeterProvider` must be set.
`MetricsHook` performs metric collection by tapping into various hook stages.

Below are the metrics extracted by this hook and dimensions they carry:

| Metric key | Description | Unit | Dimensions |
| -------------------------------------- | ------------------------------- | ------------ | ----------------------------- |
| feature_flag.evaluation_requests_total | Number of evaluation requests | {request} | key, provider name |
| feature_flag.evaluation_success_total | Flag evaluation successes | {impression} | key, provider name, reason |
| feature_flag.evaluation_error_total | Flag evaluation errors | 1 | key, provider name, exception |
| feature_flag.evaluation_active_count | Active flag evaluations counter | 1 | key, provider name |

Consider the following code example for usage.

#### Example

The following example demonstrates the use of the `MetricsHook` with the `OpenFeature dotnet-sdk`. The metrics are sent to the `console`.

```csharp
using OpenFeature.Contrib.Providers.Flagd;
using OpenFeature;
using OpenFeature.Hooks;
using OpenTelemetry;
using OpenTelemetry.Metrics;

namespace OpenFeatureTestApp
{
class Hello {
static void Main(string[] args) {

// set up the OpenTelemetry OTLP exporter
var meterProvider = Sdk.CreateMeterProviderBuilder()
.AddMeter("OpenFeature")
.ConfigureResource(r => r.AddService("openfeature-test"))
.AddConsoleExporter()
.Build();

// add the MetricsHook to the OpenFeature instance
OpenFeature.Api.Instance.AddHooks(new MetricsHook());

var flagdProvider = new FlagdProvider(new Uri("http://localhost:8013"));

// Set the flagdProvider as the provider for the OpenFeature SDK
OpenFeature.Api.Instance.SetProvider(flagdProvider);

var client = OpenFeature.Api.Instance.GetClient("my-app");

var val = client.GetBooleanValueAsync("myBoolFlag", false, null);

// Print the value of the 'myBoolFlag' feature flag
System.Console.WriteLine(val.Result.ToString());
}
}
}
```

After running this example, you should be able to see some metrics being generated into the console.

<!-- x-hide-in-docs-start -->

## ⭐️ Support the project
Expand Down
17 changes: 17 additions & 0 deletions samples/AspNetCore/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,33 @@
using OpenFeature.DependencyInjection.Providers.Memory;
using OpenFeature.Hooks;
using OpenFeature.Providers.Memory;
using OpenTelemetry.Logs;
using OpenTelemetry.Metrics;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddProblemDetails();

// Configure OpenTelemetry
builder.Services.AddOpenTelemetry()
.ConfigureResource(resource => resource.AddService("openfeature-aspnetcore-sample"))
.WithTracing(tracing => tracing
.AddAspNetCoreInstrumentation()
.AddOtlpExporter())
.WithMetrics(metrics => metrics
.AddAspNetCoreInstrumentation()
.AddMeter("OpenFeature")
.AddOtlpExporter());

builder.Services.AddOpenFeature(featureBuilder =>
{
featureBuilder.AddHostedFeatureLifecycle()
.AddHook(sp => new LoggingHook(sp.GetRequiredService<ILogger<LoggingHook>>()))
.AddHook<MetricsHook>()
.AddHook<TraceEnricherHook>()
.AddInMemoryProvider("InMemory", _ => new Dictionary<string, Flag>()
{
{
Expand Down
10 changes: 10 additions & 0 deletions samples/AspNetCore/Samples.AspNetCore.csproj
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<ManagePackageVersionsCentrally>false</ManagePackageVersionsCentrally>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\OpenFeature.DependencyInjection\OpenFeature.DependencyInjection.csproj" />
<ProjectReference Include="..\..\src\OpenFeature.Hosting\OpenFeature.Hosting.csproj" />
<ProjectReference Include="..\..\src\OpenFeature\OpenFeature.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.9.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.9.0" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.9.0" />
</ItemGroup>

</Project>
16 changes: 16 additions & 0 deletions src/OpenFeature/Hooks/MetricsConstants.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
namespace OpenFeature.Hooks;

internal static class MetricsConstants
{
internal const string ActiveCountName = "feature_flag.evaluation_active_count";
internal const string RequestsTotalName = "feature_flag.evaluation_requests_total";
internal const string SuccessTotalName = "feature_flag.evaluation_success_total";
internal const string ErrorTotalName = "feature_flag.evaluation_error_total";

internal const string ActiveDescription = "active flag evaluations counter";
internal const string RequestsDescription = "feature flag evaluation request counter";
internal const string SuccessDescription = "feature flag evaluation success counter";
internal const string ErrorDescription = "feature flag evaluation error counter";

internal const string ExceptionAttr = "exception";
}
100 changes: 100 additions & 0 deletions src/OpenFeature/Hooks/MetricsHook.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
using System.Diagnostics;
using System.Diagnostics.Metrics;
using System.Reflection;
using OpenFeature.Constant;
using OpenFeature.Model;
using OpenFeature.Telemetry;

namespace OpenFeature.Hooks;

/// <summary>
/// Represents a hook for capturing metrics related to flag evaluations.
/// The meter instrumentation name is "OpenFeature".
/// </summary>
/// <remarks> This is still experimental and subject to change. </remarks>
public class MetricsHook : Hook
{
private static readonly AssemblyName AssemblyName = typeof(MetricsHook).Assembly.GetName();
private static readonly string InstrumentationName = AssemblyName.Name ?? "OpenFeature";
private static readonly string InstrumentationVersion = AssemblyName.Version?.ToString() ?? "1.0.0";
private static readonly Meter Meter = new(InstrumentationName, InstrumentationVersion);

private readonly UpDownCounter<long> _evaluationActiveUpDownCounter;
private readonly Counter<long> _evaluationRequestCounter;
private readonly Counter<long> _evaluationSuccessCounter;
private readonly Counter<long> _evaluationErrorCounter;

/// <summary>
/// Initializes a new instance of the <see cref="MetricsHook"/> class.
/// </summary>
public MetricsHook()
{
this._evaluationActiveUpDownCounter = Meter.CreateUpDownCounter<long>(MetricsConstants.ActiveCountName, description: MetricsConstants.ActiveDescription);
this._evaluationRequestCounter = Meter.CreateCounter<long>(MetricsConstants.RequestsTotalName, "{request}", MetricsConstants.RequestsDescription);
this._evaluationSuccessCounter = Meter.CreateCounter<long>(MetricsConstants.SuccessTotalName, "{impression}", MetricsConstants.SuccessDescription);
this._evaluationErrorCounter = Meter.CreateCounter<long>(MetricsConstants.ErrorTotalName, description: MetricsConstants.ErrorDescription);
}

/// <inheritdoc/>
public override ValueTask<EvaluationContext> BeforeAsync<T>(HookContext<T> context, IReadOnlyDictionary<string, object>? hints = null, CancellationToken cancellationToken = default)
{
var tagList = new TagList
{
{ TelemetryConstants.Key, context.FlagKey },
{ TelemetryConstants.Provider, context.ProviderMetadata.Name }
};

this._evaluationActiveUpDownCounter.Add(1, tagList);
this._evaluationRequestCounter.Add(1, tagList);

return base.BeforeAsync(context, hints, cancellationToken);
}


/// <inheritdoc/>
public override ValueTask AfterAsync<T>(HookContext<T> context, FlagEvaluationDetails<T> details, IReadOnlyDictionary<string, object>? hints = null, CancellationToken cancellationToken = default)
{
var tagList = new TagList
{
{ TelemetryConstants.Key, context.FlagKey },
{ TelemetryConstants.Provider, context.ProviderMetadata.Name },
{ TelemetryConstants.Reason, details.Reason ?? Reason.Unknown.ToString() }
};

this._evaluationSuccessCounter.Add(1, tagList);

return base.AfterAsync(context, details, hints, cancellationToken);
}

/// <inheritdoc/>
public override ValueTask ErrorAsync<T>(HookContext<T> context, Exception error, IReadOnlyDictionary<string, object>? hints = null, CancellationToken cancellationToken = default)
{
var tagList = new TagList
{
{ TelemetryConstants.Key, context.FlagKey },
{ TelemetryConstants.Provider, context.ProviderMetadata.Name },
{ MetricsConstants.ExceptionAttr, error.Message }
};

this._evaluationErrorCounter.Add(1, tagList);

return base.ErrorAsync(context, error, hints, cancellationToken);
}

/// <inheritdoc/>
public override ValueTask FinallyAsync<T>(HookContext<T> context,
FlagEvaluationDetails<T> evaluationDetails,
IReadOnlyDictionary<string, object>? hints = null,
CancellationToken cancellationToken = default)
{
var tagList = new TagList
{
{ TelemetryConstants.Key, context.FlagKey },
{ TelemetryConstants.Provider, context.ProviderMetadata.Name }
};

this._evaluationActiveUpDownCounter.Add(-1, tagList);

return base.FinallyAsync(context, evaluationDetails, hints, cancellationToken);
}
}
Loading