# Add OpenTelemetry Tracing to a NestJS Service

Yes. An agent adds the OpenTelemetry Node SDK to your NestJS service for HTTP, Express and the database, exports a real trace to a collector, and opens a PR.

[Run this on Rush](https://cloud.getrush.ai/run/add-opentelemetry/nestjs/), in a Project that has your repository.

## What the agent does

The agent clones the repository into its own cloud sandbox and reads how the service boots, because instrumentation has to load before the modules it patches. In a NestJS service like brocoders/nestjs-boilerplate, `package.json` pins `@nestjs/core`, `@nestjs/common` and `@nestjs/platform-express` at 11.1.18, `@nestjs/swagger` 11.2.6, `@nestjs/config` 4.0.3, `typeorm` 0.3.28, `mongoose` 9.4.1 and `nestjs-i18n` 10.6.0, with Jest 30.3.0 and TypeScript 5.9.3; `.nvmrc` and the `Dockerfile` agree on Node 24.14.1. `src/main.ts` starts with `import 'dotenv/config'`, then builds the app with `NestFactory.create(AppModule)`, adds `helmet`, CORS from `app.corsOrigins`, `enableShutdownHooks()`, a global prefix from `app.apiPrefix`, URI versioning, a `ValidationPipe`, two interceptors and Swagger at `/docs`, and listens on `app.port`. A search for `opentelemetry` across `src/`, `package.json` and the compose files finds nothing.

It reads the two shapes the service can take. `src/app.module.ts` picks `MongooseModule.forRootAsync` or `TypeOrmModule.forRootAsync` from `databaseConfig().isDocumentDatabase`, so the same code runs against PostgreSQL through `pg` or against MongoDB, and the instrumentation has to cover both. Configuration follows one pattern throughout: a `registerAs` config file per module under `src/<module>/config/`, validated by `validateConfig` with a class-validator class, and loaded in `ConfigModule.forRoot({ load: [...] })`. `env-example-relational` and `env-example-document` document every variable. `CLAUDE.md` at the root says entities are generated, never hand-written, which the agent respects by touching no entity.

The change is small on purpose. A new `src/instrumentation.ts` builds a `NodeSDK` from `@opentelemetry/sdk-node` with `getNodeAutoInstrumentations()` from `@opentelemetry/auto-instrumentations-node`, which covers `http`, `express`, `pg`, `mongoose` and the `@nestjs/core` instrumentation that names spans after controllers and handlers, and an OTLP HTTP trace exporter. It becomes the first import of `src/main.ts`, ahead of `dotenv/config`, with the reason in a comment: `http` must be patched before Nest or TypeORM load it. The SDK reads the standard variables, `OTEL_SERVICE_NAME` and `OTEL_EXPORTER_OTLP_ENDPOINT`, so no custom config class is needed; the agent adds both to the two `env-example` files and defaults the service name to `APP_NAME`, which is `NestJS API` in the example. `sdk.shutdown()` is wired to the same signals `enableShutdownHooks()` handles so the last spans flush on `SIGTERM`.

To prove it, the agent adds an `otel-collector` service to `docker-compose.yaml` running the collector with a debug exporter that prints spans, starts the stack with the relational env file, calls `POST /api/v1/auth/email/login` and `GET /api/v1/users`, and reads the span tree from the collector's log: the Express request span, the Nest handler span beneath it, and the `pg` query spans beneath that. A Jest test in `src/instrumentation.spec.ts` wires an `InMemorySpanExporter` and asserts a span is produced for an HTTP request to the running Nest app, so the wiring is covered without a collector. It runs `npm run lint`, `npm run test` and `npm run build`, then opens the pull request.

## Start it with one API call

`POST https://api.prix.dev/o/{org}/sessions`

```json
{
  "agent": "claude",
  "input": "Add OpenTelemetry tracing to this NestJS service. Read src/main.ts, src/app.module.ts, one of the src/*/config/*.config.ts files, env-example-relational, env-example-document and docker-compose.yaml first. Add src/instrumentation.ts with the Node SDK, auto-instrumentations for http, express, pg, mongoose and @nestjs/core, and an OTLP HTTP exporter configured by OTEL_SERVICE_NAME and OTEL_EXPORTER_OTLP_ENDPOINT; import it first in src/main.ts and flush on shutdown. Add both variables to the two env-example files. Add an otel-collector service with a debug exporter to docker-compose.yaml, start the stack, make a login and a users request, and keep the collector output showing the request, handler and database spans. Add a Jest test with an InMemorySpanExporter. Do not touch entity files or generators. Run npm run lint, npm run test and npm run build, and open a pull request with the span output.",
  "resources": [
    {
      "url": "https://github.com/your-org/your-app"
    }
  ]
}
```

1. **Get an API key.** In the console, open Settings, then API keys, and create a key for your org. Send it in the Authorization header as a Bearer token.
2. **Send the request.** POST the body in request.json to https://api.prix.dev/o/{org}/sessions, with your repository as the resources URL. The run starts in its own cloud sandbox.
3. **Read what came back.** Follow the run on its live stream or wait for the run.completed webhook, then read the result and its trajectory.

## What comes back

What comes back is a pull request on its own branch that adds `src/instrumentation.ts` and `src/instrumentation.spec.ts`, adds one import line at the top of `src/main.ts` and a shutdown hook, adds the OpenTelemetry packages to `package.json` and `package-lock.json`, adds two variables to each `env-example` file, and adds the `otel-collector` service to `docker-compose.yaml`. No entity, module, controller or generator template changes. The description quotes the collector's debug output for the login and users requests, with the span names and parent relationships visible, and lists the packages added with their versions from the lockfile.

A reviewer checks that the instrumentation import is the first line of `src/main.ts`, before `dotenv/config`, and that the auto-instrumentation list covers the database driver the deployment uses, `pg` for `DATABASE_TYPE=postgres` and `mongoose` for the document variant. They check that with `OTEL_EXPORTER_OTLP_ENDPOINT` unset the service still starts and the exporter fails quietly, which the test exercises, and that `npm run build` output in the session is clean.

The session's transcript keeps the compose start, the two requests, the collector log and the lint, test and build runs, so a reviewer who does not want to start the stack can read the span tree as it was printed.

## Run it on every service without tracing

A script on your server lists the organization's repositories through `GET /o/{org}/github`, checks each `package.json` for an `@opentelemetry/sdk-node` dependency, and sends one `POST /o/{org}/sessions` per service without it, with this prompt and that repository as the `resources` URL. Each run works in its own sandbox and opens its own pull request.

Set a `webhook_url` on the API key and Rush POSTs `run.completed` when the turn finishes, with the `session_url` to read the pull request from; a `run.failed` names the service that needs a person.

## Why run it on Rush?

Adding tracing is a one-line import and a day of checking: did it load before Express, does the database driver show up, do spans flush on shutdown, does the service still start without a collector. The agent answers each question by running the service in its own sandbox with a collector next to it and reading the spans, and your laptop never runs the stack.

The pull request carries the span tree it produced, the test that keeps the wiring covered and the build output, so review is reading rather than reproducing. Several services can be instrumented at once, each in its own sandbox and branch.

## Frequently asked questions

### Will it send traces to my vendor?

Only where `OTEL_EXPORTER_OTLP_ENDPOINT` points. The pull request sets no endpoint; the `env-example` files document the variable, and the compose change points at a local collector with a debug exporter. Point the variable at your vendor's OTLP endpoint and add their auth header through `OTEL_EXPORTER_OTLP_HEADERS`, with no code change.

### This boilerplate runs on PostgreSQL or MongoDB. Which one gets spans?

Both. The auto-instrumentations include `pg`, which TypeORM uses when `DATABASE_TYPE=postgres`, and `mongoose` for the document variant, and the SDK only activates the ones whose modules load. The agent proves the relational path in the sandbox because that is the compose file's default, and says so.

### Why must the import be first in main.ts?

Auto-instrumentation works by patching modules such as `http` and `express` when they are first required. `src/main.ts` currently starts with `import 'dotenv/config'` and then imports Nest, which loads `http`; an instrumentation import placed after that patches nothing. The agent puts it first and leaves a comment so a future import sort does not move it.

### Will tracing slow the service down?

Spans are recorded in memory and exported in batches by the SDK's batch processor, off the request path. The agent sets no sampler, so every request is traced; the pull request names `OTEL_TRACES_SAMPLER` and `OTEL_TRACES_SAMPLER_ARG` as the variables to set a ratio when the volume calls for it, again with no code change.
