@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 +59 -20
- package/dist/index.cjs +582 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +245 -1
- package/dist/index.d.ts +245 -1
- package/dist/index.mjs +580 -8
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -20,6 +20,8 @@ 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,
|
|
24
|
+
CloudWatchLogsErrorAdapter: () => CloudWatchLogsErrorAdapter,
|
|
23
25
|
CompositeErrorAdapter: () => CompositeErrorAdapter,
|
|
24
26
|
ConsoleErrorAdapter: () => ConsoleErrorAdapter,
|
|
25
27
|
EventEmitterErrorAdapter: () => EventEmitterErrorAdapter,
|
|
@@ -191,6 +193,12 @@ var ConsoleErrorAdapter = class {
|
|
|
191
193
|
}
|
|
192
194
|
};
|
|
193
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
|
+
|
|
194
202
|
// src/uuid.ts
|
|
195
203
|
function generateId() {
|
|
196
204
|
const cryptoApi = globalThis.crypto;
|
|
@@ -292,10 +300,10 @@ async function hmacSha256Hex(secret, message) {
|
|
|
292
300
|
"signingSecret needs Web Crypto (crypto.subtle), which this runtime does not provide, so the entry was not sent unsigned"
|
|
293
301
|
);
|
|
294
302
|
}
|
|
295
|
-
const
|
|
303
|
+
const encoder2 = new TextEncoder();
|
|
296
304
|
const key = await subtle.importKey(
|
|
297
305
|
"raw",
|
|
298
|
-
|
|
306
|
+
encoder2.encode(secret),
|
|
299
307
|
{ name: "HMAC", hash: "SHA-256" },
|
|
300
308
|
false,
|
|
301
309
|
["sign"]
|
|
@@ -303,7 +311,7 @@ async function hmacSha256Hex(secret, message) {
|
|
|
303
311
|
const signature = await subtle.sign(
|
|
304
312
|
"HMAC",
|
|
305
313
|
key,
|
|
306
|
-
|
|
314
|
+
encoder2.encode(message)
|
|
307
315
|
);
|
|
308
316
|
return Array.from(
|
|
309
317
|
new Uint8Array(signature),
|
|
@@ -402,6 +410,36 @@ var WebhookErrorAdapter = class {
|
|
|
402
410
|
`could not prepare the entry: ${this.redact(describeError(error))}`
|
|
403
411
|
);
|
|
404
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) {
|
|
405
443
|
let attempt = await this.send(body);
|
|
406
444
|
let attempts = 1;
|
|
407
445
|
while (!attempt.ok && attempt.retryable && attempts <= this.retries) {
|
|
@@ -421,11 +459,7 @@ var WebhookErrorAdapter = class {
|
|
|
421
459
|
if (retryAfterMs !== void 0) {
|
|
422
460
|
return Math.min(retryAfterMs, this.maxRetryDelayMs);
|
|
423
461
|
}
|
|
424
|
-
|
|
425
|
-
this.maxRetryDelayMs,
|
|
426
|
-
this.retryDelayMs * 2 ** (retryNumber - 1)
|
|
427
|
-
);
|
|
428
|
-
return ceiling / 2 + Math.random() * ceiling / 2;
|
|
462
|
+
return backoffDelayMs(retryNumber, this.retryDelayMs, this.maxRetryDelayMs);
|
|
429
463
|
}
|
|
430
464
|
async send(body) {
|
|
431
465
|
const controller = new AbortController();
|
|
@@ -1857,8 +1891,548 @@ var WebSocketErrorAdapter = class {
|
|
|
1857
1891
|
}
|
|
1858
1892
|
}
|
|
1859
1893
|
};
|
|
1894
|
+
|
|
1895
|
+
// src/adapters/cloudwatch-logs-error-adapter.ts
|
|
1896
|
+
var MAX_EVENT_BYTES = 262144;
|
|
1897
|
+
var EVENT_OVERHEAD_BYTES = 26;
|
|
1898
|
+
var MAX_BATCH_BYTES = 1048576;
|
|
1899
|
+
var MAX_BATCH_EVENTS = 1e4;
|
|
1900
|
+
var MAX_BATCH_SPAN_MS = 24 * 60 * 60 * 1e3;
|
|
1901
|
+
var BUFFER_LIMIT_MULTIPLIER2 = 10;
|
|
1902
|
+
var encoder = new TextEncoder();
|
|
1903
|
+
function eventBytes(message) {
|
|
1904
|
+
return encoder.encode(message).length + EVENT_OVERHEAD_BYTES;
|
|
1905
|
+
}
|
|
1906
|
+
function errorName(error) {
|
|
1907
|
+
return typeof error === "object" && error !== null ? error.name : void 0;
|
|
1908
|
+
}
|
|
1909
|
+
function splitIntoRequests(events) {
|
|
1910
|
+
const requests = [];
|
|
1911
|
+
let current = [];
|
|
1912
|
+
let bytes = 0;
|
|
1913
|
+
for (const event of events) {
|
|
1914
|
+
const size = eventBytes(event.message);
|
|
1915
|
+
const first = current[0];
|
|
1916
|
+
if (first !== void 0 && (current.length >= MAX_BATCH_EVENTS || bytes + size > MAX_BATCH_BYTES || event.timestamp - first.timestamp > MAX_BATCH_SPAN_MS)) {
|
|
1917
|
+
requests.push(current);
|
|
1918
|
+
current = [];
|
|
1919
|
+
bytes = 0;
|
|
1920
|
+
}
|
|
1921
|
+
current.push(event);
|
|
1922
|
+
bytes += size;
|
|
1923
|
+
}
|
|
1924
|
+
if (current.length > 0) {
|
|
1925
|
+
requests.push(current);
|
|
1926
|
+
}
|
|
1927
|
+
return requests;
|
|
1928
|
+
}
|
|
1929
|
+
var CloudWatchLogsErrorAdapter = class {
|
|
1930
|
+
/** @throws {RangeError} if a name is empty, the batch or timeout options are invalid, or `createMissing` is on but the client cannot create streams. */
|
|
1931
|
+
constructor(client, logGroupName, logStreamName, options = {}) {
|
|
1932
|
+
this.client = client;
|
|
1933
|
+
this.logGroupName = logGroupName;
|
|
1934
|
+
this.logStreamName = logStreamName;
|
|
1935
|
+
this.buffer = [];
|
|
1936
|
+
this.timer = null;
|
|
1937
|
+
this.flushQueue = new SerialQueue();
|
|
1938
|
+
this.createMissing = options.createMissing ?? true;
|
|
1939
|
+
this.batch = options.batch;
|
|
1940
|
+
this.logPrefix = options.prefix ?? DEFAULT_PREFIX;
|
|
1941
|
+
this.timeoutMs = resolveTimeoutMs(
|
|
1942
|
+
options.timeoutMs,
|
|
1943
|
+
"CloudWatchLogsErrorAdapter"
|
|
1944
|
+
);
|
|
1945
|
+
if (logGroupName === "") {
|
|
1946
|
+
throw new RangeError(
|
|
1947
|
+
"CloudWatchLogsErrorAdapter: logGroupName must not be empty."
|
|
1948
|
+
);
|
|
1949
|
+
}
|
|
1950
|
+
if (logStreamName === "") {
|
|
1951
|
+
throw new RangeError(
|
|
1952
|
+
"CloudWatchLogsErrorAdapter: logStreamName must not be empty."
|
|
1953
|
+
);
|
|
1954
|
+
}
|
|
1955
|
+
if (this.createMissing && typeof client.createLogStream !== "function") {
|
|
1956
|
+
throw new RangeError(
|
|
1957
|
+
"CloudWatchLogsErrorAdapter: createMissing is on, but the client has no createLogStream method. Pass createMissing: false, or use an AWS SDK CloudWatchLogs client."
|
|
1958
|
+
);
|
|
1959
|
+
}
|
|
1960
|
+
if (this.batch !== void 0) {
|
|
1961
|
+
if (!Number.isInteger(this.batch.batchSize) || this.batch.batchSize < 1) {
|
|
1962
|
+
throw new RangeError(
|
|
1963
|
+
`CloudWatchLogsErrorAdapter: batch.batchSize must be a positive integer, received ${String(this.batch.batchSize)}.`
|
|
1964
|
+
);
|
|
1965
|
+
}
|
|
1966
|
+
if (this.batch.maxWaitMs !== void 0 && (!Number.isFinite(this.batch.maxWaitMs) || this.batch.maxWaitMs <= 0)) {
|
|
1967
|
+
throw new RangeError(
|
|
1968
|
+
`CloudWatchLogsErrorAdapter: batch.maxWaitMs must be a positive number, received ${String(this.batch.maxWaitMs)}.`
|
|
1969
|
+
);
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
async log(entry) {
|
|
1974
|
+
try {
|
|
1975
|
+
const id = resolveEntryId(entry);
|
|
1976
|
+
const record = { id, ...serializeEntry(entry) };
|
|
1977
|
+
const timestamp = Date.parse(record.timestamp);
|
|
1978
|
+
if (Number.isNaN(timestamp)) {
|
|
1979
|
+
throw new RangeError(
|
|
1980
|
+
`timestamp "${record.timestamp}" is not a valid ISO 8601 date`
|
|
1981
|
+
);
|
|
1982
|
+
}
|
|
1983
|
+
const message = JSON.stringify(record);
|
|
1984
|
+
const size = eventBytes(message);
|
|
1985
|
+
if (size > MAX_EVENT_BYTES) {
|
|
1986
|
+
throw new RangeError(
|
|
1987
|
+
`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`
|
|
1988
|
+
);
|
|
1989
|
+
}
|
|
1990
|
+
const event = { id, timestamp, message };
|
|
1991
|
+
if (this.batch === void 0) {
|
|
1992
|
+
const rejection = await this.send([event]);
|
|
1993
|
+
if (rejection !== void 0) {
|
|
1994
|
+
throw new Error(rejection);
|
|
1995
|
+
}
|
|
1996
|
+
return { ok: true };
|
|
1997
|
+
}
|
|
1998
|
+
return await this.addToBatch(event, this.batch);
|
|
1999
|
+
} catch (error) {
|
|
2000
|
+
return this.fail(error);
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
/**
|
|
2004
|
+
* Sends any buffered events now. Resolves ok when nothing is buffered. Events
|
|
2005
|
+
* from a failed send stay buffered and are retried by the next flush.
|
|
2006
|
+
*/
|
|
2007
|
+
flush() {
|
|
2008
|
+
return this.flushQueue.run(
|
|
2009
|
+
() => this.sendBuffered()
|
|
2010
|
+
);
|
|
2011
|
+
}
|
|
2012
|
+
async addToBatch(event, batch) {
|
|
2013
|
+
this.buffer.push(event);
|
|
2014
|
+
const limit = batch.batchSize * BUFFER_LIMIT_MULTIPLIER2;
|
|
2015
|
+
if (this.buffer.length > limit) {
|
|
2016
|
+
const dropped = this.buffer.length - limit;
|
|
2017
|
+
this.buffer = this.buffer.slice(dropped);
|
|
2018
|
+
console.error(
|
|
2019
|
+
`${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).`
|
|
2020
|
+
);
|
|
2021
|
+
}
|
|
2022
|
+
if (this.buffer.length >= batch.batchSize) {
|
|
2023
|
+
return this.flush();
|
|
2024
|
+
}
|
|
2025
|
+
this.scheduleFlush(batch);
|
|
2026
|
+
return { ok: true };
|
|
2027
|
+
}
|
|
2028
|
+
scheduleFlush(batch) {
|
|
2029
|
+
if (batch.maxWaitMs === void 0 || this.timer !== null) {
|
|
2030
|
+
return;
|
|
2031
|
+
}
|
|
2032
|
+
const timer = setTimeout(() => {
|
|
2033
|
+
this.timer = null;
|
|
2034
|
+
void this.flush();
|
|
2035
|
+
}, batch.maxWaitMs);
|
|
2036
|
+
timer.unref?.();
|
|
2037
|
+
this.timer = timer;
|
|
2038
|
+
}
|
|
2039
|
+
async sendBuffered() {
|
|
2040
|
+
if (this.timer !== null) {
|
|
2041
|
+
clearTimeout(this.timer);
|
|
2042
|
+
this.timer = null;
|
|
2043
|
+
}
|
|
2044
|
+
const pending = this.buffer.slice();
|
|
2045
|
+
if (pending.length === 0) {
|
|
2046
|
+
return { ok: true };
|
|
2047
|
+
}
|
|
2048
|
+
pending.sort(
|
|
2049
|
+
(a, b) => a.timestamp - b.timestamp
|
|
2050
|
+
);
|
|
2051
|
+
let rejectedTotal = 0;
|
|
2052
|
+
let lastRejection = "";
|
|
2053
|
+
try {
|
|
2054
|
+
for (const request of splitIntoRequests(pending)) {
|
|
2055
|
+
const rejection = await this.send(request);
|
|
2056
|
+
const sent = new Set(request);
|
|
2057
|
+
this.buffer = this.buffer.filter(
|
|
2058
|
+
(event) => !sent.has(event)
|
|
2059
|
+
);
|
|
2060
|
+
if (rejection !== void 0) {
|
|
2061
|
+
rejectedTotal += request.length;
|
|
2062
|
+
lastRejection = rejection;
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2065
|
+
} catch (error) {
|
|
2066
|
+
if (this.batch !== void 0) {
|
|
2067
|
+
this.scheduleFlush(this.batch);
|
|
2068
|
+
}
|
|
2069
|
+
return this.fail(error, this.buffer.length);
|
|
2070
|
+
}
|
|
2071
|
+
if (rejectedTotal > 0) {
|
|
2072
|
+
return this.fail(new Error(lastRejection), rejectedTotal);
|
|
2073
|
+
}
|
|
2074
|
+
return { ok: true };
|
|
2075
|
+
}
|
|
2076
|
+
/**
|
|
2077
|
+
* Sends one request, creating the stream first if CloudWatch says it is
|
|
2078
|
+
* missing. Resolves to a description of any events CloudWatch refused, or
|
|
2079
|
+
* undefined when it took them all.
|
|
2080
|
+
*/
|
|
2081
|
+
async send(events) {
|
|
2082
|
+
let output;
|
|
2083
|
+
try {
|
|
2084
|
+
output = await this.putLogEvents(events);
|
|
2085
|
+
} catch (error) {
|
|
2086
|
+
if (!this.createMissing || errorName(error) !== "ResourceNotFoundException") {
|
|
2087
|
+
throw error;
|
|
2088
|
+
}
|
|
2089
|
+
await this.createStream();
|
|
2090
|
+
output = await this.putLogEvents(events);
|
|
2091
|
+
}
|
|
2092
|
+
return describeRejection(output, events.length);
|
|
2093
|
+
}
|
|
2094
|
+
putLogEvents(events) {
|
|
2095
|
+
return withTimeout(
|
|
2096
|
+
() => this.client.putLogEvents({
|
|
2097
|
+
logGroupName: this.logGroupName,
|
|
2098
|
+
logStreamName: this.logStreamName,
|
|
2099
|
+
logEvents: events.map(
|
|
2100
|
+
({ timestamp, message }) => ({
|
|
2101
|
+
timestamp,
|
|
2102
|
+
message
|
|
2103
|
+
})
|
|
2104
|
+
)
|
|
2105
|
+
}),
|
|
2106
|
+
this.timeoutMs,
|
|
2107
|
+
timeoutMessage("CloudWatch Logs", this.timeoutMs)
|
|
2108
|
+
);
|
|
2109
|
+
}
|
|
2110
|
+
async createStream() {
|
|
2111
|
+
const createStream = () => withTimeout(
|
|
2112
|
+
() => this.client.createLogStream({
|
|
2113
|
+
logGroupName: this.logGroupName,
|
|
2114
|
+
logStreamName: this.logStreamName
|
|
2115
|
+
}),
|
|
2116
|
+
this.timeoutMs,
|
|
2117
|
+
timeoutMessage("CloudWatch Logs", this.timeoutMs, "create the stream")
|
|
2118
|
+
);
|
|
2119
|
+
try {
|
|
2120
|
+
await createStream();
|
|
2121
|
+
} catch (error) {
|
|
2122
|
+
const name = errorName(error);
|
|
2123
|
+
if (name === "ResourceAlreadyExistsException") {
|
|
2124
|
+
return;
|
|
2125
|
+
}
|
|
2126
|
+
if (name !== "ResourceNotFoundException" || typeof this.client.createLogGroup !== "function") {
|
|
2127
|
+
throw error;
|
|
2128
|
+
}
|
|
2129
|
+
const createGroup = this.client.createLogGroup.bind(this.client);
|
|
2130
|
+
try {
|
|
2131
|
+
await withTimeout(
|
|
2132
|
+
() => createGroup({ logGroupName: this.logGroupName }),
|
|
2133
|
+
this.timeoutMs,
|
|
2134
|
+
timeoutMessage("CloudWatch Logs", this.timeoutMs, "create the group")
|
|
2135
|
+
);
|
|
2136
|
+
} catch (groupError) {
|
|
2137
|
+
if (errorName(groupError) !== "ResourceAlreadyExistsException") {
|
|
2138
|
+
throw groupError;
|
|
2139
|
+
}
|
|
2140
|
+
}
|
|
2141
|
+
try {
|
|
2142
|
+
await createStream();
|
|
2143
|
+
} catch (retryError) {
|
|
2144
|
+
if (errorName(retryError) !== "ResourceAlreadyExistsException") {
|
|
2145
|
+
throw retryError;
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
}
|
|
2150
|
+
fail(error, count) {
|
|
2151
|
+
return reportFailure({
|
|
2152
|
+
adapterName: "CloudWatchLogsErrorAdapter",
|
|
2153
|
+
prefix: this.logPrefix,
|
|
2154
|
+
action: `to send to log stream "${this.logStreamName}" in log group "${this.logGroupName}"${count === void 0 ? "" : ` (${count} event${count === 1 ? "" : "s"} affected)`}`,
|
|
2155
|
+
error
|
|
2156
|
+
});
|
|
2157
|
+
}
|
|
2158
|
+
};
|
|
2159
|
+
function describeRejection(output, eventCount) {
|
|
2160
|
+
const info = typeof output === "object" && output !== null ? output.rejectedLogEventsInfo : void 0;
|
|
2161
|
+
if (info === void 0 || info === null) {
|
|
2162
|
+
return void 0;
|
|
2163
|
+
}
|
|
2164
|
+
const reasons = [];
|
|
2165
|
+
if (info.tooOldLogEventEndIndex !== void 0) {
|
|
2166
|
+
reasons.push(
|
|
2167
|
+
`${info.tooOldLogEventEndIndex} event(s) were older than the 14 days CloudWatch accepts`
|
|
2168
|
+
);
|
|
2169
|
+
}
|
|
2170
|
+
if (info.expiredLogEventEndIndex !== void 0) {
|
|
2171
|
+
reasons.push(
|
|
2172
|
+
`${info.expiredLogEventEndIndex} event(s) were older than the log group's retention period`
|
|
2173
|
+
);
|
|
2174
|
+
}
|
|
2175
|
+
if (info.tooNewLogEventStartIndex !== void 0) {
|
|
2176
|
+
reasons.push(
|
|
2177
|
+
`${Math.max(0, eventCount - info.tooNewLogEventStartIndex)} event(s) were more than 2 hours ahead of the current time`
|
|
2178
|
+
);
|
|
2179
|
+
}
|
|
2180
|
+
return reasons.length === 0 ? void 0 : `CloudWatch Logs accepted the request but rejected events: ${reasons.join("; ")}`;
|
|
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
|
+
};
|
|
1860
2432
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1861
2433
|
0 && (module.exports = {
|
|
2434
|
+
BatchingErrorAdapter,
|
|
2435
|
+
CloudWatchLogsErrorAdapter,
|
|
1862
2436
|
CompositeErrorAdapter,
|
|
1863
2437
|
ConsoleErrorAdapter,
|
|
1864
2438
|
EventEmitterErrorAdapter,
|