RabbitMQ message profiling
Profile RabbitMQ messages consumed via @RabbitSubscribe and inspect routing key, payload and timing in the web profiler.
This tutorial shows how to profile RabbitMQ messages consumed with @golevelup/nestjs-rabbitmq. Each consumed message produces a profile - with a Message panel plus any HTTP, cache, or database activity the handler triggered - that appears in a dedicated RabbitMQ view at /_profiler. The last steps cover the other direction: the messages your app publishes.
Prerequisites
@eleven-labs/nest-profilerinstalled and configured@golevelup/nestjs-rabbitmqconsuming messages via@RabbitSubscribe
Step 1 - Install the package
pnpm add @eleven-labs/nest-profiler-rabbitmq@golevelup/nestjs-rabbitmq and amqplib are optional peer dependencies - the ones you already use to consume messages.
Step 2 - Register the module
Add RabbitMqCollectorModule to the application that consumes your messages (the same process that hosts the profiler):
import { Module } from '@nestjs/common';
import { ProfilerModule } from '@eleven-labs/nest-profiler';
import { RabbitMqCollectorModule } from '@eleven-labs/nest-profiler-rabbitmq';
import { ConditionalModule } from '@nestjs/config';
import { ProfilerModule } from '@eleven-labs/nest-profiler';
import { RabbitMQModule } from './rabbitmq.module';
const isProfilerEnabled = (env: NodeJS.ProcessEnv) => env['PROFILER_ENABLED'] === 'true';
@Module({
imports: [
ConditionalModule.registerWhen(ProfilerModule.forRoot({ isGlobal: true }), isProfilerEnabled),
ConditionalModule.registerWhen(RabbitMqCollectorModule.forRoot(), isProfilerEnabled),
RabbitMQModule,
],
})
export class AppModule {}Step 3 - Keep your consumer unchanged
You do not change anything for profiling - write an ordinary @RabbitSubscribe handler:
import { Injectable } from '@nestjs/common';
import { RabbitSubscribe } from '@golevelup/nestjs-rabbitmq';
import type { ConsumeMessage } from 'amqplib';
@Injectable()
export class NarrationService {
@RabbitSubscribe({
exchange: 'articles.events',
routingKey: 'published.*',
queue: 'tts.narration',
})
async createGeneration(message: ArticleEvent, raw: ConsumeMessage): Promise<void> {
// … your business logic …
}
}Step 4 - Inspect the profile
Publish a message (or trigger your normal flow) and open /_profiler, then pick RabbitMQ in the sidebar. Messages get their own view, with a filter bar of their own. Open one to find:
- Message - exchange, routing key, handler, redelivered flag, consumer/delivery tags, headers and payload (the HTTP request/response tabs are hidden)
- HTTP Client, Database, … - whatever the handler triggered
A handler that throws is captured too: its profile is marked failed (status 500) and the thrown error appears in the Exceptions tab.
Try it in the example app
The example API wires this end to end behind feature flags (off by default): creating a review publishes a review.created event that a @RabbitSubscribe consumer reacts to. Start the infrastructure and run it with the flags on:
docker compose up -d mongodb rabbitmq
FEATURE_MONGOOSE=true FEATURE_RABBITMQ=true pnpm example:devThen create a review and open /_profiler to see the consumed message profiled:
curl -X POST http://localhost:3000/api/v1/reviews \
-H 'content-type: application/json' \
-d '{ "productId": "1", "rating": 5, "comment": "Great product!", "authorId": 1 }'Configuration
RabbitMqCollectorModule.forRoot({
captureHeaders: true, // RabbitMQ headers (sensitive ones masked)
captureBody: true, // deserialized payload - disable if payloads are large
maskHeaders: ['x-tenant-secret'], // merged with the built-in mask list
});Gate it per environment with ConditionalModule.registerWhen(RabbitMqCollectorModule.forRoot({ … }), isProfilerEnabled) — the same pattern used for every profiler module.
Driving options from ConfigService
Use forRootAsync() to resolve captureBody/maskHeaders from ConfigService (or any provider), gated per environment with ConditionalModule — the recommended way (for the enabled flag, see Enabling and disabling the profiler):
import { ConditionalModule, ConfigService } from '@nestjs/config';
ConditionalModule.registerWhen(
RabbitMqCollectorModule.forRootAsync({
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
captureBody: config.get<boolean>('profiler.rabbitmqCaptureBody') ?? true,
maskHeaders: config.get<string[]>('profiler.maskHeaders') ?? [],
}),
}),
isProfilerEnabled,
);Profiling what you publish, too
See the whole RabbitMQ surface, not just the traffic
With @eleven-labs/nest-profiler-routes installed, the same
module contributes a Discover / RabbitMQ view: the topology your RabbitMQModule declared —
connections (credentials masked), exchanges, queues with the binding and x-… arguments that feed
them, exchange bindings — then every consumer, each expanding to its full subscription: queue,
exchange, routing keys, connection, queueOptions and error behaviour.
It is read from the resolved configuration, so it also answers the questions traffic cannot: which
dead-letter and retry queues exist, which handler runs on which connection, and whether a handler
referencing a module-level handlers entry is registered at all.
The same package ships RabbitMqPublishCollectorModule, which lists the messages a profiled request published in a RabbitMQ panel. It is independent of the module above — register it on its own in a publish-only API, or alongside it to also see the messages your consumers republish:
import { RabbitMqPublishCollectorModule } from '@eleven-labs/nest-profiler-rabbitmq';
ConditionalModule.registerWhen(RabbitMqPublishCollectorModule.forRoot(), isProfilerEnabled);Nothing changes in your publishers: keep injecting AmqpConnection and calling publish(). Each row shows the exchange, routing key, message properties, masked headers, payload, duration and outcome, tagged slow / n-plus-one / error by the rule engine — publishing the same message once per loop iteration is exactly the N+1 the panel makes visible.
RabbitMqPublishCollectorModule.forRoot({
captureBody: true, // the published message - disable if payloads are large
maskHeaders: ['x-tenant-secret'], // merged with the built-in mask list
slowThreshold: 50, // ms before a publish is tagged `slow`
});How it works
A consumed message has no HTTP request/response, so the module registers an IContextAdapter for the rmq context that creates a fresh profile per message and registers the rabbitmq entrypoint type (the RabbitMQ list view and Message detail tab). The publish collector works the other way around: it patches AmqpConnection.prototype.publish and appends to whatever profile is active. See How it works on the package page.