Watchers
Query watchers (MikroORM, TypeORM, Prisma) and behavioral watchers (mail, cache, schedule, events, logs, redis, model) — each a small package you add to the watchers array.
Core captures requests and exceptions on its own. Everything else is a watcher you add to the watchers array. Each is its own package — install the ones that match your stack. Every watcher records in the caller's async context, so each entry lands in the active request/job batch automatically (see Capture & correlation). Recording failures are always swallowed: a Telescope error can never change the outcome of the thing it's watching.
Query watchers
MikroORM — @dudousxd/nestjs-telescope-mikro-orm
Captures every executed SQL query, correlates it to the request that triggered it, tags slow queries, and ships a pure N+1 detector. (The same package ships a storage adapter.) Requires MikroORM v7+.
MikroORM v7 stores its logger in a private field set once at Configuration construction time — runtime replacement isn't possible. So the host must wire loggerFactory before constructing the ORM; that factory is the capture mechanism. Use forRootAsync so TelescopeService can be injected first:
import { TelescopeModule, TelescopeService } from '@dudousxd/nestjs-telescope';
import { MikroOrmQueryWatcher, telescopeMikroOrmLogger } from '@dudousxd/nestjs-telescope-mikro-orm';
import { MikroOrmModule } from '@mikro-orm/nestjs';
@Module({
imports: [
TelescopeModule.forRoot({
authorizer: () => true, // restrict in production
watchers: [new MikroOrmQueryWatcher()],
}),
MikroOrmModule.forRootAsync({
inject: [TelescopeService],
useFactory: (telescope: TelescopeService) => ({
// ...your driver, dbName, entities
debug: ['query'],
loggerFactory: telescopeMikroOrmLogger(
(input) => telescope.record(input),
{ slowMs: 100 }, // queries >= 100ms get a 'slow' tag
),
}),
}),
],
})
export class AppModule {}Queries recorded this way inherit the active ALS batch, so each one correlates to its request. The package also exports detectNPlusOne(batchEntries, threshold) — a pure function (no I/O) that returns every query template run at least threshold times in a batch.
Capture only happens while debug is on, but debug also echoes every query to stdout. To feed Telescope without the console noise, keep debug: ['query'] and pass silent: true (since @dudousxd/nestjs-telescope-mikro-orm@1.12.0) — it swaps MikroORM's writer for a no-op so queries still flow into Telescope while nothing is printed:
loggerFactory: telescopeMikroOrmLogger(
(input) => telescope.record(input),
{ slowMs: 100, silent: true }, // record into Telescope, no stdout echo
),TypeORM — @dudousxd/nestjs-telescope-typeorm
TypeORM lets you supply a custom Logger at DataSource construction; this package ships one whose logQuery records each statement. Because the logger runs in the query's async context, queries correlate to the active batch via ALS — exactly like MikroORM.
import { TelescopeModule, TelescopeService } from '@dudousxd/nestjs-telescope';
import { telescopeTypeOrmLogger } from '@dudousxd/nestjs-telescope-typeorm';
import { TypeOrmModule } from '@nestjs/typeorm';
@Module({
imports: [
TelescopeModule.forRoot({
authorizer: () => true, // restrict in production
}),
TypeOrmModule.forRootAsync({
inject: [TelescopeService],
useFactory: (telescope: TelescopeService) => ({
// ...your connection options, entities
logging: true, // or ['query'] — required, else TypeORM never calls logQuery
logger: telescopeTypeOrmLogger((input) => telescope.record(input)),
}),
}),
],
})
export class AppModule {}TypeORM's logQuery carries no execution time, so normal queries record durationMs: null; slow queries surfaced via logQuerySlow (gated by maxQueryExecutionTime) record a real duration and a slow tag. Pass { slowMs } to set the threshold the package uses.
Prisma — @dudousxd/nestjs-telescope-prisma
Captures every SQL statement Prisma executes (SQL + duration + params) for a full query log and slow-query visibility. Construct the client with query event logging, then hand it to the watcher:
import { PrismaClient } from '@prisma/client';
import { TelescopeModule } from '@dudousxd/nestjs-telescope';
import { PrismaQueryWatcher } from '@dudousxd/nestjs-telescope-prisma';
const prisma = new PrismaClient({ log: [{ emit: 'event', level: 'query' }] });
TelescopeModule.forRoot({ watchers: [new PrismaQueryWatcher(prisma, { slowMs: 500 })] });No request/job correlation (Prisma engine limit)
prisma.$on('query', cb) fires detached from the caller's async context — the engine emits query events on its own channel, after the fact. So Prisma query entries are orphaned: they don't correlate to the request/job that issued them, and per-batch N+1 detection doesn't apply. They're still captured because a full query log plus slow-query visibility is valuable on its own. If per-request query correlation matters, prefer the MikroORM or TypeORM adapter.
Behavioral watchers
Mail — @dudousxd/nestjs-telescope-mail
Wraps a nodemailer transporter's sendMail to capture every email — sender, recipients, subject, a short body preview, and sent/failed status — correlated to the request or job that sent it.
import { MailWatcher } from '@dudousxd/nestjs-telescope-mail';
TelescopeModule.forRoot({ watchers: [new MailWatcher(transporter, { mailer: 'ses' })] });On a rejected send it records status: 'failed' (tagged failed) and re-throws — your error handling is untouched. The patch is per-transporter and idempotent.
Cache — @dudousxd/nestjs-telescope-cache
Captures cache hits and misses. Three modes: zero-config (new CacheWatcher() auto-discovers the standard @nestjs/cache-manager CACHE_MANAGER and patches its get/set), explicit (pass a specific Cache), and an escape hatch for custom caches like BentoCache via an instrument(emit, ctx) hook.
import { CacheWatcher } from '@dudousxd/nestjs-telescope-cache';
TelescopeModule.forRoot({ watchers: [new CacheWatcher()] }); // auto-discovers CACHE_MANAGERThe philosophy: official libraries are auto-instrumented out of the box; any other cache becomes instrumentable through instrument — Telescope never has to special-case every cache library. If CACHE_MANAGER isn't registered, the watcher logs a one-line warning and no-ops.
@nestjs/cache-manager is optional
@nestjs/cache-manager is an optional peer dependency. It is imported lazily, on demand, only on the zero-config auto-discovery path that resolves CACHE_MANAGER. So BentoCache-only (and any other custom-cache-only) hosts that use the instrument(emit, ctx) escape hatch — or that pass an explicit Cache — do not need to install @nestjs/cache-manager at all. Loading the watcher never triggers a runtime resolution of the peer.
Schedule — @dudousxd/nestjs-telescope-schedule
Wraps @nestjs/schedule @Cron / @Interval / @Timeout handlers so each run opens a schedule batch — correlating the queries and exceptions that task emits — and records a job entry for the run itself (outcome + duration).
import { ScheduleWatcher } from '@dudousxd/nestjs-telescope-schedule';
@Module({
imports: [
// Import Telescope BEFORE ScheduleModule (see ordering note).
TelescopeModule.forRoot({ watchers: [new ScheduleWatcher({ slowMs: 1000 })] }),
ScheduleModule.forRoot(),
],
})
export class AppModule {}Ordering caveat
@nestjs/schedule's explorer captures each handler reference at its own onModuleInit. For the prototype patch to take effect, Telescope's module must initialize before ScheduleModule — import it first / higher in the tree. If ScheduleModule initializes first, it binds the un-wrapped handler and that task isn't captured.
Events — @dudousxd/nestjs-telescope-events
Attaches a single wildcard listener (emitter.onAny) to the app's @nestjs/event-emitter EventEmitter2 singleton and records one event entry per emit (name, payload, listener count), correlated to the request/job that emitted it. It resolves the emitter from the Nest container — no wiring.
import { EventsWatcher } from '@dudousxd/nestjs-telescope-events';
TelescopeModule.forRoot({ watchers: [new EventsWatcher()] });Logs — @dudousxd/nestjs-telescope-logs
Captures Nest Logger output. Zero-config: just add the watcher. By default LogsWatcher tees the @nestjs/common Logger facade — every new Logger(Ctx) instance log and every Logger.log(...) static is recorded, with no host changes:
import { LogsWatcher } from '@dudousxd/nestjs-telescope-logs';
TelescopeModule.forRoot({ watchers: [new LogsWatcher()] });The patch calls the original first, so console output and any custom logger behind the facade are untouched. It's idempotent process-wide and never throws into your log call.
Fallback — explicit logger. Hosts that bypass the Logger facade (logging straight through their app logger) can install TelescopeConsoleLogger instead; it logs to the console and forwards each line to Telescope's sink:
import { NestFactory } from '@nestjs/core';
import { LogsWatcher, TelescopeConsoleLogger } from '@dudousxd/nestjs-telescope-logs';
const app = await NestFactory.create(AppModule, { bufferLogs: true });
app.useLogger(new TelescopeConsoleLogger());
// auto-patch + explicit coexist; a single log still records exactly once.
TelescopeModule.forRoot({ watchers: [new LogsWatcher()] });Pass new LogsWatcher({ autoPatch: false }) to capture only via TelescopeConsoleLogger. Both modes can run together — a shared re-entrancy guard dedupes a facade call that reaches both paths, so one log records once.
Telescope's own log contexts (Telescope, Recorder, LogsWatcher, EventsWatcher) are skipped so the watcher never records its own lines or feeds a loop.
Level-aware sampling. warn / error / fatal log entries count as errors for tail-sampling, so you can sample logs hard and never lose them:
TelescopeModule.forRoot({ sampling: { log: { rate: 0.1, keepErrors: true } } });Redis commands — @dudousxd/nestjs-telescope-redis-watcher
Monkey-patches a wrapped ioredis client's sendCommand, recording one redis entry per command (command, args, durationMs), correlated to the request/job that issued it. Two construction forms: wrap a client you hold, or resolve one lazily via instrument.
import { RedisCommandWatcher } from '@dudousxd/nestjs-telescope-redis-watcher';
TelescopeModule.forRoot({ watchers: [new RedisCommandWatcher(ioredisClient)] });Pass a dedicated/observed client. If you share one ioredis with Telescope's own Redis storage provider, those storage commands get captured too — noise.
Models — @dudousxd/nestjs-telescope-mikro-orm-watcher
Captures every MikroORM entity lifecycle change (create / update / delete) as a model entry (action, entity, id, changes), correlated to the request or job that triggered the flush. It registers an EventSubscriber on the EntityManager's event manager.
import { MikroOrmModelWatcher } from '@dudousxd/nestjs-telescope-mikro-orm-watcher';
TelescopeModule.forRoot({ watchers: [new MikroOrmModelWatcher()] });Telescope's own storage entities (TelescopeEntry / TelescopeRollup) are skipped by class name, so persisting an entry never records another model entry.
Beyond per-entity changes, the watcher also records the flush and transaction lifecycle with measured durations, keyed per EntityManager so concurrent units of work stay isolated:
- On
onFlush/afterFlushit records a{ kind: 'flush' }entry carrying the elapseddurationMs. - On
beforeTransactionStart+afterTransactionCommit/afterTransactionRollbackit records a{ kind: 'transaction', outcome }entry (outcomeis'committed'or'rolled-back') with the elapseddurationMs.
When you also run @dudousxd/nestjs-telescope-otel, these flush and transaction entries map to the telescope_mikroorm_flush_total / telescope_mikroorm_flush_duration_ms and telescope_mikroorm_transaction_total / telescope_mikroorm_transaction_duration_ms metrics.
@dudousxd/nestjs-telescope-ui (dashboard)
The bundled dashboard SPA served by a NestJS module — plus the composable React components, hooks, and typed client to build your own admin.
Storage adapters
Persist Telescope entries in your own database (MikroORM — MySQL/SQLite) or share one store across replicas (Redis). Same StorageProvider contract everywhere.