@api3/commons 0.3.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -49,6 +49,8 @@ To release a new version follow these steps:
49
49
  commit it.
50
50
  3. `pnpm publish --access public` - Publish the new version to NPM.
51
51
  4. `git push --follow-tags` - Push the tagged commit upstream.
52
+ 5. Create a new [release on GitHub](https://github.com/api3dao/commons/releases). Use the "Generate release notes"
53
+ feature to generate the release notes from the PR titles.
52
54
 
53
55
  ## Development notes
54
56
 
@@ -0,0 +1,5 @@
1
+ /// <reference types="node" />
2
+ import { AsyncLocalStorage } from 'node:async_hooks';
3
+ import type { LogContext } from '.';
4
+ export declare const getAsyncLocalStorage: () => AsyncLocalStorage<LogContext>;
5
+ //# sourceMappingURL=async-storage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"async-storage.d.ts","sourceRoot":"","sources":["../../src/logger/async-storage.ts"],"names":[],"mappings":";AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAErD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,GAAG,CAAC;AAIpC,eAAO,MAAM,oBAAoB,qCAGhC,CAAC"}
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getAsyncLocalStorage = void 0;
4
+ const node_async_hooks_1 = require("node:async_hooks");
5
+ let asyncLocalStorage;
6
+ const getAsyncLocalStorage = () => {
7
+ if (!asyncLocalStorage)
8
+ asyncLocalStorage = new node_async_hooks_1.AsyncLocalStorage();
9
+ return asyncLocalStorage;
10
+ };
11
+ exports.getAsyncLocalStorage = getAsyncLocalStorage;
12
+ //# sourceMappingURL=async-storage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"async-storage.js","sourceRoot":"","sources":["../../src/logger/async-storage.ts"],"names":[],"mappings":";;;AAAA,uDAAqD;AAIrD,IAAI,iBAAgD,CAAC;AAE9C,MAAM,oBAAoB,GAAG,GAAG,EAAE;IACvC,IAAI,CAAC,iBAAiB;QAAE,iBAAiB,GAAG,IAAI,oCAAiB,EAAc,CAAC;IAChF,OAAO,iBAAiB,CAAC;AAC3B,CAAC,CAAC;AAHW,QAAA,oBAAoB,wBAG/B"}
@@ -1,3 +1,4 @@
1
+ import winston from 'winston';
1
2
  export declare const logFormatOptions: readonly ["json", "pretty"];
2
3
  export type LogFormat = (typeof logFormatOptions)[number];
3
4
  export declare const logLevelOptions: readonly ["debug", "info", "warn", "error"];
@@ -8,8 +9,10 @@ export interface LogConfig {
8
9
  format: LogFormat;
9
10
  minLevel: LogLevel;
10
11
  }
12
+ export declare const createBaseLogger: (config: LogConfig) => winston.Logger;
11
13
  export type LogContext = Record<string, any>;
12
14
  export interface Logger {
15
+ runWithContext: <T>(context: LogContext, fn: () => T) => T;
13
16
  debug: (message: string, context?: LogContext) => void;
14
17
  info: (message: string, context?: LogContext) => void;
15
18
  warn: (message: string, context?: LogContext) => void;
@@ -18,6 +21,7 @@ export interface Logger {
18
21
  name: string;
19
22
  }) => Logger;
20
23
  }
24
+ export declare const wrapper: (logger: winston.Logger) => Logger;
21
25
  export declare const validateLogConfig: (config: unknown) => LogConfig;
22
26
  export declare const createLogger: (config: LogConfig) => Logger;
23
27
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/logger/index.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,gBAAgB,6BAA8B,CAAC;AAE5D,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,gBAAgB,CAAC,CAAC,MAAM,CAAC,CAAC;AAE1D,eAAO,MAAM,eAAe,6CAA8C,CAAC;AAE3E,MAAM,MAAM,QAAQ,GAAG,CAAC,OAAO,eAAe,CAAC,CAAC,MAAM,CAAC,CAAC;AAExD,MAAM,WAAW,SAAS;IACxB,QAAQ,EAAE,OAAO,CAAC;IAClB,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,SAAS,CAAC;IAClB,QAAQ,EAAE,QAAQ,CAAC;CACpB;AAwDD,MAAM,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;AAE7C,MAAM,WAAW,MAAM;IACrB,KAAK,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,UAAU,KAAK,IAAI,CAAC;IACvD,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,UAAU,KAAK,IAAI,CAAC;IACtD,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,UAAU,KAAK,IAAI,CAAC;IACtD,KAAK,EAAE,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,UAAU,KAAK,IAAI,CAAC,GACtD,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,CAAC,EAAE,UAAU,KAAK,IAAI,CAAC,CAAC;IAClE,KAAK,EAAE,CAAC,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,KAAK,MAAM,CAAC;CAC9C;AAsBD,eAAO,MAAM,iBAAiB,WAAY,OAAO,KAAG,SAuBnD,CAAC;AAEF,eAAO,MAAM,YAAY,WAAY,SAAS,WAG7C,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/logger/index.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,SAAS,CAAC;AAK9B,eAAO,MAAM,gBAAgB,6BAA8B,CAAC;AAE5D,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,gBAAgB,CAAC,CAAC,MAAM,CAAC,CAAC;AAE1D,eAAO,MAAM,eAAe,6CAA8C,CAAC;AAE3E,MAAM,MAAM,QAAQ,GAAG,CAAC,OAAO,eAAe,CAAC,CAAC,MAAM,CAAC,CAAC;AAExD,MAAM,WAAW,SAAS;IACxB,QAAQ,EAAE,OAAO,CAAC;IAClB,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,SAAS,CAAC;IAClB,QAAQ,EAAE,QAAQ,CAAC;CACpB;AAqCD,eAAO,MAAM,gBAAgB,WAAY,SAAS,mBAiBjD,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;AAE7C,MAAM,WAAW,MAAM;IACrB,cAAc,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,UAAU,EAAE,EAAE,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC;IAC3D,KAAK,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,UAAU,KAAK,IAAI,CAAC;IACvD,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,UAAU,KAAK,IAAI,CAAC;IACtD,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,UAAU,KAAK,IAAI,CAAC;IAKtD,KAAK,EAAE,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,UAAU,KAAK,IAAI,CAAC,GACtD,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,CAAC,EAAE,UAAU,KAAK,IAAI,CAAC,CAAC;IAClE,KAAK,EAAE,CAAC,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,KAAK,MAAM,CAAC;CAC9C;AAID,eAAO,MAAM,OAAO,WAAY,cAAc,KAAG,MAyChD,CAAC;AAEF,eAAO,MAAM,iBAAiB,WAAY,OAAO,KAAG,SAuBnD,CAAC;AAEF,eAAO,MAAM,YAAY,WAAY,SAAS,WAG7C,CAAC"}
@@ -3,9 +3,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.createLogger = exports.validateLogConfig = exports.logLevelOptions = exports.logFormatOptions = void 0;
6
+ exports.createLogger = exports.validateLogConfig = exports.wrapper = exports.createBaseLogger = exports.logLevelOptions = exports.logFormatOptions = void 0;
7
7
  const winston_1 = __importDefault(require("winston"));
8
8
  const winston_console_format_1 = require("winston-console-format");
9
+ const async_storage_1 = require("./async-storage");
9
10
  exports.logFormatOptions = ['json', 'pretty'];
10
11
  exports.logLevelOptions = ['debug', 'info', 'warn', 'error'];
11
12
  const createConsoleTransport = (config) => {
@@ -50,26 +51,52 @@ const createBaseLogger = (config) => {
50
51
  transports: [createConsoleTransport(config)],
51
52
  });
52
53
  };
54
+ exports.createBaseLogger = createBaseLogger;
53
55
  // Winston by default merges content of `context` among the rest of the fields for the JSON format.
54
56
  // That's causing an override of fields `name` and `message` if they are present.
55
57
  const wrapper = (logger) => {
56
58
  return {
57
- debug: (message, context) => logger.debug(message, context ? { context } : undefined),
58
- info: (message, context) => logger.info(message, context ? { context } : undefined),
59
- warn: (message, context) => logger.warn(message, context ? { context } : undefined),
59
+ debug: (message, localContext) => {
60
+ const globalContext = (0, async_storage_1.getAsyncLocalStorage)().getStore();
61
+ const fullContext = globalContext || localContext ? { ...globalContext, ...localContext } : undefined;
62
+ logger.debug(message, fullContext);
63
+ },
64
+ info: (message, localContext) => {
65
+ const globalContext = (0, async_storage_1.getAsyncLocalStorage)().getStore();
66
+ const fullContext = globalContext || localContext ? { ...globalContext, ...localContext } : undefined;
67
+ logger.info(message, fullContext);
68
+ },
69
+ warn: (message, localContext) => {
70
+ const globalContext = (0, async_storage_1.getAsyncLocalStorage)().getStore();
71
+ const fullContext = globalContext || localContext ? { ...globalContext, ...localContext } : undefined;
72
+ logger.warn(message, fullContext);
73
+ },
60
74
  // We need to handle both overloads of the `error` function
61
- error: (message, errorOrContext, context) => {
75
+ error: (message, errorOrLocalContext, localContext) => {
76
+ const globalContext = (0, async_storage_1.getAsyncLocalStorage)().getStore();
62
77
  // eslint-disable-next-line lodash/prefer-lodash-typecheck
63
- if (errorOrContext instanceof Error) {
64
- logger.error(message, errorOrContext, context ? { context } : undefined);
78
+ if (errorOrLocalContext instanceof Error) {
79
+ const fullContext = globalContext || localContext ? { ...globalContext, ...localContext } : undefined;
80
+ logger.error(message, errorOrLocalContext, fullContext);
65
81
  }
66
82
  else {
67
- logger.error(message, errorOrContext ? { context: errorOrContext } : undefined);
83
+ const fullContext = globalContext || errorOrLocalContext ? { ...globalContext, ...errorOrLocalContext } : undefined;
84
+ logger.error(message, fullContext);
68
85
  }
69
86
  },
70
- child: (options) => wrapper(logger.child(options)),
87
+ child: (options) => (0, exports.wrapper)(logger.child(options)),
88
+ runWithContext: (context, fn) => {
89
+ const asyncStorage = (0, async_storage_1.getAsyncLocalStorage)();
90
+ const oldContext = asyncStorage.getStore() ?? {};
91
+ // From https://nodejs.org/api/async_context.html#asynclocalstoragerunstore-callback-args
92
+ //
93
+ // If the callback function throws an error, the error is thrown by run() too. The stacktrace is not impacted by
94
+ // this call and the context is exited.
95
+ return asyncStorage.run({ ...oldContext, ...context }, fn);
96
+ },
71
97
  };
72
98
  };
99
+ exports.wrapper = wrapper;
73
100
  const validateLogConfig = (config) => {
74
101
  // eslint-disable-next-line lodash/prefer-lodash-typecheck
75
102
  if (typeof config !== 'object' || config === null) {
@@ -95,7 +122,7 @@ const validateLogConfig = (config) => {
95
122
  exports.validateLogConfig = validateLogConfig;
96
123
  const createLogger = (config) => {
97
124
  // Ensure that the logger configuration is valid.
98
- return wrapper(createBaseLogger((0, exports.validateLogConfig)(config)));
125
+ return (0, exports.wrapper)((0, exports.createBaseLogger)((0, exports.validateLogConfig)(config)));
99
126
  };
100
127
  exports.createLogger = createLogger;
101
128
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/logger/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/logger/index.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@api3/commons",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "keywords": [],
5
5
  "license": "MIT",
6
6
  "engines": {
@@ -0,0 +1,10 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+
3
+ import type { LogContext } from '.';
4
+
5
+ let asyncLocalStorage: AsyncLocalStorage<LogContext>;
6
+
7
+ export const getAsyncLocalStorage = () => {
8
+ if (!asyncLocalStorage) asyncLocalStorage = new AsyncLocalStorage<LogContext>();
9
+ return asyncLocalStorage;
10
+ };
@@ -0,0 +1,127 @@
1
+ import { noop } from 'lodash';
2
+
3
+ import { type LogConfig, createBaseLogger, wrapper } from '.';
4
+
5
+ const createTestLogger = (
6
+ logConfig: LogConfig = { enabled: true, minLevel: 'debug', format: 'json', colorize: false }
7
+ ) => {
8
+ const baseLogger = createBaseLogger(logConfig);
9
+ const logger = wrapper(baseLogger);
10
+ jest.spyOn(baseLogger, 'debug').mockImplementation(noop as any);
11
+ jest.spyOn(baseLogger, 'info').mockImplementation(noop as any);
12
+ jest.spyOn(baseLogger, 'warn').mockImplementation(noop as any);
13
+ jest.spyOn(baseLogger, 'error').mockImplementation(noop as any);
14
+ jest.spyOn(baseLogger, 'child').mockImplementation(noop as any);
15
+
16
+ return { baseLogger, logger };
17
+ };
18
+
19
+ describe('log context', () => {
20
+ it('works with sync functions', () => {
21
+ const { baseLogger, logger } = createTestLogger();
22
+
23
+ logger.runWithContext({ requestId: 'parent' }, () => {
24
+ logger.debug('parent start');
25
+ logger.runWithContext({ requestId: 'child' }, () => {
26
+ logger.debug('child');
27
+ });
28
+
29
+ logger.debug('parent end');
30
+ });
31
+
32
+ expect(baseLogger.debug).toHaveBeenCalledWith('parent start', { requestId: 'parent' });
33
+ expect(baseLogger.debug).toHaveBeenCalledWith('child', { requestId: 'child' });
34
+ expect(baseLogger.debug).toHaveBeenCalledWith('parent end', { requestId: 'parent' });
35
+ });
36
+
37
+ it('works with async functions', async () => {
38
+ const { baseLogger, logger } = createTestLogger();
39
+
40
+ await logger.runWithContext({ requestId: 'parent' }, async () => {
41
+ logger.debug('parent start');
42
+ await logger.runWithContext({ requestId: 'child' }, async () => {
43
+ await new Promise((resolve) => setTimeout(resolve, 50));
44
+ logger.debug('child');
45
+ });
46
+
47
+ logger.debug('parent end');
48
+ });
49
+
50
+ expect(baseLogger.debug).toHaveBeenCalledTimes(3);
51
+ expect(baseLogger.debug).toHaveBeenCalledWith('parent start', { requestId: 'parent' });
52
+ expect(baseLogger.debug).toHaveBeenCalledWith('child', { requestId: 'child' });
53
+ expect(baseLogger.debug).toHaveBeenCalledWith('parent end', { requestId: 'parent' });
54
+ });
55
+
56
+ it('works with deeply nested functions', async () => {
57
+ const { baseLogger, logger } = createTestLogger();
58
+
59
+ await logger.runWithContext({ parent: true }, async () => {
60
+ logger.debug('parent start');
61
+
62
+ await logger.runWithContext({ A: true }, async () => {
63
+ logger.debug('A start');
64
+
65
+ await logger.runWithContext({ B: true }, async () => {
66
+ setTimeout(() => logger.debug('C'), 25);
67
+ setTimeout(() => logger.debug('D'), 50);
68
+ setTimeout(() => logger.debug('E'), 75);
69
+
70
+ await new Promise((resolve) => setTimeout(resolve, 100));
71
+ logger.debug('B end');
72
+ });
73
+
74
+ logger.debug('A end');
75
+ });
76
+
77
+ logger.debug('parent end');
78
+ });
79
+
80
+ expect(baseLogger.debug).toHaveBeenCalledTimes(8);
81
+ expect(baseLogger.debug).toHaveBeenCalledWith('parent start', { parent: true });
82
+ expect(baseLogger.debug).toHaveBeenCalledWith('A start', { parent: true, A: true });
83
+ expect(baseLogger.debug).toHaveBeenCalledWith('C', { parent: true, A: true, B: true });
84
+ expect(baseLogger.debug).toHaveBeenCalledWith('D', { parent: true, A: true, B: true });
85
+ expect(baseLogger.debug).toHaveBeenCalledWith('E', { parent: true, A: true, B: true });
86
+ expect(baseLogger.debug).toHaveBeenCalledWith('B end', { parent: true, A: true, B: true });
87
+ expect(baseLogger.debug).toHaveBeenCalledWith('A end', { parent: true, A: true });
88
+ expect(baseLogger.debug).toHaveBeenCalledWith('parent end', { parent: true });
89
+ });
90
+
91
+ it('throws if the sync callback function throws', () => {
92
+ const { logger } = createTestLogger();
93
+
94
+ expect(() =>
95
+ logger.runWithContext({}, () => {
96
+ throw new Error('some-error');
97
+ })
98
+ ).toThrow('some-error');
99
+ });
100
+
101
+ it('returns rejected promise if the async callback function rejects', async () => {
102
+ const { logger } = createTestLogger();
103
+
104
+ await expect(async () =>
105
+ // eslint-disable-next-line @typescript-eslint/require-await
106
+ logger.runWithContext({}, async () => {
107
+ throw new Error('some-error');
108
+ })
109
+ ).rejects.toThrow('some-error');
110
+ });
111
+
112
+ it('can log using all variants of logger.error', () => {
113
+ const { baseLogger, logger } = createTestLogger();
114
+
115
+ logger.error('only message');
116
+ logger.error('message and context', { requestId: 'parent' });
117
+ logger.error('message and error', new Error('some-error'));
118
+ logger.error('message, error and context', new Error('some-error'), { requestId: 'parent' });
119
+
120
+ expect(baseLogger.error).toHaveBeenNthCalledWith(1, 'only message', undefined);
121
+ expect(baseLogger.error).toHaveBeenNthCalledWith(2, 'message and context', { requestId: 'parent' });
122
+ expect(baseLogger.error).toHaveBeenNthCalledWith(3, 'message and error', new Error('some-error'), undefined);
123
+ expect(baseLogger.error).toHaveBeenNthCalledWith(4, 'message, error and context', new Error('some-error'), {
124
+ requestId: 'parent',
125
+ });
126
+ });
127
+ });
@@ -1,6 +1,8 @@
1
1
  import winston from 'winston';
2
2
  import { consoleFormat } from 'winston-console-format';
3
3
 
4
+ import { getAsyncLocalStorage } from './async-storage';
5
+
4
6
  export const logFormatOptions = ['json', 'pretty'] as const;
5
7
 
6
8
  export type LogFormat = (typeof logFormatOptions)[number];
@@ -51,7 +53,7 @@ const createConsoleTransport = (config: LogConfig) => {
51
53
  }
52
54
  };
53
55
 
54
- const createBaseLogger = (config: LogConfig) => {
56
+ export const createBaseLogger = (config: LogConfig) => {
55
57
  const { enabled, minLevel } = config;
56
58
 
57
59
  return winston.createLogger({
@@ -73,9 +75,14 @@ const createBaseLogger = (config: LogConfig) => {
73
75
  export type LogContext = Record<string, any>;
74
76
 
75
77
  export interface Logger {
78
+ runWithContext: <T>(context: LogContext, fn: () => T) => T;
76
79
  debug: (message: string, context?: LogContext) => void;
77
80
  info: (message: string, context?: LogContext) => void;
78
81
  warn: (message: string, context?: LogContext) => void;
82
+ // We need to handle both overloads of the `error` function. It may a bit surprising that the variants are joined with
83
+ // "&" instead of "|", but it forces TypeScript to be more deliberate when resolving overloads. For functions, this
84
+ // means the implementation must handle all overloads, and TypeScript will look for the correct overload based on the
85
+ // arguments provided.
79
86
  error: ((message: string, context?: LogContext) => void) &
80
87
  ((message: string, error: Error, context?: LogContext) => void);
81
88
  child: (options: { name: string }) => Logger;
@@ -83,21 +90,46 @@ export interface Logger {
83
90
 
84
91
  // Winston by default merges content of `context` among the rest of the fields for the JSON format.
85
92
  // That's causing an override of fields `name` and `message` if they are present.
86
- const wrapper = (logger: Logger): Logger => {
93
+ export const wrapper = (logger: winston.Logger): Logger => {
87
94
  return {
88
- debug: (message, context) => logger.debug(message, context ? { context } : undefined),
89
- info: (message, context) => logger.info(message, context ? { context } : undefined),
90
- warn: (message, context) => logger.warn(message, context ? { context } : undefined),
95
+ debug: (message, localContext) => {
96
+ const globalContext = getAsyncLocalStorage().getStore();
97
+ const fullContext = globalContext || localContext ? { ...globalContext, ...localContext } : undefined;
98
+ logger.debug(message, fullContext);
99
+ },
100
+ info: (message, localContext) => {
101
+ const globalContext = getAsyncLocalStorage().getStore();
102
+ const fullContext = globalContext || localContext ? { ...globalContext, ...localContext } : undefined;
103
+ logger.info(message, fullContext);
104
+ },
105
+ warn: (message, localContext) => {
106
+ const globalContext = getAsyncLocalStorage().getStore();
107
+ const fullContext = globalContext || localContext ? { ...globalContext, ...localContext } : undefined;
108
+ logger.warn(message, fullContext);
109
+ },
91
110
  // We need to handle both overloads of the `error` function
92
- error: (message, errorOrContext, context) => {
111
+ error: (message, errorOrLocalContext: Error | LogContext, localContext?: LogContext) => {
112
+ const globalContext = getAsyncLocalStorage().getStore();
93
113
  // eslint-disable-next-line lodash/prefer-lodash-typecheck
94
- if (errorOrContext instanceof Error) {
95
- logger.error(message, errorOrContext, context ? { context } : undefined);
114
+ if (errorOrLocalContext instanceof Error) {
115
+ const fullContext = globalContext || localContext ? { ...globalContext, ...localContext } : undefined;
116
+ logger.error(message, errorOrLocalContext, fullContext);
96
117
  } else {
97
- logger.error(message, errorOrContext ? { context: errorOrContext } : undefined);
118
+ const fullContext =
119
+ globalContext || errorOrLocalContext ? { ...globalContext, ...errorOrLocalContext } : undefined;
120
+ logger.error(message, fullContext);
98
121
  }
99
122
  },
100
123
  child: (options) => wrapper(logger.child(options)),
124
+ runWithContext: (context, fn) => {
125
+ const asyncStorage = getAsyncLocalStorage();
126
+ const oldContext = asyncStorage.getStore() ?? {};
127
+ // From https://nodejs.org/api/async_context.html#asynclocalstoragerunstore-callback-args
128
+ //
129
+ // If the callback function throws an error, the error is thrown by run() too. The stacktrace is not impacted by
130
+ // this call and the context is exited.
131
+ return asyncStorage.run({ ...oldContext, ...context }, fn);
132
+ },
101
133
  } as Logger;
102
134
  };
103
135