Skip to content

Commit 96ddaf2

Browse files
Merge pull request #7 from rakibulislam8226/pre-master
Pre master
2 parents 9b2ad6f + 8674968 commit 96ddaf2

26 files changed

Lines changed: 2207 additions & 73 deletions

CHANGELOG.md

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,41 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1313
1414
## [Unreleased]
1515

16+
## [0.2.0] - 2026-07-23
17+
1618
### Added
19+
- **Automatic background-job capture.** Background work is now captured like HTTP
20+
requests and socket events, with **zero extra setup**`DebugModule.forRoot()`
21+
is all you need. Each run becomes its own profile with the SQL/Redis/HTTP it
22+
makes (N+1 detection, timeline and all), plus the queue, job name, id, attempt,
23+
payload (redacted) and return value, surfaced in a new **Jobs** monitor.
24+
Auto-detected across the DI container for **`@nestjs/bullmq`** (`@Processor` /
25+
`WorkerHost`), **`@nestjs/microservices`** consumers (`@MessagePattern` /
26+
`@EventPattern`), **`@nestjs/schedule`** (`@Cron`/`@Interval`/`@Timeout`), and
27+
DI-provided **bee-queue / Agenda** (best-effort). Turn it off with `jobs: false`,
28+
skip a processor with `@DebugIgnore()`, or drop payloads with
29+
`captureJobData: false`. For workers outside Nest's DI (or legacy `@nestjs/bull`)
30+
the exported `@TrackJob()` decorator and `trackJob()` helper capture a handler in
31+
one line. No new dependency is added.
32+
- **Shared Redis storage for multi-process apps — via `NEST_DEBUG_PANEL_STORAGE`.**
33+
When your API and BullMQ worker run as separate processes, each only sees its
34+
own in-memory captures, so the API's panel can't show the worker's jobs. Set
35+
`NEST_DEBUG_PANEL_STORAGE=redis` in both processes and a single panel shows
36+
everything — requests, socket events and the worker's jobs — with **no code
37+
change** (storage defaults to in-memory). It connects via
38+
`NEST_DEBUG_PANEL_REDIS_URL` (or `REDIS_URL`, or discrete
39+
`REDIS_HOST`/`REDIS_PORT`/`REDIS_PASSWORD`/`REDIS_DB`), reusing the Redis BullMQ
40+
already needs, and **falls back to in-memory with a warning** if Redis is unavailable —
41+
never breaking boot. Advanced: a `RedisStorage` driver is also exported for
42+
code-level control (pass an existing ioredis `client`, or a `url`; configurable
43+
`prefix`, `maxRequests`, `ttlSeconds`), and any custom `DebugStorage` instance
44+
passed to `forRoot()` still takes precedence over the env var.
45+
- **SQL formatting in the dashboard.** Click a SQL row in the request **Timeline**
46+
to expand the full statement, pretty-printed. The **SQL** tab gains a
47+
Pretty / Compact / Raw format selector that reformats every captured query
48+
in place.
49+
- **Broader npm keywords** so the package surfaces for more NestJS
50+
debugging/profiling/queue searches.
1751
- **`NEST_DEBUG_PANEL_ENABLED` environment variable.** Toggle the panel on or off
1852
straight from the environment — no code change. When set to a recognized
1953
boolean (`true`/`1`/`yes`/`on` or `false`/`0`/`no`/`off`, case-insensitive) it
@@ -59,4 +93,5 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
5993
- Auto-instrumentation only warns that raw SQL is unavailable when neither a
6094
driver adapter nor query-event logging is present.
6195

62-
[Unreleased]: https://github.com/rakibulislam8226/nest-debug-panel/compare/v0.1.7...HEAD
96+
[Unreleased]: https://github.com/rakibulislam8226/nest-debug-panel/compare/v0.2.0...HEAD
97+
[0.2.0]: https://github.com/rakibulislam8226/nest-debug-panel/compare/v0.1.7...v0.2.0

README.md

Lines changed: 76 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ On top of that:
7171
- **Memory**: heap and RSS deltas per request, event-loop delay
7272
- **A timeline** that lays all of the above in order, plus your own custom marks
7373
- **Socket.io events** — inbound `@SubscribeMessage` handlers captured like a request, with all the SQL/Redis/HTTP they run (see below)
74+
- **Background jobs** — BullMQ processors, microservice consumers and scheduled tasks captured like a request, with everything they run (see below)
7475

7576
## Socket.io events
7677

@@ -82,6 +83,75 @@ Socket events appear in the **same list** as HTTP requests, tagged with a `WS` b
8283

8384
> Capture is on by default. For rare cases the automatic attachment can't reach (e.g. a single handler, or a gateway created outside the module scan), the `@TrackSocketEvents()` decorator is exported as an explicit opt-in — normally you won't need it.
8485
86+
## Background jobs
87+
88+
Background work is captured the same way as requests — no annotations, no per-queue wiring. Each run becomes its own profile with everything it did (SQL/Redis/HTTP, N+1, timeline, exceptions), plus the queue, job name, id, attempt, payload (redacted) and return value. Runs appear in the **Jobs** monitor.
89+
90+
**Setup depends on your process layout:**
91+
92+
- **Jobs run in the same process as the panel** (e.g. everything under one `npm start`): nothing to do — `DebugModule.forRoot()` is all you need, and jobs show up right next to requests and sockets.
93+
- **Jobs run in a separate worker process** (the common production layout — an API process and a dedicated worker, e.g. a `worker.module.ts` bootstrapped by its own `main.ts`/`worker.ts`): a NestJS module tree is only instrumented in the process that actually loads it, so **`DebugModule.forRoot()` must be imported in the worker's own root module too** — not just the API's. Adding it there does not require an HTTP server; a standalone `NestFactory.createApplicationContext(WorkerModule)` bootstrap works fine. Once both processes load it, set `NEST_DEBUG_PANEL_STORAGE=redis` in **both** and one panel shows requests, sockets and jobs together — one env var on top of the import, using the Redis you already run for BullMQ (full setup just below in [Multi-process apps](#multi-process-apps-api--worker)).
94+
95+
What's captured automatically, with **zero extra code**:
96+
97+
| Library / pattern | Auto | How |
98+
| --- | --- | --- |
99+
| **`@nestjs/bullmq`** (`@Processor` + `WorkerHost`) | ✅ always | the processor class is wrapped at startup |
100+
| **`@nestjs/microservices`** (`@MessagePattern` / `@EventPattern`) | ✅ always | consumers already flow through the interceptor |
101+
| **`@nestjs/schedule`** (`@Cron` / `@Interval` / `@Timeout`) | ✅ always | decorated methods are wrapped at startup |
102+
| **bee-queue / Agenda** (registered as a DI provider) | ✅ best-effort | the queue's registrar is wrapped so your handler is traced |
103+
104+
Turn it off with `jobs: false`, skip one processor with `@DebugIgnore()`, or drop payloads with `captureJobData: false`.
105+
106+
**The only case needing a line of code** is a worker created entirely outside Nest's DI (e.g. a bare `new Worker(...)`), or legacy `@nestjs/bull`. Annotate the handler with `@TrackJob()`, or wrap it with `trackJob()`:
107+
108+
```ts
109+
import { TrackJob, trackJob } from 'nest-debug-panel';
110+
111+
// As a decorator on any method that handles a job:
112+
@TrackJob({ queue: 'emails', jobName: 'welcome' })
113+
async handle(job: Job) { /* ... */ }
114+
115+
// Or functionally, around any handler:
116+
new Worker('emails', (job) =>
117+
trackJob({ library: 'bullmq', queue: 'emails', jobName: job.name }, () => doWork(job), job.data),
118+
);
119+
```
120+
121+
## Multi-process apps (API + worker)
122+
123+
Most production NestJS apps run the HTTP API and the BullMQ worker as **two separate processes** (e.g. `src/main.ts` bootstrapping `AppModule`, and a separate `src/worker.ts` bootstrapping `WorkerModule`). Getting this right needs **two things**, and missing either one is the most common reason the Jobs list looks empty:
124+
125+
**1. Import `DebugModule.forRoot()` in *every* process's own root module — including the worker's.** The panel only instruments the module tree it's actually loaded into. If your worker's root module (e.g. `WorkerModule`) never imports it, nothing in that process is ever wrapped, no matter what else you configure. This is true even when the worker has no HTTP server — `DebugModule.forRoot()` works the same inside a standalone `NestFactory.createApplicationContext(...)` bootstrap:
126+
127+
```ts
128+
// worker.module.ts — the worker's OWN root module, separate from AppModule
129+
import { Module } from '@nestjs/common';
130+
import { DebugModule } from 'nest-debug-panel';
131+
132+
@Module({
133+
imports: [
134+
DebugModule.forRoot(),
135+
// ...your BullMQ / processor modules
136+
],
137+
})
138+
export class WorkerModule {}
139+
```
140+
141+
**2. Point both processes at the same Redis**, so the API's panel can see what the worker captured — each process still has its own in-memory store by default, and in-memory is never shared across processes. Set the same environment variable in **both**:
142+
143+
```bash
144+
NEST_DEBUG_PANEL_STORAGE=redis # shared Redis store (reads NEST_DEBUG_PANEL_REDIS_URL, or REDIS_URL)
145+
# (unset, or =memory) # default: in-memory, per-process
146+
```
147+
148+
You already run Redis for BullMQ, so there's nothing new to install. With both pieces in place — the import in the worker's module, and the env var in both processes — one panel shows requests, sockets **and** the worker's jobs together.
149+
150+
- The connection comes from `NEST_DEBUG_PANEL_REDIS_URL` or `REDIS_URL`; if you configure Redis in parts instead, it's built from `REDIS_HOST` / `REDIS_PORT` / `REDIS_PASSWORD` / `REDIS_DB` — so most apps need no new variable at all.
151+
- **Fail-safe:** if Redis or the URL is missing, it falls back to in-memory with a warning — it never breaks boot.
152+
- The **worker doesn't need to serve HTTP** — it just writes captures to Redis; the API process serves the panel and reads them back.
153+
- **Single-process app** (everything under one `npm start`)? You don't need any of this — the default in-memory store already shows jobs alongside requests.
154+
85155
## Database, Redis and HTTP capture is automatic
86156

87157
At startup the panel scans your app's providers and instruments what it recognizes. In most projects you install the package and queries just show up:
@@ -116,6 +186,8 @@ DebugModule.forRoot({
116186
captureHttp: true,
117187
captureLogs: true, // capture console.* emitted during a request (Logs monitor)
118188
sockets: true, // capture socket.io gateway events (automatic, no per-gateway setup)
189+
jobs: true, // capture background jobs / messages / scheduled runs (automatic)
190+
captureJobData: true, // capture the job payload (job.data), redacted
119191
autoInstrument: true, // scan providers and hook them automatically
120192
slowQueryThreshold: 100, // ms; queries at or above get flagged
121193
slowRequestThreshold: 500, // ms; requests at or above get flagged
@@ -127,7 +199,7 @@ DebugModule.forRoot({
127199
maxBodyLength: 65536, // bytes kept per captured body
128200
getUser: (req) => (req as any).user,
129201
authorize: (req) => true, // gate the dashboard, e.g. admins only
130-
storage: undefined, // custom storage driver; default is in-memory
202+
storage: undefined, // custom DebugStorage instance; mode is set via NEST_DEBUG_PANEL_STORAGE
131203
plugins: [],
132204
});
133205
```
@@ -294,14 +366,10 @@ Build your own frontend or tooling on top of it if you like.
294366

295367
## Storage
296368

297-
The default store is an in-memory ring buffer that keeps the latest `maxRequests` profiles. Need persistence or sharing across instances? Implement the five-method `DebugStorage` interface and pass it in:
369+
The default store is an in-memory ring buffer that keeps the latest `maxRequests` profiles — perfect for a single-process app.
298370

299-
```ts
300-
class RedisStorage implements DebugStorage {
301-
save(profile) { ... } find(id) { ... } list() { ... } clear() { ... } count() { ... }
302-
}
303-
DebugModule.forRoot({ storage: new RedisStorage(client) });
304-
```
371+
- **Running an API + worker as separate processes?** Set `NEST_DEBUG_PANEL_STORAGE=redis` so one panel shows everything — see [Multi-process apps (API + worker)](#multi-process-apps-api--worker).
372+
- **Bringing your own driver?** Implement the five-method `DebugStorage` interface (`save`/`find`/`list`/`clear`/`count`) and pass it as `storage` — a database or file store works the same way.
305373

306374
## Example app
307375

package.json

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "nest-debug-panel",
3-
"version": "0.1.5",
3+
"version": "0.2.0",
44
"description": "Debug panel for NestJS — request inspector & profiler with a built-in dashboard: SQL queries, N+1 detection, Redis, HTTP calls, exceptions, memory and a per-request timeline.",
55
"license": "MIT",
66
"author": "Rakibul Islam <rakibulislam8226@gmail.com>",
@@ -28,21 +28,42 @@
2828
},
2929
"keywords": [
3030
"nestjs",
31+
"nest",
3132
"debug",
3233
"debug-panel",
34+
"nestjs-debug",
35+
"nestjs-devtools",
36+
"nestjs-profiler",
3337
"profiler",
3438
"profiling",
3539
"request-inspector",
40+
"request-profiler",
41+
"telescope",
42+
"laravel-telescope",
3643
"dashboard",
3744
"devtools",
45+
"apm",
3846
"sql",
47+
"sql-logger",
48+
"query",
3949
"n+1",
50+
"n-plus-one",
51+
"slow-query",
4052
"prisma",
4153
"typeorm",
4254
"mongoose",
55+
"drizzle",
56+
"knex",
57+
"sequelize",
4358
"redis",
59+
"http",
60+
"websocket",
61+
"socket.io",
62+
"express",
63+
"fastify",
4464
"observability",
45-
"monitoring"
65+
"monitoring",
66+
"performance"
4667
],
4768
"peerDependencies": {
4869
"@nestjs/common": "^9.0.0 || ^10.0.0 || ^11.0.0",

src/api/debug.controller.ts

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Controller, Delete, Get, Inject, Param, Query, Req, Res, UseGuards } from '@nestjs/common';
1+
import { Controller, Delete, Get, Inject, Logger, Param, Query, Req, Res, UseGuards } from '@nestjs/common';
22
import { DEBUG_OPTIONS, DEBUG_STORAGE, DEFAULT_ROUTE_PREFIX } from '../constants';
33
import type { ResolvedDebugOptions } from '../config/debug-options';
44
import type { DebugStorage } from '../interfaces/storage.interface';
@@ -50,11 +50,38 @@ interface AdapterResponse {
5050
@UseGuards(DebugAccessGuard)
5151
@Controller(DEFAULT_ROUTE_PREFIX)
5252
export class DebugController {
53+
private readonly logger = new Logger('NestDebugPanel');
54+
5355
constructor(
5456
@Inject(DEBUG_OPTIONS) private readonly options: ResolvedDebugOptions,
5557
@Inject(DEBUG_STORAGE) private readonly storage: DebugStorage,
5658
) {}
5759

60+
/**
61+
* Viewing the panel must never 500 just because the storage backend (e.g. a
62+
* Redis blip) is momentarily unreachable — that's a hiccup in the dashboard,
63+
* never in the host app. Reads degrade to an empty/absent result with a
64+
* warning; the poll simply shows nothing this tick and recovers on its own
65+
* once storage is back.
66+
*/
67+
private async safeList(): Promise<RequestSummary[]> {
68+
try {
69+
return await this.storage.list();
70+
} catch (error) {
71+
this.logger.warn(`Storage list() failed: ${String(error)}`);
72+
return [];
73+
}
74+
}
75+
76+
private async safeFind(id: string): Promise<RequestProfile | undefined> {
77+
try {
78+
return await this.storage.find(id);
79+
} catch (error) {
80+
this.logger.warn(`Storage find() failed: ${String(error)}`);
81+
return undefined;
82+
}
83+
}
84+
5885
@Get()
5986
async index(
6087
@Req() request: NegotiableRequest,
@@ -114,7 +141,7 @@ export class DebugController {
114141
}
115142
}
116143
// HTTP requests and socket events share one list; the UI filters by kind.
117-
send(response, 200, JSON_TYPE, safeJson(await this.storage.list()));
144+
send(response, 200, JSON_TYPE, safeJson(await this.safeList()));
118145
}
119146

120147
/**
@@ -128,17 +155,19 @@ export class DebugController {
128155
pick: (profile: RequestProfile) => T[] | undefined,
129156
map: (item: T, requestId: string, requestLabel: string) => Record<string, unknown>,
130157
): Promise<Record<string, unknown>[]> {
131-
const summaries = await this.storage.list();
158+
const summaries = await this.safeList();
132159
const out: Record<string, unknown>[] = [];
133160
for (const summary of summaries) {
134161
if (!has(summary)) continue;
135-
const profile = await this.storage.find(summary.id);
162+
const profile = await this.safeFind(summary.id);
136163
const items = profile && pick(profile);
137164
if (!profile || !items?.length) continue;
138165
const label =
139166
profile.kind === 'socket'
140167
? (profile.socket?.event ?? 'socket')
141-
: `${profile.method} ${profile.url}`;
168+
: profile.kind === 'job'
169+
? `${profile.job?.queue ?? 'job'}:${profile.job?.jobName ?? ''}`
170+
: `${profile.method} ${profile.url}`;
142171
for (const item of items) {
143172
out.push(map(item, profile.id, label));
144173
if (out.length >= 1000) return out;
@@ -153,7 +182,7 @@ export class DebugController {
153182
@Req() request: NegotiableRequest,
154183
@Res() response: AdapterResponse,
155184
): Promise<void> {
156-
const profile = await this.storage.find(id);
185+
const profile = await this.safeFind(id);
157186
if (!profile) {
158187
send(
159188
response,
@@ -181,8 +210,13 @@ export class DebugController {
181210

182211
@Delete()
183212
async clear(@Res() response: AdapterResponse): Promise<void> {
184-
await this.storage.clear();
185-
send(response, 200, JSON_TYPE, safeJson({ cleared: true }));
213+
try {
214+
await this.storage.clear();
215+
send(response, 200, JSON_TYPE, safeJson({ cleared: true }));
216+
} catch (error) {
217+
this.logger.warn(`Storage clear() failed: ${String(error)}`);
218+
send(response, 200, JSON_TYPE, safeJson({ cleared: false }));
219+
}
186220
}
187221
}
188222

src/config/debug-options.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,16 @@ export interface DebugModuleOptions {
4747
* whenever a WebSocket context is seen. Default true. Set false to disable.
4848
*/
4949
sockets?: boolean;
50+
/**
51+
* Capture background-job / message / scheduled runs as their own profiles,
52+
* with any SQL/Redis/HTTP they run. Auto-detected across the DI container —
53+
* BullMQ (`@nestjs/bullmq`), legacy `@nestjs/bull`, `@nestjs/microservices`
54+
* consumers, `@nestjs/schedule`, and DI-provided bee-queue/Agenda/pg-boss —
55+
* with no extra setup. Default true. Set false to disable.
56+
*/
57+
jobs?: boolean;
58+
/** Capture the job payload (`job.data`), redacted. Default true. */
59+
captureJobData?: boolean;
5060
/** Queries at/above this (ms) are flagged slow. Default 100. */
5161
slowQueryThreshold?: number;
5262
/** Requests at/above this (ms) are flagged slow. Default 500. */
@@ -70,7 +80,19 @@ export interface DebugModuleOptions {
7080
getUser?: (request: unknown) => unknown;
7181
/** Gate access to the debug API/UI (e.g. only admins). */
7282
authorize?: (request: unknown) => boolean | Promise<boolean>;
73-
/** Storage driver. Default: in-memory ring buffer. */
83+
/**
84+
* Storage driver. Default: in-memory ring buffer.
85+
*
86+
* To share captures across processes (an API + a separate worker), set the
87+
* `NEST_DEBUG_PANEL_STORAGE=redis` env var — no code change needed. It switches
88+
* to a shared Redis store (connecting via `NEST_DEBUG_PANEL_REDIS_URL` or
89+
* `REDIS_URL`) and falls back to in-memory, with a warning, if Redis is
90+
* unavailable.
91+
*
92+
* Pass a `DebugStorage` instance here only for a custom driver (e.g. the
93+
* exported `RedisStorage` with your own client) — it takes precedence over the
94+
* env var.
95+
*/
7496
storage?: DebugStorage;
7597
/** Profiling plugins (Prisma, Redis, Axios, custom, ...). */
7698
plugins?: DebugPlugin[];
@@ -90,6 +112,8 @@ export interface ResolvedDebugOptions {
90112
captureHttp: boolean;
91113
captureLogs: boolean;
92114
captureSockets: boolean;
115+
captureJobs: boolean;
116+
captureJobData: boolean;
93117
slowQueryThreshold: number;
94118
slowRequestThreshold: number;
95119
nPlusOneThreshold: number;
@@ -138,6 +162,8 @@ export function resolveDebugOptions(options: DebugModuleOptions = {}): ResolvedD
138162
captureHttp: options.captureHttp ?? true,
139163
captureLogs: options.captureLogs ?? true,
140164
captureSockets: options.sockets ?? true,
165+
captureJobs: options.jobs ?? true,
166+
captureJobData: options.captureJobData ?? true,
141167
slowQueryThreshold: options.slowQueryThreshold ?? 100,
142168
slowRequestThreshold: options.slowRequestThreshold ?? 500,
143169
nPlusOneThreshold: options.nPlusOneThreshold ?? 5,

0 commit comments

Comments
 (0)