@richardmcquiston01/error-log-adapter 0.8.0 → 0.9.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 +42 -21
- package/dist/index.cjs +289 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +129 -1
- package/dist/index.d.ts +129 -1
- package/dist/index.mjs +288 -5
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -70,27 +70,28 @@ Constructor options are checked when the adapter is created and throw a descript
|
|
|
70
70
|
|
|
71
71
|
Every adapter takes an optional `prefix` (default `[error-log-adapter]`) that is printed before its failure messages.
|
|
72
72
|
|
|
73
|
-
| Adapter | Sink
|
|
74
|
-
| --------------------------------------------------- |
|
|
75
|
-
| `ConsoleErrorAdapter` | `console.error`
|
|
76
|
-
| `WebhookErrorAdapter(url)` | HTTP `POST` of the entry as JSON
|
|
77
|
-
| `MongoDbErrorAdapter(collection)` | Anything with `insertOne`, such as a MongoDB `Collection`
|
|
78
|
-
| `PostgresErrorAdapter(client)` | A Postgres table, through `pg`
|
|
79
|
-
| `MySqlErrorAdapter(client)` | A MySQL / MariaDB table, through `mysql2/promise`
|
|
80
|
-
| `SqliteErrorAdapter(client)` | A SQLite table, through any driver
|
|
81
|
-
| `SupabaseErrorAdapter(client)` | A Supabase table, through `supabase-js`
|
|
82
|
-
| `RedisErrorAdapter(client)` | A Redis list or stream
|
|
83
|
-
| `S3ErrorAdapter(client, bucket)` | S3, DigitalOcean Spaces, R2, MinIO objects
|
|
84
|
-
| `FirestoreErrorAdapter(collection)` | A Firestore collection
|
|
85
|
-
| `RealtimeDatabaseErrorAdapter(ref)` | A Realtime Database reference
|
|
86
|
-
| `FlatFileErrorAdapter(path)` | One text line per entry
|
|
87
|
-
| `JsonFileErrorAdapter(path)` | NDJSON lines or one JSON array
|
|
88
|
-
| `IndexedDbErrorAdapter(database)` | An IndexedDB object store
|
|
89
|
-
| `WebStorageErrorAdapter(storage)` | `sessionStorage` or `localStorage`, as one capped array
|
|
90
|
-
| `EventEmitterErrorAdapter(emitter)` | An event on an emitter you own (in-process hooks)
|
|
91
|
-
| `WebSocketErrorAdapter(socket)` | One JSON message over a socket you own
|
|
92
|
-
| `CloudWatchLogsErrorAdapter(client, group, stream)` | AWS CloudWatch Logs events
|
|
93
|
-
| `
|
|
73
|
+
| Adapter | Sink | Runs in | Main options |
|
|
74
|
+
| --------------------------------------------------- | -------------------------------------------------------------- | ------------- | ------------------------------------------------------------------- |
|
|
75
|
+
| `ConsoleErrorAdapter` | `console.error` | Anywhere | |
|
|
76
|
+
| `WebhookErrorAdapter(url)` | HTTP `POST` of the entry as JSON | Anywhere | `headers`, `timeoutMs`, `retries`, `signingSecret`, `allowInsecure` |
|
|
77
|
+
| `MongoDbErrorAdapter(collection)` | Anything with `insertOne`, such as a MongoDB `Collection` | Node | `timeoutMs` |
|
|
78
|
+
| `PostgresErrorAdapter(client)` | A Postgres table, through `pg` | Node | `tableName`, `createTable`, `timeoutMs` |
|
|
79
|
+
| `MySqlErrorAdapter(client)` | A MySQL / MariaDB table, through `mysql2/promise` | Node | `tableName`, `createTable`, `timeoutMs` |
|
|
80
|
+
| `SqliteErrorAdapter(client)` | A SQLite table, through any driver | Node, Bun | `tableName`, `createTable`, `timeoutMs` |
|
|
81
|
+
| `SupabaseErrorAdapter(client)` | A Supabase table, through `supabase-js` | Anywhere | `tableName`, `timeoutMs` |
|
|
82
|
+
| `RedisErrorAdapter(client)` | A Redis list or stream | Node | `mode`, `key`, `maxEntries`, `side`, `timeoutMs` |
|
|
83
|
+
| `S3ErrorAdapter(client, bucket)` | S3, DigitalOcean Spaces, R2, MinIO objects | Node | `keyPrefix`, `batch`, `contentType`, `timeoutMs` |
|
|
84
|
+
| `FirestoreErrorAdapter(collection)` | A Firestore collection | Node, browser | `timeoutMs` |
|
|
85
|
+
| `RealtimeDatabaseErrorAdapter(ref)` | A Realtime Database reference | Node, browser | `timeoutMs` |
|
|
86
|
+
| `FlatFileErrorAdapter(path)` | One text line per entry | Node, Bun | `formatLine`, `maxBytes`, `maxFiles` |
|
|
87
|
+
| `JsonFileErrorAdapter(path)` | NDJSON lines or one JSON array | Node, Bun | `mode`, `maxEntries`, `indent` |
|
|
88
|
+
| `IndexedDbErrorAdapter(database)` | An IndexedDB object store | Browser | `storeName`, `timeoutMs` |
|
|
89
|
+
| `WebStorageErrorAdapter(storage)` | `sessionStorage` or `localStorage`, as one capped array | Browser | `key`, `maxEntries`, `locks` |
|
|
90
|
+
| `EventEmitterErrorAdapter(emitter)` | An event on an emitter you own (in-process hooks) | Anywhere | `eventName`, `requireListener` |
|
|
91
|
+
| `WebSocketErrorAdapter(socket)` | One JSON message over a socket you own | Anywhere | (none) |
|
|
92
|
+
| `CloudWatchLogsErrorAdapter(client, group, stream)` | AWS CloudWatch Logs events | Node | `batch`, `createMissing`, `timeoutMs` |
|
|
93
|
+
| `BatchingErrorAdapter(adapter)` | A queue in front of any adapter: batches, retries with backoff | Anywhere | `batchSize`, `maxWaitMs`, `retries`, `maxBuffered`, `onDrop` |
|
|
94
|
+
| `CompositeErrorAdapter([...])` | Several adapters at once; succeeds only if all succeed | Anywhere | `timeoutMs` |
|
|
94
95
|
|
|
95
96
|
Every stored record has the same shape: a UUID `id`, `code`, `message`, `timestamp` (ISO 8601), and optional `source` and `cause`. An `Error` cause is stored as `{ name, message, stack }`. `serializeEntry` and `serializeCause` are exported if you build your own sink.
|
|
96
97
|
|
|
@@ -105,6 +106,7 @@ What changes durability or size, by adapter:
|
|
|
105
106
|
- **Event emitter**: each entry is emitted as an `error-log` event whose payload is the stored record, for in-process hooks such as a toast, a metrics counter or a test assertion. Listeners run synchronously, so one that throws makes `log()` return `ok: false` with its message. Node's `EventEmitter` and mitt work as they are; a browser `EventTarget` needs a one-line wrapper (shown in the example below). Don't name the event `error` on a Node `EventEmitter`: it throws when nobody listens. Node's `emit` returns `false` when there are no listeners; pass `requireListener: true` to report that as a failure instead of letting the entry vanish.
|
|
106
107
|
- **WebSocket**: each entry is sent as one JSON text message (the serialized entry plus its UUID `id`) over a socket you own; the adapter never connects, reconnects or buffers. If the socket's `readyState` is not open (connecting, closing or closed), `log()` returns `ok: false` and says which. `ok: true` means the socket accepted the message, not that the server got it, since a WebSocket has no acknowledgement. Browsers, Node's global `WebSocket`, Deno, Bun and `ws` all fit. Pair it with a durable adapter in a `CompositeErrorAdapter` if entries must survive the socket being down.
|
|
107
108
|
- **CloudWatch Logs**: each entry is sent through `PutLogEvents` as a JSON event (the serialized entry plus its UUID `id`) stamped with the entry's time, so Logs Insights can query its fields. Pass an AWS SDK v3 `CloudWatchLogs` client. AWS no longer needs sequence tokens, so none are tracked. If the stream (or group) does not exist, the adapter creates it and tries again once (`createMissing`, on by default; it needs `logs:CreateLogStream` and `logs:CreateLogGroup`, and a group created this way never expires its logs, so set a retention policy). CloudWatch can answer ok and still refuse events older than 14 days or more than 2 hours ahead; that is reported as `ok: false`. An event over 256 KiB is refused before sending. With `batch: { batchSize, maxWaitMs }`, events are buffered and sent together (up to 10,000 events and 1 MiB per request, oldest first), `log()` returns `ok` as soon as the event is buffered, and you should call `flush()` before the process exits. In Lambda and ECS, stdout already reaches CloudWatch; this is for apps running elsewhere.
|
|
109
|
+
- **Batching wrapper**: `BatchingErrorAdapter` puts a queue in front of any other adapter. `log()` returns `ok: true` as soon as the entry is queued, so a slow or failing sink never holds up the app, and entries are delivered in order in batches (`batchSize`, default 20, or after `maxWaitMs`, default 5000). A failed delivery is retried with exponential backoff (`retries`, default 3) and then given up on: the entries go to `onDrop` and the console, and `flush()` returns `ok: false` once. Delivery stops at the first failure so a dead sink is not hammered. If the wrapped adapter has a `logBatch` method (the webhook does) the batch goes in one call, which for the webhook means one request carrying a JSON array, so the receiver must accept an array; pass `useBatchMethod: false` to keep one request per entry. `ok: true` means _queued_, not stored, and the queue lives in memory: call `flush()` before the process exits, and put a durable adapter beside it in a `CompositeErrorAdapter` if entries must survive a crash. Do not also enable the wrapped adapter's own retries (the webhook's `retries`); they multiply. The buffer holds `maxBuffered` entries (default 1000) and drops the oldest past that.
|
|
108
110
|
- **Webhook**: the URL must be `https://` (or `http://localhost`, `127.0.0.1`, `[::1]`) unless you pass `allowInsecure: true`, because entries carry messages and stack traces. Redirects are not followed, since a redirect would turn the `POST` into a body-less `GET` or re-send the entry and your headers to another host; a redirect is reported as `ok: false`. Failure messages contain only the origin, never the path or query, because webhook URLs often hold a secret. Set `retries` to try again after a network error, a timeout, HTTP 408, 425, 429 or 5xx (exponential backoff with jitter from `retryDelayMs`, capped by `maxRetryDelayMs`, honoring `Retry-After`; other 4xx answers are never retried). With `retries` set, every attempt sends the same body including an `id`, so the receiver can drop a repeat. Set `signingSecret` to add `X-Signature-Timestamp` and `X-Signature-256` (`sha256=` + hex HMAC-SHA256 of `<timestamp>.<body>`); the receiver recomputes it and rejects old timestamps.
|
|
109
111
|
|
|
110
112
|
### When a sink is slow or down
|
|
@@ -275,6 +277,23 @@ const errorLog = new CloudWatchLogsErrorAdapter(
|
|
|
275
277
|
process.on('beforeExit', () => void errorLog.flush());
|
|
276
278
|
```
|
|
277
279
|
|
|
280
|
+
Batch and retry in front of a webhook, so a slow endpoint never holds up the app:
|
|
281
|
+
|
|
282
|
+
```ts
|
|
283
|
+
const errorLog = new BatchingErrorAdapter(
|
|
284
|
+
new WebhookErrorAdapter('https://example.com/errors', {
|
|
285
|
+
signingSecret: process.env.ERROR_WEBHOOK_SECRET,
|
|
286
|
+
}),
|
|
287
|
+
{
|
|
288
|
+
batchSize: 50,
|
|
289
|
+
maxWaitMs: 2000,
|
|
290
|
+
onDrop: (entries, reason) =>
|
|
291
|
+
console.warn(`lost ${entries.length}: ${reason}`),
|
|
292
|
+
}
|
|
293
|
+
);
|
|
294
|
+
process.on('beforeExit', () => void errorLog.flush());
|
|
295
|
+
```
|
|
296
|
+
|
|
278
297
|
Write your own adapter by implementing the interface. Catch failures and return them rather than throwing:
|
|
279
298
|
|
|
280
299
|
```ts
|
|
@@ -303,6 +322,8 @@ class SlackErrorAdapter implements ErrorLogAdapter {
|
|
|
303
322
|
}
|
|
304
323
|
```
|
|
305
324
|
|
|
325
|
+
The full guide, [docs/custom-adapters.md](./docs/custom-adapters.md), covers the contract, timeouts, entry ids, buffering adapters, testing, and a checklist.
|
|
326
|
+
|
|
306
327
|
See [ROADMAP.md](./ROADMAP.md) for planned adapters and [CHANGELOG.md](./CHANGELOG.md) for what changed in each release.
|
|
307
328
|
|
|
308
329
|
### Development
|
package/dist/index.cjs
CHANGED
|
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
+
BatchingErrorAdapter: () => BatchingErrorAdapter,
|
|
23
24
|
CloudWatchLogsErrorAdapter: () => CloudWatchLogsErrorAdapter,
|
|
24
25
|
CompositeErrorAdapter: () => CompositeErrorAdapter,
|
|
25
26
|
ConsoleErrorAdapter: () => ConsoleErrorAdapter,
|
|
@@ -192,6 +193,12 @@ var ConsoleErrorAdapter = class {
|
|
|
192
193
|
}
|
|
193
194
|
};
|
|
194
195
|
|
|
196
|
+
// src/backoff.ts
|
|
197
|
+
function backoffDelayMs(retryNumber, baseMs, maxMs) {
|
|
198
|
+
const ceiling = Math.min(maxMs, baseMs * 2 ** (retryNumber - 1));
|
|
199
|
+
return ceiling / 2 + Math.random() * ceiling / 2;
|
|
200
|
+
}
|
|
201
|
+
|
|
195
202
|
// src/uuid.ts
|
|
196
203
|
function generateId() {
|
|
197
204
|
const cryptoApi = globalThis.crypto;
|
|
@@ -403,6 +410,36 @@ var WebhookErrorAdapter = class {
|
|
|
403
410
|
`could not prepare the entry: ${this.redact(describeError(error))}`
|
|
404
411
|
);
|
|
405
412
|
}
|
|
413
|
+
return this.deliver(body);
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Sends several entries in one request, as a JSON array of the same records
|
|
417
|
+
* `log` sends one at a time (each with an `id`). `BatchingErrorAdapter` uses
|
|
418
|
+
* this when it wraps a webhook, so the receiver must accept an array. The
|
|
419
|
+
* whole request succeeds or fails together.
|
|
420
|
+
*/
|
|
421
|
+
async logBatch(entries) {
|
|
422
|
+
if (entries.length === 0) {
|
|
423
|
+
return { ok: true };
|
|
424
|
+
}
|
|
425
|
+
let body;
|
|
426
|
+
try {
|
|
427
|
+
body = JSON.stringify(
|
|
428
|
+
entries.map((entry) => ({
|
|
429
|
+
id: resolveEntryId(entry),
|
|
430
|
+
...serializeEntry(entry)
|
|
431
|
+
}))
|
|
432
|
+
);
|
|
433
|
+
} catch (error) {
|
|
434
|
+
console.error(`${this.prefix} WebhookErrorAdapter:`, error);
|
|
435
|
+
return this.fail(
|
|
436
|
+
`could not prepare the entries: ${this.redact(describeError(error))}`
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
return this.deliver(body);
|
|
440
|
+
}
|
|
441
|
+
/** Sends one prepared body, trying again as `retries` allows. */
|
|
442
|
+
async deliver(body) {
|
|
406
443
|
let attempt = await this.send(body);
|
|
407
444
|
let attempts = 1;
|
|
408
445
|
while (!attempt.ok && attempt.retryable && attempts <= this.retries) {
|
|
@@ -422,11 +459,7 @@ var WebhookErrorAdapter = class {
|
|
|
422
459
|
if (retryAfterMs !== void 0) {
|
|
423
460
|
return Math.min(retryAfterMs, this.maxRetryDelayMs);
|
|
424
461
|
}
|
|
425
|
-
|
|
426
|
-
this.maxRetryDelayMs,
|
|
427
|
-
this.retryDelayMs * 2 ** (retryNumber - 1)
|
|
428
|
-
);
|
|
429
|
-
return ceiling / 2 + Math.random() * ceiling / 2;
|
|
462
|
+
return backoffDelayMs(retryNumber, this.retryDelayMs, this.maxRetryDelayMs);
|
|
430
463
|
}
|
|
431
464
|
async send(body) {
|
|
432
465
|
const controller = new AbortController();
|
|
@@ -2146,8 +2179,259 @@ function describeRejection(output, eventCount) {
|
|
|
2146
2179
|
}
|
|
2147
2180
|
return reasons.length === 0 ? void 0 : `CloudWatch Logs accepted the request but rejected events: ${reasons.join("; ")}`;
|
|
2148
2181
|
}
|
|
2182
|
+
|
|
2183
|
+
// src/adapters/batching-error-adapter.ts
|
|
2184
|
+
var DEFAULT_BATCH_SIZE = 20;
|
|
2185
|
+
var DEFAULT_MAX_WAIT_MS = 5e3;
|
|
2186
|
+
var DEFAULT_RETRIES = 3;
|
|
2187
|
+
var DEFAULT_RETRY_DELAY_MS2 = 500;
|
|
2188
|
+
var DEFAULT_MAX_RETRY_DELAY_MS2 = 3e4;
|
|
2189
|
+
var DEFAULT_MAX_BUFFERED = 1e3;
|
|
2190
|
+
function hasLogBatch(adapter) {
|
|
2191
|
+
return typeof adapter.logBatch === "function";
|
|
2192
|
+
}
|
|
2193
|
+
var BatchingErrorAdapter = class {
|
|
2194
|
+
/** @throws {RangeError} if an option is out of range. */
|
|
2195
|
+
constructor(inner, options = {}) {
|
|
2196
|
+
this.inner = inner;
|
|
2197
|
+
this.queue = [];
|
|
2198
|
+
this.waitTimer = null;
|
|
2199
|
+
this.backoffTimer = null;
|
|
2200
|
+
this.draining = null;
|
|
2201
|
+
this.flushers = 0;
|
|
2202
|
+
this.droppedSinceFlush = 0;
|
|
2203
|
+
this.lastDropReason = "";
|
|
2204
|
+
this.batchSize = options.batchSize ?? DEFAULT_BATCH_SIZE;
|
|
2205
|
+
this.maxWaitMs = options.maxWaitMs ?? DEFAULT_MAX_WAIT_MS;
|
|
2206
|
+
this.retries = options.retries ?? DEFAULT_RETRIES;
|
|
2207
|
+
this.retryDelayMs = options.retryDelayMs ?? DEFAULT_RETRY_DELAY_MS2;
|
|
2208
|
+
this.maxRetryDelayMs = options.maxRetryDelayMs ?? DEFAULT_MAX_RETRY_DELAY_MS2;
|
|
2209
|
+
this.maxBuffered = options.maxBuffered ?? DEFAULT_MAX_BUFFERED;
|
|
2210
|
+
this.useBatchMethod = options.useBatchMethod ?? true;
|
|
2211
|
+
this.onDrop = options.onDrop;
|
|
2212
|
+
this.logPrefix = options.prefix ?? DEFAULT_PREFIX;
|
|
2213
|
+
if (!Number.isInteger(this.batchSize) || this.batchSize < 1) {
|
|
2214
|
+
throw new RangeError(
|
|
2215
|
+
`BatchingErrorAdapter: batchSize must be a positive integer, received ${String(options.batchSize)}.`
|
|
2216
|
+
);
|
|
2217
|
+
}
|
|
2218
|
+
if (Number.isNaN(this.maxWaitMs) || this.maxWaitMs <= 0) {
|
|
2219
|
+
throw new RangeError(
|
|
2220
|
+
`BatchingErrorAdapter: maxWaitMs must be a positive number or Infinity, received ${String(options.maxWaitMs)}.`
|
|
2221
|
+
);
|
|
2222
|
+
}
|
|
2223
|
+
if (!Number.isInteger(this.retries) || this.retries < 0) {
|
|
2224
|
+
throw new RangeError(
|
|
2225
|
+
`BatchingErrorAdapter: retries must be a whole number of 0 or more, received ${String(options.retries)}.`
|
|
2226
|
+
);
|
|
2227
|
+
}
|
|
2228
|
+
if (!Number.isFinite(this.retryDelayMs) || this.retryDelayMs <= 0) {
|
|
2229
|
+
throw new RangeError(
|
|
2230
|
+
`BatchingErrorAdapter: retryDelayMs must be a positive number, received ${String(options.retryDelayMs)}.`
|
|
2231
|
+
);
|
|
2232
|
+
}
|
|
2233
|
+
if (!Number.isFinite(this.maxRetryDelayMs) || this.maxRetryDelayMs < this.retryDelayMs) {
|
|
2234
|
+
throw new RangeError(
|
|
2235
|
+
`BatchingErrorAdapter: maxRetryDelayMs must be a number no smaller than retryDelayMs (${this.retryDelayMs}), received ${String(options.maxRetryDelayMs)}.`
|
|
2236
|
+
);
|
|
2237
|
+
}
|
|
2238
|
+
if (!Number.isInteger(this.maxBuffered) || this.maxBuffered < this.batchSize) {
|
|
2239
|
+
throw new RangeError(
|
|
2240
|
+
`BatchingErrorAdapter: maxBuffered must be a whole number no smaller than batchSize (${this.batchSize}), received ${String(options.maxBuffered)}.`
|
|
2241
|
+
);
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
/** Queues the entry. Resolves ok straight away; delivery happens in the background. */
|
|
2245
|
+
log(entry) {
|
|
2246
|
+
this.queue.push({ entry, failures: 0 });
|
|
2247
|
+
if (this.queue.length > this.maxBuffered) {
|
|
2248
|
+
const overflow = this.queue.splice(
|
|
2249
|
+
0,
|
|
2250
|
+
this.queue.length - this.maxBuffered
|
|
2251
|
+
);
|
|
2252
|
+
this.drop(
|
|
2253
|
+
overflow,
|
|
2254
|
+
`the buffer of ${this.maxBuffered} entries was full because the wrapped adapter keeps failing`
|
|
2255
|
+
);
|
|
2256
|
+
}
|
|
2257
|
+
if (this.queue.length >= this.batchSize) {
|
|
2258
|
+
this.startDraining();
|
|
2259
|
+
} else {
|
|
2260
|
+
this.scheduleDrain();
|
|
2261
|
+
}
|
|
2262
|
+
return Promise.resolve({ ok: true });
|
|
2263
|
+
}
|
|
2264
|
+
/**
|
|
2265
|
+
* Delivers everything queued, retrying as configured, and resolves once the
|
|
2266
|
+
* queue is empty. Resolves `ok: false` if any entries were given up on since
|
|
2267
|
+
* the last flush. With retries on and the sink down this can take a while
|
|
2268
|
+
* (the waits add up to well over a minute with the defaults).
|
|
2269
|
+
*/
|
|
2270
|
+
async flush() {
|
|
2271
|
+
this.flushers += 1;
|
|
2272
|
+
this.backoffTimer?.ref?.();
|
|
2273
|
+
try {
|
|
2274
|
+
this.startDraining();
|
|
2275
|
+
while (this.draining !== null) {
|
|
2276
|
+
await this.draining;
|
|
2277
|
+
}
|
|
2278
|
+
} finally {
|
|
2279
|
+
this.flushers -= 1;
|
|
2280
|
+
}
|
|
2281
|
+
if (this.droppedSinceFlush > 0) {
|
|
2282
|
+
const count = this.droppedSinceFlush;
|
|
2283
|
+
const reason = this.lastDropReason;
|
|
2284
|
+
this.droppedSinceFlush = 0;
|
|
2285
|
+
this.lastDropReason = "";
|
|
2286
|
+
return {
|
|
2287
|
+
ok: false,
|
|
2288
|
+
error: `BatchingErrorAdapter gave up on ${count} entr${count === 1 ? "y" : "ies"}: ${reason}`
|
|
2289
|
+
};
|
|
2290
|
+
}
|
|
2291
|
+
return { ok: true };
|
|
2292
|
+
}
|
|
2293
|
+
scheduleDrain() {
|
|
2294
|
+
if (this.waitTimer !== null || this.maxWaitMs === Infinity) {
|
|
2295
|
+
return;
|
|
2296
|
+
}
|
|
2297
|
+
const timer = setTimeout(() => {
|
|
2298
|
+
this.waitTimer = null;
|
|
2299
|
+
this.startDraining();
|
|
2300
|
+
}, this.maxWaitMs);
|
|
2301
|
+
timer.unref?.();
|
|
2302
|
+
this.waitTimer = timer;
|
|
2303
|
+
}
|
|
2304
|
+
startDraining() {
|
|
2305
|
+
if (this.waitTimer !== null) {
|
|
2306
|
+
clearTimeout(this.waitTimer);
|
|
2307
|
+
this.waitTimer = null;
|
|
2308
|
+
}
|
|
2309
|
+
if (this.draining !== null || this.queue.length === 0) {
|
|
2310
|
+
return;
|
|
2311
|
+
}
|
|
2312
|
+
this.draining = this.drain().catch((error) => {
|
|
2313
|
+
console.error(`${this.logPrefix} BatchingErrorAdapter:`, error);
|
|
2314
|
+
}).finally(() => {
|
|
2315
|
+
this.draining = null;
|
|
2316
|
+
if (this.queue.length > 0) {
|
|
2317
|
+
this.scheduleDrain();
|
|
2318
|
+
}
|
|
2319
|
+
});
|
|
2320
|
+
}
|
|
2321
|
+
/** Delivers batches until the queue is empty, waiting out a backoff after each failure. */
|
|
2322
|
+
async drain() {
|
|
2323
|
+
let failedRounds = 0;
|
|
2324
|
+
while (this.queue.length > 0) {
|
|
2325
|
+
const batch = this.queue.slice(0, this.batchSize);
|
|
2326
|
+
const failure = await this.deliver(batch);
|
|
2327
|
+
if (failure === void 0) {
|
|
2328
|
+
failedRounds = 0;
|
|
2329
|
+
continue;
|
|
2330
|
+
}
|
|
2331
|
+
failedRounds += 1;
|
|
2332
|
+
const exhausted = this.queue.filter(
|
|
2333
|
+
(item) => item.failures > this.retries
|
|
2334
|
+
);
|
|
2335
|
+
if (exhausted.length > 0) {
|
|
2336
|
+
this.queue = this.queue.filter(
|
|
2337
|
+
(item) => item.failures <= this.retries
|
|
2338
|
+
);
|
|
2339
|
+
this.drop(
|
|
2340
|
+
exhausted,
|
|
2341
|
+
`delivery failed ${this.retries + 1} time${this.retries === 0 ? "" : "s"}; last error: ${failure}`
|
|
2342
|
+
);
|
|
2343
|
+
}
|
|
2344
|
+
if (this.queue.length > 0) {
|
|
2345
|
+
await this.backoff(failedRounds);
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2348
|
+
}
|
|
2349
|
+
/**
|
|
2350
|
+
* Tries to deliver `batch` (the front of the queue). Delivered entries leave
|
|
2351
|
+
* the queue; an entry that failed has its failure count raised and stays.
|
|
2352
|
+
* Resolves to the failure message, or undefined when everything went through.
|
|
2353
|
+
*/
|
|
2354
|
+
async deliver(batch) {
|
|
2355
|
+
const remove = (delivered) => {
|
|
2356
|
+
const gone = new Set(delivered);
|
|
2357
|
+
this.queue = this.queue.filter(
|
|
2358
|
+
(item) => !gone.has(item)
|
|
2359
|
+
);
|
|
2360
|
+
};
|
|
2361
|
+
if (this.useBatchMethod && hasLogBatch(this.inner)) {
|
|
2362
|
+
const result = await this.safely(
|
|
2363
|
+
() => this.inner.logBatch(
|
|
2364
|
+
batch.map(
|
|
2365
|
+
(item) => item.entry
|
|
2366
|
+
)
|
|
2367
|
+
)
|
|
2368
|
+
);
|
|
2369
|
+
if (result.ok) {
|
|
2370
|
+
remove(batch);
|
|
2371
|
+
return void 0;
|
|
2372
|
+
}
|
|
2373
|
+
for (const item of batch) {
|
|
2374
|
+
item.failures += 1;
|
|
2375
|
+
}
|
|
2376
|
+
return result.error ?? "the wrapped adapter reported a failure";
|
|
2377
|
+
}
|
|
2378
|
+
for (const item of batch) {
|
|
2379
|
+
const result = await this.safely(
|
|
2380
|
+
() => this.inner.log(item.entry)
|
|
2381
|
+
);
|
|
2382
|
+
if (result.ok) {
|
|
2383
|
+
remove([item]);
|
|
2384
|
+
continue;
|
|
2385
|
+
}
|
|
2386
|
+
item.failures += 1;
|
|
2387
|
+
return result.error ?? "the wrapped adapter reported a failure";
|
|
2388
|
+
}
|
|
2389
|
+
return void 0;
|
|
2390
|
+
}
|
|
2391
|
+
/** The wrapped adapter should not throw, but a bug in one must not stop the queue. */
|
|
2392
|
+
async safely(call) {
|
|
2393
|
+
try {
|
|
2394
|
+
return await call();
|
|
2395
|
+
} catch (error) {
|
|
2396
|
+
return { ok: false, error: describeError(error) };
|
|
2397
|
+
}
|
|
2398
|
+
}
|
|
2399
|
+
backoff(failedRounds) {
|
|
2400
|
+
return new Promise((resolve) => {
|
|
2401
|
+
const timer = setTimeout(
|
|
2402
|
+
() => {
|
|
2403
|
+
this.backoffTimer = null;
|
|
2404
|
+
resolve();
|
|
2405
|
+
},
|
|
2406
|
+
backoffDelayMs(failedRounds, this.retryDelayMs, this.maxRetryDelayMs)
|
|
2407
|
+
);
|
|
2408
|
+
if (this.flushers === 0) {
|
|
2409
|
+
timer.unref?.();
|
|
2410
|
+
}
|
|
2411
|
+
this.backoffTimer = timer;
|
|
2412
|
+
});
|
|
2413
|
+
}
|
|
2414
|
+
drop(items, reason) {
|
|
2415
|
+
if (items.length === 0) {
|
|
2416
|
+
return;
|
|
2417
|
+
}
|
|
2418
|
+
this.droppedSinceFlush += items.length;
|
|
2419
|
+
this.lastDropReason = reason;
|
|
2420
|
+
console.error(
|
|
2421
|
+
`${this.logPrefix} BatchingErrorAdapter: gave up on ${items.length} entr${items.length === 1 ? "y" : "ies"}: ${reason}`
|
|
2422
|
+
);
|
|
2423
|
+
try {
|
|
2424
|
+
this.onDrop?.(
|
|
2425
|
+
items.map((item) => item.entry),
|
|
2426
|
+
reason
|
|
2427
|
+
);
|
|
2428
|
+
} catch {
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2431
|
+
};
|
|
2149
2432
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2150
2433
|
0 && (module.exports = {
|
|
2434
|
+
BatchingErrorAdapter,
|
|
2151
2435
|
CloudWatchLogsErrorAdapter,
|
|
2152
2436
|
CompositeErrorAdapter,
|
|
2153
2437
|
ConsoleErrorAdapter,
|