@richardmcquiston01/error-log-adapter 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -21
- package/dist/index.cjs +321 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +160 -4
- package/dist/index.d.ts +160 -4
- package/dist/index.mjs +320 -7
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -140,6 +140,12 @@ var ConsoleErrorAdapter = class {
|
|
|
140
140
|
}
|
|
141
141
|
};
|
|
142
142
|
|
|
143
|
+
// src/backoff.ts
|
|
144
|
+
function backoffDelayMs(retryNumber, baseMs, maxMs) {
|
|
145
|
+
const ceiling = Math.min(maxMs, baseMs * 2 ** (retryNumber - 1));
|
|
146
|
+
return ceiling / 2 + Math.random() * ceiling / 2;
|
|
147
|
+
}
|
|
148
|
+
|
|
143
149
|
// src/uuid.ts
|
|
144
150
|
function generateId() {
|
|
145
151
|
const cryptoApi = globalThis.crypto;
|
|
@@ -351,6 +357,36 @@ var WebhookErrorAdapter = class {
|
|
|
351
357
|
`could not prepare the entry: ${this.redact(describeError(error))}`
|
|
352
358
|
);
|
|
353
359
|
}
|
|
360
|
+
return this.deliver(body);
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Sends several entries in one request, as a JSON array of the same records
|
|
364
|
+
* `log` sends one at a time (each with an `id`). `BatchingErrorAdapter` uses
|
|
365
|
+
* this when it wraps a webhook, so the receiver must accept an array. The
|
|
366
|
+
* whole request succeeds or fails together.
|
|
367
|
+
*/
|
|
368
|
+
async logBatch(entries) {
|
|
369
|
+
if (entries.length === 0) {
|
|
370
|
+
return { ok: true };
|
|
371
|
+
}
|
|
372
|
+
let body;
|
|
373
|
+
try {
|
|
374
|
+
body = JSON.stringify(
|
|
375
|
+
entries.map((entry) => ({
|
|
376
|
+
id: resolveEntryId(entry),
|
|
377
|
+
...serializeEntry(entry)
|
|
378
|
+
}))
|
|
379
|
+
);
|
|
380
|
+
} catch (error) {
|
|
381
|
+
console.error(`${this.prefix} WebhookErrorAdapter:`, error);
|
|
382
|
+
return this.fail(
|
|
383
|
+
`could not prepare the entries: ${this.redact(describeError(error))}`
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
return this.deliver(body);
|
|
387
|
+
}
|
|
388
|
+
/** Sends one prepared body, trying again as `retries` allows. */
|
|
389
|
+
async deliver(body) {
|
|
354
390
|
let attempt = await this.send(body);
|
|
355
391
|
let attempts = 1;
|
|
356
392
|
while (!attempt.ok && attempt.retryable && attempts <= this.retries) {
|
|
@@ -370,11 +406,7 @@ var WebhookErrorAdapter = class {
|
|
|
370
406
|
if (retryAfterMs !== void 0) {
|
|
371
407
|
return Math.min(retryAfterMs, this.maxRetryDelayMs);
|
|
372
408
|
}
|
|
373
|
-
|
|
374
|
-
this.maxRetryDelayMs,
|
|
375
|
-
this.retryDelayMs * 2 ** (retryNumber - 1)
|
|
376
|
-
);
|
|
377
|
-
return ceiling / 2 + Math.random() * ceiling / 2;
|
|
409
|
+
return backoffDelayMs(retryNumber, this.retryDelayMs, this.maxRetryDelayMs);
|
|
378
410
|
}
|
|
379
411
|
async send(body) {
|
|
380
412
|
const controller = new AbortController();
|
|
@@ -1542,6 +1574,15 @@ var S3ErrorAdapter = class {
|
|
|
1542
1574
|
};
|
|
1543
1575
|
|
|
1544
1576
|
// src/adapters/firebase-error-adapter.ts
|
|
1577
|
+
var DEFAULT_EXPIRE_FIELD = "expire_at";
|
|
1578
|
+
var RESERVED_FIELDS = /* @__PURE__ */ new Set([
|
|
1579
|
+
"id",
|
|
1580
|
+
"code",
|
|
1581
|
+
"message",
|
|
1582
|
+
"timestamp",
|
|
1583
|
+
"source",
|
|
1584
|
+
"cause"
|
|
1585
|
+
]);
|
|
1545
1586
|
function waitForWrite(start, timeoutMs) {
|
|
1546
1587
|
return withTimeout(
|
|
1547
1588
|
start,
|
|
@@ -1566,7 +1607,7 @@ function buildRecord(entry) {
|
|
|
1566
1607
|
return record;
|
|
1567
1608
|
}
|
|
1568
1609
|
var FirestoreErrorAdapter = class {
|
|
1569
|
-
/** @throws {RangeError} if `options.timeoutMs` is not a positive number or `Infinity
|
|
1610
|
+
/** @throws {RangeError} if `options.timeoutMs` is not a positive number or `Infinity`, or the expiry options are invalid. */
|
|
1570
1611
|
constructor(collection, options = {}) {
|
|
1571
1612
|
this.collection = collection;
|
|
1572
1613
|
this.prefix = options.prefix ?? DEFAULT_PREFIX;
|
|
@@ -1574,10 +1615,31 @@ var FirestoreErrorAdapter = class {
|
|
|
1574
1615
|
options.timeoutMs,
|
|
1575
1616
|
"FirestoreErrorAdapter"
|
|
1576
1617
|
);
|
|
1618
|
+
this.expireAfterMs = options.expireAfterMs;
|
|
1619
|
+
this.expireField = options.expireField ?? DEFAULT_EXPIRE_FIELD;
|
|
1620
|
+
if (this.expireAfterMs !== void 0 && (!Number.isFinite(this.expireAfterMs) || this.expireAfterMs <= 0)) {
|
|
1621
|
+
throw new RangeError(
|
|
1622
|
+
`FirestoreErrorAdapter: expireAfterMs must be a positive number of milliseconds, received ${String(options.expireAfterMs)}.`
|
|
1623
|
+
);
|
|
1624
|
+
}
|
|
1625
|
+
if (this.expireField === "" || /[./]/.test(this.expireField) || RESERVED_FIELDS.has(this.expireField)) {
|
|
1626
|
+
throw new RangeError(
|
|
1627
|
+
`FirestoreErrorAdapter: expireField must be a name other than ${[...RESERVED_FIELDS].join(", ")}, without "." or "/", received ${JSON.stringify(this.expireField)}.`
|
|
1628
|
+
);
|
|
1629
|
+
}
|
|
1577
1630
|
}
|
|
1578
1631
|
async log(entry) {
|
|
1579
1632
|
try {
|
|
1580
|
-
const record = buildRecord(entry);
|
|
1633
|
+
const record = { ...buildRecord(entry) };
|
|
1634
|
+
if (this.expireAfterMs !== void 0) {
|
|
1635
|
+
const occurredAt = Date.parse(record.timestamp);
|
|
1636
|
+
if (Number.isNaN(occurredAt)) {
|
|
1637
|
+
throw new RangeError(
|
|
1638
|
+
`timestamp "${record.timestamp}" is not a valid ISO 8601 date, so the expiry time cannot be worked out`
|
|
1639
|
+
);
|
|
1640
|
+
}
|
|
1641
|
+
record[this.expireField] = new Date(occurredAt + this.expireAfterMs);
|
|
1642
|
+
}
|
|
1581
1643
|
await waitForWrite(
|
|
1582
1644
|
() => this.collection.doc(record.id).set(record),
|
|
1583
1645
|
this.timeoutMs
|
|
@@ -2094,7 +2156,258 @@ function describeRejection(output, eventCount) {
|
|
|
2094
2156
|
}
|
|
2095
2157
|
return reasons.length === 0 ? void 0 : `CloudWatch Logs accepted the request but rejected events: ${reasons.join("; ")}`;
|
|
2096
2158
|
}
|
|
2159
|
+
|
|
2160
|
+
// src/adapters/batching-error-adapter.ts
|
|
2161
|
+
var DEFAULT_BATCH_SIZE = 20;
|
|
2162
|
+
var DEFAULT_MAX_WAIT_MS = 5e3;
|
|
2163
|
+
var DEFAULT_RETRIES = 3;
|
|
2164
|
+
var DEFAULT_RETRY_DELAY_MS2 = 500;
|
|
2165
|
+
var DEFAULT_MAX_RETRY_DELAY_MS2 = 3e4;
|
|
2166
|
+
var DEFAULT_MAX_BUFFERED = 1e3;
|
|
2167
|
+
function hasLogBatch(adapter) {
|
|
2168
|
+
return typeof adapter.logBatch === "function";
|
|
2169
|
+
}
|
|
2170
|
+
var BatchingErrorAdapter = class {
|
|
2171
|
+
/** @throws {RangeError} if an option is out of range. */
|
|
2172
|
+
constructor(inner, options = {}) {
|
|
2173
|
+
this.inner = inner;
|
|
2174
|
+
this.queue = [];
|
|
2175
|
+
this.waitTimer = null;
|
|
2176
|
+
this.backoffTimer = null;
|
|
2177
|
+
this.draining = null;
|
|
2178
|
+
this.flushers = 0;
|
|
2179
|
+
this.droppedSinceFlush = 0;
|
|
2180
|
+
this.lastDropReason = "";
|
|
2181
|
+
this.batchSize = options.batchSize ?? DEFAULT_BATCH_SIZE;
|
|
2182
|
+
this.maxWaitMs = options.maxWaitMs ?? DEFAULT_MAX_WAIT_MS;
|
|
2183
|
+
this.retries = options.retries ?? DEFAULT_RETRIES;
|
|
2184
|
+
this.retryDelayMs = options.retryDelayMs ?? DEFAULT_RETRY_DELAY_MS2;
|
|
2185
|
+
this.maxRetryDelayMs = options.maxRetryDelayMs ?? DEFAULT_MAX_RETRY_DELAY_MS2;
|
|
2186
|
+
this.maxBuffered = options.maxBuffered ?? DEFAULT_MAX_BUFFERED;
|
|
2187
|
+
this.useBatchMethod = options.useBatchMethod ?? true;
|
|
2188
|
+
this.onDrop = options.onDrop;
|
|
2189
|
+
this.logPrefix = options.prefix ?? DEFAULT_PREFIX;
|
|
2190
|
+
if (!Number.isInteger(this.batchSize) || this.batchSize < 1) {
|
|
2191
|
+
throw new RangeError(
|
|
2192
|
+
`BatchingErrorAdapter: batchSize must be a positive integer, received ${String(options.batchSize)}.`
|
|
2193
|
+
);
|
|
2194
|
+
}
|
|
2195
|
+
if (Number.isNaN(this.maxWaitMs) || this.maxWaitMs <= 0) {
|
|
2196
|
+
throw new RangeError(
|
|
2197
|
+
`BatchingErrorAdapter: maxWaitMs must be a positive number or Infinity, received ${String(options.maxWaitMs)}.`
|
|
2198
|
+
);
|
|
2199
|
+
}
|
|
2200
|
+
if (!Number.isInteger(this.retries) || this.retries < 0) {
|
|
2201
|
+
throw new RangeError(
|
|
2202
|
+
`BatchingErrorAdapter: retries must be a whole number of 0 or more, received ${String(options.retries)}.`
|
|
2203
|
+
);
|
|
2204
|
+
}
|
|
2205
|
+
if (!Number.isFinite(this.retryDelayMs) || this.retryDelayMs <= 0) {
|
|
2206
|
+
throw new RangeError(
|
|
2207
|
+
`BatchingErrorAdapter: retryDelayMs must be a positive number, received ${String(options.retryDelayMs)}.`
|
|
2208
|
+
);
|
|
2209
|
+
}
|
|
2210
|
+
if (!Number.isFinite(this.maxRetryDelayMs) || this.maxRetryDelayMs < this.retryDelayMs) {
|
|
2211
|
+
throw new RangeError(
|
|
2212
|
+
`BatchingErrorAdapter: maxRetryDelayMs must be a number no smaller than retryDelayMs (${this.retryDelayMs}), received ${String(options.maxRetryDelayMs)}.`
|
|
2213
|
+
);
|
|
2214
|
+
}
|
|
2215
|
+
if (!Number.isInteger(this.maxBuffered) || this.maxBuffered < this.batchSize) {
|
|
2216
|
+
throw new RangeError(
|
|
2217
|
+
`BatchingErrorAdapter: maxBuffered must be a whole number no smaller than batchSize (${this.batchSize}), received ${String(options.maxBuffered)}.`
|
|
2218
|
+
);
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
/** Queues the entry. Resolves ok straight away; delivery happens in the background. */
|
|
2222
|
+
log(entry) {
|
|
2223
|
+
this.queue.push({ entry, failures: 0 });
|
|
2224
|
+
if (this.queue.length > this.maxBuffered) {
|
|
2225
|
+
const overflow = this.queue.splice(
|
|
2226
|
+
0,
|
|
2227
|
+
this.queue.length - this.maxBuffered
|
|
2228
|
+
);
|
|
2229
|
+
this.drop(
|
|
2230
|
+
overflow,
|
|
2231
|
+
`the buffer of ${this.maxBuffered} entries was full because the wrapped adapter keeps failing`
|
|
2232
|
+
);
|
|
2233
|
+
}
|
|
2234
|
+
if (this.queue.length >= this.batchSize) {
|
|
2235
|
+
this.startDraining();
|
|
2236
|
+
} else {
|
|
2237
|
+
this.scheduleDrain();
|
|
2238
|
+
}
|
|
2239
|
+
return Promise.resolve({ ok: true });
|
|
2240
|
+
}
|
|
2241
|
+
/**
|
|
2242
|
+
* Delivers everything queued, retrying as configured, and resolves once the
|
|
2243
|
+
* queue is empty. Resolves `ok: false` if any entries were given up on since
|
|
2244
|
+
* the last flush. With retries on and the sink down this can take a while
|
|
2245
|
+
* (the waits add up to well over a minute with the defaults).
|
|
2246
|
+
*/
|
|
2247
|
+
async flush() {
|
|
2248
|
+
this.flushers += 1;
|
|
2249
|
+
this.backoffTimer?.ref?.();
|
|
2250
|
+
try {
|
|
2251
|
+
this.startDraining();
|
|
2252
|
+
while (this.draining !== null) {
|
|
2253
|
+
await this.draining;
|
|
2254
|
+
}
|
|
2255
|
+
} finally {
|
|
2256
|
+
this.flushers -= 1;
|
|
2257
|
+
}
|
|
2258
|
+
if (this.droppedSinceFlush > 0) {
|
|
2259
|
+
const count = this.droppedSinceFlush;
|
|
2260
|
+
const reason = this.lastDropReason;
|
|
2261
|
+
this.droppedSinceFlush = 0;
|
|
2262
|
+
this.lastDropReason = "";
|
|
2263
|
+
return {
|
|
2264
|
+
ok: false,
|
|
2265
|
+
error: `BatchingErrorAdapter gave up on ${count} entr${count === 1 ? "y" : "ies"}: ${reason}`
|
|
2266
|
+
};
|
|
2267
|
+
}
|
|
2268
|
+
return { ok: true };
|
|
2269
|
+
}
|
|
2270
|
+
scheduleDrain() {
|
|
2271
|
+
if (this.waitTimer !== null || this.maxWaitMs === Infinity) {
|
|
2272
|
+
return;
|
|
2273
|
+
}
|
|
2274
|
+
const timer = setTimeout(() => {
|
|
2275
|
+
this.waitTimer = null;
|
|
2276
|
+
this.startDraining();
|
|
2277
|
+
}, this.maxWaitMs);
|
|
2278
|
+
timer.unref?.();
|
|
2279
|
+
this.waitTimer = timer;
|
|
2280
|
+
}
|
|
2281
|
+
startDraining() {
|
|
2282
|
+
if (this.waitTimer !== null) {
|
|
2283
|
+
clearTimeout(this.waitTimer);
|
|
2284
|
+
this.waitTimer = null;
|
|
2285
|
+
}
|
|
2286
|
+
if (this.draining !== null || this.queue.length === 0) {
|
|
2287
|
+
return;
|
|
2288
|
+
}
|
|
2289
|
+
this.draining = this.drain().catch((error) => {
|
|
2290
|
+
console.error(`${this.logPrefix} BatchingErrorAdapter:`, error);
|
|
2291
|
+
}).finally(() => {
|
|
2292
|
+
this.draining = null;
|
|
2293
|
+
if (this.queue.length > 0) {
|
|
2294
|
+
this.scheduleDrain();
|
|
2295
|
+
}
|
|
2296
|
+
});
|
|
2297
|
+
}
|
|
2298
|
+
/** Delivers batches until the queue is empty, waiting out a backoff after each failure. */
|
|
2299
|
+
async drain() {
|
|
2300
|
+
let failedRounds = 0;
|
|
2301
|
+
while (this.queue.length > 0) {
|
|
2302
|
+
const batch = this.queue.slice(0, this.batchSize);
|
|
2303
|
+
const failure = await this.deliver(batch);
|
|
2304
|
+
if (failure === void 0) {
|
|
2305
|
+
failedRounds = 0;
|
|
2306
|
+
continue;
|
|
2307
|
+
}
|
|
2308
|
+
failedRounds += 1;
|
|
2309
|
+
const exhausted = this.queue.filter(
|
|
2310
|
+
(item) => item.failures > this.retries
|
|
2311
|
+
);
|
|
2312
|
+
if (exhausted.length > 0) {
|
|
2313
|
+
this.queue = this.queue.filter(
|
|
2314
|
+
(item) => item.failures <= this.retries
|
|
2315
|
+
);
|
|
2316
|
+
this.drop(
|
|
2317
|
+
exhausted,
|
|
2318
|
+
`delivery failed ${this.retries + 1} time${this.retries === 0 ? "" : "s"}; last error: ${failure}`
|
|
2319
|
+
);
|
|
2320
|
+
}
|
|
2321
|
+
if (this.queue.length > 0) {
|
|
2322
|
+
await this.backoff(failedRounds);
|
|
2323
|
+
}
|
|
2324
|
+
}
|
|
2325
|
+
}
|
|
2326
|
+
/**
|
|
2327
|
+
* Tries to deliver `batch` (the front of the queue). Delivered entries leave
|
|
2328
|
+
* the queue; an entry that failed has its failure count raised and stays.
|
|
2329
|
+
* Resolves to the failure message, or undefined when everything went through.
|
|
2330
|
+
*/
|
|
2331
|
+
async deliver(batch) {
|
|
2332
|
+
const remove = (delivered) => {
|
|
2333
|
+
const gone = new Set(delivered);
|
|
2334
|
+
this.queue = this.queue.filter(
|
|
2335
|
+
(item) => !gone.has(item)
|
|
2336
|
+
);
|
|
2337
|
+
};
|
|
2338
|
+
if (this.useBatchMethod && hasLogBatch(this.inner)) {
|
|
2339
|
+
const result = await this.safely(
|
|
2340
|
+
() => this.inner.logBatch(
|
|
2341
|
+
batch.map(
|
|
2342
|
+
(item) => item.entry
|
|
2343
|
+
)
|
|
2344
|
+
)
|
|
2345
|
+
);
|
|
2346
|
+
if (result.ok) {
|
|
2347
|
+
remove(batch);
|
|
2348
|
+
return void 0;
|
|
2349
|
+
}
|
|
2350
|
+
for (const item of batch) {
|
|
2351
|
+
item.failures += 1;
|
|
2352
|
+
}
|
|
2353
|
+
return result.error ?? "the wrapped adapter reported a failure";
|
|
2354
|
+
}
|
|
2355
|
+
for (const item of batch) {
|
|
2356
|
+
const result = await this.safely(
|
|
2357
|
+
() => this.inner.log(item.entry)
|
|
2358
|
+
);
|
|
2359
|
+
if (result.ok) {
|
|
2360
|
+
remove([item]);
|
|
2361
|
+
continue;
|
|
2362
|
+
}
|
|
2363
|
+
item.failures += 1;
|
|
2364
|
+
return result.error ?? "the wrapped adapter reported a failure";
|
|
2365
|
+
}
|
|
2366
|
+
return void 0;
|
|
2367
|
+
}
|
|
2368
|
+
/** The wrapped adapter should not throw, but a bug in one must not stop the queue. */
|
|
2369
|
+
async safely(call) {
|
|
2370
|
+
try {
|
|
2371
|
+
return await call();
|
|
2372
|
+
} catch (error) {
|
|
2373
|
+
return { ok: false, error: describeError(error) };
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
2376
|
+
backoff(failedRounds) {
|
|
2377
|
+
return new Promise((resolve) => {
|
|
2378
|
+
const timer = setTimeout(
|
|
2379
|
+
() => {
|
|
2380
|
+
this.backoffTimer = null;
|
|
2381
|
+
resolve();
|
|
2382
|
+
},
|
|
2383
|
+
backoffDelayMs(failedRounds, this.retryDelayMs, this.maxRetryDelayMs)
|
|
2384
|
+
);
|
|
2385
|
+
if (this.flushers === 0) {
|
|
2386
|
+
timer.unref?.();
|
|
2387
|
+
}
|
|
2388
|
+
this.backoffTimer = timer;
|
|
2389
|
+
});
|
|
2390
|
+
}
|
|
2391
|
+
drop(items, reason) {
|
|
2392
|
+
if (items.length === 0) {
|
|
2393
|
+
return;
|
|
2394
|
+
}
|
|
2395
|
+
this.droppedSinceFlush += items.length;
|
|
2396
|
+
this.lastDropReason = reason;
|
|
2397
|
+
console.error(
|
|
2398
|
+
`${this.logPrefix} BatchingErrorAdapter: gave up on ${items.length} entr${items.length === 1 ? "y" : "ies"}: ${reason}`
|
|
2399
|
+
);
|
|
2400
|
+
try {
|
|
2401
|
+
this.onDrop?.(
|
|
2402
|
+
items.map((item) => item.entry),
|
|
2403
|
+
reason
|
|
2404
|
+
);
|
|
2405
|
+
} catch {
|
|
2406
|
+
}
|
|
2407
|
+
}
|
|
2408
|
+
};
|
|
2097
2409
|
export {
|
|
2410
|
+
BatchingErrorAdapter,
|
|
2098
2411
|
CloudWatchLogsErrorAdapter,
|
|
2099
2412
|
CompositeErrorAdapter,
|
|
2100
2413
|
ConsoleErrorAdapter,
|