Skip to content

Commit 821ae17

Browse files
committed
feat: implement new tracing hooks
Signed-off-by: Lukas Reining <[email protected]>
1 parent 15ae73b commit 821ae17

File tree

9 files changed

+249
-66
lines changed

9 files changed

+249
-66
lines changed

libs/hooks/open-telemetry/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,5 +94,5 @@ Run `nx package hooks-open-telemetry` to build the library.
9494

9595
Run `nx test hooks-open-telemetry` to execute the unit tests via [Jest](https://jestjs.io).
9696

97-
[otel-spec]: https://opentelemetry.io/docs/reference/specification/trace/semantic_conventions/feature-flags/
97+
[otel-spec]: https://opentelemetry.io/docs/specs/semconv/feature-flags/feature-flags-logs/
9898
[hook-concept]: https://openfeature.dev/docs/reference/concepts/hooks

libs/hooks/open-telemetry/package-lock.json

Lines changed: 61 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

libs/hooks/open-telemetry/package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@
1515
"current-version": "echo $npm_package_version"
1616
},
1717
"peerDependencies": {
18-
"@openfeature/server-sdk": "^1.13.0",
18+
"@openfeature/core": "^1.9.1",
1919
"@opentelemetry/api": ">=1.3.0"
20+
},
21+
"dependencies": {
22+
"@opentelemetry/api-events": "^0.202.0"
2023
}
2124
}
Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
// see: https://opentelemetry.io/docs/specs/otel/logs/semantic_conventions/feature-flags/
21
export const FEATURE_FLAG = 'feature_flag';
32
export const EXCEPTION_ATTR = 'exception';
43

@@ -10,7 +9,13 @@ export const ERROR_TOTAL_NAME = `${FEATURE_FLAG}.evaluation_error_total`;
109
export type EvaluationAttributes = { [key: `${typeof FEATURE_FLAG}.${string}`]: string | undefined };
1110
export type ExceptionAttributes = { [EXCEPTION_ATTR]: string };
1211

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`;
12+
export const KEY_ATTR = `${FEATURE_FLAG}.key` as const;
13+
export const RESULT_VARIANT_ATTR = `${FEATURE_FLAG}.result.variant` as const;
14+
export const RESULT_VALUE_ATTR = `${FEATURE_FLAG}.result.value` as const;
15+
export const RESULT_REASON_ATTR = `${FEATURE_FLAG}.result.reason` as const;
16+
export const ERROR_TYPE_ATTR = `${FEATURE_FLAG}.error.type` as const;
17+
export const ERROR_MESSAGE_ATTR = `${FEATURE_FLAG}.error.message` as const;
18+
export const CONTEXT_ID_ATTR = `${FEATURE_FLAG}.context.id` as const;
19+
export const PROVIDER_NAME_ATTR = `${FEATURE_FLAG}.provider.name` as const;
20+
export const SET_ID_ATTR = `${FEATURE_FLAG}.set.name` as const;
21+
export const VERSION_ATTR = `${FEATURE_FLAG}.version` as const;

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

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
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';
66
import {
77
ACTIVE_COUNT_NAME,
88
ERROR_TOTAL_NAME,
9-
KEY_ATTR,
109
PROVIDER_NAME_ATTR,
11-
REASON_ATTR,
10+
RESULT_REASON_ATTR,
1211
REQUESTS_TOTAL_NAME,
1312
SUCCESS_TOTAL_NAME,
14-
VARIANT_ATTR,
13+
RESULT_VARIANT_ATTR,
14+
KEY_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: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -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';
@@ -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
}
Lines changed: 62 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,56 @@
1-
import type { FlagMetadata, Logger } from '@openfeature/server-sdk';
1+
import type {
2+
EvaluationDetails,
3+
FlagMetadata,
4+
FlagValue,
5+
HookContext,
6+
Logger,
7+
TelemetryAttribute,
8+
} from '@openfeature/core';
9+
import { createEvaluationEvent } from '@openfeature/core';
210
import type { Attributes } from '@opentelemetry/api';
311

12+
type EvaluationEvent = { name: string; attributes: Attributes };
13+
type TelemetryAttributesNames = [keyof typeof TelemetryAttribute][number] | string;
14+
415
export type AttributeMapper = (flagMetadata: FlagMetadata) => Attributes;
516

17+
export type EventMutator = (event: EvaluationEvent) => EvaluationEvent;
18+
619
export type OpenTelemetryHookOptions = {
720
/**
821
* A function that maps OpenFeature flag metadata values to OpenTelemetry attributes.
22+
* This can be used to add custom attributes to the telemetry event.
23+
* Note: This function is applied after the excludeAttributes option.
924
*/
1025
attributeMapper?: AttributeMapper;
26+
27+
/**
28+
* Exclude specific attributes from being added to the telemetry event.
29+
* This is useful for excluding sensitive information, or reducing the size of the event.
30+
* By default, no attributes are excluded.
31+
* Note: This option is applied before the attributeMapper and eventMutator options.
32+
*/
33+
excludeAttributes?: TelemetryAttributesNames[];
34+
35+
/**
36+
* Takes a telemetry event and returns a telemetry event.
37+
* This can be used to filter out attributes that are not needed or to add additional attributes.
38+
* Note: This function is applied after the attributeMapper and excludeAttributes options.
39+
*/
40+
eventMutator?: EventMutator;
1141
};
1242

1343
/**
1444
* Base class that does some logging and safely wraps the AttributeMapper.
1545
*/
1646
export abstract class OpenTelemetryHook {
17-
protected safeAttributeMapper: AttributeMapper;
1847
protected abstract name: string;
1948

20-
constructor(options?: OpenTelemetryHookOptions, logger?: Logger) {
49+
protected attributesToExclude: TelemetryAttributesNames[];
50+
protected safeAttributeMapper: AttributeMapper;
51+
protected safeEventMutator: EventMutator;
52+
53+
protected constructor(options?: OpenTelemetryHookOptions, logger?: Logger) {
2154
this.safeAttributeMapper = (flagMetadata: FlagMetadata) => {
2255
try {
2356
return options?.attributeMapper?.(flagMetadata) || {};
@@ -26,5 +59,31 @@ export abstract class OpenTelemetryHook {
2659
return {};
2760
}
2861
};
62+
this.safeEventMutator = (event: EvaluationEvent) => {
63+
try {
64+
return options?.eventMutator?.(event) ?? event;
65+
} catch (err) {
66+
logger?.debug(`${this.name}: error in eventMutator, ${err.message}, ${err.stack}`);
67+
return event;
68+
}
69+
};
70+
this.attributesToExclude = options?.excludeAttributes ?? [];
71+
}
72+
73+
protected toEvaluationEvent(
74+
hookContext: Readonly<HookContext>,
75+
evaluationDetails: EvaluationDetails<FlagValue>,
76+
): EvaluationEvent {
77+
const { name, attributes } = createEvaluationEvent(hookContext, evaluationDetails);
78+
const customAttributes = this.safeAttributeMapper(evaluationDetails.flagMetadata);
79+
80+
for (const attributeToExclude of this.attributesToExclude) {
81+
delete attributes[attributeToExclude];
82+
}
83+
84+
return this.safeEventMutator({
85+
name,
86+
attributes: { ...attributes, ...customAttributes },
87+
});
2988
}
3089
}

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

Lines changed: 14 additions & 12 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 { MapHookData } from '@openfeature/core';
23

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

1415
// Import must be after the mocks
15-
import { TracingHook } from './tracing-hook';
16+
import { SpanEventTracingHook } from './tracing-hook';
1617

1718
describe('OpenTelemetry Hooks', () => {
1819
const hookContext: HookContext = {
@@ -30,9 +31,10 @@ describe('OpenTelemetry Hooks', () => {
3031
defaultValue: true,
3132
flagValueType: 'boolean',
3233
logger: console,
34+
hookData: new MapHookData(),
3335
};
3436

35-
let tracingHook: TracingHook;
37+
let tracingHook: SpanEventTracingHook;
3638

3739
afterEach(() => {
3840
jest.clearAllMocks();
@@ -41,7 +43,7 @@ describe('OpenTelemetry Hooks', () => {
4143
describe('after stage', () => {
4244
describe('no attribute mapper', () => {
4345
beforeEach(() => {
44-
tracingHook = new TracingHook();
46+
tracingHook = new SpanEventTracingHook();
4547
});
4648

4749
it('should use the variant value on the span event', () => {
@@ -52,7 +54,7 @@ describe('OpenTelemetry Hooks', () => {
5254
flagMetadata: {},
5355
};
5456

55-
tracingHook.after(hookContext, evaluationDetails);
57+
tracingHook.finally(hookContext, evaluationDetails);
5658

5759
expect(addEvent).toBeCalledWith('feature_flag', {
5860
'feature_flag.key': 'testFlagKey',
@@ -68,7 +70,7 @@ describe('OpenTelemetry Hooks', () => {
6870
flagMetadata: {},
6971
};
7072

71-
tracingHook.after(hookContext, evaluationDetails);
73+
tracingHook.finally(hookContext, evaluationDetails);
7274

7375
expect(addEvent).toBeCalledWith('feature_flag', {
7476
'feature_flag.key': 'testFlagKey',
@@ -83,7 +85,7 @@ describe('OpenTelemetry Hooks', () => {
8385
value: 'already-string',
8486
flagMetadata: {},
8587
};
86-
tracingHook.after(hookContext, evaluationDetails);
88+
tracingHook.finally(hookContext, evaluationDetails);
8789

8890
expect(addEvent).toBeCalledWith('feature_flag', {
8991
'feature_flag.key': 'testFlagKey',
@@ -101,15 +103,15 @@ describe('OpenTelemetry Hooks', () => {
101103
flagMetadata: {},
102104
};
103105

104-
tracingHook.after(hookContext, evaluationDetails);
106+
tracingHook.finally(hookContext, evaluationDetails);
105107
expect(addEvent).not.toBeCalled();
106108
});
107109
});
108110

109111
describe('attribute mapper configured', () => {
110112
describe('no error in mapper', () => {
111113
beforeEach(() => {
112-
tracingHook = new TracingHook({
114+
tracingHook = new SpanEventTracingHook({
113115
attributeMapper: (flagMetadata) => {
114116
return {
115117
customAttr1: flagMetadata.metadata1,
@@ -132,7 +134,7 @@ describe('OpenTelemetry Hooks', () => {
132134
},
133135
};
134136

135-
tracingHook.after(hookContext, evaluationDetails);
137+
tracingHook.finally(hookContext, evaluationDetails);
136138

137139
expect(addEvent).toBeCalledWith('feature_flag', {
138140
'feature_flag.key': 'testFlagKey',
@@ -147,7 +149,7 @@ describe('OpenTelemetry Hooks', () => {
147149

148150
describe('error in mapper', () => {
149151
beforeEach(() => {
150-
tracingHook = new TracingHook({
152+
tracingHook = new SpanEventTracingHook({
151153
attributeMapper: () => {
152154
throw new Error('fake error');
153155
},
@@ -166,7 +168,7 @@ describe('OpenTelemetry Hooks', () => {
166168
},
167169
};
168170

169-
tracingHook.after(hookContext, evaluationDetails);
171+
tracingHook.finally(hookContext, evaluationDetails);
170172

171173
expect(addEvent).toBeCalledWith('feature_flag', {
172174
'feature_flag.key': 'testFlagKey',

0 commit comments

Comments
 (0)