logquill 0.1.2 → 0.3.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 +358 -8
- package/dist/index.cjs +1497 -15
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1078 -13
- package/dist/index.d.ts +1078 -13
- package/dist/index.mjs +1470 -16
- package/dist/index.mjs.map +1 -1
- package/package.json +57 -1
package/dist/index.cjs
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
var crypto = require('crypto');
|
|
4
|
+
var zlib = require('zlib');
|
|
3
5
|
var fs = require('fs');
|
|
4
6
|
var path = require('path');
|
|
5
7
|
|
|
6
|
-
// src/levels.ts
|
|
8
|
+
// src/core/levels.ts
|
|
7
9
|
var Level = /* @__PURE__ */ ((Level2) => {
|
|
8
10
|
Level2[Level2["TRACE"] = 5] = "TRACE";
|
|
9
11
|
Level2[Level2["DEBUG"] = 10] = "DEBUG";
|
|
@@ -38,7 +40,7 @@ function parseLevel(level) {
|
|
|
38
40
|
return level;
|
|
39
41
|
}
|
|
40
42
|
|
|
41
|
-
// src/records.ts
|
|
43
|
+
// src/core/records.ts
|
|
42
44
|
function utcTimestamp() {
|
|
43
45
|
return (/* @__PURE__ */ new Date()).toISOString();
|
|
44
46
|
}
|
|
@@ -52,14 +54,25 @@ function createRecord(params) {
|
|
|
52
54
|
};
|
|
53
55
|
}
|
|
54
56
|
|
|
55
|
-
// src/formatter.ts
|
|
57
|
+
// src/core/formatter.ts
|
|
56
58
|
var JSONFormatter = class {
|
|
57
59
|
format(record) {
|
|
58
60
|
return JSON.stringify(record);
|
|
59
61
|
}
|
|
60
62
|
};
|
|
61
63
|
|
|
62
|
-
// src/
|
|
64
|
+
// src/core/plugin.ts
|
|
65
|
+
var FunctionPlugin = class {
|
|
66
|
+
func;
|
|
67
|
+
constructor(func) {
|
|
68
|
+
this.func = func;
|
|
69
|
+
}
|
|
70
|
+
beforeLog(record) {
|
|
71
|
+
return this.func(record);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// src/plugins/context-plugin.ts
|
|
63
76
|
var ContextPlugin = class {
|
|
64
77
|
context;
|
|
65
78
|
constructor(context) {
|
|
@@ -70,7 +83,7 @@ var ContextPlugin = class {
|
|
|
70
83
|
}
|
|
71
84
|
};
|
|
72
85
|
|
|
73
|
-
// src/redact-plugin.ts
|
|
86
|
+
// src/plugins/redact-plugin.ts
|
|
74
87
|
var DEFAULT_REDACTED_KEYS = ["password", "token", "secret", "api_key", "authorization"];
|
|
75
88
|
var RedactPlugin = class {
|
|
76
89
|
keys;
|
|
@@ -88,23 +101,413 @@ var RedactPlugin = class {
|
|
|
88
101
|
}
|
|
89
102
|
};
|
|
90
103
|
|
|
91
|
-
// src/
|
|
104
|
+
// src/plugins/pii-redact-plugin.ts
|
|
105
|
+
var DEFAULT_PII_PATTERNS = {
|
|
106
|
+
email: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g,
|
|
107
|
+
ssn: /\b\d{3}-\d{2}-\d{4}\b/g,
|
|
108
|
+
creditCard: /\b(?:\d[ -]?){13,16}\b/g,
|
|
109
|
+
phone: /\b(?:\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b/g
|
|
110
|
+
};
|
|
111
|
+
var MAX_DEPTH = 50;
|
|
112
|
+
var PIIRedactPlugin = class {
|
|
113
|
+
patterns;
|
|
114
|
+
replacement;
|
|
115
|
+
constructor(options = {}) {
|
|
116
|
+
this.patterns = options.patterns ?? DEFAULT_PII_PATTERNS;
|
|
117
|
+
this.replacement = options.replacement ?? "***";
|
|
118
|
+
}
|
|
119
|
+
beforeLog(record) {
|
|
120
|
+
return { ...record, meta: this.redactValue(record.meta, /* @__PURE__ */ new Set(), 0) };
|
|
121
|
+
}
|
|
122
|
+
redactValue(value, seen, depth) {
|
|
123
|
+
if (depth > MAX_DEPTH) {
|
|
124
|
+
return value;
|
|
125
|
+
}
|
|
126
|
+
if (typeof value === "string") {
|
|
127
|
+
return this.redactText(value);
|
|
128
|
+
}
|
|
129
|
+
if (Array.isArray(value)) {
|
|
130
|
+
if (seen.has(value)) {
|
|
131
|
+
return value;
|
|
132
|
+
}
|
|
133
|
+
const nextSeen = new Set(seen).add(value);
|
|
134
|
+
return value.map((entry) => this.redactValue(entry, nextSeen, depth + 1));
|
|
135
|
+
}
|
|
136
|
+
if (value !== null && typeof value === "object") {
|
|
137
|
+
if (seen.has(value)) {
|
|
138
|
+
return value;
|
|
139
|
+
}
|
|
140
|
+
const nextSeen = new Set(seen).add(value);
|
|
141
|
+
const result = {};
|
|
142
|
+
for (const [key, entryValue] of Object.entries(value)) {
|
|
143
|
+
result[key] = this.redactValue(entryValue, nextSeen, depth + 1);
|
|
144
|
+
}
|
|
145
|
+
return result;
|
|
146
|
+
}
|
|
147
|
+
return value;
|
|
148
|
+
}
|
|
149
|
+
redactText(text) {
|
|
150
|
+
let redacted = text;
|
|
151
|
+
for (const pattern of Object.values(this.patterns)) {
|
|
152
|
+
const global = pattern.global ? pattern : new RegExp(pattern.source, `${pattern.flags}g`);
|
|
153
|
+
redacted = redacted.replace(global, this.replacement);
|
|
154
|
+
}
|
|
155
|
+
return redacted;
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
// src/plugins/sampling-plugin.ts
|
|
92
160
|
var SamplingPlugin = class {
|
|
93
161
|
rate;
|
|
162
|
+
traceKey;
|
|
163
|
+
elevateAt;
|
|
164
|
+
transports;
|
|
165
|
+
maxBufferedRecords;
|
|
166
|
+
maxTraces;
|
|
94
167
|
rng;
|
|
168
|
+
buffer = /* @__PURE__ */ new Map();
|
|
169
|
+
bufferedCount = 0;
|
|
170
|
+
elevated = /* @__PURE__ */ new Set();
|
|
95
171
|
constructor(rate, options = {}) {
|
|
96
172
|
if (rate < 0 || rate > 1) {
|
|
97
173
|
throw new Error(`rate must be between 0 and 1, got ${String(rate)}`);
|
|
98
174
|
}
|
|
99
175
|
this.rate = rate;
|
|
100
176
|
this.rng = options.rng ?? Math.random;
|
|
177
|
+
this.traceKey = options.traceKey ?? "traceId";
|
|
178
|
+
this.elevateAt = parseLevel(options.elevateAt ?? 40 /* ERROR */);
|
|
179
|
+
this.transports = options.transports;
|
|
180
|
+
this.maxBufferedRecords = options.maxBufferedRecords ?? 1e3;
|
|
181
|
+
this.maxTraces = options.maxTraces ?? 200;
|
|
101
182
|
}
|
|
102
183
|
beforeLog(record) {
|
|
103
|
-
|
|
184
|
+
const transports = this.transports;
|
|
185
|
+
if (transports === void 0) {
|
|
186
|
+
return this.rng() < this.rate ? record : null;
|
|
187
|
+
}
|
|
188
|
+
const traceId = record.meta[this.traceKey];
|
|
189
|
+
if (traceId !== void 0 && this.elevated.has(traceId)) {
|
|
190
|
+
return record;
|
|
191
|
+
}
|
|
192
|
+
const keep = this.rng() < this.rate;
|
|
193
|
+
const reachedElevateLevel = parseLevel(record.level) >= this.elevateAt;
|
|
194
|
+
if (traceId !== void 0 && reachedElevateLevel) {
|
|
195
|
+
this.elevate(traceId, transports);
|
|
196
|
+
return record;
|
|
197
|
+
}
|
|
198
|
+
if (keep) {
|
|
199
|
+
return record;
|
|
200
|
+
}
|
|
201
|
+
if (traceId !== void 0) {
|
|
202
|
+
this.bufferRecord(traceId, record);
|
|
203
|
+
}
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
elevate(traceId, transports) {
|
|
207
|
+
this.elevated.add(traceId);
|
|
208
|
+
const buffered = this.buffer.get(traceId) ?? [];
|
|
209
|
+
this.buffer.delete(traceId);
|
|
210
|
+
this.bufferedCount -= buffered.length;
|
|
211
|
+
for (const bufferedRecord of buffered) {
|
|
212
|
+
for (const transport of transports) {
|
|
213
|
+
transport.write(transport.format(bufferedRecord), bufferedRecord);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
bufferRecord(traceId, record) {
|
|
218
|
+
let records = this.buffer.get(traceId);
|
|
219
|
+
if (records) {
|
|
220
|
+
this.buffer.delete(traceId);
|
|
221
|
+
this.buffer.set(traceId, records);
|
|
222
|
+
} else {
|
|
223
|
+
if (this.buffer.size >= this.maxTraces) {
|
|
224
|
+
this.evictOldestTrace();
|
|
225
|
+
}
|
|
226
|
+
records = [];
|
|
227
|
+
this.buffer.set(traceId, records);
|
|
228
|
+
}
|
|
229
|
+
records.push(record);
|
|
230
|
+
this.bufferedCount += 1;
|
|
231
|
+
while (this.bufferedCount > this.maxBufferedRecords && this.buffer.size > 0) {
|
|
232
|
+
this.evictOldestTrace();
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
evictOldestTrace() {
|
|
236
|
+
const oldest = this.buffer.entries().next();
|
|
237
|
+
if (oldest.done) {
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
const [oldestKey, oldestRecords] = oldest.value;
|
|
241
|
+
this.buffer.delete(oldestKey);
|
|
242
|
+
this.bufferedCount -= oldestRecords.length;
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
var GENESIS_HASH = "0".repeat(64);
|
|
246
|
+
function canonicalStringify(value) {
|
|
247
|
+
if (Array.isArray(value)) {
|
|
248
|
+
return `[${value.map((entry) => canonicalStringify(entry)).join(",")}]`;
|
|
249
|
+
}
|
|
250
|
+
if (value !== null && typeof value === "object") {
|
|
251
|
+
const entries = Object.entries(value).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
252
|
+
return `{${entries.map(([key, entryValue]) => `${JSON.stringify(key)}:${canonicalStringify(entryValue)}`).join(",")}}`;
|
|
253
|
+
}
|
|
254
|
+
if (value === void 0) {
|
|
255
|
+
return "null";
|
|
256
|
+
}
|
|
257
|
+
return JSON.stringify(value);
|
|
258
|
+
}
|
|
259
|
+
function computeHash(record, prevHash) {
|
|
260
|
+
const restMeta = Object.fromEntries(Object.entries(record.meta).filter(([key]) => key !== "hash" && key !== "prevHash"));
|
|
261
|
+
const payload = canonicalStringify({
|
|
262
|
+
timestamp: record.timestamp,
|
|
263
|
+
level: record.level,
|
|
264
|
+
logger: record.logger,
|
|
265
|
+
message: record.message,
|
|
266
|
+
meta: restMeta
|
|
267
|
+
});
|
|
268
|
+
return crypto.createHash("sha256").update(`${prevHash}${payload}`).digest("hex");
|
|
269
|
+
}
|
|
270
|
+
var TamperEvidentPlugin = class {
|
|
271
|
+
genesisHash;
|
|
272
|
+
lastHash;
|
|
273
|
+
constructor(options = {}) {
|
|
274
|
+
this.genesisHash = options.genesisHash ?? GENESIS_HASH;
|
|
275
|
+
this.lastHash = this.genesisHash;
|
|
276
|
+
}
|
|
277
|
+
beforeLog(record) {
|
|
278
|
+
const prevHash = this.lastHash;
|
|
279
|
+
const digest = computeHash(record, prevHash);
|
|
280
|
+
const next = { ...record, meta: { ...record.meta, prevHash, hash: digest } };
|
|
281
|
+
this.lastHash = digest;
|
|
282
|
+
return next;
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Returns `true` iff every record's hash matches its content plus the
|
|
286
|
+
* previous record's hash, in the given order. Returns `false` at the
|
|
287
|
+
* first break in the chain (an edited, removed, or reordered record).
|
|
288
|
+
*/
|
|
289
|
+
static verifyChain(records, options = {}) {
|
|
290
|
+
let prevHash = options.genesisHash ?? GENESIS_HASH;
|
|
291
|
+
for (const record of records) {
|
|
292
|
+
const storedHash = record.meta.hash;
|
|
293
|
+
const storedPrevHash = record.meta.prevHash;
|
|
294
|
+
if (typeof storedHash !== "string" || storedPrevHash !== prevHash) {
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
if (computeHash(record, prevHash) !== storedHash) {
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
prevHash = storedHash;
|
|
301
|
+
}
|
|
302
|
+
return true;
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
// src/plugins/alerting-plugin.ts
|
|
307
|
+
function defaultDedupeKey(record) {
|
|
308
|
+
return `${record.level}:${record.logger}:${record.message}`;
|
|
309
|
+
}
|
|
310
|
+
var AlertingPlugin = class {
|
|
311
|
+
threshold;
|
|
312
|
+
dedupeWindowMs;
|
|
313
|
+
maxTrackedKeys;
|
|
314
|
+
dedupeKeyFn;
|
|
315
|
+
windows = /* @__PURE__ */ new Map();
|
|
316
|
+
constructor(options = {}) {
|
|
317
|
+
this.threshold = parseLevel(options.threshold ?? 40 /* ERROR */);
|
|
318
|
+
this.dedupeWindowMs = options.dedupeWindowMs ?? 3e5;
|
|
319
|
+
this.dedupeKeyFn = options.dedupeKey ?? defaultDedupeKey;
|
|
320
|
+
this.maxTrackedKeys = options.maxTrackedKeys ?? 500;
|
|
321
|
+
}
|
|
322
|
+
afterLog(record) {
|
|
323
|
+
if (parseLevel(record.level) < this.threshold) {
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
const key = this.dedupeKeyFn(record);
|
|
327
|
+
const existing = this.windows.get(key);
|
|
328
|
+
if (existing) {
|
|
329
|
+
existing.count += 1;
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
if (this.windows.size >= this.maxTrackedKeys) {
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
const timer = setTimeout(() => {
|
|
336
|
+
this.flush(key);
|
|
337
|
+
}, this.dedupeWindowMs);
|
|
338
|
+
timer.unref();
|
|
339
|
+
this.windows.set(key, { record, count: 1, timer });
|
|
340
|
+
this.safeSend(record, 1);
|
|
341
|
+
}
|
|
342
|
+
flush(key) {
|
|
343
|
+
const window = this.windows.get(key);
|
|
344
|
+
this.windows.delete(key);
|
|
345
|
+
if (!window || window.count <= 1) {
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
this.safeSend(window.record, window.count);
|
|
349
|
+
}
|
|
350
|
+
safeSend(record, occurrences) {
|
|
351
|
+
Promise.resolve().then(() => this.sendAlert(record, occurrences)).catch((error) => {
|
|
352
|
+
try {
|
|
353
|
+
this.onError?.(error, record);
|
|
354
|
+
} catch {
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
/** Cancel any pending dedupe-window timers. Call on logger shutdown. */
|
|
359
|
+
close() {
|
|
360
|
+
const windows = [...this.windows.values()];
|
|
361
|
+
this.windows.clear();
|
|
362
|
+
for (const window of windows) {
|
|
363
|
+
clearTimeout(window.timer);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
// src/plugins/slack-alert-plugin.ts
|
|
369
|
+
async function fetchSlackSender(webhookUrl, body) {
|
|
370
|
+
const response = await fetch(webhookUrl, {
|
|
371
|
+
method: "POST",
|
|
372
|
+
headers: { "Content-Type": "application/json" },
|
|
373
|
+
body
|
|
374
|
+
});
|
|
375
|
+
if (!response.ok) {
|
|
376
|
+
throw new Error(
|
|
377
|
+
`SlackAlertPlugin: webhook returned HTTP ${String(response.status)} \u2014 check the webhook URL is still valid in Slack's app config`
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
function formatMessage(record, occurrences) {
|
|
382
|
+
const suffix = occurrences > 1 ? ` (x${String(occurrences)})` : "";
|
|
383
|
+
return `[${record.level}] ${record.logger}: ${record.message}${suffix}`;
|
|
384
|
+
}
|
|
385
|
+
var SlackAlertPlugin = class extends AlertingPlugin {
|
|
386
|
+
webhookUrl;
|
|
387
|
+
sender;
|
|
388
|
+
constructor(webhookUrl, options = {}) {
|
|
389
|
+
super(options);
|
|
390
|
+
this.webhookUrl = webhookUrl;
|
|
391
|
+
this.sender = options.sender ?? fetchSlackSender;
|
|
392
|
+
}
|
|
393
|
+
async sendAlert(record, occurrences) {
|
|
394
|
+
const body = JSON.stringify({ text: formatMessage(record, occurrences) });
|
|
395
|
+
await this.sender(this.webhookUrl, body);
|
|
396
|
+
}
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
// src/plugins/pagerduty-alert-plugin.ts
|
|
400
|
+
var ENDPOINT = "https://events.pagerduty.com/v2/enqueue";
|
|
401
|
+
var SEVERITY = { ERROR: "error", FATAL: "critical" };
|
|
402
|
+
async function fetchPagerDutySender(body) {
|
|
403
|
+
const response = await fetch(ENDPOINT, {
|
|
404
|
+
method: "POST",
|
|
405
|
+
headers: { "Content-Type": "application/json" },
|
|
406
|
+
body
|
|
407
|
+
});
|
|
408
|
+
if (!response.ok) {
|
|
409
|
+
throw new Error(
|
|
410
|
+
`PagerDutyAlertPlugin: Events API returned HTTP ${String(response.status)} \u2014 check the routing key is a valid Events API v2 integration key`
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
var PagerDutyAlertPlugin = class extends AlertingPlugin {
|
|
415
|
+
routingKey;
|
|
416
|
+
sender;
|
|
417
|
+
constructor(routingKey, options = {}) {
|
|
418
|
+
super(options);
|
|
419
|
+
this.routingKey = routingKey;
|
|
420
|
+
this.sender = options.sender ?? fetchPagerDutySender;
|
|
421
|
+
}
|
|
422
|
+
async sendAlert(record, occurrences) {
|
|
423
|
+
let summary = `${record.logger}: ${record.message}`;
|
|
424
|
+
if (occurrences > 1) {
|
|
425
|
+
summary += ` (x${String(occurrences)})`;
|
|
426
|
+
}
|
|
427
|
+
const body = JSON.stringify({
|
|
428
|
+
routing_key: this.routingKey,
|
|
429
|
+
event_action: "trigger",
|
|
430
|
+
payload: {
|
|
431
|
+
summary,
|
|
432
|
+
severity: SEVERITY[record.level] ?? "error",
|
|
433
|
+
source: record.logger,
|
|
434
|
+
timestamp: record.timestamp,
|
|
435
|
+
custom_details: { occurrences, ...record.meta }
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
await this.sender(body);
|
|
439
|
+
}
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
// src/plugins/email-alert-plugin.ts
|
|
443
|
+
var EmailAlertPlugin = class extends AlertingPlugin {
|
|
444
|
+
smtpHost;
|
|
445
|
+
smtpPort;
|
|
446
|
+
fromAddr;
|
|
447
|
+
toAddrs;
|
|
448
|
+
username;
|
|
449
|
+
password;
|
|
450
|
+
useTls;
|
|
451
|
+
injectedSender;
|
|
452
|
+
transporter;
|
|
453
|
+
constructor(options) {
|
|
454
|
+
super(options);
|
|
455
|
+
this.smtpHost = options.smtpHost;
|
|
456
|
+
this.smtpPort = options.smtpPort;
|
|
457
|
+
this.fromAddr = options.fromAddr;
|
|
458
|
+
this.toAddrs = options.toAddrs;
|
|
459
|
+
this.username = options.username;
|
|
460
|
+
this.password = options.password;
|
|
461
|
+
this.useTls = options.useTls ?? true;
|
|
462
|
+
this.injectedSender = options.sender;
|
|
463
|
+
}
|
|
464
|
+
async sendAlert(record, occurrences) {
|
|
465
|
+
let subject = `[${record.level}] ${record.logger}`;
|
|
466
|
+
if (occurrences > 1) {
|
|
467
|
+
subject += ` (x${String(occurrences)})`;
|
|
468
|
+
}
|
|
469
|
+
const text = [
|
|
470
|
+
record.message,
|
|
471
|
+
"",
|
|
472
|
+
`occurrences: ${String(occurrences)}`,
|
|
473
|
+
`timestamp: ${record.timestamp}`,
|
|
474
|
+
`meta: ${JSON.stringify(record.meta)}`
|
|
475
|
+
].join("\n");
|
|
476
|
+
const message = { from: this.fromAddr, to: this.toAddrs, subject, text };
|
|
477
|
+
if (this.injectedSender) {
|
|
478
|
+
await this.injectedSender(message);
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
const transporter = this.transporter ?? await this.importTransporter();
|
|
482
|
+
await transporter.sendMail({ from: message.from, to: message.to.join(", "), subject: message.subject, text: message.text });
|
|
483
|
+
}
|
|
484
|
+
async importTransporter() {
|
|
485
|
+
let createTransport;
|
|
486
|
+
try {
|
|
487
|
+
const moduleName = "nodemailer";
|
|
488
|
+
const mod = await import(moduleName);
|
|
489
|
+
const resolved = mod.default?.createTransport ?? mod.createTransport;
|
|
490
|
+
if (!resolved) {
|
|
491
|
+
throw new Error("no createTransport export found");
|
|
492
|
+
}
|
|
493
|
+
createTransport = resolved;
|
|
494
|
+
} catch {
|
|
495
|
+
throw new Error(
|
|
496
|
+
"EmailAlertPlugin: install `nodemailer` to use this plugin without providing a `sender` \u2014 `npm install nodemailer`"
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
this.transporter = createTransport({
|
|
500
|
+
host: this.smtpHost,
|
|
501
|
+
port: this.smtpPort,
|
|
502
|
+
secure: false,
|
|
503
|
+
requireTLS: this.useTls,
|
|
504
|
+
auth: this.username && this.password ? { user: this.username, pass: this.password } : void 0
|
|
505
|
+
});
|
|
506
|
+
return this.transporter;
|
|
104
507
|
}
|
|
105
508
|
};
|
|
106
509
|
|
|
107
|
-
// src/transport.ts
|
|
510
|
+
// src/transports/transport.ts
|
|
108
511
|
var Transport = class {
|
|
109
512
|
formatter;
|
|
110
513
|
constructor(formatter = new JSONFormatter()) {
|
|
@@ -130,7 +533,1050 @@ var CollectingTransport = class extends Transport {
|
|
|
130
533
|
}
|
|
131
534
|
};
|
|
132
535
|
|
|
133
|
-
// src/
|
|
536
|
+
// src/transports/batching-transport.ts
|
|
537
|
+
var BatchingTransport = class extends Transport {
|
|
538
|
+
maxRecords;
|
|
539
|
+
maxBytes;
|
|
540
|
+
buffer = [];
|
|
541
|
+
bufferBytes = 0;
|
|
542
|
+
constructor(options = {}) {
|
|
543
|
+
super(options.formatter);
|
|
544
|
+
this.maxRecords = options.maxRecords ?? 100;
|
|
545
|
+
this.maxBytes = options.maxBytes ?? 1e6;
|
|
546
|
+
}
|
|
547
|
+
/** Converts a written record into the buffered item type. Defaults to the record itself. */
|
|
548
|
+
toItem(formatted, record) {
|
|
549
|
+
return record;
|
|
550
|
+
}
|
|
551
|
+
/** Estimated byte size of one buffered item, used for the `maxBytes` bound. */
|
|
552
|
+
sizeOf(item) {
|
|
553
|
+
return JSON.stringify(item).length;
|
|
554
|
+
}
|
|
555
|
+
write(formatted, record) {
|
|
556
|
+
const item = this.toItem(formatted, record);
|
|
557
|
+
this.buffer.push(item);
|
|
558
|
+
this.bufferBytes += this.sizeOf(item);
|
|
559
|
+
if (this.buffer.length >= this.maxRecords || this.bufferBytes >= this.maxBytes) {
|
|
560
|
+
this.flush();
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
/** Send the current batch now, even if it hasn't reached a bound. */
|
|
564
|
+
flush() {
|
|
565
|
+
if (this.buffer.length === 0) {
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
const batch = this.buffer;
|
|
569
|
+
this.buffer = [];
|
|
570
|
+
this.bufferBytes = 0;
|
|
571
|
+
const result = this.sendBatch(batch);
|
|
572
|
+
if (result) {
|
|
573
|
+
result.catch((error) => {
|
|
574
|
+
console.error(`${this.constructor.name}: failed to send log batch`, error);
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
close() {
|
|
579
|
+
this.flush();
|
|
580
|
+
}
|
|
581
|
+
};
|
|
582
|
+
|
|
583
|
+
// src/transports/sql/base-sql-transport.ts
|
|
584
|
+
function metaString(meta, key) {
|
|
585
|
+
const value = meta[key];
|
|
586
|
+
return typeof value === "string" ? value : null;
|
|
587
|
+
}
|
|
588
|
+
var BaseSQLTransport = class extends BatchingTransport {
|
|
589
|
+
tableName;
|
|
590
|
+
ensureSchema;
|
|
591
|
+
schemaEnsured = false;
|
|
592
|
+
constructor(options = {}) {
|
|
593
|
+
super(options);
|
|
594
|
+
this.tableName = options.tableName ?? "logs";
|
|
595
|
+
this.ensureSchema = options.ensureSchema ?? false;
|
|
596
|
+
}
|
|
597
|
+
toItem(_formatted, record) {
|
|
598
|
+
return {
|
|
599
|
+
timestamp: record.timestamp,
|
|
600
|
+
level: record.level,
|
|
601
|
+
logger: record.logger,
|
|
602
|
+
message: record.message,
|
|
603
|
+
meta: JSON.stringify(record.meta),
|
|
604
|
+
runId: metaString(record.meta, "runId"),
|
|
605
|
+
spanId: metaString(record.meta, "spanId"),
|
|
606
|
+
parentSpanId: metaString(record.meta, "parentSpanId"),
|
|
607
|
+
traceId: metaString(record.meta, "traceId")
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
sizeOf(row) {
|
|
611
|
+
return row.message.length + row.meta.length + 96;
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* Minimal, dialect-generic `CREATE TABLE IF NOT EXISTS` for dev/test use via
|
|
615
|
+
* `ensureSchema: true`. Production deployments should manage this table with
|
|
616
|
+
* a real migration instead — override in a subclass for dialect-correct
|
|
617
|
+
* column types (e.g. `JSONB` on Postgres).
|
|
618
|
+
*/
|
|
619
|
+
createTableSQL() {
|
|
620
|
+
return `CREATE TABLE IF NOT EXISTS ${this.tableName} (
|
|
621
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
622
|
+
timestamp TEXT NOT NULL,
|
|
623
|
+
level TEXT NOT NULL,
|
|
624
|
+
logger TEXT NOT NULL,
|
|
625
|
+
message TEXT NOT NULL,
|
|
626
|
+
meta TEXT NOT NULL,
|
|
627
|
+
runId TEXT,
|
|
628
|
+
spanId TEXT,
|
|
629
|
+
parentSpanId TEXT,
|
|
630
|
+
traceId TEXT
|
|
631
|
+
)`;
|
|
632
|
+
}
|
|
633
|
+
async sendBatch(rows) {
|
|
634
|
+
if (this.ensureSchema && !this.schemaEnsured) {
|
|
635
|
+
this.schemaEnsured = true;
|
|
636
|
+
await this.ensureTable();
|
|
637
|
+
}
|
|
638
|
+
await this.insertRows(rows);
|
|
639
|
+
}
|
|
640
|
+
};
|
|
641
|
+
|
|
642
|
+
// src/transports/sql/sqlite-transport.ts
|
|
643
|
+
var SQLiteTransport = class extends BaseSQLTransport {
|
|
644
|
+
injectedClient;
|
|
645
|
+
filename;
|
|
646
|
+
client;
|
|
647
|
+
constructor(options = {}) {
|
|
648
|
+
super(options);
|
|
649
|
+
this.injectedClient = options.client;
|
|
650
|
+
this.filename = options.filename ?? ":memory:";
|
|
651
|
+
}
|
|
652
|
+
/** Synchronously available client, if one was injected or already imported — avoids an unnecessary microtask hop on the hot path. */
|
|
653
|
+
resolvedClient() {
|
|
654
|
+
return this.injectedClient ?? this.client;
|
|
655
|
+
}
|
|
656
|
+
async importClient() {
|
|
657
|
+
let DatabaseCtor;
|
|
658
|
+
try {
|
|
659
|
+
const moduleName = "better-sqlite3";
|
|
660
|
+
const mod = await import(moduleName);
|
|
661
|
+
DatabaseCtor = mod.default;
|
|
662
|
+
} catch {
|
|
663
|
+
throw new Error(
|
|
664
|
+
"SQLiteTransport: install `better-sqlite3` to use this transport without providing a client \u2014 `npm install better-sqlite3`"
|
|
665
|
+
);
|
|
666
|
+
}
|
|
667
|
+
this.client = new DatabaseCtor(this.filename);
|
|
668
|
+
return this.client;
|
|
669
|
+
}
|
|
670
|
+
async ensureTable() {
|
|
671
|
+
const client = this.resolvedClient() ?? await this.importClient();
|
|
672
|
+
client.exec(this.createTableSQL());
|
|
673
|
+
}
|
|
674
|
+
async insertRows(rows) {
|
|
675
|
+
const client = this.resolvedClient() ?? await this.importClient();
|
|
676
|
+
const stmt = client.prepare(
|
|
677
|
+
`INSERT INTO ${this.tableName} (timestamp, level, logger, message, meta, runId, spanId, parentSpanId, traceId) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
678
|
+
);
|
|
679
|
+
const insertMany = client.transaction((batch) => {
|
|
680
|
+
for (const row of batch) {
|
|
681
|
+
stmt.run(
|
|
682
|
+
row.timestamp,
|
|
683
|
+
row.level,
|
|
684
|
+
row.logger,
|
|
685
|
+
row.message,
|
|
686
|
+
row.meta,
|
|
687
|
+
row.runId,
|
|
688
|
+
row.spanId,
|
|
689
|
+
row.parentSpanId,
|
|
690
|
+
row.traceId
|
|
691
|
+
);
|
|
692
|
+
}
|
|
693
|
+
});
|
|
694
|
+
insertMany(rows);
|
|
695
|
+
}
|
|
696
|
+
};
|
|
697
|
+
|
|
698
|
+
// src/transports/sql/postgres-transport.ts
|
|
699
|
+
var PostgresTransport = class extends BaseSQLTransport {
|
|
700
|
+
injectedClient;
|
|
701
|
+
connectionString;
|
|
702
|
+
connectionConfig;
|
|
703
|
+
client;
|
|
704
|
+
constructor(options = {}) {
|
|
705
|
+
super(options);
|
|
706
|
+
this.injectedClient = options.client;
|
|
707
|
+
this.connectionString = options.connectionString;
|
|
708
|
+
this.connectionConfig = options.connectionConfig;
|
|
709
|
+
}
|
|
710
|
+
/** Synchronously available client, if one was injected or already imported — avoids an unnecessary microtask hop on the hot path. */
|
|
711
|
+
resolvedClient() {
|
|
712
|
+
return this.injectedClient ?? this.client;
|
|
713
|
+
}
|
|
714
|
+
async importClient() {
|
|
715
|
+
let PoolCtor;
|
|
716
|
+
try {
|
|
717
|
+
const moduleName = "pg";
|
|
718
|
+
const mod = await import(moduleName);
|
|
719
|
+
const resolved = mod.default?.Pool ?? mod.Pool;
|
|
720
|
+
if (!resolved) {
|
|
721
|
+
throw new Error("no Pool export found");
|
|
722
|
+
}
|
|
723
|
+
PoolCtor = resolved;
|
|
724
|
+
} catch {
|
|
725
|
+
throw new Error(
|
|
726
|
+
"PostgresTransport: install `pg` to use this transport without providing a client \u2014 `npm install pg`"
|
|
727
|
+
);
|
|
728
|
+
}
|
|
729
|
+
const config = this.connectionString ? { connectionString: this.connectionString } : this.connectionConfig ?? {};
|
|
730
|
+
this.client = new PoolCtor(config);
|
|
731
|
+
return this.client;
|
|
732
|
+
}
|
|
733
|
+
/** Postgres-correct `CREATE TABLE IF NOT EXISTS`: `SERIAL` primary key, `JSONB` for `meta`. */
|
|
734
|
+
createTableSQL() {
|
|
735
|
+
return `CREATE TABLE IF NOT EXISTS ${this.tableName} (
|
|
736
|
+
id SERIAL PRIMARY KEY,
|
|
737
|
+
timestamp TEXT NOT NULL,
|
|
738
|
+
level TEXT NOT NULL,
|
|
739
|
+
logger TEXT NOT NULL,
|
|
740
|
+
message TEXT NOT NULL,
|
|
741
|
+
meta JSONB NOT NULL,
|
|
742
|
+
"runId" TEXT,
|
|
743
|
+
"spanId" TEXT,
|
|
744
|
+
"parentSpanId" TEXT,
|
|
745
|
+
"traceId" TEXT
|
|
746
|
+
)`;
|
|
747
|
+
}
|
|
748
|
+
async ensureTable() {
|
|
749
|
+
const client = this.resolvedClient() ?? await this.importClient();
|
|
750
|
+
await client.query(this.createTableSQL(), []);
|
|
751
|
+
}
|
|
752
|
+
async insertRows(rows) {
|
|
753
|
+
const client = this.resolvedClient() ?? await this.importClient();
|
|
754
|
+
const columns = 9;
|
|
755
|
+
const values = [];
|
|
756
|
+
const placeholders = [];
|
|
757
|
+
rows.forEach((row, rowIndex) => {
|
|
758
|
+
const base = rowIndex * columns;
|
|
759
|
+
placeholders.push(
|
|
760
|
+
`(${Array.from({ length: columns }, (_, col) => `$${String(base + col + 1)}`).join(", ")})`
|
|
761
|
+
);
|
|
762
|
+
values.push(
|
|
763
|
+
row.timestamp,
|
|
764
|
+
row.level,
|
|
765
|
+
row.logger,
|
|
766
|
+
row.message,
|
|
767
|
+
row.meta,
|
|
768
|
+
row.runId,
|
|
769
|
+
row.spanId,
|
|
770
|
+
row.parentSpanId,
|
|
771
|
+
row.traceId
|
|
772
|
+
);
|
|
773
|
+
});
|
|
774
|
+
const sql = `INSERT INTO ${this.tableName} (timestamp, level, logger, message, meta, "runId", "spanId", "parentSpanId", "traceId") VALUES ${placeholders.join(", ")}`;
|
|
775
|
+
await client.query(sql, values);
|
|
776
|
+
}
|
|
777
|
+
};
|
|
778
|
+
|
|
779
|
+
// src/transports/sql/mysql-transport.ts
|
|
780
|
+
var MySQLTransport = class extends BaseSQLTransport {
|
|
781
|
+
injectedClient;
|
|
782
|
+
connectionString;
|
|
783
|
+
connectionConfig;
|
|
784
|
+
client;
|
|
785
|
+
constructor(options = {}) {
|
|
786
|
+
super(options);
|
|
787
|
+
this.injectedClient = options.client;
|
|
788
|
+
this.connectionString = options.connectionString;
|
|
789
|
+
this.connectionConfig = options.connectionConfig;
|
|
790
|
+
}
|
|
791
|
+
/** Synchronously available client, if one was injected or already imported — avoids an unnecessary microtask hop on the hot path. */
|
|
792
|
+
resolvedClient() {
|
|
793
|
+
return this.injectedClient ?? this.client;
|
|
794
|
+
}
|
|
795
|
+
async importClient() {
|
|
796
|
+
let createPool;
|
|
797
|
+
try {
|
|
798
|
+
const moduleName = "mysql2/promise";
|
|
799
|
+
const mod = await import(moduleName);
|
|
800
|
+
const resolved = mod.default?.createPool ?? mod.createPool;
|
|
801
|
+
if (!resolved) {
|
|
802
|
+
throw new Error("no createPool export found");
|
|
803
|
+
}
|
|
804
|
+
createPool = resolved;
|
|
805
|
+
} catch {
|
|
806
|
+
throw new Error(
|
|
807
|
+
"MySQLTransport: install `mysql2` to use this transport without providing a client \u2014 `npm install mysql2`"
|
|
808
|
+
);
|
|
809
|
+
}
|
|
810
|
+
const target = this.connectionString ?? this.connectionConfig ?? {};
|
|
811
|
+
this.client = createPool(target);
|
|
812
|
+
return this.client;
|
|
813
|
+
}
|
|
814
|
+
/** MySQL-correct `CREATE TABLE IF NOT EXISTS`: `AUTO_INCREMENT` primary key, `JSON` column type for `meta`. */
|
|
815
|
+
createTableSQL() {
|
|
816
|
+
return `CREATE TABLE IF NOT EXISTS ${this.tableName} (
|
|
817
|
+
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
818
|
+
timestamp VARCHAR(64) NOT NULL,
|
|
819
|
+
level VARCHAR(16) NOT NULL,
|
|
820
|
+
logger VARCHAR(255) NOT NULL,
|
|
821
|
+
message TEXT NOT NULL,
|
|
822
|
+
meta JSON NOT NULL,
|
|
823
|
+
runId VARCHAR(255),
|
|
824
|
+
spanId VARCHAR(255),
|
|
825
|
+
parentSpanId VARCHAR(255),
|
|
826
|
+
traceId VARCHAR(255)
|
|
827
|
+
)`;
|
|
828
|
+
}
|
|
829
|
+
async ensureTable() {
|
|
830
|
+
const client = this.resolvedClient() ?? await this.importClient();
|
|
831
|
+
await client.execute(this.createTableSQL(), []);
|
|
832
|
+
}
|
|
833
|
+
async insertRows(rows) {
|
|
834
|
+
const client = this.resolvedClient() ?? await this.importClient();
|
|
835
|
+
const columns = 9;
|
|
836
|
+
const values = [];
|
|
837
|
+
const placeholders = [];
|
|
838
|
+
for (const row of rows) {
|
|
839
|
+
placeholders.push(`(${Array.from({ length: columns }, () => "?").join(", ")})`);
|
|
840
|
+
values.push(
|
|
841
|
+
row.timestamp,
|
|
842
|
+
row.level,
|
|
843
|
+
row.logger,
|
|
844
|
+
row.message,
|
|
845
|
+
row.meta,
|
|
846
|
+
row.runId,
|
|
847
|
+
row.spanId,
|
|
848
|
+
row.parentSpanId,
|
|
849
|
+
row.traceId
|
|
850
|
+
);
|
|
851
|
+
}
|
|
852
|
+
const sql = `INSERT INTO ${this.tableName} (timestamp, level, logger, message, meta, runId, spanId, parentSpanId, traceId) VALUES ${placeholders.join(", ")}`;
|
|
853
|
+
await client.execute(sql, values);
|
|
854
|
+
}
|
|
855
|
+
};
|
|
856
|
+
|
|
857
|
+
// src/transports/nosql/mongodb-transport.ts
|
|
858
|
+
var MongoDBTransport = class extends BatchingTransport {
|
|
859
|
+
injectedCollection;
|
|
860
|
+
connectionString;
|
|
861
|
+
database;
|
|
862
|
+
collectionName;
|
|
863
|
+
collection;
|
|
864
|
+
constructor(options = {}) {
|
|
865
|
+
super(options);
|
|
866
|
+
this.injectedCollection = options.collection;
|
|
867
|
+
this.connectionString = options.connectionString;
|
|
868
|
+
this.database = options.database ?? "logquill";
|
|
869
|
+
this.collectionName = options.collectionName ?? "logs";
|
|
870
|
+
}
|
|
871
|
+
/** Synchronously available collection, if one was injected or already connected — avoids an unnecessary microtask hop on the hot path. */
|
|
872
|
+
resolvedCollection() {
|
|
873
|
+
return this.injectedCollection ?? this.collection;
|
|
874
|
+
}
|
|
875
|
+
async importCollection() {
|
|
876
|
+
if (!this.connectionString) {
|
|
877
|
+
throw new Error(
|
|
878
|
+
"MongoDBTransport: provide either a `collection` or a `connectionString` to connect with \u2014 neither was given"
|
|
879
|
+
);
|
|
880
|
+
}
|
|
881
|
+
let MongoClientCtor;
|
|
882
|
+
try {
|
|
883
|
+
const moduleName = "mongodb";
|
|
884
|
+
const mod = await import(moduleName);
|
|
885
|
+
MongoClientCtor = mod.MongoClient;
|
|
886
|
+
} catch {
|
|
887
|
+
throw new Error(
|
|
888
|
+
"MongoDBTransport: install `mongodb` to use this transport without providing a client \u2014 `npm install mongodb`"
|
|
889
|
+
);
|
|
890
|
+
}
|
|
891
|
+
const client = new MongoClientCtor(this.connectionString);
|
|
892
|
+
await client.connect();
|
|
893
|
+
this.collection = client.db(this.database).collection(this.collectionName);
|
|
894
|
+
return this.collection;
|
|
895
|
+
}
|
|
896
|
+
async sendBatch(batch) {
|
|
897
|
+
const collection = this.resolvedCollection() ?? await this.importCollection();
|
|
898
|
+
await collection.insertMany(batch.map((record) => ({ ...record })));
|
|
899
|
+
}
|
|
900
|
+
};
|
|
901
|
+
|
|
902
|
+
// src/transports/nosql/dynamodb-transport.ts
|
|
903
|
+
var DYNAMO_BATCH_LIMIT = 25;
|
|
904
|
+
function toAttributeValue(value) {
|
|
905
|
+
if (value === null || value === void 0) {
|
|
906
|
+
return { NULL: true };
|
|
907
|
+
}
|
|
908
|
+
if (typeof value === "string") {
|
|
909
|
+
return { S: value };
|
|
910
|
+
}
|
|
911
|
+
if (typeof value === "number") {
|
|
912
|
+
return { N: String(value) };
|
|
913
|
+
}
|
|
914
|
+
if (typeof value === "boolean") {
|
|
915
|
+
return { BOOL: value };
|
|
916
|
+
}
|
|
917
|
+
if (typeof value === "bigint") {
|
|
918
|
+
return { N: value.toString() };
|
|
919
|
+
}
|
|
920
|
+
if (Array.isArray(value)) {
|
|
921
|
+
return { L: value.map((entry) => toAttributeValue(entry)) };
|
|
922
|
+
}
|
|
923
|
+
if (typeof value === "object") {
|
|
924
|
+
const m = {};
|
|
925
|
+
for (const [key, entryValue] of Object.entries(value)) {
|
|
926
|
+
m[key] = toAttributeValue(entryValue);
|
|
927
|
+
}
|
|
928
|
+
return { M: m };
|
|
929
|
+
}
|
|
930
|
+
return { NULL: true };
|
|
931
|
+
}
|
|
932
|
+
function marshallItem(item) {
|
|
933
|
+
return toAttributeValue({ ...item }).M;
|
|
934
|
+
}
|
|
935
|
+
var DynamoDBTransport = class extends BatchingTransport {
|
|
936
|
+
injectedClient;
|
|
937
|
+
tableName;
|
|
938
|
+
region;
|
|
939
|
+
client;
|
|
940
|
+
constructor(options = {}) {
|
|
941
|
+
super(options);
|
|
942
|
+
this.injectedClient = options.client;
|
|
943
|
+
this.tableName = options.tableName ?? "logs";
|
|
944
|
+
this.region = options.region;
|
|
945
|
+
}
|
|
946
|
+
/** Synchronously available client, if one was injected or already imported — avoids an unnecessary microtask hop on the hot path. */
|
|
947
|
+
resolvedClient() {
|
|
948
|
+
return this.injectedClient ?? this.client;
|
|
949
|
+
}
|
|
950
|
+
async importClient() {
|
|
951
|
+
let DynamoDBClientCtor;
|
|
952
|
+
let BatchWriteItemCommandCtor;
|
|
953
|
+
try {
|
|
954
|
+
const moduleName = "@aws-sdk/client-dynamodb";
|
|
955
|
+
const mod = await import(moduleName);
|
|
956
|
+
DynamoDBClientCtor = mod.DynamoDBClient;
|
|
957
|
+
BatchWriteItemCommandCtor = mod.BatchWriteItemCommand;
|
|
958
|
+
} catch {
|
|
959
|
+
throw new Error(
|
|
960
|
+
"DynamoDBTransport: install `@aws-sdk/client-dynamodb` to use this transport without providing a client \u2014 `npm install @aws-sdk/client-dynamodb`"
|
|
961
|
+
);
|
|
962
|
+
}
|
|
963
|
+
const sdkClient = new DynamoDBClientCtor(this.region ? { region: this.region } : {});
|
|
964
|
+
this.client = {
|
|
965
|
+
async batchWriteItems(tableName, items) {
|
|
966
|
+
const requestItems = {
|
|
967
|
+
[tableName]: items.map((item) => ({ PutRequest: { Item: marshallItem(item) } }))
|
|
968
|
+
};
|
|
969
|
+
return sdkClient.send(new BatchWriteItemCommandCtor({ RequestItems: requestItems }));
|
|
970
|
+
}
|
|
971
|
+
};
|
|
972
|
+
return this.client;
|
|
973
|
+
}
|
|
974
|
+
/** `meta.runId`, else `meta.traceId`, else the logger name — see the class doc for why. */
|
|
975
|
+
partitionKey(record) {
|
|
976
|
+
const runId = record.meta.runId;
|
|
977
|
+
if (typeof runId === "string" && runId.length > 0) {
|
|
978
|
+
return runId;
|
|
979
|
+
}
|
|
980
|
+
const traceId = record.meta.traceId;
|
|
981
|
+
if (typeof traceId === "string" && traceId.length > 0) {
|
|
982
|
+
return traceId;
|
|
983
|
+
}
|
|
984
|
+
return record.logger;
|
|
985
|
+
}
|
|
986
|
+
toDynamoItem(record) {
|
|
987
|
+
const spanId = record.meta.spanId;
|
|
988
|
+
const parentSpanId = record.meta.parentSpanId;
|
|
989
|
+
const traceId = record.meta.traceId;
|
|
990
|
+
return {
|
|
991
|
+
runId: this.partitionKey(record),
|
|
992
|
+
timestamp: record.timestamp,
|
|
993
|
+
level: record.level,
|
|
994
|
+
logger: record.logger,
|
|
995
|
+
message: record.message,
|
|
996
|
+
meta: record.meta,
|
|
997
|
+
...typeof spanId === "string" ? { spanId } : {},
|
|
998
|
+
...typeof parentSpanId === "string" ? { parentSpanId } : {},
|
|
999
|
+
...typeof traceId === "string" ? { traceId } : {}
|
|
1000
|
+
};
|
|
1001
|
+
}
|
|
1002
|
+
async sendBatch(batch) {
|
|
1003
|
+
const client = this.resolvedClient() ?? await this.importClient();
|
|
1004
|
+
const items = batch.map((record) => this.toDynamoItem(record));
|
|
1005
|
+
const chunks = [];
|
|
1006
|
+
for (let offset = 0; offset < items.length; offset += DYNAMO_BATCH_LIMIT) {
|
|
1007
|
+
chunks.push(items.slice(offset, offset + DYNAMO_BATCH_LIMIT));
|
|
1008
|
+
}
|
|
1009
|
+
await Promise.all(chunks.map((chunk) => client.batchWriteItems(this.tableName, chunk)));
|
|
1010
|
+
}
|
|
1011
|
+
};
|
|
1012
|
+
|
|
1013
|
+
// src/transports/nosql/redis-transport.ts
|
|
1014
|
+
var RedisTransport = class extends BatchingTransport {
|
|
1015
|
+
injectedClient;
|
|
1016
|
+
url;
|
|
1017
|
+
stream;
|
|
1018
|
+
client;
|
|
1019
|
+
constructor(options = {}) {
|
|
1020
|
+
super(options);
|
|
1021
|
+
this.injectedClient = options.client;
|
|
1022
|
+
this.url = options.url ?? "redis://localhost:6379";
|
|
1023
|
+
this.stream = options.stream ?? "logquill:logs";
|
|
1024
|
+
}
|
|
1025
|
+
/** Synchronously available client, if one was injected or already connected — avoids an unnecessary microtask hop on the hot path. */
|
|
1026
|
+
resolvedClient() {
|
|
1027
|
+
return this.injectedClient ?? this.client;
|
|
1028
|
+
}
|
|
1029
|
+
async importClient() {
|
|
1030
|
+
let createClient;
|
|
1031
|
+
try {
|
|
1032
|
+
const moduleName = "redis";
|
|
1033
|
+
const mod = await import(moduleName);
|
|
1034
|
+
createClient = mod.createClient;
|
|
1035
|
+
} catch {
|
|
1036
|
+
throw new Error(
|
|
1037
|
+
"RedisTransport: install `redis` to use this transport without providing a client \u2014 `npm install redis`"
|
|
1038
|
+
);
|
|
1039
|
+
}
|
|
1040
|
+
const client = createClient({ url: this.url });
|
|
1041
|
+
await client.connect();
|
|
1042
|
+
this.client = client;
|
|
1043
|
+
return client;
|
|
1044
|
+
}
|
|
1045
|
+
toFields(record) {
|
|
1046
|
+
return {
|
|
1047
|
+
timestamp: record.timestamp,
|
|
1048
|
+
level: record.level,
|
|
1049
|
+
logger: record.logger,
|
|
1050
|
+
message: record.message,
|
|
1051
|
+
meta: JSON.stringify(record.meta)
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
async sendBatch(batch) {
|
|
1055
|
+
const client = this.resolvedClient() ?? await this.importClient();
|
|
1056
|
+
await Promise.all(batch.map((record) => client.xAdd(this.stream, "*", this.toFields(record))));
|
|
1057
|
+
}
|
|
1058
|
+
};
|
|
1059
|
+
|
|
1060
|
+
// src/transports/queue/base-queue-transport.ts
|
|
1061
|
+
var BaseQueueTransport = class extends BatchingTransport {
|
|
1062
|
+
topic;
|
|
1063
|
+
constructor(options) {
|
|
1064
|
+
super(options);
|
|
1065
|
+
this.topic = options.topic;
|
|
1066
|
+
}
|
|
1067
|
+
sendBatch(batch) {
|
|
1068
|
+
return this.publishBatch(batch);
|
|
1069
|
+
}
|
|
1070
|
+
};
|
|
1071
|
+
|
|
1072
|
+
// src/transports/queue/kafka-transport.ts
|
|
1073
|
+
function metaKey(meta, key) {
|
|
1074
|
+
const value = meta[key];
|
|
1075
|
+
return typeof value === "string" ? value : void 0;
|
|
1076
|
+
}
|
|
1077
|
+
var KafkaTransport = class extends BaseQueueTransport {
|
|
1078
|
+
injectedClient;
|
|
1079
|
+
brokers;
|
|
1080
|
+
client;
|
|
1081
|
+
constructor(options) {
|
|
1082
|
+
super(options);
|
|
1083
|
+
this.injectedClient = options.client;
|
|
1084
|
+
this.brokers = options.brokers ?? ["localhost:9092"];
|
|
1085
|
+
}
|
|
1086
|
+
/** Synchronously available producer, if one was injected or already imported — avoids an unnecessary microtask hop on the hot path. */
|
|
1087
|
+
resolvedClient() {
|
|
1088
|
+
return this.injectedClient ?? this.client;
|
|
1089
|
+
}
|
|
1090
|
+
/**
|
|
1091
|
+
* Builds the real `kafkajs` producer and connects it. Connection happens
|
|
1092
|
+
* here, once, as part of acquiring the driver — not on every
|
|
1093
|
+
* `publishBatch()` call — so an injected `client` (tests, or a caller's
|
|
1094
|
+
* own already-connected producer) is trusted to already be ready and is
|
|
1095
|
+
* never re-connected.
|
|
1096
|
+
*/
|
|
1097
|
+
async importClient() {
|
|
1098
|
+
let producer;
|
|
1099
|
+
try {
|
|
1100
|
+
const moduleName = "kafkajs";
|
|
1101
|
+
const mod = await import(moduleName);
|
|
1102
|
+
producer = new mod.Kafka({ brokers: this.brokers }).producer();
|
|
1103
|
+
await producer.connect?.();
|
|
1104
|
+
} catch {
|
|
1105
|
+
throw new Error(
|
|
1106
|
+
"KafkaTransport: install `kafkajs` to use this transport without providing a client \u2014 `npm install kafkajs`"
|
|
1107
|
+
);
|
|
1108
|
+
}
|
|
1109
|
+
this.client = producer;
|
|
1110
|
+
return producer;
|
|
1111
|
+
}
|
|
1112
|
+
async publishBatch(records) {
|
|
1113
|
+
const client = this.resolvedClient() ?? await this.importClient();
|
|
1114
|
+
await client.send({
|
|
1115
|
+
topic: this.topic,
|
|
1116
|
+
messages: records.map((record) => ({
|
|
1117
|
+
key: metaKey(record.meta, "runId") ?? metaKey(record.meta, "traceId") ?? null,
|
|
1118
|
+
value: JSON.stringify(record)
|
|
1119
|
+
}))
|
|
1120
|
+
});
|
|
1121
|
+
}
|
|
1122
|
+
};
|
|
1123
|
+
|
|
1124
|
+
// src/transports/queue/rabbitmq-transport.ts
|
|
1125
|
+
var RabbitMQTransport = class extends BaseQueueTransport {
|
|
1126
|
+
injectedClient;
|
|
1127
|
+
url;
|
|
1128
|
+
client;
|
|
1129
|
+
constructor(options) {
|
|
1130
|
+
super(options);
|
|
1131
|
+
this.injectedClient = options.client;
|
|
1132
|
+
this.url = options.url ?? "amqp://localhost";
|
|
1133
|
+
}
|
|
1134
|
+
/** Synchronously available channel, if one was injected or already imported — avoids an unnecessary microtask hop on the hot path. */
|
|
1135
|
+
resolvedClient() {
|
|
1136
|
+
return this.injectedClient ?? this.client;
|
|
1137
|
+
}
|
|
1138
|
+
/**
|
|
1139
|
+
* Opens the real `amqplib` connection/channel and asserts the queue
|
|
1140
|
+
* exists. Both happen here, once, as part of acquiring the driver — not on
|
|
1141
|
+
* every `publishBatch()` call — so an injected `client` (tests, or a
|
|
1142
|
+
* caller's own already-open channel) is trusted to already have its queue
|
|
1143
|
+
* set up and is never re-asserted.
|
|
1144
|
+
*/
|
|
1145
|
+
async importClient() {
|
|
1146
|
+
let channel;
|
|
1147
|
+
try {
|
|
1148
|
+
const moduleName = "amqplib";
|
|
1149
|
+
const mod = await import(moduleName);
|
|
1150
|
+
const connection = await mod.connect(this.url);
|
|
1151
|
+
channel = await connection.createChannel();
|
|
1152
|
+
await channel.assertQueue?.(this.topic);
|
|
1153
|
+
} catch {
|
|
1154
|
+
throw new Error(
|
|
1155
|
+
"RabbitMQTransport: install `amqplib` to use this transport without providing a client \u2014 `npm install amqplib`"
|
|
1156
|
+
);
|
|
1157
|
+
}
|
|
1158
|
+
this.client = channel;
|
|
1159
|
+
return channel;
|
|
1160
|
+
}
|
|
1161
|
+
async publishBatch(records) {
|
|
1162
|
+
const client = this.resolvedClient() ?? await this.importClient();
|
|
1163
|
+
for (const record of records) {
|
|
1164
|
+
client.sendToQueue(this.topic, Buffer.from(JSON.stringify(record)));
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
};
|
|
1168
|
+
|
|
1169
|
+
// src/transports/queue/sqs-transport.ts
|
|
1170
|
+
var SQS_BATCH_LIMIT = 10;
|
|
1171
|
+
var SQSTransport = class extends BaseQueueTransport {
|
|
1172
|
+
injectedClient;
|
|
1173
|
+
region;
|
|
1174
|
+
client;
|
|
1175
|
+
constructor(options) {
|
|
1176
|
+
super(options);
|
|
1177
|
+
this.injectedClient = options.client;
|
|
1178
|
+
this.region = options.region;
|
|
1179
|
+
}
|
|
1180
|
+
/** Synchronously available client, if one was injected or already imported — avoids an unnecessary microtask hop on the hot path. */
|
|
1181
|
+
resolvedClient() {
|
|
1182
|
+
return this.injectedClient ?? this.client;
|
|
1183
|
+
}
|
|
1184
|
+
async importClient() {
|
|
1185
|
+
let client;
|
|
1186
|
+
try {
|
|
1187
|
+
const moduleName = "@aws-sdk/client-sqs";
|
|
1188
|
+
const mod = await import(moduleName);
|
|
1189
|
+
const sdkClient = new mod.SQSClient({ region: this.region });
|
|
1190
|
+
const CommandCtor = mod.SendMessageBatchCommand;
|
|
1191
|
+
client = {
|
|
1192
|
+
sendMessageBatch: (queueUrl, entries) => sdkClient.send(
|
|
1193
|
+
new CommandCtor({
|
|
1194
|
+
QueueUrl: queueUrl,
|
|
1195
|
+
Entries: entries.map((entry) => ({ Id: entry.id, MessageBody: entry.body }))
|
|
1196
|
+
})
|
|
1197
|
+
)
|
|
1198
|
+
};
|
|
1199
|
+
} catch {
|
|
1200
|
+
throw new Error(
|
|
1201
|
+
"SQSTransport: install `@aws-sdk/client-sqs` to use this transport without providing a client \u2014 `npm install @aws-sdk/client-sqs`"
|
|
1202
|
+
);
|
|
1203
|
+
}
|
|
1204
|
+
this.client = client;
|
|
1205
|
+
return client;
|
|
1206
|
+
}
|
|
1207
|
+
async publishBatch(records) {
|
|
1208
|
+
const client = this.resolvedClient() ?? await this.importClient();
|
|
1209
|
+
const chunks = [];
|
|
1210
|
+
for (let start = 0; start < records.length; start += SQS_BATCH_LIMIT) {
|
|
1211
|
+
chunks.push(records.slice(start, start + SQS_BATCH_LIMIT));
|
|
1212
|
+
}
|
|
1213
|
+
await Promise.all(
|
|
1214
|
+
chunks.map(
|
|
1215
|
+
(chunk) => client.sendMessageBatch(
|
|
1216
|
+
this.topic,
|
|
1217
|
+
chunk.map((record, index) => ({ id: String(index), body: JSON.stringify(record) }))
|
|
1218
|
+
)
|
|
1219
|
+
)
|
|
1220
|
+
);
|
|
1221
|
+
}
|
|
1222
|
+
};
|
|
1223
|
+
|
|
1224
|
+
// src/transports/queue/pubsub-transport.ts
|
|
1225
|
+
var PubSubTransport = class extends BaseQueueTransport {
|
|
1226
|
+
injectedClient;
|
|
1227
|
+
projectId;
|
|
1228
|
+
client;
|
|
1229
|
+
constructor(options) {
|
|
1230
|
+
super(options);
|
|
1231
|
+
this.injectedClient = options.client;
|
|
1232
|
+
this.projectId = options.projectId;
|
|
1233
|
+
}
|
|
1234
|
+
/** Synchronously available topic reference, if one was injected or already imported — avoids an unnecessary microtask hop on the hot path. */
|
|
1235
|
+
resolvedClient() {
|
|
1236
|
+
return this.injectedClient ?? this.client;
|
|
1237
|
+
}
|
|
1238
|
+
async importClient() {
|
|
1239
|
+
let topic;
|
|
1240
|
+
try {
|
|
1241
|
+
const moduleName = "@google-cloud/pubsub";
|
|
1242
|
+
const mod = await import(moduleName);
|
|
1243
|
+
topic = new mod.PubSub({ projectId: this.projectId }).topic(this.topic);
|
|
1244
|
+
} catch {
|
|
1245
|
+
throw new Error(
|
|
1246
|
+
"PubSubTransport: install `@google-cloud/pubsub` to use this transport without providing a client \u2014 `npm install @google-cloud/pubsub`"
|
|
1247
|
+
);
|
|
1248
|
+
}
|
|
1249
|
+
this.client = topic;
|
|
1250
|
+
return topic;
|
|
1251
|
+
}
|
|
1252
|
+
async publishBatch(records) {
|
|
1253
|
+
const client = this.resolvedClient() ?? await this.importClient();
|
|
1254
|
+
await Promise.all(records.map((record) => client.publishMessage({ data: Buffer.from(JSON.stringify(record)) })));
|
|
1255
|
+
}
|
|
1256
|
+
};
|
|
1257
|
+
|
|
1258
|
+
// src/transports/cloud/cloudwatch-transport.ts
|
|
1259
|
+
var CloudWatchTransport = class extends BatchingTransport {
|
|
1260
|
+
logGroupName;
|
|
1261
|
+
logStreamName;
|
|
1262
|
+
region;
|
|
1263
|
+
injectedClient;
|
|
1264
|
+
client;
|
|
1265
|
+
constructor(options) {
|
|
1266
|
+
super(options);
|
|
1267
|
+
this.logGroupName = options.logGroupName;
|
|
1268
|
+
this.logStreamName = options.logStreamName;
|
|
1269
|
+
this.region = options.region;
|
|
1270
|
+
this.injectedClient = options.client;
|
|
1271
|
+
}
|
|
1272
|
+
/** Synchronously available client, if one was injected or already imported — avoids an unnecessary microtask hop on the hot path. */
|
|
1273
|
+
resolvedClient() {
|
|
1274
|
+
return this.injectedClient ?? this.client;
|
|
1275
|
+
}
|
|
1276
|
+
async importClient() {
|
|
1277
|
+
let ClientCtor;
|
|
1278
|
+
let PutLogEventsCommandCtor;
|
|
1279
|
+
try {
|
|
1280
|
+
const moduleName = "@aws-sdk/client-cloudwatch-logs";
|
|
1281
|
+
const mod = await import(moduleName);
|
|
1282
|
+
ClientCtor = mod.CloudWatchLogsClient;
|
|
1283
|
+
PutLogEventsCommandCtor = mod.PutLogEventsCommand;
|
|
1284
|
+
} catch {
|
|
1285
|
+
throw new Error(
|
|
1286
|
+
"CloudWatchTransport: install `@aws-sdk/client-cloudwatch-logs` to use this transport without providing a client \u2014 `npm install @aws-sdk/client-cloudwatch-logs`"
|
|
1287
|
+
);
|
|
1288
|
+
}
|
|
1289
|
+
const sdkClient = new ClientCtor({ region: this.region });
|
|
1290
|
+
this.client = {
|
|
1291
|
+
putLogEvents: (logGroupName, logStreamName, events) => sdkClient.send(
|
|
1292
|
+
new PutLogEventsCommandCtor({
|
|
1293
|
+
logGroupName,
|
|
1294
|
+
logStreamName,
|
|
1295
|
+
logEvents: events
|
|
1296
|
+
})
|
|
1297
|
+
)
|
|
1298
|
+
};
|
|
1299
|
+
return this.client;
|
|
1300
|
+
}
|
|
1301
|
+
async sendBatch(batch) {
|
|
1302
|
+
const client = this.resolvedClient() ?? await this.importClient();
|
|
1303
|
+
const events = batch.map((record) => ({
|
|
1304
|
+
timestamp: Date.parse(record.timestamp),
|
|
1305
|
+
message: this.format(record)
|
|
1306
|
+
})).sort((a, b) => a.timestamp - b.timestamp);
|
|
1307
|
+
await client.putLogEvents(this.logGroupName, this.logStreamName, events);
|
|
1308
|
+
}
|
|
1309
|
+
};
|
|
1310
|
+
|
|
1311
|
+
// src/transports/cloud/cloud-logging-transport.ts
|
|
1312
|
+
function gcpSeverity(levelValue) {
|
|
1313
|
+
const level = parseLevel(levelValue);
|
|
1314
|
+
switch (level) {
|
|
1315
|
+
case 5 /* TRACE */:
|
|
1316
|
+
case 10 /* DEBUG */:
|
|
1317
|
+
return "DEBUG";
|
|
1318
|
+
case 20 /* INFO */:
|
|
1319
|
+
return "INFO";
|
|
1320
|
+
case 30 /* WARN */:
|
|
1321
|
+
return "WARNING";
|
|
1322
|
+
case 40 /* ERROR */:
|
|
1323
|
+
return "ERROR";
|
|
1324
|
+
case 50 /* FATAL */:
|
|
1325
|
+
return "CRITICAL";
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
var CloudLoggingTransport = class extends BatchingTransport {
|
|
1329
|
+
logName;
|
|
1330
|
+
projectId;
|
|
1331
|
+
injectedClient;
|
|
1332
|
+
client;
|
|
1333
|
+
constructor(options = {}) {
|
|
1334
|
+
super(options);
|
|
1335
|
+
this.logName = options.logName ?? "logquill";
|
|
1336
|
+
this.projectId = options.projectId;
|
|
1337
|
+
this.injectedClient = options.client;
|
|
1338
|
+
}
|
|
1339
|
+
/** Synchronously available client, if one was injected or already imported — avoids an unnecessary microtask hop on the hot path. */
|
|
1340
|
+
resolvedClient() {
|
|
1341
|
+
return this.injectedClient ?? this.client;
|
|
1342
|
+
}
|
|
1343
|
+
async importClient() {
|
|
1344
|
+
let LoggingCtor;
|
|
1345
|
+
try {
|
|
1346
|
+
const moduleName = "@google-cloud/logging";
|
|
1347
|
+
const mod = await import(moduleName);
|
|
1348
|
+
LoggingCtor = mod.Logging;
|
|
1349
|
+
} catch {
|
|
1350
|
+
throw new Error(
|
|
1351
|
+
"CloudLoggingTransport: install `@google-cloud/logging` to use this transport without providing a client \u2014 `npm install @google-cloud/logging`"
|
|
1352
|
+
);
|
|
1353
|
+
}
|
|
1354
|
+
const logging = new LoggingCtor({ projectId: this.projectId });
|
|
1355
|
+
const log = logging.log(this.logName);
|
|
1356
|
+
this.client = {
|
|
1357
|
+
writeLogEntries: (entries) => log.write(entries)
|
|
1358
|
+
};
|
|
1359
|
+
return this.client;
|
|
1360
|
+
}
|
|
1361
|
+
async sendBatch(batch) {
|
|
1362
|
+
const client = this.resolvedClient() ?? await this.importClient();
|
|
1363
|
+
const entries = batch.map((record) => ({
|
|
1364
|
+
severity: gcpSeverity(record.level),
|
|
1365
|
+
timestamp: record.timestamp,
|
|
1366
|
+
jsonPayload: JSON.parse(this.format(record))
|
|
1367
|
+
}));
|
|
1368
|
+
await client.writeLogEntries(entries);
|
|
1369
|
+
}
|
|
1370
|
+
};
|
|
1371
|
+
|
|
1372
|
+
// src/transports/cloud/app-insights-transport.ts
|
|
1373
|
+
function appInsightsSeverity(levelValue) {
|
|
1374
|
+
const level = parseLevel(levelValue);
|
|
1375
|
+
switch (level) {
|
|
1376
|
+
case 5 /* TRACE */:
|
|
1377
|
+
case 10 /* DEBUG */:
|
|
1378
|
+
return 0;
|
|
1379
|
+
// Verbose
|
|
1380
|
+
case 20 /* INFO */:
|
|
1381
|
+
return 1;
|
|
1382
|
+
// Information
|
|
1383
|
+
case 30 /* WARN */:
|
|
1384
|
+
return 2;
|
|
1385
|
+
// Warning
|
|
1386
|
+
case 40 /* ERROR */:
|
|
1387
|
+
return 3;
|
|
1388
|
+
// Error
|
|
1389
|
+
case 50 /* FATAL */:
|
|
1390
|
+
return 4;
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
var AppInsightsTransport = class extends BatchingTransport {
|
|
1394
|
+
connectionString;
|
|
1395
|
+
injectedClient;
|
|
1396
|
+
client;
|
|
1397
|
+
constructor(options = {}) {
|
|
1398
|
+
super(options);
|
|
1399
|
+
this.connectionString = options.connectionString;
|
|
1400
|
+
this.injectedClient = options.client;
|
|
1401
|
+
}
|
|
1402
|
+
/** Synchronously available client, if one was injected or already imported — avoids an unnecessary microtask hop on the hot path. */
|
|
1403
|
+
resolvedClient() {
|
|
1404
|
+
return this.injectedClient ?? this.client;
|
|
1405
|
+
}
|
|
1406
|
+
async importClient() {
|
|
1407
|
+
let TelemetryClientCtor;
|
|
1408
|
+
try {
|
|
1409
|
+
const moduleName = "applicationinsights";
|
|
1410
|
+
const mod = await import(moduleName);
|
|
1411
|
+
TelemetryClientCtor = mod.TelemetryClient;
|
|
1412
|
+
} catch {
|
|
1413
|
+
throw new Error(
|
|
1414
|
+
"AppInsightsTransport: install `applicationinsights` to use this transport without providing a client \u2014 `npm install applicationinsights`"
|
|
1415
|
+
);
|
|
1416
|
+
}
|
|
1417
|
+
const telemetryClient = new TelemetryClientCtor(this.connectionString);
|
|
1418
|
+
this.client = {
|
|
1419
|
+
trackTraceBatch: (traces) => {
|
|
1420
|
+
for (const trace of traces) {
|
|
1421
|
+
telemetryClient.trackTrace(trace);
|
|
1422
|
+
}
|
|
1423
|
+
telemetryClient.flush();
|
|
1424
|
+
return Promise.resolve();
|
|
1425
|
+
}
|
|
1426
|
+
};
|
|
1427
|
+
return this.client;
|
|
1428
|
+
}
|
|
1429
|
+
async sendBatch(batch) {
|
|
1430
|
+
const client = this.resolvedClient() ?? await this.importClient();
|
|
1431
|
+
const traces = batch.map((record) => ({
|
|
1432
|
+
message: this.format(record),
|
|
1433
|
+
severity: appInsightsSeverity(record.level)
|
|
1434
|
+
}));
|
|
1435
|
+
await client.trackTraceBatch(traces);
|
|
1436
|
+
}
|
|
1437
|
+
};
|
|
1438
|
+
|
|
1439
|
+
// src/transports/cloud/datadog-transport.ts
|
|
1440
|
+
async function fetchDatadogSender(url, apiKey, batch) {
|
|
1441
|
+
const response = await fetch(url, {
|
|
1442
|
+
method: "POST",
|
|
1443
|
+
headers: { "Content-Type": "application/json", "DD-API-KEY": apiKey },
|
|
1444
|
+
body: `[${batch.join(",")}]`
|
|
1445
|
+
});
|
|
1446
|
+
if (!response.ok) {
|
|
1447
|
+
throw new Error(
|
|
1448
|
+
`DatadogTransport: request to ${url} failed with status ${String(response.status)} \u2014 check the API key and site region`
|
|
1449
|
+
);
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
var DatadogTransport = class extends BatchingTransport {
|
|
1453
|
+
url;
|
|
1454
|
+
apiKey;
|
|
1455
|
+
site;
|
|
1456
|
+
sender;
|
|
1457
|
+
constructor(options) {
|
|
1458
|
+
super(options);
|
|
1459
|
+
this.apiKey = options.apiKey;
|
|
1460
|
+
this.site = options.site ?? "datadoghq.com";
|
|
1461
|
+
this.url = `https://http-intake.logs.${this.site}/api/v2/logs`;
|
|
1462
|
+
this.sender = options.sender ?? fetchDatadogSender;
|
|
1463
|
+
}
|
|
1464
|
+
sendBatch(batch) {
|
|
1465
|
+
const formatted = batch.map((record) => this.format(record));
|
|
1466
|
+
return this.sender(this.url, this.apiKey, formatted);
|
|
1467
|
+
}
|
|
1468
|
+
};
|
|
1469
|
+
|
|
1470
|
+
// src/transports/cloud/elasticsearch-transport.ts
|
|
1471
|
+
async function fetchElasticsearchSender(url, headers, body) {
|
|
1472
|
+
const response = await fetch(url, {
|
|
1473
|
+
method: "POST",
|
|
1474
|
+
headers: { ...headers, "Content-Type": "application/x-ndjson" },
|
|
1475
|
+
body
|
|
1476
|
+
});
|
|
1477
|
+
if (!response.ok) {
|
|
1478
|
+
throw new Error(`ElasticsearchTransport: request to ${url} failed with status ${String(response.status)}`);
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
var ElasticsearchTransport = class extends BatchingTransport {
|
|
1482
|
+
url;
|
|
1483
|
+
index;
|
|
1484
|
+
apiKey;
|
|
1485
|
+
sender;
|
|
1486
|
+
constructor(options) {
|
|
1487
|
+
super(options);
|
|
1488
|
+
this.index = options.index ?? "logs";
|
|
1489
|
+
this.url = `${options.node.replace(/\/+$/, "")}/_bulk`;
|
|
1490
|
+
this.apiKey = options.apiKey;
|
|
1491
|
+
this.sender = options.sender ?? fetchElasticsearchSender;
|
|
1492
|
+
}
|
|
1493
|
+
sendBatch(batch) {
|
|
1494
|
+
const lines = [];
|
|
1495
|
+
for (const record of batch) {
|
|
1496
|
+
lines.push(JSON.stringify({ index: { _index: this.index } }));
|
|
1497
|
+
lines.push(this.format(record));
|
|
1498
|
+
}
|
|
1499
|
+
const body = `${lines.join("\n")}
|
|
1500
|
+
`;
|
|
1501
|
+
const headers = {};
|
|
1502
|
+
if (this.apiKey !== void 0) {
|
|
1503
|
+
headers.Authorization = `ApiKey ${this.apiKey}`;
|
|
1504
|
+
}
|
|
1505
|
+
return this.sender(this.url, headers, body);
|
|
1506
|
+
}
|
|
1507
|
+
};
|
|
1508
|
+
async function fetchNewRelicSender(url, headers, body) {
|
|
1509
|
+
const response = await fetch(url, { method: "POST", headers, body });
|
|
1510
|
+
return {
|
|
1511
|
+
ok: response.ok,
|
|
1512
|
+
status: response.status,
|
|
1513
|
+
retryAfter: response.headers.get("retry-after")
|
|
1514
|
+
};
|
|
1515
|
+
}
|
|
1516
|
+
function withoutEventType(record) {
|
|
1517
|
+
const meta = { ...record.meta };
|
|
1518
|
+
delete meta.eventType;
|
|
1519
|
+
return { ...record, meta };
|
|
1520
|
+
}
|
|
1521
|
+
function resumeTimestamp(retryAfter, now) {
|
|
1522
|
+
if (retryAfter === null) {
|
|
1523
|
+
return now + 6e4;
|
|
1524
|
+
}
|
|
1525
|
+
const seconds = Number(retryAfter);
|
|
1526
|
+
if (Number.isFinite(seconds)) {
|
|
1527
|
+
return now + seconds * 1e3;
|
|
1528
|
+
}
|
|
1529
|
+
const dateMs = Date.parse(retryAfter);
|
|
1530
|
+
return Number.isNaN(dateMs) ? now + 6e4 : dateMs;
|
|
1531
|
+
}
|
|
1532
|
+
var NewRelicTransport = class extends BatchingTransport {
|
|
1533
|
+
url;
|
|
1534
|
+
region;
|
|
1535
|
+
licenseKey;
|
|
1536
|
+
sender;
|
|
1537
|
+
clock;
|
|
1538
|
+
pausedUntil = null;
|
|
1539
|
+
constructor(options) {
|
|
1540
|
+
super(options);
|
|
1541
|
+
this.licenseKey = options.licenseKey;
|
|
1542
|
+
this.region = options.region ?? "US";
|
|
1543
|
+
this.url = this.region === "EU" ? "https://log-api.eu.newrelic.com/log/v1" : "https://log-api.newrelic.com/log/v1";
|
|
1544
|
+
this.sender = options.sender ?? fetchNewRelicSender;
|
|
1545
|
+
this.clock = options.clock ?? Date.now;
|
|
1546
|
+
}
|
|
1547
|
+
async sendBatch(batch) {
|
|
1548
|
+
const now = this.clock();
|
|
1549
|
+
if (this.pausedUntil !== null && now < this.pausedUntil) {
|
|
1550
|
+
console.error(
|
|
1551
|
+
`NewRelicTransport: sends paused until ${new Date(this.pausedUntil).toISOString()} after a 429 rate-limit response \u2014 skipping this batch rather than making a doomed request`
|
|
1552
|
+
);
|
|
1553
|
+
return;
|
|
1554
|
+
}
|
|
1555
|
+
this.pausedUntil = null;
|
|
1556
|
+
const records = batch.map((record) => withoutEventType(record));
|
|
1557
|
+
const body = zlib.gzipSync(Buffer.from(JSON.stringify(records)));
|
|
1558
|
+
const headers = {
|
|
1559
|
+
"Content-Type": "application/json",
|
|
1560
|
+
"Content-Encoding": "gzip",
|
|
1561
|
+
"Api-Key": this.licenseKey
|
|
1562
|
+
};
|
|
1563
|
+
const result = await this.sender(this.url, headers, body);
|
|
1564
|
+
if (result.status === 429) {
|
|
1565
|
+
this.pausedUntil = resumeTimestamp(result.retryAfter, now);
|
|
1566
|
+
console.error(
|
|
1567
|
+
`NewRelicTransport: received 429 from New Relic \u2014 pausing sends until ${new Date(this.pausedUntil).toISOString()}. Reduce log volume or increase batching to stay under the rate limit.`
|
|
1568
|
+
);
|
|
1569
|
+
return;
|
|
1570
|
+
}
|
|
1571
|
+
if (!result.ok) {
|
|
1572
|
+
throw new Error(
|
|
1573
|
+
`NewRelicTransport: request to ${this.url} failed with status ${String(result.status)} \u2014 check the license key and region`
|
|
1574
|
+
);
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
};
|
|
1578
|
+
|
|
1579
|
+
// src/transports/console-transport.ts
|
|
134
1580
|
var COLORS = {
|
|
135
1581
|
[5 /* TRACE */]: "\x1B[90m",
|
|
136
1582
|
// gray
|
|
@@ -218,7 +1664,7 @@ var FileTransport = class extends Transport {
|
|
|
218
1664
|
}
|
|
219
1665
|
};
|
|
220
1666
|
|
|
221
|
-
// src/http-transport.ts
|
|
1667
|
+
// src/transports/http-transport.ts
|
|
222
1668
|
async function fetchSender(url, batch) {
|
|
223
1669
|
const response = await fetch(url, {
|
|
224
1670
|
method: "POST",
|
|
@@ -265,7 +1711,7 @@ var HTTPTransport = class extends Transport {
|
|
|
265
1711
|
}
|
|
266
1712
|
};
|
|
267
1713
|
|
|
268
|
-
// src/logger.ts
|
|
1714
|
+
// src/core/logger.ts
|
|
269
1715
|
var Logger = class _Logger {
|
|
270
1716
|
name;
|
|
271
1717
|
transports;
|
|
@@ -276,7 +1722,10 @@ var Logger = class _Logger {
|
|
|
276
1722
|
this.name = name;
|
|
277
1723
|
this.currentLevel = parseLevel(options.level ?? 20 /* INFO */);
|
|
278
1724
|
this.transports = options.transports ? [...options.transports] : [];
|
|
279
|
-
this.plugins =
|
|
1725
|
+
this.plugins = [];
|
|
1726
|
+
for (const plugin of options.plugins ?? []) {
|
|
1727
|
+
this.use(plugin);
|
|
1728
|
+
}
|
|
280
1729
|
this.baseMeta = options.meta ? { ...options.meta } : {};
|
|
281
1730
|
}
|
|
282
1731
|
get level() {
|
|
@@ -285,9 +1734,14 @@ var Logger = class _Logger {
|
|
|
285
1734
|
setLevel(level) {
|
|
286
1735
|
this.currentLevel = parseLevel(level);
|
|
287
1736
|
}
|
|
288
|
-
/**
|
|
1737
|
+
/**
|
|
1738
|
+
* Register a plugin, or a plain `beforeLog`-style function. A function is
|
|
1739
|
+
* wrapped internally as an anonymous `Plugin` (`FunctionPlugin`) — the
|
|
1740
|
+
* same middleware ergonomics as Express/Koa, without needing to read the
|
|
1741
|
+
* `Plugin` interface first. Returns `this` so calls can be chained.
|
|
1742
|
+
*/
|
|
289
1743
|
use(plugin) {
|
|
290
|
-
this.plugins.push(plugin);
|
|
1744
|
+
this.plugins.push(typeof plugin === "function" ? new FunctionPlugin(plugin) : plugin);
|
|
291
1745
|
return this;
|
|
292
1746
|
}
|
|
293
1747
|
/** Close every attached transport. Call on shutdown to flush buffered writes. */
|
|
@@ -367,19 +1821,47 @@ var Logger = class _Logger {
|
|
|
367
1821
|
};
|
|
368
1822
|
|
|
369
1823
|
// src/index.ts
|
|
370
|
-
var VERSION = "0.
|
|
1824
|
+
var VERSION = "0.3.0";
|
|
371
1825
|
|
|
1826
|
+
exports.AlertingPlugin = AlertingPlugin;
|
|
1827
|
+
exports.AppInsightsTransport = AppInsightsTransport;
|
|
1828
|
+
exports.BaseQueueTransport = BaseQueueTransport;
|
|
1829
|
+
exports.BaseSQLTransport = BaseSQLTransport;
|
|
1830
|
+
exports.BatchingTransport = BatchingTransport;
|
|
1831
|
+
exports.CloudLoggingTransport = CloudLoggingTransport;
|
|
1832
|
+
exports.CloudWatchTransport = CloudWatchTransport;
|
|
372
1833
|
exports.CollectingTransport = CollectingTransport;
|
|
373
1834
|
exports.ConsoleTransport = ConsoleTransport;
|
|
374
1835
|
exports.ContextPlugin = ContextPlugin;
|
|
1836
|
+
exports.DEFAULT_PII_PATTERNS = DEFAULT_PII_PATTERNS;
|
|
375
1837
|
exports.DEFAULT_REDACTED_KEYS = DEFAULT_REDACTED_KEYS;
|
|
1838
|
+
exports.DatadogTransport = DatadogTransport;
|
|
1839
|
+
exports.DynamoDBTransport = DynamoDBTransport;
|
|
1840
|
+
exports.ElasticsearchTransport = ElasticsearchTransport;
|
|
1841
|
+
exports.EmailAlertPlugin = EmailAlertPlugin;
|
|
376
1842
|
exports.FileTransport = FileTransport;
|
|
1843
|
+
exports.FunctionPlugin = FunctionPlugin;
|
|
1844
|
+
exports.GENESIS_HASH = GENESIS_HASH;
|
|
377
1845
|
exports.HTTPTransport = HTTPTransport;
|
|
378
1846
|
exports.JSONFormatter = JSONFormatter;
|
|
1847
|
+
exports.KafkaTransport = KafkaTransport;
|
|
379
1848
|
exports.Level = Level;
|
|
380
1849
|
exports.Logger = Logger;
|
|
1850
|
+
exports.MongoDBTransport = MongoDBTransport;
|
|
1851
|
+
exports.MySQLTransport = MySQLTransport;
|
|
1852
|
+
exports.NewRelicTransport = NewRelicTransport;
|
|
1853
|
+
exports.PIIRedactPlugin = PIIRedactPlugin;
|
|
1854
|
+
exports.PagerDutyAlertPlugin = PagerDutyAlertPlugin;
|
|
1855
|
+
exports.PostgresTransport = PostgresTransport;
|
|
1856
|
+
exports.PubSubTransport = PubSubTransport;
|
|
1857
|
+
exports.RabbitMQTransport = RabbitMQTransport;
|
|
381
1858
|
exports.RedactPlugin = RedactPlugin;
|
|
1859
|
+
exports.RedisTransport = RedisTransport;
|
|
1860
|
+
exports.SQLiteTransport = SQLiteTransport;
|
|
1861
|
+
exports.SQSTransport = SQSTransport;
|
|
382
1862
|
exports.SamplingPlugin = SamplingPlugin;
|
|
1863
|
+
exports.SlackAlertPlugin = SlackAlertPlugin;
|
|
1864
|
+
exports.TamperEvidentPlugin = TamperEvidentPlugin;
|
|
383
1865
|
exports.Transport = Transport;
|
|
384
1866
|
exports.VERSION = VERSION;
|
|
385
1867
|
exports.createRecord = createRecord;
|