@maestro-js/log 1.0.0-alpha.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/README.md +316 -0
- package/dist/index.d.ts +221 -0
- package/dist/index.js +337 -0
- package/package.json +38 -0
package/README.md
ADDED
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
# @maestro-js/log
|
|
2
|
+
|
|
3
|
+
Structured logging with pluggable transports and channel-based routing. Messages have a severity level (`emergency`, `error`, `warn`, `info`) and an optional channel for routing to specific transports.
|
|
4
|
+
|
|
5
|
+
## Quick Setup
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { Log } from '@maestro-js/log'
|
|
9
|
+
|
|
10
|
+
// Use the default logger directly (falls back to console if no transports added)
|
|
11
|
+
Log.info('Server started on port', 3000)
|
|
12
|
+
|
|
13
|
+
// Or create an independent logger with transports
|
|
14
|
+
const logger = Log.create({
|
|
15
|
+
transports: [
|
|
16
|
+
Log.transports.console(),
|
|
17
|
+
Log.transports.file({ filename: '/var/log/app.log' })
|
|
18
|
+
]
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
logger.info('Server started on port', 3000)
|
|
22
|
+
logger.error('Connection failed', err)
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Architecture
|
|
26
|
+
|
|
27
|
+
The package does NOT use the service-registry Provider pattern. Instead it exports a `Log` object that acts as a default logger facade and a `Log.create()` factory for independent logger instances.
|
|
28
|
+
|
|
29
|
+
### Source Files
|
|
30
|
+
|
|
31
|
+
- `packages/log/src/index.ts` -- Main entry: `Log.create()`, default logger facade, `Log.transports` namespace
|
|
32
|
+
- `packages/log/src/log-types.ts` -- Core types: `LogLevel`, `LogChannel`, `LogMessage`, `LogTransport`, `LogMessageFilter`, `LogFormatter`
|
|
33
|
+
- `packages/log/src/console-transport.ts` -- Console transport with colorized output
|
|
34
|
+
- `packages/log/src/file-transport.ts` -- File transport with streaming writes and `flush()`
|
|
35
|
+
- `packages/log/src/sentry-transport.ts` -- Sentry error/message capture transport
|
|
36
|
+
- `packages/log/src/sentry-breadcrumbs-transport.ts` -- Sentry breadcrumbs transport
|
|
37
|
+
- `packages/log/src/stub-transport.ts` -- In-memory transport for testing
|
|
38
|
+
- `packages/log/src/transport-helpers.ts` -- `parseLogMessageFilter()` utility
|
|
39
|
+
|
|
40
|
+
### Key Types
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
type LogLevel = 'emergency' | 'error' | 'warn' | 'info'
|
|
44
|
+
type LogChannel = string | null
|
|
45
|
+
|
|
46
|
+
interface LogMessage<LogMessageData extends any[] = any[]> {
|
|
47
|
+
data: LogMessageData
|
|
48
|
+
level: LogLevel
|
|
49
|
+
channel: LogChannel
|
|
50
|
+
time: Iso.Instant
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface LogTransport<LogMessageData extends any[] = any[]> {
|
|
54
|
+
write(message: LogMessage<LogMessageData>): void
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
type LogMessageFilter<LogMessageData extends any[] = any[]> =
|
|
58
|
+
| { level?: LogLevel | LogLevel[]; channel?: LogChannel | LogChannel[] }
|
|
59
|
+
| ((message: LogMessage<LogMessageData>) => boolean)
|
|
60
|
+
| undefined
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Transports
|
|
64
|
+
|
|
65
|
+
All transports accept an optional `filter` for level/channel-based routing.
|
|
66
|
+
|
|
67
|
+
### Console Transport
|
|
68
|
+
|
|
69
|
+
Colorized output to stdout with configurable formatting.
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
Log.transports.console()
|
|
73
|
+
Log.transports.console({ filter: { level: ['error', 'emergency'] } })
|
|
74
|
+
Log.transports.console({ format: (message) => `${message.level}: ${message.data.join(' ')}\n` })
|
|
75
|
+
// Pass InspectOptions instead of a formatter function
|
|
76
|
+
Log.transports.console({ format: { colors: true, depth: 4 } })
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### File Transport
|
|
80
|
+
|
|
81
|
+
Appends log messages to a file via streaming. Creates the directory if it does not exist. Returns a transport with an additional `flush()` method.
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
const fileTransport = Log.transports.file({
|
|
85
|
+
filename: '/var/log/app.log',
|
|
86
|
+
filter: { level: ['error', 'emergency'] },
|
|
87
|
+
format: (message) => JSON.stringify(message) + '\n'
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
// Ensure all buffered writes are flushed to disk
|
|
91
|
+
await fileTransport.flush()
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Sentry Transport
|
|
95
|
+
|
|
96
|
+
Captures errors and messages to Sentry. Errors at `error`/`emergency` level use `captureException`; other levels use `captureMessage`.
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
import * as Sentry from '@sentry/node'
|
|
100
|
+
|
|
101
|
+
Log.transports.sentry({
|
|
102
|
+
sentry: Sentry,
|
|
103
|
+
filter: { level: ['error', 'emergency'] },
|
|
104
|
+
getCaptureContext(message) {
|
|
105
|
+
return {
|
|
106
|
+
tags: { channel: message.channel ?? 'default' },
|
|
107
|
+
extra: {},
|
|
108
|
+
level: message.level === 'emergency' ? 'fatal' : message.level
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
})
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Sentry Breadcrumbs Transport
|
|
115
|
+
|
|
116
|
+
Adds log messages as Sentry breadcrumbs for context on future error captures.
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
import * as Sentry from '@sentry/node'
|
|
120
|
+
|
|
121
|
+
Log.transports.sentryBreadcrumbs({
|
|
122
|
+
sentry: Sentry,
|
|
123
|
+
filter: { level: ['info', 'warn'] }
|
|
124
|
+
})
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Stub Transport
|
|
128
|
+
|
|
129
|
+
In-memory transport for testing. Captures messages and provides `listMessages()` and `reset()`.
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
const stub = Log.transports.stub()
|
|
133
|
+
const logger = Log.create({ transports: [stub] })
|
|
134
|
+
|
|
135
|
+
logger.info('test message')
|
|
136
|
+
stub.listMessages() // => [{ data: ['test message'], level: 'info', channel: null, time: '...' }]
|
|
137
|
+
stub.reset()
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Silence Transport
|
|
141
|
+
|
|
142
|
+
Suppresses all output. Use to disable logging entirely.
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
Log.addTransport(Log.transports.silence)
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## API Reference
|
|
149
|
+
|
|
150
|
+
### `Log.create(config?)`
|
|
151
|
+
|
|
152
|
+
Create an independent logger instance.
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
const logger = Log.create({
|
|
156
|
+
transports: [Log.transports.console()]
|
|
157
|
+
})
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Default Logger Facade
|
|
161
|
+
|
|
162
|
+
Call logging methods directly on `Log` without creating an instance.
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
Log.addTransport(Log.transports.console())
|
|
166
|
+
Log.info('message', data)
|
|
167
|
+
Log.warn('slow query', { durationMs: 500 })
|
|
168
|
+
Log.error('failed', new Error('timeout'))
|
|
169
|
+
Log.emergency('system down')
|
|
170
|
+
Log.removeTransport(someTransport)
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Logging Methods
|
|
174
|
+
|
|
175
|
+
- `info(...data)` -- Log at info level
|
|
176
|
+
- `warn(...data)` -- Log at warn level
|
|
177
|
+
- `error(...data)` -- Log at error level
|
|
178
|
+
- `emergency(...data)` -- Log at emergency level
|
|
179
|
+
- `write(message)` -- Write a raw log message (supply `level`, `data`, and `time` yourself)
|
|
180
|
+
|
|
181
|
+
### Channels
|
|
182
|
+
|
|
183
|
+
Create channel-scoped sub-loggers that tag every message with a channel name. Transports can filter by channel.
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
const logger = Log.create({ transports: [Log.transports.console()] })
|
|
187
|
+
const dbLog = logger.channel('database')
|
|
188
|
+
dbLog.info('Query executed', { table: 'users', durationMs: 12 })
|
|
189
|
+
dbLog.warn('Slow query detected')
|
|
190
|
+
|
|
191
|
+
// Filter transport to only receive database channel messages
|
|
192
|
+
Log.transports.console({ filter: { channel: 'database' } })
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### `Log.overrideNodeJsConsole(logger)`
|
|
196
|
+
|
|
197
|
+
Replace global `console.log`, `console.warn`, `console.error`, `console.info`, and `console.debug` with methods from the provided logger. Returns a restore function.
|
|
198
|
+
|
|
199
|
+
```ts
|
|
200
|
+
const restore = Log.overrideNodeJsConsole(logger)
|
|
201
|
+
console.log('now goes through logger')
|
|
202
|
+
restore() // restores original console methods
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## Filtering
|
|
206
|
+
|
|
207
|
+
Transports accept a `filter` option that controls which messages reach them.
|
|
208
|
+
|
|
209
|
+
```ts
|
|
210
|
+
// Filter by level
|
|
211
|
+
Log.transports.console({ filter: { level: 'error' } })
|
|
212
|
+
Log.transports.console({ filter: { level: ['error', 'emergency'] } })
|
|
213
|
+
|
|
214
|
+
// Filter by channel
|
|
215
|
+
Log.transports.console({ filter: { channel: 'database' } })
|
|
216
|
+
|
|
217
|
+
// Filter by both level and channel
|
|
218
|
+
Log.transports.console({ filter: { level: 'error', channel: 'database' } })
|
|
219
|
+
|
|
220
|
+
// Custom filter function
|
|
221
|
+
Log.transports.console({
|
|
222
|
+
filter: (message) => message.level === 'error' && message.data.some((d) => d instanceof Error)
|
|
223
|
+
})
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
## Fallback Behavior
|
|
227
|
+
|
|
228
|
+
When a logger has no transports registered, it falls back to a default `ConsoleTransport()`. Add `Log.transports.silence` to explicitly suppress all output.
|
|
229
|
+
|
|
230
|
+
## Custom Transports
|
|
231
|
+
|
|
232
|
+
Implement the `LogTransport` interface.
|
|
233
|
+
|
|
234
|
+
```ts
|
|
235
|
+
const customTransport: Log.Transport = {
|
|
236
|
+
write(message) {
|
|
237
|
+
// message.data, message.level, message.channel, message.time
|
|
238
|
+
fetch('https://log-service.example.com', {
|
|
239
|
+
method: 'POST',
|
|
240
|
+
body: JSON.stringify(message)
|
|
241
|
+
})
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
## Testing
|
|
247
|
+
|
|
248
|
+
Run tests:
|
|
249
|
+
|
|
250
|
+
```bash
|
|
251
|
+
pnpm --filter @maestro-js/log test
|
|
252
|
+
cd packages/log && npx beartest ./tests/log.test.ts
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
Use `StubTransport` to capture and assert on log output:
|
|
256
|
+
|
|
257
|
+
```ts
|
|
258
|
+
import { test } from 'beartest-js'
|
|
259
|
+
import { expect } from 'expect'
|
|
260
|
+
import { Log } from '@maestro-js/log'
|
|
261
|
+
|
|
262
|
+
test('should capture log messages', () => {
|
|
263
|
+
const stub = Log.transports.stub()
|
|
264
|
+
const logger = Log.create({ transports: [stub] })
|
|
265
|
+
|
|
266
|
+
logger.info('test')
|
|
267
|
+
|
|
268
|
+
const messages = stub.listMessages()
|
|
269
|
+
expect(messages).toHaveLength(1)
|
|
270
|
+
expect(messages[0].level).toBe('info')
|
|
271
|
+
expect(messages[0].data).toEqual(['test'])
|
|
272
|
+
|
|
273
|
+
stub.reset()
|
|
274
|
+
})
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
Use inline transports for targeted assertions:
|
|
278
|
+
|
|
279
|
+
```ts
|
|
280
|
+
test('should write to transport', () => {
|
|
281
|
+
const messages: Log.Message[] = []
|
|
282
|
+
const log = Log.create()
|
|
283
|
+
log.addTransport({ write(message) { messages.push(message) } })
|
|
284
|
+
|
|
285
|
+
log.info('log')
|
|
286
|
+
expect(messages).toHaveLength(1)
|
|
287
|
+
})
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
## Cross-Package Integration
|
|
291
|
+
|
|
292
|
+
Log is used widely across Maestro packages as the standard logging mechanism:
|
|
293
|
+
|
|
294
|
+
- **queue, recurring-jobs, mail** -- Log job processing, email sending, and scheduling events
|
|
295
|
+
- **db** -- Log queries and connection events
|
|
296
|
+
- **cache, events, broadcast** -- Log cache operations and event dispatching
|
|
297
|
+
- **metrics, exceptions** -- Log metric collection and exception handling
|
|
298
|
+
|
|
299
|
+
Typical integration pattern in other packages:
|
|
300
|
+
|
|
301
|
+
```ts
|
|
302
|
+
import { Log } from '@maestro-js/log'
|
|
303
|
+
|
|
304
|
+
const log = Log.create()
|
|
305
|
+
// or use Log.info(), Log.error() directly via the default facade
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
### Context Integration
|
|
309
|
+
|
|
310
|
+
Combine with `@maestro-js/context` (AsyncLocalStorage) for request-scoped logging by passing contextual data through log message data arguments.
|
|
311
|
+
|
|
312
|
+
## Dependencies
|
|
313
|
+
|
|
314
|
+
- `colorette` -- Terminal color support for console transport
|
|
315
|
+
- `iso-fns2` -- ISO timestamp types (`Iso.Instant`)
|
|
316
|
+
- `@sentry/types` (devDependency) -- Sentry type definitions
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { Iso } from 'iso-fns2';
|
|
2
|
+
import { InspectOptions } from 'node:util';
|
|
3
|
+
|
|
4
|
+
type LogLevel = 'emergency' | 'error' | 'warn' | 'info';
|
|
5
|
+
type LogChannel = string | null;
|
|
6
|
+
type LogFormatter<LogMessageData extends any[] = any[]> = (message: LogMessage<LogMessageData>) => string;
|
|
7
|
+
interface LogMessage<LogMessageData extends any[] = any[]> {
|
|
8
|
+
data: LogMessageData;
|
|
9
|
+
level: LogLevel;
|
|
10
|
+
channel: LogChannel;
|
|
11
|
+
time: Iso.Instant;
|
|
12
|
+
}
|
|
13
|
+
type LogMessageFilter<LogMessageData extends any[] = any[]> = {
|
|
14
|
+
level?: LogLevel | LogLevel[] | undefined;
|
|
15
|
+
channel?: LogChannel | LogChannel[] | undefined;
|
|
16
|
+
} | ((message: LogMessage<LogMessageData>) => boolean) | undefined;
|
|
17
|
+
interface LogTransport<LogMessageData extends any[] = any[]> {
|
|
18
|
+
write(message: LogMessage<LogMessageData>): void;
|
|
19
|
+
}
|
|
20
|
+
interface LogFunctions<LogMessageData extends any[] = any[]> {
|
|
21
|
+
write(message: Omit<LogMessage<LogMessageData>, 'channel'>): void;
|
|
22
|
+
info(...data: LogMessageData): void;
|
|
23
|
+
warn(...data: LogMessageData): void;
|
|
24
|
+
error(...data: LogMessageData): void;
|
|
25
|
+
emergency(...data: LogMessageData): void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
declare function ConsoleTransport<LogMessageData extends any[] = any[]>(options?: {
|
|
29
|
+
filter?: LogMessageFilter<LogMessageData>;
|
|
30
|
+
format?: LogFormatter<LogMessageData> | InspectOptions;
|
|
31
|
+
}): LogTransport<LogMessageData>;
|
|
32
|
+
|
|
33
|
+
declare function FileTransport<LogMessageData extends any[] = any[]>(options: {
|
|
34
|
+
filter?: LogMessageFilter<LogMessageData>;
|
|
35
|
+
format?: LogFormatter<LogMessageData>;
|
|
36
|
+
filename: string;
|
|
37
|
+
}): {
|
|
38
|
+
flush(): Promise<void>;
|
|
39
|
+
write(message: LogMessage<LogMessageData>): void;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
type Primitive = number | string | boolean | bigint | symbol | null | undefined;
|
|
43
|
+
interface SentryCaptureContext {
|
|
44
|
+
tags: {
|
|
45
|
+
[key: string]: Primitive;
|
|
46
|
+
};
|
|
47
|
+
extra: Record<string, unknown>;
|
|
48
|
+
level?: 'fatal' | 'error' | 'warning' | 'log' | 'info' | 'debug';
|
|
49
|
+
}
|
|
50
|
+
declare function SentryTransport<LogMessageData extends any[] = any[]>(options: {
|
|
51
|
+
filter?: LogMessageFilter<LogMessageData>;
|
|
52
|
+
sentry: {
|
|
53
|
+
captureException(exception: any, context: SentryCaptureContext): unknown;
|
|
54
|
+
captureMessage(message: string, context: SentryCaptureContext): unknown;
|
|
55
|
+
};
|
|
56
|
+
getCaptureContext(message: LogMessage): SentryCaptureContext;
|
|
57
|
+
}): LogTransport<LogMessageData>;
|
|
58
|
+
|
|
59
|
+
declare function SentryBreadcrumbsTransport<LogMessageData extends any[] = any[]>(options: {
|
|
60
|
+
filter?: LogMessageFilter<LogMessageData>;
|
|
61
|
+
sentry: {
|
|
62
|
+
addBreadcrumb(breadcrumb: {
|
|
63
|
+
level?: 'fatal' | 'error' | 'warning' | 'log' | 'info' | 'debug';
|
|
64
|
+
category?: string;
|
|
65
|
+
message?: string;
|
|
66
|
+
data?: {
|
|
67
|
+
[key: string]: any;
|
|
68
|
+
};
|
|
69
|
+
timestamp?: number;
|
|
70
|
+
}): void;
|
|
71
|
+
};
|
|
72
|
+
}): LogTransport<LogMessageData>;
|
|
73
|
+
|
|
74
|
+
declare function StubTransport<LogMessageData extends any[] = any[]>(options?: {
|
|
75
|
+
filter?: LogMessageFilter<LogMessageData>;
|
|
76
|
+
transform?: (message: LogMessage<LogMessageData>) => LogMessage<LogMessageData>;
|
|
77
|
+
}): {
|
|
78
|
+
write(message: LogMessage<LogMessageData>): void;
|
|
79
|
+
reset(): void;
|
|
80
|
+
listMessages(): LogMessage<LogMessageData>[];
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Creates a new logger. By default loggers have no transports
|
|
85
|
+
*/
|
|
86
|
+
declare function create<LoggerMessageData extends any[] = any>(config?: Log.Config<LoggerMessageData>): Log.Logger<LoggerMessageData>;
|
|
87
|
+
/**
|
|
88
|
+
* Monkey patches the default console methods with methods
|
|
89
|
+
* from the supplied logger. Returns a function that restores
|
|
90
|
+
* the original console methods.
|
|
91
|
+
*/
|
|
92
|
+
declare function overrideNodeJsConsole(logger: LogFunctions): () => void;
|
|
93
|
+
/**
|
|
94
|
+
* Structured logging with pluggable transports and channel-based routing.
|
|
95
|
+
*
|
|
96
|
+
* Log messages have a severity {@link Log.Level level} (`emergency`, `error`,
|
|
97
|
+
* `warn`, `info`) and an optional {@link Log.Channel channel} for routing.
|
|
98
|
+
* {@link Log.Transport Transports} move messages from the logger to their
|
|
99
|
+
* final destination — stdout, a file, Sentry, Slack, etc.
|
|
100
|
+
*
|
|
101
|
+
* If no transports are registered the logger falls back to console output.
|
|
102
|
+
* Add the built-in `Log.transports.silence` transport to suppress output
|
|
103
|
+
* entirely.
|
|
104
|
+
*
|
|
105
|
+
* @example
|
|
106
|
+
* ```ts
|
|
107
|
+
* import { Log } from '@maestro-js/log'
|
|
108
|
+
*
|
|
109
|
+
* // Create a logger with transports
|
|
110
|
+
* const logger = Log.create({
|
|
111
|
+
* transports: [
|
|
112
|
+
* Log.transports.console(),
|
|
113
|
+
* Log.transports.file({ filename: '/var/log/app.log' })
|
|
114
|
+
* ]
|
|
115
|
+
* })
|
|
116
|
+
*
|
|
117
|
+
* // Log at different levels
|
|
118
|
+
* logger.info('Server started on port', 3000)
|
|
119
|
+
* logger.error('Connection failed', err)
|
|
120
|
+
*
|
|
121
|
+
* // Route messages through a named channel
|
|
122
|
+
* const db = logger.channel('database')
|
|
123
|
+
* db.warn('Slow query detected', query)
|
|
124
|
+
*
|
|
125
|
+
* // Or use the default facade directly
|
|
126
|
+
* Log.addTransport(Log.transports.console())
|
|
127
|
+
* Log.info('Hello from the default logger')
|
|
128
|
+
* ```
|
|
129
|
+
*/
|
|
130
|
+
declare namespace Log {
|
|
131
|
+
/** Configuration options passed to {@link Log.create}. */
|
|
132
|
+
interface Config<LoggerMessageData extends any[] = any> {
|
|
133
|
+
transports?: Log.Transport<LoggerMessageData>[];
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Full logger instance returned by {@link Log.create}.
|
|
137
|
+
*
|
|
138
|
+
* Extends {@link Log.LogFunctions} with transport management and the
|
|
139
|
+
* ability to create channel-scoped sub-loggers.
|
|
140
|
+
*
|
|
141
|
+
* @example
|
|
142
|
+
* ```ts
|
|
143
|
+
* const logger = Log.create()
|
|
144
|
+
* logger.addTransport(Log.transports.console())
|
|
145
|
+
* const dbLog = logger.channel('database')
|
|
146
|
+
* dbLog.info('Query executed', { table: 'users', durationMs: 12 })
|
|
147
|
+
* ```
|
|
148
|
+
*/
|
|
149
|
+
interface Logger<LogMessageData extends any[] = any[]> extends Log.LogFunctions<LogMessageData> {
|
|
150
|
+
/** Appends one or more transports to this logger. */
|
|
151
|
+
addTransport(...transport: Log.Transport<LogMessageData>[]): void;
|
|
152
|
+
/** Removes one or more previously added transports. */
|
|
153
|
+
removeTransport(...transport: Log.Transport<LogMessageData>[]): void;
|
|
154
|
+
/** Return a channel-scoped subset of logging methods that tag every message with `channel`. */
|
|
155
|
+
channel<ChannelMessageData extends LogMessageData = LogMessageData>(channel: LogChannel): Log.LogFunctions<ChannelMessageData>;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Channel-scoped subset of logging methods.
|
|
159
|
+
*
|
|
160
|
+
* Returned by {@link Log.Logger.channel} and also the base shape of every
|
|
161
|
+
* {@link Log.Logger}.
|
|
162
|
+
*
|
|
163
|
+
* @example
|
|
164
|
+
* ```ts
|
|
165
|
+
* const logger = Log.create({ transports: [Log.transports.console()] })
|
|
166
|
+
* logger.info('Server started on port', 3000)
|
|
167
|
+
* logger.warn('Disk usage high', { usedPct: 83 })
|
|
168
|
+
* logger.error('Payment failed', new Error('Timeout'))
|
|
169
|
+
* ```
|
|
170
|
+
*/
|
|
171
|
+
interface LogFunctions<LogMessageData extends any[] = any[]> {
|
|
172
|
+
/** Write a raw log message (you must supply `level` and `data` yourself). */
|
|
173
|
+
write(message: Omit<LogMessage<LogMessageData>, 'channel'>): void;
|
|
174
|
+
/** Log at `info` level. */
|
|
175
|
+
info(...data: LogMessageData): void;
|
|
176
|
+
/** Log at `warn` level. */
|
|
177
|
+
warn(...data: LogMessageData): void;
|
|
178
|
+
/** Log at `error` level. */
|
|
179
|
+
error(...data: LogMessageData): void;
|
|
180
|
+
/** Log at `emergency` level. */
|
|
181
|
+
emergency(...data: LogMessageData): void;
|
|
182
|
+
}
|
|
183
|
+
/** Named scope that tags log messages for transport filtering. */
|
|
184
|
+
type Channel = LogChannel;
|
|
185
|
+
/** Converts a {@link Log.Message} into a string for output. */
|
|
186
|
+
type Formatter<LogMessageData extends any[] = any[]> = LogFormatter<LogMessageData>;
|
|
187
|
+
/** Severity level for log messages, from most to least critical: `emergency`, `error`, `warn`, `info`. */
|
|
188
|
+
type Level = LogLevel;
|
|
189
|
+
/** Structured log entry passed to every {@link Log.Transport}. */
|
|
190
|
+
type Message<LogMessageData extends any[] = any[]> = LogMessage<LogMessageData>;
|
|
191
|
+
/** Predicate or descriptor that controls which messages reach a {@link Log.Transport}. */
|
|
192
|
+
type MessageFilter<LogMessageData extends any[] = any[]> = LogMessageFilter<LogMessageData>;
|
|
193
|
+
/** Destination that receives {@link Log.Message messages} from a logger. */
|
|
194
|
+
type Transport<LogMessageData extends any[] = any[]> = LogTransport<LogMessageData>;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Structured logging with pluggable transports.
|
|
198
|
+
* Use `Log.create()` for an independent logger, or call methods directly on the default instance.
|
|
199
|
+
*/
|
|
200
|
+
declare const Log: {
|
|
201
|
+
create: typeof create;
|
|
202
|
+
overrideNodeJsConsole: typeof overrideNodeJsConsole;
|
|
203
|
+
write: (message: Omit<LogMessage<any>, "channel">) => void;
|
|
204
|
+
info: (...data: any) => void;
|
|
205
|
+
warn: (...data: any) => void;
|
|
206
|
+
error: (...data: any) => void;
|
|
207
|
+
emergency: (...data: any) => void;
|
|
208
|
+
addTransport: (...transport: Log.Transport<any>[]) => void;
|
|
209
|
+
removeTransport: (...transport: Log.Transport<any>[]) => void;
|
|
210
|
+
channel: <ChannelMessageData extends any = any>(channel: LogChannel) => Log.LogFunctions<ChannelMessageData>;
|
|
211
|
+
transports: {
|
|
212
|
+
console: typeof ConsoleTransport;
|
|
213
|
+
file: typeof FileTransport;
|
|
214
|
+
sentry: typeof SentryTransport;
|
|
215
|
+
sentryBreadcrumbs: typeof SentryBreadcrumbsTransport;
|
|
216
|
+
silence: LogTransport<any[]>;
|
|
217
|
+
stub: typeof StubTransport;
|
|
218
|
+
};
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
export { Log };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
// src/transport-helpers.ts
|
|
2
|
+
var alwaysTrue = () => true;
|
|
3
|
+
function parseLogMessageFilter(filter) {
|
|
4
|
+
if (filter === void 0) {
|
|
5
|
+
return alwaysTrue;
|
|
6
|
+
} else if (typeof filter === "function") {
|
|
7
|
+
return filter;
|
|
8
|
+
} else {
|
|
9
|
+
const levelSet = new Set(Array.isArray(filter.level) ? filter.level : [filter.level]);
|
|
10
|
+
const levelMatch = filter.level === void 0 ? alwaysTrue : (level) => levelSet.has(level);
|
|
11
|
+
const channelSet = new Set(Array.isArray(filter.channel) ? filter.channel : [filter.channel]);
|
|
12
|
+
const channelMatch = filter.channel === void 0 ? alwaysTrue : (channel) => channelSet.has(channel);
|
|
13
|
+
return (message) => levelMatch(message.level) && channelMatch(message.channel);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// src/console-transport.ts
|
|
18
|
+
import { formatWithOptions } from "util";
|
|
19
|
+
import { isColorSupported, red, green, yellow, bgRed } from "colorette";
|
|
20
|
+
function ConsoleTransport(options) {
|
|
21
|
+
const { filter: filterRaw, format: formatRaw } = options ?? {};
|
|
22
|
+
const filter = parseLogMessageFilter(filterRaw);
|
|
23
|
+
const format2 = typeof formatRaw === "function" ? formatRaw : (message) => defaultFormatter(message, formatRaw);
|
|
24
|
+
return {
|
|
25
|
+
write(message) {
|
|
26
|
+
if (filter(message)) {
|
|
27
|
+
process.stdout.write(format2(message));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function colorizeLevel(level, colorize) {
|
|
33
|
+
if (!colorize) {
|
|
34
|
+
return level.toUpperCase();
|
|
35
|
+
} else if (level === "emergency") {
|
|
36
|
+
return bgRed(level.toUpperCase());
|
|
37
|
+
} else if (level === "error") {
|
|
38
|
+
return red(level.toUpperCase());
|
|
39
|
+
} else if (level === "warn") {
|
|
40
|
+
return yellow(level.toUpperCase());
|
|
41
|
+
} else if (level === "info") {
|
|
42
|
+
return green(level.toUpperCase());
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function formatTime(date) {
|
|
46
|
+
return `${date.getHours().toString().padStart(2, "0")}:${date.getMinutes().toString().padStart(2, "0")}:${date.getSeconds().toString().padStart(2, "0")}.${date.getMilliseconds().toString().padStart(3, "0")}`;
|
|
47
|
+
}
|
|
48
|
+
function defaultFormatter(message, options = {}) {
|
|
49
|
+
const inColor = options?.colors ?? isColorSupported;
|
|
50
|
+
const inspectOptions = { ...options, colors: inColor };
|
|
51
|
+
return `[${formatTime(new Date(message.time))}] ${colorizeLevel(message.level, inColor)}: ${formatWithOptions(
|
|
52
|
+
inspectOptions,
|
|
53
|
+
...Array.isArray(message.data) ? message.data : [message.data]
|
|
54
|
+
)}
|
|
55
|
+
`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// src/file-transport.ts
|
|
59
|
+
import fs from "fs";
|
|
60
|
+
import path from "path";
|
|
61
|
+
import { Transform } from "stream";
|
|
62
|
+
import util from "util";
|
|
63
|
+
function FileTransport(options) {
|
|
64
|
+
function defaultFormatter2(message) {
|
|
65
|
+
return logMessageToString(message) + "\n";
|
|
66
|
+
}
|
|
67
|
+
const { filter: filterRaw, format: format2 = defaultFormatter2, filename } = options ?? {};
|
|
68
|
+
const folder = path.parse(filename).dir;
|
|
69
|
+
try {
|
|
70
|
+
fs.mkdirSync(folder);
|
|
71
|
+
} catch (e) {
|
|
72
|
+
if (e.code !== "EEXIST") {
|
|
73
|
+
throw e;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const fileStream = fs.createWriteStream(filename, { flags: "a" });
|
|
77
|
+
const filter = parseLogMessageFilter(filterRaw);
|
|
78
|
+
const noContent = Buffer.alloc(0);
|
|
79
|
+
const stream = new Transform({
|
|
80
|
+
objectMode: true,
|
|
81
|
+
transform(message, encoding, callback) {
|
|
82
|
+
if (filter(message)) {
|
|
83
|
+
callback(null, format2(message));
|
|
84
|
+
} else {
|
|
85
|
+
callback(null, noContent);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
stream.pipe(fileStream);
|
|
90
|
+
const transport = {
|
|
91
|
+
write(message) {
|
|
92
|
+
stream.write(message, void 0);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
return {
|
|
96
|
+
...transport,
|
|
97
|
+
async flush() {
|
|
98
|
+
await new Promise((resolve) => {
|
|
99
|
+
if (stream.destroyed) {
|
|
100
|
+
resolve();
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (stream.writableNeedDrain) {
|
|
104
|
+
stream.once("drain", resolve);
|
|
105
|
+
} else {
|
|
106
|
+
resolve();
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
await new Promise((resolve) => {
|
|
110
|
+
if (fileStream.destroyed) {
|
|
111
|
+
resolve();
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (fileStream.writableNeedDrain) {
|
|
115
|
+
fileStream.once("drain", resolve);
|
|
116
|
+
} else {
|
|
117
|
+
resolve();
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
function logMessageToString(obj) {
|
|
124
|
+
try {
|
|
125
|
+
return JSON.stringify(obj);
|
|
126
|
+
} catch {
|
|
127
|
+
return util.formatWithOptions({ compact: true, breakLength: Infinity, depth: Infinity }, obj);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// src/sentry-transport.ts
|
|
132
|
+
import { format } from "util";
|
|
133
|
+
function SentryTransport(options) {
|
|
134
|
+
const { filter: filterRaw, sentry, getCaptureContext } = options ?? {};
|
|
135
|
+
const filter = parseLogMessageFilter(filterRaw);
|
|
136
|
+
return {
|
|
137
|
+
write(message) {
|
|
138
|
+
if (filter(message)) {
|
|
139
|
+
if (message.level === "error" || message.level === "emergency") {
|
|
140
|
+
const exception = message.data.find((d) => d instanceof Error) ?? new Error(format(...message.data));
|
|
141
|
+
sentry.captureException(exception, getCaptureContext(message));
|
|
142
|
+
} else {
|
|
143
|
+
sentry.captureMessage(format(...message.data), getCaptureContext(message));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// src/sentry-breadcrumbs-transport.ts
|
|
151
|
+
import util2 from "util";
|
|
152
|
+
function SentryBreadcrumbsTransport(options) {
|
|
153
|
+
const { filter: filterRaw, sentry } = options ?? {};
|
|
154
|
+
const filter = parseLogMessageFilter(filterRaw);
|
|
155
|
+
return {
|
|
156
|
+
write(message) {
|
|
157
|
+
if (filter(message)) {
|
|
158
|
+
sentry.addBreadcrumb({
|
|
159
|
+
level: message.level === "emergency" ? "fatal" : message.level === "error" ? "error" : message.level === "warn" ? "warning" : "info",
|
|
160
|
+
category: message.channel ?? void 0,
|
|
161
|
+
message: message.data.length ? util2.format(...message.data) : void 0,
|
|
162
|
+
data: message.data.find((d) => d !== null && typeof d === "object" && !(d instanceof Error)),
|
|
163
|
+
timestamp: Math.floor(new Date(message.time).getTime() / 1e3)
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// src/stub-transport.ts
|
|
171
|
+
function StubTransport(options) {
|
|
172
|
+
const { filter: filterRaw } = options ?? {};
|
|
173
|
+
const transform = options?.transform ?? ((a) => a);
|
|
174
|
+
const filter = parseLogMessageFilter(filterRaw);
|
|
175
|
+
let messages = [];
|
|
176
|
+
return {
|
|
177
|
+
write(message) {
|
|
178
|
+
if (filter(message)) {
|
|
179
|
+
messages.push(transform(message));
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
reset() {
|
|
183
|
+
messages = [];
|
|
184
|
+
},
|
|
185
|
+
listMessages() {
|
|
186
|
+
return messages;
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// src/index.ts
|
|
192
|
+
var defaultConsoleTransport = ConsoleTransport();
|
|
193
|
+
function create(config) {
|
|
194
|
+
let transports = [...config?.transports ?? []];
|
|
195
|
+
function write(message) {
|
|
196
|
+
try {
|
|
197
|
+
if (transports.length) {
|
|
198
|
+
transports.forEach((t) => t.write({ ...message, channel: null }));
|
|
199
|
+
} else {
|
|
200
|
+
defaultConsoleTransport.write({ ...message, channel: null });
|
|
201
|
+
}
|
|
202
|
+
} catch (e) {
|
|
203
|
+
process.stderr.write(Buffer.from(`Failed to log message. ${(e instanceof Error ? e : new Error()).stack}`).toString());
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
function writeHelper(messageRaw) {
|
|
207
|
+
const message = { ...messageRaw, time: (/* @__PURE__ */ new Date()).toISOString() };
|
|
208
|
+
write(message);
|
|
209
|
+
}
|
|
210
|
+
function addTransport(...transportToAdd) {
|
|
211
|
+
transports = [...transports, ...transportToAdd];
|
|
212
|
+
}
|
|
213
|
+
function removeTransport(...transportToRemove) {
|
|
214
|
+
transports = transports.filter((t) => !transportToRemove.includes(t));
|
|
215
|
+
}
|
|
216
|
+
function info(...data) {
|
|
217
|
+
writeHelper({
|
|
218
|
+
data,
|
|
219
|
+
level: "info",
|
|
220
|
+
channel: null
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
function warn(...data) {
|
|
224
|
+
writeHelper({
|
|
225
|
+
data,
|
|
226
|
+
level: "warn",
|
|
227
|
+
channel: null
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
function error(...data) {
|
|
231
|
+
writeHelper({
|
|
232
|
+
data,
|
|
233
|
+
level: "error",
|
|
234
|
+
channel: null
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
function emergency(...data) {
|
|
238
|
+
writeHelper({
|
|
239
|
+
data,
|
|
240
|
+
level: "emergency",
|
|
241
|
+
channel: null
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
function channel(channel2) {
|
|
245
|
+
function write2(message) {
|
|
246
|
+
try {
|
|
247
|
+
if (transports.length) {
|
|
248
|
+
transports.forEach((t) => t.write({ ...message, channel: channel2 }));
|
|
249
|
+
} else {
|
|
250
|
+
defaultConsoleTransport.write({ ...message, channel: channel2 });
|
|
251
|
+
}
|
|
252
|
+
} catch (e) {
|
|
253
|
+
process.stderr.write(
|
|
254
|
+
Buffer.from(`Failed to log message. ${(e instanceof Error ? e : new Error()).stack}`).toString()
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
function writeHelper2(messageRaw) {
|
|
259
|
+
const message = { ...messageRaw, time: (/* @__PURE__ */ new Date()).toISOString() };
|
|
260
|
+
write2(message);
|
|
261
|
+
}
|
|
262
|
+
function info2(...data) {
|
|
263
|
+
writeHelper2({
|
|
264
|
+
data,
|
|
265
|
+
level: "info"
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
function warn2(...data) {
|
|
269
|
+
writeHelper2({
|
|
270
|
+
data,
|
|
271
|
+
level: "warn"
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
function error2(...data) {
|
|
275
|
+
writeHelper2({
|
|
276
|
+
data,
|
|
277
|
+
level: "error"
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
function emergency2(...data) {
|
|
281
|
+
writeHelper2({
|
|
282
|
+
data,
|
|
283
|
+
level: "emergency"
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
return { info: info2, warn: warn2, error: error2, emergency: emergency2, write: write2 };
|
|
287
|
+
}
|
|
288
|
+
return { info, warn, error, emergency, write, channel, addTransport, removeTransport };
|
|
289
|
+
}
|
|
290
|
+
function overrideNodeJsConsole(logger) {
|
|
291
|
+
const original = {
|
|
292
|
+
debug: console.debug,
|
|
293
|
+
log: console.log,
|
|
294
|
+
info: console.info,
|
|
295
|
+
warn: console.warn,
|
|
296
|
+
error: console.error
|
|
297
|
+
};
|
|
298
|
+
console.debug = logger.info;
|
|
299
|
+
console.log = logger.info;
|
|
300
|
+
console.info = logger.info;
|
|
301
|
+
console.warn = logger.warn;
|
|
302
|
+
console.error = logger.error;
|
|
303
|
+
return function restoreNodeJsConsole() {
|
|
304
|
+
console.debug = original.debug;
|
|
305
|
+
console.log = original.log;
|
|
306
|
+
console.info = original.info;
|
|
307
|
+
console.warn = original.warn;
|
|
308
|
+
console.error = original.error;
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
var defaultLogger = create();
|
|
312
|
+
var Log = {
|
|
313
|
+
create,
|
|
314
|
+
overrideNodeJsConsole,
|
|
315
|
+
write: defaultLogger.write,
|
|
316
|
+
info: defaultLogger.info,
|
|
317
|
+
warn: defaultLogger.warn,
|
|
318
|
+
error: defaultLogger.error,
|
|
319
|
+
emergency: defaultLogger.emergency,
|
|
320
|
+
addTransport: defaultLogger.addTransport,
|
|
321
|
+
removeTransport: defaultLogger.removeTransport,
|
|
322
|
+
channel: defaultLogger.channel,
|
|
323
|
+
transports: {
|
|
324
|
+
console: ConsoleTransport,
|
|
325
|
+
file: FileTransport,
|
|
326
|
+
sentry: SentryTransport,
|
|
327
|
+
sentryBreadcrumbs: SentryBreadcrumbsTransport,
|
|
328
|
+
silence: {
|
|
329
|
+
write() {
|
|
330
|
+
}
|
|
331
|
+
},
|
|
332
|
+
stub: StubTransport
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
export {
|
|
336
|
+
Log
|
|
337
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@maestro-js/log",
|
|
3
|
+
"description": "Structured logging with pluggable transports and channel-based routing for the Maestro framework. Use when working with @maestro-js/log, adding or modifying log transports, configuring log channels, filtering log messages by level or channel, integrating logging into other packages, or writing tests that capture log output. Key capabilities: create loggers with pluggable transports (console, file, Sentry, stub), route messages through named channels, filter by level and channel, override Node.js console, custom formatters, and the silence transport for suppressing output.",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"exports": {
|
|
6
|
+
".": {
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"default": "./dist/index.js"
|
|
9
|
+
}
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"colorette": "^2.0.20",
|
|
13
|
+
"iso-fns2": "npm:iso-fns@2.0.0-alpha.26"
|
|
14
|
+
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"@sentry/types": "^8.30.0",
|
|
17
|
+
"@types/node": "^22.19.11"
|
|
18
|
+
},
|
|
19
|
+
"version": "1.0.0-alpha.0",
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "restricted"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"dist"
|
|
25
|
+
],
|
|
26
|
+
"license": "UNLICENSED",
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=22.18.0"
|
|
29
|
+
},
|
|
30
|
+
"repository": "https://github.com/Marcato-Partners/maestro-js",
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build": "tsup --config ../../tsup.config.ts",
|
|
33
|
+
"typecheck": "tsc --noEmit",
|
|
34
|
+
"test": "beartest ./tests/**/*",
|
|
35
|
+
"format": "prettier --write src/ tests/",
|
|
36
|
+
"lint": "prettier --check src/ tests/"
|
|
37
|
+
}
|
|
38
|
+
}
|