@lensmcp/nest-instrumentation 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,160 @@
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
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * `createLensmcpNestApp` — a drop-in for `NestFactory.create(AppModule)`
3
+ * that wires LensMCP with zero edits to the host's `AppModule`.
4
+ *
5
+ * // main.ts
6
+ * import { createLensmcpNestApp } from '@lensmcp/nest-instrumentation';
7
+ * import { AppModule } from './app.module';
8
+ *
9
+ * const app = await createLensmcpNestApp(AppModule, { projectName: 'api' });
10
+ * await app.listen(3100);
11
+ *
12
+ * It builds a wrapper module that imports `AppModule` + `LensmcpModule`,
13
+ * and defaults `trace.autoInstrumentMethods` + `memory.scope: 'all'` on,
14
+ * so every provider's methods + containers are instrumented without any
15
+ * `@TraceMethod` / `@TraceProvider`.
16
+ */
17
+ import { type DynamicModule, type INestApplication, type NestApplicationOptions, type Type } from '@nestjs/common';
18
+ import type { LensmcpModuleOptions } from './types.js';
19
+ export interface CreateLensmcpNestAppOptions extends Partial<LensmcpModuleOptions> {
20
+ /** Forwarded to `NestFactory.create` (logger, cors, etc.). */
21
+ nestOptions?: NestApplicationOptions;
22
+ }
23
+ export declare function createLensmcpNestApp(appModule: Type<unknown> | DynamicModule, options?: CreateLensmcpNestAppOptions): Promise<INestApplication>;
24
+ //# sourceMappingURL=lensmcp-app.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lensmcp-app.d.ts","sourceRoot":"","sources":["../../src/lib/lensmcp-app.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,EAEL,KAAK,aAAa,EAClB,KAAK,gBAAgB,EACrB,KAAK,sBAAsB,EAC3B,KAAK,IAAI,EACV,MAAM,gBAAgB,CAAC;AAGxB,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAEvD,MAAM,WAAW,2BAA4B,SAAQ,OAAO,CAAC,oBAAoB,CAAC;IAChF,8DAA8D;IAC9D,WAAW,CAAC,EAAE,sBAAsB,CAAC;CACtC;AAED,wBAAsB,oBAAoB,CACxC,SAAS,EAAE,IAAI,CAAC,OAAO,CAAC,GAAG,aAAa,EACxC,OAAO,GAAE,2BAAgC,GACxC,OAAO,CAAC,gBAAgB,CAAC,CAsB3B"}
@@ -0,0 +1,43 @@
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
+ }
@@ -0,0 +1,27 @@
1
+ import { type DynamicModule } from '@nestjs/common';
2
+ import type { LensmcpModuleOptions } from './types.js';
3
+ /**
4
+ * `LensmcpModule.forRoot({ projectName: "api", … })` — the host's
5
+ * AppModule imports this to wire LensMCP into a NestJS app.
6
+ *
7
+ * import { Module } from '@nestjs/common';
8
+ * import { LensmcpModule } from '@lensmcp/nest-instrumentation';
9
+ *
10
+ * @Module({
11
+ * imports: [LensmcpModule.forRoot({ projectName: 'api' })],
12
+ * })
13
+ * export class AppModule {}
14
+ *
15
+ * Side effects:
16
+ * • Registers an APP_INTERCEPTOR so every request gets a
17
+ * LensmcpRequestContext + a server-request span event.
18
+ * • Registers a provider tracker that emits `singleton-instance`
19
+ * events on bootstrap + lifecycle.
20
+ * • Calls `setActiveOptions(...)` synchronously so `@TraceMethod`
21
+ * decorators applied to constructor-injected providers find the
22
+ * options at decoration time.
23
+ */
24
+ export declare class LensmcpModule {
25
+ static forRoot(options: LensmcpModuleOptions): DynamicModule;
26
+ }
27
+ //# sourceMappingURL=lensmcp.module.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lensmcp.module.d.ts","sourceRoot":"","sources":["../../src/lib/lensmcp.module.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,aAAa,EAAyB,MAAM,gBAAgB,CAAC;AAO3E,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAEvD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,qBACa,aAAa;IACxB,MAAM,CAAC,OAAO,CAAC,OAAO,EAAE,oBAAoB,GAAG,aAAa;CAoC7D"}
@@ -0,0 +1,70 @@
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 };
@@ -0,0 +1,28 @@
1
+ import { type OnApplicationBootstrap, type OnModuleDestroy } from '@nestjs/common';
2
+ import { ModuleRef } from '@nestjs/core';
3
+ /**
4
+ * On application bootstrap, walks the Nest container and emits one
5
+ * `singleton-instance` event per provider so the `apps/nest` reducer
6
+ * can materialise `nest://providers`. On module destroy (HMR / shutdown)
7
+ * marks each instance as `disposed`.
8
+ *
9
+ * `generation` is the LensMCP module-instance counter (incremented each
10
+ * time `LensmcpModule.forRoot` is constructed), which mirrors a hot
11
+ * reload. Lets the agent ask "is the new gen serving requests, or did
12
+ * an old one stick around?".
13
+ */
14
+ export declare class LensmcpProviderTracker implements OnApplicationBootstrap, OnModuleDestroy {
15
+ private readonly moduleRef;
16
+ private active;
17
+ constructor(moduleRef: ModuleRef);
18
+ onApplicationBootstrap(): void;
19
+ onModuleDestroy(): void;
20
+ /**
21
+ * Handle a provider with no bootstrap instance. Request-scoped and
22
+ * transient providers fall here; value/factory providers (no class
23
+ * `metatype`) are ignored. Wraps the class's shared prototype once and
24
+ * emits a provider event with the real scope.
25
+ */
26
+ private trackScopedProvider;
27
+ }
28
+ //# sourceMappingURL=provider-tracker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider-tracker.d.ts","sourceRoot":"","sources":["../../src/lib/provider-tracker.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,KAAK,sBAAsB,EAAE,KAAK,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAC/F,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAyBzC;;;;;;;;;;GAUG;AACH,qBACa,sBAAuB,YAAW,sBAAsB,EAAE,eAAe;IAGxE,OAAO,CAAC,QAAQ,CAAC,SAAS;IAFtC,OAAO,CAAC,MAAM,CAA6B;gBAEd,SAAS,EAAE,SAAS;IAEjD,sBAAsB,IAAI,IAAI;IA2F9B,eAAe,IAAI,IAAI;IAavB;;;;;OAKG;IACH,OAAO,CAAC,mBAAmB;CAuC5B"}