Skip to content

Commit

Permalink
feat(log): helper to capture logs in an async stack
Browse files Browse the repository at this point in the history
  • Loading branch information
julien-f committed Sep 25, 2024
1 parent c0f1d61 commit 1e051bd
Show file tree
Hide file tree
Showing 5 changed files with 172 additions and 0 deletions.
63 changes: 63 additions & 0 deletions @xen-orchestra/log/.USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,3 +192,66 @@ app ERROR Something went wrong
app ERROR duplicates of the previous log were hidden { nDuplicates: 2 }
app INFO This is a different message
```

#### Capture

> Allow capturing all logs emitted during a call, even through asynchronous operations.
Before being able to use this feature, you need to add the transport:

```js
import { configure } from '@xen-orchestra/log/configure'
import { createCaptureTransport } from '@xen-orchestra/log/capture'
import createConsoleTransport from '@xen-orchestra/log/transports/console'

// transport that will be used globally, when not in a captured environment
const fallbackTransport = {
filter: process.env.DEBUG,
level: 'warn',

transport: createConsoleTransport(),
}

// create the capture transport and pass it the fallback one
const captureTransport = createCaptureTransport(fallbackTransport)

// configure @xen-orchestra/log to use our transport
configure(captureTransport)
```

Now the `captureLogs(onLog, fn)` can be used:

```js
import { captureLogs } from '@xen-orchestra/log/capture'
import { createLogger } from '@xen-orchestra/log'

const logger = createLogger('my-logger')

await captureLogs(
log => {
// every logs emitted in the async context of `fn` will arrive here
//
// do not emit logs in this function or this will create a loop.
},
async () => {
logger.debug('synchronous logs are captured')

setTimeout(() => {
logger.debug('logs from asynchronous callbacks too')
}, 50)

await new Promise(resolve => setTimeout(resolve, 50))

logger.debug('logs in async functions or promise chains too')

// To escape capture, run code in `captureLogs` with `undefined`
// as the first param
captureLogs(undefined, () => {
logger.debug('this log will not be captured')
})

// Returned value and error is forwarded by `captureLogs`
return Math.PI
}
)
```
63 changes: 63 additions & 0 deletions @xen-orchestra/log/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,69 @@ app ERROR duplicates of the previous log were hidden { nDuplicates: 2 }
app INFO This is a different message
```

#### Capture

> Allow capturing all logs emitted during a call, even through asynchronous operations.
Before being able to use this feature, you need to add the transport:

```js
import { configure } from '@xen-orchestra/log/configure'
import { createCaptureTransport } from '@xen-orchestra/log/capture'
import createConsoleTransport from '@xen-orchestra/log/transports/console'

// transport that will be used globally, when not in a captured environment
const fallbackTransport = {
filter: process.env.DEBUG,
level: 'warn',

transport: createConsoleTransport(),
}

// create the capture transport and pass it the fallback one
const captureTransport = createCaptureTransport(fallbackTransport)

// configure @xen-orchestra/log to use our transport
configure(captureTransport)
```

Now the `captureLogs(onLog, fn)` can be used:

```js
import { captureLogs } from '@xen-orchestra/log/capture'
import { createLogger } from '@xen-orchestra/log'

const logger = createLogger('my-logger')

await captureLogs(
log => {
// every logs emitted in the async context of `fn` will arrive here
//
// do not emit logs in this function or this will create a loop.
},
async () => {
logger.debug('synchronous logs are captured')

setTimeout(() => {
logger.debug('logs from asynchronous callbacks too')
}, 50)

await new Promise(resolve => setTimeout(resolve, 50))

logger.debug('logs in async functions or promise chains too')

// To escape capture, run code in `captureLogs` with `undefined`
// as the first param
captureLogs(undefined, () => {
logger.debug('this log will not be captured')
})

// Returned value and error is forwarded by `captureLogs`
return Math.PI
}
)
```

## Contributions

Contributions are _very_ welcomed, either on the documentation or on
Expand Down
39 changes: 39 additions & 0 deletions @xen-orchestra/log/capture.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
'use strict'

// Even though the lib is compatible with Node >=8.3,
// the capture feature requires Node >=13.10
//
// eslint-disable-next-line n/no-unsupported-features/node-builtins
const { AsyncLocalStorage } = require('node:async_hooks')

const createTransport = require('./_createTransport.js')

// stored in the global context so that various versions of the library can interact.
const symbol = Symbol.for('@xen-orchestra/log/capture')
const asyncStorage = global[symbol] || (global[symbol] = new AsyncLocalStorage())

/**
* Runs `fn` capturing all emitted logs (sync and async) and forward them to `onLog`.
*
* @template {(...args: any) => any} F
* @param {undefined | (log: object) => void} onLog
* @param {F} fn
* @returns {ReturnType<F>}
*/
exports.captureLogs = function captureLogs(onLog, fn) {
return asyncStorage.run(onLog, fn)
}

/**
* Creates a transport for the `captureLogs` feature.
*
* @param {*} fallback - The transport to use as a fallback when not capturing
* @returns {(log: object) => void}
*/
exports.createCaptureTransport = function createCaptureTransport(fallback) {
fallback = fallback === undefined ? Function.prototype : createTransport(fallback)

return function captureTransport(log) {
;(asyncStorage.getStore() || fallback)(log)
}
}
6 changes: 6 additions & 0 deletions @xen-orchestra/log/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@
"version": "0.6.0",
"license": "ISC",
"description": "Logging system with decoupled producers/consumer",
"keywords": [
"async",
"asynchronous",
"capture",
"context"
],
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/log",
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"repository": {
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@

- @xen-orchestra/defined patch
- @xen-orchestra/lite minor
- @xen-orchestra/log minor
- @xen-orchestra/web minor
- @xen-orchestra/web-core minor
- @xen-orchestra/xapi patch
Expand Down

0 comments on commit 1e051bd

Please sign in to comment.