Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(log): helper to capture logs in an async stack #7975

Merged
merged 1 commit into from
Oct 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
2 changes: 2 additions & 0 deletions CHANGELOG.unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,6 @@
<!--packages-start-->

- @xen-orchestra/log minor

<!--packages-end-->
Loading