Skip to content

Instantly share code, notes, and snippets.

@quezak
Last active April 30, 2020 10:32
Show Gist options
  • Save quezak/0a9f33d20aa0c13fd86f547f75d9da47 to your computer and use it in GitHub Desktop.
Save quezak/0a9f33d20aa0c13fd86f547f75d9da47 to your computer and use it in GitHub Desktop.
cls-hooked usage example: context tagged logs
// 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