Skip to content

Commit 343c0c6

Browse files
committed
feat: update otel hook to use new semconv
1 parent ae20bc8 commit 343c0c6

File tree

7 files changed

+154
-67
lines changed

7 files changed

+154
-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: 51 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { EvaluationDetails, HookContext } from '@openfeature/server-sdk';
1+
import { ErrorCode, EvaluationDetails, HookContext } from '@openfeature/core';
22

33
const addEvent = jest.fn();
44
const recordException = jest.fn();
@@ -13,6 +13,7 @@ jest.mock('@opentelemetry/api', () => ({
1313

1414
// Import must be after the mocks
1515
import { TracingHook } from './tracing-hook';
16+
import { ALL_EVENT_ATTRS } from '../conventions';
1617

1718
describe('OpenTelemetry Hooks', () => {
1819
const hookContext: HookContext = {
@@ -56,28 +57,52 @@ describe('OpenTelemetry Hooks', () => {
5657

5758
expect(addEvent).toBeCalledWith('feature_flag', {
5859
'feature_flag.key': 'testFlagKey',
59-
'feature_flag.provider_name': 'testProvider',
60-
'feature_flag.variant': 'enabled',
60+
'feature_flag.provider.name': 'testProvider',
61+
'feature_flag.result.variant': 'enabled',
6162
});
6263
});
6364

64-
it('should use a stringified value as the variant value on the span event', () => {
65+
it('should use the error values on the span event', () => {
6566
const evaluationDetails: EvaluationDetails<boolean> = {
67+
value: false,
6668
flagKey: hookContext.flagKey,
67-
value: true,
69+
errorCode: ErrorCode.PROVIDER_FATAL,
70+
errorMessage: 'fake error message',
6871
flagMetadata: {},
6972
};
7073

7174
tracingHook.after(hookContext, evaluationDetails);
7275

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

80-
it('should set the value without extra quotes if value is already a string', () => {
105+
it('should set the value', () => {
81106
const evaluationDetails: EvaluationDetails<string> = {
82107
flagKey: hookContext.flagKey,
83108
value: 'already-string',
@@ -87,22 +112,29 @@ describe('OpenTelemetry Hooks', () => {
87112

88113
expect(addEvent).toBeCalledWith('feature_flag', {
89114
'feature_flag.key': 'testFlagKey',
90-
'feature_flag.provider_name': 'testProvider',
91-
'feature_flag.variant': 'already-string',
115+
'feature_flag.provider.name': 'testProvider',
116+
'feature_flag.result.value': 'already-string',
92117
});
93118
});
94119

95-
it('should not call addEvent because there is no active span', () => {
96-
getActiveSpan.mockReturnValueOnce(undefined);
120+
it('should use the error values and value on the span event', () => {
97121
const evaluationDetails: EvaluationDetails<boolean> = {
122+
value: false,
98123
flagKey: hookContext.flagKey,
99-
value: true,
100-
variant: 'enabled',
124+
errorCode: ErrorCode.PROVIDER_FATAL,
125+
errorMessage: 'fake error message',
101126
flagMetadata: {},
102127
};
103128

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

@@ -136,8 +168,8 @@ describe('OpenTelemetry Hooks', () => {
136168

137169
expect(addEvent).toBeCalledWith('feature_flag', {
138170
'feature_flag.key': 'testFlagKey',
139-
'feature_flag.provider_name': 'testProvider',
140-
'feature_flag.variant': 'enabled',
171+
'feature_flag.provider.name': 'testProvider',
172+
'feature_flag.result.variant': 'enabled',
141173
customAttr1: 'one',
142174
customAttr2: 2,
143175
customAttr3: true,
@@ -170,8 +202,8 @@ describe('OpenTelemetry Hooks', () => {
170202

171203
expect(addEvent).toBeCalledWith('feature_flag', {
172204
'feature_flag.key': 'testFlagKey',
173-
'feature_flag.provider_name': 'testProvider',
174-
'feature_flag.variant': 'enabled',
205+
'feature_flag.provider.name': 'testProvider',
206+
'feature_flag.result.variant': 'enabled',
175207
});
176208
});
177209
});

0 commit comments

Comments
 (0)