Last active
April 30, 2020 10:32
-
-
Save quezak/0a9f33d20aa0c13fd86f547f75d9da47 to your computer and use it in GitHub Desktop.
cls-hooked usage example: context tagged logs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// How to use cls-hooked, for example to tag logs with some useful request context. | |
// (https://github.com/Jeff-Lewis/cls-hooked) | |
// Confirmed to work correctly in production using Node 12.4. | |
// | |
// Author: github.com/quezak/, cheers from Ramp Network 💚💙 | |
// ----------------------------------- | |
// context-handler.ts | |
// ----------------------------------- | |
import { createNamespace } from 'cls-hooked'; | |
// Use any custom class here, containing all the details you need in a context | |
import { ContextData } from '..../context-data'; | |
// cls_hooked namespace ids should be globally unique, append the PID to be sure | |
const CONTEXT_NS_ID = `RAMP_SWAPS_CONTEXT_NS_${process.pid}`; | |
const CONTEXT_KEY = 'SWAP_CONTEXT'; | |
const CONTEXT_NS = createNamespace(CONTEXT_NS_ID); | |
/** inject this into your Express or Nest app: `app.use(httpContextMiddleware)` */ | |
export function httpContextMiddleware(req: any, res: any, next: any): void { | |
CONTEXT_NS.run(() => { | |
CONTEXT_NS.bindEmitter(req); | |
CONTEXT_NS.bindEmitter(res); | |
setContext(new ContextData(/* your context initializers */)); | |
next(); | |
}); | |
} | |
/** | |
* Use this to manually wrap externally-triggered scopes other than requests in a context, | |
* e.g. task handlers, interval jobs, etc. | |
*/ | |
export function contextWrapper<FT extends (...args: any[]) => any>( | |
contextOrFactory: ContextData | (() => ContextData), | |
fn: FT, | |
): (...args: Parameters<FT>) => ReturnType<FT> { | |
return (...args: Parameters<FT>) => | |
CONTEXT_NS.runAndReturn(() => { | |
const context = contextOrFactory instanceof ContextData | |
? contextOrFactory | |
: contextOrFactory(); | |
setContext(context); | |
return fn(...args); | |
}); | |
} | |
export function getContext(): ContextData | undefined { | |
return CONTEXT_NS.active | |
? CONTEXT_NS.get(CONTEXT_KEY) | |
: undefined; | |
} | |
export function setContext(context: ContextData): ContextData | undefined { | |
return CONTEXT_NS.active | |
? CONTEXT_NS.set(CONTEXT_KEY, context) | |
: undefined; | |
} | |
export function updateContext(updates: Partial<ContextData>): ContextData | undefined { | |
const context = getContext() ?? setContext(new ContextData()); | |
if (!context) { | |
// optional: Log.error(updateContext, 'cannot find context to update'); | |
return undefined; | |
} | |
return Object.assign(context, updates); | |
} | |
// ----------------------------------- | |
// example context usage: ContextLogger | |
// ----------------------------------- | |
import chalk from 'chalk'; | |
export class ContextLogger extends Logger { | |
private static readonly COLOR_CONTEXT_ID = chalk.bold.gray; | |
private static readonly COLOR_CONTEXT = chalk.cyan; | |
protected prepareHeader(level: EventSeverity, source: LogSource): string { | |
const context = getContext(); | |
const header = super.prepareHeader(level, source); | |
if (!context) return header; | |
const contextIdStr = ContextLogger.COLOR_CONTEXT_ID(`[${context.id}]`); | |
const userStr = context.userId ? `[U${context.userId}]` : ''; | |
// append purchase id, etc, whatever you need to easily search for in logs | |
const contextStr = ContextLogger.COLOR_CONTEXT(`${userStr}<other details>`); | |
return `${header}${contextIdStr}${contextStr}`; | |
} | |
} | |
// ----------------------------------- | |
// example non-request contexts: RabbitMQ task handlers with amqplib | |
// ----------------------------------- | |
export class QueuedTaskService { | |
// (simplified excerpt) | |
async registerConsumer(queueName: string, consumer: TaskConsumer): Promise<void> { | |
await this.channel.assertQueue(queueName); | |
await this.channel.consume( | |
queueName, | |
contextWrapper( | |
() => new ContextData({ type: QUEUED_TASK }), | |
// this can also set the task id in context | |
async msg => this.dispatchTask(msg, consumer), | |
), | |
{ noAck: false }, | |
); | |
} | |
} | |
// ----------------------------------- | |
// bonus cool non-request usage: make each test in jest a separate context, so in need of test | |
// debugging you can easily see the test id in errors or logs | |
// usage: append this file to `setupFilesAfterEnv` in your jest config | |
// ----------------------------------- | |
const __originalIt = it; | |
const __originalDescribe = describe; | |
let __suiteCounter = 0; | |
// Wrap the `describe` and `it` callbacks in a context: | |
// (1) to avoid 'cannot find context to update' warnings from functions that are normally called | |
// within a request/task context | |
// (2) to increase logs readability when debugging tests | |
// eslint-disable-next-line no-global-assign | |
describe = new Proxy(__originalDescribe, { | |
// eslint-disable-next-line @typescript-eslint/typedef | |
apply(describeTarget, describeThisArg, [suiteName, suiteFn]): void { | |
const suiteTag = `S${__suiteCounter++}`; | |
let __testCounter = 0; | |
// eslint-disable-next-line no-global-assign | |
it = new Proxy(__originalIt, { | |
// eslint-disable-next-line @typescript-eslint/typedef | |
apply(itTarget, itThisArg, [testName, testFn, ...restItParams]): void { | |
const testTag = `${suiteTag}T${__testCounter++}`; | |
const wrappedName = `[${testTag}] ${testName}`; | |
const wrappedFn = contextWrapper(new ContextData({ id: testTag }), testFn); | |
return itTarget.call(itThisArg, wrappedName, wrappedFn, ...restItParams); | |
}, | |
}); | |
return describeTarget.apply(describeThisArg, [ | |
`[${suiteTag}] ${suiteName}`, | |
contextWrapper(new ContextData({ id: suiteTag }), suiteFn), | |
]); | |
}, | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment