-
Notifications
You must be signed in to change notification settings - Fork 42
feat: logging hook #1114
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
feat: logging hook #1114
Changes from 4 commits
26dbbaf
4112a0e
730a57d
ab62ee4
ae87eeb
4efe485
8d6a353
64cc2ff
97f4eb7
3277412
587bf1a
c4c4620
d9ca44f
2743830
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
import type { OpenFeatureError } from '../errors'; | ||
import type { BaseHook } from './hook'; | ||
import type { BeforeHookContext, HookContext, HookHints } from './hooks'; | ||
import type { FlagValue, EvaluationDetails } from '../evaluation'; | ||
|
||
import { DefaultLogger, SafeLogger } from '../logger'; | ||
|
||
type LoggerPayload = Record<string, unknown>; | ||
|
||
const DOMAIN_KEY = 'domain'; | ||
const PROVIDER_NAME_KEY = 'provider_name'; | ||
const FLAG_KEY_KEY = 'flag_key'; | ||
const DEFAULT_VALUE_KEY = 'default_value'; | ||
const EVALUATION_CONTEXT_KEY = 'evaluation_context'; | ||
const ERROR_CODE_KEY = 'error_code'; | ||
const ERROR_MESSAGE_KEY = 'error_message'; | ||
const REASON_KEY = 'reason'; | ||
const VARIANT_KEY = 'variant'; | ||
const VALUE_KEY = 'value'; | ||
|
||
export class LoggingHook implements BaseHook { | ||
readonly includeEvaluationContext: boolean = false; | ||
readonly logger = new SafeLogger(new DefaultLogger(true, true)); | ||
|
||
constructor(includeEvaluationContext: boolean = false) { | ||
this.includeEvaluationContext = !!includeEvaluationContext; | ||
} | ||
|
||
before(hookContext: BeforeHookContext): void { | ||
const payload: LoggerPayload = { stage: 'before' }; | ||
this.addCommonProps(payload, hookContext); | ||
this.logger.debug(payload); | ||
} | ||
|
||
after(hookContext: Readonly<HookContext<FlagValue>>, evaluationDetails: EvaluationDetails<FlagValue>): void { | ||
const payload: LoggerPayload = { stage: 'after' }; | ||
|
||
payload[REASON_KEY] = evaluationDetails.reason; | ||
payload[VARIANT_KEY] = evaluationDetails.variant; | ||
payload[VALUE_KEY] = evaluationDetails.value; | ||
|
||
this.addCommonProps(payload, hookContext); | ||
this.logger.debug(payload); | ||
} | ||
|
||
error(hookContext: Readonly<HookContext<FlagValue>>, error: OpenFeatureError): void { | ||
const payload: LoggerPayload = { stage: 'error' }; | ||
|
||
payload[ERROR_MESSAGE_KEY] = error.message; | ||
payload[ERROR_CODE_KEY] = error.code; | ||
|
||
this.addCommonProps(payload, hookContext); | ||
this.logger.error(payload); | ||
} | ||
|
||
finally(hookContext: Readonly<HookContext<FlagValue>>, hookHints?: HookHints): void { | ||
beeme1mr marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
this.logger.info(hookContext, hookHints); | ||
} | ||
|
||
private addCommonProps(payload: LoggerPayload, hookContext: HookContext): void { | ||
payload[DOMAIN_KEY] = hookContext.clientMetadata.domain; | ||
payload[PROVIDER_NAME_KEY] = hookContext.providerMetadata.name; | ||
payload[FLAG_KEY_KEY] = hookContext.flagKey; | ||
payload[DEFAULT_VALUE_KEY] = hookContext.defaultValue; | ||
|
||
if (this.includeEvaluationContext) { | ||
payload[EVALUATION_CONTEXT_KEY] = hookContext.context; | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,6 +3,15 @@ | |
import type { Logger } from './logger'; | ||
|
||
export class DefaultLogger implements Logger { | ||
|
||
private readonly showInfo : boolean = false; | ||
private readonly showDebug : boolean = false; | ||
|
||
constructor(showInfo: boolean = false, showDebug: boolean = false){ | ||
beeme1mr marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
this.showInfo = showInfo; | ||
this.showDebug = showDebug; | ||
} | ||
|
||
error(...args: unknown[]): void { | ||
console.error(...args); | ||
} | ||
|
@@ -11,7 +20,15 @@ export class DefaultLogger implements Logger { | |
console.warn(...args); | ||
} | ||
|
||
info(): void {} | ||
|
||
debug(): void {} | ||
info(...args: unknown[]): void { | ||
if(this.showInfo) { | ||
console.info(...args); | ||
} | ||
} | ||
|
||
debug(...args: unknown[]): void { | ||
if(this.showDebug) { | ||
console.debug(...args); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,158 @@ | ||
import { GeneralError } from '../src/errors'; | ||
import type { HookContext } from '../src/hooks/hooks'; | ||
import { LoggingHook } from '../src/hooks/logging-hook'; | ||
import { DefaultLogger, SafeLogger } from '../src/logger'; | ||
|
||
describe('LoggingHook', () => { | ||
const FLAG_KEY = 'some-key'; | ||
const DEFAULT_VALUE = 'default'; | ||
const DOMAIN = 'some-domain'; | ||
const PROVIDER_NAME = 'some-provider'; | ||
const REASON = 'some-reason'; | ||
const VALUE = 'some-value'; | ||
const VARIANT = 'some-variant'; | ||
const ERROR_MESSAGE = 'some fake error!'; | ||
const DOMAIN_KEY = 'domain'; | ||
const PROVIDER_NAME_KEY = 'provider_name'; | ||
const FLAG_KEY_KEY = 'flag_key'; | ||
const DEFAULT_VALUE_KEY = 'default_value'; | ||
const EVALUATION_CONTEXT_KEY = 'evaluation_context'; | ||
const ERROR_CODE_KEY = 'error_code'; | ||
const ERROR_MESSAGE_KEY = 'error_message'; | ||
|
||
let hookContext: HookContext; | ||
const logger : SafeLogger = new SafeLogger(new DefaultLogger(true, true)); | ||
|
||
beforeEach(() => { | ||
const mockProviderMetaData = { name: PROVIDER_NAME }; | ||
|
||
// Mock the hook context | ||
hookContext = { | ||
flagKey: FLAG_KEY, | ||
defaultValue: DEFAULT_VALUE, | ||
flagValueType: 'boolean', | ||
context: { targetingKey: 'some-targeting-key' }, | ||
logger: logger, | ||
clientMetadata: { domain: DOMAIN, providerMetadata: mockProviderMetaData }, | ||
providerMetadata: mockProviderMetaData, | ||
}; | ||
|
||
console.debug = jest.fn(); | ||
console.warn = jest.fn(); | ||
console.info = jest.fn(); | ||
console.error = jest.fn(); | ||
}); | ||
|
||
afterEach(() => { | ||
jest.restoreAllMocks(); | ||
}); | ||
|
||
test('should log all props except evaluation context in before hook', () => { | ||
const hook = new LoggingHook(false); | ||
|
||
hook.before(hookContext); | ||
|
||
expect(console.debug).toHaveBeenCalled(); | ||
|
||
expect((console.debug as jest.Mock).mock.calls[0][0]).toMatchObject({ | ||
stage: 'before', | ||
[DOMAIN_KEY]: hookContext.clientMetadata.domain, | ||
[PROVIDER_NAME_KEY]: hookContext.providerMetadata.name, | ||
[FLAG_KEY_KEY]: hookContext.flagKey, | ||
[DEFAULT_VALUE_KEY]: hookContext.defaultValue | ||
}); | ||
|
||
}); | ||
|
||
test('should log all props and evaluation context in before hook when enabled', () => { | ||
const hook = new LoggingHook(true); | ||
|
||
hook.before(hookContext); | ||
|
||
expect(console.debug).toHaveBeenCalled(); | ||
|
||
expect((console.debug as jest.Mock).mock.calls[0][0]).toMatchObject({ | ||
stage: 'before', | ||
[DOMAIN_KEY]: hookContext.clientMetadata.domain, | ||
[PROVIDER_NAME_KEY]: hookContext.providerMetadata.name, | ||
[FLAG_KEY_KEY]: hookContext.flagKey, | ||
[DEFAULT_VALUE_KEY]: hookContext.defaultValue, | ||
[EVALUATION_CONTEXT_KEY]: hookContext.context | ||
}); | ||
|
||
}); | ||
|
||
test('should log all props except evaluation context in after hook', () => { | ||
const hook = new LoggingHook(false); | ||
const details = { flagKey: FLAG_KEY, flagMetadata: {}, reason: REASON, variant: VARIANT, value: VALUE }; | ||
|
||
hook.after(hookContext, details); | ||
|
||
expect(console.debug).toHaveBeenCalled(); | ||
|
||
expect((console.debug as jest.Mock).mock.calls[0][0]).toMatchObject({ | ||
stage: 'after', | ||
[DOMAIN_KEY]: hookContext.clientMetadata.domain, | ||
[PROVIDER_NAME_KEY]: hookContext.providerMetadata.name, | ||
[FLAG_KEY_KEY]: hookContext.flagKey, | ||
[DEFAULT_VALUE_KEY]: hookContext.defaultValue | ||
}); | ||
}); | ||
|
||
test('should log all props and evaluation context in after hook when enabled', () => { | ||
const hook = new LoggingHook(true); | ||
const details = { flagKey: FLAG_KEY, flagMetadata: {}, reason: REASON, variant: VARIANT, value: VALUE }; | ||
|
||
hook.after(hookContext, details); | ||
|
||
expect(console.debug).toHaveBeenCalled(); | ||
|
||
expect((console.debug as jest.Mock).mock.calls[0][0]).toMatchObject({ | ||
stage: 'after', | ||
[DOMAIN_KEY]: hookContext.clientMetadata.domain, | ||
[PROVIDER_NAME_KEY]: hookContext.providerMetadata.name, | ||
[FLAG_KEY_KEY]: hookContext.flagKey, | ||
[DEFAULT_VALUE_KEY]: hookContext.defaultValue, | ||
[EVALUATION_CONTEXT_KEY]: hookContext.context | ||
}); | ||
}); | ||
|
||
test('should log all props except evaluation context in error hook', () => { | ||
const hook = new LoggingHook(false); | ||
const error = new GeneralError(ERROR_MESSAGE); | ||
|
||
hook.error(hookContext, error); | ||
|
||
expect(console.error).toHaveBeenCalled(); | ||
|
||
expect((console.error as jest.Mock).mock.calls[0][0]).toMatchObject({ | ||
stage: 'error', | ||
[ERROR_MESSAGE_KEY]: error.message, | ||
[ERROR_CODE_KEY]: error.code, | ||
[DOMAIN_KEY]: hookContext.clientMetadata.domain, | ||
[PROVIDER_NAME_KEY]: hookContext.providerMetadata.name, | ||
[FLAG_KEY_KEY]: hookContext.flagKey, | ||
[DEFAULT_VALUE_KEY]: hookContext.defaultValue, | ||
}); | ||
}); | ||
|
||
test('should log all props and evaluation context in error hook when enabled', () => { | ||
const hook = new LoggingHook(true); | ||
const error = new GeneralError(ERROR_MESSAGE); | ||
|
||
hook.error(hookContext, error); | ||
|
||
expect(console.error).toHaveBeenCalled(); | ||
|
||
expect((console.error as jest.Mock).mock.calls[0][0]).toMatchObject({ | ||
stage: 'error', | ||
[ERROR_MESSAGE_KEY]: error.message, | ||
[ERROR_CODE_KEY]: error.code, | ||
[DOMAIN_KEY]: hookContext.clientMetadata.domain, | ||
[PROVIDER_NAME_KEY]: hookContext.providerMetadata.name, | ||
[FLAG_KEY_KEY]: hookContext.flagKey, | ||
[DEFAULT_VALUE_KEY]: hookContext.defaultValue, | ||
[EVALUATION_CONTEXT_KEY]: hookContext.context | ||
}); | ||
}); | ||
}); |
Uh oh!
There was an error while loading. Please reload this page.