# @lensmcp/node-instrumentation

> Zero-touch Node runtime taps for [LensMCP](https://github.com/kiwiapps-ltd/lensmcp) — the observability lens for coding agents.

`@lensmcp/node-instrumentation` instruments a running Node.js service **without touching its source**. It is loaded by the runner via `node --require @lensmcp/node-instrumentation/register`, before your app bundle, so it can patch Node built-ins and external modules (`fs`, `fetch`/`http(s)`, `child_process`, `pg`, `ioredis`, `bullmq`, `@nestjs/core`) and the process streams **in place**. As your service runs, those patches emit LensMCP events onto the bus that the dashboard and MCP server consume.

The whole point is that the host stays completely ordinary. There are no emit helpers, no `createLensmcpNestApp`, no dual-mode bootstrap — your `main.ts` looks exactly like an uninstrumented service. Requiring the module *is* the installation: it must run before the app loads so the require hook sees every external on first `require`/`import`. Every tap is wrapped in `try/catch` and the bus writer fails silently, so instrumentation can never throw into — or break — the host.

## Install

```bash
yarn add @lensmcp/node-instrumentation
```

You normally do not install or import this package yourself. The `@lensmcp/cluster` serve executor injects it into each service pod via `--require` and sets the relevant environment variables. Add it directly only if you are wiring a runner by hand.

## Usage

Activate it by preloading `register` ahead of your entrypoint — this is what does the instrumentation:

```bash
node --require @lensmcp/node-instrumentation/register your-app.js
```

`@lensmcp/cluster` does exactly this for you when it serves a pod, so under the cluster you do nothing.

If you need to install the taps programmatically (e.g. from a custom runner), call the idempotent installer exported from the package root:

```ts
import { installLensmcpNodeInstrumentation } from '@lensmcp/node-instrumentation';

installLensmcpNodeInstrumentation(); // safe to call more than once — guarded by a global marker
```

The package root also re-exports the individual tap functions (`tapFs`, `tapEgress`, `tapExec`, `pgTap`, `redisTap`, `bullmqTap`, `nestTap`, `installChildAppBridge`) and the bus helpers (`lensEmit`, `PROJECT`, `EVENT_FILE`) for advanced use.

## What it taps

Built-ins and globals are patched at install; external modules (`pg`, `ioredis`, `bullmq`, `@nestjs/core`) are patched lazily through a require hook the first time they load.

| Layer | How it's patched | Emits (source / kind) |
|---|---|---|
| Filesystem | `fs` read/write/append/stream (sync, callback, promise) | `fs` / `fs-ops` — a 1s aggregate (read/write counts, bytes, top paths) |
| Network egress | global `fetch` + `http`/`https.request` wrap | `external` / `egress` — outbound method, host, status, duration (internal/loopback/RFC1918 destinations skipped) |
| Child processes | `child_process` spawn/fork/exec/execFile (+ sync forms) | `exec` / `exec` — command, exit code/signal, duration |
| Postgres | require hook → `pg` `Client.prototype.query` | `db` / `db-query` — op, table, query, params, row count, duration |
| Redis | require hook → `ioredis` `Redis.prototype.sendCommand` | `redis` / `redis-op` — op, key, command (BullMQ traffic and housekeeping excluded; rate-capped) |
| Queues | require hook → `bullmq` `Queue`/`Worker` prototypes | `queue` / `queue-enqueued`, `queue-active`, `queue-completed`, `queue-failed`, `queue-process`, plus a 2s `queue-state` snapshot |
| NestJS | require hook → `@nestjs/core` `NestFactory.create` grafts `LensmcpModule` | the events from [`@lensmcp/nest-instrumentation`](https://github.com/kiwiapps-ltd/lensmcp) — class traces, request flows, memory |
| stdout / stderr | `process.stdout`/`stderr` `write` wrap | `nestjs` / `runtime` — one log event per line (original output is preserved) |

Under the cluster devserver (`APP_RUNNER=1`) the NestJS tap also makes a standard `NestFactory.create(...)` + `app.listen(port)` service pod-servable: `listen` is intercepted to `init()` the app and hand its raw request handler to a capture queue instead of binding TCP, with no devserver contract in your `main.ts`.

## Configuration

All configuration is via environment variables (the cluster serve executor sets these for you).

| Variable | Effect |
|---|---|
| `LENSMCP_AUTO=0` | Disable **all** instrumentation — no taps are installed. |
| `LENSMCP_NEST=0` | Keep the other taps but **skip the NestJS graft** (`NestFactory.create` is left untouched). |
| `LENSMCP_LOGS=0` | Disable the stdout/stderr log tap. |
| `LENSMCP_PROJECT` | Service identity stamped on every event. Falls back to `SERVICE_NAME`, then `"app"`. |
| `LENSMCP_EVENT_FILE` | Path to the JSONL event bus. Defaults to `$CWD/.lensmcp/events.jsonl`. |

The stdout/stderr tap and the zero-touch pod bootstrap additionally require `APP_RUNNER=1` (set by the cluster runner); outside a pod they are no-ops so a standalone `--require` run stays quiet.

## How it fits

`@lensmcp/node-instrumentation` is injected by **`@lensmcp/cluster`**, which serves each service as a hot-swappable pod with this module preloaded. The taps write `BaseEvent`-shaped JSON lines to the LensMCP bus, where they are picked up by `@lensmcp/core` and surfaced through the MCP server and dashboard.

It pairs with **`@lensmcp/nest-instrumentation`** (a direct dependency): the NestJS tap grafts that package's `LensmcpModule` onto your root module to add class-level traces, request flows, and memory tracking — again without any import in host code.

---

Part of [LensMCP](https://github.com/kiwiapps-ltd/lensmcp). Apache-2.0.
