@lensmcp/nest-instrumentation 1.18.4 → 1.18.7

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/index.js CHANGED
@@ -1,9 +1 @@
1
- export { LensmcpModule } from './lib/lensmcp.module.js';
2
- export { createLensmcpNestApp, } from './lib/lensmcp-app.js';
3
- export { TraceMethod } from './lib/trace-method.js';
4
- export { LensmcpIgnore, traceWrap, isTraced, isIgnored } from './lib/instrument.js';
5
- export { TraceInterceptor } from './lib/trace-interceptor.js';
6
- export { LensmcpProviderTracker } from './lib/provider-tracker.js';
7
- export { TraceProvider, readTraceProvider, } from './lib/trace-provider.js';
8
- export { LENSMCP_CONTEXT_STORAGE, currentLensmcpContext, runInLensmcpContext, } from './lib/context.js';
9
- export { defaultEventSink, inMemorySink } from './lib/event-sink.js';
1
+ "use strict";export{LensmcpModule}from"./lib/lensmcp.module.js";export{createLensmcpNestApp}from"./lib/lensmcp-app.js";export{TraceMethod}from"./lib/trace-method.js";export{LensmcpIgnore,traceWrap,isTraced,isIgnored}from"./lib/instrument.js";export{TraceInterceptor}from"./lib/trace-interceptor.js";export{LensmcpProviderTracker}from"./lib/provider-tracker.js";export{TraceProvider,readTraceProvider}from"./lib/trace-provider.js";export{LENSMCP_CONTEXT_STORAGE,currentLensmcpContext,runInLensmcpContext}from"./lib/context.js";export{defaultEventSink,inMemorySink}from"./lib/event-sink.js";
package/lib/context.js CHANGED
@@ -1,8 +1 @@
1
- import { AsyncLocalStorage } from 'node:async_hooks';
2
- export const LENSMCP_CONTEXT_STORAGE = new AsyncLocalStorage();
3
- export function currentLensmcpContext() {
4
- return LENSMCP_CONTEXT_STORAGE.getStore();
5
- }
6
- export function runInLensmcpContext(ctx, fn) {
7
- return LENSMCP_CONTEXT_STORAGE.run(ctx, fn);
8
- }
1
+ "use strict";var o=Object.defineProperty;var t=(e,n)=>o(e,"name",{value:n,configurable:!0});var c=Object.defineProperty,r=t((e,n)=>c(e,"name",{value:n,configurable:!0}),"r");import{AsyncLocalStorage as u}from"node:async_hooks";export const LENSMCP_CONTEXT_STORAGE=new u;export function currentLensmcpContext(){return LENSMCP_CONTEXT_STORAGE.getStore()}t(currentLensmcpContext,"currentLensmcpContext"),r(currentLensmcpContext,"currentLensmcpContext");export function runInLensmcpContext(e,n){return LENSMCP_CONTEXT_STORAGE.run(e,n)}t(runInLensmcpContext,"runInLensmcpContext"),r(runInLensmcpContext,"runInLensmcpContext");
package/lib/event-sink.js CHANGED
@@ -1,110 +1,3 @@
1
- import { createSocket } from 'node:dgram';
2
- import { connect } from 'node:net';
3
- import { appendFile } from 'node:fs/promises';
4
- /**
5
- * Resolve an `EventSink` from env vars + caller overrides.
6
- *
7
- * Resolution order:
8
- * 1. `LENSMCP_EVENT_FILE` — append one JSON line per event.
9
- * The Phase 3 e2e smoke uses this (no IPC plumbing needed).
10
- * 2. `LENSMCP_UDS` — Unix domain socket, framed NDJSON, reconnecting.
11
- * The hardened transport (reliable, no datagram size cap).
12
- * 3. `LENSMCP_IPC_SOCKET` — UDP datagram per event (one-shot, lossy).
13
- * 4. fallthrough — write to stdout under a `[lensmcp]` prefix so
14
- * humans can see something useful even without a session.
15
- *
16
- * The UDS client is a compact mirror of `@lensmcp/session`'s `createUdsSink`
17
- * (inlined to keep this app-injected lib free of the session bundle).
18
- */
19
- export function defaultEventSink() {
20
- const filePath = process.env['LENSMCP_EVENT_FILE'];
21
- if (filePath) {
22
- return (event) => {
23
- void appendFile(filePath, JSON.stringify(event) + '\n').catch(() => undefined);
24
- };
25
- }
26
- const udsPath = process.env['LENSMCP_UDS'];
27
- if (udsPath) {
28
- return udsEventSink(udsPath);
29
- }
30
- const socketPath = process.env['LENSMCP_IPC_SOCKET'];
31
- if (socketPath) {
32
- const [host, portStr] = socketPath.split(':');
33
- if (host && portStr) {
34
- const port = Number(portStr);
35
- const sock = createSocket('udp4');
36
- sock.unref();
37
- return (event) => {
38
- try {
39
- const buf = Buffer.from(JSON.stringify(event));
40
- sock.send(buf, port, host, () => undefined);
41
- }
42
- catch {
43
- /* swallow */
44
- }
45
- };
46
- }
47
- }
48
- return (event) => {
49
- if (event.severity === 'error' || event.severity === 'fatal') {
50
- console.warn(`[lensmcp] ${event.severity}: ${event.title}`);
51
- }
52
- };
53
- }
54
- export function inMemorySink() {
55
- const events = [];
56
- return { sink: (e) => events.push(e), events };
57
- }
58
- /** Reconnecting NDJSON client over a Unix domain socket (see uds.ts). */
59
- function udsEventSink(path) {
60
- const queue = [];
61
- let sock = null;
62
- let connecting = false;
63
- const flush = () => {
64
- if (!sock || !sock.writable)
65
- return;
66
- while (queue.length > 0)
67
- sock.write(queue.shift());
68
- };
69
- const open = () => {
70
- if (connecting || sock)
71
- return;
72
- connecting = true;
73
- const s = connect(path);
74
- s.on('connect', () => {
75
- connecting = false;
76
- sock = s;
77
- flush();
78
- });
79
- s.on('drain', flush);
80
- s.on('error', () => {
81
- connecting = false;
82
- sock = null;
83
- });
84
- s.on('close', () => {
85
- sock = null;
86
- });
87
- s.unref();
88
- };
89
- open();
90
- return (event) => {
91
- queue.push(JSON.stringify(event) + '\n');
92
- while (queue.length > 10000)
93
- queue.shift();
94
- if (sock && sock.writable) {
95
- flush();
96
- return;
97
- }
98
- // Dead socket (server restart) — drop it so open() reconnects.
99
- if (sock) {
100
- try {
101
- sock.destroy();
102
- }
103
- catch {
104
- /* ignore */
105
- }
106
- sock = null;
107
- }
108
- open();
109
- };
110
- }
1
+ "use strict";var p=Object.defineProperty;var c=(t,e)=>p(t,"name",{value:e,configurable:!0});var S=Object.defineProperty,s=c((t,e)=>S(t,"name",{value:e,configurable:!0}),"s");import{createSocket as v}from"node:dgram";import{connect as m}from"node:net";import{appendFile as y}from"node:fs/promises";export function defaultEventSink(){const t=process.env.LENSMCP_EVENT_FILE;if(t)return r=>{y(t,JSON.stringify(r)+`
2
+ `).catch(()=>{})};const e=process.env.LENSMCP_UDS;if(e)return u(e);const n=process.env.LENSMCP_IPC_SOCKET;if(n){const[r,i]=n.split(":");if(r&&i){const f=Number(i),o=v("udp4");return o.unref(),l=>{try{const a=Buffer.from(JSON.stringify(l));o.send(a,f,r,()=>{})}catch{}}}}return r=>{(r.severity==="error"||r.severity==="fatal")&&console.warn(`[lensmcp] ${r.severity}: ${r.title}`)}}c(defaultEventSink,"defaultEventSink"),s(defaultEventSink,"defaultEventSink");export function inMemorySink(){const t=[];return{sink:s(e=>t.push(e),"sink"),events:t}}c(inMemorySink,"inMemorySink"),s(inMemorySink,"inMemorySink");function u(t){const e=[];let n=null,r=!1;const i=s(()=>{if(!(!n||!n.writable))for(;e.length>0;)n.write(e.shift())},"flush"),f=s(()=>{if(r||n)return;r=!0;const o=m(t);o.on("connect",()=>{r=!1,n=o,i()}),o.on("drain",i),o.on("error",()=>{r=!1,n=null}),o.on("close",()=>{n=null}),o.unref()},"open");return f(),o=>{for(e.push(JSON.stringify(o)+`
3
+ `);e.length>1e4;)e.shift();if(n&&n.writable){i();return}if(n){try{n.destroy()}catch{}n=null}f()}}c(u,"d"),s(u,"udsEventSink");
package/lib/instrument.js CHANGED
@@ -1,160 +1 @@
1
- import { fingerprint, ulid } from '@lensmcp/core';
2
- import { currentLensmcpContext } from './context.js';
3
- import { activeOptions } from './state.js';
4
- /** Marks a function already wrapped by traceWrap (avoids double-wrap). */
5
- export const LENSMCP_TRACED = Symbol.for('@lensmcp/nest-instrumentation/traced');
6
- /** Marks a class/method opted out of auto-instrumentation. */
7
- export const LENSMCP_IGNORE = Symbol.for('@lensmcp/nest-instrumentation/ignore');
8
- /** `@LensmcpIgnore()` — skip a class (all methods) or a single method. */
9
- export function LensmcpIgnore() {
10
- return ((target, propertyKey) => {
11
- if (propertyKey === undefined) {
12
- target[LENSMCP_IGNORE] = true;
13
- }
14
- else {
15
- const fn = target[propertyKey];
16
- if (typeof fn === 'function') {
17
- fn[LENSMCP_IGNORE] = true;
18
- }
19
- }
20
- });
21
- }
22
- export function isTraced(fn) {
23
- return typeof fn === 'function' && Boolean(fn[LENSMCP_TRACED]);
24
- }
25
- export function isIgnored(target) {
26
- return (!!target &&
27
- (typeof target === 'function' || typeof target === 'object') &&
28
- Boolean(target[LENSMCP_IGNORE]));
29
- }
30
- /**
31
- * Wrap `fn` with span + db-call tracing. Pure: no decorator/Nest
32
- * coupling, so the auto-instrumenter can apply it to any instance method.
33
- */
34
- export function traceWrap(fn, spanName, options = {}) {
35
- if (isTraced(fn))
36
- return fn;
37
- const isDb = options.db ?? false;
38
- const wrapped = function (...args) {
39
- let opts;
40
- try {
41
- opts = activeOptions();
42
- }
43
- catch {
44
- return fn.apply(this, args);
45
- }
46
- if (!opts.trace.services) {
47
- return fn.apply(this, args);
48
- }
49
- const startedAt = Date.now();
50
- const ctx = currentLensmcpContext();
51
- const dbCountAtStart = ctx?.dbCallCount ?? 0;
52
- const redisCountAtStart = ctx?.redisCallCount ?? 0;
53
- const externalCountAtStart = ctx?.externalCallCount ?? 0;
54
- const emitSpan = (status, err) => {
55
- const durationMs = Date.now() - startedAt;
56
- const event = {
57
- id: ulid(),
58
- sessionId: opts.sessionId,
59
- timestamp: Date.now(),
60
- source: 'nestjs',
61
- category: 'backend',
62
- severity: status === 'error' ? 'error' : 'info',
63
- context: { sessionId: opts.sessionId, flowId: ctx?.flowId, requestId: ctx?.requestId },
64
- fingerprint: fingerprint({ kind: 'method-call', identity: spanName }),
65
- title: `${spanName} (${durationMs}ms)${status === 'error' ? ' ✗' : ''}`,
66
- message: err instanceof Error ? err.message : undefined,
67
- raw: {
68
- kind: 'span',
69
- span: {
70
- name: spanName,
71
- flowId: ctx?.flowId,
72
- requestId: ctx?.requestId,
73
- startTime: startedAt,
74
- endTime: Date.now(),
75
- durationMs,
76
- status,
77
- },
78
- },
79
- };
80
- opts.emit(event);
81
- };
82
- const emitDbQuery = () => {
83
- if (ctx)
84
- ctx.dbCallCount = (ctx.dbCallCount ?? 0) + 1;
85
- opts.emit({
86
- id: ulid(),
87
- sessionId: opts.sessionId,
88
- timestamp: Date.now(),
89
- source: 'db',
90
- category: 'db',
91
- severity: 'info',
92
- context: { sessionId: opts.sessionId, flowId: ctx?.flowId, requestId: ctx?.requestId },
93
- fingerprint: fingerprint({ kind: 'db-query', identity: spanName }),
94
- title: `query ${spanName}`,
95
- raw: { kind: 'db-query', query: { signature: spanName } },
96
- });
97
- };
98
- const emitLoopIfNeeded = () => {
99
- if (isDb || !ctx)
100
- return;
101
- const dbDelta = (ctx.dbCallCount ?? 0) - dbCountAtStart;
102
- const redisDelta = (ctx.redisCallCount ?? 0) - redisCountAtStart;
103
- const externalDelta = (ctx.externalCallCount ?? 0) - externalCountAtStart;
104
- // One method issuing the SAME class of operation ≥3 times is the
105
- // N+1 / fan-out signal (a loop of selects, a Promise.all of fetches).
106
- // Two ops is normal shape (ensure + query); three starts the pattern.
107
- if (dbDelta < 3 && redisDelta < 3 && externalDelta < 3)
108
- return;
109
- const durationMs = Date.now() - startedAt;
110
- const parts = [];
111
- if (dbDelta >= 3)
112
- parts.push(`${dbDelta} DB calls`);
113
- if (redisDelta >= 3)
114
- parts.push(`${redisDelta} redis calls`);
115
- if (externalDelta >= 3)
116
- parts.push(`${externalDelta} external calls`);
117
- opts.emit({
118
- id: ulid(),
119
- sessionId: opts.sessionId,
120
- timestamp: Date.now(),
121
- source: 'nestjs',
122
- category: 'backend',
123
- severity: 'warning',
124
- context: { sessionId: opts.sessionId, flowId: ctx.flowId, requestId: ctx.requestId },
125
- fingerprint: fingerprint({ kind: 'loop', identity: spanName }),
126
- title: `loop in ${spanName} (${parts.join(', ')})`,
127
- raw: {
128
- kind: 'loop',
129
- loop: {
130
- iterations: Math.max(dbDelta, redisDelta, externalDelta),
131
- durationMs,
132
- startedAt,
133
- dbCallsInsideLoop: dbDelta,
134
- redisCallsInsideLoop: redisDelta,
135
- externalCallsInsideLoop: externalDelta,
136
- awaitedOperationsInsideLoop: dbDelta + redisDelta + externalDelta,
137
- },
138
- },
139
- });
140
- };
141
- const onOk = () => { if (isDb)
142
- emitDbQuery(); emitSpan('ok'); emitLoopIfNeeded(); };
143
- const onErr = (err) => { if (isDb)
144
- emitDbQuery(); emitSpan('error', err); emitLoopIfNeeded(); };
145
- try {
146
- const result = fn.apply(this, args);
147
- if (result instanceof Promise) {
148
- return result.then((v) => { onOk(); return v; }, (err) => { onErr(err); throw err; });
149
- }
150
- onOk();
151
- return result;
152
- }
153
- catch (err) {
154
- onErr(err);
155
- throw err;
156
- }
157
- };
158
- wrapped[LENSMCP_TRACED] = true;
159
- return wrapped;
160
- }
1
+ "use strict";var L=Object.defineProperty;var d=(e,o)=>L(e,"name",{value:o,configurable:!0});var v=Object.defineProperty,i=d((e,o)=>v(e,"name",{value:o,configurable:!0}),"i");import{fingerprint as m,ulid as y}from"@lensmcp/core";import{currentLensmcpContext as $}from"./context.js";import{activeOptions as E}from"./state.js";export const LENSMCP_TRACED=Symbol.for("@lensmcp/nest-instrumentation/traced"),LENSMCP_IGNORE=Symbol.for("@lensmcp/nest-instrumentation/ignore");export function LensmcpIgnore(){return((e,o)=>{if(o===void 0)e[LENSMCP_IGNORE]=!0;else{const l=e[o];typeof l=="function"&&(l[LENSMCP_IGNORE]=!0)}})}d(LensmcpIgnore,"LensmcpIgnore"),i(LensmcpIgnore,"LensmcpIgnore");export function isTraced(e){return typeof e=="function"&&!!e[LENSMCP_TRACED]}d(isTraced,"isTraced"),i(isTraced,"isTraced");export function isIgnored(e){return!!e&&(typeof e=="function"||typeof e=="object")&&!!e[LENSMCP_IGNORE]}d(isIgnored,"isIgnored"),i(isIgnored,"isIgnored");export function traceWrap(e,o,l={}){if(isTraced(e))return e;const p=l.db??!1,w=i(function(...f){let r;try{r=E()}catch{return e.apply(this,f)}if(!r.trace.services)return e.apply(this,f);const u=Date.now(),n=$(),q=n?.dbCallCount??0,k=n?.redisCallCount??0,D=n?.externalCallCount??0,C=i((t,s)=>{const a=Date.now()-u,I={id:y(),sessionId:r.sessionId,timestamp:Date.now(),source:"nestjs",category:"backend",severity:t==="error"?"error":"info",context:{sessionId:r.sessionId,flowId:n?.flowId,requestId:n?.requestId},fingerprint:m({kind:"method-call",identity:o}),title:`${o} (${a}ms)${t==="error"?" \u2717":""}`,message:s instanceof Error?s.message:void 0,raw:{kind:"span",span:{name:o,flowId:n?.flowId,requestId:n?.requestId,startTime:u,endTime:Date.now(),durationMs:a,status:t}}};r.emit(I)},"emitSpan"),b=i(()=>{n&&(n.dbCallCount=(n.dbCallCount??0)+1),r.emit({id:y(),sessionId:r.sessionId,timestamp:Date.now(),source:"db",category:"db",severity:"info",context:{sessionId:r.sessionId,flowId:n?.flowId,requestId:n?.requestId},fingerprint:m({kind:"db-query",identity:o}),title:`query ${o}`,raw:{kind:"db-query",query:{signature:o}}})},"emitDbQuery"),g=i(()=>{if(p||!n)return;const t=(n.dbCallCount??0)-q,s=(n.redisCallCount??0)-k,a=(n.externalCallCount??0)-D;if(t<3&&s<3&&a<3)return;const I=Date.now()-u,c=[];t>=3&&c.push(`${t} DB calls`),s>=3&&c.push(`${s} redis calls`),a>=3&&c.push(`${a} external calls`),r.emit({id:y(),sessionId:r.sessionId,timestamp:Date.now(),source:"nestjs",category:"backend",severity:"warning",context:{sessionId:r.sessionId,flowId:n.flowId,requestId:n.requestId},fingerprint:m({kind:"loop",identity:o}),title:`loop in ${o} (${c.join(", ")})`,raw:{kind:"loop",loop:{iterations:Math.max(t,s,a),durationMs:I,startedAt:u,dbCallsInsideLoop:t,redisCallsInsideLoop:s,externalCallsInsideLoop:a,awaitedOperationsInsideLoop:t+s+a}}})},"emitLoopIfNeeded"),x=i(()=>{p&&b(),C("ok"),g()},"onOk"),h=i(t=>{p&&b(),C("error",t),g()},"onErr");try{const t=e.apply(this,f);return t instanceof Promise?t.then(s=>(x(),s),s=>{throw h(s),s}):(x(),t)}catch(t){throw h(t),t}},"wrapped");return w[LENSMCP_TRACED]=!0,w}d(traceWrap,"traceWrap"),i(traceWrap,"traceWrap");
@@ -1,43 +1 @@
1
- import { __decorate } from "tslib";
2
- /**
3
- * `createLensmcpNestApp` — a drop-in for `NestFactory.create(AppModule)`
4
- * that wires LensMCP with zero edits to the host's `AppModule`.
5
- *
6
- * // main.ts
7
- * import { createLensmcpNestApp } from '@lensmcp/nest-instrumentation';
8
- * import { AppModule } from './app.module';
9
- *
10
- * const app = await createLensmcpNestApp(AppModule, { projectName: 'api' });
11
- * await app.listen(3100);
12
- *
13
- * It builds a wrapper module that imports `AppModule` + `LensmcpModule`,
14
- * and defaults `trace.autoInstrumentMethods` + `memory.scope: 'all'` on,
15
- * so every provider's methods + containers are instrumented without any
16
- * `@TraceMethod` / `@TraceProvider`.
17
- */
18
- import { Module, } from '@nestjs/common';
19
- import { NestFactory } from '@nestjs/core';
20
- import { LensmcpModule } from './lensmcp.module.js';
21
- export async function createLensmcpNestApp(appModule, options = {}) {
22
- const { nestOptions, projectName, ...rest } = options;
23
- const lensmcp = LensmcpModule.forRoot({
24
- projectName: projectName ?? process.env['LENSMCP_PROJECT'] ?? 'app',
25
- ...rest,
26
- // Zero-config defaults: instrument everything unless the caller
27
- // explicitly narrows it.
28
- trace: { autoInstrumentMethods: true, ...rest.trace },
29
- memory: { mode: 'light', scope: 'all', ...rest.memory },
30
- });
31
- // Lensmcp first → TraceInterceptor outermost (short-circuiting user
32
- // interceptors must not bypass tracing).
33
- let LensmcpRootModule = class LensmcpRootModule {
34
- };
35
- LensmcpRootModule = __decorate([
36
- Module({ imports: [lensmcp, appModule] })
37
- ], LensmcpRootModule);
38
- // Marker for @lensmcp/node-instrumentation's NestFactory.create auto-graft:
39
- // an already-wrapped root must not be wrapped a second time (duplicate
40
- // interceptors → duplicate events).
41
- LensmcpRootModule[Symbol.for('lensmcp.wrappedRoot')] = true;
42
- return NestFactory.create(LensmcpRootModule, nestOptions);
43
- }
1
+ "use strict";var n=Object.defineProperty;var r=(e,t)=>n(e,"name",{value:t,configurable:!0});var i=Object.defineProperty,a=r((e,t)=>i(e,"name",{value:t,configurable:!0}),"m");import{__decorate as l}from"tslib";import{Module as u}from"@nestjs/common";import{NestFactory as d}from"@nestjs/core";import{LensmcpModule as f}from"./lensmcp.module.js";export async function createLensmcpNestApp(e,t={}){const{nestOptions:p,projectName:c,...s}=t,m=f.forRoot({projectName:c??process.env.LENSMCP_PROJECT??"app",...s,trace:{autoInstrumentMethods:!0,...s.trace},memory:{mode:"light",scope:"all",...s.memory}});let o=class{static{r(this,"e")}static{a(this,"LensmcpRootModule")}};return o=l([u({imports:[m,e]})],o),o[Symbol.for("lensmcp.wrappedRoot")]=!0,d.create(o,p)}r(createLensmcpNestApp,"createLensmcpNestApp"),a(createLensmcpNestApp,"createLensmcpNestApp");
@@ -1,70 +1 @@
1
- var LensmcpModule_1;
2
- import { __decorate } from "tslib";
3
- import { Module } from '@nestjs/common';
4
- import { APP_INTERCEPTOR } from '@nestjs/core';
5
- import { configureMemoryTracker } from '@lensmcp/memory-tracker';
6
- import { TraceInterceptor } from './trace-interceptor.js';
7
- import { LensmcpProviderTracker } from './provider-tracker.js';
8
- import { setActiveOptions } from './state.js';
9
- import { currentLensmcpContext } from './context.js';
10
- /**
11
- * `LensmcpModule.forRoot({ projectName: "api", … })` — the host's
12
- * AppModule imports this to wire LensMCP into a NestJS app.
13
- *
14
- * import { Module } from '@nestjs/common';
15
- * import { LensmcpModule } from '@lensmcp/nest-instrumentation';
16
- *
17
- * @Module({
18
- * imports: [LensmcpModule.forRoot({ projectName: 'api' })],
19
- * })
20
- * export class AppModule {}
21
- *
22
- * Side effects:
23
- * • Registers an APP_INTERCEPTOR so every request gets a
24
- * LensmcpRequestContext + a server-request span event.
25
- * • Registers a provider tracker that emits `singleton-instance`
26
- * events on bootstrap + lifecycle.
27
- * • Calls `setActiveOptions(...)` synchronously so `@TraceMethod`
28
- * decorators applied to constructor-injected providers find the
29
- * options at decoration time.
30
- */
31
- let LensmcpModule = LensmcpModule_1 = class LensmcpModule {
32
- static forRoot(options) {
33
- // Resolve options immediately so subsequent imports and decorator
34
- // applications see the populated state singleton.
35
- const resolved = setActiveOptions(options);
36
- // Phase 7: wire the memory tracker to the same sink + the per-request
37
- // ALS context so each container mutation attributes to the active flow.
38
- if (resolved.memory.mode !== 'off') {
39
- configureMemoryTracker({
40
- sessionId: resolved.sessionId,
41
- sink: resolved.emit,
42
- contextGetter: () => {
43
- const ctx = currentLensmcpContext();
44
- if (!ctx)
45
- return undefined;
46
- return {
47
- sessionId: ctx.sessionId,
48
- flowId: ctx.flowId,
49
- requestId: ctx.requestId,
50
- originNodeId: ctx.originNodeId,
51
- };
52
- },
53
- });
54
- }
55
- const providers = [
56
- { provide: APP_INTERCEPTOR, useClass: TraceInterceptor },
57
- LensmcpProviderTracker,
58
- ];
59
- return {
60
- module: LensmcpModule_1,
61
- providers,
62
- exports: [],
63
- global: true,
64
- };
65
- }
66
- };
67
- LensmcpModule = LensmcpModule_1 = __decorate([
68
- Module({})
69
- ], LensmcpModule);
70
- export { LensmcpModule };
1
+ "use strict";var n=Object.defineProperty;var t=(r,e)=>n(r,"name",{value:e,configurable:!0});var a=Object.defineProperty,m=t((r,e)=>a(r,"name",{value:e,configurable:!0}),"t"),s;import{__decorate as c}from"tslib";import{Module as d}from"@nestjs/common";import{APP_INTERCEPTOR as p}from"@nestjs/core";import{configureMemoryTracker as f}from"@lensmcp/memory-tracker";import{TraceInterceptor as u}from"./trace-interceptor.js";import{LensmcpProviderTracker as l}from"./provider-tracker.js";import{setActiveOptions as I}from"./state.js";import{currentLensmcpContext as v}from"./context.js";let i=s=class{static{t(this,"s")}static{m(this,"LensmcpModule")}static forRoot(r){const e=I(r);return e.memory.mode!=="off"&&f({sessionId:e.sessionId,sink:e.emit,contextGetter:m(()=>{const o=v();if(o)return{sessionId:o.sessionId,flowId:o.flowId,requestId:o.requestId,originNodeId:o.originNodeId}},"contextGetter")}),{module:s,providers:[{provide:p,useClass:u},l],exports:[],global:!0}}};i=s=c([d({})],i);export{i as LensmcpModule};
@@ -1,348 +1 @@
1
- import { __decorate, __metadata } from "tslib";
2
- import { Injectable } from '@nestjs/common';
3
- import { ModuleRef } from '@nestjs/core';
4
- import { fingerprint, ulid } from '@lensmcp/core';
5
- import { trackContainer } from '@lensmcp/memory-tracker';
6
- import { activeOptions, currentGeneration } from './state.js';
7
- import { readTraceProvider } from './trace-provider.js';
8
- import { traceWrap, isTraced, isIgnored } from './instrument.js';
9
- // Nest lifecycle hooks + base Object methods we never auto-wrap.
10
- const SKIP_METHODS = new Set([
11
- 'constructor',
12
- 'onModuleInit',
13
- 'onApplicationBootstrap',
14
- 'onModuleDestroy',
15
- 'beforeApplicationShutdown',
16
- 'onApplicationShutdown',
17
- ]);
18
- // Framework NOISE methods — pure in-memory metadata lookups that fire dozens
19
- // of times per request, always at ~0ms, and never represent real work.
20
- // Measured (foodguard, 2026-07-29): 373 of the first 500 events in one
21
- // run-detail page-load flow were `DataSource.findMetadata/getMetadata/
22
- // hasMetadata` + `Reflector.get*` spans. Skipped at WRAP time (not at emit)
23
- // so the suppressed methods also skip the per-call Date.now()/ulid/event
24
- // allocation. Gated by `trace.suppressNoiseMethods` (default true).
25
- // Real work stays traced: `Repository.find`, `EntityManager.find`, driver
26
- // query wraps, and every app-defined provider method are unaffected.
27
- const NOISE_METHODS = {
28
- // TypeORM DataSource — metadata table lookups + structural allocators.
29
- DataSource: new Set([
30
- 'findMetadata', 'getMetadata', 'hasMetadata', 'getRepository',
31
- 'getTreeRepository', 'getMongoRepository', 'createQueryBuilder',
32
- 'createEntityManager', 'createQueryRunner', 'defaultReplicationModeForReads',
33
- ]),
34
- // TypeORM EntityManager — same structural allocators (queries stay traced).
35
- EntityManager: new Set([
36
- 'getRepository', 'getTreeRepository', 'getCustomRepository', 'createQueryBuilder',
37
- ]),
38
- // Nest core Reflector — decorator-metadata reads on every guard/interceptor hop.
39
- Reflector: new Set(['get', 'getAll', 'getAllAndOverride', 'getAllAndMerge']),
40
- };
41
- function isNoiseMethod(cls, name) {
42
- let suppress = true;
43
- try {
44
- suppress = activeOptions().trace.suppressNoiseMethods;
45
- }
46
- catch { /* pre-bootstrap: default on */ }
47
- if (!suppress)
48
- return false;
49
- return NOISE_METHODS[cls]?.has(name) ?? false;
50
- }
51
- // Provider "class" names that are really built-ins (value/factory providers).
52
- const BUILTIN_CLASS = /^(_|Object|String|Number|Array|Promise|Function|RegExp|Map|Set)$/;
53
- // Marks a shared prototype already auto-instrumented, so request-scoped /
54
- // transient providers (instantiated per request) are wrapped exactly once.
55
- const LENSMCP_PROTO_WRAPPED = Symbol.for('@lensmcp/nest-instrumentation/proto-wrapped');
56
- /**
57
- * On application bootstrap, walks the Nest container and emits one
58
- * `singleton-instance` event per provider so the `apps/nest` reducer
59
- * can materialise `nest://providers`. On module destroy (HMR / shutdown)
60
- * marks each instance as `disposed`.
61
- *
62
- * `generation` is the LensMCP module-instance counter (incremented each
63
- * time `LensmcpModule.forRoot` is constructed), which mirrors a hot
64
- * reload. Lets the agent ask "is the new gen serving requests, or did
65
- * an old one stick around?".
66
- */
67
- let LensmcpProviderTracker = class LensmcpProviderTracker {
68
- constructor(moduleRef) {
69
- this.moduleRef = moduleRef;
70
- this.active = new Map(); // logicalId → instanceId
71
- }
72
- onApplicationBootstrap() {
73
- const opts = activeOptions();
74
- const gen = currentGeneration();
75
- // Nest's ModuleRef doesn't expose the full provider list publicly.
76
- // Phase 3 uses a small reflection trick: ModuleRef has an internal
77
- // `container` accessor used by interceptors. We treat it as opaque
78
- // and use the public `get` for known classes — the deeper enumeration
79
- // lands in Phase 3.5 once we ship a `@TraceProvider` decorator.
80
- const internalContainer = this.moduleRef.container;
81
- if (!internalContainer?.getModules)
82
- return;
83
- for (const [, module] of internalContainer.getModules()) {
84
- // Controllers live in their OWN map (module.controllers), not in
85
- // providers — without this loop the trace jumps from server-request
86
- // straight to the service, hiding the controller hop the request
87
- // actually goes through.
88
- const controllers = module?.controllers;
89
- if (controllers && typeof controllers.forEach === 'function') {
90
- controllers.forEach((wrapper) => {
91
- const instance = wrapper?.instance;
92
- if (!instance || typeof instance !== 'object')
93
- return;
94
- const ctor = instance.constructor;
95
- const cls = ctor?.name;
96
- if (!cls || BUILTIN_CLASS.test(cls))
97
- return;
98
- const logicalId = `nest:controller:${cls}`;
99
- const instanceId = `${cls}#gen${gen}#${shortHash(`${cls}:${gen}`)}`;
100
- this.active.set(logicalId, instanceId);
101
- opts.emit(makeProviderEvent({
102
- logicalId,
103
- instanceId,
104
- generation: gen,
105
- module: module?.metatype?.name,
106
- scope: scopeLabel(wrapper?.scope),
107
- lifecycle: 'active',
108
- }));
109
- if (opts.trace.autoInstrumentMethods && ctor) {
110
- autoInstrumentProviderMethods(instance, ctor, cls);
111
- }
112
- });
113
- }
114
- const providers = module?.providers;
115
- if (!providers || typeof providers.forEach !== 'function')
116
- continue;
117
- providers.forEach((wrapper) => {
118
- const instance = wrapper?.instance;
119
- // Request-scoped / transient providers get a fresh instance per
120
- // request/injection. Nest does create a *static-context* instance at
121
- // bootstrap, but wrapping that one instance wouldn't cover the
122
- // per-request ones — so for any non-singleton scope we wrap the
123
- // shared prototype (traceWrap keeps `this`, so every per-request
124
- // instance is traced). Gate on scope, not instance presence.
125
- if (scopeLabel(wrapper?.scope) !== 'SINGLETON') {
126
- this.trackScopedProvider(wrapper, module, gen, opts);
127
- return;
128
- }
129
- if (!instance || typeof instance !== 'object')
130
- return;
131
- const ctor = instance.constructor;
132
- const cls = ctor?.name;
133
- if (!cls || BUILTIN_CLASS.test(cls))
134
- return;
135
- const logicalId = `nest:provider:${cls}`;
136
- const instanceId = `${cls}#gen${gen}#${shortHash(`${cls}:${gen}`)}`;
137
- this.active.set(logicalId, instanceId);
138
- opts.emit(makeProviderEvent({
139
- logicalId,
140
- instanceId,
141
- generation: gen,
142
- module: module?.metatype?.name,
143
- scope: scopeLabel(wrapper?.scope),
144
- lifecycle: 'active',
145
- }));
146
- // Phase 7: memory container tracking. Wrap Map/Set/Array fields
147
- // on tagged providers (or all providers when scope === 'all').
148
- if (opts.memory.mode !== 'off') {
149
- const tag = readTraceProvider(ctor);
150
- const shouldTrack = opts.memory.scope === 'all' || (tag?.memory ?? false);
151
- if (shouldTrack) {
152
- trackProviderContainers(instance, instanceId);
153
- }
154
- }
155
- // Phase 8: zero-config — auto-wrap this provider's methods with
156
- // span + db-call tracing (no @TraceMethod needed). Skips methods
157
- // already wrapped (@TraceMethod) or marked @LensmcpIgnore.
158
- if (opts.trace.autoInstrumentMethods && ctor) {
159
- autoInstrumentProviderMethods(instance, ctor, cls);
160
- }
161
- });
162
- }
163
- }
164
- onModuleDestroy() {
165
- const opts = activeOptions();
166
- const gen = currentGeneration();
167
- for (const [logicalId, instanceId] of this.active) {
168
- opts.emit(makeProviderEvent({
169
- logicalId,
170
- instanceId,
171
- generation: gen,
172
- lifecycle: 'disposed',
173
- }));
174
- }
175
- }
176
- /**
177
- * Handle a provider with no bootstrap instance. Request-scoped and
178
- * transient providers fall here; value/factory providers (no class
179
- * `metatype`) are ignored. Wraps the class's shared prototype once and
180
- * emits a provider event with the real scope.
181
- */
182
- trackScopedProvider(wrapper, module, gen, opts) {
183
- // Prefer the class metatype; fall back to the static-context instance's
184
- // constructor when the wrapper doesn't expose a metatype.
185
- let ctorUnknown;
186
- if (typeof wrapper?.metatype === 'function') {
187
- ctorUnknown = wrapper.metatype;
188
- }
189
- else if (wrapper?.instance && typeof wrapper.instance === 'object') {
190
- ctorUnknown = wrapper.instance.constructor;
191
- }
192
- if (typeof ctorUnknown !== 'function')
193
- return; // value/factory provider
194
- const ctor = ctorUnknown;
195
- if (!ctor.prototype)
196
- return;
197
- const cls = ctor.name;
198
- if (!cls || BUILTIN_CLASS.test(cls))
199
- return;
200
- const scope = scopeLabel(wrapper?.scope);
201
- const logicalId = `nest:provider:${cls}`;
202
- const instanceId = `${cls}#gen${gen}#${scope.toLowerCase()}`;
203
- if (!this.active.has(logicalId))
204
- this.active.set(logicalId, instanceId);
205
- opts.emit(makeProviderEvent({
206
- logicalId,
207
- instanceId,
208
- generation: gen,
209
- module: module?.metatype?.name,
210
- scope,
211
- lifecycle: 'active',
212
- }));
213
- if (opts.trace.autoInstrumentMethods) {
214
- autoInstrumentPrototypeMethods(ctor, cls);
215
- }
216
- }
217
- };
218
- LensmcpProviderTracker = __decorate([
219
- Injectable(),
220
- __metadata("design:paramtypes", [ModuleRef])
221
- ], LensmcpProviderTracker);
222
- export { LensmcpProviderTracker };
223
- /** Normalise Nest's scope (enum number or string) to a stable label. */
224
- function scopeLabel(scope) {
225
- if (scope === 1 || scope === 'TRANSIENT')
226
- return 'TRANSIENT';
227
- if (scope === 2 || scope === 'REQUEST')
228
- return 'REQUEST';
229
- return 'SINGLETON';
230
- }
231
- function makeProviderEvent(p) {
232
- const opts = activeOptions();
233
- return {
234
- id: ulid(),
235
- sessionId: opts.sessionId,
236
- timestamp: Date.now(),
237
- source: 'nestjs',
238
- category: 'backend',
239
- severity: 'info',
240
- context: { sessionId: opts.sessionId },
241
- fingerprint: fingerprint({ kind: 'nest-provider', identity: p.logicalId }),
242
- title: `Nest provider ${p.logicalId} ${p.lifecycle}`,
243
- raw: {
244
- kind: 'singleton-instance',
245
- provider: { ...p, createdAt: Date.now() },
246
- },
247
- };
248
- }
249
- function shortHash(input) {
250
- let h = 5381;
251
- for (let i = 0; i < input.length; i++)
252
- h = ((h << 5) + h + input.charCodeAt(i)) | 0;
253
- return (h >>> 0).toString(16).slice(0, 6);
254
- }
255
- /**
256
- * Scan an instance's own enumerable fields for Map/Set/Array containers
257
- * and wrap them so mutations emit memory-mutation events.
258
- */
259
- function trackProviderContainers(instance, ownerInstanceId) {
260
- for (const fieldName of Object.keys(instance)) {
261
- const value = instance[fieldName];
262
- if (value instanceof Map || value instanceof Set || Array.isArray(value)) {
263
- trackContainer({ ownerInstanceId, fieldName, container: value });
264
- }
265
- }
266
- }
267
- /**
268
- * Replace each of a provider's own prototype methods with a traced
269
- * wrapper, in place on the instance. Wraps on the instance (not the
270
- * shared prototype) so two providers of the same class don't collide and
271
- * `this` stays bound. Skips lifecycle hooks, getters/setters,
272
- * non-functions, already-traced (@TraceMethod), and @LensmcpIgnore.
273
- */
274
- function autoInstrumentProviderMethods(instance, ctor, cls) {
275
- if (isIgnored(ctor))
276
- return;
277
- const proto = Object.getPrototypeOf(instance);
278
- if (!proto || proto === Object.prototype)
279
- return;
280
- for (const name of Object.getOwnPropertyNames(proto)) {
281
- if (SKIP_METHODS.has(name) || isNoiseMethod(cls, name))
282
- continue;
283
- const desc = Object.getOwnPropertyDescriptor(proto, name);
284
- if (!desc || typeof desc.value !== 'function' || desc.get || desc.set)
285
- continue;
286
- const original = desc.value;
287
- if (isTraced(original) || isIgnored(original))
288
- continue;
289
- const wrapped = traceWrap(original, `${cls}.${name}`);
290
- try {
291
- // Define on the instance so we don't mutate the shared prototype.
292
- Object.defineProperty(instance, name, {
293
- value: wrapped,
294
- writable: true,
295
- enumerable: false,
296
- configurable: true,
297
- });
298
- }
299
- catch {
300
- /* read-only / exotic — skip */
301
- }
302
- }
303
- }
304
- /**
305
- * Auto-wrap the methods on a class's shared prototype, in place. Used for
306
- * request-scoped / transient providers, which have no instance at bootstrap
307
- * — every per-request instance resolves these methods from the prototype,
308
- * and `traceWrap` preserves `this`, so each call is traced correctly.
309
- * Marked once on the prototype so HMR generations don't re-wrap.
310
- */
311
- function autoInstrumentPrototypeMethods(metatype, cls) {
312
- if (isIgnored(metatype))
313
- return;
314
- const proto = metatype.prototype;
315
- if (!proto || proto === Object.prototype)
316
- return;
317
- const marker = proto;
318
- if (marker[LENSMCP_PROTO_WRAPPED])
319
- return;
320
- const wrappedNames = [];
321
- for (const name of Object.getOwnPropertyNames(proto)) {
322
- if (SKIP_METHODS.has(name) || isNoiseMethod(cls, name))
323
- continue;
324
- const desc = Object.getOwnPropertyDescriptor(proto, name);
325
- if (!desc || typeof desc.value !== 'function' || desc.get || desc.set)
326
- continue;
327
- const original = desc.value;
328
- if (isTraced(original) || isIgnored(original))
329
- continue;
330
- const wrapped = traceWrap(original, `${cls}.${name}`);
331
- try {
332
- Object.defineProperty(proto, name, {
333
- value: wrapped,
334
- writable: true,
335
- enumerable: false,
336
- configurable: true,
337
- });
338
- wrappedNames.push(name);
339
- }
340
- catch {
341
- /* read-only / exotic — skip */
342
- }
343
- }
344
- marker[LENSMCP_PROTO_WRAPPED] = true;
345
- if (process.env['LENSMCP_DEBUG']) {
346
- console.error(`[lensmcp] proto-wrapped ${cls}: [${wrappedNames.join(', ')}]`);
347
- }
348
- }
1
+ "use strict";var N=Object.defineProperty;var f=(e,t)=>N(e,"name",{value:t,configurable:!0});var T=Object.defineProperty,u=f((e,t)=>T(e,"name",{value:t,configurable:!0}),"l");import{__decorate as k,__metadata as D}from"tslib";import{Injectable as L}from"@nestjs/common";import{ModuleRef as C}from"@nestjs/core";import{fingerprint as _,ulid as B}from"@lensmcp/core";import{trackContainer as Q}from"@lensmcp/memory-tracker";import{activeOptions as g,currentGeneration as O}from"./state.js";import{readTraceProvider as G}from"./trace-provider.js";import{traceWrap as S,isTraced as w,isIgnored as v}from"./instrument.js";const P=new Set(["constructor","onModuleInit","onApplicationBootstrap","onModuleDestroy","beforeApplicationShutdown","onApplicationShutdown"]),U={DataSource:new Set(["findMetadata","getMetadata","hasMetadata","getRepository","getTreeRepository","getMongoRepository","createQueryBuilder","createEntityManager","createQueryRunner","defaultReplicationModeForReads"]),EntityManager:new Set(["getRepository","getTreeRepository","getCustomRepository","createQueryBuilder"]),Reflector:new Set(["get","getAll","getAllAndOverride","getAllAndMerge"])};function h(e,t){let o=!0;try{o=g().trace.suppressNoiseMethods}catch{}return o?U[e]?.has(t)??!1:!1}f(h,"$"),u(h,"isNoiseMethod");const b=/^(_|Object|String|Number|Array|Promise|Function|RegExp|Map|Set)$/,j=Symbol.for("@lensmcp/nest-instrumentation/proto-wrapped");let I=class{static{f(this,"I")}static{u(this,"LensmcpProviderTracker")}constructor(e){this.moduleRef=e,this.active=new Map}onApplicationBootstrap(){const e=g(),t=O(),o=this.moduleRef.container;if(o?.getModules)for(const[,n]of o.getModules()){const c=n?.controllers;c&&typeof c.forEach=="function"&&c.forEach(r=>{const s=r?.instance;if(!s||typeof s!="object")return;const p=s.constructor,a=p?.name;if(!a||b.test(a))return;const l=`nest:controller:${a}`,d=`${a}#gen${t}#${M(`${a}:${t}`)}`;this.active.set(l,d),e.emit(y({logicalId:l,instanceId:d,generation:t,module:n?.metatype?.name,scope:m(r?.scope),lifecycle:"active"})),e.trace.autoInstrumentMethods&&p&&$(s,p,a)});const i=n?.providers;!i||typeof i.forEach!="function"||i.forEach(r=>{const s=r?.instance;if(m(r?.scope)!=="SINGLETON"){this.trackScopedProvider(r,n,t,e);return}if(!s||typeof s!="object")return;const p=s.constructor,a=p?.name;if(!a||b.test(a))return;const l=`nest:provider:${a}`,d=`${a}#gen${t}#${M(`${a}:${t}`)}`;if(this.active.set(l,d),e.emit(y({logicalId:l,instanceId:d,generation:t,module:n?.metatype?.name,scope:m(r?.scope),lifecycle:"active"})),e.memory.mode!=="off"){const A=G(p);(e.memory.scope==="all"||(A?.memory??!1))&&E(s,d)}e.trace.autoInstrumentMethods&&p&&$(s,p,a)})}}onModuleDestroy(){const e=g(),t=O();for(const[o,n]of this.active)e.emit(y({logicalId:o,instanceId:n,generation:t,lifecycle:"disposed"}))}trackScopedProvider(e,t,o,n){let c;if(typeof e?.metatype=="function"?c=e.metatype:e?.instance&&typeof e.instance=="object"&&(c=e.instance.constructor),typeof c!="function")return;const i=c;if(!i.prototype)return;const r=i.name;if(!r||b.test(r))return;const s=m(e?.scope),p=`nest:provider:${r}`,a=`${r}#gen${o}#${s.toLowerCase()}`;this.active.has(p)||this.active.set(p,a),n.emit(y({logicalId:p,instanceId:a,generation:o,module:t?.metatype?.name,scope:s,lifecycle:"active"})),n.trace.autoInstrumentMethods&&R(i,r)}};I=k([L(),D("design:paramtypes",[C])],I);export{I as LensmcpProviderTracker};function m(e){return e===1||e==="TRANSIENT"?"TRANSIENT":e===2||e==="REQUEST"?"REQUEST":"SINGLETON"}f(m,"g"),u(m,"scopeLabel");function y(e){const t=g();return{id:B(),sessionId:t.sessionId,timestamp:Date.now(),source:"nestjs",category:"backend",severity:"info",context:{sessionId:t.sessionId},fingerprint:_({kind:"nest-provider",identity:e.logicalId}),title:`Nest provider ${e.logicalId} ${e.lifecycle}`,raw:{kind:"singleton-instance",provider:{...e,createdAt:Date.now()}}}}f(y,"h"),u(y,"makeProviderEvent");function M(e){let t=5381;for(let o=0;o<e.length;o++)t=(t<<5)+t+e.charCodeAt(o)|0;return(t>>>0).toString(16).slice(0,6)}f(M,"P"),u(M,"shortHash");function E(e,t){for(const o of Object.keys(e)){const n=e[o];(n instanceof Map||n instanceof Set||Array.isArray(n))&&Q({ownerInstanceId:t,fieldName:o,container:n})}}f(E,"Q"),u(E,"trackProviderContainers");function $(e,t,o){if(v(t))return;const n=Object.getPrototypeOf(e);if(!(!n||n===Object.prototype))for(const c of Object.getOwnPropertyNames(n)){if(P.has(c)||h(o,c))continue;const i=Object.getOwnPropertyDescriptor(n,c);if(!i||typeof i.value!="function"||i.get||i.set)continue;const r=i.value;if(w(r)||v(r))continue;const s=S(r,`${o}.${c}`);try{Object.defineProperty(e,c,{value:s,writable:!0,enumerable:!1,configurable:!0})}catch{}}}f($,"N"),u($,"autoInstrumentProviderMethods");function R(e,t){if(v(e))return;const o=e.prototype;if(!o||o===Object.prototype)return;const n=o;if(n[j])return;const c=[];for(const i of Object.getOwnPropertyNames(o)){if(P.has(i)||h(t,i))continue;const r=Object.getOwnPropertyDescriptor(o,i);if(!r||typeof r.value!="function"||r.get||r.set)continue;const s=r.value;if(w(s)||v(s))continue;const p=S(s,`${t}.${i}`);try{Object.defineProperty(o,i,{value:p,writable:!0,enumerable:!1,configurable:!0}),c.push(i)}catch{}}n[j]=!0,process.env.LENSMCP_DEBUG&&console.error(`[lensmcp] proto-wrapped ${t}: [${c.join(", ")}]`)}f(R,"U"),u(R,"autoInstrumentPrototypeMethods");
package/lib/state.js CHANGED
@@ -1,44 +1 @@
1
- import { ulid } from '@lensmcp/core';
2
- import { defaultEventSink } from './event-sink.js';
3
- /**
4
- * Per-process singleton holding the resolved instrumentation options.
5
- * Tied to `LensmcpModule.forRoot()` — Nest constructs the module once
6
- * per app boot, so `setActiveOptions` runs exactly once per generation.
7
- */
8
- let active;
9
- let generation = 0;
10
- export function setActiveOptions(opts) {
11
- const resolved = {
12
- projectName: opts.projectName,
13
- sessionId: opts.sessionId ?? process.env['LENSMCP_SESSION_ID'] ?? ulid(),
14
- emit: opts.emit ?? defaultEventSink(),
15
- trace: {
16
- requests: opts.trace?.requests ?? true,
17
- guards: opts.trace?.guards ?? true,
18
- controllers: opts.trace?.controllers ?? true,
19
- services: opts.trace?.services ?? true,
20
- db: opts.trace?.db ?? false,
21
- redis: opts.trace?.redis ?? false,
22
- queues: opts.trace?.queues ?? false,
23
- autoInstrumentMethods: opts.trace?.autoInstrumentMethods ?? false,
24
- suppressNoiseMethods: opts.trace?.suppressNoiseMethods ?? true,
25
- },
26
- memory: {
27
- mode: opts.memory?.mode ?? 'off',
28
- scope: opts.memory?.scope ?? 'tagged',
29
- },
30
- };
31
- active = resolved;
32
- generation += 1;
33
- return resolved;
34
- }
35
- export function activeOptions() {
36
- if (!active) {
37
- throw new Error('@lensmcp/nest-instrumentation: LensmcpModule.forRoot(...) has not been registered. ' +
38
- 'Did you forget to add it to your AppModule?');
39
- }
40
- return active;
41
- }
42
- export function currentGeneration() {
43
- return generation;
44
- }
1
+ "use strict";var i=Object.defineProperty;var r=(e,t)=>i(e,"name",{value:t,configurable:!0});var c=Object.defineProperty,o=r((e,t)=>c(e,"name",{value:t,configurable:!0}),"t");import{ulid as u}from"@lensmcp/core";import{defaultEventSink as a}from"./event-sink.js";let s,n=0;export function setActiveOptions(e){const t={projectName:e.projectName,sessionId:e.sessionId??process.env.LENSMCP_SESSION_ID??u(),emit:e.emit??a(),trace:{requests:e.trace?.requests??!0,guards:e.trace?.guards??!0,controllers:e.trace?.controllers??!0,services:e.trace?.services??!0,db:e.trace?.db??!1,redis:e.trace?.redis??!1,queues:e.trace?.queues??!1,autoInstrumentMethods:e.trace?.autoInstrumentMethods??!1,suppressNoiseMethods:e.trace?.suppressNoiseMethods??!0},memory:{mode:e.memory?.mode??"off",scope:e.memory?.scope??"tagged"}};return s=t,n+=1,t}r(setActiveOptions,"setActiveOptions"),o(setActiveOptions,"setActiveOptions");export function activeOptions(){if(!s)throw new Error("@lensmcp/nest-instrumentation: LensmcpModule.forRoot(...) has not been registered. Did you forget to add it to your AppModule?");return s}r(activeOptions,"activeOptions"),o(activeOptions,"activeOptions");export function currentGeneration(){return n}r(currentGeneration,"currentGeneration"),o(currentGeneration,"currentGeneration");
@@ -1,194 +1 @@
1
- import { __decorate } from "tslib";
2
- import { Injectable } from '@nestjs/common';
3
- import { Observable } from 'rxjs';
4
- import { catchError, tap } from 'rxjs/operators';
5
- import { fingerprint, ulid } from '@lensmcp/core';
6
- import { activeMemoryOptions, onFlowStart, onFlowSettled } from '@lensmcp/memory-tracker';
7
- import { LENSMCP_CONTEXT_STORAGE } from './context.js';
8
- import { activeOptions } from './state.js';
9
- /**
10
- * Wraps every Nest HTTP request:
11
- * 1. Reads `x-request-id`, `x-lensmcp-flow-id`, `x-lensmcp-origin-node-id`,
12
- * `traceparent` headers from the incoming request.
13
- * 2. Runs the rest of the request inside a new
14
- * `LensmcpRequestContext` so any `@TraceMethod`-decorated service
15
- * call, DB query, or queue job downstream inherits the same flow.
16
- * 3. Emits a `server-request` event on completion with method, route,
17
- * status, and duration.
18
- */
19
- let TraceInterceptor = class TraceInterceptor {
20
- intercept(execCtx, next) {
21
- const opts = activeOptions();
22
- if (!opts.trace.requests)
23
- return next.handle();
24
- const http = execCtx.switchToHttp();
25
- const req = http.getRequest();
26
- const res = http.getResponse();
27
- const requestId = stringHeader(req.headers?.['x-request-id']) ?? ulid();
28
- const ctx = {
29
- sessionId: opts.sessionId,
30
- requestId,
31
- flowId: stringHeader(req.headers?.['x-lensmcp-flow-id']),
32
- originNodeId: stringHeader(req.headers?.['x-lensmcp-origin-node-id']),
33
- traceparent: stringHeader(req.headers?.['traceparent']),
34
- dbCallCount: 0,
35
- };
36
- const method = (req.method ?? 'GET').toUpperCase();
37
- const route = req.route?.path ?? req.url ?? '<unknown>';
38
- // The controller hop, by name. Wrapping controller instances at bootstrap
39
- // is too late — Nest's router captured the raw method reference when the
40
- // routes were mapped — so the interceptor (which brackets the handler)
41
- // is the seam that can see and time the controller call.
42
- const controllerCls = execCtx.getClass?.()?.name;
43
- const handlerName = execCtx.getHandler?.()?.name;
44
- const startedAt = Date.now();
45
- // Phase 7.5: bracket the request as a memory "flow" so the
46
- // suspect-leak detector can compare tracked-container sizes at
47
- // request start vs after it settles. Keyed by requestId.
48
- const memoryOn = opts.memory.mode !== 'off';
49
- if (memoryOn)
50
- onFlowStart(requestId);
51
- const settleAndCheck = () => {
52
- if (!memoryOn)
53
- return;
54
- const settleMs = activeMemoryOptions()?.settleMs ?? 1000;
55
- const t = setTimeout(() => onFlowSettled(requestId), settleMs);
56
- t.unref?.();
57
- };
58
- return new Observable((subscriber) => {
59
- LENSMCP_CONTEXT_STORAGE.run(ctx, () => {
60
- next
61
- .handle()
62
- .pipe(tap({
63
- next: (v) => subscriber.next(v),
64
- complete: () => {
65
- emitRequest(opts.projectName, ctx, method, route, res.statusCode ?? 200, startedAt);
66
- emitControllerSpan(ctx, controllerCls, handlerName, startedAt, 'ok');
67
- settleAndCheck();
68
- subscriber.complete();
69
- },
70
- }), catchError((err) => {
71
- // The exception filter hasn't written the response yet — Nest's
72
- // default (201 for POST) is still on res.statusCode. The thrown
73
- // error knows the real status: HttpException via getStatus(),
74
- // domain errors via the `status`/`statusCode` field convention.
75
- const e = err;
76
- const errStatus = typeof e?.getStatus === 'function' ? e.getStatus()
77
- : typeof e?.status === 'number' && e.status >= 100 ? e.status
78
- : typeof e?.statusCode === 'number' && e.statusCode >= 100 ? e.statusCode
79
- : res.statusCode && res.statusCode >= 400 ? res.statusCode
80
- : 500;
81
- emitRequest(opts.projectName, ctx, method, route, errStatus, startedAt, err);
82
- emitControllerSpan(ctx, controllerCls, handlerName, startedAt, 'error');
83
- settleAndCheck();
84
- subscriber.error(err);
85
- return new Observable();
86
- }))
87
- .subscribe();
88
- });
89
- });
90
- }
91
- };
92
- TraceInterceptor = __decorate([
93
- Injectable()
94
- ], TraceInterceptor);
95
- export { TraceInterceptor };
96
- function stringHeader(v) {
97
- if (typeof v === 'string')
98
- return v;
99
- if (Array.isArray(v) && typeof v[0] === 'string')
100
- return v[0];
101
- return undefined;
102
- }
103
- /** The controller hop as a span — `CompanyController.create (12ms)`. */
104
- function emitControllerSpan(ctx, controllerCls, handlerName, startedAt, status) {
105
- if (!controllerCls || !handlerName)
106
- return;
107
- const opts = activeOptions();
108
- const durationMs = Date.now() - startedAt;
109
- const name = `${controllerCls}.${handlerName}`;
110
- opts.emit({
111
- id: ulid(),
112
- sessionId: ctx.sessionId,
113
- timestamp: Date.now(),
114
- source: 'nestjs',
115
- category: 'backend',
116
- severity: status === 'error' ? 'error' : 'info',
117
- context: { sessionId: ctx.sessionId, flowId: ctx.flowId, requestId: ctx.requestId },
118
- fingerprint: fingerprint({ kind: 'method-call', identity: name }),
119
- title: `${name} (${durationMs}ms)${status === 'error' ? ' ✗' : ''}`,
120
- raw: {
121
- kind: 'span',
122
- span: {
123
- name,
124
- role: 'controller',
125
- flowId: ctx.flowId,
126
- requestId: ctx.requestId,
127
- startTime: startedAt,
128
- endTime: Date.now(),
129
- durationMs,
130
- status,
131
- },
132
- },
133
- });
134
- }
135
- function emitRequest(projectName, ctx, method, route, status, startedAt, err) {
136
- const opts = activeOptions();
137
- const durationMs = Date.now() - startedAt;
138
- const isError = status >= 500 || err !== undefined;
139
- const event = {
140
- id: ulid(),
141
- sessionId: ctx.sessionId,
142
- timestamp: Date.now(),
143
- source: 'nestjs',
144
- category: 'backend',
145
- severity: isError ? 'error' : 'info',
146
- context: {
147
- sessionId: ctx.sessionId,
148
- flowId: ctx.flowId,
149
- requestId: ctx.requestId,
150
- originNodeId: ctx.originNodeId,
151
- traceparent: ctx.traceparent,
152
- },
153
- fingerprint: fingerprint({
154
- kind: 'server-request',
155
- identity: `${method}:${route}:${Math.floor(status / 100)}xx`,
156
- }),
157
- title: `${method} ${route} → ${status} (${durationMs}ms)`,
158
- message: err instanceof Error ? err.message : undefined,
159
- raw: {
160
- kind: 'server-request',
161
- request: {
162
- method,
163
- route,
164
- status,
165
- durationMs,
166
- startedAt,
167
- endedAt: Date.now(),
168
- flowId: ctx.flowId,
169
- requestId: ctx.requestId,
170
- },
171
- },
172
- };
173
- // Also emit a span-shaped event so trace:// picks it up.
174
- const spanEvent = {
175
- ...event,
176
- id: ulid(),
177
- raw: {
178
- kind: 'span',
179
- span: {
180
- name: `nest.request ${method} ${route}`,
181
- traceId: ctx.traceparent,
182
- flowId: ctx.flowId,
183
- requestId: ctx.requestId,
184
- startTime: startedAt,
185
- endTime: Date.now(),
186
- durationMs,
187
- status: isError ? 'error' : 'ok',
188
- },
189
- },
190
- };
191
- void projectName; // reserved for per-project namespacing
192
- opts.emit(event);
193
- opts.emit(spanEvent);
194
- }
1
+ "use strict";var M=Object.defineProperty;var m=(e,t)=>M(e,"name",{value:t,configurable:!0});var N=Object.defineProperty,u=m((e,t)=>N(e,"name",{value:t,configurable:!0}),"p");import{__decorate as S}from"tslib";import{Injectable as j}from"@nestjs/common";import{Observable as k}from"rxjs";import{catchError as A,tap as E}from"rxjs/operators";import{fingerprint as x,ulid as y}from"@lensmcp/core";import{activeMemoryOptions as O,onFlowStart as R,onFlowSettled as _}from"@lensmcp/memory-tracker";import{LENSMCP_CONTEXT_STORAGE as H}from"./context.js";import{activeOptions as h}from"./state.js";let C=class{static{m(this,"$")}static{u(this,"TraceInterceptor")}intercept(e,t){const s=h();if(!s.trace.requests)return t.handle();const n=e.switchToHttp(),r=n.getRequest(),a=n.getResponse(),d=w(r.headers?.["x-request-id"])??y(),o={sessionId:s.sessionId,requestId:d,flowId:w(r.headers?.["x-lensmcp-flow-id"]),originNodeId:w(r.headers?.["x-lensmcp-origin-node-id"]),traceparent:w(r.headers?.traceparent),dbCallCount:0},c=(r.method??"GET").toUpperCase(),p=r.route?.path??r.url??"<unknown>",f=e.getClass?.()?.name,g=e.getHandler?.()?.name,q=Date.now(),v=s.memory.mode!=="off";v&&R(d);const T=u(()=>{if(!v)return;const l=O()?.settleMs??1e3;setTimeout(()=>_(d),l).unref?.()},"settleAndCheck");return new k(l=>{H.run(o,()=>{t.handle().pipe(E({next:u(I=>l.next(I),"next"),complete:u(()=>{b(s.projectName,o,c,p,a.statusCode??200,q),$(o,f,g,q,"ok"),T(),l.complete()},"complete")}),A(I=>{const i=I,D=typeof i?.getStatus=="function"?i.getStatus():typeof i?.status=="number"&&i.status>=100?i.status:typeof i?.statusCode=="number"&&i.statusCode>=100?i.statusCode:a.statusCode&&a.statusCode>=400?a.statusCode:500;return b(s.projectName,o,c,p,D,q,I),$(o,f,g,q,"error"),T(),l.error(I),new k})).subscribe()})})}};C=S([j()],C);export{C as TraceInterceptor};function w(e){if(typeof e=="string")return e;if(Array.isArray(e)&&typeof e[0]=="string")return e[0]}m(w,"q"),u(w,"stringHeader");function $(e,t,s,n,r){if(!t||!s)return;const a=h(),d=Date.now()-n,o=`${t}.${s}`;a.emit({id:y(),sessionId:e.sessionId,timestamp:Date.now(),source:"nestjs",category:"backend",severity:r==="error"?"error":"info",context:{sessionId:e.sessionId,flowId:e.flowId,requestId:e.requestId},fingerprint:x({kind:"method-call",identity:o}),title:`${o} (${d}ms)${r==="error"?" \u2717":""}`,raw:{kind:"span",span:{name:o,role:"controller",flowId:e.flowId,requestId:e.requestId,startTime:n,endTime:Date.now(),durationMs:d,status:r}}})}m($,"E"),u($,"emitControllerSpan");function b(e,t,s,n,r,a,d){const o=h(),c=Date.now()-a,p=r>=500||d!==void 0,f={id:y(),sessionId:t.sessionId,timestamp:Date.now(),source:"nestjs",category:"backend",severity:p?"error":"info",context:{sessionId:t.sessionId,flowId:t.flowId,requestId:t.requestId,originNodeId:t.originNodeId,traceparent:t.traceparent},fingerprint:x({kind:"server-request",identity:`${s}:${n}:${Math.floor(r/100)}xx`}),title:`${s} ${n} \u2192 ${r} (${c}ms)`,message:d instanceof Error?d.message:void 0,raw:{kind:"server-request",request:{method:s,route:n,status:r,durationMs:c,startedAt:a,endedAt:Date.now(),flowId:t.flowId,requestId:t.requestId}}},g={...f,id:y(),raw:{kind:"span",span:{name:`nest.request ${s} ${n}`,traceId:t.traceparent,flowId:t.flowId,requestId:t.requestId,startTime:a,endTime:Date.now(),durationMs:c,status:p?"error":"ok"}}};o.emit(f),o.emit(g)}m(b,"S"),u(b,"emitRequest");
@@ -1,31 +1 @@
1
- import { traceWrap } from './instrument.js';
2
- /**
3
- * `@TraceMethod()` — decorate a service/controller method so every
4
- * invocation emits a `span` event with method name + duration. Inherits
5
- * the request's `flowId`/`requestId` from the `AsyncLocalStorage`
6
- * context set by `TraceInterceptor`.
7
- *
8
- * @Injectable()
9
- * class AuthService {
10
- * @TraceMethod() login(dto) { … }
11
- * @TraceMethod({ db: true }) findUser(id) { … }
12
- * }
13
- *
14
- * Wraps sync + async methods; sync throws / async rejections become
15
- * `status=error` spans. Uses the same `traceWrap` the bootstrap
16
- * auto-instrumenter uses, so a method is never double-wrapped.
17
- */
18
- export function TraceMethod(nameOverrideOrOptions) {
19
- const options = typeof nameOverrideOrOptions === 'string'
20
- ? { name: nameOverrideOrOptions }
21
- : nameOverrideOrOptions ?? {};
22
- return function (target, propertyKey, descriptor) {
23
- const fn = descriptor.value;
24
- if (typeof fn !== 'function')
25
- return descriptor;
26
- const className = target.constructor?.name ?? 'Anonymous';
27
- const spanName = options.name ?? `${className}.${String(propertyKey)}`;
28
- descriptor.value = traceWrap(fn, spanName, { db: options.db });
29
- return descriptor;
30
- };
31
- }
1
+ "use strict";var s=Object.defineProperty;var n=(e,t)=>s(e,"name",{value:t,configurable:!0});var f=Object.defineProperty,m=n((e,t)=>f(e,"name",{value:t,configurable:!0}),"u");import{traceWrap as p}from"./instrument.js";export function TraceMethod(e){const t=typeof e=="string"?{name:e}:e??{};return function(o,c,r){const a=r.value;if(typeof a!="function")return r;const u=o.constructor?.name??"Anonymous",i=t.name??`${u}.${String(c)}`;return r.value=p(a,i,{db:t.db}),r}}n(TraceMethod,"TraceMethod"),m(TraceMethod,"TraceMethod");
@@ -1,25 +1 @@
1
- /**
2
- * `@TraceProvider({ memory: true })` — opt a provider into memory
3
- * container tracking. The provider tracker reads this metadata at
4
- * bootstrap and, for each tagged provider (or all providers when
5
- * `memory.scope === 'all'`), scans the instance's own enumerable
6
- * fields for Map / Set / Array containers and wraps them via
7
- * `trackContainer`.
8
- *
9
- * Implemented without `reflect-metadata` writes beyond a plain
10
- * property flag so it stays decoupled from Nest's metadata system.
11
- */
12
- const TRACE_PROVIDER_FLAG = Symbol.for('@lensmcp/nest-instrumentation/trace-provider');
13
- export function TraceProvider(options = {}) {
14
- return (target) => {
15
- target[TRACE_PROVIDER_FLAG] = {
16
- memory: options.memory ?? true,
17
- };
18
- };
19
- }
20
- export function readTraceProvider(ctor) {
21
- if (!ctor || (typeof ctor !== 'function' && typeof ctor !== 'object')) {
22
- return undefined;
23
- }
24
- return ctor[TRACE_PROVIDER_FLAG];
25
- }
1
+ "use strict";var c=Object.defineProperty;var o=(r,e)=>c(r,"name",{value:e,configurable:!0});var i=Object.defineProperty,t=o((r,e)=>i(r,"name",{value:e,configurable:!0}),"n");const n=Symbol.for("@lensmcp/nest-instrumentation/trace-provider");export function TraceProvider(r={}){return e=>{e[n]={memory:r.memory??!0}}}o(TraceProvider,"TraceProvider"),t(TraceProvider,"TraceProvider");export function readTraceProvider(r){if(!(!r||typeof r!="function"&&typeof r!="object"))return r[n]}o(readTraceProvider,"readTraceProvider"),t(readTraceProvider,"readTraceProvider");
package/lib/types.js CHANGED
@@ -1 +1 @@
1
- export {};
1
+ "use strict";export{};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lensmcp/nest-instrumentation",
3
- "version": "1.18.4",
3
+ "version": "1.18.7",
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.4",
18
- "@lensmcp/memory-tracker": "1.18.4",
19
- "@lensmcp/protocol-types": "1.18.4",
17
+ "@lensmcp/core": "1.18.7",
18
+ "@lensmcp/memory-tracker": "1.18.7",
19
+ "@lensmcp/protocol-types": "1.18.7",
20
20
  "tslib": "^2.3.0"
21
21
  },
22
22
  "peerDependencies": {