@lensmcp/nest-instrumentation 1.23.12 → 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/README.md CHANGED
@@ -151,7 +151,7 @@ Re-exported for advanced use and custom integrations:
151
151
 
152
152
  Every event it emits is a `BaseEvent` from [`@lensmcp/protocol-types`](https://github.com/kiwiapps-ltd/lensmcp) (kinds: `server-request`, `span`, `db-query`, `loop`, `singleton-instance`), shared by every LensMCP source so they reduce into the same resource views. Memory tracking is delegated to `@lensmcp/memory-tracker`, and ids / fingerprints come from `@lensmcp/core`.
153
153
 
154
- By default events are routed by `defaultEventSink()`, which picks a transport from the environment: `LENSMCP_EVENT_FILE` (append JSONL), `LENSMCP_UDS` (reconnecting NDJSON over a Unix domain socket), or `LENSMCP_IPC_SOCKET` (UDP datagrams); with no session configured it falls back to logging only errors to stdout under a `[lensmcp]` prefix.
154
+ By default events are routed by `defaultEventSink()`, which picks a transport from the environment: `LENSMCP_EVENT_FILE` (append JSONL, rotated with every other bus writer under `LENSMCP_EVENT_MAX_BYTES` / `LENSMCP_EVENT_MAX_FILES`), `LENSMCP_UDS` (reconnecting NDJSON over a Unix domain socket), or `LENSMCP_IPC_SOCKET` (UDP datagrams); with no session configured it falls back to logging only errors to stdout under a `[lensmcp]` prefix.
155
155
 
156
156
  ---
157
157
 
@@ -0,0 +1,32 @@
1
+ /**
2
+ * A request context's END, as a signal other instrumentation can wait on.
3
+ *
4
+ * The context is opened and closed here (`TraceInterceptor`), but what needs to know that a request
5
+ * is over lives in `@lensmcp/node-instrumentation` — the pg tap's per-request SQL account
6
+ * (issues/closed/foodguard-39), which used to guess "over" from a second of quiet SQL, and so could read a
7
+ * request as complete while it was still running statements. That package depends on this one, so
8
+ * this one cannot import it back.
9
+ *
10
+ * The seam is the context object itself. The interceptor attaches an end signal to it under a
11
+ * REGISTERED symbol — `Symbol.for`, the same key in every copy of either package, the way
12
+ * `lensmcp.wrappedRoot` already crosses between them — and a tap that holds the live context reads it
13
+ * there. No import in either direction. A context without one (opened by `runInLensmcpContext`
14
+ * directly) simply gives no end signal, and its SQL account falls back to the quiet period.
15
+ *
16
+ * `@lensmcp/node-instrumentation`'s `flow-context.ts` speaks the same protocol for the contexts it
17
+ * opens itself (queue jobs); its `sql-accounting.spec.ts` pins the two together by running this
18
+ * package's real interceptor against its real pg tap.
19
+ */
20
+ export declare const CONTEXT_END: unique symbol;
21
+ /** What a context's end signal offers whoever holds the context. */
22
+ export interface ContextEndSignal {
23
+ /** True once the context's work is over. */
24
+ readonly ended: boolean;
25
+ /** Call `listener` once the context ends — at once if it already has. */
26
+ onEnd(listener: () => void): void;
27
+ }
28
+ /**
29
+ * Give `ctx` an end signal; returns the function that fires it. Firing is idempotent, and a listener
30
+ * that throws never reaches the host's request.
31
+ */
32
+ export declare function attachContextEnd(ctx: object): () => void;
@@ -0,0 +1 @@
1
+ "use strict";var s=Object.defineProperty;var r=(e,t)=>s(e,"name",{value:t,configurable:!0});var u=Object.defineProperty,a=r((e,t)=>u(e,"name",{value:t,configurable:!0}),"r");export const CONTEXT_END=Symbol.for("lensmcp.contextEnd");export function attachContextEnd(e){let t=!1;const c=[];return Object.defineProperty(e,CONTEXT_END,{value:{get ended(){return t},onEnd(n){t?o(n):c.push(n)}}}),()=>{if(!t){t=!0;for(const n of c.splice(0))o(n)}}}r(attachContextEnd,"attachContextEnd"),a(attachContextEnd,"attachContextEnd");function o(e){try{e()}catch{}}r(o,"s"),a(o,"runListener");
@@ -4,7 +4,8 @@ import type { EventSink } from './types.js';
4
4
  * Resolve an `EventSink` from env vars + caller overrides.
5
5
  *
6
6
  * Resolution order:
7
- * 1. `LENSMCP_EVENT_FILE` — append one JSON line per event.
7
+ * 1. `LENSMCP_EVENT_FILE` — append one JSON line per event, rotating the
8
+ * shared bus file with every other writer (see {@link busRotator}).
8
9
  * The Phase 3 e2e smoke uses this (no IPC plumbing needed).
9
10
  * 2. `LENSMCP_UDS` — Unix domain socket, framed NDJSON, reconnecting.
10
11
  * The hardened transport (reliable, no datagram size cap).
package/lib/event-sink.js CHANGED
@@ -1,3 +1,3 @@
1
- "use strict";var m=Object.defineProperty;var u=(n,e)=>m(n,"name",{value:e,configurable:!0});var d=Object.defineProperty,c=u((n,e)=>d(n,"name",{value:e,configurable:!0}),"s");import{createSocket as S}from"node:dgram";import{connect as v}from"node:net";import{appendFile as y}from"node:fs/promises";import{boundedClone as E,createEventFloodGuard as h,DEFAULT_RAW_BYTE_LIMIT as g}from"@lensmcp/protocol-types";import{maybeRotateEventFile as k}from"@lensmcp/protocol-types/bounded-writer";export function defaultEventSink(){const n=process.env.LENSMCP_EVENT_FILE;if(n){const r=h({burst:30,perSec:5});let i=Promise.resolve();return s=>{const o=r.admit(String(s.fingerprint??""));if(!o.ok)return;const a=f(s,o.suppressed);a&&(i=i.then(async()=>{k(n,a.length),await y(n,a).catch(()=>{})}))}}const e=process.env.LENSMCP_UDS;if(e)return l(e);const t=process.env.LENSMCP_IPC_SOCKET;if(t){const[r,i]=t.split(":");if(r&&i){const s=Number(i),o=S("udp4");return o.unref(),a=>{try{const p=Buffer.from(JSON.stringify(a));o.send(p,s,r,()=>{})}catch{}}}}return r=>{(r.severity==="error"||r.severity==="fatal")&&console.warn(`[lensmcp] ${r.severity}: ${r.title}`)}}u(defaultEventSink,"defaultEventSink"),c(defaultEventSink,"defaultEventSink");function f(n,e){try{const t=E(n.raw,{rawByteLimit:g}).value,r={...n,raw:t};return e>0&&(r.floodSuppressed=e),JSON.stringify(r)+`
2
- `}catch{return}}u(f,"E"),c(f,"serializeBounded");export function inMemorySink(){const n=[];return{sink:c(e=>n.push(e),"sink"),events:n}}u(inMemorySink,"inMemorySink"),c(inMemorySink,"inMemorySink");function l(n){const e=[];let t=null,r=!1;const i=c(()=>{if(!(!t||!t.writable))for(;e.length>0;)t.write(e.shift())},"flush"),s=c(()=>{if(r||t)return;r=!0;const o=v(n);o.on("connect",()=>{r=!1,t=o,i()}),o.on("drain",i),o.on("error",()=>{r=!1,t=null}),o.on("close",()=>{t=null}),o.unref()},"open");return s(),o=>{for(e.push(JSON.stringify(o)+`
3
- `);e.length>1e4;)e.shift();if(t&&t.writable){i();return}if(t){try{t.destroy()}catch{}t=null}s()}}u(l,"w"),c(l,"udsEventSink");
1
+ "use strict";var y=Object.defineProperty;var a=(n,t)=>y(n,"name",{value:t,configurable:!0});var m=Object.defineProperty,i=a((n,t)=>m(n,"name",{value:t,configurable:!0}),"s");import{createSocket as d}from"node:dgram";import{connect as v}from"node:net";import{appendFileSync as E,closeSync as h,openSync as k,renameSync as b,statSync as g,unlinkSync as L}from"node:fs";import{boundedClone as N,createEventFloodGuard as _,DEFAULT_RAW_BYTE_LIMIT as w}from"@lensmcp/protocol-types";import{busCapsFromEnv as B,createBusRotator as C}from"@lensmcp/core/bus-rotation";export function defaultEventSink(){const n=process.env.LENSMCP_EVENT_FILE;if(n){const r=_({burst:30,perSec:5}),s=l(n);return c=>{const o=r.admit(String(c.fingerprint??""));if(!o.ok)return;const u=f(c,o.suppressed);if(u)try{s(Buffer.byteLength(u)),E(n,u)}catch{}}}const t=process.env.LENSMCP_UDS;if(t)return p(t);const e=process.env.LENSMCP_IPC_SOCKET;if(e){const[r,s]=e.split(":");if(r&&s){const c=Number(s),o=d("udp4");return o.unref(),u=>{try{const S=Buffer.from(JSON.stringify(u));o.send(S,c,r,()=>{})}catch{}}}}return r=>{(r.severity==="error"||r.severity==="fatal")&&console.warn(`[lensmcp] ${r.severity}: ${r.title}`)}}a(defaultEventSink,"defaultEventSink"),i(defaultEventSink,"defaultEventSink");function f(n,t){try{const e=N(n.raw,{rawByteLimit:w}).value,r={...n,raw:e};return t>0&&(r.floodSuppressed=t),JSON.stringify(r)+`
2
+ `}catch{return}}a(f,"L"),i(f,"serializeBounded");const M={statSync:g,renameSync:b,unlinkSync:L,openSync:k,closeSync:h};function l(n){return C(n,{...B(),fs:M})}a(l,"v"),i(l,"busRotator");export function inMemorySink(){const n=[];return{sink:i(t=>n.push(t),"sink"),events:n}}a(inMemorySink,"inMemorySink"),i(inMemorySink,"inMemorySink");function p(n){const t=[];let e=null,r=!1;const s=i(()=>{if(!(!e||!e.writable))for(;t.length>0;)e.write(t.shift())},"flush"),c=i(()=>{if(r||e)return;r=!0;const o=v(n);o.on("connect",()=>{r=!1,e=o,s()}),o.on("drain",s),o.on("error",()=>{r=!1,e=null}),o.on("close",()=>{e=null}),o.unref()},"open");return c(),o=>{for(t.push(JSON.stringify(o)+`
3
+ `);t.length>1e4;)t.shift();if(e&&e.writable){s();return}if(e){try{e.destroy()}catch{}e=null}c()}}a(p,"B"),i(p,"udsEventSink");
@@ -22,7 +22,7 @@ export interface CreateLensmcpNestAppOptions extends Partial<LensmcpModuleOption
22
22
  }
23
23
  /**
24
24
  * The adapter generic is carried through, exactly as `NestFactory.create` carries it
25
- * (issues/codex-security/59).
25
+ * (issues/closed/codex-security-59).
26
26
  *
27
27
  * const app = await createLensmcpNestApp<NestExpressApplication>(AppModule, { projectName: 'api' });
28
28
  * app.useStaticAssets(join(__dirname, 'public'));
@@ -9,6 +9,8 @@ import { Observable } from 'rxjs';
9
9
  * call, DB query, or queue job downstream inherits the same flow.
10
10
  * 3. Emits a `server-request` event on completion with method, route,
11
11
  * status, and duration.
12
+ * 4. Signals the context's END (`context-end.ts`) once the request is
13
+ * over, for the taps that wait on it.
12
14
  */
13
15
  export declare class TraceInterceptor implements NestInterceptor {
14
16
  intercept(execCtx: ExecutionContext, next: CallHandler): Observable<unknown>;
@@ -1,3 +1,3 @@
1
- "use strict";var _=Object.defineProperty;var c=(t,e)=>_(t,"name",{value:e,configurable:!0});var H=Object.defineProperty,i=c((t,e)=>H(t,"name",{value:e,configurable:!0}),"a");import{__decorate as P}from"tslib";import{Injectable as F}from"@nestjs/common";import{Observable as D}from"rxjs";import{catchError as G,tap as L}from"rxjs/operators";import{fingerprint as N,ulid as g}from"@lensmcp/core";import{activeMemoryOptions as U,onFlowStart as X,onFlowSettled as z}from"@lensmcp/memory-tracker";import{LENSMCP_CONTEXT_STORAGE as B}from"./context.js";import{activeOptions as C}from"./state.js";let b=class{static{c(this,"C")}static{i(this,"TraceInterceptor")}intercept(t,e){const r=C();if(!r.trace.requests)return e.handle();const d=t.switchToHttp(),s=d.getRequest(),o=d.getResponse(),a=l(s.headers?.["x-request-id"])??g(),n={sessionId:r.sessionId,requestId:a,parentRequestId:l(s.headers?.["x-lensmcp-parent-request-id"]),flowId:l(s.headers?.["x-lensmcp-flow-id"]),originNodeId:l(s.headers?.["x-lensmcp-origin-node-id"]),traceparent:l(s.headers?.traceparent),dbCallCount:0},u=(s.method??"GET").toUpperCase(),m=s.route?.path??s.url??"<unknown>",f=t.getClass?.()?.name,I=t.getHandler?.()?.name,w=Date.now(),$=r.memory.mode!=="off";$&&X(a);const j=i(()=>{if(!$)return;const q=U()?.settleMs??1e3;setTimeout(()=>z(a,n.flowId),q).unref?.()},"settleAndCheck");return new D(q=>{let T;return B.run(n,()=>{T=e.handle().pipe(L({next:i(p=>q.next(p),"next"),complete:i(()=>{y(r.projectName,n,u,m,o.statusCode??200,w),v(n,f,I,w,"ok"),j(),q.complete()},"complete")}),G(p=>{const x=E(p);if(x!==void 0||typeof o.once!="function")y(r.projectName,n,u,m,x??k(o),w,p);else{let R=!1;const S=i(A=>{if(R)return;R=!0;const O=A&&o.statusCode&&o.statusCode>=400?o.statusCode:k(o);y(r.projectName,n,u,m,O,w,p)},"settle");o.once("finish",()=>S(!0)),o.once("close",()=>S(!1))}return v(n,f,I,w,"error"),j(),q.error(p),new D})).subscribe()}),()=>T?.unsubscribe()})}};b=P([F()],b);export{b as TraceInterceptor};function E(t){const e=t;if(typeof e?.getStatus=="function")return e.getStatus();if(typeof e?.status=="number"&&e.status>=100)return e.status;if(typeof e?.statusCode=="number"&&e.statusCode>=100)return e.statusCode}c(E,"z"),i(E,"ownStatus");function k(t){return t.statusCode&&t.statusCode>=400?t.statusCode:500}c(k,"M"),i(k,"inferStatus");function l(t){if(typeof t=="string")return t;if(Array.isArray(t)&&typeof t[0]=="string")return t[0]}c(l,"w"),i(l,"stringHeader");function v(t,e,r,d,s){if(!e||!r)return;const o=C(),a=Date.now()-d,n=`${e}.${r}`,u={id:g(),sessionId:t.sessionId,timestamp:Date.now(),source:"nestjs",category:"backend",severity:s==="error"?"error":"info",context:{sessionId:t.sessionId,flowId:t.flowId,requestId:t.requestId},fingerprint:N({kind:"method-call",identity:n}),title:`${n} (${a}ms)${s==="error"?" \u2717":""}`,raw:{kind:"span",span:{name:n,role:"controller",flowId:t.flowId,requestId:t.requestId,startTime:d,endTime:Date.now(),durationMs:a,status:s}}};h(u,C().projectName),o.emit(u)}c(v,"N"),i(v,"emitControllerSpan");function M(t){if(!(t instanceof Error))return{message:String(t)};const e=t.stack?t.stack.split(`
1
+ "use strict";var H=Object.defineProperty;var c=(t,e)=>H(t,"name",{value:e,configurable:!0});var L=Object.defineProperty,a=c((t,e)=>L(t,"name",{value:e,configurable:!0}),"o");import{__decorate as U}from"tslib";import{Injectable as X}from"@nestjs/common";import{Observable as O}from"rxjs";import{catchError as z,tap as B}from"rxjs/operators";import{fingerprint as M,ulid as g}from"@lensmcp/core";import{activeMemoryOptions as J,onFlowStart as K,onFlowSettled as Q}from"@lensmcp/memory-tracker";import{LENSMCP_CONTEXT_STORAGE as V}from"./context.js";import{attachContextEnd as W}from"./context-end.js";import{activeOptions as h}from"./state.js";let b=class{static{c(this,"h")}static{a(this,"TraceInterceptor")}intercept(t,e){const s=h();if(!s.trace.requests)return e.handle();const d=t.switchToHttp(),o=d.getRequest(),r=d.getResponse(),i=I(o.headers?.["x-request-id"])??g(),n={sessionId:s.sessionId,requestId:i,parentRequestId:I(o.headers?.["x-lensmcp-parent-request-id"]),flowId:I(o.headers?.["x-lensmcp-flow-id"]),originNodeId:I(o.headers?.["x-lensmcp-origin-node-id"]),traceparent:I(o.headers?.traceparent),dbCallCount:0},u=(o.method??"GET").toUpperCase(),f=o.route?.path??o.url??"<unknown>",m=t.getClass?.()?.name,w=t.getHandler?.()?.name,q=Date.now(),j=s.memory.mode!=="off";j&&K(i);const x=a(()=>{if(!j)return;const p=J()?.settleMs??1e3;setTimeout(()=>Q(i,n.flowId),p).unref?.()},"settleAndCheck"),P=W(n);let T=!1,v=typeof r?.once!="function";const R=a(()=>{T&&v&&P()},"endIfOver");if(!v){const p=a(()=>{v=!0,R()},"responseOut");r.once?.("finish",p),r.once?.("close",p)}return new O(p=>{let S;return V.run(n,()=>{S=e.handle().pipe(B({next:a(l=>p.next(l),"next"),complete:a(()=>{y(s.projectName,n,u,f,r.statusCode??200,q),$(n,m,w,q,"ok"),x(),p.complete()},"complete")}),z(l=>{const D=A(l);if(D!==void 0||typeof r.once!="function")y(s.projectName,n,u,f,D??k(r),q,l);else{let N=!1;const E=a(F=>{if(N)return;N=!0;const G=F&&r.statusCode&&r.statusCode>=400?r.statusCode:k(r);y(s.projectName,n,u,f,G,q,l)},"settle");r.once("finish",()=>E(!0)),r.once("close",()=>E(!1))}return $(n,m,w,q,"error"),x(),p.error(l),new O})).subscribe()}),()=>{S?.unsubscribe(),T=!0,R()}})}};b=U([X()],b);export{b as TraceInterceptor};function A(t){const e=t;if(typeof e?.getStatus=="function")return e.getStatus();if(typeof e?.status=="number"&&e.status>=100)return e.status;if(typeof e?.statusCode=="number"&&e.statusCode>=100)return e.statusCode}c(A,"V"),a(A,"ownStatus");function k(t){return t.statusCode&&t.statusCode>=400?t.statusCode:500}c(k,"N"),a(k,"inferStatus");function I(t){if(typeof t=="string")return t;if(Array.isArray(t)&&typeof t[0]=="string")return t[0]}c(I,"w"),a(I,"stringHeader");function $(t,e,s,d,o){if(!e||!s)return;const r=h(),i=Date.now()-d,n=`${e}.${s}`,u={id:g(),sessionId:t.sessionId,timestamp:Date.now(),source:"nestjs",category:"backend",severity:o==="error"?"error":"info",context:{sessionId:t.sessionId,flowId:t.flowId,requestId:t.requestId},fingerprint:M({kind:"method-call",identity:n}),title:`${n} (${i}ms)${o==="error"?" \u2717":""}`,raw:{kind:"span",span:{name:n,role:"controller",flowId:t.flowId,requestId:t.requestId,startTime:d,endTime:Date.now(),durationMs:i,status:o}}};C(u,h().projectName),r.emit(u)}c($,"_"),a($,"emitControllerSpan");function _(t){if(!(t instanceof Error))return{message:String(t)};const e=t.stack?t.stack.split(`
2
2
  `).slice(0,30).join(`
3
- `):void 0;return{...t.name?{name:t.name}:{},message:t.message,...e?{stack:e}:{}}}c(M,"B"),i(M,"errorDetail");function h(t,e){if(!e)return;const r=t.raw;r&&r.project===void 0&&(r.project=e)}c(h,"h"),i(h,"stampProject");function y(t,e,r,d,s,o,a){const n=C(),u=Date.now()-o,m=s>=500||a!==void 0,f={id:g(),sessionId:e.sessionId,timestamp:Date.now(),source:"nestjs",category:"backend",severity:m?"error":"info",context:{sessionId:e.sessionId,flowId:e.flowId,requestId:e.requestId,...e.parentRequestId?{parentRequestId:e.parentRequestId}:{},originNodeId:e.originNodeId,traceparent:e.traceparent},fingerprint:N({kind:"server-request",identity:`${r}:${d}:${Math.floor(s/100)}xx`}),title:`${r} ${d} \u2192 ${s} (${u}ms)`,message:a instanceof Error?a.message:void 0,raw:{kind:"server-request",request:{method:r,route:d,status:s,durationMs:u,startedAt:o,endedAt:Date.now(),flowId:e.flowId,requestId:e.requestId,...e.parentRequestId?{parentRequestId:e.parentRequestId}:{},...a?{error:M(a)}:{}}}},I={...f,id:g(),raw:{kind:"span",span:{name:`nest.request ${r} ${d}`,traceId:e.traceparent,flowId:e.flowId,requestId:e.requestId,startTime:o,endTime:Date.now(),durationMs:u,status:m?"error":"ok"}}};h(f,t),h(I,t),n.emit(f),n.emit(I)}c(y,"k"),i(y,"emitRequest");
3
+ `):void 0;return{...t.name?{name:t.name}:{},message:t.message,...e?{stack:e}:{}}}c(_,"W"),a(_,"errorDetail");function C(t,e){if(!e)return;const s=t.raw;s&&s.project===void 0&&(s.project=e)}c(C,"k"),a(C,"stampProject");function y(t,e,s,d,o,r,i){const n=h(),u=Date.now()-r,f=o>=500||i!==void 0,m={id:g(),sessionId:e.sessionId,timestamp:Date.now(),source:"nestjs",category:"backend",severity:f?"error":"info",context:{sessionId:e.sessionId,flowId:e.flowId,requestId:e.requestId,...e.parentRequestId?{parentRequestId:e.parentRequestId}:{},originNodeId:e.originNodeId,traceparent:e.traceparent},fingerprint:M({kind:"server-request",identity:`${s}:${d}:${Math.floor(o/100)}xx`}),title:`${s} ${d} \u2192 ${o} (${u}ms)`,message:i instanceof Error?i.message:void 0,raw:{kind:"server-request",request:{method:s,route:d,status:o,durationMs:u,startedAt:r,endedAt:Date.now(),flowId:e.flowId,requestId:e.requestId,...e.parentRequestId?{parentRequestId:e.parentRequestId}:{},...i?{error:_(i)}:{}}}},w={...m,id:g(),raw:{kind:"span",span:{name:`nest.request ${s} ${d}`,traceId:e.traceparent,flowId:e.flowId,requestId:e.requestId,startTime:r,endTime:Date.now(),durationMs:u,status:f?"error":"ok"}}};C(m,t),C(w,t),n.emit(m),n.emit(w)}c(y,"$"),a(y,"emitRequest");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lensmcp/nest-instrumentation",
3
- "version": "1.23.12",
3
+ "version": "1.24.0",
4
4
  "type": "module",
5
5
  "main": "./index.js",
6
6
  "module": "./index.js",
@@ -14,9 +14,9 @@
14
14
  }
15
15
  },
16
16
  "dependencies": {
17
- "@lensmcp/core": "1.23.12",
18
- "@lensmcp/memory-tracker": "1.23.12",
19
- "@lensmcp/protocol-types": "1.23.12",
17
+ "@lensmcp/core": "1.24.0",
18
+ "@lensmcp/memory-tracker": "1.24.0",
19
+ "@lensmcp/protocol-types": "1.24.0",
20
20
  "tslib": "^2.3.0"
21
21
  },
22
22
  "peerDependencies": {