@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 +21 -0
- package/README.md +145 -0
- package/dist/index.cjs +424 -0
- package/dist/index.d.cts +102 -0
- package/dist/index.d.ts +102 -0
- package/dist/index.js +418 -0
- package/package.json +48 -0
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;
|
package/dist/index.d.cts
ADDED
|
@@ -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.d.ts
ADDED
|
@@ -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
|
+
}
|