@remit/logger-lambda 0.0.12 → 0.0.14
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/package.json +1 -1
- package/src/log-output.test.ts +122 -14
- package/src/logger.ts +30 -24
- package/src/metrics.test.ts +2 -2
package/package.json
CHANGED
package/src/log-output.test.ts
CHANGED
|
@@ -28,7 +28,9 @@ process.stdout.write = ((
|
|
|
28
28
|
return (originalWrite as (...args: unknown[]) => boolean)(chunk, ...rest);
|
|
29
29
|
}) as typeof process.stdout.write;
|
|
30
30
|
|
|
31
|
-
const { createLogger, logger, withLogContext } = await import(
|
|
31
|
+
const { createLogger, logger, withLogContext, withTelemetry } = await import(
|
|
32
|
+
"./logger.js"
|
|
33
|
+
);
|
|
32
34
|
|
|
33
35
|
const parse = (): Line[] =>
|
|
34
36
|
written
|
|
@@ -65,6 +67,23 @@ const one = (emit: () => void): Line => {
|
|
|
65
67
|
return lines[0];
|
|
66
68
|
};
|
|
67
69
|
|
|
70
|
+
// Both scopes have to be open at the same moment for a leak to be observable;
|
|
71
|
+
// a gate each side waits on pins that interleaving.
|
|
72
|
+
const gate = (): { reached: Promise<void>; pass: () => void } => {
|
|
73
|
+
let pass!: () => void;
|
|
74
|
+
const reached = new Promise<void>((resolve) => {
|
|
75
|
+
pass = () => resolve();
|
|
76
|
+
});
|
|
77
|
+
return { reached, pass };
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// Sorted by message, so the comparison covers every line exactly once without
|
|
81
|
+
// naming the order the scheduler happened to finish them in.
|
|
82
|
+
const requestIdPerMessage = (lines: Line[]): [string, unknown][] =>
|
|
83
|
+
lines
|
|
84
|
+
.map((line): [string, unknown] => [String(line.msg), line.requestId])
|
|
85
|
+
.sort(([left], [right]) => left.localeCompare(right));
|
|
86
|
+
|
|
68
87
|
describe("log output", () => {
|
|
69
88
|
it("writes one JSON object per line on stdout", () => {
|
|
70
89
|
const log = createLogger();
|
|
@@ -309,25 +328,30 @@ describe("withLogContext", () => {
|
|
|
309
328
|
});
|
|
310
329
|
|
|
311
330
|
it("keeps overlapping scopes apart across awaits", async () => {
|
|
312
|
-
const
|
|
313
|
-
|
|
314
|
-
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
315
|
-
logger.warn(`handled ${id}`);
|
|
316
|
-
});
|
|
331
|
+
const slowStarted = gate();
|
|
332
|
+
const fastLogged = gate();
|
|
317
333
|
|
|
318
334
|
// The slow request opens its scope first and logs last, which is exactly
|
|
319
335
|
// the interleaving a shared mutable binding gets wrong.
|
|
320
336
|
const lines = await captureAsync(async () => {
|
|
321
|
-
await Promise.all([
|
|
337
|
+
await Promise.all([
|
|
338
|
+
withLogContext({ requestId: "slow" }, async () => {
|
|
339
|
+
slowStarted.pass();
|
|
340
|
+
await fastLogged.reached;
|
|
341
|
+
logger.warn("handled slow");
|
|
342
|
+
}),
|
|
343
|
+
withLogContext({ requestId: "fast" }, async () => {
|
|
344
|
+
await slowStarted.reached;
|
|
345
|
+
logger.warn("handled fast");
|
|
346
|
+
fastLogged.pass();
|
|
347
|
+
}),
|
|
348
|
+
]);
|
|
322
349
|
});
|
|
323
350
|
|
|
324
|
-
assert.deepEqual(
|
|
325
|
-
|
|
326
|
-
[
|
|
327
|
-
|
|
328
|
-
["handled slow", "slow"],
|
|
329
|
-
],
|
|
330
|
-
);
|
|
351
|
+
assert.deepEqual(requestIdPerMessage(lines), [
|
|
352
|
+
["handled fast", "fast"],
|
|
353
|
+
["handled slow", "slow"],
|
|
354
|
+
]);
|
|
331
355
|
});
|
|
332
356
|
|
|
333
357
|
it("nests, adding to the scope it runs inside", () => {
|
|
@@ -353,3 +377,87 @@ describe("withLogContext", () => {
|
|
|
353
377
|
assert.equal(repeats.length, 1);
|
|
354
378
|
});
|
|
355
379
|
});
|
|
380
|
+
|
|
381
|
+
describe("withTelemetry", () => {
|
|
382
|
+
const context = {
|
|
383
|
+
awsRequestId: "req-1",
|
|
384
|
+
functionName: "test-function",
|
|
385
|
+
} as unknown as Parameters<Parameters<typeof withTelemetry>[0]>[1];
|
|
386
|
+
|
|
387
|
+
it("puts its own start line inside the scope", async () => {
|
|
388
|
+
const lines = await captureAsync(async () => {
|
|
389
|
+
await withTelemetry(async () => "ok")({}, context);
|
|
390
|
+
});
|
|
391
|
+
const started = lines.find(
|
|
392
|
+
(line) => line.msg === "Lambda invocation started",
|
|
393
|
+
);
|
|
394
|
+
assert.ok(started, "expected the start line");
|
|
395
|
+
assert.equal(started.requestId, "req-1");
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
// The line an operator correlates from is written after the handler has
|
|
399
|
+
// unwound, which is exactly what a scope opened inside the handler misses.
|
|
400
|
+
it("puts its own failure line inside the scope", async () => {
|
|
401
|
+
const lines = await captureAsync(async () => {
|
|
402
|
+
await assert.rejects(
|
|
403
|
+
withTelemetry(async () => {
|
|
404
|
+
throw new Error("boom");
|
|
405
|
+
})({}, context),
|
|
406
|
+
);
|
|
407
|
+
});
|
|
408
|
+
const failed = lines.find(
|
|
409
|
+
(line) => line.msg === "Lambda invocation failed",
|
|
410
|
+
);
|
|
411
|
+
assert.ok(failed, "expected the failure line");
|
|
412
|
+
assert.equal(failed.requestId, "req-1");
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
it("carries the same requestId onto the handler's own lines", async () => {
|
|
416
|
+
const lines = await captureAsync(async () => {
|
|
417
|
+
await withTelemetry(async () => {
|
|
418
|
+
logger.warn("from the handler");
|
|
419
|
+
return "ok";
|
|
420
|
+
})({}, context);
|
|
421
|
+
});
|
|
422
|
+
assert.deepEqual(
|
|
423
|
+
[...new Set(lines.map((line) => line.requestId))],
|
|
424
|
+
["req-1"],
|
|
425
|
+
);
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
it("keeps concurrent invocations apart", async () => {
|
|
429
|
+
const slowStarted = gate();
|
|
430
|
+
const fastLogged = gate();
|
|
431
|
+
|
|
432
|
+
const invoke = (id: string, handler: () => Promise<void>) =>
|
|
433
|
+
withTelemetry(handler)({}, {
|
|
434
|
+
awsRequestId: id,
|
|
435
|
+
functionName: "test-function",
|
|
436
|
+
} as unknown as typeof context);
|
|
437
|
+
|
|
438
|
+
const lines = await captureAsync(async () => {
|
|
439
|
+
await Promise.all([
|
|
440
|
+
invoke("slow", async () => {
|
|
441
|
+
slowStarted.pass();
|
|
442
|
+
await fastLogged.reached;
|
|
443
|
+
logger.warn("handled slow");
|
|
444
|
+
}),
|
|
445
|
+
invoke("fast", async () => {
|
|
446
|
+
await slowStarted.reached;
|
|
447
|
+
logger.warn("handled fast");
|
|
448
|
+
fastLogged.pass();
|
|
449
|
+
}),
|
|
450
|
+
]);
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
assert.deepEqual(
|
|
454
|
+
requestIdPerMessage(
|
|
455
|
+
lines.filter((line) => String(line.msg).startsWith("handled ")),
|
|
456
|
+
),
|
|
457
|
+
[
|
|
458
|
+
["handled fast", "fast"],
|
|
459
|
+
["handled slow", "slow"],
|
|
460
|
+
],
|
|
461
|
+
);
|
|
462
|
+
});
|
|
463
|
+
});
|
package/src/logger.ts
CHANGED
|
@@ -159,32 +159,38 @@ export const logger: Logger = createAdapter(root, {});
|
|
|
159
159
|
|
|
160
160
|
export const createLogger = (): Logger => createAdapter(root, {});
|
|
161
161
|
|
|
162
|
+
// The scope opens around the wrapper's own two lines, not just the handler:
|
|
163
|
+
// "Lambda invocation failed" is the line an operator correlates from, and it is
|
|
164
|
+
// written after the handler has already unwound, so a scope opened inside the
|
|
165
|
+
// handler no longer covers it. Everything the invocation writes — the wrapper's
|
|
166
|
+
// lines and the handler's — carries the same `requestId`.
|
|
162
167
|
export const withTelemetry = <TEvent, TResult>(
|
|
163
168
|
handler: (event: TEvent, context: Context) => Promise<TResult>,
|
|
164
169
|
): ((event: TEvent, context: Context) => Promise<TResult>) => {
|
|
165
|
-
return async (event: TEvent, context: Context): Promise<TResult> =>
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
const start = Date.now();
|
|
171
|
-
|
|
172
|
-
try {
|
|
173
|
-
const result = await handler(event, context);
|
|
174
|
-
recordHandlerOutcome({
|
|
175
|
-
handler: context.functionName,
|
|
176
|
-
outcome: "success",
|
|
177
|
-
durationMs: Date.now() - start,
|
|
178
|
-
});
|
|
179
|
-
return result;
|
|
180
|
-
} catch (err) {
|
|
181
|
-
recordHandlerOutcome({
|
|
182
|
-
handler: context.functionName,
|
|
183
|
-
outcome: "failure",
|
|
184
|
-
durationMs: Date.now() - start,
|
|
170
|
+
return async (event: TEvent, context: Context): Promise<TResult> =>
|
|
171
|
+
withLogContext({ requestId: context.awsRequestId }, async () => {
|
|
172
|
+
logger.debug("Lambda invocation started", {
|
|
173
|
+
functionName: context.functionName,
|
|
185
174
|
});
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
175
|
+
|
|
176
|
+
const start = Date.now();
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
const result = await handler(event, context);
|
|
180
|
+
recordHandlerOutcome({
|
|
181
|
+
handler: context.functionName,
|
|
182
|
+
outcome: "success",
|
|
183
|
+
durationMs: Date.now() - start,
|
|
184
|
+
});
|
|
185
|
+
return result;
|
|
186
|
+
} catch (err) {
|
|
187
|
+
recordHandlerOutcome({
|
|
188
|
+
handler: context.functionName,
|
|
189
|
+
outcome: "failure",
|
|
190
|
+
durationMs: Date.now() - start,
|
|
191
|
+
});
|
|
192
|
+
logger.error("Lambda invocation failed", { error: err });
|
|
193
|
+
throw err;
|
|
194
|
+
}
|
|
195
|
+
});
|
|
190
196
|
};
|
package/src/metrics.test.ts
CHANGED
|
@@ -353,7 +353,7 @@ describe("the /metrics endpoint", () => {
|
|
|
353
353
|
onError: (error) => reported.push(error),
|
|
354
354
|
});
|
|
355
355
|
servers.push(server);
|
|
356
|
-
await new Promise((resolve) =>
|
|
356
|
+
await new Promise<void>((resolve) => server.once("error", () => resolve()));
|
|
357
357
|
assert.equal(reported.length, 1);
|
|
358
358
|
assert.match(String(reported[0]), /EADDRINUSE/);
|
|
359
359
|
// The port that was already bound still answers: nothing died.
|
|
@@ -370,7 +370,7 @@ describe("the /metrics endpoint", () => {
|
|
|
370
370
|
}) as typeof process.stderr.write;
|
|
371
371
|
const server = startMetricsServer({ port: held, host: "127.0.0.1" });
|
|
372
372
|
servers.push(server);
|
|
373
|
-
await new Promise((resolve) =>
|
|
373
|
+
await new Promise<void>((resolve) => server.once("error", () => resolve()));
|
|
374
374
|
process.stderr.write = restore;
|
|
375
375
|
|
|
376
376
|
assert.equal(written.length, 1);
|