@steve31415/baselib 3.13.0 → 3.15.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.
@@ -1,7 +1,9 @@
1
1
  export declare function requestStarted(): void;
2
2
  export declare function requestEnded(): void;
3
- /** The fields a diagnostic event carries: requests in flight right now and
4
- * how long ago the last one ended (undefined before the first request). */
3
+ /** The fields an event carries: requests in flight right now and how long
4
+ * ago the last one ended. Before the first request has ended there is no
5
+ * such moment, so `sinceLastRequestMs` is omitted rather than invented;
6
+ * `inFlightRequests` alone marks a server that has not finished one yet. */
5
7
  export declare function activitySnapshot(): {
6
8
  inFlightRequests: number;
7
9
  sinceLastRequestMs?: number;
@@ -10,5 +12,5 @@ export declare function activitySnapshot(): {
10
12
  * request ends when the handler returns its Response; a streaming body
11
13
  * that outlives that is not tracked. */
12
14
  export declare function trackActivity<A extends unknown[]>(fetch: (...args: A) => Response | Promise<Response>): (...args: A) => Promise<Response>;
13
- /** Test hook: forget everything. */
15
+ /** Test hook: forget everything, including the logger registration. */
14
16
  export declare function resetActivity(): void;
package/dist/activity.js CHANGED
@@ -1,5 +1,5 @@
1
- // Request activity of this process, for log events that need to know
2
- // whether Cloud Run had CPU allocated at the time: with request-based
1
+ // Request activity of this process, carried by every server log event so
2
+ // any of them can say whether Cloud Run had CPU allocated at the time: with request-based
3
3
  // billing (the fleet default) an instance's CPU is throttled to almost
4
4
  // nothing between requests, and timers, connection handshakes and
5
5
  // credential refreshes crawl. A connection failure logged with
@@ -7,10 +7,13 @@
7
7
  // throttled tail; one with `inFlightRequests: 1` did not.
8
8
  //
9
9
  // `startServer` tracks every HTTP request automatically. A custom server
10
- // wraps its fetch handler with `trackActivity`. WebSocket sessions are not
10
+ // wraps its fetch handler with `trackActivity`. Wrapping is also what
11
+ // registers this module as the logger's process-meta source (process-meta.ts), so a
12
+ // process that serves nothing logs no activity fields. WebSocket sessions are not
11
13
  // counted (they hold CPU on Cloud Run, but the upgrade bypasses fetch), so
12
14
  // on a WebSocket-heavy app zero in-flight requests does not prove the
13
15
  // instance was idle.
16
+ import { setProcessMetaSource } from './process-meta.js';
14
17
  let inFlight = 0;
15
18
  let lastRequestEndedAt = 0;
16
19
  export function requestStarted() {
@@ -20,8 +23,10 @@ export function requestEnded() {
20
23
  inFlight = Math.max(0, inFlight - 1);
21
24
  lastRequestEndedAt = Date.now();
22
25
  }
23
- /** The fields a diagnostic event carries: requests in flight right now and
24
- * how long ago the last one ended (undefined before the first request). */
26
+ /** The fields an event carries: requests in flight right now and how long
27
+ * ago the last one ended. Before the first request has ended there is no
28
+ * such moment, so `sinceLastRequestMs` is omitted rather than invented;
29
+ * `inFlightRequests` alone marks a server that has not finished one yet. */
25
30
  export function activitySnapshot() {
26
31
  return {
27
32
  inFlightRequests: inFlight,
@@ -32,6 +37,7 @@ export function activitySnapshot() {
32
37
  * request ends when the handler returns its Response; a streaming body
33
38
  * that outlives that is not tracked. */
34
39
  export function trackActivity(fetch) {
40
+ setProcessMetaSource(activitySnapshot);
35
41
  return async (...args) => {
36
42
  requestStarted();
37
43
  try {
@@ -42,8 +48,9 @@ export function trackActivity(fetch) {
42
48
  }
43
49
  };
44
50
  }
45
- /** Test hook: forget everything. */
51
+ /** Test hook: forget everything, including the logger registration. */
46
52
  export function resetActivity() {
53
+ setProcessMetaSource(undefined);
47
54
  inFlight = 0;
48
55
  lastRequestEndedAt = 0;
49
56
  }
package/dist/db.d.ts CHANGED
@@ -100,9 +100,9 @@ export declare const FLEET_POOL_DEFAULTS: {
100
100
  * any transaction or advisory lock it held is gone.
101
101
  * Each event carries `elapsedMs` (time in that phase: the connect
102
102
  * attempt, the wait, the idle spell, the checkout), the pg `code`, the
103
- * pool's counts, `inFlightRequests` + `sinceLastRequestMs` (`activity.ts`:
104
- * was Cloud Run CPU allocated?) and, given a Cloud SQL credential reader,
105
- * `credentialRemainingMs`. Query:
103
+ * pool's counts and, given a Cloud SQL credential reader,
104
+ * `credentialRemainingMs`; the logger adds request activity, as on every
105
+ * server event (`activity.ts`: was Cloud Run CPU allocated?). Query:
106
106
  * `['apps'] | where kind == 'db-connection-loss' | summarize count() by app, tostring(meta.phase)`.
107
107
  *
108
108
  * The checked-out path is the one that used to crash the process: pg-pool
package/dist/db.js CHANGED
@@ -5,8 +5,8 @@
5
5
  import { readdir, readFile } from 'node:fs/promises';
6
6
  import { join } from 'node:path';
7
7
  import pg from 'pg';
8
- import { activitySnapshot } from './activity.js';
9
8
  import { requireEnv } from './config.js';
9
+ import { processMeta } from './process-meta.js';
10
10
  import { serializeError } from './log-core.js';
11
11
  // DATE columns come back as 'YYYY-MM-DD' strings, not JS Dates: calendar
12
12
  // dates are timezone-free by meaning, and a Date object smears them across
@@ -106,9 +106,9 @@ const instrumented = new WeakSet();
106
106
  * any transaction or advisory lock it held is gone.
107
107
  * Each event carries `elapsedMs` (time in that phase: the connect
108
108
  * attempt, the wait, the idle spell, the checkout), the pg `code`, the
109
- * pool's counts, `inFlightRequests` + `sinceLastRequestMs` (`activity.ts`:
110
- * was Cloud Run CPU allocated?) and, given a Cloud SQL credential reader,
111
- * `credentialRemainingMs`. Query:
109
+ * pool's counts and, given a Cloud SQL credential reader,
110
+ * `credentialRemainingMs`; the logger adds request activity, as on every
111
+ * server event (`activity.ts`: was Cloud Run CPU allocated?). Query:
112
112
  * `['apps'] | where kind == 'db-connection-loss' | summarize count() by app, tostring(meta.phase)`.
113
113
  *
114
114
  * The checked-out path is the one that used to crash the process: pg-pool
@@ -134,7 +134,6 @@ export function logPoolErrors(pool, logger, options = {}) {
134
134
  ...(since === undefined ? {} : { elapsedMs: Date.now() - since }),
135
135
  code: err.code,
136
136
  pool: { total: pool.totalCount, idle: pool.idleCount, waiting: pool.waitingCount },
137
- ...activitySnapshot(),
138
137
  ...(options.credentialExpiry
139
138
  ? expiry === undefined
140
139
  ? { credentialUnknown: true }
@@ -144,8 +143,9 @@ export function logPoolErrors(pool, logger, options = {}) {
144
143
  };
145
144
  if (log)
146
145
  log.warn(message, meta);
146
+ // No logger to add the activity fields, so add them here.
147
147
  else
148
- console.warn(message, { kind: CONNECTION_LOSS_KIND, ...meta, error: serializeError(err) });
148
+ console.warn(message, { kind: CONNECTION_LOSS_KIND, ...processMeta(), ...meta, error: serializeError(err) });
149
149
  };
150
150
  // Per-client bookkeeping across one checkout: the borrower-side error
151
151
  // listener (pg-pool drops its own at checkout) and when the client was
package/dist/http.js CHANGED
@@ -55,8 +55,9 @@ export function startServer(opts) {
55
55
  assertBootSafety();
56
56
  logUncaughtErrors(opts.logger);
57
57
  const port = opts.port ?? Number(process.env.PORT ?? 8080);
58
- // Every request counts as activity (activity.ts) so connection-loss
59
- // events can say whether Cloud Run had CPU allocated at the time.
58
+ // Every request counts as activity (activity.ts), and wrapping registers
59
+ // the logger's activity source: every event from here on says whether
60
+ // Cloud Run had CPU allocated at the time.
60
61
  const server = serve({ fetch: trackActivity(opts.app.fetch), port }, (info) => {
61
62
  opts.logger.info('server started', { port: info.port });
62
63
  });
@@ -121,6 +121,10 @@ export interface EventFactoryOptions {
121
121
  app: string;
122
122
  source: 'server' | 'browser';
123
123
  context?: Record<string, unknown>;
124
+ /** Process-level fields merged under `meta` on every event, read at event
125
+ * time (the server logger passes process-meta.ts's request-activity source).
126
+ * Ignored for browser events. The caller's own meta wins on a name clash. */
127
+ processMeta?: () => Record<string, unknown> | undefined;
124
128
  }
125
129
  export declare function makeEvent(opts: EventFactoryOptions, level: LogLevel, message: string, meta?: Record<string, unknown>): LogEvent;
126
130
  /** The event for one logger call plus, the first time a site's meta breaks
@@ -152,5 +156,9 @@ export declare function shapeMeta(meta: Record<string, unknown>): {
152
156
  issues: ShapeIssue[];
153
157
  leaves: number;
154
158
  };
159
+ /** Toggle strict shape checking — for tests that exercise the rewrite/WARN
160
+ * behavior itself. Restore with setLogShapeStrict(true) (or leave it: vitest
161
+ * isolates module state per file). */
162
+ export declare function setLogShapeStrict(on: boolean): void;
155
163
  /** Forget which sites have been flagged (tests). */
156
164
  export declare function resetLogShapeWarnings(): void;
package/dist/log-core.js CHANGED
@@ -361,6 +361,11 @@ function buildEvent(opts, level, message, meta) {
361
361
  if (rewrites.length > 0)
362
362
  m = { ...m, logShape: rewrites.join('+') };
363
363
  }
364
+ // Added after shaping: a fixed vocabulary that must not count against the
365
+ // site's leaf budget.
366
+ const processMeta = opts.source === 'server' ? opts.processMeta?.() : undefined;
367
+ if (processMeta)
368
+ m = { ...processMeta, ...m };
364
369
  const event = {
365
370
  _time: new Date().toISOString(),
366
371
  app: opts.app,
@@ -377,6 +382,12 @@ function buildEvent(opts, level, message, meta) {
377
382
  * the shape rules in this process, a WARN `kind=log-shape` naming the site. */
378
383
  export function makeEvents(opts, level, message, meta) {
379
384
  const { event, issues } = buildEvent(opts, level, message, meta);
385
+ if (issues.length > 0 && strict) {
386
+ throw new Error(`log-shape (strict): "${message}" — ${issues.map(describeIssue).join('; ')}. ` +
387
+ 'Fix the logging site: keys are columns (OPERATIONS.md). ' +
388
+ '(Strict mode is on under vitest so shape bugs fail tests instead of ' +
389
+ 'shipping as rewrites; setLogShapeStrict(false) to test the rewrite itself.)');
390
+ }
380
391
  const fresh = issues.filter((i) => {
381
392
  const key = `${i.kind}\n${message}`;
382
393
  if (flagged.has(key))
@@ -457,6 +468,27 @@ export function shapeMeta(meta) {
457
468
  // Sites already flagged in this process, by issue kind and message: the
458
469
  // first offending event is worth a WARN, the ten-thousandth is not.
459
470
  const flagged = new Set();
471
+ // Strict mode: makeEvents throws on a shape issue instead of rewriting.
472
+ // On by default under vitest so a shape bug fails the suite that exercises
473
+ // the logging site rather than reaching production as a rewritten event
474
+ // with a once-per-process WARN. LOG_SHAPE_STRICT=1/0 overrides the default
475
+ // either way (production stays permissive: a logger must never throw there).
476
+ function strictDefault() {
477
+ // log-core is shared with the browser build; no bare `process` access.
478
+ if (typeof process === 'undefined' || !process.env)
479
+ return false;
480
+ const explicit = process.env.LOG_SHAPE_STRICT;
481
+ if (explicit !== undefined && explicit !== '')
482
+ return explicit !== '0';
483
+ return process.env.VITEST !== undefined;
484
+ }
485
+ let strict = strictDefault();
486
+ /** Toggle strict shape checking — for tests that exercise the rewrite/WARN
487
+ * behavior itself. Restore with setLogShapeStrict(true) (or leave it: vitest
488
+ * isolates module state per file). */
489
+ export function setLogShapeStrict(on) {
490
+ strict = on;
491
+ }
460
492
  /** Forget which sites have been flagged (tests). */
461
493
  export function resetLogShapeWarnings() {
462
494
  flagged.clear();
package/dist/log.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Logger, LogShipper, ShipperOptions } from './log-core.js';
2
- export { LogShipper, serializeError, truncate } from './log-core.js';
3
- export type { Logger, LogEvent, LogLevel, ShipperOptions, ShippedBatch, ShipFailureReason, } from './log-core.js';
2
+ export { LogShipper, makeEvent, makeEvents, serializeError, setLogShapeStrict, truncate } from './log-core.js';
3
+ export type { EventFactoryOptions, Logger, LogEvent, LogLevel, ShipperOptions, ShippedBatch, ShipFailureReason, } from './log-core.js';
4
4
  export interface LoggerOptions {
5
5
  /** App name, e.g. 'auth'. Becomes the `app` field queried in Axiom. */
6
6
  app: string;
package/dist/log.js CHANGED
@@ -9,7 +9,11 @@
9
9
  // log-core's key-shape guard rewrites offenders and WARNs once per site.
10
10
  import { writeSync } from 'node:fs';
11
11
  import { LogShipper, makeEvents, truncate, } from './log-core.js';
12
- export { LogShipper, serializeError, truncate } from './log-core.js';
12
+ import { processMeta } from './process-meta.js';
13
+ // makeEvent/makeEvents are exported for app test fakes: routing a recording
14
+ // logger's calls through makeEvents applies the key-shape guard (strict under
15
+ // vitest) to fake-logged sites too.
16
+ export { LogShipper, makeEvent, makeEvents, serializeError, setLogShapeStrict, truncate } from './log-core.js';
13
17
  // Registry of every shipper created here (plus any registered explicitly),
14
18
  // used by flushAllLoggers and graceful shutdown. Shipper-level on purpose:
15
19
  // with() children share their root's shipper, so registering loggers leaked
@@ -144,7 +148,7 @@ export function createLogger(opts) {
144
148
  process.once('beforeExit', () => void shipper.finalFlush());
145
149
  }
146
150
  function build(context) {
147
- const factory = { app: opts.app, source: 'server', context };
151
+ const factory = { app: opts.app, source: 'server', context, processMeta };
148
152
  const emit = (level, message, meta) => {
149
153
  for (const event of makeEvents(factory, level, message, meta)) {
150
154
  if (stdout)
@@ -0,0 +1,3 @@
1
+ export declare function setProcessMetaSource(next: (() => Record<string, unknown>) | undefined): void;
2
+ /** The registered source's fields now; undefined when none is registered. */
3
+ export declare function processMeta(): Record<string, unknown> | undefined;
@@ -0,0 +1,16 @@
1
+ // The one process-wide source of fields the server logger merges under
2
+ // `meta` on every event. Today that is request activity (activity.ts),
3
+ // registered by whatever serves HTTP (trackActivity, so startServer and
4
+ // custom servers alike): a process that never registers — a Cloud Run Job,
5
+ // a CLI, a test — emits no such fields at all, so absence means "not a
6
+ // server" and 0 means "idle server". Internal: not part of the package's
7
+ // public surface, and kept apart from log.ts so neither the logger nor
8
+ // activity.ts depends on the other.
9
+ let source;
10
+ export function setProcessMetaSource(next) {
11
+ source = next;
12
+ }
13
+ /** The registered source's fields now; undefined when none is registered. */
14
+ export function processMeta() {
15
+ return source?.();
16
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@steve31415/baselib",
3
- "version": "3.13.0",
3
+ "version": "3.15.0",
4
4
  "description": "Plasticine new-world shared platform library: logging, auth, service-to-service auth, db, HTTP, sync, app updates",
5
5
  "type": "module",
6
6
  "license": "MIT",