@richardmcquiston01/error-log-adapter 0.6.0 → 0.8.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,25 +70,27 @@ 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
- | `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
+ | `CompositeErrorAdapter([...])` | Several adapters at once; succeeds only if all succeed | Anywhere | `timeoutMs` |
92
94
 
93
95
  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.
94
96
 
@@ -101,6 +103,8 @@ What changes durability or size, by adapter:
101
103
  - **Redis and Web Storage** (`maxEntries`): the oldest entries are dropped past the cap.
102
104
  - **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.
103
105
  - **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
+ - **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
+ - **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.
104
108
  - **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.
105
109
 
106
110
  ### When a sink is slow or down
@@ -134,6 +138,7 @@ Without an `id`, every call to `log()` gets a new random one, so logging twice s
134
138
  | IndexedDB, Web Storage, JSON file (`array` mode) | Replaces the earlier record |
135
139
  | S3 | Overwrites the same object only if the retry also has the same `timestamp` (the key contains both) |
136
140
  | Webhook | The `id` is added to the JSON body so the receiver can drop repeats |
141
+ | CloudWatch Logs | Cannot detect a repeat (the `id` is in the message, so Logs Insights can `dedup id`) |
137
142
  | Redis, flat file, JSON file (`ndjson` mode), console | Cannot detect a repeat |
138
143
 
139
144
  `CompositeErrorAdapter` passes the entry through unchanged, so every sink sees the same `id`.
@@ -245,6 +250,31 @@ target.addEventListener('error-log', (event) =>
245
250
  const errorLog = new EventEmitterErrorAdapter(emitter);
246
251
  ```
247
252
 
253
+ Send errors over a socket you manage, with a file as the fallback while it is down:
254
+
255
+ ```ts
256
+ const socket = new WebSocket('wss://logs.example.com/errors');
257
+ const errorLog = new CompositeErrorAdapter([
258
+ new WebSocketErrorAdapter(socket),
259
+ new FlatFileErrorAdapter('./errors.log'),
260
+ ]);
261
+ ```
262
+
263
+ Send errors to CloudWatch Logs from an app running outside AWS:
264
+
265
+ ```ts
266
+ import { CloudWatchLogs } from '@aws-sdk/client-cloudwatch-logs';
267
+ import { CloudWatchLogsErrorAdapter } from '@richardmcquiston01/error-log-adapter';
268
+
269
+ const errorLog = new CloudWatchLogsErrorAdapter(
270
+ new CloudWatchLogs({ region: 'us-east-1' }),
271
+ '/my-app/errors',
272
+ `web-${process.pid}`,
273
+ { batch: { batchSize: 100, maxWaitMs: 5000 } }
274
+ );
275
+ process.on('beforeExit', () => void errorLog.flush());
276
+ ```
277
+
248
278
  Write your own adapter by implementing the interface. Catch failures and return them rather than throwing:
249
279
 
250
280
  ```ts
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
+ CloudWatchLogsErrorAdapter: () => CloudWatchLogsErrorAdapter,
23
24
  CompositeErrorAdapter: () => CompositeErrorAdapter,
24
25
  ConsoleErrorAdapter: () => ConsoleErrorAdapter,
25
26
  EventEmitterErrorAdapter: () => EventEmitterErrorAdapter,
@@ -35,6 +36,7 @@ __export(index_exports, {
35
36
  S3ErrorAdapter: () => S3ErrorAdapter,
36
37
  SqliteErrorAdapter: () => SqliteErrorAdapter,
37
38
  SupabaseErrorAdapter: () => SupabaseErrorAdapter,
39
+ WebSocketErrorAdapter: () => WebSocketErrorAdapter,
38
40
  WebStorageErrorAdapter: () => WebStorageErrorAdapter,
39
41
  WebhookErrorAdapter: () => WebhookErrorAdapter,
40
42
  buildCreateErrorLogTableSql: () => buildCreateErrorLogTableSql,
@@ -291,10 +293,10 @@ async function hmacSha256Hex(secret, message) {
291
293
  "signingSecret needs Web Crypto (crypto.subtle), which this runtime does not provide, so the entry was not sent unsigned"
292
294
  );
293
295
  }
294
- const encoder = new TextEncoder();
296
+ const encoder2 = new TextEncoder();
295
297
  const key = await subtle.importKey(
296
298
  "raw",
297
- encoder.encode(secret),
299
+ encoder2.encode(secret),
298
300
  { name: "HMAC", hash: "SHA-256" },
299
301
  false,
300
302
  ["sign"]
@@ -302,7 +304,7 @@ async function hmacSha256Hex(secret, message) {
302
304
  const signature = await subtle.sign(
303
305
  "HMAC",
304
306
  key,
305
- encoder.encode(message)
307
+ encoder2.encode(message)
306
308
  );
307
309
  return Array.from(
308
310
  new Uint8Array(signature),
@@ -1820,8 +1822,333 @@ var EventEmitterErrorAdapter = class {
1820
1822
  }
1821
1823
  }
1822
1824
  };
1825
+
1826
+ // src/adapters/websocket-error-adapter.ts
1827
+ var OPEN = 1;
1828
+ var STATE_NAMES = {
1829
+ 0: "still connecting",
1830
+ 1: "open",
1831
+ 2: "closing",
1832
+ 3: "closed"
1833
+ };
1834
+ var WebSocketErrorAdapter = class {
1835
+ constructor(socket, options = {}) {
1836
+ this.socket = socket;
1837
+ this.prefix = options.prefix ?? DEFAULT_PREFIX;
1838
+ }
1839
+ async log(entry) {
1840
+ try {
1841
+ const state = this.socket.readyState;
1842
+ if (state !== OPEN) {
1843
+ throw new Error(
1844
+ `the socket is ${STATE_NAMES[state] ?? `in unknown state ${String(state)}`} (readyState ${String(state)}), so the entry was not sent; connect or reconnect it first`
1845
+ );
1846
+ }
1847
+ this.socket.send(
1848
+ JSON.stringify({ id: resolveEntryId(entry), ...serializeEntry(entry) })
1849
+ );
1850
+ return { ok: true };
1851
+ } catch (error) {
1852
+ return reportFailure({
1853
+ adapterName: "WebSocketErrorAdapter",
1854
+ prefix: this.prefix,
1855
+ action: "to send over the socket",
1856
+ error
1857
+ });
1858
+ }
1859
+ }
1860
+ };
1861
+
1862
+ // src/adapters/cloudwatch-logs-error-adapter.ts
1863
+ var MAX_EVENT_BYTES = 262144;
1864
+ var EVENT_OVERHEAD_BYTES = 26;
1865
+ var MAX_BATCH_BYTES = 1048576;
1866
+ var MAX_BATCH_EVENTS = 1e4;
1867
+ var MAX_BATCH_SPAN_MS = 24 * 60 * 60 * 1e3;
1868
+ var BUFFER_LIMIT_MULTIPLIER2 = 10;
1869
+ var encoder = new TextEncoder();
1870
+ function eventBytes(message) {
1871
+ return encoder.encode(message).length + EVENT_OVERHEAD_BYTES;
1872
+ }
1873
+ function errorName(error) {
1874
+ return typeof error === "object" && error !== null ? error.name : void 0;
1875
+ }
1876
+ function splitIntoRequests(events) {
1877
+ const requests = [];
1878
+ let current = [];
1879
+ let bytes = 0;
1880
+ for (const event of events) {
1881
+ const size = eventBytes(event.message);
1882
+ const first = current[0];
1883
+ if (first !== void 0 && (current.length >= MAX_BATCH_EVENTS || bytes + size > MAX_BATCH_BYTES || event.timestamp - first.timestamp > MAX_BATCH_SPAN_MS)) {
1884
+ requests.push(current);
1885
+ current = [];
1886
+ bytes = 0;
1887
+ }
1888
+ current.push(event);
1889
+ bytes += size;
1890
+ }
1891
+ if (current.length > 0) {
1892
+ requests.push(current);
1893
+ }
1894
+ return requests;
1895
+ }
1896
+ var CloudWatchLogsErrorAdapter = class {
1897
+ /** @throws {RangeError} if a name is empty, the batch or timeout options are invalid, or `createMissing` is on but the client cannot create streams. */
1898
+ constructor(client, logGroupName, logStreamName, options = {}) {
1899
+ this.client = client;
1900
+ this.logGroupName = logGroupName;
1901
+ this.logStreamName = logStreamName;
1902
+ this.buffer = [];
1903
+ this.timer = null;
1904
+ this.flushQueue = new SerialQueue();
1905
+ this.createMissing = options.createMissing ?? true;
1906
+ this.batch = options.batch;
1907
+ this.logPrefix = options.prefix ?? DEFAULT_PREFIX;
1908
+ this.timeoutMs = resolveTimeoutMs(
1909
+ options.timeoutMs,
1910
+ "CloudWatchLogsErrorAdapter"
1911
+ );
1912
+ if (logGroupName === "") {
1913
+ throw new RangeError(
1914
+ "CloudWatchLogsErrorAdapter: logGroupName must not be empty."
1915
+ );
1916
+ }
1917
+ if (logStreamName === "") {
1918
+ throw new RangeError(
1919
+ "CloudWatchLogsErrorAdapter: logStreamName must not be empty."
1920
+ );
1921
+ }
1922
+ if (this.createMissing && typeof client.createLogStream !== "function") {
1923
+ throw new RangeError(
1924
+ "CloudWatchLogsErrorAdapter: createMissing is on, but the client has no createLogStream method. Pass createMissing: false, or use an AWS SDK CloudWatchLogs client."
1925
+ );
1926
+ }
1927
+ if (this.batch !== void 0) {
1928
+ if (!Number.isInteger(this.batch.batchSize) || this.batch.batchSize < 1) {
1929
+ throw new RangeError(
1930
+ `CloudWatchLogsErrorAdapter: batch.batchSize must be a positive integer, received ${String(this.batch.batchSize)}.`
1931
+ );
1932
+ }
1933
+ if (this.batch.maxWaitMs !== void 0 && (!Number.isFinite(this.batch.maxWaitMs) || this.batch.maxWaitMs <= 0)) {
1934
+ throw new RangeError(
1935
+ `CloudWatchLogsErrorAdapter: batch.maxWaitMs must be a positive number, received ${String(this.batch.maxWaitMs)}.`
1936
+ );
1937
+ }
1938
+ }
1939
+ }
1940
+ async log(entry) {
1941
+ try {
1942
+ const id = resolveEntryId(entry);
1943
+ const record = { id, ...serializeEntry(entry) };
1944
+ const timestamp = Date.parse(record.timestamp);
1945
+ if (Number.isNaN(timestamp)) {
1946
+ throw new RangeError(
1947
+ `timestamp "${record.timestamp}" is not a valid ISO 8601 date`
1948
+ );
1949
+ }
1950
+ const message = JSON.stringify(record);
1951
+ const size = eventBytes(message);
1952
+ if (size > MAX_EVENT_BYTES) {
1953
+ throw new RangeError(
1954
+ `the event is ${size} bytes, over the ${MAX_EVENT_BYTES} byte limit CloudWatch Logs puts on one event, so it was not sent; shorten the message or cause`
1955
+ );
1956
+ }
1957
+ const event = { id, timestamp, message };
1958
+ if (this.batch === void 0) {
1959
+ const rejection = await this.send([event]);
1960
+ if (rejection !== void 0) {
1961
+ throw new Error(rejection);
1962
+ }
1963
+ return { ok: true };
1964
+ }
1965
+ return await this.addToBatch(event, this.batch);
1966
+ } catch (error) {
1967
+ return this.fail(error);
1968
+ }
1969
+ }
1970
+ /**
1971
+ * Sends any buffered events now. Resolves ok when nothing is buffered. Events
1972
+ * from a failed send stay buffered and are retried by the next flush.
1973
+ */
1974
+ flush() {
1975
+ return this.flushQueue.run(
1976
+ () => this.sendBuffered()
1977
+ );
1978
+ }
1979
+ async addToBatch(event, batch) {
1980
+ this.buffer.push(event);
1981
+ const limit = batch.batchSize * BUFFER_LIMIT_MULTIPLIER2;
1982
+ if (this.buffer.length > limit) {
1983
+ const dropped = this.buffer.length - limit;
1984
+ this.buffer = this.buffer.slice(dropped);
1985
+ console.error(
1986
+ `${this.logPrefix} CloudWatchLogsErrorAdapter: dropped the ${dropped} oldest buffered events because sends to log stream "${this.logStreamName}" keep failing and the buffer is full (${limit} events).`
1987
+ );
1988
+ }
1989
+ if (this.buffer.length >= batch.batchSize) {
1990
+ return this.flush();
1991
+ }
1992
+ this.scheduleFlush(batch);
1993
+ return { ok: true };
1994
+ }
1995
+ scheduleFlush(batch) {
1996
+ if (batch.maxWaitMs === void 0 || this.timer !== null) {
1997
+ return;
1998
+ }
1999
+ const timer = setTimeout(() => {
2000
+ this.timer = null;
2001
+ void this.flush();
2002
+ }, batch.maxWaitMs);
2003
+ timer.unref?.();
2004
+ this.timer = timer;
2005
+ }
2006
+ async sendBuffered() {
2007
+ if (this.timer !== null) {
2008
+ clearTimeout(this.timer);
2009
+ this.timer = null;
2010
+ }
2011
+ const pending = this.buffer.slice();
2012
+ if (pending.length === 0) {
2013
+ return { ok: true };
2014
+ }
2015
+ pending.sort(
2016
+ (a, b) => a.timestamp - b.timestamp
2017
+ );
2018
+ let rejectedTotal = 0;
2019
+ let lastRejection = "";
2020
+ try {
2021
+ for (const request of splitIntoRequests(pending)) {
2022
+ const rejection = await this.send(request);
2023
+ const sent = new Set(request);
2024
+ this.buffer = this.buffer.filter(
2025
+ (event) => !sent.has(event)
2026
+ );
2027
+ if (rejection !== void 0) {
2028
+ rejectedTotal += request.length;
2029
+ lastRejection = rejection;
2030
+ }
2031
+ }
2032
+ } catch (error) {
2033
+ if (this.batch !== void 0) {
2034
+ this.scheduleFlush(this.batch);
2035
+ }
2036
+ return this.fail(error, this.buffer.length);
2037
+ }
2038
+ if (rejectedTotal > 0) {
2039
+ return this.fail(new Error(lastRejection), rejectedTotal);
2040
+ }
2041
+ return { ok: true };
2042
+ }
2043
+ /**
2044
+ * Sends one request, creating the stream first if CloudWatch says it is
2045
+ * missing. Resolves to a description of any events CloudWatch refused, or
2046
+ * undefined when it took them all.
2047
+ */
2048
+ async send(events) {
2049
+ let output;
2050
+ try {
2051
+ output = await this.putLogEvents(events);
2052
+ } catch (error) {
2053
+ if (!this.createMissing || errorName(error) !== "ResourceNotFoundException") {
2054
+ throw error;
2055
+ }
2056
+ await this.createStream();
2057
+ output = await this.putLogEvents(events);
2058
+ }
2059
+ return describeRejection(output, events.length);
2060
+ }
2061
+ putLogEvents(events) {
2062
+ return withTimeout(
2063
+ () => this.client.putLogEvents({
2064
+ logGroupName: this.logGroupName,
2065
+ logStreamName: this.logStreamName,
2066
+ logEvents: events.map(
2067
+ ({ timestamp, message }) => ({
2068
+ timestamp,
2069
+ message
2070
+ })
2071
+ )
2072
+ }),
2073
+ this.timeoutMs,
2074
+ timeoutMessage("CloudWatch Logs", this.timeoutMs)
2075
+ );
2076
+ }
2077
+ async createStream() {
2078
+ const createStream = () => withTimeout(
2079
+ () => this.client.createLogStream({
2080
+ logGroupName: this.logGroupName,
2081
+ logStreamName: this.logStreamName
2082
+ }),
2083
+ this.timeoutMs,
2084
+ timeoutMessage("CloudWatch Logs", this.timeoutMs, "create the stream")
2085
+ );
2086
+ try {
2087
+ await createStream();
2088
+ } catch (error) {
2089
+ const name = errorName(error);
2090
+ if (name === "ResourceAlreadyExistsException") {
2091
+ return;
2092
+ }
2093
+ if (name !== "ResourceNotFoundException" || typeof this.client.createLogGroup !== "function") {
2094
+ throw error;
2095
+ }
2096
+ const createGroup = this.client.createLogGroup.bind(this.client);
2097
+ try {
2098
+ await withTimeout(
2099
+ () => createGroup({ logGroupName: this.logGroupName }),
2100
+ this.timeoutMs,
2101
+ timeoutMessage("CloudWatch Logs", this.timeoutMs, "create the group")
2102
+ );
2103
+ } catch (groupError) {
2104
+ if (errorName(groupError) !== "ResourceAlreadyExistsException") {
2105
+ throw groupError;
2106
+ }
2107
+ }
2108
+ try {
2109
+ await createStream();
2110
+ } catch (retryError) {
2111
+ if (errorName(retryError) !== "ResourceAlreadyExistsException") {
2112
+ throw retryError;
2113
+ }
2114
+ }
2115
+ }
2116
+ }
2117
+ fail(error, count) {
2118
+ return reportFailure({
2119
+ adapterName: "CloudWatchLogsErrorAdapter",
2120
+ prefix: this.logPrefix,
2121
+ action: `to send to log stream "${this.logStreamName}" in log group "${this.logGroupName}"${count === void 0 ? "" : ` (${count} event${count === 1 ? "" : "s"} affected)`}`,
2122
+ error
2123
+ });
2124
+ }
2125
+ };
2126
+ function describeRejection(output, eventCount) {
2127
+ const info = typeof output === "object" && output !== null ? output.rejectedLogEventsInfo : void 0;
2128
+ if (info === void 0 || info === null) {
2129
+ return void 0;
2130
+ }
2131
+ const reasons = [];
2132
+ if (info.tooOldLogEventEndIndex !== void 0) {
2133
+ reasons.push(
2134
+ `${info.tooOldLogEventEndIndex} event(s) were older than the 14 days CloudWatch accepts`
2135
+ );
2136
+ }
2137
+ if (info.expiredLogEventEndIndex !== void 0) {
2138
+ reasons.push(
2139
+ `${info.expiredLogEventEndIndex} event(s) were older than the log group's retention period`
2140
+ );
2141
+ }
2142
+ if (info.tooNewLogEventStartIndex !== void 0) {
2143
+ reasons.push(
2144
+ `${Math.max(0, eventCount - info.tooNewLogEventStartIndex)} event(s) were more than 2 hours ahead of the current time`
2145
+ );
2146
+ }
2147
+ return reasons.length === 0 ? void 0 : `CloudWatch Logs accepted the request but rejected events: ${reasons.join("; ")}`;
2148
+ }
1823
2149
  // Annotate the CommonJS export names for ESM import in node:
1824
2150
  0 && (module.exports = {
2151
+ CloudWatchLogsErrorAdapter,
1825
2152
  CompositeErrorAdapter,
1826
2153
  ConsoleErrorAdapter,
1827
2154
  EventEmitterErrorAdapter,
@@ -1837,6 +2164,7 @@ var EventEmitterErrorAdapter = class {
1837
2164
  S3ErrorAdapter,
1838
2165
  SqliteErrorAdapter,
1839
2166
  SupabaseErrorAdapter,
2167
+ WebSocketErrorAdapter,
1840
2168
  WebStorageErrorAdapter,
1841
2169
  WebhookErrorAdapter,
1842
2170
  buildCreateErrorLogTableSql,