@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 +1 -9
- package/lib/context.js +1 -8
- package/lib/event-sink.js +3 -110
- package/lib/instrument.js +1 -160
- package/lib/lensmcp-app.js +1 -43
- package/lib/lensmcp.module.js +1 -70
- package/lib/provider-tracker.js +1 -348
- package/lib/state.js +1 -44
- package/lib/trace-interceptor.js +1 -194
- package/lib/trace-method.js +1 -31
- package/lib/trace-provider.js +1 -25
- package/lib/types.js +1 -1
- package/package.json +4 -4
package/index.js
CHANGED
|
@@ -1,9 +1 @@
|
|
|
1
|
-
export
|
|
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
|
-
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
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
|
-
|
|
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");
|
package/lib/lensmcp-app.js
CHANGED
|
@@ -1,43 +1 @@
|
|
|
1
|
-
|
|
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");
|
package/lib/lensmcp.module.js
CHANGED
|
@@ -1,70 +1 @@
|
|
|
1
|
-
var
|
|
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};
|
package/lib/provider-tracker.js
CHANGED
|
@@ -1,348 +1 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
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");
|
package/lib/trace-interceptor.js
CHANGED
|
@@ -1,194 +1 @@
|
|
|
1
|
-
|
|
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");
|
package/lib/trace-method.js
CHANGED
|
@@ -1,31 +1 @@
|
|
|
1
|
-
|
|
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");
|
package/lib/trace-provider.js
CHANGED
|
@@ -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.
|
|
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.
|
|
18
|
-
"@lensmcp/memory-tracker": "1.18.
|
|
19
|
-
"@lensmcp/protocol-types": "1.18.
|
|
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": {
|