diff --git a/@xen-orchestra/log/.USAGE.md b/@xen-orchestra/log/.USAGE.md index 5a8e92824b7..146502b86f8 100644 --- a/@xen-orchestra/log/.USAGE.md +++ b/@xen-orchestra/log/.USAGE.md @@ -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 + } +) +``` diff --git a/@xen-orchestra/log/README.md b/@xen-orchestra/log/README.md index 6dd4a0fe1f2..53c49c83411 100644 --- a/@xen-orchestra/log/README.md +++ b/@xen-orchestra/log/README.md @@ -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 diff --git a/@xen-orchestra/log/capture.js b/@xen-orchestra/log/capture.js new file mode 100644 index 00000000000..b3d1f3d3951 --- /dev/null +++ b/@xen-orchestra/log/capture.js @@ -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} + */ +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) + } +} diff --git a/@xen-orchestra/log/package.json b/@xen-orchestra/log/package.json index 0b9e56b759c..8f60388bd7f 100644 --- a/@xen-orchestra/log/package.json +++ b/@xen-orchestra/log/package.json @@ -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": { diff --git a/CHANGELOG.unreleased.md b/CHANGELOG.unreleased.md index 8a068cfd09e..8b1cff256aa 100644 --- a/CHANGELOG.unreleased.md +++ b/CHANGELOG.unreleased.md @@ -38,6 +38,7 @@ +- @xen-orchestra/log minor - xo-server minor - xo-server-perf-alert minor - xo-server-sdn-controller patch