cls-proxify
Logging on steroids with CLS and Proxy. A small library that proxies any arbitrary object with a proxy from Continuation-Local Storage a.k.a. CLS if found one. Super-useful for creating child loggers per each request with dynamic context from the request itself (e.g. adding request trace ID, adding request payload). Integrated with express, koa, fastify out-of-the-box.
Many thanks to @mcollina for the idea of combining Proxy and CLS.
Installation
npm i cls-proxify cls-hooked
Quick start
Express
TypeScript users, clsProxifyExpressMiddleware
uses typings from @types/express
. Please, run npm i -D @types/express
import { clsProxify } from 'cls-proxify'
import { clsProxifyExpressMiddleware } from 'cls-proxify/integration/express'
import * as express from 'express'
const logger = {
info: (msg: string) => console.log(msg),
}
const loggerCls = clsProxify(logger)
const app = express()
app.use(
clsProxifyExpressMiddleware((req) => {
const headerRequestID = req.headers.Traceparent
const loggerProxy = {
info: (msg: string) => `${headerRequestID}: ${msg}`,
}
return loggerProxy
}),
)
app.get('/test', (req, res) => {
loggerCls.info('My message!')
})
Koa
TypeScript users, clsProxifyKoaMiddleware
uses typings from @types/koa
. Please, run npm i -D @types/koa
import { clsProxify } from 'cls-proxify'
import { clsProxifyKoaMiddleware } from 'cls-proxify/integration/koa'
import * as Koa from 'koa'
const logger = {
info: (msg: string) => console.log(msg),
}
const loggerCls = clsProxify(logger)
const app = new Koa()
app.use(
clsProxifyKoaMiddleware((ctx) => {
const headerRequestID = ctx.req.headers.Traceparent
const loggerProxy = {
info: (msg: string) => `${headerRequestID}: ${msg}`,
}
return loggerProxy
}),
)
app.use((ctx) => {
loggerCls.info('My message!')
})
Fastify
import { clsProxify } from 'cls-proxify'
import { clsProxifyFastifyMiddleware } from 'cls-proxify/integration/fastify'
import * as fastify from 'fastify'
const logger = {
info: (msg: string) => console.log(msg),
}
const loggerCls = clsProxify(logger)
const app = fastify()
app.use(
clsProxifyFastifyMiddleware((req) => {
const headerRequestID = ctx.req.headers.Traceparent
const loggerProxy = {
info: (msg: string) => `${headerRequestID}: ${msg}`,
}
return loggerProxy
}),
)
app.get('/test', (req, res) => {
loggerCls.info('My message!')
})
Any other framework or library
import { clsProxify, getClsHookedStorage } from 'cls-proxify'
import AbstractWebServer from 'abstract-web-server'
const logger = {
info: (msg: string) => console.log(msg),
}
const loggerCls = clsProxify(logger)
const app = new AbstractWebServer()
app.use((request, response, next) => {
getClsHookedStorage().namespace.bindEmitter(request)
getClsHookedStorage().namespace.bindEmitter(response)
getClsHookedStorage().namespace.run(() => {
const headerRequestID = request.headers.Traceparent
const loggerProxy = {
info: (msg: string) => `${headerRequestID}: ${msg}`,
}
getClsHookedStorage().set(loggerProxy)
next()
})
})
app.get('/test', (req, res) => {
loggerCls.info('My message!')
})
Set custom CLS storage
import { clsProxify, setClsHookedStorage, ClsHookedStorage, ClsProxifyStorage } from 'cls-proxify'
import AbstractWebServer from 'abstract-web-server'
class CustomClsStorage extends ClsHookedStorage {
public readonly namespace = createNamespace('myNamespace')
// Or override CLS key
protected readonly key = 'yoda'
}
setClsHookedStorage(new CustomClsStorage())
// Or you can implement your own storage from scratch.
// Just make sure it conforms to `ClsProxifyStorage` interface.
class SecretStorage<T> implements ClsProxifyStorage<T> {
set(proxy: T): void {}
get(): T | undefined {}
}
setClsHookedStorage(new SecretStorage())
In depth
How it works
If you're struggling to grasp the idea behind CLS at these two articles: Request Id Tracing in Node.js Applications, A Pragmatic Overview of Async Hooks API in Node.js.
Take a look at this article which overviews how CLS works and covers the idea behind this library.
We wrap our original logger in a Proxy. Every time we try to access any property of that object we first check if there's an updated logger in CLS available. If it's there we take the property from it. If it's not we take the property from the original logger. Then for every request we create a CLS context using run
and bindEmitter
. Once the context is created we enhance our original logger with some extra data and put the updated logger in the context. Once we try to call any method of our logger we'll actually call the same method on our logger in CLS.
Does it work only for loggers?
No. You can proxify any object you want. Moreover you can even proxify functions and class constructors.
Here's a list of traps cls-proxify provides:
Take a look at the tests to get an idea of how you can utilize them.
Live demos
Troubleshooting
My context got lost
Note that some middlewares may cause CLS context to get lost. To avoid it use any third party middleware that does not need access to request ids before you use this middleware.
I'm experiencing a memory leak
Make sure you don't keep any external references to the objects inside of CLS. It may prevent them from being collected by GC.
Take a look at this issues: #21, #11.