Skip to content

Commit 26dbbaf

Browse files
committed
feat: adding logging hook and tests
feat: adds info and debug implementations to DefaultLogger Signed-off-by: Kevin Long <[email protected]>
1 parent 5e5b160 commit 26dbbaf

File tree

3 files changed

+237
-3
lines changed

3 files changed

+237
-3
lines changed
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import type { OpenFeatureError } from '../errors';
2+
import type { BaseHook } from './hook';
3+
import type { BeforeHookContext, HookContext, HookHints } from './hooks';
4+
import type { FlagValue, EvaluationDetails } from '../evaluation';
5+
6+
import { DefaultLogger, SafeLogger } from '../logger';
7+
8+
type LoggerPayload = Record<string, unknown>;
9+
10+
const DOMAIN_KEY = 'domain';
11+
const PROVIDER_NAME_KEY = 'provider_name';
12+
const FLAG_KEY_KEY = 'flag_key';
13+
const DEFAULT_VALUE_KEY = 'default_value';
14+
const EVALUATION_CONTEXT_KEY = 'evaluation_context';
15+
const ERROR_CODE_KEY = 'error_code';
16+
const ERROR_MESSAGE_KEY = 'error_message';
17+
const REASON_KEY = 'reason';
18+
const VARIANT_KEY = 'variant';
19+
const VALUE_KEY = 'value';
20+
21+
export class LoggingHook implements BaseHook {
22+
readonly includeEvaluationContext: boolean = false;
23+
readonly logger = new SafeLogger(new DefaultLogger());
24+
25+
constructor(includeEvaluationContext: boolean = false) {
26+
this.includeEvaluationContext = !!includeEvaluationContext;
27+
}
28+
29+
before(hookContext: BeforeHookContext): void {
30+
const payload: LoggerPayload = { stage: 'before' };
31+
this.addCommonProps(payload, hookContext);
32+
this.logger.debug(payload);
33+
}
34+
35+
after(hookContext: Readonly<HookContext<FlagValue>>, evaluationDetails: EvaluationDetails<FlagValue>): void {
36+
const payload: LoggerPayload = { stage: 'after' };
37+
38+
payload[REASON_KEY] = evaluationDetails.reason;
39+
payload[VARIANT_KEY] = evaluationDetails.variant;
40+
payload[VALUE_KEY] = evaluationDetails.value;
41+
42+
this.addCommonProps(payload, hookContext);
43+
this.logger.debug(payload);
44+
}
45+
46+
error(hookContext: Readonly<HookContext<FlagValue>>, error: OpenFeatureError): void {
47+
const payload: LoggerPayload = { stage: 'error' };
48+
49+
payload[ERROR_MESSAGE_KEY] = error.message;
50+
payload[ERROR_CODE_KEY] = error.code;
51+
52+
this.addCommonProps(payload, hookContext);
53+
this.logger.error(payload);
54+
}
55+
56+
finally(hookContext: Readonly<HookContext<FlagValue>>, hookHints?: HookHints): void {
57+
this.logger.info(hookContext, hookHints);
58+
}
59+
60+
private addCommonProps(payload: LoggerPayload, hookContext: HookContext): void {
61+
payload[DOMAIN_KEY] = hookContext.clientMetadata.domain;
62+
payload[PROVIDER_NAME_KEY] = hookContext.providerMetadata.name;
63+
payload[FLAG_KEY_KEY] = hookContext.flagKey;
64+
payload[DEFAULT_VALUE_KEY] = hookContext.defaultValue;
65+
66+
if (this.includeEvaluationContext) {
67+
payload[EVALUATION_CONTEXT_KEY] = hookContext.context;
68+
}
69+
}
70+
}

packages/shared/src/logger/default-logger.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,11 @@ export class DefaultLogger implements Logger {
1111
console.warn(...args);
1212
}
1313

14-
info(): void {}
15-
16-
debug(): void {}
14+
info(...args: unknown[]): void {
15+
console.info(...args);
16+
}
17+
18+
debug(...args: unknown[]): void {
19+
console.debug(...args);
20+
}
1721
}
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
import { GeneralError } from '../src/errors';
2+
import type { HookContext } from '../src/hooks/hooks';
3+
import { LoggingHook } from '../src/hooks/logging-hook';
4+
import type { SafeLogger } from '../src/logger';
5+
6+
describe('LoggingHook', () => {
7+
const FLAG_KEY = 'some-key';
8+
const DEFAULT_VALUE = 'default';
9+
const DOMAIN = 'some-domain';
10+
const PROVIDER_NAME = 'some-provider';
11+
const REASON = 'some-reason';
12+
const VALUE = 'some-value';
13+
const VARIANT = 'some-variant';
14+
const ERROR_MESSAGE = 'some fake error!';
15+
const DOMAIN_KEY = 'domain';
16+
const PROVIDER_NAME_KEY = 'provider_name';
17+
const FLAG_KEY_KEY = 'flag_key';
18+
const DEFAULT_VALUE_KEY = 'default_value';
19+
const EVALUATION_CONTEXT_KEY = 'evaluation_context';
20+
const ERROR_CODE_KEY = 'error_code';
21+
const ERROR_MESSAGE_KEY = 'error_message';
22+
23+
// const ERROR_CODE = 'GENERAL';
24+
25+
let hookContext: HookContext;
26+
let logger: SafeLogger;
27+
28+
beforeEach(() => {
29+
const mockProviderMetaData = { name: PROVIDER_NAME };
30+
31+
// Mock the hook context
32+
hookContext = {
33+
flagKey: FLAG_KEY,
34+
defaultValue: DEFAULT_VALUE,
35+
flagValueType: 'boolean',
36+
context: { targetingKey: 'some-targeting-key' },
37+
logger: logger,
38+
clientMetadata: { domain: DOMAIN, providerMetadata: mockProviderMetaData },
39+
providerMetadata: mockProviderMetaData,
40+
};
41+
42+
console.debug = jest.fn();
43+
console.warn = jest.fn();
44+
console.info = jest.fn();
45+
console.error = jest.fn();
46+
});
47+
48+
afterEach(() => {
49+
jest.restoreAllMocks();
50+
});
51+
52+
test('should log all props except evaluation context in before hook', () => {
53+
const hook = new LoggingHook(false);
54+
55+
hook.before(hookContext);
56+
57+
expect(console.debug).toHaveBeenCalled();
58+
59+
expect((console.debug as jest.Mock).mock.calls[0][0]).toMatchObject({
60+
stage: 'before',
61+
[DOMAIN_KEY]: hookContext.clientMetadata.domain,
62+
[PROVIDER_NAME_KEY]: hookContext.providerMetadata.name,
63+
[FLAG_KEY_KEY]: hookContext.flagKey,
64+
[DEFAULT_VALUE_KEY]: hookContext.defaultValue
65+
});
66+
67+
});
68+
69+
test('should log all props and evaluation context in before hook when enabled', () => {
70+
const hook = new LoggingHook(true);
71+
72+
hook.before(hookContext);
73+
74+
expect(console.debug).toHaveBeenCalled();
75+
76+
expect((console.debug as jest.Mock).mock.calls[0][0]).toMatchObject({
77+
stage: 'before',
78+
[DOMAIN_KEY]: hookContext.clientMetadata.domain,
79+
[PROVIDER_NAME_KEY]: hookContext.providerMetadata.name,
80+
[FLAG_KEY_KEY]: hookContext.flagKey,
81+
[DEFAULT_VALUE_KEY]: hookContext.defaultValue,
82+
[EVALUATION_CONTEXT_KEY]: hookContext.context
83+
});
84+
85+
});
86+
87+
test('should log all props except evaluation context in after hook', () => {
88+
const hook = new LoggingHook(false);
89+
const details = { flagKey: FLAG_KEY, flagMetadata: {}, reason: REASON, variant: VARIANT, value: VALUE };
90+
91+
hook.after(hookContext, details);
92+
93+
expect(console.debug).toHaveBeenCalled();
94+
95+
expect((console.debug as jest.Mock).mock.calls[0][0]).toMatchObject({
96+
stage: 'after',
97+
[DOMAIN_KEY]: hookContext.clientMetadata.domain,
98+
[PROVIDER_NAME_KEY]: hookContext.providerMetadata.name,
99+
[FLAG_KEY_KEY]: hookContext.flagKey,
100+
[DEFAULT_VALUE_KEY]: hookContext.defaultValue
101+
});
102+
});
103+
104+
test('should log all props and evaluation context in after hook when enabled', () => {
105+
const hook = new LoggingHook(true);
106+
const details = { flagKey: FLAG_KEY, flagMetadata: {}, reason: REASON, variant: VARIANT, value: VALUE };
107+
108+
hook.after(hookContext, details);
109+
110+
expect(console.debug).toHaveBeenCalled();
111+
112+
expect((console.debug as jest.Mock).mock.calls[0][0]).toMatchObject({
113+
stage: 'after',
114+
[DOMAIN_KEY]: hookContext.clientMetadata.domain,
115+
[PROVIDER_NAME_KEY]: hookContext.providerMetadata.name,
116+
[FLAG_KEY_KEY]: hookContext.flagKey,
117+
[DEFAULT_VALUE_KEY]: hookContext.defaultValue,
118+
[EVALUATION_CONTEXT_KEY]: hookContext.context
119+
});
120+
});
121+
122+
test('should log all props except evaluation context in error hook', () => {
123+
const hook = new LoggingHook(false);
124+
const error = new GeneralError(ERROR_MESSAGE);
125+
126+
hook.error(hookContext, error);
127+
128+
expect(console.error).toHaveBeenCalled();
129+
130+
expect((console.error as jest.Mock).mock.calls[0][0]).toMatchObject({
131+
stage: 'error',
132+
[ERROR_MESSAGE_KEY]: error.message,
133+
[ERROR_CODE_KEY]: error.code,
134+
[DOMAIN_KEY]: hookContext.clientMetadata.domain,
135+
[PROVIDER_NAME_KEY]: hookContext.providerMetadata.name,
136+
[FLAG_KEY_KEY]: hookContext.flagKey,
137+
[DEFAULT_VALUE_KEY]: hookContext.defaultValue,
138+
});
139+
});
140+
141+
test('should log all props and evaluation context in error hook when enabled', () => {
142+
const hook = new LoggingHook(true);
143+
const error = new GeneralError(ERROR_MESSAGE);
144+
145+
hook.error(hookContext, error);
146+
147+
expect(console.error).toHaveBeenCalled();
148+
149+
expect((console.error as jest.Mock).mock.calls[0][0]).toMatchObject({
150+
stage: 'error',
151+
[ERROR_MESSAGE_KEY]: error.message,
152+
[ERROR_CODE_KEY]: error.code,
153+
[DOMAIN_KEY]: hookContext.clientMetadata.domain,
154+
[PROVIDER_NAME_KEY]: hookContext.providerMetadata.name,
155+
[FLAG_KEY_KEY]: hookContext.flagKey,
156+
[DEFAULT_VALUE_KEY]: hookContext.defaultValue,
157+
[EVALUATION_CONTEXT_KEY]: hookContext.context
158+
});
159+
});
160+
});

0 commit comments

Comments
 (0)