@lensmcp/nest-instrumentation
v1.13.0
Published
Zero-config NestJS instrumentation for LensMCP — createLensmcpNestApp.
Maintainers
Readme
@lensmcp/nest-instrumentation
Zero-config NestJS instrumentation for LensMCP — the observability lens for coding agents.
This package wires LensMCP into a NestJS application so an agent can see how requests actually flow through it: every controller hop and provider-method call becomes a timed span, each HTTP request becomes a server-request event with method/route/status/duration, repeated DB / Redis / external calls inside one method surface as loop (N+1) warnings, and the live provider graph is materialised from singleton-instance events. All of it is correlated by a per-request flowId / requestId carried across async boundaries via AsyncLocalStorage, so the trace from an inbound request through its services, queries, and queue jobs stays connected.
There are two ways to turn it on, and you only need one:
- Explicit drop-in. Replace
NestFactory.create(AppModule)withcreateLensmcpNestApp(AppModule, …)in yourmain.ts. One import, no edits toAppModule. - Zero-touch graft. Do nothing. When
@lensmcp/node-instrumentationis loaded into the process, it patchesNestFactory.createand graftsLensmcpModule.forRootaround your root module automatically — no import in your source at all. This is how LensMCP instruments apps under its zero-touch runtime.
Both paths produce the same instrumentation; the drop-in is just the graft done by hand.
Install
yarn add @lensmcp/nest-instrumentation@nestjs/common, @nestjs/core (>=10 <12), and rxjs (^7) are peer dependencies — already present in any Nest app.
Usage
In your main.ts, swap NestFactory.create for createLensmcpNestApp:
// main.ts
import { createLensmcpNestApp } from '@lensmcp/nest-instrumentation';
import { AppModule } from './app.module';
const app = await createLensmcpNestApp(AppModule, {
projectName: 'api',
});
await app.listen(3100);That is the whole setup. createLensmcpNestApp builds a wrapper root module that imports both LensmcpModule and your AppModule, then calls NestFactory.create for you — so AppModule is never touched. Because the wrapper lists LensMCP first, the trace interceptor is registered outermost: a user interceptor that short-circuits (e.g. a cache hit) cannot bypass tracing.
The drop-in turns on zero-config defaults: trace.autoInstrumentMethods is true and memory defaults to { mode: 'light', scope: 'all' }, so every provider's methods are traced and every provider's Map / Set / Array fields are watched for growth — with no @TraceMethod or @TraceProvider decorators required. You can override any of these:
const app = await createLensmcpNestApp(AppModule, {
projectName: 'api',
trace: { autoInstrumentMethods: true, db: true },
memory: { mode: 'light', scope: 'tagged' },
nestOptions: { cors: true }, // forwarded to NestFactory.create
});projectName falls back to process.env.LENSMCP_PROJECT (then 'app') if omitted. The event sink is auto-selected from the environment (see How it fits).
Zero-touch alternative
If the process is already running @lensmcp/node-instrumentation, you do not need this import at all. It grafts LensmcpModule.forRoot({ trace: { autoInstrumentMethods: true }, memory: { mode: 'light', scope: 'all' } }) around your root module the moment NestFactory.create is called. An ordinary main.ts —
const app = await NestFactory.create(AppModule);
await app.listen(3100);— comes up fully instrumented with zero lens code in your source. Roots already wrapped by createLensmcpNestApp carry a marker and are left alone, so the two paths never double-wrap.
Manual module wiring (advanced)
If you build your own root module instead of using the drop-in, import LensmcpModule.forRoot(...) directly:
import { Module } from '@nestjs/common';
import { LensmcpModule } from '@lensmcp/nest-instrumentation';
@Module({
imports: [LensmcpModule.forRoot({ projectName: 'api' })],
})
export class AppModule {}Note that forRoot does not default autoInstrumentMethods on — only createLensmcpNestApp and the zero-touch graft do. Imported bare, you opt methods in per-class with @TraceMethod() (or enable trace.autoInstrumentMethods yourself).
What it captures
- Request flows — an
APP_INTERCEPTOR(TraceInterceptor) brackets every HTTP request. It readsx-request-id,x-lensmcp-flow-id,x-lensmcp-origin-node-id, and W3Ctraceparentheaders, opens aLensmcpRequestContextinAsyncLocalStorage, and on completion emits aserver-requestevent (method, route, status, duration) plus a span. Errors are resolved to their real HTTP status (viaHttpException.getStatus()or astatus/statusCodefield) rather than Nest's not-yet-written default. - Controller hops — the interceptor times the controller method itself and emits a
Controller.handler (12ms)span withrole: 'controller', so the trace shows the controller between the request and its services instead of skipping straight to the service. - Provider-method traces — when
trace.autoInstrumentMethodsis on,LensmcpProviderTrackerwalks the Nest container at bootstrap and wraps each provider's own prototype methods with a traced wrapper (in place, preservingthis). Singleton instances are wrapped per-instance; request-scoped / transient providers are wrapped once on the shared prototype so every per-request instance is covered. Lifecycle hooks (onModuleInit,onApplicationBootstrap, etc.), getters/setters, methods already decorated with@TraceMethod, and anything marked@LensmcpIgnoreare skipped. - N+1 / fan-out detection — each traced method snapshots the request's DB / Redis / external counters on entry; if a method issues ≥3 operations of the same class, it emits a
loopwarning (e.g.loop in OrdersService.list (12 DB calls)). DB call counts come from@TraceMethod({ db: true })and from the zero-touch taps in@lensmcp/node-instrumentation(pg, ioredis, fetch/http). - Provider graph & lifecycle — every controller and provider emits a
singleton-instanceevent at bootstrap (with module, scope, and agenerationcounter that increments on eachforRoot/ hot reload) and adisposedevent on module destroy, so the agent can materialise the live provider tree and tell a new generation from a stale one. - Memory — when
memory.mode !== 'off', container fields (Map/Set/Array) on providers are tracked for size and growth via@lensmcp/memory-tracker.scope: 'all'watches every provider;scope: 'tagged'watches only those marked@TraceProvider({ memory: true }). Each request is bracketed as a "flow" so the suspect-leak detector can compare container sizes at request start vs. after it settles.mode: 'deep'additionally enables heap snapshots.
API
createLensmcpNestApp(appModule, options?)
function createLensmcpNestApp(
appModule: Type<unknown> | DynamicModule,
options?: CreateLensmcpNestAppOptions,
): Promise<INestApplication>;Drop-in for NestFactory.create. Returns a standard INestApplication. Applies the zero-config defaults described above.
CreateLensmcpNestAppOptions extends Partial<LensmcpModuleOptions> (below) and adds:
| Option | Type | Description |
| ------------- | ------------------------ | -------------------------------------------------------- |
| nestOptions | NestApplicationOptions | Forwarded to NestFactory.create (logger, CORS, etc.). |
LensmcpModule.forRoot(options)
LensmcpModule.forRoot(options: LensmcpModuleOptions): DynamicModule;Returns a global dynamic module that registers the TraceInterceptor as an APP_INTERCEPTOR and the LensmcpProviderTracker, and (when memory is enabled) configures @lensmcp/memory-tracker. Options resolve synchronously at call time, so @TraceMethod decorators applied to constructor-injected providers find them at decoration time.
LensmcpModuleOptions
| Option | Type | Default | Description |
| ------------- | ----------------------------------------------- | --------------------------------------------- | -------------------------------------------------------------------------------------------------- |
| projectName | string | (required) | Stamped onto every event for resource routing. |
| sessionId | string | process.env.LENSMCP_SESSION_ID or a fresh id | Session id stamped onto every event. |
| emit | EventSink | env-selected (see below) | Override where events go. |
| trace | object (below) | tracing on, db/redis/queues/auto off | Which trace layers to enable. |
| memory | { mode?: 'off' \| 'light' \| 'deep'; scope?: 'tagged' \| 'all' } | { mode: 'off', scope: 'tagged' } | Memory container tracking. (createLensmcpNestApp / zero-touch default this to light / all.) |
trace fields (all booleans): requests, guards, controllers, services default true; db, redis, queues default false; autoInstrumentMethods defaults false for bare forRoot and true for createLensmcpNestApp / the zero-touch graft.
Decorators
@TraceMethod(name?)/@TraceMethod({ name?, db? })— trace a single method; emits a span per call.db: truemarks it a DB query (emits adb-queryevent and bumps the request DB counter).@TraceProvider({ memory? })— opt a provider into memory container tracking whenmemory.scope === 'tagged'.@LensmcpIgnore()— exclude a class (all methods) or a single method from auto-instrumentation.
Low-level building blocks
Re-exported for advanced use and custom integrations:
TraceInterceptor,LensmcpProviderTracker— the interceptor and tracker registered byforRoot.traceWrap(fn, spanName, { db? }),isTraced(fn),isIgnored(target)— the shared method-tracing core.readTraceProvider(ctor)— read@TraceProvidermetadata off a class.LENSMCP_CONTEXT_STORAGE,currentLensmcpContext(),runInLensmcpContext(ctx, fn)— the per-requestAsyncLocalStorage.defaultEventSink(),inMemorySink()— the env-driven sink and an in-memory sink for tests.
Key types
CreateLensmcpNestAppOptions, LensmcpModuleOptions, ResolvedOptions, EventSink, LensmcpRequestContext, TraceMethodOptions, TraceProviderOptions.
How it fits
@lensmcp/nest-instrumentation is the NestJS adapter in the LensMCP stack. It is grafted automatically by @lensmcp/node-instrumentation under the zero-touch runtime — which patches NestFactory.create and dynamically imports this package to call LensmcpModule.forRoot — so host source stays lens-free. Used by hand, the createLensmcpNestApp drop-in does the same wiring with one import.
Every event it emits is a BaseEvent from @lensmcp/protocol-types (kinds: server-request, span, db-query, loop, singleton-instance), shared by every LensMCP source so they reduce into the same resource views. Memory tracking is delegated to @lensmcp/memory-tracker, and ids / fingerprints come from @lensmcp/core.
By default events are routed by defaultEventSink(), which picks a transport from the environment: LENSMCP_EVENT_FILE (append JSONL), LENSMCP_UDS (reconnecting NDJSON over a Unix domain socket), or LENSMCP_IPC_SOCKET (UDP datagrams); with no session configured it falls back to logging only errors to stdout under a [lensmcp] prefix.
Part of LensMCP. Apache-2.0.
