core-services-sdk 1.3.56 → 1.3.57
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/package.json +1 -1
- package/src/http/helpers/create-request-logger.js +38 -0
- package/src/http/index.js +1 -0
- package/src/util/context.js +64 -32
- package/tests/http/helpers/create-request-logger.test.js +113 -0
- package/types/http/helpers/create-request-logger.d.ts +4 -0
- package/types/http/index.d.ts +1 -0
- package/types/util/context.d.ts +93 -24
package/package.json
CHANGED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Context } from '../../util/context.js'
|
|
2
|
+
/**
|
|
3
|
+
* Creates a request-scoped logger enriched with contextual metadata.
|
|
4
|
+
*
|
|
5
|
+
* The logger is derived from the provided Pino logger and augmented with:
|
|
6
|
+
* - correlationId (from Context)
|
|
7
|
+
* - client IP (from Context)
|
|
8
|
+
* - user agent (from Context)
|
|
9
|
+
* - operation identifier in the form: "<METHOD> <URL>"
|
|
10
|
+
*
|
|
11
|
+
* Intended to be used per incoming Fastify request, typically at the beginning
|
|
12
|
+
* of a request lifecycle, so all subsequent logs automatically include
|
|
13
|
+
* request-specific context.
|
|
14
|
+
*
|
|
15
|
+
* @param {import('fastify').FastifyRequest} request
|
|
16
|
+
* The Fastify request object.
|
|
17
|
+
*
|
|
18
|
+
* @param {import('pino').Logger} log
|
|
19
|
+
* Base Pino logger instance.
|
|
20
|
+
*
|
|
21
|
+
* @returns {import('pino').Logger}
|
|
22
|
+
* A child Pino logger enriched with request and context metadata.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* const requestLog = createRequestLogger(request, log, Context)
|
|
26
|
+
* requestLog.info('Handling request')
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
export const createRequestLogger = (request, log) => {
|
|
30
|
+
const { correlationId, ip, userAgent } = Context?.all() || {}
|
|
31
|
+
|
|
32
|
+
return log.child({
|
|
33
|
+
ip,
|
|
34
|
+
userAgent,
|
|
35
|
+
correlationId,
|
|
36
|
+
op: `${request.method} ${request?.url || request?.routeOptions?.url || request.raw.url}`,
|
|
37
|
+
})
|
|
38
|
+
}
|
package/src/http/index.js
CHANGED
package/src/util/context.js
CHANGED
|
@@ -3,79 +3,111 @@ import { AsyncLocalStorage } from 'node:async_hooks'
|
|
|
3
3
|
const als = new AsyncLocalStorage()
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
* Represents the data stored in the async context for a single execution flow.
|
|
7
|
+
*
|
|
8
|
+
* This object is propagated automatically across async boundaries
|
|
9
|
+
* using Node.js AsyncLocalStorage.
|
|
10
|
+
*
|
|
11
|
+
* It defines the contract between producers (who set values)
|
|
12
|
+
* and consumers (who read values) of the context.
|
|
13
|
+
*
|
|
14
|
+
* @typedef {Object} ContextStore
|
|
15
|
+
*
|
|
16
|
+
* @property {string} [correlationId] - Unique identifier for request or operation tracing.
|
|
17
|
+
* @property {string} [ip] - Client IP address.
|
|
18
|
+
* @property {string} [userAgent] - Client user agent string.
|
|
19
|
+
* @property {string} [tenantId] - Active tenant identifier.
|
|
20
|
+
* @property {string} [userId] - Authenticated user identifier.
|
|
10
21
|
*/
|
|
11
22
|
|
|
12
|
-
|
|
23
|
+
/**
|
|
24
|
+
* Async execution context manager built on top of Node.js AsyncLocalStorage.
|
|
25
|
+
*
|
|
26
|
+
* This class provides a thin, static API for storing and accessing
|
|
27
|
+
* request-scoped (or async-chain-scoped) metadata such as correlation IDs,
|
|
28
|
+
* user information, tenant identifiers, and similar data.
|
|
29
|
+
*
|
|
30
|
+
* The context is bound to the current async execution chain using
|
|
31
|
+
* AsyncLocalStorage and is automatically propagated across `await` boundaries.
|
|
32
|
+
*
|
|
33
|
+
* This class is intentionally static and acts as a singleton wrapper
|
|
34
|
+
* around AsyncLocalStorage.
|
|
35
|
+
*/
|
|
36
|
+
export class Context {
|
|
13
37
|
/**
|
|
14
|
-
* Run a callback within a given context store.
|
|
15
|
-
*
|
|
16
|
-
*
|
|
38
|
+
* Run a callback within a given async context store.
|
|
39
|
+
*
|
|
40
|
+
* All asynchronous operations spawned inside the callback
|
|
41
|
+
* will have access to the provided store via {@link Context.get},
|
|
42
|
+
* {@link Context.set}, or {@link Context.all}.
|
|
17
43
|
*
|
|
18
44
|
* @template T
|
|
19
|
-
* @param {
|
|
45
|
+
* @param {ContextStore} store - Initial context store for this execution.
|
|
20
46
|
* @param {() => T} callback - Function to execute inside the context.
|
|
21
47
|
* @returns {T} The return value of the callback (sync or async).
|
|
22
48
|
*
|
|
23
49
|
* @example
|
|
24
50
|
* Context.run(
|
|
25
|
-
* { correlationId: 'abc123' },
|
|
51
|
+
* { correlationId: 'abc123', userId: 'usr_1' },
|
|
26
52
|
* async () => {
|
|
27
53
|
* console.log(Context.get('correlationId')) // "abc123"
|
|
28
54
|
* }
|
|
29
55
|
* )
|
|
30
56
|
*/
|
|
31
|
-
run(store, callback) {
|
|
57
|
+
static run(store, callback) {
|
|
32
58
|
return als.run(store, callback)
|
|
33
|
-
}
|
|
59
|
+
}
|
|
34
60
|
|
|
35
61
|
/**
|
|
36
62
|
* Retrieve a single value from the current async context store.
|
|
37
63
|
*
|
|
38
|
-
* @
|
|
39
|
-
*
|
|
40
|
-
*
|
|
64
|
+
* If called outside of an active {@link Context.run},
|
|
65
|
+
* this method returns `undefined`.
|
|
66
|
+
*
|
|
67
|
+
* @template {keyof ContextStore} K
|
|
68
|
+
* @param {K} key - Context property name.
|
|
69
|
+
* @returns {ContextStore[K] | undefined} The stored value, if present.
|
|
41
70
|
*
|
|
42
71
|
* @example
|
|
43
|
-
* const
|
|
72
|
+
* const tenantId = Context.get('tenantId')
|
|
44
73
|
*/
|
|
45
|
-
get(key) {
|
|
74
|
+
static get(key) {
|
|
46
75
|
const store = als.getStore()
|
|
47
76
|
return store?.[key]
|
|
48
|
-
}
|
|
77
|
+
}
|
|
49
78
|
|
|
50
79
|
/**
|
|
51
|
-
* Set a single key-value pair
|
|
52
|
-
*
|
|
53
|
-
*
|
|
80
|
+
* Set a single key-value pair on the current async context store.
|
|
81
|
+
*
|
|
82
|
+
* If called outside of an active {@link Context.run},
|
|
83
|
+
* this method is a no-op.
|
|
54
84
|
*
|
|
55
|
-
* @
|
|
56
|
-
* @param {
|
|
85
|
+
* @template {keyof ContextStore} K
|
|
86
|
+
* @param {K} key - Context property name.
|
|
87
|
+
* @param {ContextStore[K]} value - Value to store.
|
|
57
88
|
*
|
|
58
89
|
* @example
|
|
59
90
|
* Context.set('tenantId', 'tnt_1234')
|
|
60
91
|
*/
|
|
61
|
-
set(key, value) {
|
|
92
|
+
static set(key, value) {
|
|
62
93
|
const store = als.getStore()
|
|
63
94
|
if (store) {
|
|
64
95
|
store[key] = value
|
|
65
96
|
}
|
|
66
|
-
}
|
|
97
|
+
}
|
|
67
98
|
|
|
68
99
|
/**
|
|
69
|
-
* Get the
|
|
100
|
+
* Get the full context store for the current async execution.
|
|
101
|
+
*
|
|
102
|
+
* If no context is active, an empty object is returned.
|
|
70
103
|
*
|
|
71
|
-
* @returns {
|
|
72
|
-
* or an empty object if no store exists.
|
|
104
|
+
* @returns {ContextStore}
|
|
73
105
|
*
|
|
74
106
|
* @example
|
|
75
|
-
* const
|
|
76
|
-
* console.log(
|
|
107
|
+
* const ctx = Context.all()
|
|
108
|
+
* console.log(ctx.correlationId)
|
|
77
109
|
*/
|
|
78
|
-
all() {
|
|
110
|
+
static all() {
|
|
79
111
|
return als.getStore() || {}
|
|
80
|
-
}
|
|
112
|
+
}
|
|
81
113
|
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import { createRequestLogger } from '../../../src/http/helpers/create-request-logger.js'
|
|
5
|
+
import { Context } from '../../../src/util/context.js'
|
|
6
|
+
//@ts-ignore
|
|
7
|
+
describe('createRequestLogger', () => {
|
|
8
|
+
let baseLogger
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
baseLogger = {
|
|
12
|
+
child: vi.fn(),
|
|
13
|
+
}
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('creates a child logger with context and request data', () => {
|
|
17
|
+
const request = {
|
|
18
|
+
method: 'GET',
|
|
19
|
+
url: '/test',
|
|
20
|
+
raw: { url: '/raw-test' },
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const childLogger = {}
|
|
24
|
+
baseLogger.child.mockReturnValue(childLogger)
|
|
25
|
+
|
|
26
|
+
Context.run(
|
|
27
|
+
{
|
|
28
|
+
correlationId: 'corr-123',
|
|
29
|
+
ip: '127.0.0.1',
|
|
30
|
+
userAgent: 'vitest-agent',
|
|
31
|
+
},
|
|
32
|
+
() => {
|
|
33
|
+
const result = createRequestLogger(request, baseLogger)
|
|
34
|
+
|
|
35
|
+
expect(baseLogger.child).toHaveBeenCalledOnce()
|
|
36
|
+
expect(baseLogger.child).toHaveBeenCalledWith({
|
|
37
|
+
ip: '127.0.0.1',
|
|
38
|
+
userAgent: 'vitest-agent',
|
|
39
|
+
correlationId: 'corr-123',
|
|
40
|
+
op: 'GET /test',
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
expect(result).toBe(childLogger)
|
|
44
|
+
},
|
|
45
|
+
)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('falls back to routeOptions.url when request.url is missing', () => {
|
|
49
|
+
const request = {
|
|
50
|
+
method: 'POST',
|
|
51
|
+
routeOptions: { url: '/route-url' },
|
|
52
|
+
raw: { url: '/raw-url' },
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
Context.run(
|
|
56
|
+
{
|
|
57
|
+
correlationId: 'corr-456',
|
|
58
|
+
ip: '10.0.0.1',
|
|
59
|
+
userAgent: 'agent-x',
|
|
60
|
+
},
|
|
61
|
+
() => {
|
|
62
|
+
createRequestLogger(request, baseLogger)
|
|
63
|
+
|
|
64
|
+
expect(baseLogger.child).toHaveBeenCalledWith(
|
|
65
|
+
expect.objectContaining({
|
|
66
|
+
op: 'POST /route-url',
|
|
67
|
+
}),
|
|
68
|
+
)
|
|
69
|
+
},
|
|
70
|
+
)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('falls back to request.raw.url when neither url nor routeOptions.url exist', () => {
|
|
74
|
+
const request = {
|
|
75
|
+
method: 'PUT',
|
|
76
|
+
raw: { url: '/raw-only' },
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
Context.run(
|
|
80
|
+
{
|
|
81
|
+
correlationId: 'corr-789',
|
|
82
|
+
ip: '192.168.1.1',
|
|
83
|
+
userAgent: 'agent-y',
|
|
84
|
+
},
|
|
85
|
+
() => {
|
|
86
|
+
createRequestLogger(request, baseLogger)
|
|
87
|
+
|
|
88
|
+
expect(baseLogger.child).toHaveBeenCalledWith(
|
|
89
|
+
expect.objectContaining({
|
|
90
|
+
op: 'PUT /raw-only',
|
|
91
|
+
}),
|
|
92
|
+
)
|
|
93
|
+
},
|
|
94
|
+
)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('handles missing context gracefully when no Context is active', () => {
|
|
98
|
+
const request = {
|
|
99
|
+
method: 'DELETE',
|
|
100
|
+
url: '/delete',
|
|
101
|
+
raw: { url: '/delete' },
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
createRequestLogger(request, baseLogger)
|
|
105
|
+
|
|
106
|
+
expect(baseLogger.child).toHaveBeenCalledWith({
|
|
107
|
+
ip: undefined,
|
|
108
|
+
userAgent: undefined,
|
|
109
|
+
correlationId: undefined,
|
|
110
|
+
op: 'DELETE /delete',
|
|
111
|
+
})
|
|
112
|
+
})
|
|
113
|
+
})
|
package/types/http/index.d.ts
CHANGED
package/types/util/context.d.ts
CHANGED
|
@@ -1,55 +1,124 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Represents the data stored in the async context for a single execution flow.
|
|
3
|
+
*
|
|
4
|
+
* This object is propagated automatically across async boundaries
|
|
5
|
+
* using Node.js AsyncLocalStorage.
|
|
6
|
+
*
|
|
7
|
+
* It defines the contract between producers (who set values)
|
|
8
|
+
* and consumers (who read values) of the context.
|
|
9
|
+
*
|
|
10
|
+
* @typedef {Object} ContextStore
|
|
11
|
+
*
|
|
12
|
+
* @property {string} [correlationId] - Unique identifier for request or operation tracing.
|
|
13
|
+
* @property {string} [ip] - Client IP address.
|
|
14
|
+
* @property {string} [userAgent] - Client user agent string.
|
|
15
|
+
* @property {string} [tenantId] - Active tenant identifier.
|
|
16
|
+
* @property {string} [userId] - Authenticated user identifier.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Async execution context manager built on top of Node.js AsyncLocalStorage.
|
|
20
|
+
*
|
|
21
|
+
* This class provides a thin, static API for storing and accessing
|
|
22
|
+
* request-scoped (or async-chain-scoped) metadata such as correlation IDs,
|
|
23
|
+
* user information, tenant identifiers, and similar data.
|
|
24
|
+
*
|
|
25
|
+
* The context is bound to the current async execution chain using
|
|
26
|
+
* AsyncLocalStorage and is automatically propagated across `await` boundaries.
|
|
27
|
+
*
|
|
28
|
+
* This class is intentionally static and acts as a singleton wrapper
|
|
29
|
+
* around AsyncLocalStorage.
|
|
30
|
+
*/
|
|
31
|
+
export class Context {
|
|
2
32
|
/**
|
|
3
|
-
* Run a callback within a given context store.
|
|
4
|
-
*
|
|
5
|
-
*
|
|
33
|
+
* Run a callback within a given async context store.
|
|
34
|
+
*
|
|
35
|
+
* All asynchronous operations spawned inside the callback
|
|
36
|
+
* will have access to the provided store via {@link Context.get},
|
|
37
|
+
* {@link Context.set}, or {@link Context.all}.
|
|
6
38
|
*
|
|
7
39
|
* @template T
|
|
8
|
-
* @param {
|
|
40
|
+
* @param {ContextStore} store - Initial context store for this execution.
|
|
9
41
|
* @param {() => T} callback - Function to execute inside the context.
|
|
10
42
|
* @returns {T} The return value of the callback (sync or async).
|
|
11
43
|
*
|
|
12
44
|
* @example
|
|
13
45
|
* Context.run(
|
|
14
|
-
* { correlationId: 'abc123' },
|
|
46
|
+
* { correlationId: 'abc123', userId: 'usr_1' },
|
|
15
47
|
* async () => {
|
|
16
48
|
* console.log(Context.get('correlationId')) // "abc123"
|
|
17
49
|
* }
|
|
18
50
|
* )
|
|
19
51
|
*/
|
|
20
|
-
|
|
52
|
+
static run<T>(store: ContextStore, callback: () => T): T
|
|
21
53
|
/**
|
|
22
54
|
* Retrieve a single value from the current async context store.
|
|
23
55
|
*
|
|
24
|
-
* @
|
|
25
|
-
*
|
|
26
|
-
*
|
|
56
|
+
* If called outside of an active {@link Context.run},
|
|
57
|
+
* this method returns `undefined`.
|
|
58
|
+
*
|
|
59
|
+
* @template {keyof ContextStore} K
|
|
60
|
+
* @param {K} key - Context property name.
|
|
61
|
+
* @returns {ContextStore[K] | undefined} The stored value, if present.
|
|
27
62
|
*
|
|
28
63
|
* @example
|
|
29
|
-
* const
|
|
64
|
+
* const tenantId = Context.get('tenantId')
|
|
30
65
|
*/
|
|
31
|
-
|
|
66
|
+
static get<K extends keyof ContextStore>(key: K): ContextStore[K] | undefined
|
|
32
67
|
/**
|
|
33
|
-
* Set a single key-value pair
|
|
34
|
-
*
|
|
35
|
-
*
|
|
68
|
+
* Set a single key-value pair on the current async context store.
|
|
69
|
+
*
|
|
70
|
+
* If called outside of an active {@link Context.run},
|
|
71
|
+
* this method is a no-op.
|
|
36
72
|
*
|
|
37
|
-
* @
|
|
38
|
-
* @param {
|
|
73
|
+
* @template {keyof ContextStore} K
|
|
74
|
+
* @param {K} key - Context property name.
|
|
75
|
+
* @param {ContextStore[K]} value - Value to store.
|
|
39
76
|
*
|
|
40
77
|
* @example
|
|
41
78
|
* Context.set('tenantId', 'tnt_1234')
|
|
42
79
|
*/
|
|
43
|
-
|
|
80
|
+
static set<K extends keyof ContextStore>(key: K, value: ContextStore[K]): void
|
|
44
81
|
/**
|
|
45
|
-
* Get the
|
|
82
|
+
* Get the full context store for the current async execution.
|
|
46
83
|
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
84
|
+
* If no context is active, an empty object is returned.
|
|
85
|
+
*
|
|
86
|
+
* @returns {ContextStore}
|
|
49
87
|
*
|
|
50
88
|
* @example
|
|
51
|
-
* const
|
|
52
|
-
* console.log(
|
|
89
|
+
* const ctx = Context.all()
|
|
90
|
+
* console.log(ctx.correlationId)
|
|
91
|
+
*/
|
|
92
|
+
static all(): ContextStore
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Represents the data stored in the async context for a single execution flow.
|
|
96
|
+
*
|
|
97
|
+
* This object is propagated automatically across async boundaries
|
|
98
|
+
* using Node.js AsyncLocalStorage.
|
|
99
|
+
*
|
|
100
|
+
* It defines the contract between producers (who set values)
|
|
101
|
+
* and consumers (who read values) of the context.
|
|
102
|
+
*/
|
|
103
|
+
export type ContextStore = {
|
|
104
|
+
/**
|
|
105
|
+
* - Unique identifier for request or operation tracing.
|
|
106
|
+
*/
|
|
107
|
+
correlationId?: string
|
|
108
|
+
/**
|
|
109
|
+
* - Client IP address.
|
|
110
|
+
*/
|
|
111
|
+
ip?: string
|
|
112
|
+
/**
|
|
113
|
+
* - Client user agent string.
|
|
114
|
+
*/
|
|
115
|
+
userAgent?: string
|
|
116
|
+
/**
|
|
117
|
+
* - Active tenant identifier.
|
|
118
|
+
*/
|
|
119
|
+
tenantId?: string
|
|
120
|
+
/**
|
|
121
|
+
* - Authenticated user identifier.
|
|
53
122
|
*/
|
|
54
|
-
|
|
123
|
+
userId?: string
|
|
55
124
|
}
|