NestJS Packages

nest-profiler

Advanced NestJS web profiler module inspired by Symfony's Web Profiler.

@eleven-labs/nest-profiler

@eleven-labs/nest-profiler provides execution profiling for NestJS applications. Each profiled execution receives a unique token, and the collected data (request, response, performance, logs, exceptions, custom collectors) can be inspected at /_profiler/{token}.

Profiler UI — profiles list with filters, HTTP statuses, durations and global panels

Installation

pnpm add @eleven-labs/nest-profiler nestjs-cls

nestjs-cls is a required peer dependency used for per-execution context propagation.

Configuration

app.module.ts
import { Module } from '@nestjs/common';
import { ProfilerModule } from '@eleven-labs/nest-profiler';

@Module({
  imports: [
    ProfilerModule.forRoot({
      isGlobal: true,
      // The host app owns the decision — packages never read process.env.
      enabled: process.env.NODE_ENV !== 'production',
      maxProfiles: 100,
    }),
  ],
})
export class AppModule {}

Async configuration

enabled is a synchronous, top-level bootstrap flag — it is not resolved by useFactory (it must be known before the async factory runs, so the active layer can be skipped at module-build time). Keep it outside the factory:

app.module.ts
ProfilerModule.forRootAsync({
  enabled: process.env.NODE_ENV !== 'production',
  useFactory: (config: ConfigService) => ({
    maxProfiles: config.get('PROFILER_MAX_PROFILES', 100),
  }),
  inject: [ConfigService],
});

Enable log capture

Wrap any existing logger with profilerService.createLogger() so that every log entry is captured in the active request profile. The wrapper is a transparent proxy: it returns the same type as the logger you pass in, captures its level methods, and forwards everything else — so it is logger-agnostic and works with NestJS's ConsoleLogger, nestjs-pino, nest-winston, etc.

main.ts
import { ConsoleLogger } from '@nestjs/common';

const app = await NestFactory.create(AppModule, { bufferLogs: true });

const profilerService = app.get(ProfilerService);

app.useLogger(profilerService.createLogger(new ConsoleLogger('MyApplication')));

Capturing a directly-injected logger

app.useLogger() only routes logs that go through NestJS's Logger. A logger injected directly (e.g. nestjs-pino's PinoLogger) bypasses it — wrap that instance too:

constructor(
  profiler: ProfilerService,
  @InjectPinoLogger(MyService.name) pinoLogger: PinoLogger,
) {
  // pino's own `info()` keeps working AND is now captured into the profile
  this.logger = profiler.createLogger(pinoLogger);
}

The default mapping already knows the common third-party method names (pino's infolog, traceverbose, …). For an exotic logger, pass a custom map:

import { DEFAULT_LOG_METHODS } from '@eleven-labs/nest-profiler';

profiler.createLogger(myLogger, { ...DEFAULT_LOG_METHODS, silly: 'verbose' });

Debug headers

Every non-profiler request receives response headers:

HeaderValue
X-Debug-TokenThe request token (UUID v4)
X-Debug-Token-LinkLink to /_profiler/{token}

Profiler UI endpoints

EndpointDescription
GET /_profilerList of recent profiles (HTML)
GET /_profiler/:tokenProfile detail page (HTML)
GET /_profiler/:token/dataRaw profile data (JSON)

List filters

The profile list supports server-side filtering via query parameters:

GET /_profiler?method=GET&minDuration=100&url=/api
ParameterDescription
methodHTTP method (GET, POST, …)
statusCodeResponse status code
minDurationMinimum duration in ms
maxDurationMaximum duration in ms
urlURL contains this string

Export a profile

Every profile detail page has an Export JSON button. You can also download the raw profile directly:

curl http://localhost:3000/_profiler/{token}/data > profile.json

Securing the UI

Set the PROFILER_TOKEN environment variable to protect /_profiler/* with a Bearer token:

PROFILER_TOKEN=your-secret-token

Then access the profiler with:

curl -H "Authorization: Bearer your-secret-token" http://localhost:3000/_profiler

When PROFILER_TOKEN is not set, the profiler UI is publicly accessible (suitable for local development).

Timeline spans

Instrument any code with startSpan() to capture custom timing data in the Timeline panel:

import { ProfilerService } from '@eleven-labs/nest-profiler';

@Injectable()
export class UserService {
  constructor(private readonly profiler: ProfilerService) {}

  async findAll() {
    const stop = this.profiler.startSpan('db.findAll');
    const users = await this.userRepository.find();
    stop();
    return users;
  }
}

The built-in Timeline collector is always active and displays all spans as a visual bar chart.

Custom collectors

Annotate a provider with @ProfilerCollector() to automatically add a custom data panel to every profile. The collector is auto-discovered via NestJS DiscoveryModule — no manual registration required.

import { Injectable } from '@nestjs/common';
import { ProfilerCollector, IProfilerCollector, Profile } from '@eleven-labs/nest-profiler';
import * as path from 'path';

const MY_ICON = `<svg viewBox="0 0 16 16" fill="currentColor">...</svg>`;

@Injectable()
@ProfilerCollector({
  name: 'myCollector',
  label: 'My Collector',
  icon: MY_ICON,
  priority: 50,
})
export class MyCollector implements IProfilerCollector {
  readonly name = 'myCollector';
  readonly label = 'My Collector';
  readonly icon = MY_ICON;
  readonly priority = 50;

  getBadgeValue(profile: Profile): string | null {
    // Return a value to display as a badge in the toolbar
    return '42';
  }

  getTemplatePath(): string {
    // Optional: path to a custom EJS panel template
    return path.join(__dirname, 'templates', 'my-collector-panel.ejs');
  }

  collect(profile: Profile): unknown {
    // Return any serializable data for this panel
    return { items: [] };
  }
}

Register the collector as a provider in your module — the profiler discovers it automatically at startup.

Custom EJS panel template

When getTemplatePath() is defined, the profiler renders your custom EJS template instead of the default JSON dump. The template receives:

VariableTypeDescription
dataunknownValue returned by collect()
profileProfileThe full request profile
panelCollectorPanelInfoPanel metadata (name, label…)
highlightSql(sql: string) => stringSQL syntax highlighter
toJson(val: unknown) => stringJSON formatter
isoDate(ts: number) => stringISO date formatter
timeOnly(ts: number) => stringTime-only formatter

Custom protocol adapters

The IContextAdapter interface lets you profile any non-HTTP protocol (gRPC, Kafka, WebSockets…) without modifying the core. Implement the interface, register it with the PROFILER_CONTEXT_ADAPTERS multi-token, and ProfilerInterceptor will delegate that context type to your adapter automatically.

import { Injectable } from '@nestjs/common';
import type { ExecutionContext } from '@nestjs/common';
import { PROFILER_CONTEXT_ADAPTERS, PROFILER_REQ_KEY } from '@eleven-labs/nest-profiler';
import type { IContextAdapter, Profile } from '@eleven-labs/nest-profiler';

@Injectable()
export class GrpcContextAdapter implements IContextAdapter {
  readonly contextType = 'rpc';

  recoverProfile(ctx: ExecutionContext): Profile | null {
    const [metadata] = ctx.getArgs();
    return ((metadata as Record<symbol, unknown>)?.[PROFILER_REQ_KEY] as Profile) ?? null;
  }

  enrichProfile(profile: Profile, _ctx: ExecutionContext): void {
    // add protocol-specific data to profile.request
  }
}

// Register in a dedicated module:
@Module({
  providers: [
    GrpcContextAdapter,
    { provide: PROFILER_CONTEXT_ADAPTERS, useExisting: GrpcContextAdapter, multi: true },
  ],
})
export class GrpcProfilerModule {}

@eleven-labs/nest-profiler-graphql is the reference implementation of this pattern for GraphQL (Apollo, Mercurius, graphql-yoga).

Storage backends

Three options are available, controlled by storageType or storage.

Memory (default)

Profiles are kept in an in-memory LRU map and are lost on restart.

ProfilerModule.forRoot({
  storageType: 'memory', // default — no need to specify
  maxProfiles: 100,
  ttl: 3600,
});

File system

Profiles are stored as individual JSON files and survive restarts. Inspired by Symfony's file profiler.

ProfilerModule.forRoot({
  storageType: 'file',
  storagePath: '.profiler', // relative to cwd, default: '.profiler'
  maxProfiles: 200,
  ttl: 86400, // 24h
});

Each profile is written to {storagePath}/{token}.json. The directory is created automatically. Add .profiler/ to .gitignore.

The in-memory index is reconstructed from disk on startup — expired profiles are cleaned up automatically.

Custom adapter

Implement IProfilerStorageAdapter to plug in any backend (Redis, database, …):

import type {
  IProfilerStorageAdapter,
  StorageFindOptions,
  Profile,
} from '@eleven-labs/nest-profiler';

export class RedisStorageAdapter implements IProfilerStorageAdapter {
  async save(profile: Profile): Promise<void> {
    /* ... */
  }
  async findAll(options?: StorageFindOptions): Promise<Profile[]> {
    /* ... */
  }
  async findOne(token: string): Promise<Profile | undefined> {
    /* ... */
  }
  async clear(): Promise<void> {
    /* ... */
  }
}

ProfilerModule.forRoot({
  storage: new RedisStorageAdapter(redisClient), // takes precedence over storageType
});

Public API

import {
  ProfilerModule,
  ProfilerService,
  ProfilerStorageService,
  ProfilerViewsSetup,
  CollectorRegistry,
  ProfilerCollector,
  TimelineCollector,
  PROFILER_STORAGE_ADAPTER,
  MemoryStorageAdapter,
  FileStorageAdapter,
} from '@eleven-labs/nest-profiler';

import type {
  ProfilerModuleOptions,
  ProfilerModuleAsyncOptions,
  IProfilerCollector,
  IProfilerStorageAdapter,
  StorageFindOptions,
  CollectorPanelInfo,
  Profile,
  LogEntry,
  ExceptionEntry,
  TimelineSpan,
  EventEntry,
  SecurityContext,
} from '@eleven-labs/nest-profiler';

Options

OptionTypeDefaultDescription
enabledbooleantrueEnable or disable the profiler.
pathstring/_profilerBase path for the profiler UI.
maxProfilesnumber100Maximum profiles kept (LRU eviction).
ttlnumber3600Profile time-to-live in seconds.
isGlobalbooleanfalseRegister the module as a global NestJS module.
storageType'memory' | 'file''memory'Built-in storage backend.
storagePathstring.profilerDirectory for file storage (relative or absolute).
storageIProfilerStorageAdapterCustom adapter — takes precedence over storageType.
collectBodybooleanfalseCapture request/response bodies (use with caution).
collectorTimeoutnumber1000Max ms a single collector may run before it is abandoned (0 disables).
sampleRatenumber1.0Fraction of requests to profile (0.0–1.0).
ignorePaths(string | RegExp)[][]Paths to skip profiling (prefix string or RegExp).

On this page