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 9, 2024
1 parent af17f4a commit 5b2ff48
Show file tree
Hide file tree
Showing 5 changed files with 146 additions and 0 deletions.
57 changes: 57 additions & 0 deletions @xen-orchestra/log/.USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,3 +192,60 @@ 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
},
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')

// Returned value and error is forwarded by `captureLogs`
return Math.PI
}
)
```
57 changes: 57 additions & 0 deletions @xen-orchestra/log/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,63 @@ 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
},
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')

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

## Contributions

Contributions are _very_ welcomed, either on the documentation or on
Expand Down
25 changes: 25 additions & 0 deletions @xen-orchestra/log/capture.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
'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())

exports.captureLogs = function (onLog, fn) {
return asyncStorage.run(onLog, fn)
}

exports.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 @@ -38,6 +38,7 @@
<!--packages-start-->

- @xen-orchestra/log minor
- xo-server minor
- xo-server-perf-alert minor
- xo-server-sdn-controller patch
Expand Down

0 comments on commit 5b2ff48

Please sign in to comment.