• Stars
    star
    156
  • Rank 234,889 (Top 5 %)
  • Language
    TypeScript
  • License
    MIT License
  • Created over 5 years ago
  • Updated over 2 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Logging on steroids with CLS and Proxy. Integrated with express, koa, fastify.

cls-proxify Build Status Coverage Status Tweet

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}`,
    }
    // this value will be accesible in CLS by key 'cls-proxify'
    // it will be used as a proxy for `loggerCls`
    return loggerProxy
  }),
)

app.get('/test', (req, res) => {
  loggerCls.info('My message!')
  // Logs `${headerRequestID}: My message!` into the console
  // Say, we send GET /test with header 'Traceparent' set to 12345
  // It's going to log '12345: My message!'
  // If it doesn't find anything in CLS by key 'cls-proxify' it uses the original `logger` and logs '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}`,
    }
    // this value will be accesible in CLS by key 'cls-proxify'
    // it will be used as a proxy for `loggerCls`
    return loggerProxy
  }),
)

app.use((ctx) => {
  loggerCls.info('My message!')
  // Logs `${headerRequestID}: My message!` into the console
  // Say, we send GET / with header 'Traceparent' set to 12345
  // It's going to log '12345: My message!'
  // If it doesn't find anything in CLS by key 'cls-proxify' it uses the original `logger` and logs 'My message!'
})

Fastify

import { clsProxify } from 'cls-proxify'
import { clsProxifyFastifyPlugin } 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.register(clsProxifyFastifyPlugin, {
  proxify: (req) => {
    const headerRequestID = ctx.req.headers.Traceparent
    const loggerProxy = {
      info: (msg: string) => `${headerRequestID}: ${msg}`,
    }
    // this value will be accesible in CLS by key 'cls-proxify'
    // it will be used as a proxy for `loggerCls`
    return loggerProxy
  },
})

app.get('/test', (req, res) => {
  loggerCls.info('My message!')
  // Logs `${headerRequestID}: My message!` into the console
  // Say, we send GET /test with header 'Traceparent' set to 12345
  // It's going to log '12345: My message!'
  // If it doesn't find anything in CLS by key 'cls-proxify' it uses the original `logger` and logs '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()
// Assuming this AbstractWebServer supports some form of middlewares
app.use((request, response, next) => {
  // Assuming your request and response are event emitters
  getClsHookedStorage().namespace.bindEmitter(request)
  getClsHookedStorage().namespace.bindEmitter(response)

  getClsHookedStorage().namespace.run(() => {
    const headerRequestID = request.headers.Traceparent
    // this value will be accesible in CLS by key 'cls-proxify'
    // it will be used as a proxy for `loggerCls`
    const loggerProxy = {
      info: (msg: string) => `${headerRequestID}: ${msg}`,
    }
    getClsHookedStorage().set(loggerProxy)

    next()
  })
})

app.get('/test', (req, res) => {
  loggerCls.info('My message!')
  // Logs `${headerRequestID}: My message!` into the console
  // Say, we send GET /test with header 'Traceparent' set to 12345
  // It's going to log '12345: My message!'
  // If it doesn't find anything in CLS by key 'cls-proxify' it uses the original `logger` and logs 'My message!'
})

Set custom CLS storage

import { clsProxify, setClsHookedStorage, ClsHookedStorage, ClsProxifyStorage } from 'cls-proxify'
import AbstractWebServer from 'abstract-web-server'

// You can subclass existing ClsHookedStorage
class CustomClsStorage extends ClsHookedStorage {
  // Override namespace
  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

Usage with pino and fastify

Usage with pino and express

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.

More Repositories

1

class-logger

Boilerplate-free decorator-based class logging
TypeScript
69
star
2

singleton

Singleton decorator. No constructor monkeypatching. Zero dependencies. Built with TypeScript.
TypeScript
26
star
3

reducer-class

Boilerplate free class-based reducer creator. Built with TypeScript. Works with Redux and NGRX. Has integration with immer.
TypeScript
26
star
4

angular-builder-custom-terser-options

Angular builder that allows Terser (Uglify) customization
TypeScript
21
star
5

flux-action-class

Boilerplate free class-based action creator. Following flux-standard-action spec. Built with TypeScript. Works with redux and ngrx.
TypeScript
21
star
6

axios-rest-resource

Schema-based HTTP client powered by axios. Written in Typescript. Heavily inspired by AngularJS' $resource.
TypeScript
17
star
7

talks

HTML
14
star
8

fixed-size-list

A small library that brings a fixed-length list (aka circular buffer) with an event emitter to Typescript and Javascript
TypeScript
7
star
9

cls-class-proxy

A Proxy-based lightweight library to add Continuation-Local Storage aka (CLS) to class contructor, method calls, getters and setters.
TypeScript
6
star
10

types-redux-orm

Typescript types for redux-orm
5
star
11

chrome-tab-carousel

TypeScript
5
star
12

flex-attr

Flex Layout is a set of CSS rules, which brings the power of flexbox into your project without you writing a single line of CSS yourself. Available for LESS, SASS and vanilla CSS.
CSS
3
star
13

aigoncharov.github.io

HTML
2
star
14

hazelcast-web-proxy

TypeScript
2
star
15

metadata-utils

Convenience utils for reflected metadata
TypeScript
2
star
16

swe-to-ai-diary

1
star
17

css-attribute-selector-performance-test

Python
1
star
18

js-benchmark-function

JavaScript
1
star
19

angular-responsive-breakpoints

JavaScript
1
star