Skip to content

Commit 5845e2b

Browse files
authored
feat: Add Metrics Hook (#114)
Signed-off-by: André Silva <[email protected]>
1 parent 755c549 commit 5845e2b

File tree

6 files changed

+386
-3
lines changed

6 files changed

+386
-3
lines changed

.github/component_owners.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ components:
44
src/OpenFeature.Contrib.Hooks.Otel:
55
- bacherfl
66
- toddbaert
7+
- askpt
78
src/OpenFeature.Contrib.Providers.Flagd:
89
- bacherfl
910
- toddbaert
@@ -17,6 +18,7 @@ components:
1718
test/OpenFeature.Contrib.Hooks.Otel.Test:
1819
- bacherfl
1920
- toddbaert
21+
- askpt
2022
test/OpenFeature.Contrib.Providers.Flagd.Test:
2123
- bacherfl
2224
- toddbaert
@@ -27,4 +29,4 @@ components:
2729
- matthewelwell
2830

2931
ignored-authors:
30-
- renovate-bot
32+
- renovate-bot

.vscode/settings.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"dotnet.defaultSolution": "DotnetSdkContrib.sln"
3+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
namespace OpenFeature.Contrib.Hooks.Otel
2+
{
3+
internal static class MetricsConstants
4+
{
5+
internal const string ActiveCountName = "feature_flag.evaluation_active_count";
6+
internal const string RequestsTotalName = "feature_flag.evaluation_requests_total";
7+
internal const string SuccessTotalName = "feature_flag.evaluation_success_total";
8+
internal const string ErrorTotalName = "feature_flag.evaluation_error_total";
9+
10+
internal const string ActiveDescription = "active flag evaluations counter";
11+
internal const string RequestsDescription = "feature flag evaluation request counter";
12+
internal const string SuccessDescription = "feature flag evaluation success counter";
13+
internal const string ErrorDescription = "feature flag evaluation error counter";
14+
15+
internal const string KeyAttr = "key";
16+
internal const string ProviderNameAttr = "provider_name";
17+
internal const string VariantAttr = "variant";
18+
internal const string ReasonAttr = "reason";
19+
internal const string ExceptionAttr = "exception";
20+
}
21+
}
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Diagnostics;
4+
using System.Diagnostics.Metrics;
5+
using System.Reflection;
6+
using System.Threading.Tasks;
7+
using OpenFeature.Model;
8+
9+
namespace OpenFeature.Contrib.Hooks.Otel
10+
{
11+
/// <summary>
12+
/// Represents a hook for capturing metrics related to flag evaluations.
13+
/// The meter name is "OpenFeature.Contrib.Hooks.Otel".
14+
/// </summary>
15+
public class MetricsHook : Hook
16+
{
17+
private static readonly AssemblyName AssemblyName = typeof(MetricsHook).Assembly.GetName();
18+
private static readonly string InstrumentationName = AssemblyName.Name;
19+
private static readonly string InstrumentationVersion = AssemblyName.Version?.ToString();
20+
21+
private readonly UpDownCounter<long> _evaluationActiveUpDownCounter;
22+
private readonly Counter<long> _evaluationRequestCounter;
23+
private readonly Counter<long> _evaluationSuccessCounter;
24+
private readonly Counter<long> _evaluationErrorCounter;
25+
26+
/// <summary>
27+
/// Initializes a new instance of the <see cref="MetricsHook"/> class.
28+
/// </summary>
29+
public MetricsHook()
30+
{
31+
var meter = new Meter(InstrumentationName, InstrumentationVersion);
32+
33+
_evaluationActiveUpDownCounter = meter.CreateUpDownCounter<long>(MetricsConstants.ActiveCountName, description: MetricsConstants.ActiveDescription);
34+
_evaluationRequestCounter = meter.CreateCounter<long>(MetricsConstants.RequestsTotalName, "{request}", MetricsConstants.RequestsDescription);
35+
_evaluationSuccessCounter = meter.CreateCounter<long>(MetricsConstants.SuccessTotalName, "{impression}", MetricsConstants.SuccessDescription);
36+
_evaluationErrorCounter = meter.CreateCounter<long>(MetricsConstants.ErrorTotalName, description: MetricsConstants.ErrorDescription);
37+
}
38+
39+
/// <summary>
40+
/// Executes before the flag evaluation and captures metrics related to the evaluation.
41+
/// The metrics are captured in the following order:
42+
/// 1. The active count is incremented. (feature_flag.evaluation_active_count)
43+
/// 2. The request count is incremented. (feature_flag.evaluation_requests_total)
44+
/// </summary>
45+
/// <typeparam name="T">The type of the flag value.</typeparam>
46+
/// <param name="context">The hook context.</param>
47+
/// <param name="hints">The optional hints.</param>
48+
/// <returns>The evaluation context.</returns>
49+
public override Task<EvaluationContext> Before<T>(HookContext<T> context, IReadOnlyDictionary<string, object> hints = null)
50+
{
51+
var tagList = new TagList
52+
{
53+
{ MetricsConstants.KeyAttr, context.FlagKey },
54+
{ MetricsConstants.ProviderNameAttr, context.ProviderMetadata.Name }
55+
};
56+
57+
_evaluationActiveUpDownCounter.Add(1, tagList);
58+
_evaluationRequestCounter.Add(1, tagList);
59+
60+
return base.Before(context, hints);
61+
}
62+
63+
64+
/// <summary>
65+
/// Executes after the flag evaluation and captures metrics related to the evaluation.
66+
/// The metrics are captured in the following order:
67+
/// 1. The success count is incremented. (feature_flag.evaluation_success_total)
68+
/// </summary>
69+
/// <typeparam name="T">The type of the flag value.</typeparam>
70+
/// <param name="context">The hook context.</param>
71+
/// <param name="details">The flag evaluation details.</param>
72+
/// <param name="hints">The optional hints.</param>
73+
/// <returns>The evaluation context.</returns>
74+
public override Task After<T>(HookContext<T> context, FlagEvaluationDetails<T> details, IReadOnlyDictionary<string, object> hints = null)
75+
{
76+
var tagList = new TagList
77+
{
78+
{ MetricsConstants.KeyAttr, context.FlagKey },
79+
{ MetricsConstants.ProviderNameAttr, context.ProviderMetadata.Name },
80+
{ MetricsConstants.VariantAttr, details.Variant ?? details.Value?.ToString() },
81+
{ MetricsConstants.ReasonAttr, details.Reason ?? "UNKNOWN" }
82+
};
83+
84+
_evaluationSuccessCounter.Add(1, tagList);
85+
86+
return base.After(context, details, hints);
87+
}
88+
89+
/// <summary>
90+
/// Executes when an error occurs during flag evaluation and captures metrics related to the error.
91+
/// The metrics are captured in the following order:
92+
/// 1. The error count is incremented. (feature_flag.evaluation_error_total)
93+
/// </summary>
94+
/// <typeparam name="T">The type of the flag value.</typeparam>
95+
/// <param name="context">The hook context.</param>
96+
/// <param name="error">The exception that occurred.</param>
97+
/// <param name="hints">The optional hints.</param>
98+
/// <returns>The evaluation context.</returns>
99+
public override Task Error<T>(HookContext<T> context, Exception error, IReadOnlyDictionary<string, object> hints = null)
100+
{
101+
var tagList = new TagList
102+
{
103+
{ MetricsConstants.KeyAttr, context.FlagKey },
104+
{ MetricsConstants.ProviderNameAttr, context.ProviderMetadata.Name },
105+
{ MetricsConstants.ExceptionAttr, error?.Message ?? "Unknown error" }
106+
};
107+
108+
_evaluationErrorCounter.Add(1, tagList);
109+
110+
return base.Error(context, error, hints);
111+
}
112+
113+
/// <summary>
114+
/// Executes after the flag evaluation is complete and captures metrics related to the evaluation.
115+
/// The active count is decremented. (feature_flag.evaluation_active_count)
116+
/// </summary>
117+
/// <typeparam name="T">The type of the flag value.</typeparam>
118+
/// <param name="context">The hook context.</param>
119+
/// <param name="hints">The optional hints.</param>
120+
/// <returns>The evaluation context.</returns>
121+
public override Task Finally<T>(HookContext<T> context, IReadOnlyDictionary<string, object> hints = null)
122+
{
123+
var tagList = new TagList
124+
{
125+
{ MetricsConstants.KeyAttr, context.FlagKey },
126+
{ MetricsConstants.ProviderNameAttr, context.ProviderMetadata.Name }
127+
};
128+
129+
_evaluationActiveUpDownCounter.Add(-1, tagList);
130+
131+
return base.Finally(context, hints);
132+
}
133+
}
134+
}

src/OpenFeature.Contrib.Hooks.Otel/README.md

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
- open-feature/dotnet-sdk >= v1.0
66

7-
## Usage
7+
## Usage - Traces
88

99
For this hook to function correctly a global `TracerProvider` must be set, an example of how to do this can be found below.
1010

@@ -32,7 +32,7 @@ namespace OpenFeatureTestApp
3232
var tracerProvider = Sdk.CreateTracerProviderBuilder()
3333
.AddSource("my-tracer")
3434
.ConfigureResource(r => r.AddService("jaeger-test"))
35-
.AddOtlpExporter(o =>
35+
.AddOtlpExporter(o =>
3636
{
3737
o.ExportProcessorType = ExportProcessorType.Simple;
3838
})
@@ -65,6 +65,66 @@ In case something went wrong during a feature flag evaluation, you will see an e
6565

6666
![](./assets/otlp-error.png)
6767

68+
## Usage - Metrics
69+
70+
For this hook to function correctly a global `MeterProvider` must be set.
71+
`MetricsHook` performs metric collection by tapping into various hook stages.
72+
73+
Below are the metrics extracted by this hook and dimensions they carry:
74+
75+
| Metric key | Description | Unit | Dimensions |
76+
| -------------------------------------- | ------------------------------- | ------------ | ----------------------------------- |
77+
| feature_flag.evaluation_requests_total | Number of evaluation requests | {request} | key, provider name |
78+
| feature_flag.evaluation_success_total | Flag evaluation successes | {impression} | key, provider name, reason, variant |
79+
| feature_flag.evaluation_error_total | Flag evaluation errors | Counter | key, provider name |
80+
| feature_flag.evaluation_active_count | Active flag evaluations counter | Counter | key |
81+
82+
Consider the following code example for usage.
83+
84+
### Example
85+
86+
The following example demonstrates the use of the `OpenTelemetry hook` with the `OpenFeature dotnet-sdk`. The metrics are sent to the `console`.
87+
88+
```csharp
89+
using OpenFeature.Contrib.Providers.Flagd;
90+
using OpenFeature;
91+
using OpenFeature.Contrib.Hooks.Otel;
92+
using OpenTelemetry;
93+
using OpenTelemetry.Metrics;
94+
95+
namespace OpenFeatureTestApp
96+
{
97+
class Hello {
98+
static void Main(string[] args) {
99+
100+
// set up the OpenTelemetry OTLP exporter
101+
var meterProvider = Sdk.CreateMeterProviderBuilder()
102+
.AddMeter("OpenFeature.Contrib.Hooks.Otel")
103+
.ConfigureResource(r => r.AddService("openfeature-test"))
104+
.AddConsoleExporter()
105+
.Build();
106+
107+
// add the Otel Hook to the OpenFeature instance
108+
OpenFeature.Api.Instance.AddHooks(new MetricsHook());
109+
110+
var flagdProvider = new FlagdProvider(new Uri("http://localhost:8013"));
111+
112+
// Set the flagdProvider as the provider for the OpenFeature SDK
113+
OpenFeature.Api.Instance.SetProvider(flagdProvider);
114+
115+
var client = OpenFeature.Api.Instance.GetClient("my-app");
116+
117+
var val = client.GetBooleanValue("myBoolFlag", false, null);
118+
119+
// Print the value of the 'myBoolFlag' feature flag
120+
System.Console.WriteLine(val.Result.ToString());
121+
}
122+
}
123+
}
124+
```
125+
126+
After running this example, you should be able to see some metrics being generated into the console.
127+
68128
## License
69129

70130
Apache 2.0 - See [LICENSE](./../../LICENSE) for more information.

0 commit comments

Comments
 (0)