@richardmcquiston01/error-log-adapter 0.7.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 CHANGED
@@ -70,26 +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 | 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
- | `CompositeErrorAdapter([...])` | Several adapters at once; succeeds only if all succeed | Anywhere | `timeoutMs` |
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` |
93
95
 
94
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.
95
97
 
@@ -103,6 +105,8 @@ What changes durability or size, by adapter:
103
105
  - **Browser storage**: `sessionStorage` belongs to one tab. `localStorage` is shared by every tab and has no atomic update, so two tabs logging at the same instant can overwrite each other's entry. `WebStorageErrorAdapter` takes a `navigator.locks` lock around each write, which reduces the loss but cannot remove it, since tabs in separate processes can still read a stale copy. Use `IndexedDbErrorAdapter` when every entry matters. Create its object store from your `onupgradeneeded` handler with `createErrorLogStore`. A stored value the adapter cannot read is copied to `<key>.corrupt` before it is replaced.
104
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.
105
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.
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.
106
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.
107
111
 
108
112
  ### When a sink is slow or down
@@ -136,6 +140,7 @@ Without an `id`, every call to `log()` gets a new random one, so logging twice s
136
140
  | IndexedDB, Web Storage, JSON file (`array` mode) | Replaces the earlier record |
137
141
  | S3 | Overwrites the same object only if the retry also has the same `timestamp` (the key contains both) |
138
142
  | Webhook | The `id` is added to the JSON body so the receiver can drop repeats |
143
+ | CloudWatch Logs | Cannot detect a repeat (the `id` is in the message, so Logs Insights can `dedup id`) |
139
144
  | Redis, flat file, JSON file (`ndjson` mode), console | Cannot detect a repeat |
140
145
 
141
146
  `CompositeErrorAdapter` passes the entry through unchanged, so every sink sees the same `id`.
@@ -257,6 +262,38 @@ const errorLog = new CompositeErrorAdapter([
257
262
  ]);
258
263
  ```
259
264
 
265
+ Send errors to CloudWatch Logs from an app running outside AWS:
266
+
267
+ ```ts
268
+ import { CloudWatchLogs } from '@aws-sdk/client-cloudwatch-logs';
269
+ import { CloudWatchLogsErrorAdapter } from '@richardmcquiston01/error-log-adapter';
270
+
271
+ const errorLog = new CloudWatchLogsErrorAdapter(
272
+ new CloudWatchLogs({ region: 'us-east-1' }),
273
+ '/my-app/errors',
274
+ `web-${process.pid}`,
275
+ { batch: { batchSize: 100, maxWaitMs: 5000 } }
276
+ );
277
+ process.on('beforeExit', () => void errorLog.flush());
278
+ ```
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
+
260
297
  Write your own adapter by implementing the interface. Catch failures and return them rather than throwing:
261
298
 
262
299
  ```ts
@@ -285,6 +322,8 @@ class SlackErrorAdapter implements ErrorLogAdapter {
285
322
  }
286
323
  ```
287
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
+
288
327
  See [ROADMAP.md](./ROADMAP.md) for planned adapters and [CHANGELOG.md](./CHANGELOG.md) for what changed in each release.
289
328
 
290
329
  ### Development