@dudousxd/nestjs-telescope

中文文档
4.2.0发布于 2026/11/13MITTypeScriptES Module构建来源已验证近 30 天下载 5091

Laravel Telescope-style observability console for NestJS — core: watchers, recorder, correlation, SQLite store, headless API.

原始使用文档
文档目录11 节
  1. Protecting the routes
  2. Memory bounds
  3. Redaction is also a memory bound
  4. Down-sample high-volume streams
  5. Watch the truncated counter
  6. Explain slow queries
  7. On-demand prune
  8. Alerts
  9. Overload protection
  10. Request replay
  11. MCP server (coding agents)

@dudousxd/nestjs-telescope

Core of @dudousxd/nestjs-telescope — request + exception watchers, the recorder, ALS-based batch correlation, the SQLite store, the headless API, and the pruner. See the root README.md for the quick start.

Protecting the routes

Telescope has no login of its own: sign-in stays your app's. One option, authorize, decides who may use the console. It runs after your app's authentication (global guards and decorators), so request.user is set, and takes one of five forms:

TelescopeModule.forRoot({
  authorize: (ctx) => ctx.switchToHttp().getRequest().user?.isAdmin === true,
  // authorize: ConsoleAccess,                              // @Injectable() implements ConsoleAuthorizer
  // authorize: () => import('./console-access.js'),        // lazy, default export
  // authorize: { policy: ConsolePolicy, ability: 'view' }, // @nestjs/authorization
  // authorize: 'console.telescope',                        // gate name (@dudousxd/nestjs-authz)
});
TelescopeUiModule.forRoot({ authorize: /* the same */ });

It covers the API, the live stream and the MCP endpoint (mcpAuthorize replaces it on MCP alone); pass the same value to the UI module, which serves the page. Return true/false, { allowed: false, reason: 'unauthenticated' } (401) or { redirectTo: '/login' } (302 for the page, 401 with redirectTo for the API; the dashboard follows it). Denials answer { "error": "forbidden" } (403) or { "error": "unauthenticated" } (401); a throw denies.

For header-based callers (coding agents, Prometheus), authorizeByToken checks Authorization: Bearer <token> in constant time:

TelescopeModule.forRoot({
  mcp: true,
  authorize: { policy: ConsolePolicy, ability: 'view' },
  mcpAuthorize: authorizeByToken({ token: process.env.TELESCOPE_MCP_TOKEN }),
});

Without authorize the console is open outside production; with NODE_ENV === 'production' every request answers 403 and one warning is logged at boot. decorators (authentication, throttling, ...) still run first but don't count as access control on their own. The same rule covers the otel GET /metrics scrape (forExporter(exporter, { authorize })).

Mount the routes yourself. routes: false mounts no controller (capture, storage, watchers and every service keep running) and the module exports what the controllers need. Extend the exported controllers and protect them with @UseConsoleAuthorize(), the same contract. With no argument it runs the module's authorize; list it ABOVE your auth guard (Nest applies class decorators bottom-up, so the auth guard runs first and sets request.user):

@Controller('ops/telescope/api')
@UseConsoleAuthorize()
@UseGuards(AuthGuard)
class ConsoleApi extends TelescopeController {}

@Controller('ops/telescope')
@UseConsoleAuthorize()
@UseGuards(AuthGuard)
class ConsolePage extends TelescopeUiController {}

@Module({
  imports: [
    TelescopeModule.forRoot({
      path: 'ops/telescope',
      routes: false,
      authorize: { policy: ConsolePolicy, ability: 'view' },
    }),
    TelescopeUiModule.forRoot({ path: 'ops/telescope', routes: false }),
  ],
  controllers: [ConsoleApi, ConsolePage],
})
export class OpsModule {}

StreamController, TelescopeMcpController and ClientErrorController extend the same way. Keep path set to where you mount them, since the request capture skips the console's own calls and the page rebases its assets on it. Every mounted controller is also marked open to @nestjs/authorization's deny-by-default global guard (Can.Anyone()), so authorize decides; a Can(...) in decorators is still enforced.

Public by design: the client-error beacon. POST <path>/api/client-errors (off until clientErrors.enabled) takes reports from any visitor's browser, so it takes neither decorators nor authorize. It keeps its byte cap and per-IP rate limit. To put it behind your own auth, use routes: false and extend ClientErrorController.

Mutations (queue retry/remove/promote/redrive/enqueue, prune, replay, profiling) are a feature switched on with mutations: true (off: 403). Who may call them is decided by authorize like every other route; for a stricter rule on mutations, mount the routes yourself and override those methods with an extra @Can(...) (re-declare the route decorators on the override).

Memory bounds

Telescope holds a bounded working set in memory, by design:

heap ≈ prune.after × ingest rate × bytes per entry

There is no unbounded leak — but the three factors multiply, so a high ingest rate of fat entries against a generous retention window can still pin a lot of heap. The library bounds each factor at capture time so a host can't drive a GC death spiral by capturing large content.

Redaction is also a memory bound

record() runs redact() synchronously. Besides masking sensitive keys, that walk is the detach: it snapshots content into a plain, reference-free clone, releasing any live object graph (e.g. a hydrated ORM req.user that references its EntityManager, identity map, and relations). The clone is bounded so a fat graph can't produce a fat entry:

BoundDefaultBeyond it
maxDepth8subtree → '[Truncated: depth]'
maxStringLength8_192string → first N chars + '…[truncated]'
maxArrayLength200first N items + '[Truncated: N of M items]'
maxNodes5_000remaining subtrees → '[Truncated: size]'

The defaults are generous: a normal request / query / cache entry is cloned byte-identically and never trips a bound. Tune them per app via redact:

TelescopeModule.forRoot({
  redact: {
    keys: ['x-internal-token'],
    maxNodes: 2_000, // tighter cap for very fat content
  },
});

[!WARNING] Redaction must stay synchronous. Deferring it to the flush dropped capture cost but OOM'd a production host: record() then only stashed RAW content, so up to bufferSize live ORM entity graphs were retained until flush. The sync clone is load-bearing — it bounds memory by detaching at capture time.

Down-sample high-volume streams

Sampling is a store-volume lever (requests are already off the response path). Cache hits especially dominate the working-set product, so keep a fraction of them:

TelescopeModule.forRoot({
  prune: { after: '1h' },
  sampling: { cache: 0.1 }, // keep 10% of cache entries
});

With prune set but sampling empty, the library logs a one-line INFO at boot pointing at this recipe.

Tail-sampling: keep what matters

A per-type rate can also be a rule object to keep a fraction of the noise while always retaining the entries you'd never want sampled out — errors and slow ones. Plain-number config is unchanged; the object form is purely additive:

TelescopeModule.forRoot({
  sampling: {
    cache: { rate: 0.1, keepErrors: true, keepSlowMs: 500 },
  },
});
  • rate — base keep-rate (0–1), applied to ordinary entries of that type.
  • keepErrors — always keep entries that look like errors: a failed tag, content.failed === true, or content.statusCode >= 500 (cheap, shallow field reads — no deep walk on the hot path).
  • keepSlowMs — always keep entries whose durationMs >= keepSlowMs.

So { rate: 0.1, keepErrors: true, keepSlowMs: 500 } keeps 10% of healthy fast cache hits but 100% of failed or ≥500ms ones.

Watch the truncated counter

GET /telescope/api/health reports a truncatedCount: entries whose content hit a redaction bound and was clipped. A non-zero, climbing value means a watcher is capturing fat content (often an ORM entity) — the fix is to project lighter content (e.g. req.user → { id, email }) or tighten redact / add sampling. The dashboard's Overview health card surfaces the same figure as a Truncated stat.

Explain slow queries

Telescope is DB-agnostic, so query EXPLAIN is an opt-in host hook — you bring the connection and dialect. Supply explainQuery, and the dashboard's query detail grows an Explain button (POST /telescope/api/queries/explain, { entryId }) that replays the captured SQL/bindings through your hook and renders { plan }. The hook runs arbitrary EXPLAIN <sql> against your database, so scope its connection read-only. A hook that throws surfaces as a clean { message } error, not a crash. Hidden when unset (meta.explainEnabled).

TelescopeModule.forRoot({
  explainQuery: async (sql, bindings) => {
    const [rows] = await pool.query(`EXPLAIN FORMAT=JSON ${sql}`, bindings);
    return rows;
  },
});

On-demand prune

With a prune retention window configured, GET /telescope/api/retention reports the resolved window and POST /telescope/api/retention/prune runs a prune on demand. Pruning is a mutation, so like the queue console it needs mutations: true (403 otherwise), and 400 when no prune window is set. The Overview Retention card shows a Prune now button only when both are in place (meta.pruneEnabled).

Alerts

Telescope sends an alert to every configured channel when a rule trips. Rules run on an unref'd interval; a failing channel is logged and never crashes the host.

import { slackChannel, webhookChannel } from '@dudousxd/nestjs-telescope';

TelescopeModule.forRoot({
  alerts: {
    channels: [
      slackChannel(process.env.SLACK_WEBHOOK_URL!), // formatted Slack message
      webhookChannel('https://relay.example/alerts'), // raw JSON POST
    ],
    every: '1m',       // evaluation cadence (default '1m')
    cooldown: '15m',   // per-rule re-notify suppression (default '15m')
    rules: [
      { type: 'exception-rate', window: '5m', threshold: 10 },
      { type: 'slow-request-rate', window: '5m', thresholdMs: 1000, count: 20 },
      { type: 'dropped-entries', threshold: 100 },
    ],
  },
});
  • exception-rate — fires when >= threshold exceptions were recorded in window.
  • slow-request-rate — fires when >= count requests slower than thresholdMs were recorded in window.
  • dropped-entries — fires when the Recorder's droppedCount grew by >= threshold since the previous evaluation (a delta).

A configured alerts with no channels or empty rules is a fail-closed boot error. webhookChannel POSTs this payload:

{ "rule": { "...": "..." }, "value": 12, "threshold": 10, "firedAt": "2026-06-04T00:00:00.000Z", "instanceId": "host-1" }

meta.alerts reports { enabled, ruleCount }.

Overload protection

Telescope watches the process event-loop lag (perf_hooks.monitorEventLoopDelay) on an unref'd interval and pauses the Recorder when the p99 lag crosses a threshold, resuming once it recovers — so a telescope under load can never amplify an incident. While paused, new record() calls are dropped. It's on by default at 200ms; degrades to a no-op when perf_hooks lacks the monitor.

TelescopeModule.forRoot({
  overloadProtection: true,                       // default — pause above 200ms p99
  // overloadProtection: { maxEventLoopLagMs: 100 }, // tune the threshold
  // overloadProtection: false,                      // disable entirely
});

Request replay

GET /telescope/api/entries/:id/replay re-issues a captured request entry against the local app (127.0.0.1) and returns the outcome. Replaying actually hits the app (it may write), so it's a mutation: like prune / queue actions it needs mutations: true (403 otherwise). The replayed request carries an x-telescope-replay: 1 header (so the host can recognize or skip it) and strips cookie/authorization/host headers — a replay must not silently reuse the captured session. Replayed requests are tagged replay. The dashboard's request detail exposes a Replay button when mutations: true is set.

MCP server (coding agents)

With mcp enabled, Telescope serves a stateless JSON-RPC Model Context Protocol server over streamable HTTP at POST /telescope/api/mcp, so a coding agent (Claude Code, Cursor, …) can debug straight from the captured data — backed by the same storage/stats/diagnosis APIs as the dashboard. JSON-RPC is hand-rolled (no SDK dependency); being stateless, the transport's GET stream returns 405 and DELETE is a 200 no-op.

TelescopeModule.forRoot({
  mcp: true, // omit (or false) and the endpoint answers 404
});

Register it with Claude Code:

claude mcp add --transport http telescope http://localhost:3000/telescope/api/mcp

Tools: list_entries (filter by type/search/tag/time window), get_entry (one entry + its full batch), get_batch, get_stats (last-hour health snapshot), and diagnose_exception (AI diagnosis, when ai is configured). It is protected like every console route, by authorize, or by mcpAuthorize on this route alone (agent clients send a header: mcpAuthorize: authorizeByToken({ token })), or by your own controller.

版本与发布记录

@dudousxd/nestjs-telescope 共发布 58 个版本,首次发布于 2026/06/05;过去 12 个月发布了 58 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
4.2.0latest2026/11/13今天
4.1.02026/11/13今天
4.0.02026/11/121 天前
3.3.02026/11/121 天前
3.2.02026/11/121 天前
3.1.02026/11/121 天前

关于 @dudousxd/nestjs-telescope 的常见问题

@dudousxd/nestjs-telescope 是什么?

Laravel Telescope-style observability console for NestJS — core: watchers, recorder, correlation, SQLite store, headless API.

如何安装 @dudousxd/nestjs-telescope?

在项目目录运行 npm install @dudousxd/nestjs-telescope,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:rxjs(>=7.0.0)、@nestjs/core(>=10.0.0)、@nestjs/common(>=10.0.0)、reflect-metadata(>=0.1.13)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@dudousxd/nestjs-telescope 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@dudousxd/nestjs-telescope 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@dudousxd/nestjs-telescope 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=24。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@dudousxd/nestjs-telescope 可以商用吗?

@dudousxd/nestjs-telescope 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@dudousxd/nestjs-telescope 的下载量有多少?

根据 npm 官方下载统计,@dudousxd/nestjs-telescope 在 2026/10/13 – 2026/11/11 这 30 天内被下载 5,091 次,日均约 170 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@dudousxd/nestjs-telescope 还在维护吗?

最新版本 4.2.0 发布于 2026/11/13(今天);至今共发布 58 个版本,其中过去 12 个月发布了 58 个;首次发布于 2026/06/05。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。