@lensmcp/nest-instrumentation 1.23.13 → 1.24.1
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/lib/context-end.d.ts +32 -0
- package/lib/context-end.js +1 -0
- package/lib/event-sink.d.ts +2 -1
- package/lib/event-sink.js +3 -3
- package/lib/lensmcp-app.d.ts +1 -1
- package/lib/trace-interceptor.d.ts +15 -0
- package/lib/trace-interceptor.js +2 -2
- package/package.json +4 -4
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");
|
package/lib/event-sink.d.ts
CHANGED
|
@@ -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
|
|
2
|
-
`}catch{return}}
|
|
3
|
-
`);
|
|
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");
|
package/lib/lensmcp-app.d.ts
CHANGED
|
@@ -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
|
|
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,7 +9,22 @@ 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>;
|
|
15
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* What became of a request, for a reader deciding whether something is broken (issues/closed/foodguard-43):
|
|
20
|
+
* - `ok` — it answered below 400;
|
|
21
|
+
* - `refused` — a 4xx: the app turned the request away on purpose (not found, forbidden, invalid). Evidence of the
|
|
22
|
+
* request, not of a fault — a passing abuse or foreign-tenant test is made of these;
|
|
23
|
+
* - `declared` — a 5xx other than 500 the app chose to answer with (503 while a dependency is unavailable);
|
|
24
|
+
* - `fault` — a 500, or a programming error (a `TypeError`, a `ReferenceError`, …) whatever status it was mapped to.
|
|
25
|
+
*
|
|
26
|
+
* Every thrown value used to be an error, so a 404 the app meant read exactly like a crash: the lens's health
|
|
27
|
+
* headline went to "blocking" over a passing negative test.
|
|
28
|
+
*/
|
|
29
|
+
export type RequestOutcome = 'ok' | 'refused' | 'declared' | 'fault';
|
|
30
|
+
export declare function requestOutcome(status: number, err?: unknown): RequestOutcome;
|
package/lib/trace-interceptor.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
"use strict";var
|
|
1
|
+
"use strict";var U=Object.defineProperty;var u=(e,t)=>U(e,"name",{value:t,configurable:!0});var L=Object.defineProperty,n=u((e,t)=>L(e,"name",{value:t,configurable:!0}),"r");import{__decorate as X}from"tslib";import{Injectable as z}from"@nestjs/common";import{Observable as M}from"rxjs";import{catchError as B,tap as J}from"rxjs/operators";import{fingerprint as A,ulid as g}from"@lensmcp/core";import{activeMemoryOptions as K,onFlowStart as Q,onFlowSettled as V}from"@lensmcp/memory-tracker";import{LENSMCP_CONTEXT_STORAGE as W}from"./context.js";import{attachContextEnd as Y}from"./context-end.js";import{activeOptions as y}from"./state.js";let k=class{static{u(this,"h")}static{n(this,"TraceInterceptor")}intercept(e,t){const r=y();if(!r.trace.requests)return t.handle();const d=e.switchToHttp(),o=d.getRequest(),s=d.getResponse(),i=I(o.headers?.["x-request-id"])??g(),a={sessionId:r.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},c=(o.method??"GET").toUpperCase(),m=o.route?.path??o.url??"<unknown>",p=e.getClass?.()?.name,w=e.getHandler?.()?.name,q=Date.now(),R=r.memory.mode!=="off";R&&Q(i);const $=n(()=>{if(!R)return;const f=K()?.settleMs??1e3;setTimeout(()=>V(i,a.flowId),f).unref?.()},"settleAndCheck"),F=Y(a);let T=!1,v=typeof s?.once!="function";const j=n(()=>{T&&v&&F()},"endIfOver");if(!v){const f=n(()=>{v=!0,j()},"responseOut");s.once?.("finish",f),s.once?.("close",f)}return new M(f=>{let S;return W.run(a,()=>{S=t.handle().pipe(J({next:n(l=>f.next(l),"next"),complete:n(()=>{C(r.projectName,a,c,m,s.statusCode??200,q),x(a,p,w,q,"ok"),$(),f.complete()},"complete")}),B(l=>{const O=E(l);if(O!==void 0||typeof s.once!="function")C(r.projectName,a,c,m,O??b(s),q,l);else{let D=!1;const N=n(G=>{if(D)return;D=!0;const H=G&&s.statusCode&&s.statusCode>=400?s.statusCode:b(s);C(r.projectName,a,c,m,H,q,l)},"settle");s.once("finish",()=>N(!0)),s.once("close",()=>N(!1))}return x(a,p,w,q,"error"),$(),f.error(l),new M})).subscribe()}),()=>{S?.unsubscribe(),T=!0,j()}})}};k=X([z()],k);export{k as TraceInterceptor};function E(e){const t=e;if(typeof t?.getStatus=="function")return t.getStatus();if(typeof t?.status=="number"&&t.status>=100)return t.status;if(typeof t?.statusCode=="number"&&t.statusCode>=100)return t.statusCode}u(E,"N"),n(E,"ownStatus");const Z=new Set(["TypeError","ReferenceError","SyntaxError","RangeError","EvalError","URIError"]);export function requestOutcome(e,t){return t instanceof Error&&Z.has(t.name)&&E(t)===void 0?"fault":e<400?"ok":e<500?"refused":e===500?"fault":"declared"}u(requestOutcome,"requestOutcome"),n(requestOutcome,"requestOutcome");function _(e){return e==="ok"?"info":e==="refused"?"warning":"error"}u(_,"Y"),n(_,"severityOf");function b(e){return e.statusCode&&e.statusCode>=400?e.statusCode:500}u(b,"_"),n(b,"inferStatus");function I(e){if(typeof e=="string")return e;if(Array.isArray(e)&&typeof e[0]=="string")return e[0]}u(I,"w"),n(I,"stringHeader");function x(e,t,r,d,o){if(!t||!r)return;const s=y(),i=Date.now()-d,a=`${t}.${r}`,c={id:g(),sessionId:e.sessionId,timestamp:Date.now(),source:"nestjs",category:"backend",severity:o==="error"?"error":"info",context:{sessionId:e.sessionId,flowId:e.flowId,requestId:e.requestId},fingerprint:A({kind:"method-call",identity:a}),title:`${a} (${i}ms)${o==="error"?" \u2717":""}`,raw:{kind:"span",span:{name:a,role:"controller",flowId:e.flowId,requestId:e.requestId,startTime:d,endTime:Date.now(),durationMs:i,status:o}}};h(c,y().projectName),s.emit(c)}u(x,"A"),n(x,"emitControllerSpan");function P(e){if(!(e instanceof Error))return{message:String(e)};const t=e.stack?e.stack.split(`
|
|
2
2
|
`).slice(0,30).join(`
|
|
3
|
-
`):void 0;return{...
|
|
3
|
+
`):void 0;return{...e.name?{name:e.name}:{},message:e.message,...t?{stack:t}:{}}}u(P,"Z"),n(P,"errorDetail");function h(e,t){if(!t)return;const r=e.raw;r&&r.project===void 0&&(r.project=t)}u(h,"C"),n(h,"stampProject");function C(e,t,r,d,o,s,i){const a=y(),c=Date.now()-s,m=requestOutcome(o,i),p={id:g(),sessionId:t.sessionId,timestamp:Date.now(),source:"nestjs",category:"backend",severity:_(m),context:{sessionId:t.sessionId,flowId:t.flowId,requestId:t.requestId,...t.parentRequestId?{parentRequestId:t.parentRequestId}:{},originNodeId:t.originNodeId,traceparent:t.traceparent},fingerprint:A({kind:"server-request",identity:`${r}:${d}:${Math.floor(o/100)}xx`}),title:`${r} ${d} \u2192 ${o} (${c}ms)`,message:i instanceof Error?i.message:void 0,raw:{kind:"server-request",request:{method:r,route:d,status:o,outcome:m,durationMs:c,startedAt:s,endedAt:Date.now(),flowId:t.flowId,requestId:t.requestId,...t.parentRequestId?{parentRequestId:t.parentRequestId}:{},...i?{error:P(i)}:{}}}},w={...p,id:g(),raw:{kind:"span",span:{name:`nest.request ${r} ${d}`,traceId:t.traceparent,flowId:t.flowId,requestId:t.requestId,startTime:s,endTime:Date.now(),durationMs:c,status:m==="ok"?"ok":"error"}}};h(p,e),h(w,e),a.emit(p),a.emit(w)}u(C,"E"),n(C,"emitRequest");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lensmcp/nest-instrumentation",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.24.1",
|
|
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.
|
|
18
|
-
"@lensmcp/memory-tracker": "1.
|
|
19
|
-
"@lensmcp/protocol-types": "1.
|
|
17
|
+
"@lensmcp/core": "1.24.1",
|
|
18
|
+
"@lensmcp/memory-tracker": "1.24.1",
|
|
19
|
+
"@lensmcp/protocol-types": "1.24.1",
|
|
20
20
|
"tslib": "^2.3.0"
|
|
21
21
|
},
|
|
22
22
|
"peerDependencies": {
|