@sprqvntrs/logger 1.0.0
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/LICENSE +21 -0
- package/README.md +311 -0
- package/index.ts +53 -0
- package/package.json +54 -0
- package/src/context/async-context.ts +126 -0
- package/src/core/buffered-logger.ts +210 -0
- package/src/core/destinations.ts +63 -0
- package/src/core/logger.ts +208 -0
- package/src/core/pino-config.ts +92 -0
- package/src/core/serializers.ts +81 -0
- package/src/middleware/http.ts +225 -0
- package/src/presets/server.ts +206 -0
- package/src/testing/mock-logger.ts +234 -0
- package/src/types.ts +198 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025-2026 SPRQVNTRS
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
# @sprqvntrs/logger
|
|
2
|
+
|
|
3
|
+
TypeScript logging package built on [Pino](https://github.com/pinojs/pino) with structured logging, request tracing, and testing utilities.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @sprqvntrs/logger
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { createLogger } from '@sprqvntrs/logger';
|
|
15
|
+
|
|
16
|
+
const logger = createLogger({
|
|
17
|
+
serviceName: 'my-service',
|
|
18
|
+
level: 'debug', // optional, defaults to LOG_LEVEL env or 'info'
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
logger.info('Server started', { port: 3000 });
|
|
22
|
+
logger.error('Operation failed', { error: new Error('Something went wrong') });
|
|
23
|
+
|
|
24
|
+
// Child loggers for scoped context
|
|
25
|
+
const requestLogger = logger.child({ requestId: '123', userId: 'user-456' });
|
|
26
|
+
requestLogger.info('Processing request'); // includes requestId and userId
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Features
|
|
30
|
+
|
|
31
|
+
- Factory function (`createLogger`) and static class (`StaticLogger`) APIs
|
|
32
|
+
- Type-safe structured logging with `LogContext` interface
|
|
33
|
+
- Child logger support for scoped logging
|
|
34
|
+
- AsyncLocalStorage-based request ID propagation
|
|
35
|
+
- HTTP middleware with path/extension exclusions
|
|
36
|
+
- Server lifecycle presets (start, shutdown, errors)
|
|
37
|
+
- Worker/job logging presets
|
|
38
|
+
- Mock logger and spy utilities for testing
|
|
39
|
+
- Automatic redaction of sensitive fields (passwords, tokens, etc.)
|
|
40
|
+
- Opt-in pretty printing via `pino-pretty` (consumer-installed)
|
|
41
|
+
|
|
42
|
+
## Entry Points
|
|
43
|
+
|
|
44
|
+
| Import | Description |
|
|
45
|
+
|--------|-------------|
|
|
46
|
+
| `@sprqvntrs/logger` | Main exports (createLogger, StaticLogger, types, context utils) |
|
|
47
|
+
| `@sprqvntrs/logger/http` | HTTP middleware (createHttpLogger) |
|
|
48
|
+
| `@sprqvntrs/logger/server` | Server/worker lifecycle logging |
|
|
49
|
+
| `@sprqvntrs/logger/testing` | Mock logger for tests |
|
|
50
|
+
|
|
51
|
+
## API Reference
|
|
52
|
+
|
|
53
|
+
### `createLogger(options)`
|
|
54
|
+
|
|
55
|
+
Creates a new logger instance.
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
interface CreateLoggerOptions {
|
|
59
|
+
serviceName: string; // Required: included in all logs
|
|
60
|
+
level?: LogLevel; // 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal'
|
|
61
|
+
version?: string; // Service version (defaults to npm_package_version)
|
|
62
|
+
pretty?: boolean; // Pretty print (default: false, requires pino-pretty installed)
|
|
63
|
+
redactPaths?: string[]; // Additional paths to redact
|
|
64
|
+
base?: Record<string, unknown>; // Additional base fields
|
|
65
|
+
timestamp?: boolean; // Include timestamp (default: true)
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Logger Methods
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
interface Logger {
|
|
73
|
+
trace(message: string, context?: LogContext): void;
|
|
74
|
+
debug(message: string, context?: LogContext): void;
|
|
75
|
+
info(message: string, context?: LogContext): void;
|
|
76
|
+
warn(message: string, context?: LogContext): void;
|
|
77
|
+
error(message: string, context?: LogContext): void;
|
|
78
|
+
fatal(message: string, context?: LogContext): void;
|
|
79
|
+
|
|
80
|
+
child(bindings: Record<string, unknown>): Logger;
|
|
81
|
+
readonly pino: PinoLogger; // Access underlying Pino instance
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Static Logger (Backward Compatible)
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
import Logger from '@sprqvntrs/logger';
|
|
89
|
+
|
|
90
|
+
// Configure once at startup
|
|
91
|
+
Logger.configure({ serviceName: 'my-service' });
|
|
92
|
+
|
|
93
|
+
// Use anywhere
|
|
94
|
+
Logger.info('message', { key: 'value' });
|
|
95
|
+
Logger.error('failed', { error: new Error('oops') });
|
|
96
|
+
|
|
97
|
+
// Create child loggers
|
|
98
|
+
const child = Logger.child({ component: 'auth' });
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## HTTP Middleware
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
import { createHttpLogger } from '@sprqvntrs/logger/http';
|
|
105
|
+
import { createLogger } from '@sprqvntrs/logger';
|
|
106
|
+
|
|
107
|
+
const logger = createLogger({ serviceName: 'api' });
|
|
108
|
+
|
|
109
|
+
const httpLogger = createHttpLogger({
|
|
110
|
+
logger, // Use existing logger
|
|
111
|
+
excludePaths: ['/health', '/metrics'], // Don't log these paths
|
|
112
|
+
excludeExtensions: ['.js', '.css', '.png'], // Don't log static files
|
|
113
|
+
customProps: (req, res) => ({ // Add custom fields
|
|
114
|
+
userAgent: req.headers['user-agent'],
|
|
115
|
+
}),
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// Express
|
|
119
|
+
app.use(httpLogger);
|
|
120
|
+
|
|
121
|
+
// Fastify
|
|
122
|
+
fastify.addHook('onRequest', (req, reply, done) => {
|
|
123
|
+
httpLogger(req.raw, reply.raw, done);
|
|
124
|
+
});
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Request Context Propagation
|
|
128
|
+
|
|
129
|
+
Automatically include request IDs in all logs using AsyncLocalStorage:
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
import {
|
|
133
|
+
withRequestContext,
|
|
134
|
+
generateRequestId,
|
|
135
|
+
createRequestContextMiddleware,
|
|
136
|
+
} from '@sprqvntrs/logger';
|
|
137
|
+
|
|
138
|
+
// Express middleware
|
|
139
|
+
app.use(createRequestContextMiddleware());
|
|
140
|
+
|
|
141
|
+
// Or manual context
|
|
142
|
+
app.use((req, res, next) => {
|
|
143
|
+
const requestId = req.headers['x-request-id'] || generateRequestId();
|
|
144
|
+
withRequestContext({ requestId }, () => next());
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// All subsequent logs automatically include requestId
|
|
148
|
+
logger.info('Processing'); // { requestId: '...', msg: 'Processing' }
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Server Lifecycle Logging
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
import { createServerLogger } from '@sprqvntrs/logger/server';
|
|
155
|
+
|
|
156
|
+
const { logger, logServerStart, logShutdown, logServerClosed } =
|
|
157
|
+
createServerLogger({
|
|
158
|
+
serviceName: 'api',
|
|
159
|
+
registerGlobalHandlers: true, // Handle uncaughtException/unhandledRejection
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const server = app.listen(3000, () => {
|
|
163
|
+
logServerStart(3000, { url: 'http://localhost:3000' });
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
process.on('SIGTERM', () => {
|
|
167
|
+
logShutdown('SIGTERM');
|
|
168
|
+
server.close(() => logServerClosed());
|
|
169
|
+
});
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Worker/Job Logging
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
import { createWorkerLogger } from '@sprqvntrs/logger/server';
|
|
176
|
+
|
|
177
|
+
const { logger, logJobStart, logJobComplete, logJobFailed } =
|
|
178
|
+
createWorkerLogger({ serviceName: 'background-worker' });
|
|
179
|
+
|
|
180
|
+
async function processJob(job: Job) {
|
|
181
|
+
const startTime = Date.now();
|
|
182
|
+
logJobStart(job.id, job.type);
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
await executeJob(job);
|
|
186
|
+
logJobComplete(job.id, job.type, Date.now() - startTime);
|
|
187
|
+
} catch (error) {
|
|
188
|
+
logJobFailed(job.id, job.type, error);
|
|
189
|
+
throw error;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## Testing
|
|
195
|
+
|
|
196
|
+
```typescript
|
|
197
|
+
import { createMockLogger } from '@sprqvntrs/logger/testing';
|
|
198
|
+
|
|
199
|
+
describe('MyService', () => {
|
|
200
|
+
let logger: MockLogger;
|
|
201
|
+
|
|
202
|
+
beforeEach(() => {
|
|
203
|
+
logger = createMockLogger();
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('logs user creation', () => {
|
|
207
|
+
const service = new MyService(logger);
|
|
208
|
+
service.createUser({ name: 'Test' });
|
|
209
|
+
|
|
210
|
+
expect(logger.hasLog((log) =>
|
|
211
|
+
log.level === 'info' &&
|
|
212
|
+
log.message.includes('User created')
|
|
213
|
+
)).toBe(true);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('captures error context', () => {
|
|
217
|
+
const service = new MyService(logger);
|
|
218
|
+
service.failingOperation();
|
|
219
|
+
|
|
220
|
+
const errorLogs = logger.getLogsByLevel('error');
|
|
221
|
+
expect(errorLogs).toHaveLength(1);
|
|
222
|
+
expect(errorLogs[0]?.context?.error).toBeDefined();
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
afterEach(() => {
|
|
226
|
+
logger.clear();
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### Spy Logger
|
|
232
|
+
|
|
233
|
+
Wrap a real logger to capture calls while still logging:
|
|
234
|
+
|
|
235
|
+
```typescript
|
|
236
|
+
import { createSpyLogger, createLogger } from '@sprqvntrs/logger/testing';
|
|
237
|
+
|
|
238
|
+
const realLogger = createLogger({ serviceName: 'test' });
|
|
239
|
+
const { logger, getCalls } = createSpyLogger(realLogger);
|
|
240
|
+
|
|
241
|
+
logger.info('test message', { key: 'value' });
|
|
242
|
+
|
|
243
|
+
expect(getCalls('info')).toContainEqual({
|
|
244
|
+
message: 'test message',
|
|
245
|
+
context: { key: 'value' },
|
|
246
|
+
});
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
## Environment Variables
|
|
250
|
+
|
|
251
|
+
| Variable | Description | Default |
|
|
252
|
+
|----------|-------------|---------|
|
|
253
|
+
| `LOG_LEVEL` | Log level | `info` |
|
|
254
|
+
| `NODE_ENV` | Included as `env` in log base fields | - |
|
|
255
|
+
|
|
256
|
+
## Pretty Printing (Development)
|
|
257
|
+
|
|
258
|
+
Pretty output is **opt-in**. To enable it, install `pino-pretty` as a dev dependency in your application and pass `pretty: true`:
|
|
259
|
+
|
|
260
|
+
```bash
|
|
261
|
+
pnpm add -D pino-pretty
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
```typescript
|
|
265
|
+
const logger = createLogger({
|
|
266
|
+
serviceName: 'my-service',
|
|
267
|
+
pretty: process.env.NODE_ENV !== 'production', // your choice when to enable
|
|
268
|
+
});
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
Without `pretty: true`, the logger emits structured JSON regardless of `NODE_ENV` — which is what production log aggregators (Datadog, CloudWatch, Loki) expect. If you pass `pretty: true` without `pino-pretty` installed, Pino will throw at instantiation.
|
|
272
|
+
|
|
273
|
+
## Automatic Redaction
|
|
274
|
+
|
|
275
|
+
The following paths are automatically redacted from logs:
|
|
276
|
+
|
|
277
|
+
- `password`, `token`, `authorization`, `apiKey`, `api_key`, `secret`, `credential`
|
|
278
|
+
- Nested paths: `*.password`, `*.token`, etc.
|
|
279
|
+
- Headers: `headers.authorization`, `headers.cookie`
|
|
280
|
+
|
|
281
|
+
Add custom redaction paths:
|
|
282
|
+
|
|
283
|
+
```typescript
|
|
284
|
+
const logger = createLogger({
|
|
285
|
+
serviceName: 'api',
|
|
286
|
+
redactPaths: ['*.ssn', 'creditCard'],
|
|
287
|
+
});
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
## TypeScript
|
|
291
|
+
|
|
292
|
+
This package exports TypeScript source files directly. Configure your bundler to transpile `node_modules/@sprqvntrs/*`:
|
|
293
|
+
|
|
294
|
+
```json
|
|
295
|
+
{
|
|
296
|
+
"compilerOptions": {
|
|
297
|
+
"moduleResolution": "bundler",
|
|
298
|
+
"allowImportingTsExtensions": true
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
## Raw TypeScript
|
|
304
|
+
|
|
305
|
+
This package ships raw TypeScript (`main` and `types` point at `index.ts`), so a Vite
|
|
306
|
+
consumer (Vite, React Router, Remix) must add the scope to `ssr.noExternal`:
|
|
307
|
+
`ssr: { noExternal: [/^@sprqvntrs\//] }`.
|
|
308
|
+
|
|
309
|
+
## License
|
|
310
|
+
|
|
311
|
+
MIT
|
package/index.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Main factory function
|
|
2
|
+
export { createLogger, StaticLogger } from './src/core/logger';
|
|
3
|
+
|
|
4
|
+
// Backward-compatible default export (static logger)
|
|
5
|
+
export { StaticLogger as default } from './src/core/logger';
|
|
6
|
+
|
|
7
|
+
// Buffered logger
|
|
8
|
+
export { createBufferedLogger } from './src/core/buffered-logger';
|
|
9
|
+
|
|
10
|
+
// Flush destinations
|
|
11
|
+
export { jsonDestination } from './src/core/destinations';
|
|
12
|
+
export type { JsonDestinationOptions } from './src/core/destinations';
|
|
13
|
+
|
|
14
|
+
// Types
|
|
15
|
+
export type {
|
|
16
|
+
Logger,
|
|
17
|
+
LogLevel,
|
|
18
|
+
LogContext,
|
|
19
|
+
LogEntry,
|
|
20
|
+
MockLogger,
|
|
21
|
+
CreateLoggerOptions,
|
|
22
|
+
LoggerConfigureOptions,
|
|
23
|
+
HttpLoggerOptions,
|
|
24
|
+
RequestContext,
|
|
25
|
+
ServerLogger,
|
|
26
|
+
BufferedLogger,
|
|
27
|
+
BufferedLoggerOptions,
|
|
28
|
+
FlushDestination,
|
|
29
|
+
SerializedLogEntry,
|
|
30
|
+
} from './src/types';
|
|
31
|
+
|
|
32
|
+
// Context utilities for request tracing
|
|
33
|
+
export {
|
|
34
|
+
withRequestContext,
|
|
35
|
+
withRequestContextAsync,
|
|
36
|
+
getRequestContext,
|
|
37
|
+
getRequestId,
|
|
38
|
+
getContextValue,
|
|
39
|
+
updateRequestContext,
|
|
40
|
+
generateRequestId,
|
|
41
|
+
createRequestContextMiddleware,
|
|
42
|
+
} from './src/context/async-context';
|
|
43
|
+
|
|
44
|
+
// Serializers (for advanced customization)
|
|
45
|
+
export {
|
|
46
|
+
errorSerializer,
|
|
47
|
+
requestSerializer,
|
|
48
|
+
responseSerializer,
|
|
49
|
+
getSerializers,
|
|
50
|
+
} from './src/core/serializers';
|
|
51
|
+
|
|
52
|
+
// Pino config (for advanced customization)
|
|
53
|
+
export { createPinoConfig, createPinoInstance } from './src/core/pino-config';
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sprqvntrs/logger",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./index.ts",
|
|
6
|
+
"types": "./index.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./index.ts",
|
|
9
|
+
"./http": "./src/middleware/http.ts",
|
|
10
|
+
"./server": "./src/presets/server.ts",
|
|
11
|
+
"./testing": "./src/testing/mock-logger.ts"
|
|
12
|
+
},
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/SPRQVNTRS/platform.git",
|
|
17
|
+
"directory": "packages/logger"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"src/**/*",
|
|
21
|
+
"index.ts",
|
|
22
|
+
"LICENSE"
|
|
23
|
+
],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"test": "vitest run --passWithNoTests",
|
|
26
|
+
"typecheck": "tsc --noEmit"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"pino": "^9.6.0",
|
|
30
|
+
"pino-http": "^10.4.0"
|
|
31
|
+
},
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"pino-pretty": "^13.0.0"
|
|
34
|
+
},
|
|
35
|
+
"peerDependenciesMeta": {
|
|
36
|
+
"pino-pretty": {
|
|
37
|
+
"optional": true
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@types/node": "^22.0.0",
|
|
42
|
+
"pino-pretty": "^13.0.0",
|
|
43
|
+
"tsx": "^4.20.6",
|
|
44
|
+
"typescript": "^5.6.0",
|
|
45
|
+
"vitest": "^3.2.4"
|
|
46
|
+
},
|
|
47
|
+
"publishConfig": {
|
|
48
|
+
"access": "public"
|
|
49
|
+
},
|
|
50
|
+
"homepage": "https://github.com/SPRQVNTRS/platform/tree/main/packages/logger#readme",
|
|
51
|
+
"bugs": {
|
|
52
|
+
"url": "https://github.com/SPRQVNTRS/platform/issues"
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
import type { RequestContext } from '../types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* AsyncLocalStorage instance for maintaining request context
|
|
6
|
+
* across async operations
|
|
7
|
+
*/
|
|
8
|
+
const asyncLocalStorage = new AsyncLocalStorage<RequestContext>();
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Generates a unique request ID
|
|
12
|
+
* Uses crypto.randomUUID if available, falls back to timestamp-based ID
|
|
13
|
+
*/
|
|
14
|
+
export function generateRequestId(): string {
|
|
15
|
+
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
|
16
|
+
return crypto.randomUUID();
|
|
17
|
+
}
|
|
18
|
+
// Fallback for older Node.js versions
|
|
19
|
+
return `${Date.now().toString(36)}-${Math.random().toString(36).substring(2, 11)}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Run a function with request context
|
|
24
|
+
* The context will be available to all async operations within the callback
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```typescript
|
|
28
|
+
* // In middleware
|
|
29
|
+
* app.use((req, res, next) => {
|
|
30
|
+
* withRequestContext({ requestId: generateRequestId() }, () => {
|
|
31
|
+
* next();
|
|
32
|
+
* });
|
|
33
|
+
* });
|
|
34
|
+
*
|
|
35
|
+
* // Later in any handler or service
|
|
36
|
+
* logger.info('Processing'); // requestId automatically included
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export function withRequestContext<T>(context: RequestContext, fn: () => T): T {
|
|
40
|
+
return asyncLocalStorage.run(context, fn);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Run an async function with request context
|
|
45
|
+
* Convenience wrapper for async functions
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* ```typescript
|
|
49
|
+
* await withRequestContextAsync({ requestId: '123' }, async () => {
|
|
50
|
+
* await someAsyncOperation();
|
|
51
|
+
* logger.info('Done'); // requestId automatically included
|
|
52
|
+
* });
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
export async function withRequestContextAsync<T>(
|
|
56
|
+
context: RequestContext,
|
|
57
|
+
fn: () => Promise<T>
|
|
58
|
+
): Promise<T> {
|
|
59
|
+
return asyncLocalStorage.run(context, fn);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Get the current request context
|
|
64
|
+
* Returns undefined if not running within a request context
|
|
65
|
+
*/
|
|
66
|
+
export function getRequestContext(): RequestContext | undefined {
|
|
67
|
+
return asyncLocalStorage.getStore();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Get a specific value from the current request context
|
|
72
|
+
*/
|
|
73
|
+
export function getContextValue<K extends keyof RequestContext>(
|
|
74
|
+
key: K
|
|
75
|
+
): RequestContext[K] | undefined {
|
|
76
|
+
const store = asyncLocalStorage.getStore();
|
|
77
|
+
return store?.[key];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Get the current request ID from context
|
|
82
|
+
* Convenience method for the most common use case
|
|
83
|
+
*/
|
|
84
|
+
export function getRequestId(): string | undefined {
|
|
85
|
+
return getContextValue('requestId');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Update the current request context
|
|
90
|
+
* Merges new values with existing context
|
|
91
|
+
* Only works within an active context - no-op otherwise
|
|
92
|
+
*/
|
|
93
|
+
export function updateRequestContext(updates: Partial<RequestContext>): void {
|
|
94
|
+
const store = asyncLocalStorage.getStore();
|
|
95
|
+
if (store) {
|
|
96
|
+
Object.assign(store, updates);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Create a middleware-style function for Express/Koa/etc
|
|
102
|
+
* that sets up request context for each request
|
|
103
|
+
*
|
|
104
|
+
* @example
|
|
105
|
+
* ```typescript
|
|
106
|
+
* // Express
|
|
107
|
+
* app.use(createRequestContextMiddleware());
|
|
108
|
+
*
|
|
109
|
+
* // With custom ID extraction
|
|
110
|
+
* app.use(createRequestContextMiddleware((req) => ({
|
|
111
|
+
* requestId: req.headers['x-request-id'] || generateRequestId(),
|
|
112
|
+
* userId: req.user?.id,
|
|
113
|
+
* })));
|
|
114
|
+
* ```
|
|
115
|
+
*/
|
|
116
|
+
export function createRequestContextMiddleware(
|
|
117
|
+
contextExtractor?: (req: unknown) => RequestContext
|
|
118
|
+
): (req: unknown, res: unknown, next: () => void) => void {
|
|
119
|
+
return (req, _res, next) => {
|
|
120
|
+
const context = contextExtractor
|
|
121
|
+
? contextExtractor(req)
|
|
122
|
+
: { requestId: generateRequestId() };
|
|
123
|
+
|
|
124
|
+
withRequestContext(context, next);
|
|
125
|
+
};
|
|
126
|
+
}
|