@rx-ted/packages-logger 0.0.1

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ben
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,145 @@
1
+ # @rx-ted/logger
2
+
3
+ A lightweight logging library with modular architecture — Logger, Handler, and Formatter separation. Supports multiple handlers, child loggers, and file rotation.
4
+
5
+ ## Features
6
+
7
+ - **Modular Architecture**: Logger → Handler → Formatter separation
8
+ - **Multiple Handlers**: Console and File handlers with independent configuration
9
+ - **Child Loggers**: Inheritance with scope-based namespaces
10
+ - **File Rotation**: Auto-dated filenames with max file count limits
11
+ - **Dual Formatters**: JSON and TEXT formats
12
+ - **Multi-Runtime**: Works with Bun, Node.js, Deno, and Cloudflare Workers
13
+
14
+ ## Install
15
+
16
+ ```bash
17
+ pnpm add @rx-ted/logger
18
+ ```
19
+
20
+ ## Quick Start
21
+
22
+ ```typescript
23
+ import { Logger } from "@rx-ted/logger";
24
+
25
+ const logger = new Logger({ name: "app" });
26
+
27
+ logger.info("Hello World");
28
+ logger.debug("Debug info", { userId: 123 });
29
+ logger.warn("Warning message");
30
+ logger.error("Error occurred", { code: "ERR_001" });
31
+ ```
32
+
33
+ ## Logger Options
34
+
35
+ | Option | Type | Default | Description |
36
+ | ----------- | ---------------------------------------- | --------- | ---------------------- |
37
+ | `name` | `string` | `'app'` | Logger namespace |
38
+ | `scope` | `string` | - | Child logger scope |
39
+ | `level` | `'debug' \| 'info' \| 'warn' \| 'error'` | `'info'` | Log level |
40
+ | `handlers` | `HandlerConfig[]` | `console` | Handler configurations |
41
+ | `propagate` | `boolean` | `true` | Propagate to parent |
42
+ | `onError` | `(err: LogError) => void` | - | Error callback |
43
+
44
+ ## Handlers
45
+
46
+ ### Console Handler
47
+
48
+ ```typescript
49
+ const logger = new Logger({
50
+ name: "app",
51
+ handlers: [
52
+ {
53
+ type: "console",
54
+ level: "debug",
55
+ formatter: {
56
+ type: "text",
57
+ template: "[{time}] [{level}] {namespace}: {message}",
58
+ colors: true,
59
+ },
60
+ },
61
+ ],
62
+ });
63
+ ```
64
+
65
+ ### File Handler
66
+
67
+ ```typescript
68
+ const logger = new Logger({
69
+ name: "app",
70
+ handlers: [
71
+ {
72
+ type: "file",
73
+ level: "info",
74
+ formatter: { type: "json" }, // or 'text'
75
+ maxSize: "10MB", // default: 10MB
76
+ maxFiles: 7, // default: 7
77
+ },
78
+ ],
79
+ });
80
+ ```
81
+
82
+ File handler features:
83
+
84
+ - Auto-dated filenames: `app.2026-04-20.json`
85
+ - Automatic old file cleanup when exceeding `maxFiles`
86
+ - Automatic date rollover at midnight
87
+
88
+ ## Child Loggers
89
+
90
+ ```typescript
91
+ const logger = new Logger({ name: "app" });
92
+
93
+ const child = logger.child("module");
94
+
95
+ console.log(child.namespace); // "app.module"
96
+ ```
97
+
98
+ Child loggers:
99
+
100
+ - Inherit parent's handlers by default
101
+ - Inherit parent's level
102
+ - Disable propagation by default (set `propagate: true` to enable)
103
+ - Require `scope` option (cannot use `name`)
104
+
105
+ ## Methods
106
+
107
+ - `debug(message, context?)` - Debug level
108
+ - `info(message, context?)` - Info level
109
+ - `warn(message, context?)` - Warning level
110
+ - `error(message, context?)` - Error level
111
+ - `child(scope, options?)` - Create child logger
112
+ - `setLevel(level)` - Change log level
113
+ - `addHandler(handler)` - Add handler
114
+ - `close()` - Close all handlers
115
+
116
+ ## Error Handling
117
+
118
+ ```typescript
119
+ const logger = new Logger({
120
+ name: "app",
121
+ handlers: [
122
+ {
123
+ type: "file",
124
+ onError: (err) => {
125
+ console.error("Handler error:", err.error);
126
+ },
127
+ },
128
+ ],
129
+ onError: (err) => {
130
+ console.error("Logger error:", err.error);
131
+ },
132
+ });
133
+ ```
134
+
135
+ ## Runtime Notes
136
+
137
+ - File handler is automatically disabled in Cloudflare Workers environment
138
+ - Supports Bun, Node.js, and Deno runtimes
139
+
140
+ ## Testing
141
+
142
+ ```bash
143
+ pnpm test # Run tests
144
+ pnpm test:coverage # Run with coverage
145
+ ```
package/dist/index.cjs ADDED
@@ -0,0 +1,424 @@
1
+ 'use strict';
2
+
3
+ var stdEnv = require('std-env');
4
+
5
+ // src/formatter/json.ts
6
+ var JsonFormatter = class {
7
+ fields;
8
+ constructor(config = { type: "json" }) {
9
+ this.fields = config.fields ?? [];
10
+ }
11
+ format(entry) {
12
+ const output = {
13
+ ...entry.context,
14
+ level: entry.level,
15
+ time: entry.time,
16
+ namespace: entry.namespace,
17
+ message: entry.message
18
+ };
19
+ for (const field of this.fields) {
20
+ if (entry[field] !== void 0) {
21
+ output[field] = entry[field];
22
+ }
23
+ }
24
+ return JSON.stringify(output);
25
+ }
26
+ };
27
+
28
+ // src/formatter/text.ts
29
+ var RESET = "\x1B[0m";
30
+ var RED = "\x1B[31m";
31
+ var YELLOW = "\x1B[33m";
32
+ var BLUE = "\x1B[34m";
33
+ var GRAY = "\x1B[90m";
34
+ var colors = {
35
+ debug: GRAY,
36
+ info: BLUE,
37
+ warn: YELLOW,
38
+ error: RED
39
+ };
40
+ var TextFormatter = class {
41
+ template;
42
+ colors;
43
+ constructor(config) {
44
+ this.template = config.template ?? "[{time}] [{level}] {namespace}: {message}";
45
+ this.colors = config.colors ?? true;
46
+ }
47
+ format(entry) {
48
+ let message = this.template.replace(/{time}/g, entry.time).replace(/{level}/g, entry.level).replace(/{namespace}/g, entry.namespace).replace(/{message}/g, entry.message);
49
+ if (entry.context) {
50
+ message += ` ${JSON.stringify(entry.context)}`;
51
+ }
52
+ if (this.colors) {
53
+ const color = colors[entry.level] ?? "";
54
+ return `${color}${message}${RESET}`;
55
+ }
56
+ return message;
57
+ }
58
+ };
59
+
60
+ // src/formatter/index.ts
61
+ function createFormatter(config) {
62
+ if (config.type === "json") {
63
+ return new JsonFormatter(config);
64
+ }
65
+ return new TextFormatter(config);
66
+ }
67
+ var formatters = {
68
+ json: () => new JsonFormatter({ type: "json" }),
69
+ consoleText: () => new TextFormatter({
70
+ type: "text",
71
+ template: "[{time}] [{level}] {namespace}: {message}",
72
+ colors: true
73
+ }),
74
+ fileText: () => new TextFormatter({
75
+ type: "text",
76
+ template: "[{time}] [{level}] {namespace}: {message}",
77
+ colors: false
78
+ })
79
+ };
80
+
81
+ // src/levels.ts
82
+ var LOG_LEVELS = {
83
+ debug: 0,
84
+ info: 1,
85
+ warn: 2,
86
+ error: 3
87
+ };
88
+ function shouldLog(level, minLevel) {
89
+ return LOG_LEVELS[level] >= LOG_LEVELS[minLevel];
90
+ }
91
+ function normalizeLevel(level) {
92
+ if (!level) return "info";
93
+ const normalized = level.toLowerCase();
94
+ if (normalized === "debug" || normalized === "info" || normalized === "warn" || normalized === "error") {
95
+ return normalized;
96
+ }
97
+ return "info";
98
+ }
99
+
100
+ // src/handler/console.ts
101
+ var ConsoleHandler = class {
102
+ level;
103
+ formatter;
104
+ onError;
105
+ constructor(config) {
106
+ this.level = config.level ?? "debug";
107
+ this.onError = config.onError;
108
+ this.formatter = createFormatter(
109
+ config.formatter ?? {
110
+ type: "text",
111
+ template: "[{time}] [{level}] {namespace}: {message}",
112
+ colors: true
113
+ }
114
+ );
115
+ }
116
+ setLevel(level) {
117
+ this.level = level;
118
+ }
119
+ setFormatter(formatter) {
120
+ this.formatter = formatter;
121
+ }
122
+ write(entry) {
123
+ if (!shouldLog(entry.level, this.level)) return;
124
+ const output = this.formatter.format(entry);
125
+ if (entry.level === "warn") {
126
+ console.warn(output);
127
+ } else if (entry.level === "error") {
128
+ console.error(output);
129
+ } else {
130
+ console.log(output);
131
+ }
132
+ }
133
+ async close() {
134
+ }
135
+ notifyError(err) {
136
+ if (this.onError) {
137
+ this.onError(err);
138
+ }
139
+ }
140
+ };
141
+
142
+ // src/handler/file.ts
143
+ function parseSize(size) {
144
+ const match = size.match(/^(\d+)(MB|KB)?$/i);
145
+ if (!match) return 10 * 1024 * 1024;
146
+ const value = Number.parseInt(match[1], 10);
147
+ const unit = match[2]?.toUpperCase() || "MB";
148
+ return unit === "KB" ? value * 1024 : value * 1024 * 1024;
149
+ }
150
+ var FileHandler = class {
151
+ constructor(config, namespace) {
152
+ this.config = config;
153
+ this.level = config.level ?? "debug";
154
+ this.onError = config.onError;
155
+ this.formatter = createFormatter(config.formatter ?? { type: "json" });
156
+ this.maxFiles = config.maxFiles ?? 7;
157
+ this.prefix = namespace.replace(/\./g, "-");
158
+ this.ext = this.config.formatter?.type === "text" ? "log" : "jsonl";
159
+ this.baseDir = "./logs";
160
+ this.currentPath = this.getTodayPath();
161
+ }
162
+ config;
163
+ level;
164
+ formatter;
165
+ buffer = [];
166
+ timer;
167
+ size = 0;
168
+ closed = false;
169
+ currentPath;
170
+ maxFiles;
171
+ baseDir;
172
+ prefix;
173
+ ext;
174
+ onError;
175
+ getTodayPath() {
176
+ const now = /* @__PURE__ */ new Date();
177
+ const date = now.toISOString().split("T")[0];
178
+ return `${this.baseDir}/${this.prefix}.${date}.${this.ext}`;
179
+ }
180
+ setLevel(level) {
181
+ this.level = level;
182
+ }
183
+ setFormatter(formatter) {
184
+ this.formatter = formatter;
185
+ }
186
+ write(entry) {
187
+ if (this.closed || !shouldLog(entry.level, this.level)) return;
188
+ const now = /* @__PURE__ */ new Date();
189
+ const today = now.toISOString().split("T")[0];
190
+ if (!this.currentPath.includes(today)) {
191
+ this.currentPath = `${this.baseDir}/${this.prefix}.${today}.${this.ext}`;
192
+ }
193
+ const output = `${this.formatter.format(entry)}
194
+ `;
195
+ this.buffer.push(output);
196
+ this.size += output.length;
197
+ if (!this.timer) {
198
+ this.timer = setTimeout(async () => {
199
+ this.timer = void 0;
200
+ await this.flush();
201
+ }, 1e3);
202
+ }
203
+ if (this.size >= parseSize(this.config.maxSize ?? "10MB")) {
204
+ this.flush().catch(() => {
205
+ });
206
+ }
207
+ }
208
+ async flush() {
209
+ if (this.buffer.length === 0 || this.closed) return;
210
+ const content = this.buffer.join("");
211
+ this.buffer = [];
212
+ this.size = 0;
213
+ await this.writeToFile(content);
214
+ await this.cleanupOldFiles();
215
+ }
216
+ async ensureDir() {
217
+ try {
218
+ const Bun = globalThis.Bun;
219
+ const Deno = globalThis.Deno;
220
+ if (Bun) {
221
+ Bun.mkdir(this.baseDir, { recursive: true });
222
+ } else if (Deno) {
223
+ await Deno.mkdir(this.baseDir, { recursive: true });
224
+ } else {
225
+ const fs = await import('fs/promises');
226
+ await fs.mkdir(this.baseDir, { recursive: true });
227
+ }
228
+ } catch (err) {
229
+ console.error("Failed to create log directory:", err);
230
+ }
231
+ }
232
+ async writeToFile(content) {
233
+ await this.ensureDir();
234
+ try {
235
+ const Bun = globalThis.Bun;
236
+ const Deno = globalThis.Deno;
237
+ if (Bun) {
238
+ Bun.write(this.currentPath, content, { flag: "a" });
239
+ } else if (Deno) {
240
+ await Deno.writeTextFile(this.currentPath, content);
241
+ } else {
242
+ const fs = await import('fs/promises');
243
+ await fs.writeFile(this.currentPath, content, { flag: "a" });
244
+ }
245
+ } catch (err) {
246
+ console.error("Failed to write to log file:", err);
247
+ }
248
+ }
249
+ async cleanupOldFiles() {
250
+ try {
251
+ const Bun = globalThis.Bun;
252
+ const Deno = globalThis.Deno;
253
+ let files;
254
+ if (Bun) {
255
+ const entries = Bun.readdir(this.baseDir);
256
+ files = entries.filter((f) => f.startsWith(this.prefix)).sort();
257
+ } else if (Deno) {
258
+ const entries = await Deno.readDir(this.baseDir);
259
+ files = [];
260
+ for await (const entry of entries) {
261
+ if (entry.isFile && entry.name.startsWith(this.prefix)) {
262
+ files.push(entry.name);
263
+ }
264
+ }
265
+ files.sort();
266
+ } else {
267
+ const fs = await import('fs/promises');
268
+ const entries = await fs.readdir(this.baseDir);
269
+ files = entries.filter((f) => f.startsWith(this.prefix)).sort();
270
+ }
271
+ while (files.length >= this.maxFiles) {
272
+ const oldest = files.shift();
273
+ if (oldest) {
274
+ const filePath = `${this.baseDir}/${oldest}`;
275
+ if (Bun) {
276
+ Bun.remove(filePath);
277
+ } else if (Deno) {
278
+ await Deno.remove(filePath);
279
+ } else {
280
+ const fs = await import('fs/promises');
281
+ await fs.unlink(filePath);
282
+ }
283
+ }
284
+ }
285
+ } catch {
286
+ }
287
+ }
288
+ async close() {
289
+ if (this.timer) {
290
+ clearTimeout(this.timer);
291
+ this.timer = void 0;
292
+ }
293
+ const content = this.buffer.join("");
294
+ this.buffer = [];
295
+ this.size = 0;
296
+ this.closed = true;
297
+ if (content) {
298
+ await this.writeToFile(content);
299
+ }
300
+ }
301
+ notifyError(err) {
302
+ if (this.onError) {
303
+ this.onError(err);
304
+ }
305
+ }
306
+ };
307
+
308
+ // src/handler/index.ts
309
+ function createHandler(config, namespace) {
310
+ if (config.type === "console") {
311
+ return new ConsoleHandler(config);
312
+ }
313
+ return new FileHandler(config, namespace);
314
+ }
315
+ var Logger = class _Logger {
316
+ namespace;
317
+ level;
318
+ handlers;
319
+ propagate;
320
+ parent;
321
+ onError;
322
+ constructor(options = {}) {
323
+ const parent = options.logger instanceof _Logger ? options.logger : void 0;
324
+ const isChild = !!parent;
325
+ if (parent && options.name) {
326
+ throw new Error(
327
+ 'Cannot use "name" when creating child logger with parent. Use "scope" only.'
328
+ );
329
+ }
330
+ this.level = options.level ?? parent?.level ?? "info";
331
+ this.propagate = isChild ? false : options.propagate ?? true;
332
+ this.parent = parent;
333
+ this.onError = options.onError;
334
+ if (parent) {
335
+ if (!options.scope) {
336
+ throw new Error('Child logger requires "scope" option.');
337
+ }
338
+ this.namespace = `${parent.namespace}.${options.scope}`;
339
+ } else {
340
+ this.namespace = options.name ?? options.scope ?? "app";
341
+ }
342
+ if (isChild) {
343
+ this.handlers = parent?.handlers ?? [];
344
+ } else {
345
+ const configs = options.handlers ?? [{ type: "console" }];
346
+ this.handlers = configs.filter((c) => c.type !== "file" || !stdEnv.isWorkerd).map((config) => createHandler(config, this.namespace));
347
+ }
348
+ }
349
+ child(scope, options) {
350
+ return new _Logger({
351
+ ...options,
352
+ scope,
353
+ logger: this
354
+ });
355
+ }
356
+ setLevel(level) {
357
+ this.level = level;
358
+ }
359
+ addHandler(handler) {
360
+ this.handlers.push(handler);
361
+ }
362
+ log(level, args) {
363
+ if (!shouldLog(level, normalizeLevel(this.level))) return;
364
+ const lastArg = args[args.length - 1];
365
+ const context = args.length > 1 && typeof lastArg === "object" && !Array.isArray(lastArg) ? lastArg : void 0;
366
+ const messageArgs = context ? args.slice(0, -1) : args;
367
+ const message = messageArgs.map(String).join(" ");
368
+ const entry = {
369
+ level,
370
+ time: (/* @__PURE__ */ new Date()).toISOString(),
371
+ namespace: this.namespace,
372
+ message,
373
+ context
374
+ };
375
+ this.writeToHandlers(entry);
376
+ if (this.propagate && this.parent) {
377
+ this.parent.writeToHandlers(entry);
378
+ }
379
+ }
380
+ writeToHandlers(entry) {
381
+ for (const handler of this.handlers) {
382
+ try {
383
+ handler.write(entry);
384
+ } catch (err) {
385
+ const logError = {
386
+ error: err instanceof Error ? err : new Error(String(err)),
387
+ entry,
388
+ handler
389
+ };
390
+ handler.notifyError(logError);
391
+ if (this.onError) {
392
+ this.onError(logError);
393
+ }
394
+ }
395
+ }
396
+ }
397
+ debug(...args) {
398
+ this.log("debug", args);
399
+ }
400
+ info(...args) {
401
+ this.log("info", args);
402
+ }
403
+ warn(...args) {
404
+ this.log("warn", args);
405
+ }
406
+ error(...args) {
407
+ this.log("error", args);
408
+ }
409
+ access(message, context = {}) {
410
+ this.log("info", [message, context]);
411
+ }
412
+ async close() {
413
+ await Promise.all(this.handlers.map((h) => h.close()));
414
+ }
415
+ };
416
+ function createLogger(options) {
417
+ return new Logger(options);
418
+ }
419
+
420
+ exports.Logger = Logger;
421
+ exports.createFormatter = createFormatter;
422
+ exports.createHandler = createHandler;
423
+ exports.createLogger = createLogger;
424
+ exports.formatters = formatters;
@@ -0,0 +1,102 @@
1
+ type LogLevel = 'debug' | 'info' | 'warn' | 'error';
2
+ interface LogEntry {
3
+ level: LogLevel;
4
+ time: string;
5
+ namespace: string;
6
+ message: string;
7
+ context?: Record<string, unknown>;
8
+ [key: string]: unknown;
9
+ }
10
+ interface LogError {
11
+ error: Error;
12
+ entry?: LogEntry;
13
+ handler: unknown;
14
+ }
15
+ interface FormatterConfig {
16
+ type: 'json' | 'text';
17
+ template?: string;
18
+ timeFormat?: 'iso' | 'local';
19
+ colors?: boolean;
20
+ fields?: string[];
21
+ }
22
+ type HandlerType = 'console' | 'file';
23
+ interface HandlerConfig {
24
+ type: HandlerType;
25
+ level?: LogLevel;
26
+ formatter?: FormatterConfig;
27
+ path?: string;
28
+ maxSize?: string;
29
+ maxFiles?: number;
30
+ onError?: (err: LogError) => void;
31
+ sync?: boolean;
32
+ }
33
+ interface ILogger {
34
+ debug(...args: unknown[]): void;
35
+ info(...args: unknown[]): void;
36
+ warn(...args: unknown[]): void;
37
+ error(...args: unknown[]): void;
38
+ close(): Promise<void>;
39
+ }
40
+ interface LoggerOptions {
41
+ name?: string;
42
+ scope?: string;
43
+ level?: LogLevel;
44
+ handlers?: HandlerConfig[];
45
+ propagate?: boolean;
46
+ logger?: ILogger;
47
+ onError?: (err: LogError) => void;
48
+ }
49
+
50
+ declare class JsonFormatter {
51
+ private fields;
52
+ constructor(config?: FormatterConfig);
53
+ format(entry: LogEntry): string;
54
+ }
55
+
56
+ declare class TextFormatter {
57
+ private template;
58
+ private colors;
59
+ constructor(config: FormatterConfig);
60
+ format(entry: LogEntry): string;
61
+ }
62
+
63
+ type Formatter = {
64
+ format(entry: LogEntry): string;
65
+ };
66
+ declare function createFormatter(config: FormatterConfig): Formatter;
67
+ declare const formatters: {
68
+ json: () => JsonFormatter;
69
+ consoleText: () => TextFormatter;
70
+ fileText: () => TextFormatter;
71
+ };
72
+
73
+ type Handler = {
74
+ write(entry: LogEntry): void;
75
+ notifyError(err: LogError): void;
76
+ close(): Promise<void>;
77
+ };
78
+ declare function createHandler(config: HandlerConfig, namespace: string): Handler;
79
+
80
+ declare class Logger {
81
+ readonly namespace: string;
82
+ level: LogLevel;
83
+ readonly handlers: Handler[];
84
+ readonly propagate: boolean;
85
+ private parent?;
86
+ private onError?;
87
+ constructor(options?: LoggerOptions);
88
+ child(scope: string, options?: LoggerOptions): Logger;
89
+ setLevel(level: LogLevel): void;
90
+ addHandler(handler: Handler): void;
91
+ private log;
92
+ private writeToHandlers;
93
+ debug(...args: unknown[]): void;
94
+ info(...args: unknown[]): void;
95
+ warn(...args: unknown[]): void;
96
+ error(...args: unknown[]): void;
97
+ access(message: string, context?: Record<string, unknown>): void;
98
+ close(): Promise<void>;
99
+ }
100
+ declare function createLogger(options?: LoggerOptions): Logger;
101
+
102
+ export { type FormatterConfig, type Handler, type HandlerConfig, type HandlerType, type ILogger, type LogEntry, type LogError, type LogLevel, Logger, type LoggerOptions, createFormatter, createHandler, createLogger, formatters };
@@ -0,0 +1,102 @@
1
+ type LogLevel = 'debug' | 'info' | 'warn' | 'error';
2
+ interface LogEntry {
3
+ level: LogLevel;
4
+ time: string;
5
+ namespace: string;
6
+ message: string;
7
+ context?: Record<string, unknown>;
8
+ [key: string]: unknown;
9
+ }
10
+ interface LogError {
11
+ error: Error;
12
+ entry?: LogEntry;
13
+ handler: unknown;
14
+ }
15
+ interface FormatterConfig {
16
+ type: 'json' | 'text';
17
+ template?: string;
18
+ timeFormat?: 'iso' | 'local';
19
+ colors?: boolean;
20
+ fields?: string[];
21
+ }
22
+ type HandlerType = 'console' | 'file';
23
+ interface HandlerConfig {
24
+ type: HandlerType;
25
+ level?: LogLevel;
26
+ formatter?: FormatterConfig;
27
+ path?: string;
28
+ maxSize?: string;
29
+ maxFiles?: number;
30
+ onError?: (err: LogError) => void;
31
+ sync?: boolean;
32
+ }
33
+ interface ILogger {
34
+ debug(...args: unknown[]): void;
35
+ info(...args: unknown[]): void;
36
+ warn(...args: unknown[]): void;
37
+ error(...args: unknown[]): void;
38
+ close(): Promise<void>;
39
+ }
40
+ interface LoggerOptions {
41
+ name?: string;
42
+ scope?: string;
43
+ level?: LogLevel;
44
+ handlers?: HandlerConfig[];
45
+ propagate?: boolean;
46
+ logger?: ILogger;
47
+ onError?: (err: LogError) => void;
48
+ }
49
+
50
+ declare class JsonFormatter {
51
+ private fields;
52
+ constructor(config?: FormatterConfig);
53
+ format(entry: LogEntry): string;
54
+ }
55
+
56
+ declare class TextFormatter {
57
+ private template;
58
+ private colors;
59
+ constructor(config: FormatterConfig);
60
+ format(entry: LogEntry): string;
61
+ }
62
+
63
+ type Formatter = {
64
+ format(entry: LogEntry): string;
65
+ };
66
+ declare function createFormatter(config: FormatterConfig): Formatter;
67
+ declare const formatters: {
68
+ json: () => JsonFormatter;
69
+ consoleText: () => TextFormatter;
70
+ fileText: () => TextFormatter;
71
+ };
72
+
73
+ type Handler = {
74
+ write(entry: LogEntry): void;
75
+ notifyError(err: LogError): void;
76
+ close(): Promise<void>;
77
+ };
78
+ declare function createHandler(config: HandlerConfig, namespace: string): Handler;
79
+
80
+ declare class Logger {
81
+ readonly namespace: string;
82
+ level: LogLevel;
83
+ readonly handlers: Handler[];
84
+ readonly propagate: boolean;
85
+ private parent?;
86
+ private onError?;
87
+ constructor(options?: LoggerOptions);
88
+ child(scope: string, options?: LoggerOptions): Logger;
89
+ setLevel(level: LogLevel): void;
90
+ addHandler(handler: Handler): void;
91
+ private log;
92
+ private writeToHandlers;
93
+ debug(...args: unknown[]): void;
94
+ info(...args: unknown[]): void;
95
+ warn(...args: unknown[]): void;
96
+ error(...args: unknown[]): void;
97
+ access(message: string, context?: Record<string, unknown>): void;
98
+ close(): Promise<void>;
99
+ }
100
+ declare function createLogger(options?: LoggerOptions): Logger;
101
+
102
+ export { type FormatterConfig, type Handler, type HandlerConfig, type HandlerType, type ILogger, type LogEntry, type LogError, type LogLevel, Logger, type LoggerOptions, createFormatter, createHandler, createLogger, formatters };
package/dist/index.js ADDED
@@ -0,0 +1,418 @@
1
+ import { isWorkerd } from 'std-env';
2
+
3
+ // src/formatter/json.ts
4
+ var JsonFormatter = class {
5
+ fields;
6
+ constructor(config = { type: "json" }) {
7
+ this.fields = config.fields ?? [];
8
+ }
9
+ format(entry) {
10
+ const output = {
11
+ ...entry.context,
12
+ level: entry.level,
13
+ time: entry.time,
14
+ namespace: entry.namespace,
15
+ message: entry.message
16
+ };
17
+ for (const field of this.fields) {
18
+ if (entry[field] !== void 0) {
19
+ output[field] = entry[field];
20
+ }
21
+ }
22
+ return JSON.stringify(output);
23
+ }
24
+ };
25
+
26
+ // src/formatter/text.ts
27
+ var RESET = "\x1B[0m";
28
+ var RED = "\x1B[31m";
29
+ var YELLOW = "\x1B[33m";
30
+ var BLUE = "\x1B[34m";
31
+ var GRAY = "\x1B[90m";
32
+ var colors = {
33
+ debug: GRAY,
34
+ info: BLUE,
35
+ warn: YELLOW,
36
+ error: RED
37
+ };
38
+ var TextFormatter = class {
39
+ template;
40
+ colors;
41
+ constructor(config) {
42
+ this.template = config.template ?? "[{time}] [{level}] {namespace}: {message}";
43
+ this.colors = config.colors ?? true;
44
+ }
45
+ format(entry) {
46
+ let message = this.template.replace(/{time}/g, entry.time).replace(/{level}/g, entry.level).replace(/{namespace}/g, entry.namespace).replace(/{message}/g, entry.message);
47
+ if (entry.context) {
48
+ message += ` ${JSON.stringify(entry.context)}`;
49
+ }
50
+ if (this.colors) {
51
+ const color = colors[entry.level] ?? "";
52
+ return `${color}${message}${RESET}`;
53
+ }
54
+ return message;
55
+ }
56
+ };
57
+
58
+ // src/formatter/index.ts
59
+ function createFormatter(config) {
60
+ if (config.type === "json") {
61
+ return new JsonFormatter(config);
62
+ }
63
+ return new TextFormatter(config);
64
+ }
65
+ var formatters = {
66
+ json: () => new JsonFormatter({ type: "json" }),
67
+ consoleText: () => new TextFormatter({
68
+ type: "text",
69
+ template: "[{time}] [{level}] {namespace}: {message}",
70
+ colors: true
71
+ }),
72
+ fileText: () => new TextFormatter({
73
+ type: "text",
74
+ template: "[{time}] [{level}] {namespace}: {message}",
75
+ colors: false
76
+ })
77
+ };
78
+
79
+ // src/levels.ts
80
+ var LOG_LEVELS = {
81
+ debug: 0,
82
+ info: 1,
83
+ warn: 2,
84
+ error: 3
85
+ };
86
+ function shouldLog(level, minLevel) {
87
+ return LOG_LEVELS[level] >= LOG_LEVELS[minLevel];
88
+ }
89
+ function normalizeLevel(level) {
90
+ if (!level) return "info";
91
+ const normalized = level.toLowerCase();
92
+ if (normalized === "debug" || normalized === "info" || normalized === "warn" || normalized === "error") {
93
+ return normalized;
94
+ }
95
+ return "info";
96
+ }
97
+
98
+ // src/handler/console.ts
99
+ var ConsoleHandler = class {
100
+ level;
101
+ formatter;
102
+ onError;
103
+ constructor(config) {
104
+ this.level = config.level ?? "debug";
105
+ this.onError = config.onError;
106
+ this.formatter = createFormatter(
107
+ config.formatter ?? {
108
+ type: "text",
109
+ template: "[{time}] [{level}] {namespace}: {message}",
110
+ colors: true
111
+ }
112
+ );
113
+ }
114
+ setLevel(level) {
115
+ this.level = level;
116
+ }
117
+ setFormatter(formatter) {
118
+ this.formatter = formatter;
119
+ }
120
+ write(entry) {
121
+ if (!shouldLog(entry.level, this.level)) return;
122
+ const output = this.formatter.format(entry);
123
+ if (entry.level === "warn") {
124
+ console.warn(output);
125
+ } else if (entry.level === "error") {
126
+ console.error(output);
127
+ } else {
128
+ console.log(output);
129
+ }
130
+ }
131
+ async close() {
132
+ }
133
+ notifyError(err) {
134
+ if (this.onError) {
135
+ this.onError(err);
136
+ }
137
+ }
138
+ };
139
+
140
+ // src/handler/file.ts
141
+ function parseSize(size) {
142
+ const match = size.match(/^(\d+)(MB|KB)?$/i);
143
+ if (!match) return 10 * 1024 * 1024;
144
+ const value = Number.parseInt(match[1], 10);
145
+ const unit = match[2]?.toUpperCase() || "MB";
146
+ return unit === "KB" ? value * 1024 : value * 1024 * 1024;
147
+ }
148
+ var FileHandler = class {
149
+ constructor(config, namespace) {
150
+ this.config = config;
151
+ this.level = config.level ?? "debug";
152
+ this.onError = config.onError;
153
+ this.formatter = createFormatter(config.formatter ?? { type: "json" });
154
+ this.maxFiles = config.maxFiles ?? 7;
155
+ this.prefix = namespace.replace(/\./g, "-");
156
+ this.ext = this.config.formatter?.type === "text" ? "log" : "jsonl";
157
+ this.baseDir = "./logs";
158
+ this.currentPath = this.getTodayPath();
159
+ }
160
+ config;
161
+ level;
162
+ formatter;
163
+ buffer = [];
164
+ timer;
165
+ size = 0;
166
+ closed = false;
167
+ currentPath;
168
+ maxFiles;
169
+ baseDir;
170
+ prefix;
171
+ ext;
172
+ onError;
173
+ getTodayPath() {
174
+ const now = /* @__PURE__ */ new Date();
175
+ const date = now.toISOString().split("T")[0];
176
+ return `${this.baseDir}/${this.prefix}.${date}.${this.ext}`;
177
+ }
178
+ setLevel(level) {
179
+ this.level = level;
180
+ }
181
+ setFormatter(formatter) {
182
+ this.formatter = formatter;
183
+ }
184
+ write(entry) {
185
+ if (this.closed || !shouldLog(entry.level, this.level)) return;
186
+ const now = /* @__PURE__ */ new Date();
187
+ const today = now.toISOString().split("T")[0];
188
+ if (!this.currentPath.includes(today)) {
189
+ this.currentPath = `${this.baseDir}/${this.prefix}.${today}.${this.ext}`;
190
+ }
191
+ const output = `${this.formatter.format(entry)}
192
+ `;
193
+ this.buffer.push(output);
194
+ this.size += output.length;
195
+ if (!this.timer) {
196
+ this.timer = setTimeout(async () => {
197
+ this.timer = void 0;
198
+ await this.flush();
199
+ }, 1e3);
200
+ }
201
+ if (this.size >= parseSize(this.config.maxSize ?? "10MB")) {
202
+ this.flush().catch(() => {
203
+ });
204
+ }
205
+ }
206
+ async flush() {
207
+ if (this.buffer.length === 0 || this.closed) return;
208
+ const content = this.buffer.join("");
209
+ this.buffer = [];
210
+ this.size = 0;
211
+ await this.writeToFile(content);
212
+ await this.cleanupOldFiles();
213
+ }
214
+ async ensureDir() {
215
+ try {
216
+ const Bun = globalThis.Bun;
217
+ const Deno = globalThis.Deno;
218
+ if (Bun) {
219
+ Bun.mkdir(this.baseDir, { recursive: true });
220
+ } else if (Deno) {
221
+ await Deno.mkdir(this.baseDir, { recursive: true });
222
+ } else {
223
+ const fs = await import('fs/promises');
224
+ await fs.mkdir(this.baseDir, { recursive: true });
225
+ }
226
+ } catch (err) {
227
+ console.error("Failed to create log directory:", err);
228
+ }
229
+ }
230
+ async writeToFile(content) {
231
+ await this.ensureDir();
232
+ try {
233
+ const Bun = globalThis.Bun;
234
+ const Deno = globalThis.Deno;
235
+ if (Bun) {
236
+ Bun.write(this.currentPath, content, { flag: "a" });
237
+ } else if (Deno) {
238
+ await Deno.writeTextFile(this.currentPath, content);
239
+ } else {
240
+ const fs = await import('fs/promises');
241
+ await fs.writeFile(this.currentPath, content, { flag: "a" });
242
+ }
243
+ } catch (err) {
244
+ console.error("Failed to write to log file:", err);
245
+ }
246
+ }
247
+ async cleanupOldFiles() {
248
+ try {
249
+ const Bun = globalThis.Bun;
250
+ const Deno = globalThis.Deno;
251
+ let files;
252
+ if (Bun) {
253
+ const entries = Bun.readdir(this.baseDir);
254
+ files = entries.filter((f) => f.startsWith(this.prefix)).sort();
255
+ } else if (Deno) {
256
+ const entries = await Deno.readDir(this.baseDir);
257
+ files = [];
258
+ for await (const entry of entries) {
259
+ if (entry.isFile && entry.name.startsWith(this.prefix)) {
260
+ files.push(entry.name);
261
+ }
262
+ }
263
+ files.sort();
264
+ } else {
265
+ const fs = await import('fs/promises');
266
+ const entries = await fs.readdir(this.baseDir);
267
+ files = entries.filter((f) => f.startsWith(this.prefix)).sort();
268
+ }
269
+ while (files.length >= this.maxFiles) {
270
+ const oldest = files.shift();
271
+ if (oldest) {
272
+ const filePath = `${this.baseDir}/${oldest}`;
273
+ if (Bun) {
274
+ Bun.remove(filePath);
275
+ } else if (Deno) {
276
+ await Deno.remove(filePath);
277
+ } else {
278
+ const fs = await import('fs/promises');
279
+ await fs.unlink(filePath);
280
+ }
281
+ }
282
+ }
283
+ } catch {
284
+ }
285
+ }
286
+ async close() {
287
+ if (this.timer) {
288
+ clearTimeout(this.timer);
289
+ this.timer = void 0;
290
+ }
291
+ const content = this.buffer.join("");
292
+ this.buffer = [];
293
+ this.size = 0;
294
+ this.closed = true;
295
+ if (content) {
296
+ await this.writeToFile(content);
297
+ }
298
+ }
299
+ notifyError(err) {
300
+ if (this.onError) {
301
+ this.onError(err);
302
+ }
303
+ }
304
+ };
305
+
306
+ // src/handler/index.ts
307
+ function createHandler(config, namespace) {
308
+ if (config.type === "console") {
309
+ return new ConsoleHandler(config);
310
+ }
311
+ return new FileHandler(config, namespace);
312
+ }
313
+ var Logger = class _Logger {
314
+ namespace;
315
+ level;
316
+ handlers;
317
+ propagate;
318
+ parent;
319
+ onError;
320
+ constructor(options = {}) {
321
+ const parent = options.logger instanceof _Logger ? options.logger : void 0;
322
+ const isChild = !!parent;
323
+ if (parent && options.name) {
324
+ throw new Error(
325
+ 'Cannot use "name" when creating child logger with parent. Use "scope" only.'
326
+ );
327
+ }
328
+ this.level = options.level ?? parent?.level ?? "info";
329
+ this.propagate = isChild ? false : options.propagate ?? true;
330
+ this.parent = parent;
331
+ this.onError = options.onError;
332
+ if (parent) {
333
+ if (!options.scope) {
334
+ throw new Error('Child logger requires "scope" option.');
335
+ }
336
+ this.namespace = `${parent.namespace}.${options.scope}`;
337
+ } else {
338
+ this.namespace = options.name ?? options.scope ?? "app";
339
+ }
340
+ if (isChild) {
341
+ this.handlers = parent?.handlers ?? [];
342
+ } else {
343
+ const configs = options.handlers ?? [{ type: "console" }];
344
+ this.handlers = configs.filter((c) => c.type !== "file" || !isWorkerd).map((config) => createHandler(config, this.namespace));
345
+ }
346
+ }
347
+ child(scope, options) {
348
+ return new _Logger({
349
+ ...options,
350
+ scope,
351
+ logger: this
352
+ });
353
+ }
354
+ setLevel(level) {
355
+ this.level = level;
356
+ }
357
+ addHandler(handler) {
358
+ this.handlers.push(handler);
359
+ }
360
+ log(level, args) {
361
+ if (!shouldLog(level, normalizeLevel(this.level))) return;
362
+ const lastArg = args[args.length - 1];
363
+ const context = args.length > 1 && typeof lastArg === "object" && !Array.isArray(lastArg) ? lastArg : void 0;
364
+ const messageArgs = context ? args.slice(0, -1) : args;
365
+ const message = messageArgs.map(String).join(" ");
366
+ const entry = {
367
+ level,
368
+ time: (/* @__PURE__ */ new Date()).toISOString(),
369
+ namespace: this.namespace,
370
+ message,
371
+ context
372
+ };
373
+ this.writeToHandlers(entry);
374
+ if (this.propagate && this.parent) {
375
+ this.parent.writeToHandlers(entry);
376
+ }
377
+ }
378
+ writeToHandlers(entry) {
379
+ for (const handler of this.handlers) {
380
+ try {
381
+ handler.write(entry);
382
+ } catch (err) {
383
+ const logError = {
384
+ error: err instanceof Error ? err : new Error(String(err)),
385
+ entry,
386
+ handler
387
+ };
388
+ handler.notifyError(logError);
389
+ if (this.onError) {
390
+ this.onError(logError);
391
+ }
392
+ }
393
+ }
394
+ }
395
+ debug(...args) {
396
+ this.log("debug", args);
397
+ }
398
+ info(...args) {
399
+ this.log("info", args);
400
+ }
401
+ warn(...args) {
402
+ this.log("warn", args);
403
+ }
404
+ error(...args) {
405
+ this.log("error", args);
406
+ }
407
+ access(message, context = {}) {
408
+ this.log("info", [message, context]);
409
+ }
410
+ async close() {
411
+ await Promise.all(this.handlers.map((h) => h.close()));
412
+ }
413
+ };
414
+ function createLogger(options) {
415
+ return new Logger(options);
416
+ }
417
+
418
+ export { Logger, createFormatter, createHandler, createLogger, formatters };
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@rx-ted/packages-logger",
3
+ "version": "0.0.1",
4
+ "type": "module",
5
+ "description": "A lightweight logging library with file & console output, designed for Cloudflare/Node/Bun/Deno.",
6
+ "main": "./dist/index.cjs",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js",
12
+ "require": "./dist/index.cjs"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "author": "rx-ted",
19
+ "license": "MIT",
20
+ "publishConfig": {
21
+ "access": "public"
22
+ },
23
+ "keywords": [
24
+ "logger",
25
+ "Bun",
26
+ "Node",
27
+ "Deno",
28
+ "javascript",
29
+ "typescript"
30
+ ],
31
+ "dependencies": {
32
+ "std-env": "^4.1.0"
33
+ },
34
+ "devDependencies": {
35
+ "@types/node": "^22.19.19",
36
+ "@vitest/coverage-v8": "^4.1.6",
37
+ "tsup": "^8.5.1",
38
+ "typescript": "^5.9.2",
39
+ "vitest": "^4.1.6"
40
+ },
41
+ "scripts": {
42
+ "build": "tsup",
43
+ "typecheck": "tsc -p tsconfig.json --noEmit",
44
+ "test": "vitest run",
45
+ "test:watch": "vitest",
46
+ "test:coverage": "vitest run --coverage"
47
+ }
48
+ }