@lensmcp/node-instrumentation 1.23.13 → 1.24.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/lib/emit.d.ts +30 -7
- package/lib/emit.js +3 -3
- package/lib/flow-context.d.ts +31 -2
- package/lib/flow-context.js +1 -1
- package/lib/sql-accounting.d.ts +65 -0
- package/lib/sql-accounting.js +1 -0
- package/lib/tap-bullmq.d.ts +26 -0
- package/lib/tap-bullmq.js +1 -1
- package/lib/tap-nest.d.ts +8 -0
- package/lib/tap-nest.js +1 -1
- package/lib/tap-pg.js +1 -1
- package/package.json +4 -4
package/lib/emit.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type FloodGuard } from './flood-guard';
|
|
1
2
|
/** Per-generation cap for the events.jsonl bus (a long-running dev session hit 9 GB in the wild, and
|
|
2
3
|
* a 254 MB file OOM-crash-looped every ingesting consumer in the 2026-07-28 flood). Override with
|
|
3
4
|
* `LENSMCP_EVENT_MAX_BYTES` (default 50 MB). 0/invalid ⇒ the default. */
|
|
@@ -9,13 +10,17 @@ export declare const EVENT_MAX_FILES: number;
|
|
|
9
10
|
/**
|
|
10
11
|
* Round-robin rotation for a bus file: when the live file exceeds the per-generation cap, drop the
|
|
11
12
|
* oldest generation, shift `.1` → `.2` → … → `.N`, and rename the live file to `.1` (the next append
|
|
12
|
-
* recreates it fresh).
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
13
|
+
* recreates it fresh). Call it with the size of the line about to be appended, before appending it.
|
|
14
|
+
* Best-effort + fs-tap-safe (pristine refs, callers hold `writing`). Exported so every OTHER writer to
|
|
15
|
+
* the shared bus (the cluster gateway's observability emit, the typecheck bus) enforces the same
|
|
16
|
+
* contract — the gateway was the one writer with NO rotation, which is how foodguard's bus outgrew
|
|
17
|
+
* every consumer.
|
|
18
|
+
*
|
|
19
|
+
* WHEN to look at the size, and the cross-process-locked cascade itself, are `@lensmcp/core`'s
|
|
20
|
+
* `createBusRotator` / `rotateBusFile` — the one implementation every bus writer runs
|
|
21
|
+
* (issues/closed/foodguard-13, foodguard-40): a stat at most ~once per MB this process appends to
|
|
22
|
+
* `file`, sooner once its running total passes the cap, and a check banked only once its stat ran.
|
|
23
|
+
* `caps` exists for tests: with it, every call looks at the size.
|
|
19
24
|
*/
|
|
20
25
|
export declare function maybeRotateEventFile(file: string, addedBytes: number, caps?: {
|
|
21
26
|
maxBytes?: number;
|
|
@@ -43,6 +48,24 @@ export interface FlowCtx {
|
|
|
43
48
|
export declare function lensEmit(source: string, severity: LensSeverity, title: string, raw: Record<string, unknown>, fingerprint?: string, flowCtx?: FlowCtx,
|
|
44
49
|
/** Event category. Defaults to `cluster` — what every tap emitted before this existed. */
|
|
45
50
|
category?: string): boolean;
|
|
51
|
+
/**
|
|
52
|
+
* What became of one event: on the bus, dropped by its fingerprint's flood bucket, or lost to a
|
|
53
|
+
* write that failed (the lens offline).
|
|
54
|
+
*/
|
|
55
|
+
export type EmitOutcome = 'written' | 'suppressed' | 'failed';
|
|
56
|
+
/**
|
|
57
|
+
* {@link lensEmit}, saying WHY an event is not on the bus.
|
|
58
|
+
*
|
|
59
|
+
* The flood bucket is per FINGERPRINT and process-wide, so the `floodSuppressed` count the next
|
|
60
|
+
* admitted event carries belongs to the bucket — whichever request happens to be running when a
|
|
61
|
+
* token comes back gets handed every other request's drops (issues/closed/foodguard-39). A producer
|
|
62
|
+
* that accounts per request (the pg tap) needs the drop at the moment it happens, for the request
|
|
63
|
+
* it happened to.
|
|
64
|
+
*
|
|
65
|
+
* `guard` lets a producer bucket an event apart from the taps' shared buckets — its own fingerprint
|
|
66
|
+
* still applies there, so it is bounded exactly the same way.
|
|
67
|
+
*/
|
|
68
|
+
export declare function lensEmitOutcome(source: string, severity: LensSeverity, title: string, raw: Record<string, unknown>, fingerprint?: string, flowCtx?: FlowCtx, category?: string, guard?: FloodGuard): EmitOutcome;
|
|
46
69
|
/**
|
|
47
70
|
* Emit a raw service log line — the ACTUAL stdout/Logger output a pod prints — as
|
|
48
71
|
* a `runtime` event so it surfaces in the dashboard Logs view. Same fs-tap-safe
|
package/lib/emit.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
"use strict";var
|
|
2
|
-
`;return maybeRotateEventFile(exports.EVENT_FILE,
|
|
3
|
-
`;maybeRotateEventFile(exports.EVENT_FILE,
|
|
1
|
+
"use strict";var m=Object.defineProperty;var i=(e,r)=>m(e,"name",{value:r,configurable:!0});var u=Object.defineProperty,c=i((e,r)=>u(e,"name",{value:r,configurable:!0}),"c");Object.defineProperty(exports,"__esModule",{value:!0}),exports.EVENT_FILE=exports.PROJECT=exports.EVENT_MAX_FILES=exports.EVENT_MAX_BYTES=void 0,exports.maybeRotateEventFile=maybeRotateEventFile,exports.isLensWriting=isLensWriting,exports.lensEmit=lensEmit,exports.lensEmitOutcome=lensEmitOutcome,exports.lensLog=lensLog;const tslib_1=require("tslib"),nodeFs=tslib_1.__importStar(require("node:fs")),node_path_1=require("node:path"),bus_rotation_1=require("@lensmcp/core/bus-rotation"),flood_guard_1=require("./flood-guard"),flow_context_1=require("./flow-context"),appendFileSync=nodeFs.appendFileSync,mkdirSync=nodeFs.mkdirSync,statSync=nodeFs.statSync,renameSync=nodeFs.renameSync,unlinkSync=nodeFs.unlinkSync,openSync=nodeFs.openSync,closeSync=nodeFs.closeSync,BUS_CAPS=(0,bus_rotation_1.busCapsFromEnv)();exports.EVENT_MAX_BYTES=BUS_CAPS.maxBytes,exports.EVENT_MAX_FILES=BUS_CAPS.maxFiles;const rotators=new Map,BUS_FS={statSync,renameSync,unlinkSync,openSync,closeSync};function maybeRotateEventFile(e,r,o){if(o!==void 0){const n=o.maxBytes??exports.EVENT_MAX_BYTES,t=o.maxFiles??exports.EVENT_MAX_FILES;(0,bus_rotation_1.createBusRotator)(e,{maxBytes:n,maxFiles:t,fs:BUS_FS,checkEveryBytes:0})(r);return}let s=rotators.get(e);s||(s=(0,bus_rotation_1.createBusRotator)(e,{maxBytes:exports.EVENT_MAX_BYTES,maxFiles:exports.EVENT_MAX_FILES,fs:BUS_FS}),rotators.set(e,s)),s(r)}i(maybeRotateEventFile,"maybeRotateEventFile"),c(maybeRotateEventFile,"maybeRotateEventFile"),exports.PROJECT=process.env.LENSMCP_PROJECT||process.env.SERVICE_NAME||"app",exports.EVENT_FILE=process.env.LENSMCP_EVENT_FILE||`${process.cwd()}/.lensmcp/events.jsonl`;let writing=!1,dirReady=!1;const emitGuard=(0,flood_guard_1.createEventFloodGuard)({burst:30,perSec:5}),logGuard=(0,flood_guard_1.createEventFloodGuard)({burst:120,perSec:20});function isLensWriting(){return writing}i(isLensWriting,"isLensWriting"),c(isLensWriting,"isLensWriting");function lensEmit(e,r,o,s,n,t,a="cluster"){return lensEmitOutcome(e,r,o,s,n,t,a)==="written"}i(lensEmit,"lensEmit"),c(lensEmit,"lensEmit");function lensEmitOutcome(e,r,o,s,n,t,a="cluster",l=emitGuard){try{writing=!0;const E=n??`${e}:${String(s.name??e)}`,p=l.admit(E);if(!p.ok)return"suppressed";dirReady||(mkdirSync((0,node_path_1.dirname)(exports.EVENT_FILE),{recursive:!0}),dirReady=!0);const d=JSON.stringify({id:Date.now().toString(36)+Math.random().toString(36).slice(2,10),sessionId:"pending",timestamp:Date.now(),source:e,category:a,severity:r,context:{sessionId:"pending",projectName:exports.PROJECT,...t?.flowId?{flowId:t.flowId}:{},...t?.requestId?{requestId:t.requestId}:{},...t?.originNodeId?{originNodeId:t.originNodeId}:{}},fingerprint:E,title:o,raw:{project:exports.PROJECT,...s,...p.suppressed>0?{floodSuppressed:p.suppressed}:{}}})+`
|
|
2
|
+
`;return maybeRotateEventFile(exports.EVENT_FILE,d.length),appendFileSync(exports.EVENT_FILE,d),"written"}catch{return"failed"}finally{writing=!1}}i(lensEmitOutcome,"lensEmitOutcome"),c(lensEmitOutcome,"lensEmitOutcome");function lensLog(e,r,o){try{writing=!0;const s=e==="error"||e==="fatal"?`log:error:${exports.PROJECT}`:`log:${exports.PROJECT}`,n=logGuard.admit(s);if(!n.ok)return;dirReady||(mkdirSync((0,node_path_1.dirname)(exports.EVENT_FILE),{recursive:!0}),dirReady=!0);const t=(0,flow_context_1.flowContext)(),a=JSON.stringify({id:Date.now().toString(36)+Math.random().toString(36).slice(2,10),sessionId:"pending",timestamp:Date.now(),source:"nestjs",category:"runtime",severity:e,context:{sessionId:"pending",projectName:exports.PROJECT,...t?.flowId?{flowId:t.flowId}:{},...t?.requestId?{requestId:t.requestId}:{}},fingerprint:s,title:r,raw:{project:exports.PROJECT,kind:"log",...o?.stack?{request:{error:{message:r,stack:o.stack}}}:{},...n.suppressed>0?{floodSuppressed:n.suppressed}:{}}})+`
|
|
3
|
+
`;maybeRotateEventFile(exports.EVENT_FILE,a.length),appendFileSync(exports.EVENT_FILE,a)}catch{}finally{writing=!1}}i(lensLog,"lensLog"),c(lensLog,"lensLog");
|
package/lib/flow-context.d.ts
CHANGED
|
@@ -18,6 +18,10 @@
|
|
|
18
18
|
* A synchronous `require()` here puts BOTH loaders on the same serialized
|
|
19
19
|
* path — the second one just hits Node's module cache — so the race is
|
|
20
20
|
* structurally impossible instead of merely unlikely.
|
|
21
|
+
*
|
|
22
|
+
* The live context also carries its END signal when whoever opened it gives
|
|
23
|
+
* one (`CONTEXT_END` below): the Nest interceptor for a request, and
|
|
24
|
+
* `runInFlowContext` for a queue job.
|
|
21
25
|
*/
|
|
22
26
|
import type { FlowCtx } from './emit';
|
|
23
27
|
interface LiveCtx {
|
|
@@ -35,7 +39,7 @@ type CtxRunner = <T>(ctx: {
|
|
|
35
39
|
flowId?: string;
|
|
36
40
|
}, fn: () => T) => T;
|
|
37
41
|
/**
|
|
38
|
-
* Why the failure has to be told apart from the package being absent (issues/codex-security
|
|
42
|
+
* Why the failure has to be told apart from the package being absent (issues/closed/codex-security-46).
|
|
39
43
|
*
|
|
40
44
|
* `require(esm)` only works from Node 20.19 (and 22.12); before that it throws `ERR_REQUIRE_ESM`.
|
|
41
45
|
* The catch here said *"nest instrumentation absent"*, which on Node 18 was simply untrue — the
|
|
@@ -46,7 +50,7 @@ type CtxRunner = <T>(ctx: {
|
|
|
46
50
|
*
|
|
47
51
|
* The floor is now `>=20.19` (see this package's `engines`), and this reports rather than swallows,
|
|
48
52
|
* because an install that predates the floor must not look like an install that lacks the package.
|
|
49
|
-
* The same shape as issues/full-review
|
|
53
|
+
* The same shape as issues/closed/full-review-13 — a catch hiding the case it was not written for.
|
|
50
54
|
*/
|
|
51
55
|
export type FlowContextStatus = {
|
|
52
56
|
state: 'ready';
|
|
@@ -80,10 +84,35 @@ interface NestContextModule {
|
|
|
80
84
|
* that reaches nothing, which is the shape this round exists to stop writing.
|
|
81
85
|
*/
|
|
82
86
|
export declare function primeFlowContext(load?: () => NestContextModule): void;
|
|
87
|
+
/**
|
|
88
|
+
* A context's END signal — the protocol `@lensmcp/nest-instrumentation` defines in `context-end.ts`.
|
|
89
|
+
*
|
|
90
|
+
* The Nest interceptor opens and closes a request's context; what needs to know the request is over
|
|
91
|
+
* is here (the pg tap's SQL account, `sql-accounting.ts`), and this lib already depends on that one,
|
|
92
|
+
* so neither imports the other for it. The signal rides on the live context object, under a
|
|
93
|
+
* REGISTERED symbol both packages derive independently — the same way `lensmcp.wrappedRoot` crosses
|
|
94
|
+
* between them. `sql-accounting.spec.ts` runs the real interceptor to pin the two sides together.
|
|
95
|
+
*/
|
|
96
|
+
export declare const CONTEXT_END: unique symbol;
|
|
97
|
+
/** What a context's end signal offers whoever holds the context. */
|
|
98
|
+
export interface ContextEndSignal {
|
|
99
|
+
/** True once the context's work is over. */
|
|
100
|
+
readonly ended: boolean;
|
|
101
|
+
/** Call `listener` once the context ends — at once if it already has. */
|
|
102
|
+
onEnd(listener: () => void): void;
|
|
103
|
+
}
|
|
104
|
+
/** Give `ctx` an end signal; returns the function that fires it (idempotent; listeners never throw out). */
|
|
105
|
+
export declare function attachContextEnd(ctx: object): () => void;
|
|
106
|
+
/** The end signal a live context carries — undefined when whoever opened it gives none. */
|
|
107
|
+
export declare function contextEndOf(live: object | undefined): ContextEndSignal | undefined;
|
|
83
108
|
/**
|
|
84
109
|
* Run `fn` inside a fresh flow context (queue workers, timers — async
|
|
85
110
|
* entry points that aren't HTTP requests). Inner taps then inherit the
|
|
86
111
|
* flowId exactly like they do inside a traced request.
|
|
112
|
+
*
|
|
113
|
+
* The context ENDS when `fn`'s work does — at once for a synchronous `fn`,
|
|
114
|
+
* when its promise settles otherwise — so a queue job's SQL account is
|
|
115
|
+
* written at the job's end, not after a guessed quiet period.
|
|
87
116
|
*/
|
|
88
117
|
export declare function runInFlowContext<T>(ctx: {
|
|
89
118
|
flowId?: string;
|
package/lib/flow-context.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var
|
|
1
|
+
"use strict";var d=Object.defineProperty;var o=(e,t)=>d(e,"name",{value:t,configurable:!0});var l=Object.defineProperty,n=o((e,t)=>l(e,"name",{value:t,configurable:!0}),"n");Object.defineProperty(exports,"__esModule",{value:!0}),exports.CONTEXT_END=void 0,exports.flowContextStatus=flowContextStatus,exports.resetFlowContextForTests=resetFlowContextForTests,exports.isRequireEsmError=isRequireEsmError,exports.isModuleNotFoundError=isModuleNotFoundError,exports.primeFlowContext=primeFlowContext,exports.attachContextEnd=attachContextEnd,exports.contextEndOf=contextEndOf,exports.runInFlowContext=runInFlowContext,exports.liveFlowContext=liveFlowContext,exports.flowContext=flowContext;let getCtx=null,runWith=null,status={state:"not-primed"},warned=!1;function flowContextStatus(){return status}o(flowContextStatus,"flowContextStatus"),n(flowContextStatus,"flowContextStatus");function resetFlowContextForTests(){getCtx=null,runWith=null,status={state:"not-primed"},warned=!1}o(resetFlowContextForTests,"resetFlowContextForTests"),n(resetFlowContextForTests,"resetFlowContextForTests");function isRequireEsmError(e){const t=e?.code;return t==="ERR_REQUIRE_ESM"||t==="ERR_REQUIRE_ASYNC_MODULE"}o(isRequireEsmError,"isRequireEsmError"),n(isRequireEsmError,"isRequireEsmError");function isModuleNotFoundError(e){const t=e;return t?.code!=="MODULE_NOT_FOUND"&&t?.code!=="ERR_MODULE_NOT_FOUND"?!1:String(t?.message??"").includes("@lensmcp/nest-instrumentation")}o(isModuleNotFoundError,"isModuleNotFoundError"),n(isModuleNotFoundError,"isModuleNotFoundError");function requireNestInstrumentation(){return require("@lensmcp/nest-instrumentation")}o(requireNestInstrumentation,"requireNestInstrumentation"),n(requireNestInstrumentation,"requireNestInstrumentation");function primeFlowContext(e=requireNestInstrumentation){try{const t=e();getCtx=t.currentLensmcpContext,runWith=t.runInLensmcpContext,status={state:"ready"}}catch(t){if(getCtx=null,runWith=null,isModuleNotFoundError(t)){status={state:"absent"};return}const r=t instanceof Error?t.message:String(t);if(status=isRequireEsmError(t)?{state:"runtime-too-old",detail:r}:{state:"failed",detail:r},!warned){warned=!0;const s=status.state==="runtime-too-old"?`this Node (${process.version}) cannot require an ESM package; @lensmcp/node-instrumentation needs >=20.19`:r;console.warn(`[lensmcp] flow correlation is OFF \u2014 could not load @lensmcp/nest-instrumentation: ${s}`)}}}o(primeFlowContext,"primeFlowContext"),n(primeFlowContext,"primeFlowContext"),exports.CONTEXT_END=Symbol.for("lensmcp.contextEnd");function attachContextEnd(e){let t=!1;const r=[],s={get ended(){return t},onEnd(u){t?runEndListener(u):r.push(u)}};return Object.defineProperty(e,exports.CONTEXT_END,{value:s}),()=>{if(!t){t=!0;for(const u of r.splice(0))runEndListener(u)}}}o(attachContextEnd,"attachContextEnd"),n(attachContextEnd,"attachContextEnd");function runEndListener(e){try{e()}catch{}}o(runEndListener,"runEndListener"),n(runEndListener,"runEndListener");function contextEndOf(e){const t=e?.[exports.CONTEXT_END];return t&&typeof t.onEnd=="function"?t:void 0}o(contextEndOf,"contextEndOf"),n(contextEndOf,"contextEndOf");function runInFlowContext(e,t){if(!runWith)return t();const r={sessionId:"pending",requestId:e.requestId,...e.flowId?{flowId:e.flowId}:{}},s=attachContextEnd(r);let u;try{u=runWith(r,t)}catch{return s(),t()}const i=u;return i&&typeof i.then=="function"?i.then(c=>(s(),c),c=>{throw s(),c}):(s(),u)}o(runInFlowContext,"runInFlowContext"),n(runInFlowContext,"runInFlowContext");function liveFlowContext(){try{return getCtx?.()}catch{return}}o(liveFlowContext,"liveFlowContext"),n(liveFlowContext,"liveFlowContext");function flowContext(){const e=liveFlowContext();if(e)return{...e.flowId?{flowId:e.flowId}:{},...e.requestId?{requestId:e.requestId}:{},...e.originNodeId?{originNodeId:e.originNodeId}:{}}}o(flowContext,"flowContext"),n(flowContext,"flowContext");
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { type EmitOutcome, type FlowCtx } from './emit';
|
|
2
|
+
/**
|
|
3
|
+
* The record's `raw.kind`. `@lensmcp/core` reads it under the same name (`SQL_ACCOUNT_KIND` in
|
|
4
|
+
* `flow-segments.ts`); the two are pinned equal by `sql-accounting.spec.ts`, which reads this lib's
|
|
5
|
+
* real output through core's reader.
|
|
6
|
+
*/
|
|
7
|
+
export declare const SQL_ACCOUNT_KIND = "db-request-account";
|
|
8
|
+
/**
|
|
9
|
+
* What a record's census rests on (`raw.basis`; `@lensmcp/core` reads it under the same name).
|
|
10
|
+
* `end` — written once the request's context had ended: all of its SQL. `settled` — a context that
|
|
11
|
+
* gives no end signal, written once its SQL went quiet: later SQL in that context can still add to it.
|
|
12
|
+
*/
|
|
13
|
+
export type SqlAccountBasis = 'end' | 'settled';
|
|
14
|
+
/**
|
|
15
|
+
* Quiet time after an account's last statement before the FALLBACK record — only for SQL no end
|
|
16
|
+
* signal covers (see the module docstring). A request whose context ends is written at its end.
|
|
17
|
+
*/
|
|
18
|
+
export declare const SQL_ACCOUNT_SETTLE_MS = 1000;
|
|
19
|
+
/** One request's SQL, as its own call sites saw it. */
|
|
20
|
+
export interface SqlAccount {
|
|
21
|
+
/** Distinct per account: two requests that reuse one `x-request-id` keep two accounts. */
|
|
22
|
+
accountId: string;
|
|
23
|
+
ctx: FlowCtx;
|
|
24
|
+
/** Records written so far; a later one carries the cumulative totals and supersedes. */
|
|
25
|
+
seq: number;
|
|
26
|
+
/** Statements issued and not yet completed. */
|
|
27
|
+
inFlight: number;
|
|
28
|
+
statements: number;
|
|
29
|
+
failed: number;
|
|
30
|
+
workMs: number;
|
|
31
|
+
/** Statements whose db-query event reached the bus. */
|
|
32
|
+
recorded: number;
|
|
33
|
+
recordedWorkMs: number;
|
|
34
|
+
/** Dropped by the statement's own flood bucket — the drop happened to THIS request. */
|
|
35
|
+
suppressed: number;
|
|
36
|
+
/** Admitted, but the write failed (the lens offline). */
|
|
37
|
+
unwritten: number;
|
|
38
|
+
firstStartedAt: number;
|
|
39
|
+
lastEndedAt: number;
|
|
40
|
+
/** Its context will say when it ends, and the record waits for that — no quiet-period guess. */
|
|
41
|
+
awaitsEnd: boolean;
|
|
42
|
+
/** Its context has ended. */
|
|
43
|
+
ended: boolean;
|
|
44
|
+
/** The fallback's quiet-period timer. */
|
|
45
|
+
timer?: ReturnType<typeof setTimeout>;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* A statement is starting, in the request whose live context is `live`. Returns the account to close
|
|
49
|
+
* it against, or undefined when there is nothing to account to — no request, or a request outside any
|
|
50
|
+
* flow, which no flow view will ever read.
|
|
51
|
+
*/
|
|
52
|
+
export declare function beginSqlStatement(live: object | undefined, ctx: FlowCtx | undefined, startedAt: number): SqlAccount | undefined;
|
|
53
|
+
/** What one completed statement cost, and what became of its event. */
|
|
54
|
+
export interface SqlStatementOutcome {
|
|
55
|
+
durationMs: number;
|
|
56
|
+
failed: boolean;
|
|
57
|
+
emitted: EmitOutcome;
|
|
58
|
+
}
|
|
59
|
+
/** A statement completed. Once none are running, the account is written when it is due. */
|
|
60
|
+
export declare function endSqlStatement(account: SqlAccount | undefined, outcome: SqlStatementOutcome): void;
|
|
61
|
+
/**
|
|
62
|
+
* A statement that never ran — the driver threw before taking it. It is withdrawn rather than left
|
|
63
|
+
* in flight, where it would hold the account open (and the request unverified) forever.
|
|
64
|
+
*/
|
|
65
|
+
export declare function abandonSqlStatement(account: SqlAccount | undefined): void;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";var a=Object.defineProperty;var n=(t,e)=>a(t,"name",{value:e,configurable:!0});var f=Object.defineProperty,s=n((t,e)=>f(t,"name",{value:e,configurable:!0}),"s");Object.defineProperty(exports,"__esModule",{value:!0}),exports.SQL_ACCOUNT_SETTLE_MS=exports.SQL_ACCOUNT_KIND=void 0,exports.beginSqlStatement=beginSqlStatement,exports.endSqlStatement=endSqlStatement,exports.abandonSqlStatement=abandonSqlStatement;const flood_guard_1=require("./flood-guard"),emit_1=require("./emit"),flow_context_1=require("./flow-context");exports.SQL_ACCOUNT_KIND="db-request-account",exports.SQL_ACCOUNT_SETTLE_MS=1e3;const accountGuard=(0,flood_guard_1.createEventFloodGuard)({burst:120,perSec:20}),accounts=new WeakMap;let accountSeq=0;function newAccountId(){return accountSeq=(accountSeq+1)%16777215,`${Date.now().toString(36)}${accountSeq.toString(36)}${Math.random().toString(36).slice(2,6)}`}n(newAccountId,"newAccountId"),s(newAccountId,"newAccountId");function beginSqlStatement(t,e,d){if(!t||!e?.flowId||!e.requestId)return;let r=accounts.get(t);if(!r){r={accountId:newAccountId(),ctx:e,seq:0,inFlight:0,statements:0,failed:0,workMs:0,recorded:0,recordedWorkMs:0,suppressed:0,unwritten:0,firstStartedAt:d,lastEndedAt:d,awaitsEnd:!1,ended:!1},accounts.set(t,r);const o=(0,flow_context_1.contextEndOf)(t);if(o&&!o.ended){const i=r;i.awaitsEnd=!0,o.onEnd(()=>endSqlAccount(i))}}return r.inFlight+=1,r.timer&&(clearTimeout(r.timer),r.timer=void 0),r}n(beginSqlStatement,"beginSqlStatement"),s(beginSqlStatement,"beginSqlStatement");function endSqlStatement(t,e){t&&(t.inFlight=Math.max(0,t.inFlight-1),t.statements+=1,t.workMs+=e.durationMs,e.failed&&(t.failed+=1),e.emitted==="written"?(t.recorded+=1,t.recordedWorkMs+=e.durationMs):e.emitted==="suppressed"?t.suppressed+=1:t.unwritten+=1,t.lastEndedAt=Math.max(t.lastEndedAt,Date.now()),writeWhenDue(t))}n(endSqlStatement,"endSqlStatement"),s(endSqlStatement,"endSqlStatement");function abandonSqlStatement(t){t&&(t.inFlight=Math.max(0,t.inFlight-1),writeWhenDue(t))}n(abandonSqlStatement,"abandonSqlStatement"),s(abandonSqlStatement,"abandonSqlStatement");function endSqlAccount(t){t.ended=!0,writeWhenDue(t)}n(endSqlAccount,"endSqlAccount"),s(endSqlAccount,"endSqlAccount");function writeWhenDue(t){if(!(t.inFlight>0||t.statements===0)&&!(t.awaitsEnd&&!t.ended)){if(t.ended&&t.seq===0){writeAccount(t);return}settleLater(t)}}n(writeWhenDue,"writeWhenDue"),s(writeWhenDue,"writeWhenDue");function settleLater(t){t.timer&&clearTimeout(t.timer);const e=setTimeout(()=>writeAccount(t),exports.SQL_ACCOUNT_SETTLE_MS);e.unref?.(),t.timer=e}n(settleLater,"settleLater"),s(settleLater,"settleLater");function writeAccount(t){t.timer&&clearTimeout(t.timer),t.timer=void 0,t.seq+=1;const e=t.suppressed+t.unwritten,d=t.ended?"end":"settled";(0,emit_1.lensEmitOutcome)("lens","info",`SQL account: ${t.statements} statement${t.statements===1?"":"s"}`+(e>0?`, ${e} not on the bus`:", all on the bus"),{kind:exports.SQL_ACCOUNT_KIND,accountId:t.accountId,seq:t.seq,basis:d,statements:t.statements,failed:t.failed,workMs:t.workMs,recorded:t.recorded,recordedWorkMs:t.recordedWorkMs,suppressed:t.suppressed,unwritten:t.unwritten,firstStartedAt:t.firstStartedAt,lastEndedAt:t.lastEndedAt},`lens:sql-account:${emit_1.PROJECT}`,t.ctx,"db",accountGuard)}n(writeAccount,"writeAccount"),s(writeAccount,"writeAccount");
|
package/lib/tap-bullmq.d.ts
CHANGED
|
@@ -1,3 +1,29 @@
|
|
|
1
1
|
export declare function bullmqTap(exportsObject: unknown): boolean;
|
|
2
|
+
/** The shape the devserver child reads through the global. Keep in sync with `main.devserver.ts`. */
|
|
3
|
+
export interface WorkIntakeReport {
|
|
4
|
+
workers: number;
|
|
5
|
+
running: number;
|
|
6
|
+
runningAtStop: number;
|
|
7
|
+
takenAfterStop: number;
|
|
8
|
+
finishedAfterStop: number;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Stop every worker built by a generation OLDER than `serving` from taking new jobs. Jobs they are running
|
|
12
|
+
* finish — lock renewal is untouched — and the devserver's drain still waits for them. Returns how many
|
|
13
|
+
* workers were stopped and what they were running at that moment.
|
|
14
|
+
*
|
|
15
|
+
* "Older than", not "the previous one": a replacement that failed to build burned a generation number, and
|
|
16
|
+
* a worker the old app built after that carries it. `Infinity` stops them all (a retiring pod).
|
|
17
|
+
*
|
|
18
|
+
* `pause(true)`: stop fetching, do not wait for the running jobs — the drain does that — and leave the
|
|
19
|
+
* worker's connections alone for the app's own `close()`. bullmq does not re-check `paused` after a
|
|
20
|
+
* blocking wait returns, so a worker already waiting on its queue would still take the job that woke it;
|
|
21
|
+
* `tapWorkerIntake` refuses that fetch.
|
|
22
|
+
*/
|
|
23
|
+
export declare function stopWorkIntake(serving: number): WorkIntakeReport;
|
|
24
|
+
/** What generation `gen`'s workers have done since they were stopped; `undefined` if they never were. */
|
|
25
|
+
export declare function workIntakeReport(gen: number): WorkIntakeReport | undefined;
|
|
2
26
|
/** Test seam: stop every publisher and forget what they read from. */
|
|
3
27
|
export declare function resetQueueStatePublishersForTests(): void;
|
|
28
|
+
/** Test seam: forget every worker and what each generation did. */
|
|
29
|
+
export declare function resetWorkIntakeForTests(): void;
|
package/lib/tap-bullmq.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var b=Object.defineProperty;var d=(t,e)=>b(t,"name",{value:e,configurable:!0});var w=Object.defineProperty,a=d((t,e)=>w(t,"name",{value:e,configurable:!0}),"a");Object.defineProperty(exports,"__esModule",{value:!0}),exports.bullmqTap=bullmqTap,exports.resetQueueStatePublishersForTests=resetQueueStatePublishersForTests;const active_work_1=require("./active-work"),workerGeneration=new WeakMap,emit_1=require("./emit"),flow_context_1=require("./flow-context");function jobFlow(t){const e=t?.opts?.__lensmcpFlow;return e&&typeof e=="object"?e:void 0}d(jobFlow,"jobFlow"),a(jobFlow,"jobFlow");const jobLegId=a((t,e)=>`job:${t}#${e?.id??"?"}`,"jobLegId"),publishers=new Map,sources=new Map,sourceFor=a(t=>{let e=sources.get(t);return e||sources.set(t,e={}),e},"sourceFor"),usable=a(t=>!!t&&t.closing===void 0,"usable"),currentQueue=a(t=>{const e=sources.get(t),o=e?.app?.deref();return usable(o)?o:e?.own},"currentQueue");function ownConnection(t){if(!t||typeof t!="object")return;const e=t;if(typeof e.duplicate=="function")try{return e.duplicate()}catch{return}return Object.fromEntries(Object.entries(e).filter(([,o])=>typeof o!="function"))}d(ownConnection,"ownConnection"),a(ownConnection,"ownConnection");function bullmqTap(t){const e=t;if(typeof e?.Queue!="function"||typeof e?.Worker!="function")return!1;tappedQueue=e.Queue;try{tapQueueAdd(e)}catch{}try{tapWorkerEmit(e)}catch{}try{tapWorkerProcess(e)}catch{}try{tapWorkerConstruct(e)}catch{}return!0}d(bullmqTap,"bullmqTap"),a(bullmqTap,"bullmqTap");function tapWorkerConstruct(t){const e=t.Worker;if(typeof e!="function"||e.__lensmcpGen)return;const o=new Proxy(e,{construct(u,n,i){const l=Reflect.construct(u,n,i);try{workerGeneration.set(l,(0,active_work_1.activeWorkGeneration)())}catch{}return l}});Object.defineProperty(e,"__lensmcpGen",{value:!0,enumerable:!1}),t.Worker=o}d(tapWorkerConstruct,"tapWorkerConstruct"),a(tapWorkerConstruct,"tapWorkerConstruct");function tapWorkerProcess(t){const e=t.Worker?.prototype,o=e?.callProcessJob;if(!e||typeof o!="function"||o.__lensmcp)return;const u=a(async function(n,...i){const l=workerGeneration.get(this),s=(0,active_work_1.beginActiveWork)(l),c=jobFlow(n);if(!c?.flowId)return o.call(this,n,...i).finally(s);const r=String(this?.name??n?.queueName??"queue"),m=jobLegId(r,n),p=Date.now(),q={flowId:c.flowId,requestId:m};return(0,flow_context_1.runInFlowContext)(q,async()=>{try{const f=await o.call(this,n,...i);return(0,emit_1.lensEmit)("queue","info",`process ${n?.name}#${n?.id} (${Date.now()-p}ms)`,{queue:r,kind:"queue-process",job:n?.name,jobId:n?.id,startedAt:p,durationMs:Date.now()-p,status:"ok"},`queue:${r}`,q),f}catch(f){throw(0,emit_1.lensEmit)("queue","error",`process ${n?.name}#${n?.id} failed (${Date.now()-p}ms)`,{queue:r,kind:"queue-process",job:n?.name,jobId:n?.id,startedAt:p,durationMs:Date.now()-p,status:"error",error:String(f?.message??f)},`queue:${r}`,q),f}finally{s()}})},"wrapped");u.__lensmcp=!0,e.callProcessJob=u}d(tapWorkerProcess,"tapWorkerProcess"),a(tapWorkerProcess,"tapWorkerProcess");function tapQueueAdd(t){const e=t.Queue?.prototype,o=e?.add;if(!e||typeof o!="function"||o.__lensmcp)return;const u=a(async function(n,...i){const l=Date.now(),s=(0,flow_context_1.flowContext)();if(s?.flowId){const r=i[1]??{};i[1]={...r,__lensmcpFlow:{flowId:s.flowId,...s.requestId?{originRequestId:s.requestId}:{}}}}const c=await o.call(this,n,...i);try{const r=String(this?.name??"queue"),m=i[0];let p;try{const f=JSON.stringify(m);p=f&&f!=="undefined"?f.length>300?f.slice(0,299)+"\u2026":f:void 0}catch{}(0,emit_1.lensEmit)("queue","info",`enqueued ${n}#${c?.id} \u2192 ${r}`,{queue:r,kind:"queue-enqueued",job:n,jobId:c?.id,...p?{data:p}:{},startedAt:l,durationMs:Date.now()-l},`queue:${r}`,s);const q=sourceFor(r);q.app?.deref()!==this&&(q.app=new WeakRef(this)),ensureStatePublisher(r)}catch{}return c},"wrapped");u.__lensmcp=!0,e.add=u}d(tapQueueAdd,"tapQueueAdd"),a(tapQueueAdd,"tapQueueAdd");function tapWorkerEmit(t){const e=t.Worker?.prototype,o=e?.emit;if(!e||typeof o!="function"||o.__lensmcp)return;const u=a(function(n,...i){try{observeWorkerEvent(this,String(n),i)}catch{}return o.call(this,n,...i)},"wrapped");u.__lensmcp=!0,e.emit=u}d(tapWorkerEmit,"tapWorkerEmit"),a(tapWorkerEmit,"tapWorkerEmit");function observeWorkerEvent(t,e,o){if(e!=="active"&&e!=="completed"&&e!=="failed")return;const u=String(t.name??"queue"),n=o[0],i=jobFlow(n),l=i?.flowId?{flowId:i.flowId,requestId:jobLegId(u,n)}:void 0;if(e==="active")(0,emit_1.lensEmit)("queue","info",`processing ${n?.name}#${n?.id}`,{queue:u,kind:"queue-active",jobId:n?.id},`queue:${u}`,l);else if(e==="completed"){let s;try{const c=n?.returnvalue??o[1],r=JSON.stringify(c);s=r&&r!=="undefined"?r.length>160?r.slice(0,159)+"\u2026":r:void 0}catch{}(0,emit_1.lensEmit)("queue","info",`completed ${n?.name}#${n?.id}`,{queue:u,kind:"queue-completed",jobId:n?.id,...s?{returned:s}:{}},`queue:${u}`,l)}else{const s=o[1];(0,emit_1.lensEmit)("queue","error",`job ${n?.id} failed: ${s?.message}`,{queue:u,kind:"queue-failed",jobId:n?.id,error:String(s?.message??s)},`queue:${u}`,l)}if(!currentQueue(u))try{const s=t.opts,c=ownConnection(s?.connection);if(c){const r=snapshotQueueClass();r&&(sourceFor(u).own=new r(u,{connection:c}))}}catch{}ensureStatePublisher(u)}d(observeWorkerEvent,"observeWorkerEvent"),a(observeWorkerEvent,"observeWorkerEvent");let tappedQueue;function snapshotQueueClass(){if(tappedQueue)return tappedQueue;try{return require("bullmq").Queue}catch{return}}d(snapshotQueueClass,"snapshotQueueClass"),a(snapshotQueueClass,"snapshotQueueClass");function ensureStatePublisher(t){if(publishers.has(t))return;let e="";const o=setInterval(()=>{(async()=>{try{const u=currentQueue(t);if(!u)return;const n=await u.getJobCounts(),i=[];for(const c of["active","waiting","failed","completed","delayed"])for(const r of await u.getJobs([c],0,7)){const m=r?.data?.to;i.push({id:String(r.id),name:r.name,state:c,timestamp:r.timestamp??0,...typeof m=="string"?{to:m}:{}})}i.sort((c,r)=>Number(r.timestamp)-Number(c.timestamp));const l=i.slice(0,8),s=JSON.stringify([n,l.map(c=>`${c.id}${c.state}`)]);if(s===e)return;e=s,(0,emit_1.lensEmit)("queue","info",`queue state: ${n.active??0} active \xB7 ${n.waiting??0} waiting \xB7 ${n.failed??0} failed`,{queue:t,kind:"queue-state",counts:n,recent:l},`queue:${t}`)}catch{}})()},2e3);o.unref?.(),publishers.set(t,o)}d(ensureStatePublisher,"ensureStatePublisher"),a(ensureStatePublisher,"ensureStatePublisher");function resetQueueStatePublishersForTests(){for(const t of publishers.values())clearInterval(t);publishers.clear(),sources.clear()}d(resetQueueStatePublishersForTests,"resetQueueStatePublishersForTests"),a(resetQueueStatePublishersForTests,"resetQueueStatePublishersForTests");
|
|
1
|
+
"use strict";var y=Object.defineProperty;var a=(t,e)=>y(t,"name",{value:e,configurable:!0});var g=Object.defineProperty,u=a((t,e)=>g(t,"name",{value:e,configurable:!0}),"u");Object.defineProperty(exports,"__esModule",{value:!0}),exports.bullmqTap=bullmqTap,exports.stopWorkIntake=stopWorkIntake,exports.workIntakeReport=workIntakeReport,exports.resetQueueStatePublishersForTests=resetQueueStatePublishersForTests,exports.resetWorkIntakeForTests=resetWorkIntakeForTests;const active_work_1=require("./active-work"),workerGeneration=new WeakMap,emit_1=require("./emit"),flow_context_1=require("./flow-context");function jobFlow(t){const e=t?.opts?.__lensmcpFlow;return e&&typeof e=="object"?e:void 0}a(jobFlow,"jobFlow"),u(jobFlow,"jobFlow");const jobLegId=u((t,e)=>`job:${t}#${e?.id??"?"}`,"jobLegId"),publishers=new Map,sources=new Map,sourceFor=u(t=>{let e=sources.get(t);return e||sources.set(t,e={}),e},"sourceFor"),usable=u(t=>!!t&&t.closing===void 0,"usable"),currentQueue=u(t=>{const e=sources.get(t),o=e?.app?.deref();return usable(o)?o:e?.own},"currentQueue");function ownConnection(t){if(!t||typeof t!="object")return;const e=t;if(typeof e.duplicate=="function")try{return e.duplicate()}catch{return}return Object.fromEntries(Object.entries(e).filter(([,o])=>typeof o!="function"))}a(ownConnection,"ownConnection"),u(ownConnection,"ownConnection");function bullmqTap(t){const e=t;if(typeof e?.Queue!="function"||typeof e?.Worker!="function")return!1;tappedQueue=e.Queue;try{tapQueueAdd(e)}catch{}try{tapWorkerEmit(e)}catch{}try{tapWorkerProcess(e)}catch{}try{tapWorkerStamp(e)}catch{}try{tapWorkerIntake(e)}catch{}try{tapWorkerConstruct(e)}catch{}try{installWorkIntakeControl()}catch{}return!0}a(bullmqTap,"bullmqTap"),u(bullmqTap,"bullmqTap");function stampWorker(t){try{if(workerGeneration.has(t))return;workerGeneration.set(t,(0,active_work_1.activeWorkGeneration)()),knownWorkers.add(new WeakRef(t))}catch{}}a(stampWorker,"stampWorker"),u(stampWorker,"stampWorker");function tapWorkerConstruct(t){const e=t.Worker;if(typeof e!="function"||e.__lensmcpGen)return;const o=new Proxy(e,{construct(r,n,s){const p=Reflect.construct(r,n,s);return stampWorker(p),p}});Object.defineProperty(e,"__lensmcpGen",{value:!0,enumerable:!1}),t.Worker=o}a(tapWorkerConstruct,"tapWorkerConstruct"),u(tapWorkerConstruct,"tapWorkerConstruct");function tapWorkerStamp(t){const e=t.Worker?.prototype;if(e)for(const o of["createLockManager","run"]){const r=e[o];if(typeof r!="function"||r.__lensmcpStamp)continue;const n=u(function(...s){return stampWorker(this),r.apply(this,s)},"wrapped");n.__lensmcpStamp=!0,e[o]=n}}a(tapWorkerStamp,"tapWorkerStamp"),u(tapWorkerStamp,"tapWorkerStamp");const knownWorkers=new Set,stoppedWorkers=new WeakSet,intake=new Map,INTAKE_GENERATIONS_KEPT=64;function intakeOf(t){let e=intake.get(t);if(!e){intake.set(t,e={workers:0,running:0,runningAtStop:0,takenAfterStop:0,finishedAfterStop:0});for(const o of intake.keys()){if(intake.size<=INTAKE_GENERATIONS_KEPT)break;intake.delete(o)}}return e}a(intakeOf,"intakeOf"),u(intakeOf,"intakeOf");function beginIntake(t,e){if(e===void 0)return()=>{};const o=intakeOf(e);return o.running+=1,stoppedWorkers.has(t)&&(o.takenAfterStop+=1),()=>{o.running-=1,o.stoppedAt!==void 0&&(o.finishedAfterStop+=1)}}a(beginIntake,"beginIntake"),u(beginIntake,"beginIntake");function stopWorkIntake(t){const e={workers:0,running:0,runningAtStop:0,takenAfterStop:0,finishedAfterStop:0},o=Date.now();for(const r of knownWorkers){const n=r.deref();if(!n){knownWorkers.delete(r);continue}const s=workerGeneration.get(n);if(s===void 0||s>=t||stoppedWorkers.has(n))continue;const p=n.pause;if(typeof p!="function")continue;stoppedWorkers.add(n),knownWorkers.delete(r);try{Promise.resolve(p.call(n,!0)).catch(()=>{})}catch{}const i=intakeOf(s);i.stoppedAt===void 0&&(i.stoppedAt=o,i.runningAtStop=i.running,e.running+=i.running,e.runningAtStop+=i.running),i.workers+=1,e.workers+=1}return e}a(stopWorkIntake,"stopWorkIntake"),u(stopWorkIntake,"stopWorkIntake");function workIntakeReport(t){const e=intake.get(t);if(!(!e||e.stoppedAt===void 0))return{workers:e.workers,running:e.running,runningAtStop:e.runningAtStop,takenAfterStop:e.takenAfterStop,finishedAfterStop:e.finishedAfterStop}}a(workIntakeReport,"workIntakeReport"),u(workIntakeReport,"workIntakeReport");function tapWorkerIntake(t){const e=t.Worker?.prototype;if(!e)return;const o=e.moveToActive;if(typeof o=="function"&&!o.__lensmcpIntake){const n=u(function(...s){return stoppedWorkers.has(this)?Promise.resolve(void 0):o.apply(this,s)},"wrapped");n.__lensmcpIntake=!0,e.moveToActive=n}const r=e.resume;if(typeof r=="function"&&!r.__lensmcpIntake){const n=u(function(...s){if(!stoppedWorkers.has(this))return r.apply(this,s)},"wrapped");n.__lensmcpIntake=!0,e.resume=n}}a(tapWorkerIntake,"tapWorkerIntake"),u(tapWorkerIntake,"tapWorkerIntake");function installWorkIntakeControl(){const t=globalThis;t.__lensmcpStopWorkIntake=stopWorkIntake,t.__lensmcpWorkIntakeReport=workIntakeReport}a(installWorkIntakeControl,"installWorkIntakeControl"),u(installWorkIntakeControl,"installWorkIntakeControl");function tapWorkerProcess(t){const e=t.Worker?.prototype,o=e?.callProcessJob;if(!e||typeof o!="function"||o.__lensmcp)return;const r=u(async function(n,...s){const p=workerGeneration.get(this),i=(0,active_work_1.beginActiveWork)(p),f=beginIntake(this,p),c=u(()=>{i(),f()},"endWork"),k=jobFlow(n);if(!k?.flowId)return o.call(this,n,...s).finally(c);const d=String(this?.name??n?.queueName??"queue"),w=jobLegId(d,n),l=Date.now(),h={flowId:k.flowId,requestId:w};return(0,flow_context_1.runInFlowContext)(h,async()=>{try{const m=await o.call(this,n,...s);return(0,emit_1.lensEmit)("queue","info",`process ${n?.name}#${n?.id} (${Date.now()-l}ms)`,{queue:d,kind:"queue-process",job:n?.name,jobId:n?.id,startedAt:l,durationMs:Date.now()-l,status:"ok"},`queue:${d}`,h),m}catch(m){throw(0,emit_1.lensEmit)("queue","error",`process ${n?.name}#${n?.id} failed (${Date.now()-l}ms)`,{queue:d,kind:"queue-process",job:n?.name,jobId:n?.id,startedAt:l,durationMs:Date.now()-l,status:"error",error:String(m?.message??m)},`queue:${d}`,h),m}finally{c()}})},"wrapped");r.__lensmcp=!0,e.callProcessJob=r}a(tapWorkerProcess,"tapWorkerProcess"),u(tapWorkerProcess,"tapWorkerProcess");function tapQueueAdd(t){const e=t.Queue?.prototype,o=e?.add;if(!e||typeof o!="function"||o.__lensmcp)return;const r=u(async function(n,...s){const p=Date.now(),i=(0,flow_context_1.flowContext)();if(i?.flowId){const c=s[1]??{};s[1]={...c,__lensmcpFlow:{flowId:i.flowId,...i.requestId?{originRequestId:i.requestId}:{}}}}const f=await o.call(this,n,...s);try{const c=String(this?.name??"queue"),k=s[0];let d;try{const l=JSON.stringify(k);d=l&&l!=="undefined"?l.length>300?l.slice(0,299)+"\u2026":l:void 0}catch{}(0,emit_1.lensEmit)("queue","info",`enqueued ${n}#${f?.id} \u2192 ${c}`,{queue:c,kind:"queue-enqueued",job:n,jobId:f?.id,...d?{data:d}:{},startedAt:p,durationMs:Date.now()-p},`queue:${c}`,i);const w=sourceFor(c);w.app?.deref()!==this&&(w.app=new WeakRef(this)),ensureStatePublisher(c)}catch{}return f},"wrapped");r.__lensmcp=!0,e.add=r}a(tapQueueAdd,"tapQueueAdd"),u(tapQueueAdd,"tapQueueAdd");function tapWorkerEmit(t){const e=t.Worker?.prototype,o=e?.emit;if(!e||typeof o!="function"||o.__lensmcp)return;const r=u(function(n,...s){try{observeWorkerEvent(this,String(n),s)}catch{}return o.call(this,n,...s)},"wrapped");r.__lensmcp=!0,e.emit=r}a(tapWorkerEmit,"tapWorkerEmit"),u(tapWorkerEmit,"tapWorkerEmit");function observeWorkerEvent(t,e,o){if(e!=="active"&&e!=="completed"&&e!=="failed")return;const r=String(t.name??"queue"),n=o[0],s=jobFlow(n),p=s?.flowId?{flowId:s.flowId,requestId:jobLegId(r,n)}:void 0;if(e==="active")(0,emit_1.lensEmit)("queue","info",`processing ${n?.name}#${n?.id}`,{queue:r,kind:"queue-active",jobId:n?.id},`queue:${r}`,p);else if(e==="completed"){let i;try{const f=n?.returnvalue??o[1],c=JSON.stringify(f);i=c&&c!=="undefined"?c.length>160?c.slice(0,159)+"\u2026":c:void 0}catch{}(0,emit_1.lensEmit)("queue","info",`completed ${n?.name}#${n?.id}`,{queue:r,kind:"queue-completed",jobId:n?.id,...i?{returned:i}:{}},`queue:${r}`,p)}else{const i=o[1];(0,emit_1.lensEmit)("queue","error",`job ${n?.id} failed: ${i?.message}`,{queue:r,kind:"queue-failed",jobId:n?.id,error:String(i?.message??i)},`queue:${r}`,p)}if(!currentQueue(r))try{const i=t.opts,f=ownConnection(i?.connection);if(f){const c=snapshotQueueClass();c&&(sourceFor(r).own=new c(r,{connection:f}))}}catch{}ensureStatePublisher(r)}a(observeWorkerEvent,"observeWorkerEvent"),u(observeWorkerEvent,"observeWorkerEvent");let tappedQueue;function snapshotQueueClass(){if(tappedQueue)return tappedQueue;try{return require("bullmq").Queue}catch{return}}a(snapshotQueueClass,"snapshotQueueClass"),u(snapshotQueueClass,"snapshotQueueClass");function ensureStatePublisher(t){if(publishers.has(t))return;let e="";const o=setInterval(()=>{(async()=>{try{const r=currentQueue(t);if(!r)return;const n=await r.getJobCounts(),s=[];for(const f of["active","waiting","failed","completed","delayed"])for(const c of await r.getJobs([f],0,7)){const k=c?.data?.to;s.push({id:String(c.id),name:c.name,state:f,timestamp:c.timestamp??0,...typeof k=="string"?{to:k}:{}})}s.sort((f,c)=>Number(c.timestamp)-Number(f.timestamp));const p=s.slice(0,8),i=JSON.stringify([n,p.map(f=>`${f.id}${f.state}`)]);if(i===e)return;e=i,(0,emit_1.lensEmit)("queue","info",`queue state: ${n.active??0} active \xB7 ${n.waiting??0} waiting \xB7 ${n.failed??0} failed`,{queue:t,kind:"queue-state",counts:n,recent:p},`queue:${t}`)}catch{}})()},2e3);o.unref?.(),publishers.set(t,o)}a(ensureStatePublisher,"ensureStatePublisher"),u(ensureStatePublisher,"ensureStatePublisher");function resetQueueStatePublishersForTests(){for(const t of publishers.values())clearInterval(t);publishers.clear(),sources.clear()}a(resetQueueStatePublishersForTests,"resetQueueStatePublishersForTests"),u(resetQueueStatePublishersForTests,"resetQueueStatePublishersForTests");function resetWorkIntakeForTests(){knownWorkers.clear(),intake.clear()}a(resetWorkIntakeForTests,"resetWorkIntakeForTests"),u(resetWorkIntakeForTests,"resetWorkIntakeForTests");
|
package/lib/tap-nest.d.ts
CHANGED
|
@@ -4,6 +4,14 @@ export interface CapturedChildApp {
|
|
|
4
4
|
port: number | string;
|
|
5
5
|
};
|
|
6
6
|
close: () => Promise<void>;
|
|
7
|
+
/**
|
|
8
|
+
* The Nest application itself, as a dual-mode `createChildApp` hands it over (`BuiltChildApp.app` in the cluster
|
|
9
|
+
* devserver). Once a hot swap has closed this generation, the pod holds the app, its DI container and its root
|
|
10
|
+
* module WEAKLY and proves they were collected (`GenerationTracker`, `@lensmcp/cluster` pod-memory.ts) — without it
|
|
11
|
+
* a zero-touch pod's proof rested on the bundle's Module and a count of live `NestContainer`s
|
|
12
|
+
* (issues/closed/pod-memory-02, "Needs a decision"). Nothing here keeps it: `close` already held it.
|
|
13
|
+
*/
|
|
14
|
+
app?: unknown;
|
|
7
15
|
}
|
|
8
16
|
/**
|
|
9
17
|
* Default `createChildApp` for zero-touch bundles. Installed BEFORE the
|
package/lib/tap-nest.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var d=Object.defineProperty;var c=(e,t)=>d(e,"name",{value:t,configurable:!0});var f=Object.defineProperty,s=c((e,t)=>f(e,"name",{value:t,configurable:!0}),"s");Object.defineProperty(exports,"__esModule",{value:!0}),exports.installChildAppBridge=installChildAppBridge,exports.nestTap=nestTap;const emit_1=require("./emit"),WRAPPED=Symbol.for("lensmcp.wrappedRoot"),captures=[],waiters=[];function pushCapture(e){const t=waiters.shift();t?(clearTimeout(t.timer),t.resolve(e)):captures.push(e)}c(pushCapture,"pushCapture"),s(pushCapture,"pushCapture");function nextCapture(e=6e4){const t=captures.shift();return t?Promise.resolve(t):new Promise((n,r)=>{const i={resolve:n,reject:r,timer:setTimeout(()=>{const o=waiters.indexOf(i);o>=0&&waiters.splice(o,1),r(new Error("[lensmcp] no NestJS app captured within 60s \u2014 does main.ts call app.listen()? (zero-touch pods boot by running your main and intercepting listen)"))},e)};i.timer.unref?.(),waiters.push(i)})}c(nextCapture,"nextCapture"),s(nextCapture,"nextCapture");function installChildAppBridge(){if(process.env.APP_RUNNER!=="1")return;const e=global,t=s(()=>nextCapture(),"factory");e.createChildApp=t,e.__lensmcpCreateChildApp=t}c(installChildAppBridge,"installChildAppBridge"),s(installChildAppBridge,"installChildAppBridge");function nestTap(e){const t=e,n=t?.NestFactory,r=n?.create;if(!n||typeof r!="function")return!1;if(r.__lensmcp)return!0;const i=s(async function(o,...a){let p=o;const l=o,u=!!(l?.[WRAPPED]||l?.module?.[WRAPPED]);if(process.env.LENSMCP_NEST!=="0"&&!u)try{p=await buildLensmcpRoot(o)}catch{p=o}return r.call(this??n,p,...a)},"wrapped");if(i.__lensmcp=!0,n.create=i,process.env.APP_RUNNER==="1"){const o=t?.NestApplication?.prototype,a=o?.listen;if(o&&typeof a=="function"&&!a.__lensmcp){const p=s(async function(l,...u){return captureApp(this,l).catch(m=>(console.error("[lensmcp] pod capture failed, falling back to TCP listen:",m),a.call(this,l,...u)))},"listenWrapped");p.__lensmcp=!0,o.listen=p}}return!0}c(nestTap,"nestTap"),s(nestTap,"nestTap");async function buildLensmcpRoot(e){const t=await import("@lensmcp/nest-instrumentation"),n=require("@nestjs/common"),r=t.LensmcpModule.forRoot({projectName:emit_1.PROJECT,trace:{autoInstrumentMethods:!0},memory:{mode:"light",scope:"all"}});class i{static{c(this,"n")}static{s(this,"LensmcpZeroTouchRoot")}}return i[WRAPPED]=!0,n.Module({imports:[r,e]})(i),i}c(buildLensmcpRoot,"buildLensmcpRoot"),s(buildLensmcpRoot,"buildLensmcpRoot");async function captureApp(e,t){await e.init();let n=e.getHttpAdapter?.()?.getInstance?.();if(typeof n!="function"){const r=n;r?.ready&&await r.ready(),typeof r?.routing=="function"&&(n=s((i,o)=>r.routing(i,o),"handler"))}if(typeof n!="function")throw new Error("[lensmcp] cannot derive a request handler from the HTTP adapter");return pushCapture({handler:n,serviceConfig:{port:t??process.env.PORT??0},close:s(()=>Promise.resolve(e.close()).then(()=>{}),"close")}),e.getHttpServer?.()}c(captureApp,"captureApp"),s(captureApp,"captureApp");
|
|
1
|
+
"use strict";var d=Object.defineProperty;var c=(e,t)=>d(e,"name",{value:t,configurable:!0});var f=Object.defineProperty,s=c((e,t)=>f(e,"name",{value:t,configurable:!0}),"s");Object.defineProperty(exports,"__esModule",{value:!0}),exports.installChildAppBridge=installChildAppBridge,exports.nestTap=nestTap;const emit_1=require("./emit"),WRAPPED=Symbol.for("lensmcp.wrappedRoot"),captures=[],waiters=[];function pushCapture(e){const t=waiters.shift();t?(clearTimeout(t.timer),t.resolve(e)):captures.push(e)}c(pushCapture,"pushCapture"),s(pushCapture,"pushCapture");function nextCapture(e=6e4){const t=captures.shift();return t?Promise.resolve(t):new Promise((n,r)=>{const i={resolve:n,reject:r,timer:setTimeout(()=>{const o=waiters.indexOf(i);o>=0&&waiters.splice(o,1),r(new Error("[lensmcp] no NestJS app captured within 60s \u2014 does main.ts call app.listen()? (zero-touch pods boot by running your main and intercepting listen)"))},e)};i.timer.unref?.(),waiters.push(i)})}c(nextCapture,"nextCapture"),s(nextCapture,"nextCapture");function installChildAppBridge(){if(process.env.APP_RUNNER!=="1")return;const e=global,t=s(()=>nextCapture(),"factory");e.createChildApp=t,e.__lensmcpCreateChildApp=t}c(installChildAppBridge,"installChildAppBridge"),s(installChildAppBridge,"installChildAppBridge");function nestTap(e){const t=e,n=t?.NestFactory,r=n?.create;if(!n||typeof r!="function")return!1;if(r.__lensmcp)return!0;const i=s(async function(o,...a){let p=o;const l=o,u=!!(l?.[WRAPPED]||l?.module?.[WRAPPED]);if(process.env.LENSMCP_NEST!=="0"&&!u)try{p=await buildLensmcpRoot(o)}catch{p=o}return r.call(this??n,p,...a)},"wrapped");if(i.__lensmcp=!0,n.create=i,process.env.APP_RUNNER==="1"){const o=t?.NestApplication?.prototype,a=o?.listen;if(o&&typeof a=="function"&&!a.__lensmcp){const p=s(async function(l,...u){return captureApp(this,l).catch(m=>(console.error("[lensmcp] pod capture failed, falling back to TCP listen:",m),a.call(this,l,...u)))},"listenWrapped");p.__lensmcp=!0,o.listen=p}}return!0}c(nestTap,"nestTap"),s(nestTap,"nestTap");async function buildLensmcpRoot(e){const t=await import("@lensmcp/nest-instrumentation"),n=require("@nestjs/common"),r=t.LensmcpModule.forRoot({projectName:emit_1.PROJECT,trace:{autoInstrumentMethods:!0},memory:{mode:"light",scope:"all"}});class i{static{c(this,"n")}static{s(this,"LensmcpZeroTouchRoot")}}return i[WRAPPED]=!0,n.Module({imports:[r,e]})(i),i}c(buildLensmcpRoot,"buildLensmcpRoot"),s(buildLensmcpRoot,"buildLensmcpRoot");async function captureApp(e,t){await e.init();let n=e.getHttpAdapter?.()?.getInstance?.();if(typeof n!="function"){const r=n;r?.ready&&await r.ready(),typeof r?.routing=="function"&&(n=s((i,o)=>r.routing(i,o),"handler"))}if(typeof n!="function")throw new Error("[lensmcp] cannot derive a request handler from the HTTP adapter");return pushCapture({handler:n,serviceConfig:{port:t??process.env.PORT??0},close:s(()=>Promise.resolve(e.close()).then(()=>{}),"close"),app:e}),e.getHttpServer?.()}c(captureApp,"captureApp"),s(captureApp,"captureApp");
|
package/lib/tap-pg.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var
|
|
1
|
+
"use strict";var P=Object.defineProperty;var f=(t,e)=>P(t,"name",{value:e,configurable:!0});var D=Object.defineProperty,r=f((t,e)=>D(t,"name",{value:e,configurable:!0}),"r");Object.defineProperty(exports,"__esModule",{value:!0}),exports.pgTap=pgTap;const emit_1=require("./emit"),flow_context_1=require("./flow-context"),sql_accounting_1=require("./sql-accounting"),SKIP_OPS=new Set(["BEGIN","COMMIT","ROLLBACK","SET","SHOW","DEALLOCATE","DISCARD"]);function parseSql(t){const e=(t.match(/^\s*(\w+)/)?.[1]??"query").toUpperCase(),o=t.match(/\b(?:from|into|update|join|table(?:\s+if\s+not\s+exists)?)\s+"?([\w$.]+)"?/i)?.[1]?.split(".").pop();return{op:e,...o?{table:o}:{}}}f(parseSql,"parseSql"),r(parseSql,"parseSql");const oneLine=r(t=>t.replace(/\s+/g," ").trim(),"oneLine"),clip=r((t,e)=>t.length>e?t.slice(0,e-1)+"\u2026":t,"clip");function paramPreview(t){if(!(!Array.isArray(t)||t.length===0))return t.slice(0,10).map(e=>{if(e==null)return String(e);const o=typeof e=="string"?e:(()=>{try{return JSON.stringify(e)}catch{return String(e)}})();return clip(o,80)})}f(paramPreview,"paramPreview"),r(paramPreview,"paramPreview");const rowsOf=r(t=>t?.rowCount??t?.rows?.length??0,"rowsOf");function resultPreview(t){const e=t?.rows;if(!(!Array.isArray(e)||e.length===0))try{return clip(JSON.stringify(e.slice(0,2)),240)}catch{return}}f(resultPreview,"resultPreview"),r(resultPreview,"resultPreview");function pgTap(t){const e=t?.Client?.prototype,o=e?.query;if(!e||typeof o!="function")return!1;if(o.__lensmcp)return!0;const O=r(function(l,i,y){const u=l,g=typeof u=="string"?u:u?.text;if(!g||u&&typeof u=="object"&&typeof u.submit=="function")return o.call(this,l,i,y);const{op:v,table:a}=parseSql(g);if(SKIP_OPS.has(v))return o.call(this,l,i,y);const p=v.toLowerCase(),L=`db:${p}:${a??"query"}`,m=Date.now(),q=(0,flow_context_1.flowContext)(),w=(0,flow_context_1.liveFlowContext)();w&&(w.dbCallCount=(w.dbCallCount??0)+1);const b=(0,sql_accounting_1.beginSqlStatement)(w,q,m),A=clip(oneLine(g),500),S=paramPreview(Array.isArray(i)?i:l?.values),h=r((n,c)=>{const s=Date.now()-m;let C;if(n)C=(0,emit_1.lensEmitOutcome)("db","error",`${p}${a?" "+a:""} failed (${s}ms)`,{name:"postgres",kind:"db-query",op:p,...a?{table:a}:{},query:A,...S?{params:S}:{},startedAt:m,error:String(n?.message??n),durationMs:s},`db:error:${p}:${a??"query"}`,q);else{const $=rowsOf(c),x=resultPreview(c);C=(0,emit_1.lensEmitOutcome)("db","info",`${p} ${a??"query"} (${$} rows, ${s}ms)`,{name:"postgres",kind:"db-query",op:p,...a?{table:a}:{},query:A,...S?{params:S}:{},...x?{result:x}:{},startedAt:m,rows:$,durationMs:s},L,q)}(0,sql_accounting_1.endSqlStatement)(b,{durationMs:s,failed:!!n,emitted:C})},"done"),_=typeof y=="function"?y:typeof i=="function"?i:void 0;if(_){const n=r((c,s)=>{try{h(c,s)}catch{}_(c,s)},"wrappedCb");try{return typeof i=="function"?o.call(this,l,n):o.call(this,l,i,n)}catch(c){throw(0,sql_accounting_1.abandonSqlStatement)(b),c}}let d;try{d=o.call(this,l,i)}catch(n){throw(0,sql_accounting_1.abandonSqlStatement)(b),n}return d&&typeof d.then=="function"?d.then(n=>h(void 0,n),n=>h(n,void 0)):(0,sql_accounting_1.abandonSqlStatement)(b),d},"wrapped");return O.__lensmcp=!0,e.query=O,!0}f(pgTap,"pgTap"),r(pgTap,"pgTap");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lensmcp/node-instrumentation",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.24.0",
|
|
4
4
|
"main": "./index.js",
|
|
5
5
|
"types": "./index.d.ts",
|
|
6
6
|
"exports": {
|
|
@@ -15,9 +15,9 @@
|
|
|
15
15
|
}
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@lensmcp/core": "1.
|
|
19
|
-
"@lensmcp/nest-instrumentation": "1.
|
|
20
|
-
"@lensmcp/protocol-types": "1.
|
|
18
|
+
"@lensmcp/core": "1.24.0",
|
|
19
|
+
"@lensmcp/nest-instrumentation": "1.24.0",
|
|
20
|
+
"@lensmcp/protocol-types": "1.24.0",
|
|
21
21
|
"tslib": "^2.3.0"
|
|
22
22
|
},
|
|
23
23
|
"optionalDependencies": {
|