All jobs

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 RushIn 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 with this body:

{
  "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.

The use case this job follows, with its automation and trigger: Hand off from your terminal.

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.