@dws-std/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.md +21 -0
- package/README.md +351 -0
- package/dist/events/logger-events.d.ts +21 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +448 -0
- package/dist/logger.d.ts +213 -0
- package/dist/sinks/console-logger.d.ts +8 -0
- package/dist/sinks/devnull-logger.d.ts +8 -0
- package/dist/sinks/file-logger.d.ts +9 -0
- package/dist/types/log-levels.d.ts +1 -0
- package/dist/types/logger-sink.d.ts +36 -0
- package/dist/types/sink-bodies-intersection.d.ts +2 -0
- package/dist/types/sink-body.d.ts +2 -0
- package/dist/types/sink-factory.d.ts +12 -0
- package/dist/types/sink-map.d.ts +2 -0
- package/dist/worker-logger.d.ts +1 -0
- package/package.json +57 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dominus Web Services (DWS)
|
|
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,351 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://cdn.jsdelivr.net/gh/Dominus-Web-Service/std@main/packages/logger/logo-logger.png" alt="DWS Logger logo" width="200" />
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
# đ¯ DWS Logger
|
|
6
|
+
|
|
7
|
+
Logging in Bun often means choosing between "fast but dumb" or "smart but blocking".
|
|
8
|
+
`@dws-std/logger` gives you both: a type-safe, sink-based system that never blocks your main thread.
|
|
9
|
+
|
|
10
|
+
## Why this package?
|
|
11
|
+
|
|
12
|
+
The goal is simple: **Stop your logs from slowing down your app.**
|
|
13
|
+
|
|
14
|
+
Most loggers either block on every write or lose type safety when you need structured logging.
|
|
15
|
+
This package runs everything in a worker thread, batches automatically, and still gives you full TypeScript inference on what you log.
|
|
16
|
+
|
|
17
|
+
## đ Table of Contents
|
|
18
|
+
|
|
19
|
+
- [Features](#-features)
|
|
20
|
+
- [Installation](#-installation)
|
|
21
|
+
- [Usage](#-usage)
|
|
22
|
+
- [Custom Sinks](#-custom-sinks)
|
|
23
|
+
- [Type-Safe Logging](#-type-safe-logging)
|
|
24
|
+
- [Error Handling](#-error-handling)
|
|
25
|
+
- [Flushing and Closing](#-flushing-and-closing)
|
|
26
|
+
- [Configuration](#-configuration)
|
|
27
|
+
- [API Reference](#-api-reference)
|
|
28
|
+
- [License](#-license)
|
|
29
|
+
- [Contact](#-contact)
|
|
30
|
+
|
|
31
|
+
## ⨠Features
|
|
32
|
+
|
|
33
|
+
- ⥠**Zero Blocking** : Every log goes through a worker thread â your main loop stays fast.
|
|
34
|
+
- đ **Type-Safe** : TypeScript infers the shape of your logs. No more `any` everywhere.
|
|
35
|
+
- đ¯ **Sink Pattern** : Route logs to console, file, database, or your own custom destination.
|
|
36
|
+
- đ **Smart Batching** : Logs are grouped automatically for better I/O performance.
|
|
37
|
+
- đ **Event-Driven** : Listen to flush, close, and error events when you need them.
|
|
38
|
+
|
|
39
|
+
## đ§ Installation
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
bun add @dws-std/logger
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## âī¸ Usage
|
|
46
|
+
|
|
47
|
+
### Basic Setup
|
|
48
|
+
|
|
49
|
+
Create a logger, attach a sink, and start logging:
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
import { Logger, consoleSink } from '@dws-std/logger';
|
|
53
|
+
|
|
54
|
+
// Create a logger and register a console sink
|
|
55
|
+
const logger = new Logger().registerSink('console', consoleSink);
|
|
56
|
+
|
|
57
|
+
// Log messages (always pass an object)
|
|
58
|
+
logger.info({ message: 'Application started' });
|
|
59
|
+
logger.warn({ message: 'This is a warning' });
|
|
60
|
+
logger.error({ message: 'An error occurred', code: 500 });
|
|
61
|
+
logger.debug({ action: 'debug_info', data: { foo: 'bar' } });
|
|
62
|
+
logger.log({ event: 'generic_log' });
|
|
63
|
+
|
|
64
|
+
// Close the logger when done
|
|
65
|
+
await logger.close();
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
> âšī¸ Sinks are **factory functions**, not classes. You pass the factory itself (not the
|
|
69
|
+
> result of calling it) to `registerSink`. The worker re-evaluates the factory string
|
|
70
|
+
> and calls it with the `sinkArgs` you forwarded, so the sink is built **inside** the worker.
|
|
71
|
+
|
|
72
|
+
### Multiple Sinks
|
|
73
|
+
|
|
74
|
+
Need logs going to different places? Register as many sinks as you want:
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
import { Logger, consoleSink, fileSink } from '@dws-std/logger';
|
|
78
|
+
|
|
79
|
+
// Register multiple sinks
|
|
80
|
+
const logger = new Logger()
|
|
81
|
+
.registerSink('console', consoleSink)
|
|
82
|
+
.registerSink('file', fileSink, './app.log');
|
|
83
|
+
|
|
84
|
+
// Log to all sinks
|
|
85
|
+
logger.info({ message: 'This goes to console and file' });
|
|
86
|
+
|
|
87
|
+
// Log to specific sinks only
|
|
88
|
+
logger.error({ message: 'Only in file' }, ['file']);
|
|
89
|
+
logger.warn({ message: 'Only in console' }, ['console']);
|
|
90
|
+
|
|
91
|
+
await logger.close();
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Built-in Sinks
|
|
95
|
+
|
|
96
|
+
| Factory | Args | Description |
|
|
97
|
+
| ------------ | ----------- | --------------------------------------------------------- |
|
|
98
|
+
| `consoleSink`| _none_ | Writes JSON log entries to `console` (routed by level). |
|
|
99
|
+
| `fileSink` | `path` | Appends JSON log entries to a file via `Bun.FileSink`. |
|
|
100
|
+
| `devNullSink`| _none_ | Discards everything â useful for benchmarks / dry runs. |
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
import { Logger, devNullSink, fileSink } from '@dws-std/logger';
|
|
104
|
+
|
|
105
|
+
const logger = new Logger()
|
|
106
|
+
.registerSink('applog', fileSink, './app.log')
|
|
107
|
+
.registerSink('silent', devNullSink);
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## đ ī¸ Custom Sinks
|
|
111
|
+
|
|
112
|
+
A sink is a plain object implementing the `LoggerSink` interface â register a **factory**
|
|
113
|
+
function that builds and returns it. The factory is stringify-ed and re-evaluated inside the
|
|
114
|
+
worker, so its body must be **self-contained**: it may use its arguments, runtime globals
|
|
115
|
+
(`Bun`, `console`, `JSON`, âĻ) and dynamic `import()`, but **must not** close over
|
|
116
|
+
module-scoped imports or variables from the calling file.
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
import { Logger, type SinkFactory } from '@dws-std/logger';
|
|
120
|
+
|
|
121
|
+
// A self-contained factory: no module-scoped imports captured.
|
|
122
|
+
const databaseSink: SinkFactory<{ query: string }, [dbUrl: string]> = (dbUrl: string) => {
|
|
123
|
+
// Open the connection inside the factory â the worker owns it.
|
|
124
|
+
const connection = /* âĻopen using `dbUrl`âĻ */ {} as unknown;
|
|
125
|
+
return {
|
|
126
|
+
async log(level, timestamp, object) {
|
|
127
|
+
// object is typed as { query: string }
|
|
128
|
+
await (connection as { write: (s: string) => Promise<void> }).write(
|
|
129
|
+
JSON.stringify({ level, timestamp, object })
|
|
130
|
+
);
|
|
131
|
+
},
|
|
132
|
+
async close() {
|
|
133
|
+
await (connection as { close: () => Promise<void> }).close();
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const logger = new Logger().registerSink('database', databaseSink, 'postgres://localhost/app');
|
|
139
|
+
|
|
140
|
+
logger.info({ query: 'SELECT 1' });
|
|
141
|
+
await logger.close();
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### The `LoggerSink` interface
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
interface LoggerSink<TLogObject = unknown> {
|
|
148
|
+
log(level: LogLevels, timestamp: number, object: TLogObject): Promise<void> | void;
|
|
149
|
+
flush?(): Promise<void> | void; // called by Logger.flush()
|
|
150
|
+
close?(): Promise<void> | void; // called on Logger.close()
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
- `log` is the only required method.
|
|
155
|
+
- `flush` is optional â implement it when your sink buffers writes and you want `logger.flush()` to push them through.
|
|
156
|
+
- `close` is optional â implement it to release file handles, connections, etc.
|
|
157
|
+
|
|
158
|
+
## đ Type-Safe Logging
|
|
159
|
+
|
|
160
|
+
When you define typed sinks, TypeScript knows exactly what shape your logs need. No more
|
|
161
|
+
guessing, no more runtime surprises.
|
|
162
|
+
|
|
163
|
+
### Single Typed Sink
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
import { Logger, type LoggerSink, type LogLevels, type SinkFactory } from '@dws-std/logger';
|
|
167
|
+
|
|
168
|
+
interface UserLog {
|
|
169
|
+
userId: number;
|
|
170
|
+
action: string;
|
|
171
|
+
timestamp?: Date;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Typed factory: the returned sink only accepts UserLog objects.
|
|
175
|
+
const userLogSink: SinkFactory<UserLog> = () => ({
|
|
176
|
+
log(level: LogLevels, timestamp: number, object: UserLog): void {
|
|
177
|
+
console.log(`User ${object.userId} performed: ${object.action}`);
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
const logger = new Logger().registerSink('userLog', userLogSink);
|
|
182
|
+
|
|
183
|
+
// â
TypeScript requires the correct shape
|
|
184
|
+
logger.info({ userId: 123, action: 'login' });
|
|
185
|
+
|
|
186
|
+
// â TypeScript error: Missing required property 'action'
|
|
187
|
+
logger.info({ userId: 123 });
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### Multiple Typed Sinks
|
|
191
|
+
|
|
192
|
+
When logging to multiple sinks at once, TypeScript creates an intersection of all the
|
|
193
|
+
targeted sinks' types â you must satisfy every one of them.
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
interface UserLog {
|
|
197
|
+
userId: number;
|
|
198
|
+
action: string;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
interface ApiLog {
|
|
202
|
+
endpoint: string;
|
|
203
|
+
method: string;
|
|
204
|
+
statusCode: number;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const userLogSink: SinkFactory<UserLog> = () => ({
|
|
208
|
+
async log(_level, _ts, object) {
|
|
209
|
+
// âĻ persist object âĻ
|
|
210
|
+
void object;
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
const apiLogSink: SinkFactory<ApiLog> = () => ({
|
|
215
|
+
async log(_level, _ts, object) {
|
|
216
|
+
// âĻ persist object âĻ
|
|
217
|
+
void object;
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
const logger = new Logger()
|
|
222
|
+
.registerSink('user', userLogSink)
|
|
223
|
+
.registerSink('api', apiLogSink);
|
|
224
|
+
|
|
225
|
+
// â
Logging to both sinks requires BOTH types combined
|
|
226
|
+
logger.info(
|
|
227
|
+
{
|
|
228
|
+
userId: 123,
|
|
229
|
+
action: 'api_call',
|
|
230
|
+
endpoint: '/users',
|
|
231
|
+
method: 'POST',
|
|
232
|
+
statusCode: 201
|
|
233
|
+
},
|
|
234
|
+
['user', 'api']
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
// â
Logging to only one sink requires only that sink's type
|
|
238
|
+
logger.warn({ userId: 456, action: 'failed_attempt' }, ['user']);
|
|
239
|
+
|
|
240
|
+
// â TypeScript error: Missing api properties
|
|
241
|
+
logger.error({ userId: 789, action: 'error' }, ['user', 'api']);
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### Mixing Typed and Untyped Sinks
|
|
245
|
+
|
|
246
|
+
When you mix typed sinks with untyped ones (like `consoleSink`, which accepts `unknown`),
|
|
247
|
+
things stay flexible: the intersection with `unknown` lets extra properties through.
|
|
248
|
+
|
|
249
|
+
```ts
|
|
250
|
+
import { Logger, consoleSink, type SinkFactory } from '@dws-std/logger';
|
|
251
|
+
|
|
252
|
+
interface DatabaseLog {
|
|
253
|
+
query: string;
|
|
254
|
+
duration: number;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const databaseLogSink: SinkFactory<DatabaseLog> = () => ({
|
|
258
|
+
async log(_level, _ts, object) {
|
|
259
|
+
// âĻ persist object âĻ
|
|
260
|
+
void object;
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
const logger = new Logger()
|
|
265
|
+
.registerSink('database', databaseLogSink)
|
|
266
|
+
.registerSink('console', consoleSink); // accepts unknown
|
|
267
|
+
|
|
268
|
+
// â
Works â the database type is enforced, console accepts anything
|
|
269
|
+
logger.info(
|
|
270
|
+
{
|
|
271
|
+
query: 'SELECT * FROM users',
|
|
272
|
+
duration: 123,
|
|
273
|
+
customData: 'anything goes'
|
|
274
|
+
},
|
|
275
|
+
['database', 'console']
|
|
276
|
+
);
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
## đ¨ Error Handling
|
|
280
|
+
|
|
281
|
+
Things break. When they do, you'll want to know:
|
|
282
|
+
|
|
283
|
+
```ts
|
|
284
|
+
import { Logger, consoleSink } from '@dws-std/logger';
|
|
285
|
+
|
|
286
|
+
const logger = new Logger().registerSink('console', consoleSink);
|
|
287
|
+
|
|
288
|
+
// Listen for sink errors (a sink throwing inside the worker)
|
|
289
|
+
logger.addListener('sinkError', (error) => {
|
|
290
|
+
console.error('Logger error:', error.message);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
// Listen for sink registration errors (factory failed to build inside the worker)
|
|
294
|
+
logger.addListener('registerSinkError', (error) => {
|
|
295
|
+
console.error('Failed to register sink:', error.message);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
logger.info({ message: 'Safe to log' });
|
|
299
|
+
await logger.close();
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
## đ§š Flushing and Closing
|
|
303
|
+
|
|
304
|
+
When you need to make sure everything is written before shutting down:
|
|
305
|
+
|
|
306
|
+
```ts
|
|
307
|
+
import { Logger, consoleSink } from '@dws-std/logger';
|
|
308
|
+
|
|
309
|
+
const logger = new Logger().registerSink('console', consoleSink);
|
|
310
|
+
|
|
311
|
+
logger.info({ message: 'First message' });
|
|
312
|
+
logger.info({ message: 'Second message' });
|
|
313
|
+
|
|
314
|
+
// Wait for all pending logs to be processed
|
|
315
|
+
await logger.flush();
|
|
316
|
+
|
|
317
|
+
// Close the logger and release resources (internally calls flush)
|
|
318
|
+
await logger.close();
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
`flush()` drains both the in-memory queue **and** each sink's own buffer (via the
|
|
322
|
+
optional `flush()` method on `LoggerSink`).
|
|
323
|
+
|
|
324
|
+
## âī¸ Configuration
|
|
325
|
+
|
|
326
|
+
Fine-tune the batching and queue behavior:
|
|
327
|
+
|
|
328
|
+
```ts
|
|
329
|
+
import { Logger, consoleSink } from '@dws-std/logger';
|
|
330
|
+
|
|
331
|
+
const logger = new Logger({
|
|
332
|
+
maxPendingLogs: 10_000, // Max queued logs (default: 10,000)
|
|
333
|
+
batchSize: 100, // Logs per batch (default: 100)
|
|
334
|
+
batchTimeout: 0.1, // Ms before flushing a partial batch (default: 0.1)
|
|
335
|
+
maxMessagesInFlight: 100, // Max batches being processed (default: 100)
|
|
336
|
+
autoEnd: true, // Auto-close on process exit (default: true)
|
|
337
|
+
flushOnBeforeExit: true // Flush before exit (default: true)
|
|
338
|
+
}).registerSink('console', consoleSink);
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
## đ API Reference
|
|
342
|
+
|
|
343
|
+
Full docs: [https://dominus-web-service.github.io/std/](https://dominus-web-service.github.io/std/)
|
|
344
|
+
|
|
345
|
+
## âī¸ License
|
|
346
|
+
|
|
347
|
+
MIT - Feel free to use it.
|
|
348
|
+
|
|
349
|
+
## đ§ Contact
|
|
350
|
+
|
|
351
|
+
- GitHub: [Dominus-Web-Service](https://github.com/Dominus-Web-Service/packages)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { EventMap } from '@dws-std/common';
|
|
2
|
+
import type { Exception } from '@dws-std/error';
|
|
3
|
+
export interface LoggerEvent extends EventMap {
|
|
4
|
+
onBeforeExitError: [Exception<{
|
|
5
|
+
error: Error;
|
|
6
|
+
}>];
|
|
7
|
+
registerSinkError: [
|
|
8
|
+
Exception<{
|
|
9
|
+
sinkName: string;
|
|
10
|
+
error: Error;
|
|
11
|
+
}>
|
|
12
|
+
];
|
|
13
|
+
sinkError: [
|
|
14
|
+
Exception<{
|
|
15
|
+
sinkName: string;
|
|
16
|
+
object?: unknown;
|
|
17
|
+
error: Error;
|
|
18
|
+
}>
|
|
19
|
+
];
|
|
20
|
+
drained: [];
|
|
21
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { LOGGER_ERROR_KEYS, Logger, type LoggerOptions } from './logger';
|
|
2
|
+
export { consoleSink } from './sinks/console-logger';
|
|
3
|
+
export { devNullSink } from './sinks/devnull-logger';
|
|
4
|
+
export { fileSink } from './sinks/file-logger';
|
|
5
|
+
export type { LogLevels } from './types/log-levels';
|
|
6
|
+
export type { LoggerSink } from './types/logger-sink';
|
|
7
|
+
export type { SinkFactory } from './types/sink-factory';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/logger.ts
|
|
3
|
+
import { TypedEventEmitter } from "@dws-std/common";
|
|
4
|
+
import { Exception } from "@dws-std/error";
|
|
5
|
+
|
|
6
|
+
// src/worker-logger.ts
|
|
7
|
+
var workerFunction = () => {
|
|
8
|
+
const sinks = {};
|
|
9
|
+
const self = globalThis;
|
|
10
|
+
const processLogEntry = (log) => {
|
|
11
|
+
const { sinkNames, level, timestamp, object } = log;
|
|
12
|
+
const len = sinkNames.length;
|
|
13
|
+
let promises;
|
|
14
|
+
for (let i = 0;i < len; ++i) {
|
|
15
|
+
const sinkName = sinkNames[i];
|
|
16
|
+
if (sinkName === undefined)
|
|
17
|
+
continue;
|
|
18
|
+
const sink = sinks[sinkName];
|
|
19
|
+
if (!sink)
|
|
20
|
+
continue;
|
|
21
|
+
try {
|
|
22
|
+
const result = sink.log(level, timestamp, object);
|
|
23
|
+
if (result instanceof Promise) {
|
|
24
|
+
const guarded = result.catch((error) => {
|
|
25
|
+
self.postMessage({
|
|
26
|
+
type: "SINK_LOG_ERROR",
|
|
27
|
+
sinkName,
|
|
28
|
+
error,
|
|
29
|
+
object
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
(promises ??= []).push(guarded);
|
|
33
|
+
}
|
|
34
|
+
} catch (error) {
|
|
35
|
+
self.postMessage({
|
|
36
|
+
type: "SINK_LOG_ERROR",
|
|
37
|
+
sinkName,
|
|
38
|
+
error,
|
|
39
|
+
object
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return promises === undefined ? undefined : Promise.all(promises).then(() => {
|
|
44
|
+
return;
|
|
45
|
+
});
|
|
46
|
+
};
|
|
47
|
+
self.addEventListener("message", (event) => {
|
|
48
|
+
switch (event.data.type) {
|
|
49
|
+
case "REGISTER_SINK": {
|
|
50
|
+
const { sinkName, sinkFactoryString, sinkArgs } = event.data;
|
|
51
|
+
try {
|
|
52
|
+
const create = new Function("sinkArgs", `return (${sinkFactoryString})(...sinkArgs);`);
|
|
53
|
+
sinks[sinkName] = create(sinkArgs);
|
|
54
|
+
} catch (error) {
|
|
55
|
+
self.postMessage({
|
|
56
|
+
type: "REGISTER_SINK_ERROR",
|
|
57
|
+
sinkName,
|
|
58
|
+
error
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
case "LOG_BATCH": {
|
|
64
|
+
const { logs } = event.data;
|
|
65
|
+
const len = logs.length;
|
|
66
|
+
let pending;
|
|
67
|
+
for (let i = 0;i < len; ++i) {
|
|
68
|
+
const result = processLogEntry(logs[i]);
|
|
69
|
+
if (result !== undefined)
|
|
70
|
+
(pending ??= []).push(result);
|
|
71
|
+
}
|
|
72
|
+
if (pending === undefined)
|
|
73
|
+
self.postMessage({ type: "BATCH_COMPLETE" });
|
|
74
|
+
else
|
|
75
|
+
Promise.all(pending).then(() => self.postMessage({ type: "BATCH_COMPLETE" }));
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
case "FLUSH": {
|
|
79
|
+
let pending;
|
|
80
|
+
for (const [sinkName, sink] of Object.entries(sinks))
|
|
81
|
+
try {
|
|
82
|
+
const result = sink.flush?.();
|
|
83
|
+
if (result instanceof Promise) {
|
|
84
|
+
const guarded = result.catch((error) => {
|
|
85
|
+
self.postMessage({
|
|
86
|
+
type: "SINK_LOG_ERROR",
|
|
87
|
+
sinkName,
|
|
88
|
+
error,
|
|
89
|
+
object: undefined
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
(pending ??= []).push(guarded);
|
|
93
|
+
}
|
|
94
|
+
} catch (error) {
|
|
95
|
+
self.postMessage({
|
|
96
|
+
type: "SINK_LOG_ERROR",
|
|
97
|
+
sinkName,
|
|
98
|
+
error,
|
|
99
|
+
object: undefined
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
if (pending === undefined)
|
|
103
|
+
self.postMessage({ type: "FLUSH_COMPLETE" });
|
|
104
|
+
else
|
|
105
|
+
Promise.all(pending).then(() => self.postMessage({ type: "FLUSH_COMPLETE" }));
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
case "CLOSE": {
|
|
109
|
+
const entries = Object.entries(sinks);
|
|
110
|
+
for (const [name, sink] of entries)
|
|
111
|
+
try {
|
|
112
|
+
sink.close?.();
|
|
113
|
+
} catch (error) {
|
|
114
|
+
self.postMessage({
|
|
115
|
+
type: "SINK_CLOSE_ERROR",
|
|
116
|
+
sinkName: name,
|
|
117
|
+
error
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
self.postMessage({ type: "CLOSE_COMPLETE" });
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
default:
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
// src/logger.ts
|
|
130
|
+
var LOGGER_ERROR_KEYS = {
|
|
131
|
+
SINK_ALREADY_ADDED: "logger.sink-already-added",
|
|
132
|
+
NO_SINKS_PROVIDED: "logger.no-sinks-provided",
|
|
133
|
+
SINK_LOG_ERROR: "logger.sink-log-error",
|
|
134
|
+
SINK_CLOSE_ERROR: "logger.sink-close-error",
|
|
135
|
+
REGISTER_SINK_ERROR: "logger.register-sink-error",
|
|
136
|
+
BEFORE_EXIT_FLUSH_ERROR: "logger.before-exit-flush-error",
|
|
137
|
+
BEFORE_EXIT_CLOSE_ERROR: "logger.before-exit-close-error"
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
class Logger extends TypedEventEmitter {
|
|
141
|
+
sinks;
|
|
142
|
+
sinkKeys = [];
|
|
143
|
+
worker;
|
|
144
|
+
workerUrl;
|
|
145
|
+
maxPendingLogs;
|
|
146
|
+
maxMessagesInFlight;
|
|
147
|
+
batchSize;
|
|
148
|
+
batchTimeout;
|
|
149
|
+
autoEnd;
|
|
150
|
+
flushOnBeforeExit;
|
|
151
|
+
pendingLogs = [];
|
|
152
|
+
pendingHead = 0;
|
|
153
|
+
messagesInFlight = 0;
|
|
154
|
+
batchTimer = null;
|
|
155
|
+
isWriting = false;
|
|
156
|
+
flushResolvers = [];
|
|
157
|
+
flushingResolvers = [];
|
|
158
|
+
flushPending = false;
|
|
159
|
+
closeResolver = null;
|
|
160
|
+
backpressureResolver = null;
|
|
161
|
+
workerTerminated = false;
|
|
162
|
+
handleExit = () => {
|
|
163
|
+
this.terminateWorker();
|
|
164
|
+
};
|
|
165
|
+
handleWorkerClose = () => {
|
|
166
|
+
process.off("beforeExit", this.handleBeforeExit);
|
|
167
|
+
process.off("exit", this.handleExit);
|
|
168
|
+
};
|
|
169
|
+
constructor({
|
|
170
|
+
autoEnd = true,
|
|
171
|
+
batchSize = 100,
|
|
172
|
+
batchTimeout = 0.1,
|
|
173
|
+
flushOnBeforeExit = true,
|
|
174
|
+
maxMessagesInFlight = 100,
|
|
175
|
+
maxPendingLogs = 1e4
|
|
176
|
+
} = {}) {
|
|
177
|
+
super();
|
|
178
|
+
this.sinks = {};
|
|
179
|
+
this.maxPendingLogs = maxPendingLogs;
|
|
180
|
+
this.maxMessagesInFlight = maxMessagesInFlight;
|
|
181
|
+
this.batchSize = batchSize;
|
|
182
|
+
this.batchTimeout = batchTimeout;
|
|
183
|
+
this.autoEnd = autoEnd;
|
|
184
|
+
this.flushOnBeforeExit = flushOnBeforeExit;
|
|
185
|
+
this.workerUrl = URL.createObjectURL(new Blob([`(${workerFunction.toString()})()`], { type: "application/javascript" }));
|
|
186
|
+
this.worker = new Worker(this.workerUrl, { type: "module" });
|
|
187
|
+
this.setupWorkerMessages();
|
|
188
|
+
if (this.autoEnd)
|
|
189
|
+
this.setupAutoEnd();
|
|
190
|
+
}
|
|
191
|
+
registerSink(sinkName, sinkFactory, ...sinkArgs) {
|
|
192
|
+
if (this.sinks[sinkName] !== undefined)
|
|
193
|
+
throw new Exception("Sink is already registered", {
|
|
194
|
+
key: LOGGER_ERROR_KEYS.SINK_ALREADY_ADDED,
|
|
195
|
+
cause: { sinkName }
|
|
196
|
+
});
|
|
197
|
+
this.worker.postMessage({
|
|
198
|
+
type: "REGISTER_SINK",
|
|
199
|
+
sinkName,
|
|
200
|
+
sinkFactoryString: sinkFactory.toString(),
|
|
201
|
+
sinkArgs
|
|
202
|
+
});
|
|
203
|
+
this.sinks[sinkName] = sinkFactory;
|
|
204
|
+
this.sinkKeys.push(sinkName);
|
|
205
|
+
return this;
|
|
206
|
+
}
|
|
207
|
+
error(object, sinkNames = this.sinkKeys) {
|
|
208
|
+
this.enqueue("ERROR", object, sinkNames);
|
|
209
|
+
}
|
|
210
|
+
warn(object, sinkNames = this.sinkKeys) {
|
|
211
|
+
this.enqueue("WARN", object, sinkNames);
|
|
212
|
+
}
|
|
213
|
+
info(object, sinkNames = this.sinkKeys) {
|
|
214
|
+
this.enqueue("INFO", object, sinkNames);
|
|
215
|
+
}
|
|
216
|
+
debug(object, sinkNames = this.sinkKeys) {
|
|
217
|
+
this.enqueue("DEBUG", object, sinkNames);
|
|
218
|
+
}
|
|
219
|
+
log(object, sinkNames = this.sinkKeys) {
|
|
220
|
+
this.enqueue("LOG", object, sinkNames);
|
|
221
|
+
}
|
|
222
|
+
flush() {
|
|
223
|
+
return new Promise((resolve) => {
|
|
224
|
+
this.flushResolvers.push(resolve);
|
|
225
|
+
this.tryFlush();
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
async close() {
|
|
229
|
+
await this.flush();
|
|
230
|
+
return new Promise((resolve) => {
|
|
231
|
+
this.closeResolver = resolve;
|
|
232
|
+
this.worker.postMessage({ type: "CLOSE" });
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
enqueue(level, object, sinkNames) {
|
|
236
|
+
if (this.sinkKeys.length === 0)
|
|
237
|
+
throw new Exception("No sinks provided", {
|
|
238
|
+
key: LOGGER_ERROR_KEYS.NO_SINKS_PROVIDED,
|
|
239
|
+
cause: { level, object }
|
|
240
|
+
});
|
|
241
|
+
const pendingLength = this.pendingLogs.length - this.pendingHead;
|
|
242
|
+
if (pendingLength >= this.maxPendingLogs)
|
|
243
|
+
return;
|
|
244
|
+
this.pendingLogs.push({
|
|
245
|
+
sinkNames: sinkNames ?? this.sinkKeys,
|
|
246
|
+
level,
|
|
247
|
+
timestamp: Date.now(),
|
|
248
|
+
object
|
|
249
|
+
});
|
|
250
|
+
if (pendingLength + 1 >= this.batchSize) {
|
|
251
|
+
if (this.batchTimer !== null) {
|
|
252
|
+
clearTimeout(this.batchTimer);
|
|
253
|
+
this.batchTimer = null;
|
|
254
|
+
}
|
|
255
|
+
this.triggerProcessing();
|
|
256
|
+
} else if (this.batchTimer === null && this.batchTimeout > 0)
|
|
257
|
+
this.batchTimer = setTimeout(() => {
|
|
258
|
+
this.batchTimer = null;
|
|
259
|
+
this.triggerProcessing();
|
|
260
|
+
}, this.batchTimeout);
|
|
261
|
+
}
|
|
262
|
+
triggerProcessing() {
|
|
263
|
+
if (this.isWriting)
|
|
264
|
+
return;
|
|
265
|
+
this.isWriting = true;
|
|
266
|
+
this.processPendingLogs();
|
|
267
|
+
}
|
|
268
|
+
async processPendingLogs() {
|
|
269
|
+
while (this.pendingLogs.length - this.pendingHead > 0) {
|
|
270
|
+
if (this.messagesInFlight >= this.maxMessagesInFlight)
|
|
271
|
+
await new Promise((resolve) => {
|
|
272
|
+
this.backpressureResolver = resolve;
|
|
273
|
+
});
|
|
274
|
+
const end = Math.min(this.pendingHead + this.batchSize, this.pendingLogs.length);
|
|
275
|
+
const batch = this.pendingLogs.slice(this.pendingHead, end);
|
|
276
|
+
this.pendingHead = end;
|
|
277
|
+
this.reclaimPending();
|
|
278
|
+
this.messagesInFlight++;
|
|
279
|
+
this.worker.postMessage({
|
|
280
|
+
type: "LOG_BATCH",
|
|
281
|
+
logs: batch
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
this.isWriting = false;
|
|
285
|
+
this.emit("drained");
|
|
286
|
+
}
|
|
287
|
+
reclaimPending() {
|
|
288
|
+
if (this.pendingHead === this.pendingLogs.length) {
|
|
289
|
+
this.pendingLogs.length = 0;
|
|
290
|
+
this.pendingHead = 0;
|
|
291
|
+
} else if (this.pendingHead > this.batchSize && this.pendingHead * 2 >= this.pendingLogs.length) {
|
|
292
|
+
this.pendingLogs.splice(0, this.pendingHead);
|
|
293
|
+
this.pendingHead = 0;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
releaseBatch() {
|
|
297
|
+
this.messagesInFlight--;
|
|
298
|
+
if (this.backpressureResolver !== null) {
|
|
299
|
+
this.backpressureResolver();
|
|
300
|
+
this.backpressureResolver = null;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
tryFlush() {
|
|
304
|
+
if (this.flushResolvers.length === 0)
|
|
305
|
+
return;
|
|
306
|
+
if (this.pendingLogs.length - this.pendingHead > 0) {
|
|
307
|
+
if (!this.isWriting) {
|
|
308
|
+
this.isWriting = true;
|
|
309
|
+
this.processPendingLogs();
|
|
310
|
+
}
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
if (this.messagesInFlight === 0 && !this.flushPending) {
|
|
314
|
+
this.flushPending = true;
|
|
315
|
+
this.flushingResolvers.push(...this.flushResolvers);
|
|
316
|
+
this.flushResolvers.length = 0;
|
|
317
|
+
this.worker.postMessage({ type: "FLUSH" });
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
setupWorkerMessages() {
|
|
321
|
+
this.worker.addEventListener("message", (event) => {
|
|
322
|
+
switch (event.data.type) {
|
|
323
|
+
case "BATCH_COMPLETE":
|
|
324
|
+
this.releaseBatch();
|
|
325
|
+
this.tryFlush();
|
|
326
|
+
break;
|
|
327
|
+
case "FLUSH_COMPLETE": {
|
|
328
|
+
this.flushPending = false;
|
|
329
|
+
const resolvers = this.flushingResolvers.splice(0, this.flushingResolvers.length);
|
|
330
|
+
for (const resolve of resolvers)
|
|
331
|
+
resolve();
|
|
332
|
+
this.tryFlush();
|
|
333
|
+
break;
|
|
334
|
+
}
|
|
335
|
+
case "SINK_LOG_ERROR":
|
|
336
|
+
this.emit("sinkError", new Exception("Sink failed to log message", {
|
|
337
|
+
key: LOGGER_ERROR_KEYS.SINK_LOG_ERROR,
|
|
338
|
+
cause: {
|
|
339
|
+
sinkName: event.data.sinkName,
|
|
340
|
+
object: event.data.object,
|
|
341
|
+
error: event.data.error
|
|
342
|
+
}
|
|
343
|
+
}));
|
|
344
|
+
break;
|
|
345
|
+
case "SINK_CLOSE_ERROR":
|
|
346
|
+
this.emit("sinkError", new Exception("Sink failed to close", {
|
|
347
|
+
key: LOGGER_ERROR_KEYS.SINK_CLOSE_ERROR,
|
|
348
|
+
cause: { sinkName: event.data.sinkName, error: event.data.error }
|
|
349
|
+
}));
|
|
350
|
+
break;
|
|
351
|
+
case "REGISTER_SINK_ERROR":
|
|
352
|
+
this.emit("registerSinkError", new Exception("Failed to register sink", {
|
|
353
|
+
key: LOGGER_ERROR_KEYS.REGISTER_SINK_ERROR,
|
|
354
|
+
cause: { sinkName: event.data.sinkName, error: event.data.error }
|
|
355
|
+
}));
|
|
356
|
+
break;
|
|
357
|
+
case "CLOSE_COMPLETE":
|
|
358
|
+
this.terminateWorker();
|
|
359
|
+
if (this.closeResolver) {
|
|
360
|
+
this.closeResolver();
|
|
361
|
+
this.closeResolver = null;
|
|
362
|
+
}
|
|
363
|
+
break;
|
|
364
|
+
default:
|
|
365
|
+
break;
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
terminateWorker() {
|
|
370
|
+
if (this.workerTerminated)
|
|
371
|
+
return;
|
|
372
|
+
this.workerTerminated = true;
|
|
373
|
+
this.worker.terminate();
|
|
374
|
+
URL.revokeObjectURL(this.workerUrl);
|
|
375
|
+
}
|
|
376
|
+
setupAutoEnd() {
|
|
377
|
+
process.on("beforeExit", this.handleBeforeExit);
|
|
378
|
+
process.on("exit", this.handleExit);
|
|
379
|
+
this.worker.addEventListener("close", this.handleWorkerClose);
|
|
380
|
+
}
|
|
381
|
+
handleBeforeExit = () => {
|
|
382
|
+
if (this.flushOnBeforeExit)
|
|
383
|
+
this.flush().then(() => this.close()).catch((error) => {
|
|
384
|
+
this.emit("onBeforeExitError", new Exception("Failed to flush before exit", {
|
|
385
|
+
key: LOGGER_ERROR_KEYS.BEFORE_EXIT_FLUSH_ERROR,
|
|
386
|
+
cause: { error }
|
|
387
|
+
}));
|
|
388
|
+
});
|
|
389
|
+
else
|
|
390
|
+
this.close().catch((error) => {
|
|
391
|
+
this.emit("onBeforeExitError", new Exception("Failed to close before exit", {
|
|
392
|
+
key: LOGGER_ERROR_KEYS.BEFORE_EXIT_CLOSE_ERROR,
|
|
393
|
+
cause: { error }
|
|
394
|
+
}));
|
|
395
|
+
});
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
// src/sinks/console-logger.ts
|
|
399
|
+
var consoleSink = () => ({
|
|
400
|
+
log(level, timestamp, object) {
|
|
401
|
+
const logEntry = { timestamp, level, content: object };
|
|
402
|
+
const logLevel = level.toLowerCase();
|
|
403
|
+
const methods = {
|
|
404
|
+
error: console.error,
|
|
405
|
+
warn: console.warn,
|
|
406
|
+
info: console.info,
|
|
407
|
+
debug: console.debug,
|
|
408
|
+
log: console.log,
|
|
409
|
+
trace: console.trace
|
|
410
|
+
};
|
|
411
|
+
methods[logLevel](JSON.stringify(logEntry));
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
// src/sinks/devnull-logger.ts
|
|
415
|
+
var devNullSink = () => ({
|
|
416
|
+
log() {}
|
|
417
|
+
});
|
|
418
|
+
// src/sinks/file-logger.ts
|
|
419
|
+
var fileSink = (path) => {
|
|
420
|
+
const sink = Bun.file(path).writer();
|
|
421
|
+
let isClosed = false;
|
|
422
|
+
return {
|
|
423
|
+
log(level, timestamp, object) {
|
|
424
|
+
if (isClosed)
|
|
425
|
+
return;
|
|
426
|
+
sink.write(JSON.stringify({ timestamp, level, content: object }) + `
|
|
427
|
+
`);
|
|
428
|
+
},
|
|
429
|
+
async flush() {
|
|
430
|
+
if (isClosed)
|
|
431
|
+
return;
|
|
432
|
+
await sink.flush();
|
|
433
|
+
},
|
|
434
|
+
async close() {
|
|
435
|
+
if (isClosed)
|
|
436
|
+
return;
|
|
437
|
+
isClosed = true;
|
|
438
|
+
await sink.end();
|
|
439
|
+
}
|
|
440
|
+
};
|
|
441
|
+
};
|
|
442
|
+
export {
|
|
443
|
+
fileSink,
|
|
444
|
+
devNullSink,
|
|
445
|
+
consoleSink,
|
|
446
|
+
Logger,
|
|
447
|
+
LOGGER_ERROR_KEYS
|
|
448
|
+
};
|
package/dist/logger.d.ts
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { TypedEventEmitter } from '@dws-std/common';
|
|
2
|
+
import type { LoggerEvent } from './events/logger-events';
|
|
3
|
+
import type { LoggerSink } from './types/logger-sink';
|
|
4
|
+
import type { SinkBodiesIntersection } from './types/sink-bodies-intersection';
|
|
5
|
+
import type { SinkMap } from './types/sink-map';
|
|
6
|
+
/**
|
|
7
|
+
* Configuration options for the {@link Logger} constructor.
|
|
8
|
+
*/
|
|
9
|
+
export interface LoggerOptions {
|
|
10
|
+
/**
|
|
11
|
+
* Maximum number of log messages that can be queued in memory before dropping new logs.
|
|
12
|
+
* @default 10_000
|
|
13
|
+
*/
|
|
14
|
+
maxPendingLogs?: number;
|
|
15
|
+
/**
|
|
16
|
+
* Maximum number of messages that can be sent to the worker without acknowledgment.
|
|
17
|
+
* Controls backpressure to prevent overwhelming the worker thread.
|
|
18
|
+
* @default 100
|
|
19
|
+
*/
|
|
20
|
+
maxMessagesInFlight?: number;
|
|
21
|
+
/**
|
|
22
|
+
* Maximum number of logs to batch together before sending to the worker.
|
|
23
|
+
* Higher values = better throughput but higher latency.
|
|
24
|
+
* @default 100
|
|
25
|
+
*/
|
|
26
|
+
batchSize?: number;
|
|
27
|
+
/**
|
|
28
|
+
* Maximum time in milliseconds to wait before flushing a partial batch.
|
|
29
|
+
* Prevents logs from being delayed indefinitely when batch size is not reached.
|
|
30
|
+
* Set to 0 to disable time-based flushing.
|
|
31
|
+
* @default 0.1 (milliseconds)
|
|
32
|
+
*/
|
|
33
|
+
batchTimeout?: number;
|
|
34
|
+
/**
|
|
35
|
+
* Whether to automatically flush and close the logger when the process exits.
|
|
36
|
+
* When enabled, hooks are installed on `process.on('beforeExit')` and `process.on('exit')`.
|
|
37
|
+
* @default true
|
|
38
|
+
*/
|
|
39
|
+
autoEnd?: boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Whether to flush pending logs before the process exits.
|
|
42
|
+
* Only applies when `autoEnd` is true.
|
|
43
|
+
* @default true
|
|
44
|
+
*/
|
|
45
|
+
flushOnBeforeExit?: boolean;
|
|
46
|
+
}
|
|
47
|
+
export declare const LOGGER_ERROR_KEYS: {
|
|
48
|
+
readonly SINK_ALREADY_ADDED: "logger.sink-already-added";
|
|
49
|
+
readonly NO_SINKS_PROVIDED: "logger.no-sinks-provided";
|
|
50
|
+
readonly SINK_LOG_ERROR: "logger.sink-log-error";
|
|
51
|
+
readonly SINK_CLOSE_ERROR: "logger.sink-close-error";
|
|
52
|
+
readonly REGISTER_SINK_ERROR: "logger.register-sink-error";
|
|
53
|
+
readonly BEFORE_EXIT_FLUSH_ERROR: "logger.before-exit-flush-error";
|
|
54
|
+
readonly BEFORE_EXIT_CLOSE_ERROR: "logger.before-exit-close-error";
|
|
55
|
+
};
|
|
56
|
+
export declare class Logger<TSinks extends SinkMap = Record<never, never>> extends TypedEventEmitter<LoggerEvent> {
|
|
57
|
+
/** Map of registered sinks (logging destinations) */
|
|
58
|
+
private readonly sinks;
|
|
59
|
+
/** List of registered sink keys for quick access */
|
|
60
|
+
private readonly sinkKeys;
|
|
61
|
+
/** Worker instance handling log processing */
|
|
62
|
+
private readonly worker;
|
|
63
|
+
/** Object URL backing the worker module, revoked once the worker has loaded */
|
|
64
|
+
private readonly workerUrl;
|
|
65
|
+
/** Maximum number of pending log messages allowed in the queue */
|
|
66
|
+
private readonly maxPendingLogs;
|
|
67
|
+
/** Maximum number of messages in flight to the worker */
|
|
68
|
+
private readonly maxMessagesInFlight;
|
|
69
|
+
/** Number of logs to batch before sending to worker */
|
|
70
|
+
private readonly batchSize;
|
|
71
|
+
/** Timeout in milliseconds before flushing incomplete batch */
|
|
72
|
+
private readonly batchTimeout;
|
|
73
|
+
/** Whether to auto flush and close on process exit */
|
|
74
|
+
private readonly autoEnd;
|
|
75
|
+
/** Whether to flush before process exit */
|
|
76
|
+
private readonly flushOnBeforeExit;
|
|
77
|
+
/** Backing buffer of pending log messages (consumed from {@link pendingHead}) */
|
|
78
|
+
private readonly pendingLogs;
|
|
79
|
+
/** Index of the first not-yet-dispatched entry in {@link pendingLogs} */
|
|
80
|
+
private pendingHead;
|
|
81
|
+
/** Number of log messages currently being processed by the worker */
|
|
82
|
+
private messagesInFlight;
|
|
83
|
+
/** Timer for batching log messages */
|
|
84
|
+
private batchTimer;
|
|
85
|
+
/** Whether the logger is currently writing log messages to the worker */
|
|
86
|
+
private isWriting;
|
|
87
|
+
/** Resolvers for flush promises awaiting the next flush round */
|
|
88
|
+
private readonly flushResolvers;
|
|
89
|
+
/** Resolvers captured by the flush round currently in flight */
|
|
90
|
+
private readonly flushingResolvers;
|
|
91
|
+
/** Whether a FLUSH round-trip is currently in flight to the worker */
|
|
92
|
+
private flushPending;
|
|
93
|
+
/** Resolver for the close promise */
|
|
94
|
+
private closeResolver;
|
|
95
|
+
/** Resolver for backpressure when maxMessagesInFlight is reached */
|
|
96
|
+
private backpressureResolver;
|
|
97
|
+
/** Whether the worker has already been terminated (guards double teardown) */
|
|
98
|
+
private workerTerminated;
|
|
99
|
+
/** Handle the exit event */
|
|
100
|
+
private readonly handleExit;
|
|
101
|
+
/** Handle the worker close event */
|
|
102
|
+
private readonly handleWorkerClose;
|
|
103
|
+
/**
|
|
104
|
+
* Creates a new Logger instance with the specified options.
|
|
105
|
+
*
|
|
106
|
+
* @param options - Configuration options for the logger
|
|
107
|
+
*/
|
|
108
|
+
constructor({ autoEnd, batchSize, batchTimeout, flushOnBeforeExit, maxMessagesInFlight, maxPendingLogs }?: LoggerOptions);
|
|
109
|
+
/**
|
|
110
|
+
* Registers a new sink (logging destination) with the logger.
|
|
111
|
+
*
|
|
112
|
+
* The factory is serialized and re-evaluated inside the worker, so its body must
|
|
113
|
+
* be self-contained (no module-scoped imports captured by closure).
|
|
114
|
+
*
|
|
115
|
+
* @param sinkName - The name of the sink
|
|
116
|
+
* @param sinkFactory - The factory building the sink
|
|
117
|
+
* @param sinkArgs - The factory arguments
|
|
118
|
+
*
|
|
119
|
+
* @returns The logger instance with new type (for chaining)
|
|
120
|
+
*/
|
|
121
|
+
registerSink<TSinkName extends string, TSink extends LoggerSink, TSinkArgs extends unknown[]>(sinkName: TSinkName, sinkFactory: (...args: TSinkArgs) => TSink, ...sinkArgs: TSinkArgs): Logger<TSinks & Record<TSinkName, (...args: TSinkArgs) => TSink>>;
|
|
122
|
+
/**
|
|
123
|
+
* Logs a message at the ERROR level to the specified sinks.
|
|
124
|
+
*
|
|
125
|
+
* @param object - The log message object
|
|
126
|
+
* @param sinkNames - Optional array of sink names to log to; logs to all sinks if omitted
|
|
127
|
+
*/
|
|
128
|
+
error<SNames extends (keyof TSinks)[] = (keyof TSinks)[]>(object: SinkBodiesIntersection<TSinks, SNames[number]>, sinkNames?: SNames): void;
|
|
129
|
+
/**
|
|
130
|
+
* Logs a message at the WARN level to the specified sinks.
|
|
131
|
+
*
|
|
132
|
+
* @param object - The log message object
|
|
133
|
+
* @param sinkNames - Optional array of sink names to log to; logs to all sinks if omitted
|
|
134
|
+
*/
|
|
135
|
+
warn<SNames extends (keyof TSinks)[] = (keyof TSinks)[]>(object: SinkBodiesIntersection<TSinks, SNames[number]>, sinkNames?: SNames): void;
|
|
136
|
+
/**
|
|
137
|
+
* Logs a message at the INFO level to the specified sinks.
|
|
138
|
+
*
|
|
139
|
+
* @param object - The log message object
|
|
140
|
+
* @param sinkNames - Optional array of sink names to log to; logs to all sinks if omitted
|
|
141
|
+
*/
|
|
142
|
+
info<SNames extends (keyof TSinks)[] = (keyof TSinks)[]>(object: SinkBodiesIntersection<TSinks, SNames[number]>, sinkNames?: SNames): void;
|
|
143
|
+
/**
|
|
144
|
+
* Logs a message at the DEBUG level to the specified sinks.
|
|
145
|
+
*
|
|
146
|
+
* @param object - The log message object
|
|
147
|
+
* @param sinkNames - Optional array of sink names to log to; logs to all sinks if omitted
|
|
148
|
+
*/
|
|
149
|
+
debug<SNames extends (keyof TSinks)[] = (keyof TSinks)[]>(object: SinkBodiesIntersection<TSinks, SNames[number]>, sinkNames?: SNames): void;
|
|
150
|
+
/**
|
|
151
|
+
* Logs a message at the TRACE level to the specified sinks.
|
|
152
|
+
*
|
|
153
|
+
* @param object - The log message object
|
|
154
|
+
* @param sinkNames - Optional array of sink names to log to; logs to all sinks if omitted
|
|
155
|
+
*/
|
|
156
|
+
log<SNames extends (keyof TSinks)[] = (keyof TSinks)[]>(object: SinkBodiesIntersection<TSinks, SNames[number]>, sinkNames?: SNames): void;
|
|
157
|
+
/**
|
|
158
|
+
* Flushes all pending logs and the sinks' buffers, and waits for completion.
|
|
159
|
+
*/
|
|
160
|
+
flush(): Promise<void>;
|
|
161
|
+
/**
|
|
162
|
+
* Closes the logger, flushes pending logs, and releases resources.
|
|
163
|
+
*/
|
|
164
|
+
close(): Promise<void>;
|
|
165
|
+
/**
|
|
166
|
+
* Enqueues a log message to be processed by the worker.
|
|
167
|
+
*
|
|
168
|
+
* @param level - The log level
|
|
169
|
+
* @param object - The log message object
|
|
170
|
+
* @param sinkNames - Optional array of sink names to log to; logs to all sinks if omitted
|
|
171
|
+
*/
|
|
172
|
+
private enqueue;
|
|
173
|
+
/**
|
|
174
|
+
* Triggers processing of pending logs.
|
|
175
|
+
*/
|
|
176
|
+
private triggerProcessing;
|
|
177
|
+
/**
|
|
178
|
+
* Processes pending log messages by sending them to the worker in batches.
|
|
179
|
+
*/
|
|
180
|
+
private processPendingLogs;
|
|
181
|
+
/**
|
|
182
|
+
* Reclaims consumed slots in the pending buffer.
|
|
183
|
+
* Fully resets the buffer when drained, otherwise compacts only once the consumed
|
|
184
|
+
* prefix dominates the backing array, keeping dequeue amortized O(1).
|
|
185
|
+
*/
|
|
186
|
+
private reclaimPending;
|
|
187
|
+
/**
|
|
188
|
+
* Releases a batch by decrementing the in-flight counter and resolving backpressure if needed.
|
|
189
|
+
*/
|
|
190
|
+
private releaseBatch;
|
|
191
|
+
/**
|
|
192
|
+
* Advances the flush state machine: drains pending logs, then asks the worker to
|
|
193
|
+
* flush the sinks' buffers once everything has been dispatched and acknowledged.
|
|
194
|
+
*/
|
|
195
|
+
private tryFlush;
|
|
196
|
+
/**
|
|
197
|
+
* Sets up message handling for the worker.
|
|
198
|
+
*/
|
|
199
|
+
private setupWorkerMessages;
|
|
200
|
+
/**
|
|
201
|
+
* Terminates the worker and releases the object URL backing its module.
|
|
202
|
+
* Idempotent: safe to call from both the close handshake and process exit.
|
|
203
|
+
*/
|
|
204
|
+
private terminateWorker;
|
|
205
|
+
/**
|
|
206
|
+
* Sets up automatic flushing and closing of the logger on process exit.
|
|
207
|
+
*/
|
|
208
|
+
private setupAutoEnd;
|
|
209
|
+
/**
|
|
210
|
+
* Handles the beforeExit event.
|
|
211
|
+
*/
|
|
212
|
+
private readonly handleBeforeExit;
|
|
213
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { LoggerSink } from '#/types/logger-sink';
|
|
2
|
+
/**
|
|
3
|
+
* Creates a sink that writes JSON log entries to the console, routed to the
|
|
4
|
+
* matching `console` method for each level (`error`, `warn`, `info`, âĻ).
|
|
5
|
+
*
|
|
6
|
+
* @typeParam TLogObject - The shape of the objects this sink accepts.
|
|
7
|
+
*/
|
|
8
|
+
export declare const consoleSink: <TLogObject = unknown>() => LoggerSink<TLogObject>;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { LoggerSink } from '#/types/logger-sink';
|
|
2
|
+
/**
|
|
3
|
+
* Creates a sink that discards all logs (like `/dev/null`).
|
|
4
|
+
* Useful for benchmarking the logger overhead without I/O.
|
|
5
|
+
*
|
|
6
|
+
* @typeParam TLogObject - The shape of the objects this sink accepts.
|
|
7
|
+
*/
|
|
8
|
+
export declare const devNullSink: <TLogObject = unknown>() => LoggerSink<TLogObject>;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { LoggerSink } from '#/types/logger-sink';
|
|
2
|
+
/**
|
|
3
|
+
* Creates a sink that appends JSON log entries to a file.
|
|
4
|
+
* Uses `Bun.FileSink` for efficient buffered appending (runs in the worker context).
|
|
5
|
+
*
|
|
6
|
+
* @typeParam TLogObject - The shape of the objects this sink accepts.
|
|
7
|
+
* @param path - The destination file path.
|
|
8
|
+
*/
|
|
9
|
+
export declare const fileSink: <TLogObject = unknown>(path: string) => LoggerSink<TLogObject>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type LogLevels = 'ERROR' | 'WARN' | 'INFO' | 'DEBUG' | 'LOG' | 'TRACE';
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { LogLevels } from './log-levels';
|
|
2
|
+
/**
|
|
3
|
+
* A logging destination.
|
|
4
|
+
*
|
|
5
|
+
* This is the object a {@link SinkFactory} returns. The factory runs inside the
|
|
6
|
+
* logger worker, so the sink closes over its own state (file handles, buffers, âĻ)
|
|
7
|
+
* instead of exposing it. All values the sink needs must be created **inside** the
|
|
8
|
+
* factory body â it cannot close over module-scoped imports (use dynamic `import()`
|
|
9
|
+
* or runtime globals such as `Bun` instead).
|
|
10
|
+
*/
|
|
11
|
+
export interface LoggerSink<TLogObject = unknown, TConfig = unknown> {
|
|
12
|
+
/** Optional configuration for the sink */
|
|
13
|
+
readonly config?: TConfig;
|
|
14
|
+
/**
|
|
15
|
+
* Logs a message with the sink's implementation.
|
|
16
|
+
*
|
|
17
|
+
* @param level - The log level at which the message should be logged.
|
|
18
|
+
* @param timestamp - The epoch timestamp (ms) at which the message was logged.
|
|
19
|
+
* @param object - The object to log.
|
|
20
|
+
*
|
|
21
|
+
* @returns A promise when the sink is asynchronous; it is awaited by {@link Logger.flush}.
|
|
22
|
+
*/
|
|
23
|
+
log(level: LogLevels, timestamp: number, object: TLogObject): Promise<void> | void;
|
|
24
|
+
/**
|
|
25
|
+
* Flushes any buffered data to the underlying destination.
|
|
26
|
+
* Called by {@link Logger.flush}; implement it when your sink buffers writes
|
|
27
|
+
* (e.g. a file writer) and durability on flush matters.
|
|
28
|
+
*/
|
|
29
|
+
flush?(): Promise<void> | void;
|
|
30
|
+
/**
|
|
31
|
+
* Closes the sink and releases its resources.
|
|
32
|
+
* Called when the logger is closing. Optional - implement this if your sink needs
|
|
33
|
+
* cleanup (e.g. closing file handles, database connections).
|
|
34
|
+
*/
|
|
35
|
+
close?(): Promise<void> | void;
|
|
36
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { LoggerSink } from './logger-sink';
|
|
2
|
+
/**
|
|
3
|
+
* A factory that builds a {@link LoggerSink}.
|
|
4
|
+
*
|
|
5
|
+
* The factory is serialized and re-evaluated inside the logger worker, so its body
|
|
6
|
+
* must be self-contained: it can only rely on its arguments, runtime globals, and
|
|
7
|
+
* dynamic `import()` â never on module-scoped imports captured by closure.
|
|
8
|
+
*
|
|
9
|
+
* @typeParam TLogObject - The shape of the objects this sink accepts.
|
|
10
|
+
* @typeParam TArgs - The factory arguments forwarded from `registerSink`.
|
|
11
|
+
*/
|
|
12
|
+
export type SinkFactory<TLogObject = unknown, TArgs extends readonly unknown[] = readonly unknown[]> = (...args: TArgs) => LoggerSink<TLogObject>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const workerFunction: () => void;
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dws-std/logger",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Type-safe logging library for Bun, modular sink pattern, transform streams, and immutable API design.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"bun",
|
|
7
|
+
"dws",
|
|
8
|
+
"open-source",
|
|
9
|
+
"types",
|
|
10
|
+
"typescript",
|
|
11
|
+
"utilities"
|
|
12
|
+
],
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"author": "Dominus Web Services (DWS)",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "https://github.com/Dominus-Web-Service/std",
|
|
18
|
+
"directory": "packages/logger"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist"
|
|
22
|
+
],
|
|
23
|
+
"type": "module",
|
|
24
|
+
"exports": {
|
|
25
|
+
".": {
|
|
26
|
+
"types": "./dist/index.d.ts",
|
|
27
|
+
"default": "./dist/index.js"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "bun builder.ts",
|
|
32
|
+
"docs": "bunx typedoc --tsconfig tsconfig.build.json",
|
|
33
|
+
"fmt:check": "oxfmt --check",
|
|
34
|
+
"fmt": "oxfmt",
|
|
35
|
+
"lint:fix": "oxlint --type-aware --type-check --fix ./src",
|
|
36
|
+
"lint:github": "oxlint --type-aware --type-check --format=github ./src",
|
|
37
|
+
"lint": "oxlint --type-aware --type-check ./src",
|
|
38
|
+
"test": "bun test --pass-with-no-tests --coverage"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@dws-std/common": "workspace:^",
|
|
42
|
+
"@dws-std/error": "workspace:^"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@types/bun": "catalog:base",
|
|
46
|
+
"mitata": "^1.0.34",
|
|
47
|
+
"oxfmt": "catalog:base",
|
|
48
|
+
"oxlint": "catalog:base",
|
|
49
|
+
"oxlint-tsgolint": "catalog:base",
|
|
50
|
+
"pino": "^10.3.1",
|
|
51
|
+
"typescript": "catalog:base"
|
|
52
|
+
},
|
|
53
|
+
"peerDependencies": {
|
|
54
|
+
"@dws-std/common": "workspace:^",
|
|
55
|
+
"@dws-std/error": "workspace:^"
|
|
56
|
+
}
|
|
57
|
+
}
|