@richardmcquiston01/error-log-adapter 0.0.0-stage → 0.2.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 +202 -0
- package/README.md +156 -2
- package/dist/index.cjs +687 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +358 -0
- package/dist/index.d.ts +358 -0
- package/dist/index.mjs +647 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +58 -4
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,647 @@
|
|
|
1
|
+
// src/serialize.ts
|
|
2
|
+
function serializeCause(cause) {
|
|
3
|
+
if (cause instanceof Error) {
|
|
4
|
+
return { name: cause.name, message: cause.message, stack: cause.stack };
|
|
5
|
+
}
|
|
6
|
+
return cause;
|
|
7
|
+
}
|
|
8
|
+
function serializeEntry(entry) {
|
|
9
|
+
const serialized = {
|
|
10
|
+
code: entry.code,
|
|
11
|
+
message: entry.message,
|
|
12
|
+
timestamp: entry.timestamp ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
13
|
+
};
|
|
14
|
+
if (entry.cause !== void 0) {
|
|
15
|
+
serialized.cause = serializeCause(entry.cause);
|
|
16
|
+
}
|
|
17
|
+
if (entry.source !== void 0) {
|
|
18
|
+
serialized.source = entry.source;
|
|
19
|
+
}
|
|
20
|
+
return serialized;
|
|
21
|
+
}
|
|
22
|
+
function describeError(error) {
|
|
23
|
+
return error instanceof Error ? error.message : String(error);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// src/adapters/console-error-adapter.ts
|
|
27
|
+
var ConsoleErrorAdapter = class {
|
|
28
|
+
constructor(options = {}) {
|
|
29
|
+
this.prefix = options.prefix ?? "[error-log-adapter]";
|
|
30
|
+
}
|
|
31
|
+
async log(entry) {
|
|
32
|
+
try {
|
|
33
|
+
console.error(this.prefix, entry);
|
|
34
|
+
return { ok: true };
|
|
35
|
+
} catch (error) {
|
|
36
|
+
return {
|
|
37
|
+
ok: false,
|
|
38
|
+
error: `ConsoleErrorAdapter failed: ${String(error)}`
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// src/adapters/webhook-error-adapter.ts
|
|
45
|
+
var WebhookErrorAdapter = class {
|
|
46
|
+
constructor(url, options = {}) {
|
|
47
|
+
this.url = url;
|
|
48
|
+
this.options = options;
|
|
49
|
+
this.prefix = options.prefix ?? "[error-log-adapter]";
|
|
50
|
+
}
|
|
51
|
+
async log(entry) {
|
|
52
|
+
try {
|
|
53
|
+
const response = await fetch(this.url, {
|
|
54
|
+
method: "POST",
|
|
55
|
+
headers: {
|
|
56
|
+
"Content-Type": "application/json",
|
|
57
|
+
...this.options.headers
|
|
58
|
+
},
|
|
59
|
+
body: JSON.stringify(serializeEntry(entry))
|
|
60
|
+
});
|
|
61
|
+
if (!response.ok) {
|
|
62
|
+
return this.fail(
|
|
63
|
+
`webhook ${this.url} responded with HTTP ${response.status}`
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
return { ok: true };
|
|
67
|
+
} catch (error) {
|
|
68
|
+
console.error(`${this.prefix} WebhookErrorAdapter:`, error);
|
|
69
|
+
return this.fail(
|
|
70
|
+
`could not reach webhook ${this.url}: ${describeError(error)}`
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
fail(reason) {
|
|
75
|
+
return { ok: false, error: `WebhookErrorAdapter failed: ${reason}` };
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// src/adapters/mongodb-error-adapter.ts
|
|
80
|
+
var MongoDbErrorAdapter = class {
|
|
81
|
+
constructor(collection, options = {}) {
|
|
82
|
+
this.collection = collection;
|
|
83
|
+
this.prefix = options.prefix ?? "[error-log-adapter]";
|
|
84
|
+
}
|
|
85
|
+
async log(entry) {
|
|
86
|
+
try {
|
|
87
|
+
await this.collection.insertOne(serializeEntry(entry));
|
|
88
|
+
return { ok: true };
|
|
89
|
+
} catch (error) {
|
|
90
|
+
console.error(`${this.prefix} MongoDbErrorAdapter:`, error);
|
|
91
|
+
return {
|
|
92
|
+
ok: false,
|
|
93
|
+
error: `MongoDbErrorAdapter failed to insert entry: ${describeError(error)}`
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// src/adapters/composite-error-adapter.ts
|
|
100
|
+
var CompositeErrorAdapter = class {
|
|
101
|
+
constructor(adapters) {
|
|
102
|
+
this.adapters = adapters;
|
|
103
|
+
}
|
|
104
|
+
async log(entry) {
|
|
105
|
+
const results = await Promise.all(
|
|
106
|
+
this.adapters.map(
|
|
107
|
+
async (adapter) => {
|
|
108
|
+
try {
|
|
109
|
+
return await adapter.log(entry);
|
|
110
|
+
} catch (error) {
|
|
111
|
+
return {
|
|
112
|
+
ok: false,
|
|
113
|
+
error: `adapter threw instead of returning a result: ${String(error)}`
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
)
|
|
118
|
+
);
|
|
119
|
+
const failures = results.flatMap(
|
|
120
|
+
(result) => result.ok ? [] : [result.error ?? "unknown failure"]
|
|
121
|
+
);
|
|
122
|
+
if (failures.length === 0) {
|
|
123
|
+
return { ok: true };
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
ok: false,
|
|
127
|
+
error: `${failures.length} of ${results.length} adapters failed: ${failures.join("; ")}`
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
// src/adapters/indexeddb-error-adapter.ts
|
|
133
|
+
var DEFAULT_STORE_NAME = "error_log";
|
|
134
|
+
function createErrorLogStore(database, storeName = DEFAULT_STORE_NAME) {
|
|
135
|
+
if (database.objectStoreNames.contains(storeName)) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
database.createObjectStore(storeName, { keyPath: "id" }).createIndex("timestamp", "timestamp");
|
|
139
|
+
}
|
|
140
|
+
var IndexedDbErrorAdapter = class {
|
|
141
|
+
constructor(database, options = {}) {
|
|
142
|
+
this.database = database;
|
|
143
|
+
this.storeName = options.storeName ?? DEFAULT_STORE_NAME;
|
|
144
|
+
this.prefix = options.prefix ?? "[error-log-adapter]";
|
|
145
|
+
}
|
|
146
|
+
async log(entry) {
|
|
147
|
+
try {
|
|
148
|
+
const record = {
|
|
149
|
+
id: crypto.randomUUID(),
|
|
150
|
+
...serializeEntry(entry)
|
|
151
|
+
};
|
|
152
|
+
await this.write(record);
|
|
153
|
+
return { ok: true };
|
|
154
|
+
} catch (error) {
|
|
155
|
+
console.error(`${this.prefix} IndexedDbErrorAdapter:`, error);
|
|
156
|
+
return {
|
|
157
|
+
ok: false,
|
|
158
|
+
error: `IndexedDbErrorAdapter failed to write to object store "${this.storeName}": ${describeError(error)}`
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
/** Resolves once the transaction commits; rejects if it errors or aborts. */
|
|
163
|
+
write(record) {
|
|
164
|
+
return new Promise(
|
|
165
|
+
(resolve, reject) => {
|
|
166
|
+
const transaction = this.database.transaction(
|
|
167
|
+
this.storeName,
|
|
168
|
+
"readwrite"
|
|
169
|
+
);
|
|
170
|
+
const fail = (action) => () => reject(
|
|
171
|
+
new Error(
|
|
172
|
+
`transaction ${action}: ${transaction.error?.message ?? "no details provided"}`
|
|
173
|
+
)
|
|
174
|
+
);
|
|
175
|
+
transaction.oncomplete = () => resolve();
|
|
176
|
+
transaction.onerror = fail("failed");
|
|
177
|
+
transaction.onabort = fail("aborted");
|
|
178
|
+
transaction.objectStore(this.storeName).add(record);
|
|
179
|
+
}
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
// src/adapters/sqlite-error-adapter.ts
|
|
185
|
+
var DEFAULT_TABLE_NAME = "error_log";
|
|
186
|
+
var IDENTIFIER_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
187
|
+
function assertValidTableName(tableName) {
|
|
188
|
+
if (!IDENTIFIER_PATTERN.test(tableName)) {
|
|
189
|
+
throw new RangeError(
|
|
190
|
+
`SqliteErrorAdapter: invalid table name "${tableName}". Use only letters, digits and underscores, and do not start with a digit.`
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
function buildCreateErrorLogTableSql(tableName = DEFAULT_TABLE_NAME) {
|
|
195
|
+
assertValidTableName(tableName);
|
|
196
|
+
return [
|
|
197
|
+
`CREATE TABLE IF NOT EXISTS ${tableName} (id TEXT PRIMARY KEY NOT NULL, code TEXT NOT NULL, message TEXT NOT NULL, cause TEXT, source TEXT, occurred_at TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')))`,
|
|
198
|
+
`CREATE INDEX IF NOT EXISTS ${tableName}_occurred_at_idx ON ${tableName} (occurred_at)`
|
|
199
|
+
];
|
|
200
|
+
}
|
|
201
|
+
var SqliteErrorAdapter = class {
|
|
202
|
+
/** @throws {RangeError} if `options.tableName` is not a plain identifier. */
|
|
203
|
+
constructor(client, options = {}) {
|
|
204
|
+
this.client = client;
|
|
205
|
+
this.tableReady = null;
|
|
206
|
+
this.tableName = options.tableName ?? DEFAULT_TABLE_NAME;
|
|
207
|
+
assertValidTableName(this.tableName);
|
|
208
|
+
this.createTable = options.createTable ?? true;
|
|
209
|
+
this.prefix = options.prefix ?? "[error-log-adapter]";
|
|
210
|
+
}
|
|
211
|
+
async log(entry) {
|
|
212
|
+
try {
|
|
213
|
+
await this.ensureTable();
|
|
214
|
+
const serialized = serializeEntry(entry);
|
|
215
|
+
await this.client.run(
|
|
216
|
+
`INSERT INTO ${this.tableName} (id, code, message, cause, source, occurred_at) VALUES (?, ?, ?, ?, ?, ?)`,
|
|
217
|
+
[
|
|
218
|
+
crypto.randomUUID(),
|
|
219
|
+
serialized.code,
|
|
220
|
+
serialized.message,
|
|
221
|
+
serialized.cause === void 0 ? null : JSON.stringify(serialized.cause),
|
|
222
|
+
serialized.source ?? null,
|
|
223
|
+
serialized.timestamp
|
|
224
|
+
]
|
|
225
|
+
);
|
|
226
|
+
return { ok: true };
|
|
227
|
+
} catch (error) {
|
|
228
|
+
console.error(`${this.prefix} SqliteErrorAdapter:`, error);
|
|
229
|
+
return {
|
|
230
|
+
ok: false,
|
|
231
|
+
error: `SqliteErrorAdapter failed to write to table "${this.tableName}": ${describeError(error)}`
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
/** Creates the table once. A failed attempt is retried on the next log call. */
|
|
236
|
+
ensureTable() {
|
|
237
|
+
if (!this.createTable) {
|
|
238
|
+
return Promise.resolve();
|
|
239
|
+
}
|
|
240
|
+
if (this.tableReady === null) {
|
|
241
|
+
this.tableReady = this.runCreateStatements().catch(
|
|
242
|
+
(error) => {
|
|
243
|
+
this.tableReady = null;
|
|
244
|
+
throw error;
|
|
245
|
+
}
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
return this.tableReady;
|
|
249
|
+
}
|
|
250
|
+
async runCreateStatements() {
|
|
251
|
+
for (const sql of buildCreateErrorLogTableSql(this.tableName)) {
|
|
252
|
+
await this.client.run(sql, []);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
// src/adapters/web-storage-error-adapter.ts
|
|
258
|
+
var DEFAULT_STORAGE_KEY = "error_log";
|
|
259
|
+
var DEFAULT_MAX_ENTRIES = 100;
|
|
260
|
+
function isQuotaError(error) {
|
|
261
|
+
if (typeof error !== "object" || error === null) {
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
const { name, code } = error;
|
|
265
|
+
return typeof name === "string" && name.toLowerCase().includes("quota") || code === 22 || code === 1014;
|
|
266
|
+
}
|
|
267
|
+
var WebStorageErrorAdapter = class {
|
|
268
|
+
/** @throws {RangeError} if `options.maxEntries` is not a positive integer. */
|
|
269
|
+
constructor(storage, options = {}) {
|
|
270
|
+
this.storage = storage;
|
|
271
|
+
this.key = options.key ?? DEFAULT_STORAGE_KEY;
|
|
272
|
+
this.maxEntries = options.maxEntries ?? DEFAULT_MAX_ENTRIES;
|
|
273
|
+
this.prefix = options.prefix ?? "[error-log-adapter]";
|
|
274
|
+
if (!Number.isInteger(this.maxEntries) || this.maxEntries < 1) {
|
|
275
|
+
throw new RangeError(
|
|
276
|
+
`WebStorageErrorAdapter: maxEntries must be a positive integer, received ${String(options.maxEntries)}.`
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
async log(entry) {
|
|
281
|
+
try {
|
|
282
|
+
const record = {
|
|
283
|
+
id: crypto.randomUUID(),
|
|
284
|
+
...serializeEntry(entry)
|
|
285
|
+
};
|
|
286
|
+
this.append(record);
|
|
287
|
+
return { ok: true };
|
|
288
|
+
} catch (error) {
|
|
289
|
+
console.error(`${this.prefix} WebStorageErrorAdapter:`, error);
|
|
290
|
+
return {
|
|
291
|
+
ok: false,
|
|
292
|
+
error: `WebStorageErrorAdapter failed to write to storage key "${this.key}": ${describeError(error)}`
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
/** Reads, appends, trims, and writes back. Runs without awaiting, so calls never interleave. */
|
|
297
|
+
append(record) {
|
|
298
|
+
const records = [
|
|
299
|
+
...this.readRecords(),
|
|
300
|
+
record
|
|
301
|
+
].slice(-this.maxEntries);
|
|
302
|
+
for (; ; ) {
|
|
303
|
+
try {
|
|
304
|
+
this.storage.setItem(this.key, JSON.stringify(records));
|
|
305
|
+
return;
|
|
306
|
+
} catch (error) {
|
|
307
|
+
if (!isQuotaError(error) || records.length <= 1) {
|
|
308
|
+
throw error;
|
|
309
|
+
}
|
|
310
|
+
records.shift();
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
/** Returns the stored records, or none if the value is missing or unreadable. */
|
|
315
|
+
readRecords() {
|
|
316
|
+
const stored = this.storage.getItem(this.key);
|
|
317
|
+
if (stored === null) {
|
|
318
|
+
return [];
|
|
319
|
+
}
|
|
320
|
+
try {
|
|
321
|
+
const parsed = JSON.parse(stored);
|
|
322
|
+
if (Array.isArray(parsed)) {
|
|
323
|
+
return parsed;
|
|
324
|
+
}
|
|
325
|
+
console.error(
|
|
326
|
+
`${this.prefix} WebStorageErrorAdapter: storage key "${this.key}" did not hold an array, so it was replaced.`
|
|
327
|
+
);
|
|
328
|
+
} catch (error) {
|
|
329
|
+
console.error(
|
|
330
|
+
`${this.prefix} WebStorageErrorAdapter: storage key "${this.key}" held invalid JSON (${describeError(error)}), so it was replaced.`
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
return [];
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
// src/file-system.ts
|
|
338
|
+
async function loadNodeFileSystem() {
|
|
339
|
+
const moduleName = "node:fs/promises";
|
|
340
|
+
return await import(moduleName);
|
|
341
|
+
}
|
|
342
|
+
function parentDirectory(filePath) {
|
|
343
|
+
const lastSeparator = Math.max(
|
|
344
|
+
filePath.lastIndexOf("/"),
|
|
345
|
+
filePath.lastIndexOf("\\")
|
|
346
|
+
);
|
|
347
|
+
if (lastSeparator < 0) {
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
350
|
+
return lastSeparator === 0 ? "/" : filePath.slice(0, lastSeparator);
|
|
351
|
+
}
|
|
352
|
+
function isMissingFileError(error) {
|
|
353
|
+
return typeof error === "object" && error !== null && error.code === "ENOENT";
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// src/adapters/flat-file-error-adapter.ts
|
|
357
|
+
var DEFAULT_MAX_FILES = 3;
|
|
358
|
+
function escapeLineBreaks(text) {
|
|
359
|
+
return text.replace(/\r?\n/g, "\\n");
|
|
360
|
+
}
|
|
361
|
+
function describeCause(cause) {
|
|
362
|
+
if (typeof cause === "string") {
|
|
363
|
+
return cause;
|
|
364
|
+
}
|
|
365
|
+
if (typeof cause === "object" && cause !== null) {
|
|
366
|
+
const { name, message } = cause;
|
|
367
|
+
if (typeof name === "string" && typeof message === "string") {
|
|
368
|
+
return `${name}: ${message}`;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
try {
|
|
372
|
+
return JSON.stringify(cause) ?? String(cause);
|
|
373
|
+
} catch {
|
|
374
|
+
return String(cause);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
function formatErrorLogLine(entry) {
|
|
378
|
+
const source = entry.source === void 0 ? "" : `${entry.source}: `;
|
|
379
|
+
const cause = entry.cause === void 0 ? "" : ` (cause: ${describeCause(entry.cause)})`;
|
|
380
|
+
return escapeLineBreaks(
|
|
381
|
+
`${entry.timestamp} [${entry.code}] ${source}${entry.message}${cause}`
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
function assertPositiveInteger(value, name) {
|
|
385
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
386
|
+
throw new RangeError(
|
|
387
|
+
`FlatFileErrorAdapter: ${name} must be a positive integer, received ${String(value)}.`
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
var FlatFileErrorAdapter = class {
|
|
392
|
+
/** @throws {RangeError} if `maxBytes` or `maxFiles` is not a positive integer. */
|
|
393
|
+
constructor(filePath, options = {}) {
|
|
394
|
+
this.filePath = filePath;
|
|
395
|
+
this.directoryReady = false;
|
|
396
|
+
this.queue = Promise.resolve();
|
|
397
|
+
this.fs = options.fs;
|
|
398
|
+
this.formatLine = options.formatLine ?? formatErrorLogLine;
|
|
399
|
+
this.maxBytes = options.maxBytes;
|
|
400
|
+
this.maxFiles = options.maxFiles ?? DEFAULT_MAX_FILES;
|
|
401
|
+
this.createDirectory = options.createDirectory ?? true;
|
|
402
|
+
this.prefix = options.prefix ?? "[error-log-adapter]";
|
|
403
|
+
if (this.maxBytes !== void 0) {
|
|
404
|
+
assertPositiveInteger(this.maxBytes, "maxBytes");
|
|
405
|
+
}
|
|
406
|
+
assertPositiveInteger(this.maxFiles, "maxFiles");
|
|
407
|
+
}
|
|
408
|
+
log(entry) {
|
|
409
|
+
const result = this.queue.then(
|
|
410
|
+
() => this.write(entry)
|
|
411
|
+
);
|
|
412
|
+
this.queue = result.catch(() => void 0);
|
|
413
|
+
return result;
|
|
414
|
+
}
|
|
415
|
+
async write(entry) {
|
|
416
|
+
try {
|
|
417
|
+
const fs = await this.getFileSystem();
|
|
418
|
+
const formatted = this.formatLine(serializeEntry(entry));
|
|
419
|
+
const line = formatted.endsWith("\n") ? formatted : `${formatted}
|
|
420
|
+
`;
|
|
421
|
+
await this.prepareDirectory(fs);
|
|
422
|
+
await this.rotateIfNeeded(fs, new TextEncoder().encode(line).length);
|
|
423
|
+
await fs.appendFile(this.filePath, line);
|
|
424
|
+
return { ok: true };
|
|
425
|
+
} catch (error) {
|
|
426
|
+
console.error(`${this.prefix} FlatFileErrorAdapter:`, error);
|
|
427
|
+
return {
|
|
428
|
+
ok: false,
|
|
429
|
+
error: `FlatFileErrorAdapter failed to append to "${this.filePath}": ${describeError(error)}`
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
/** Uses the injected file system, or loads `node:fs/promises` on first use. */
|
|
434
|
+
async getFileSystem() {
|
|
435
|
+
if (this.fs === void 0) {
|
|
436
|
+
this.fs = await loadNodeFileSystem();
|
|
437
|
+
}
|
|
438
|
+
return this.fs;
|
|
439
|
+
}
|
|
440
|
+
async prepareDirectory(fs) {
|
|
441
|
+
if (!this.createDirectory || this.directoryReady) {
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
const directory = parentDirectory(this.filePath);
|
|
445
|
+
if (directory !== null) {
|
|
446
|
+
await fs.mkdir(directory, { recursive: true });
|
|
447
|
+
}
|
|
448
|
+
this.directoryReady = true;
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Moves `app.log` to `app.log.1` (shifting older files up, dropping the
|
|
452
|
+
* oldest) when the next line would push it past `maxBytes`. A failed rotation
|
|
453
|
+
* is reported, but the entry is still written so it is not lost.
|
|
454
|
+
*/
|
|
455
|
+
async rotateIfNeeded(fs, lineBytes) {
|
|
456
|
+
if (this.maxBytes === void 0) {
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
try {
|
|
460
|
+
let currentSize;
|
|
461
|
+
try {
|
|
462
|
+
currentSize = (await fs.stat(this.filePath)).size;
|
|
463
|
+
} catch (error) {
|
|
464
|
+
if (isMissingFileError(error)) {
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
throw error;
|
|
468
|
+
}
|
|
469
|
+
if (currentSize === 0 || currentSize + lineBytes <= this.maxBytes) {
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
await fs.rm(`${this.filePath}.${this.maxFiles}`, { force: true });
|
|
473
|
+
for (let index = this.maxFiles - 1; index >= 1; index -= 1) {
|
|
474
|
+
await this.renameIfPresent(
|
|
475
|
+
fs,
|
|
476
|
+
`${this.filePath}.${index}`,
|
|
477
|
+
`${this.filePath}.${index + 1}`
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
await fs.rename(this.filePath, `${this.filePath}.1`);
|
|
481
|
+
} catch (error) {
|
|
482
|
+
console.error(
|
|
483
|
+
`${this.prefix} FlatFileErrorAdapter: could not rotate "${this.filePath}", so it will keep growing: ${describeError(error)}`
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
async renameIfPresent(fs, from, to) {
|
|
488
|
+
try {
|
|
489
|
+
await fs.rename(from, to);
|
|
490
|
+
} catch (error) {
|
|
491
|
+
if (!isMissingFileError(error)) {
|
|
492
|
+
throw error;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
};
|
|
497
|
+
|
|
498
|
+
// src/adapters/json-file-error-adapter.ts
|
|
499
|
+
var DEFAULT_INDENT = 2;
|
|
500
|
+
var JsonFileErrorAdapter = class {
|
|
501
|
+
/** @throws {RangeError} if `maxEntries` or `indent` is invalid, or `maxEntries` is used outside array mode. */
|
|
502
|
+
constructor(filePath, options = {}) {
|
|
503
|
+
this.filePath = filePath;
|
|
504
|
+
this.directoryReady = false;
|
|
505
|
+
this.queue = Promise.resolve();
|
|
506
|
+
this.mode = options.mode ?? "ndjson";
|
|
507
|
+
this.maxEntries = options.maxEntries;
|
|
508
|
+
this.indent = options.indent ?? DEFAULT_INDENT;
|
|
509
|
+
this.fs = options.fs;
|
|
510
|
+
this.createDirectory = options.createDirectory ?? true;
|
|
511
|
+
this.prefix = options.prefix ?? "[error-log-adapter]";
|
|
512
|
+
if (this.maxEntries !== void 0) {
|
|
513
|
+
if (this.mode !== "array") {
|
|
514
|
+
throw new RangeError(
|
|
515
|
+
`JsonFileErrorAdapter: maxEntries only applies to mode "array", but mode is "${this.mode}". Use mode "array" or remove maxEntries.`
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
if (!Number.isInteger(this.maxEntries) || this.maxEntries < 1) {
|
|
519
|
+
throw new RangeError(
|
|
520
|
+
`JsonFileErrorAdapter: maxEntries must be a positive integer, received ${String(this.maxEntries)}.`
|
|
521
|
+
);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
if (!Number.isInteger(this.indent) || this.indent < 0) {
|
|
525
|
+
throw new RangeError(
|
|
526
|
+
`JsonFileErrorAdapter: indent must be a non-negative integer, received ${String(this.indent)}.`
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
log(entry) {
|
|
531
|
+
const result = this.queue.then(
|
|
532
|
+
() => this.write(entry)
|
|
533
|
+
);
|
|
534
|
+
this.queue = result.catch(() => void 0);
|
|
535
|
+
return result;
|
|
536
|
+
}
|
|
537
|
+
async write(entry) {
|
|
538
|
+
try {
|
|
539
|
+
const record = {
|
|
540
|
+
id: crypto.randomUUID(),
|
|
541
|
+
...serializeEntry(entry)
|
|
542
|
+
};
|
|
543
|
+
const fs = await this.getFileSystem();
|
|
544
|
+
await this.prepareDirectory(fs);
|
|
545
|
+
if (this.mode === "ndjson") {
|
|
546
|
+
await fs.appendFile(this.filePath, `${JSON.stringify(record)}
|
|
547
|
+
`);
|
|
548
|
+
} else {
|
|
549
|
+
await this.rewriteArray(fs, record);
|
|
550
|
+
}
|
|
551
|
+
return { ok: true };
|
|
552
|
+
} catch (error) {
|
|
553
|
+
console.error(`${this.prefix} JsonFileErrorAdapter:`, error);
|
|
554
|
+
return {
|
|
555
|
+
ok: false,
|
|
556
|
+
error: `JsonFileErrorAdapter failed to write "${this.filePath}" in ${this.mode} mode: ${describeError(error)}`
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
async getFileSystem() {
|
|
561
|
+
if (this.fs === void 0) {
|
|
562
|
+
this.fs = await loadNodeFileSystem();
|
|
563
|
+
}
|
|
564
|
+
return this.fs;
|
|
565
|
+
}
|
|
566
|
+
async prepareDirectory(fs) {
|
|
567
|
+
if (!this.createDirectory || this.directoryReady) {
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
const directory = parentDirectory(this.filePath);
|
|
571
|
+
if (directory !== null) {
|
|
572
|
+
await fs.mkdir(directory, { recursive: true });
|
|
573
|
+
}
|
|
574
|
+
this.directoryReady = true;
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* Adds the record to the array and replaces the file. The new content is
|
|
578
|
+
* written to a temporary file first and renamed into place, so a crash
|
|
579
|
+
* mid-write cannot leave a half-written file.
|
|
580
|
+
*/
|
|
581
|
+
async rewriteArray(fs, record) {
|
|
582
|
+
const records = [
|
|
583
|
+
...await this.readRecords(fs),
|
|
584
|
+
record
|
|
585
|
+
];
|
|
586
|
+
const kept = this.maxEntries === void 0 ? records : records.slice(-this.maxEntries);
|
|
587
|
+
const temporaryPath = `${this.filePath}.tmp`;
|
|
588
|
+
await fs.writeFile(
|
|
589
|
+
temporaryPath,
|
|
590
|
+
`${JSON.stringify(kept, null, this.indent)}
|
|
591
|
+
`
|
|
592
|
+
);
|
|
593
|
+
await fs.rename(temporaryPath, this.filePath);
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* Returns the records already in the file. A missing or empty file has none.
|
|
597
|
+
* A file that is not a JSON array is moved to `<path>.corrupt` (so nothing is
|
|
598
|
+
* lost) and logging continues with a fresh file.
|
|
599
|
+
*/
|
|
600
|
+
async readRecords(fs) {
|
|
601
|
+
let content;
|
|
602
|
+
try {
|
|
603
|
+
content = await fs.readFile(this.filePath, "utf8");
|
|
604
|
+
} catch (error) {
|
|
605
|
+
if (isMissingFileError(error)) {
|
|
606
|
+
return [];
|
|
607
|
+
}
|
|
608
|
+
throw error;
|
|
609
|
+
}
|
|
610
|
+
if (content.trim() === "") {
|
|
611
|
+
return [];
|
|
612
|
+
}
|
|
613
|
+
let problem;
|
|
614
|
+
try {
|
|
615
|
+
const parsed = JSON.parse(content);
|
|
616
|
+
if (Array.isArray(parsed)) {
|
|
617
|
+
return parsed;
|
|
618
|
+
}
|
|
619
|
+
problem = "did not hold a JSON array";
|
|
620
|
+
} catch (error) {
|
|
621
|
+
problem = `held invalid JSON (${describeError(error)})`;
|
|
622
|
+
}
|
|
623
|
+
const corruptPath = `${this.filePath}.corrupt`;
|
|
624
|
+
await fs.rename(this.filePath, corruptPath);
|
|
625
|
+
console.error(
|
|
626
|
+
`${this.prefix} JsonFileErrorAdapter: "${this.filePath}" ${problem}, so it was moved to "${corruptPath}" and a new file was started.`
|
|
627
|
+
);
|
|
628
|
+
return [];
|
|
629
|
+
}
|
|
630
|
+
};
|
|
631
|
+
export {
|
|
632
|
+
CompositeErrorAdapter,
|
|
633
|
+
ConsoleErrorAdapter,
|
|
634
|
+
FlatFileErrorAdapter,
|
|
635
|
+
IndexedDbErrorAdapter,
|
|
636
|
+
JsonFileErrorAdapter,
|
|
637
|
+
MongoDbErrorAdapter,
|
|
638
|
+
SqliteErrorAdapter,
|
|
639
|
+
WebStorageErrorAdapter,
|
|
640
|
+
WebhookErrorAdapter,
|
|
641
|
+
buildCreateErrorLogTableSql,
|
|
642
|
+
createErrorLogStore,
|
|
643
|
+
formatErrorLogLine,
|
|
644
|
+
serializeCause,
|
|
645
|
+
serializeEntry
|
|
646
|
+
};
|
|
647
|
+
//# sourceMappingURL=index.mjs.map
|