@remit/logger-lambda 0.0.11 → 0.0.12
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 +1 -1
- package/package.json +1 -1
- package/src/metrics.test.ts +77 -0
- package/src/metrics.ts +36 -5
package/README.md
CHANGED
|
@@ -92,5 +92,5 @@ one adds to the bindings of the one it runs inside.
|
|
|
92
92
|
| -------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------ |
|
|
93
93
|
| `LOG_LEVEL` | `info` | `trace`, `debug`, `info`, `warn`, `error`, `fatal` or `silent`. An unrecognised value logs one warning and falls back to the default. |
|
|
94
94
|
| `REMIT_SERVICE_NAME` | `remit` | The `service` field on every line. Stamped into each service bundle at build time by `npm-scripts/docker-bundle.mjs`. |
|
|
95
|
-
| `METRICS_PORT` | `9464` | Port `startMetricsServer` binds. |
|
|
95
|
+
| `METRICS_PORT` | `9464` | Port `startMetricsServer` binds. Empty is unset and takes the default; anything that is not a port number in 0–65535 logs one error and serves no metrics, rather than falling back. A service that cannot be scraped is a smaller failure than one that reports on the wrong port. |
|
|
96
96
|
| `METRICS_HOST` | `0.0.0.0` | Interface `startMetricsServer` binds. Set `127.0.0.1` when the service runs as a host process rather than a container. |
|
package/package.json
CHANGED
package/src/metrics.test.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
|
+
import type { Server } from "node:http";
|
|
2
3
|
import type { AddressInfo } from "node:net";
|
|
3
4
|
import { afterEach, beforeEach, describe, it } from "node:test";
|
|
4
5
|
import {
|
|
@@ -402,4 +403,80 @@ describe("the /metrics endpoint", () => {
|
|
|
402
403
|
delete process.env.METRICS_PORT;
|
|
403
404
|
assert.ok((server.address() as AddressInfo).port > 0);
|
|
404
405
|
});
|
|
406
|
+
|
|
407
|
+
const startWithMetricsPort = (value: string, reported: Error[]) => {
|
|
408
|
+
process.env.METRICS_PORT = value;
|
|
409
|
+
try {
|
|
410
|
+
return startMetricsServer({
|
|
411
|
+
host: "127.0.0.1",
|
|
412
|
+
onError: (error) => reported.push(error),
|
|
413
|
+
});
|
|
414
|
+
} finally {
|
|
415
|
+
delete process.env.METRICS_PORT;
|
|
416
|
+
}
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
for (const value of ["nine-thousand", "99999", "-1", "9464.5"]) {
|
|
420
|
+
it(`refuses to bind METRICS_PORT=${value} instead of taking the worker down`, () => {
|
|
421
|
+
const reported: Error[] = [];
|
|
422
|
+
const server = startWithMetricsPort(value, reported);
|
|
423
|
+
assert.equal(server.listening, false);
|
|
424
|
+
assert.equal(reported.length, 1);
|
|
425
|
+
assert.match(reported[0].message, /METRICS_PORT/);
|
|
426
|
+
assert.ok(reported[0].message.includes(value));
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
it("treats a blank METRICS_PORT as unconfigured", async () => {
|
|
431
|
+
const reported: Error[] = [];
|
|
432
|
+
const server = startWithMetricsPort(" ", reported);
|
|
433
|
+
servers.push(server);
|
|
434
|
+
const bound = await new Promise<boolean>((resolve) => {
|
|
435
|
+
server.once("listening", () => resolve(true));
|
|
436
|
+
server.once("error", () => resolve(false));
|
|
437
|
+
});
|
|
438
|
+
if (!bound) {
|
|
439
|
+
// 9464 is already taken on this host, which is itself proof the
|
|
440
|
+
// fallback resolved to it: an ephemeral port is never in use.
|
|
441
|
+
assert.match(String(reported[0]), /EADDRINUSE/);
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
assert.deepEqual(reported, []);
|
|
445
|
+
assert.equal((server.address() as AddressInfo).port, DEFAULT_METRICS_PORT);
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
it("refuses an out-of-range port passed by a caller", () => {
|
|
449
|
+
const reported: Error[] = [];
|
|
450
|
+
const server = startMetricsServer({
|
|
451
|
+
port: 70000,
|
|
452
|
+
host: "127.0.0.1",
|
|
453
|
+
onError: (error) => reported.push(error),
|
|
454
|
+
});
|
|
455
|
+
assert.equal(server.listening, false);
|
|
456
|
+
assert.equal(reported.length, 1);
|
|
457
|
+
assert.match(reported[0].message, /70000/);
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
it("reports a malformed port as one JSON line on stderr by default", () => {
|
|
461
|
+
const written: string[] = [];
|
|
462
|
+
const restore = process.stderr.write.bind(process.stderr);
|
|
463
|
+
process.env.METRICS_PORT = "not-a-port";
|
|
464
|
+
process.stderr.write = ((chunk: string) => {
|
|
465
|
+
written.push(String(chunk));
|
|
466
|
+
return true;
|
|
467
|
+
}) as typeof process.stderr.write;
|
|
468
|
+
let server: Server;
|
|
469
|
+
try {
|
|
470
|
+
server = startMetricsServer({ host: "127.0.0.1" });
|
|
471
|
+
} finally {
|
|
472
|
+
process.stderr.write = restore;
|
|
473
|
+
delete process.env.METRICS_PORT;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
assert.equal(server.listening, false);
|
|
477
|
+
assert.equal(written.length, 1);
|
|
478
|
+
const line = JSON.parse(written[0]) as { level: string; error: string };
|
|
479
|
+
assert.equal(line.level, "error");
|
|
480
|
+
assert.match(line.error, /not-a-port/);
|
|
481
|
+
});
|
|
405
482
|
});
|
package/src/metrics.ts
CHANGED
|
@@ -230,6 +230,31 @@ const reportToStderr = (error: Error): void => {
|
|
|
230
230
|
);
|
|
231
231
|
};
|
|
232
232
|
|
|
233
|
+
const MAX_PORT = 65535;
|
|
234
|
+
|
|
235
|
+
const isBindablePort = (port: number): boolean =>
|
|
236
|
+
Number.isInteger(port) && port >= 0 && port <= MAX_PORT;
|
|
237
|
+
|
|
238
|
+
// `listen` rejects anything outside 0..65535 by throwing ERR_SOCKET_BAD_PORT
|
|
239
|
+
// synchronously, which the `'error'` handler never sees. A blank value is a
|
|
240
|
+
// variable someone left empty rather than a request for port 0, so it reads as
|
|
241
|
+
// unset; anything else that does not parse is a typo worth naming.
|
|
242
|
+
const configuredPort = (override: number | undefined): number | Error => {
|
|
243
|
+
if (override !== undefined) {
|
|
244
|
+
return isBindablePort(override)
|
|
245
|
+
? override
|
|
246
|
+
: new Error(`not a port number: ${override}`);
|
|
247
|
+
}
|
|
248
|
+
const configured = (process.env.METRICS_PORT ?? "").trim();
|
|
249
|
+
if (configured === "") {
|
|
250
|
+
return DEFAULT_METRICS_PORT;
|
|
251
|
+
}
|
|
252
|
+
const parsed = Number(configured);
|
|
253
|
+
return isBindablePort(parsed)
|
|
254
|
+
? parsed
|
|
255
|
+
: new Error(`METRICS_PORT is not a port number: ${configured}`);
|
|
256
|
+
};
|
|
257
|
+
|
|
233
258
|
/**
|
|
234
259
|
* The listener D2 adds to each worker image, for `/metrics` alone. Bound to the
|
|
235
260
|
* compose network and never published to the host: the only host ports in the
|
|
@@ -239,8 +264,10 @@ const reportToStderr = (error: Error): void => {
|
|
|
239
264
|
* later tick, by which time the poll loop is already running, so an unhandled
|
|
240
265
|
* `'error'` event here would kill a worker in the middle of syncing mail —
|
|
241
266
|
* 9464 is the OpenTelemetry Prometheus exporter's default, so a collector on
|
|
242
|
-
* the same host is a real trigger.
|
|
243
|
-
*
|
|
267
|
+
* the same host is a real trigger. A port that is not a port at all is refused
|
|
268
|
+
* the same way, before `listen` can throw it at the caller. An observability
|
|
269
|
+
* endpoint must never be able to stop mail from arriving; absent metrics are
|
|
270
|
+
* the correct failure.
|
|
244
271
|
*
|
|
245
272
|
* Unreferenced from the event loop, so it never keeps a worker alive past the
|
|
246
273
|
* end of its poll loop.
|
|
@@ -248,14 +275,18 @@ const reportToStderr = (error: Error): void => {
|
|
|
248
275
|
export const startMetricsServer = (
|
|
249
276
|
options: MetricsServerOptions = {},
|
|
250
277
|
): Server => {
|
|
251
|
-
const port =
|
|
252
|
-
options.port ??
|
|
253
|
-
Number(process.env.METRICS_PORT ?? String(DEFAULT_METRICS_PORT));
|
|
254
278
|
const host = options.host ?? process.env.METRICS_HOST ?? "0.0.0.0";
|
|
255
279
|
const onError = options.onError ?? reportToStderr;
|
|
256
280
|
const server = createServer(createMetricsRequestListener());
|
|
257
281
|
server.unref();
|
|
258
282
|
server.on("error", onError);
|
|
283
|
+
|
|
284
|
+
const port = configuredPort(options.port);
|
|
285
|
+
if (port instanceof Error) {
|
|
286
|
+
onError(port);
|
|
287
|
+
return server;
|
|
288
|
+
}
|
|
289
|
+
|
|
259
290
|
server.listen(port, host);
|
|
260
291
|
return server;
|
|
261
292
|
};
|