Skip to content

Commit dbeac9b

Browse files
committed
feat: update otel hook to use new semconv
Signed-off-by: Lukas Reining <[email protected]>
1 parent ae20bc8 commit dbeac9b

File tree

7 files changed

+155
-67
lines changed

7 files changed

+155
-67
lines changed

libs/hooks/open-telemetry/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
"current-version": "echo $npm_package_version"
1616
},
1717
"peerDependencies": {
18-
"@openfeature/server-sdk": "^1.13.0",
18+
"@openfeature/core": "^1.0.0",
1919
"@opentelemetry/api": ">=1.3.0"
2020
}
2121
}
Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
// see: https://opentelemetry.io/docs/specs/otel/logs/semantic_conventions/feature-flags/
2+
import type { FlagValue } from '@openfeature/core';
3+
import type { AttributeValue } from '@opentelemetry/api';
4+
25
export const FEATURE_FLAG = 'feature_flag';
36
export const EXCEPTION_ATTR = 'exception';
47

@@ -10,7 +13,35 @@ export const ERROR_TOTAL_NAME = `${FEATURE_FLAG}.evaluation_error_total`;
1013
export type EvaluationAttributes = { [key: `${typeof FEATURE_FLAG}.${string}`]: string | undefined };
1114
export type ExceptionAttributes = { [EXCEPTION_ATTR]: string };
1215

13-
export const KEY_ATTR: keyof EvaluationAttributes = `${FEATURE_FLAG}.key`;
14-
export const PROVIDER_NAME_ATTR: keyof EvaluationAttributes = `${FEATURE_FLAG}.provider_name`;
15-
export const VARIANT_ATTR: keyof EvaluationAttributes = `${FEATURE_FLAG}.variant`;
16-
export const REASON_ATTR: keyof EvaluationAttributes = `${FEATURE_FLAG}.reason`;
16+
export const KEY_ATTR = `${FEATURE_FLAG}.key` as const;
17+
export const RESULT_VARIANT_ATTR = `${FEATURE_FLAG}.result.variant` as const;
18+
export const RESULT_VALUE_ATTR = `${FEATURE_FLAG}.result.value` as const;
19+
export const RESULT_REASON_ATTR = `${FEATURE_FLAG}.result.reason` as const;
20+
export const ERROR_TYPE_ATTR = `${FEATURE_FLAG}.error.type` as const;
21+
export const ERROR_MESSAGE_ATTR = `${FEATURE_FLAG}.error.message` as const;
22+
export const CONTEXT_ID_ATTR = `${FEATURE_FLAG}.context.id` as const;
23+
export const PROVIDER_NAME_ATTR = `${FEATURE_FLAG}.provider.name` as const;
24+
export const SET_ID_ATTR = `${FEATURE_FLAG}.set.name` as const;
25+
export const VERSION_ATTR = `${FEATURE_FLAG}.version` as const;
26+
27+
export const ALL_EVENT_ATTRS = [
28+
KEY_ATTR,
29+
PROVIDER_NAME_ATTR,
30+
RESULT_VARIANT_ATTR,
31+
RESULT_VALUE_ATTR,
32+
RESULT_REASON_ATTR,
33+
ERROR_TYPE_ATTR,
34+
ERROR_MESSAGE_ATTR,
35+
CONTEXT_ID_ATTR,
36+
SET_ID_ATTR,
37+
VERSION_ATTR,
38+
];
39+
40+
export type TracingEventAttributeKey = (typeof ALL_EVENT_ATTRS)[number];
41+
42+
export type TracingEvent = {
43+
[K in Exclude<TracingEventAttributeKey, 'feature_flag.key' | 'feature_flag.result.value'>]?: string;
44+
} & {
45+
[KEY_ATTR]: string;
46+
[RESULT_VALUE_ATTR]?: FlagValue;
47+
} & { [key: string]: AttributeValue | undefined };

libs/hooks/open-telemetry/src/lib/metrics/metrics-hook.spec.ts

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import type { BeforeHookContext, EvaluationDetails, HookContext } from '@openfeature/server-sdk';
2-
import { StandardResolutionReasons } from '@openfeature/server-sdk';
1+
import type { BeforeHookContext, EvaluationDetails, HookContext } from '@openfeature/core';
2+
import { StandardResolutionReasons } from '@openfeature/core';
33
import opentelemetry from '@opentelemetry/api';
44
import type { DataPoint, ScopeMetrics } from '@opentelemetry/sdk-metrics';
55
import { MeterProvider, MetricReader } from '@opentelemetry/sdk-metrics';
@@ -8,10 +8,10 @@ import {
88
ERROR_TOTAL_NAME,
99
KEY_ATTR,
1010
PROVIDER_NAME_ATTR,
11-
REASON_ATTR,
11+
RESULT_REASON_ATTR,
1212
REQUESTS_TOTAL_NAME,
1313
SUCCESS_TOTAL_NAME,
14-
VARIANT_ATTR,
14+
RESULT_VARIANT_ATTR,
1515
} from '../conventions';
1616
import { MetricsHook } from './metrics-hook';
1717
import type { AttributeMapper } from '../otel-hook';
@@ -110,8 +110,8 @@ describe(MetricsHook.name, () => {
110110
point.value === 1 &&
111111
point.attributes[KEY_ATTR] === FLAG_KEY &&
112112
point.attributes[PROVIDER_NAME_ATTR] === PROVIDER_NAME &&
113-
point.attributes[VARIANT_ATTR] === VARIANT &&
114-
point.attributes[REASON_ATTR] === StandardResolutionReasons.STATIC,
113+
point.attributes[RESULT_VARIANT_ATTR] === VARIANT &&
114+
point.attributes[RESULT_REASON_ATTR] === StandardResolutionReasons.STATIC,
115115
),
116116
).toBeTruthy();
117117
});
@@ -143,8 +143,8 @@ describe(MetricsHook.name, () => {
143143
point.value === 1 &&
144144
point.attributes[KEY_ATTR] === FLAG_KEY &&
145145
point.attributes[PROVIDER_NAME_ATTR] === PROVIDER_NAME &&
146-
point.attributes[VARIANT_ATTR] === VALUE.toString() &&
147-
point.attributes[REASON_ATTR] === StandardResolutionReasons.STATIC,
146+
point.attributes[RESULT_VARIANT_ATTR] === VALUE.toString() &&
147+
point.attributes[RESULT_REASON_ATTR] === StandardResolutionReasons.STATIC,
148148
),
149149
).toBeTruthy();
150150
});
@@ -197,8 +197,8 @@ describe(MetricsHook.name, () => {
197197
point.value === 1 &&
198198
point.attributes[KEY_ATTR] === FLAG_KEY &&
199199
point.attributes[PROVIDER_NAME_ATTR] === PROVIDER_NAME &&
200-
point.attributes[VARIANT_ATTR] === VARIANT &&
201-
point.attributes[REASON_ATTR] === StandardResolutionReasons.STATIC &&
200+
point.attributes[RESULT_VARIANT_ATTR] === VARIANT &&
201+
point.attributes[RESULT_REASON_ATTR] === StandardResolutionReasons.STATIC &&
202202
// custom attributes should be present
203203
point.attributes[CUSTOM_ATTR_KEY_1] === CUSTOM_ATTR_VALUE_1 &&
204204
point.attributes[CUSTOM_ATTR_KEY_2] === CUSTOM_ATTR_VALUE_2,
@@ -243,8 +243,8 @@ describe(MetricsHook.name, () => {
243243
point.value === 1 &&
244244
point.attributes[KEY_ATTR] === FLAG_KEY &&
245245
point.attributes[PROVIDER_NAME_ATTR] === PROVIDER_NAME &&
246-
point.attributes[VARIANT_ATTR] === VARIANT &&
247-
point.attributes[REASON_ATTR] === StandardResolutionReasons.STATIC,
246+
point.attributes[RESULT_VARIANT_ATTR] === VARIANT &&
247+
point.attributes[RESULT_REASON_ATTR] === StandardResolutionReasons.STATIC,
248248
),
249249
).toBeTruthy();
250250
});

libs/hooks/open-telemetry/src/lib/metrics/metrics-hook.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import type { BeforeHookContext, Logger } from '@openfeature/server-sdk';
1+
import type { BeforeHookContext, Logger } from '@openfeature/core';
22
import {
33
StandardResolutionReasons,
44
type EvaluationDetails,
55
type FlagValue,
6-
type Hook,
6+
type BaseHook,
77
type HookContext,
8-
} from '@openfeature/server-sdk';
8+
} from '@openfeature/core';
99
import type { Attributes, Counter, UpDownCounter } from '@opentelemetry/api';
1010
import { ValueType, metrics } from '@opentelemetry/api';
1111
import type { EvaluationAttributes, ExceptionAttributes } from '../conventions';
@@ -15,10 +15,10 @@ import {
1515
EXCEPTION_ATTR,
1616
KEY_ATTR,
1717
PROVIDER_NAME_ATTR,
18-
REASON_ATTR,
18+
RESULT_REASON_ATTR,
1919
REQUESTS_TOTAL_NAME,
2020
SUCCESS_TOTAL_NAME,
21-
VARIANT_ATTR,
21+
RESULT_VARIANT_ATTR,
2222
} from '../conventions';
2323
import type { OpenTelemetryHookOptions } from '../otel-hook';
2424
import { OpenTelemetryHook } from '../otel-hook';
@@ -39,7 +39,7 @@ const ERROR_DESCRIPTION = 'feature flag evaluation error counter';
3939
*
4040
* See {@link https://opentelemetry.io/docs/reference/specification/trace/semantic_conventions/feature-flags/}
4141
*/
42-
export class MetricsHook extends OpenTelemetryHook implements Hook {
42+
export class MetricsHook extends OpenTelemetryHook implements BaseHook {
4343
protected name = MetricsHook.name;
4444
private readonly evaluationActiveUpDownCounter: UpDownCounter<EvaluationAttributes>;
4545
private readonly evaluationRequestCounter: Counter<EvaluationAttributes>;
@@ -83,8 +83,8 @@ export class MetricsHook extends OpenTelemetryHook implements Hook {
8383
this.evaluationSuccessCounter.add(1, {
8484
[KEY_ATTR]: hookContext.flagKey,
8585
[PROVIDER_NAME_ATTR]: hookContext.providerMetadata.name,
86-
[VARIANT_ATTR]: evaluationDetails.variant ?? evaluationDetails.value?.toString(),
87-
[REASON_ATTR]: evaluationDetails.reason ?? StandardResolutionReasons.UNKNOWN,
86+
[RESULT_VARIANT_ATTR]: evaluationDetails.variant ?? evaluationDetails.value?.toString(),
87+
[RESULT_REASON_ATTR]: evaluationDetails.reason ?? StandardResolutionReasons.UNKNOWN,
8888
...this.safeAttributeMapper(evaluationDetails?.flagMetadata || {}),
8989
});
9090
}

libs/hooks/open-telemetry/src/lib/otel-hook.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { FlagMetadata, Logger } from '@openfeature/server-sdk';
1+
import type { FlagMetadata, Logger } from '@openfeature/core';
22
import type { Attributes } from '@opentelemetry/api';
33

44
export type AttributeMapper = (flagMetadata: FlagMetadata) => Attributes;
@@ -17,7 +17,7 @@ export abstract class OpenTelemetryHook {
1717
protected safeAttributeMapper: AttributeMapper;
1818
protected abstract name: string;
1919

20-
constructor(options?: OpenTelemetryHookOptions, logger?: Logger) {
20+
protected constructor(options?: OpenTelemetryHookOptions, logger?: Logger) {
2121
this.safeAttributeMapper = (flagMetadata: FlagMetadata) => {
2222
try {
2323
return options?.attributeMapper?.(flagMetadata) || {};

libs/hooks/open-telemetry/src/lib/traces/tracing-hook.spec.ts

Lines changed: 52 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import type { EvaluationDetails, HookContext } from '@openfeature/server-sdk';
1+
import type { EvaluationDetails, HookContext } from '@openfeature/core';
2+
import { ErrorCode } from '@openfeature/core';
23

34
const addEvent = jest.fn();
45
const recordException = jest.fn();
@@ -13,6 +14,7 @@ jest.mock('@opentelemetry/api', () => ({
1314

1415
// Import must be after the mocks
1516
import { TracingHook } from './tracing-hook';
17+
import { ALL_EVENT_ATTRS } from '../conventions';
1618

1719
describe('OpenTelemetry Hooks', () => {
1820
const hookContext: HookContext = {
@@ -56,28 +58,52 @@ describe('OpenTelemetry Hooks', () => {
5658

5759
expect(addEvent).toBeCalledWith('feature_flag', {
5860
'feature_flag.key': 'testFlagKey',
59-
'feature_flag.provider_name': 'testProvider',
60-
'feature_flag.variant': 'enabled',
61+
'feature_flag.provider.name': 'testProvider',
62+
'feature_flag.result.variant': 'enabled',
6163
});
6264
});
6365

64-
it('should use a stringified value as the variant value on the span event', () => {
66+
it('should use the error values on the span event', () => {
6567
const evaluationDetails: EvaluationDetails<boolean> = {
68+
value: false,
6669
flagKey: hookContext.flagKey,
67-
value: true,
70+
errorCode: ErrorCode.PROVIDER_FATAL,
71+
errorMessage: 'fake error message',
6872
flagMetadata: {},
6973
};
7074

7175
tracingHook.after(hookContext, evaluationDetails);
7276

7377
expect(addEvent).toBeCalledWith('feature_flag', {
7478
'feature_flag.key': 'testFlagKey',
75-
'feature_flag.provider_name': 'testProvider',
76-
'feature_flag.variant': 'true',
79+
'feature_flag.provider.name': 'testProvider',
80+
'feature_flag.error.type': ErrorCode.PROVIDER_FATAL.toLowerCase(),
81+
'feature_flag.error.message': 'fake error message',
82+
});
83+
});
84+
85+
it('should not call addEvent because there is no active span', () => {
86+
getActiveSpan.mockReturnValueOnce(undefined);
87+
const evaluationDetails: EvaluationDetails<boolean> = {
88+
flagKey: hookContext.flagKey,
89+
value: true,
90+
variant: 'enabled',
91+
flagMetadata: {},
92+
};
93+
94+
tracingHook.after(hookContext, evaluationDetails);
95+
expect(addEvent).not.toBeCalled();
96+
});
97+
});
98+
99+
describe('with value attribute', () => {
100+
beforeEach(() => {
101+
tracingHook = new TracingHook({
102+
includeAttributes: ALL_EVENT_ATTRS,
77103
});
78104
});
79105

80-
it('should set the value without extra quotes if value is already a string', () => {
106+
it('should set the value', () => {
81107
const evaluationDetails: EvaluationDetails<string> = {
82108
flagKey: hookContext.flagKey,
83109
value: 'already-string',
@@ -87,22 +113,29 @@ describe('OpenTelemetry Hooks', () => {
87113

88114
expect(addEvent).toBeCalledWith('feature_flag', {
89115
'feature_flag.key': 'testFlagKey',
90-
'feature_flag.provider_name': 'testProvider',
91-
'feature_flag.variant': 'already-string',
116+
'feature_flag.provider.name': 'testProvider',
117+
'feature_flag.result.value': 'already-string',
92118
});
93119
});
94120

95-
it('should not call addEvent because there is no active span', () => {
96-
getActiveSpan.mockReturnValueOnce(undefined);
121+
it('should use the error values and value on the span event', () => {
97122
const evaluationDetails: EvaluationDetails<boolean> = {
123+
value: false,
98124
flagKey: hookContext.flagKey,
99-
value: true,
100-
variant: 'enabled',
125+
errorCode: ErrorCode.PROVIDER_FATAL,
126+
errorMessage: 'fake error message',
101127
flagMetadata: {},
102128
};
103129

104130
tracingHook.after(hookContext, evaluationDetails);
105-
expect(addEvent).not.toBeCalled();
131+
132+
expect(addEvent).toBeCalledWith('feature_flag', {
133+
'feature_flag.key': 'testFlagKey',
134+
'feature_flag.provider.name': 'testProvider',
135+
'feature_flag.result.value': false,
136+
'feature_flag.error.type': ErrorCode.PROVIDER_FATAL.toLowerCase(),
137+
'feature_flag.error.message': 'fake error message',
138+
});
106139
});
107140
});
108141

@@ -136,8 +169,8 @@ describe('OpenTelemetry Hooks', () => {
136169

137170
expect(addEvent).toBeCalledWith('feature_flag', {
138171
'feature_flag.key': 'testFlagKey',
139-
'feature_flag.provider_name': 'testProvider',
140-
'feature_flag.variant': 'enabled',
172+
'feature_flag.provider.name': 'testProvider',
173+
'feature_flag.result.variant': 'enabled',
141174
customAttr1: 'one',
142175
customAttr2: 2,
143176
customAttr3: true,
@@ -170,8 +203,8 @@ describe('OpenTelemetry Hooks', () => {
170203

171204
expect(addEvent).toBeCalledWith('feature_flag', {
172205
'feature_flag.key': 'testFlagKey',
173-
'feature_flag.provider_name': 'testProvider',
174-
'feature_flag.variant': 'enabled',
206+
'feature_flag.provider.name': 'testProvider',
207+
'feature_flag.result.variant': 'enabled',
175208
});
176209
});
177210
});

0 commit comments

Comments
 (0)