@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 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
@@ -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
+ }