UNPKG

@dudousxd/nestjs-telescope

Version:

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

839 lines 37 kB
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; var __param = (this && this.__param) || function (paramIndex, decorator) { return function (target, key) { decorator(target, key, paramIndex); } }; // packages/core/src/nest/telescope.controller.ts import { BadGatewayException, BadRequestException, Body, Controller, Delete, ForbiddenException, Get, HttpCode, Inject, MethodNotAllowedException, NotFoundException, Param, Post, Query, Req, Res, ServiceUnavailableException, UseGuards, } from '@nestjs/common'; import { ModuleRef } from '@nestjs/core'; import { durationToMs } from '../config/parse-duration.js'; import { EntryType } from '../entry/entry.js'; import { ExtensionRegistry } from '../extension/registry.js'; import { collectEntriesInWindow } from '../metrics/collect-window.js'; import { QueueMetricsService } from '../metrics/queue-metrics.service.js'; import { ServerStatsService, } from '../metrics/server-stats.service.js'; import { StatsService } from '../metrics/stats.service.js'; import { TimeseriesService } from '../metrics/timeseries.service.js'; import { TracesService } from '../metrics/traces.service.js'; import { CPU_PROFILE_ENTRY_TYPE, ProfilerService } from '../profiling/profiler.service.js'; import { PulseService } from '../pulse/pulse.service.js'; import { QUEUE_ACTIONS, isQueueState, } from '../queue/queue-manager.js'; import { QueueManagerRegistry } from '../queue/queue-manager.registry.js'; import { ScheduleManagerRegistry } from '../schedule/schedule-manager.registry.js'; import { createExtensionContext } from './extension-context.factory.js'; import { replayRequest } from './request-replay.js'; import { TelescopeActionGuard } from './telescope-action.guard.js'; import { TelescopePruner } from './telescope-pruner.service.js'; import { TelescopeGuard } from './telescope.guard.js'; import { TELESCOPE_CONFIG, TELESCOPE_EXTENSIONS, TELESCOPE_OPTIONS, TELESCOPE_STORAGE, } from './telescope.options.js'; import { TelescopeService } from './telescope.service.js'; /** Maps a queue action to the optional QueueManager method that implements it. */ const ACTION_METHOD = { retry: 'retry', remove: 'remove', promote: 'promote', 'retry-all': 'retryAll', redrive: 'redrive', enqueue: 'enqueue', }; /** * The per-job actions (those whose QueueManager method has the * `(queue, id) => Promise<void>` shape). `jobAction` dispatches via * {@link ACTION_METHOD}, so this subset is the route's allow-list. */ const JOB_ACTIONS = ['retry', 'remove', 'promote']; const isJobAction = (action) => JOB_ACTIONS.includes(action); /** Type guard: a query entry carries a non-empty SQL string to explain. */ function isQueryContentWithSql(content) { return (typeof content === 'object' && content !== null && 'sql' in content && typeof content.sql === 'string' && content.sql !== ''); } /** * Parse the `window` query param (default `1h`) into milliseconds, or throw 400. * Shared by every windowed metrics route (queues/pulse/timeseries/traces/stats). */ function parseWindowMs(window) { let windowMs; try { windowMs = durationToMs(window ?? '1h'); } catch { throw new BadRequestException(`Invalid window: ${window}`); } if (!Number.isFinite(windowMs) || windowMs <= 0) { throw new BadRequestException(`Window must be positive: ${window}`); } return windowMs; } let TelescopeController = class TelescopeController { storage; service; queueMetrics; timeseriesService; tracesService; statsService; serverStats; pulse; profiler; queueManagers; scheduleManagers; options; extensions; extConfig; pruner; moduleRef; constructor(storage, service, queueMetrics, timeseriesService, tracesService, statsService, serverStats, pulse, profiler, queueManagers, scheduleManagers, options, extensions, extConfig, pruner, moduleRef) { this.storage = storage; this.service = service; this.queueMetrics = queueMetrics; this.timeseriesService = timeseriesService; this.tracesService = tracesService; this.statsService = statsService; this.serverStats = serverStats; this.pulse = pulse; this.profiler = profiler; this.queueManagers = queueManagers; this.scheduleManagers = scheduleManagers; this.options = options; this.extensions = extensions; this.extConfig = extConfig; this.pruner = pruner; this.moduleRef = moduleRef; } list(query) { const entryQuery = { ...(query.type !== undefined ? { type: query.type } : {}), ...(query.tag !== undefined ? { tag: query.tag } : {}), ...(query.familyHash !== undefined ? { familyHash: query.familyHash } : {}), ...(query.batchId !== undefined ? { batchId: query.batchId } : {}), ...(query.traceId !== undefined ? { traceId: query.traceId } : {}), ...(query.search !== undefined && query.search !== '' ? { search: query.search } : {}), ...(query.cursor !== undefined ? { cursor: query.cursor } : {}), ...(query.limit !== undefined && Number.isFinite(Number(query.limit)) ? { limit: Number(query.limit) } : {}), }; return this.storage.get(entryQuery); } show(id) { return this.storage.find(id); } // Request REPLAY. Re-issues a captured request entry against the local server // and reports the outcome. It is a MUTATION (it actually hits the app, which // may write), so — like `prune` — it stays behind the default-deny // authorizeAction gate rather than the read guard. The queue-shaped // TelescopeActionGuard can't validate it (no driver/queue/action params), so we // enforce the same default-deny here directly: no `authorizeAction` → 403. async replay(id, request) { if (!this.options.authorizeAction) { throw new ForbiddenException('Mutations are disabled (no authorizeAction configured).'); } const entry = await this.storage.find(id); if (entry === null || entry.type !== EntryType.Request) { throw new NotFoundException('No request entry with that id.'); } return replayRequest(entry.content, request); } batch(id) { return this.storage.batch(id); } tags(prefix) { return this.storage.tags(prefix); } queues(window) { const windowMs = parseWindowMs(window); return this.queueMetrics.getQueueMetrics(windowMs); } pulseHealth(window) { const windowMs = parseWindowMs(window); return this.pulse.getHealth(windowMs); } timeseries(window, buckets, type, tag) { const windowMs = parseWindowMs(window); const bucketCount = buckets !== undefined ? Number(buckets) : undefined; return this.timeseriesService.getTimeseries({ windowMs, ...(bucketCount !== undefined && Number.isFinite(bucketCount) ? { buckets: bucketCount } : {}), ...(type !== undefined ? { type } : {}), ...(tag !== undefined ? { tag } : {}), }); } traces(window, limit) { const windowMs = parseWindowMs(window); const limitCount = limit !== undefined ? Number(limit) : undefined; return this.tracesService.getTraces({ windowMs, ...(limitCount !== undefined && Number.isFinite(limitCount) ? { limit: limitCount } : {}), }); } // Nested span waterfall for ONE trace. Returns 404 when the trace has no // entries (unknown / pruned) so the dashboard can render an empty state. async waterfall(traceId) { const waterfall = await this.tracesService.getWaterfall(traceId); if (waterfall === null) { throw new NotFoundException(`No entries for trace ${traceId}.`); } return waterfall; } stats(type, window, buckets) { if (type === undefined || type === '') { throw new BadRequestException('Query parameter "type" is required.'); } const windowMs = parseWindowMs(window); const bucketCount = buckets !== undefined ? Number(buckets) : undefined; return this.statsService.getStats({ type, windowMs, ...(bucketCount !== undefined && Number.isFinite(bucketCount) ? { buckets: bucketCount } : {}), }); } async liveQueues() { const managers = this.queueManagers.all(); const all = await Promise.all(managers.map((m) => m.listQueues())); const actionsByDriver = {}; for (const manager of managers) { actionsByDriver[manager.driver] = QUEUE_ACTIONS.filter((action) => typeof manager[ACTION_METHOD[action]] === 'function'); } return { queues: all.flat(), capabilities: { mutationsEnabled: Boolean(this.options.authorizeAction), actionsByDriver, }, }; } async liveSchedules() { const ctx = this.scheduleManagers.context(); const all = await Promise.all(this.scheduleManagers.all().map((m) => m.listTasks(ctx))); return { tasks: all.flat() }; } liveCounts(driver, queue) { return this.requireManager(driver).counts(queue); } liveJobs(driver, queue, state, cursor, limit) { if (!isQueueState(state)) throw new BadRequestException(`Invalid state: ${state}`); const page = { ...(cursor !== undefined ? { cursor } : {}), ...(limit !== undefined && Number.isFinite(Number(limit)) ? { limit: Number(limit) } : {}), }; return this.requireManager(driver).listJobs(queue, state, page); } liveJob(driver, queue, id) { return this.requireManager(driver).getJob(queue, id); } async jobAction(driver, queue, id, action) { const manager = this.requireManager(driver); if (!isJobAction(action)) throw new BadRequestException(`Invalid job action: ${action}`); await this.callAction(manager[ACTION_METHOD[action]], manager, queue, id, action); return { ok: true }; } async queueAction(driver, queue, action, state) { const manager = this.requireManager(driver); if (action === 'retry-all') { if (!isQueueState(state)) throw new BadRequestException(`Invalid state: ${state}`); if (!manager.retryAll) throw new MethodNotAllowedException(`Driver ${driver} cannot retry-all`); return { ok: true, count: await manager.retryAll(queue, state) }; } if (action === 'redrive') { if (!manager.redrive) throw new MethodNotAllowedException(`Driver ${driver} cannot redrive`); return { ok: true, count: await manager.redrive(queue) }; } throw new BadRequestException(`Invalid queue action: ${action}`); } // Enqueue carries a JSON body (name + payload), so it lives on its own route // rather than under `:action`. Still gated by the same default-deny guard. async enqueue(driver, queue, body) { if (body === undefined || body === null || !('payload' in body)) { throw new BadRequestException('Body must include a "payload".'); } const manager = this.requireManager(driver); if (!manager.enqueue) throw new NotFoundException(`Driver ${driver} cannot enqueue`); const opts = body.name !== undefined ? { name: body.name } : {}; return manager.enqueue(queue, body.payload, opts, this.queueManagers.context()); } async callAction(fn, manager, queue, id, action) { if (!fn) throw new MethodNotAllowedException(`Driver ${manager.driver} cannot ${action}`); await fn.call(manager, queue, id); } requireManager(driver) { const manager = this.queueManagers.get(driver); if (!manager) throw new NotFoundException(`Unknown queue driver: ${driver}`); return manager; } meta() { return this.service.getMeta(); } serverStatsSnapshot() { return this.serverStats.getStats(); } // CPU/mem history ring buffer for the dashboard's resource-history card. serverStatsHistory() { return this.serverStats.getHistory(); } health() { return this.service.getHealth(); } // ── Extension data providers ──────────────────────────────────────────────── // Read-shaped: the UI calls this per panel to fetch a named provider's data. // Sits behind the class-level read guard. 404 for an unknown provider; 502 // when the provider throws (its message is surfaced so panel authors can see // why). Query params arrive as strings and are passed through verbatim. async extData(ext, provider, query) { const found = this.extensions.findProvider(provider); // The `:ext` segment must name the extension that actually owns the provider — // a mismatch is treated as not-found so the URL namespace can't be spoofed. if (!found || this.extensions.providerOwner(provider) !== ext) { throw new NotFoundException(`Unknown data provider "${provider}".`); } const ctx = createExtensionContext(this.moduleRef, this.extConfig); try { return await found.resolve(query, ctx); } catch (error) { throw new BadGatewayException(error.message); } } // ── Retention / prune ────────────────────────────────────────────────────── retention() { const prune = this.options.prune; // entryCount / oldestCreatedAt require an ascending scan or a count the // StorageProvider SPI does not expose cheaply. We deliberately do NOT scan, // so both stay null until/unless the SPI grows a cheap accessor. return { retention: prune ? { afterMs: durationToMs(prune.after), keepLast: prune.keepLast ?? null, } : null, entryCount: null, oldestCreatedAt: null, pruneSupported: true, }; } // Prune-run activity / retention status for the Prunes screen. Read-shaped — // sits behind the class-level read guard like the other GETs. The runs come // from the pruner's in-memory ring (PER-POD), the config from the resolved // config, and nextRunAt is the pruner's predicted next scheduled cycle. prunes() { const prune = this.extConfig.prune; const nextRunAtMs = this.pruner.getNextRunAtMs(); return { runs: this.pruner.getRuns(), config: prune ? { afterMs: prune.afterMs, intervalMs: prune.intervalMs, keepLast: prune.keepLast ?? null, ...(Object.keys(prune.perTypeMs).length > 0 ? { perType: prune.perTypeMs } : {}), } : null, nextRunAt: nextRunAtMs !== null ? new Date(nextRunAtMs).toISOString() : null, }; } // Prune is a MUTATION (deletes entries), so it stays behind the default-deny // authorizeAction gate. The queue-shaped TelescopeActionGuard can't validate // it (no driver/queue/action params), so we enforce the same default-deny // here directly: no `authorizeAction` configured → 403. The cycle runs through // the pruner so it is recorded as a `manual` run (and honours per-type + // archive retention identically to the scheduled cycle). async prune() { if (!this.options.authorizeAction) { throw new ForbiddenException('Mutations are disabled (no authorizeAction configured).'); } if (!this.options.prune) { throw new BadRequestException('No `prune` retention window is configured.'); } return { pruned: await this.pruner.pruneNow() }; } // ── Query EXPLAIN ────────────────────────────────────────────────────────── // Read-shaped (it returns a plan, mutates no Telescope state) so it sits behind // the normal read guard. NOTE: the host hook runs arbitrary `EXPLAIN <sql>` // against its database — hosts MUST scope that connection read-only. async explain(body) { const explainQuery = this.options.explainQuery; if (!explainQuery) { throw new NotFoundException('Query EXPLAIN is not configured.'); } if (body === undefined || body === null || typeof body.entryId !== 'string') { throw new BadRequestException('Body must include an "entryId".'); } const entry = await this.storage.find(body.entryId); if (!entry || entry.type !== EntryType.Query || !isQueryContentWithSql(entry.content)) { throw new NotFoundException('No query entry with SQL for that id.'); } try { // Pass the SQL/bindings EXACTLY as captured — plans carry no user data. const plan = await explainQuery(entry.content.sql, entry.content.bindings ?? []); return { plan }; } catch (error) { const message = error instanceof Error ? error.message : 'EXPLAIN failed.'; throw new ServiceUnavailableException({ message }); } } // ── AI exception diagnosis ────────────────────────────────────────────────── // Read-shaped ANALYSIS (it produces a markdown explanation, mutates no // Telescope state and runs no destructive action), so — like `explain` — it // sits behind the normal dashboard read guard, NOT the default-deny // authorizeAction mutation gate. 404 when AI isn't configured or the entry // isn't an exception; 502 when the diagnoser fails (a safe, generic message — // the model's raw error is never surfaced to the dashboard). async diagnose(id, force) { const coordinator = this.service.diagnosisCoordinator; if (coordinator === null) { throw new NotFoundException('AI diagnosis is not configured.'); } const entry = await this.storage.find(id); if (entry === null || (entry.type !== EntryType.Exception && entry.type !== EntryType.ClientException)) { throw new NotFoundException('No exception entry with that id.'); } const occurrences = await this.countExceptionFamily(entry.type, entry.familyHash); try { return await coordinator.diagnose(entry, occurrences, force === 'true'); } catch { // The diagnoser rejected (timeout / model error). Surface a generic 502; // the raw model error may carry provider internals, so it's never leaked. throw new ServiceUnavailableException({ message: 'AI diagnosis failed.' }); } } // Read-only companion to the POST above: returns the ALREADY-cached diagnosis // for this entry's family, if one exists, WITHOUT ever computing a new one (no // model call, no token cost). The detail page fetches this on open so an // auto-mode (or previously on-demand) diagnosis shows immediately instead of a // bare "Diagnose with AI" button. Same guards/404 semantics as the POST: // - 404 when AI isn't configured or the entry isn't an exception; // - 200 `{ markdown, cached: true }` when a diagnosis is cached; // - 204 (empty) when none is cached yet (the family hasn't been diagnosed). // The 204 — not a 200-with-null — keeps "nothing cached" unambiguous on the // client and never tempts a reader into treating a null body as a result. async cachedDiagnosis(id, res) { const coordinator = this.service.diagnosisCoordinator; if (coordinator === null) { throw new NotFoundException('AI diagnosis is not configured.'); } const entry = await this.storage.find(id); if (entry === null || (entry.type !== EntryType.Exception && entry.type !== EntryType.ClientException)) { throw new NotFoundException('No exception entry with that id.'); } const markdown = coordinator.peekCached(entry); if (markdown === null) { // No diagnosis cached for this family yet. 204 No Content — never invoke // the diagnoser from a GET (a read must stay free and side-effect-free). // `@Res` is `unknown` to keep express types out of the public signature // (same convention as the auth controller); narrow before setting status. setResponseStatus(res, 204); return undefined; } return { markdown, cached: true }; } /** Count entries of this exception family in the trailing 24h (>= 1). */ async countExceptionFamily(type, familyHash) { if (familyHash === null) return 1; const after = new Date(Date.now() - durationToMs('24h')); const result = await collectEntriesInWindow(this.storage, { type, familyHash, after, omitContent: true }, { scanCap: 10_000 }); return Math.max(1, result.entries.length); } // ── CPU profiling (flamegraphs) ───────────────────────────────────────────── /** * Profiler status for the dashboard's Profiles tab: whether the feature is * enabled, the sample rate, and current capture activity. Read-shaped — sits * behind the normal read guard. When profiling is disabled the tab shows an * "enable `profiling`" empty state from this payload. */ profilesStatus() { return this.profiler.status(); } /** * List captured CPU profiles, newest-first, WITHOUT their (potentially large) * frame trees — `omitContent` keeps the list cheap; the tree is fetched per * profile via {@link profile}. Read-shaped. */ async profiles(limit) { return this.storage.get({ type: CPU_PROFILE_ENTRY_TYPE, omitContent: true, ...(limit !== undefined && Number.isFinite(Number(limit)) ? { limit: Number(limit) } : {}), }); } /** * Fetch ONE profile's full frame tree (the flamegraph payload). 404 when the * id is unknown or not a cpu_profile entry. Read-shaped. */ async profile(id) { const entry = await this.storage.find(id); if (entry === null || entry.type !== CPU_PROFILE_ENTRY_TYPE) { throw new NotFoundException('No CPU profile with that id.'); } return entry; } /** * Arm an on-demand capture of the next N requests (optionally only those whose * normalized route matches `label`, e.g. "GET /users/:id"). A MUTATION-shaped * trigger — it incurs real profiling overhead — so it stays behind the same * default-deny `authorizeAction` gate as prune/replay. 400 when profiling is * disabled (so the dashboard can explain why nothing happens). */ arm(body) { if (!this.options.authorizeAction) { throw new ForbiddenException('Mutations are disabled (no authorizeAction configured).'); } if (!this.profiler.status().enabled) { throw new BadRequestException('CPU profiling is disabled (set `profiling.enabled`).'); } const count = body?.count !== undefined ? Number(body.count) : 1; if (!Number.isFinite(count) || count <= 0) { throw new BadRequestException('`count` must be a positive number.'); } return this.profiler.arm({ count, ...(typeof body?.label === 'string' && body.label !== '' ? { label: body.label } : {}), }); } async clear() { await this.storage.clear(); return { cleared: true }; } }; __decorate([ Get('entries'), __param(0, Query()), __metadata("design:type", Function), __metadata("design:paramtypes", [Object]), __metadata("design:returntype", Promise) ], TelescopeController.prototype, "list", null); __decorate([ Get('entries/:id'), __param(0, Param('id')), __metadata("design:type", Function), __metadata("design:paramtypes", [String]), __metadata("design:returntype", Promise) ], TelescopeController.prototype, "show", null); __decorate([ Get('entries/:id/replay'), __param(0, Param('id')), __param(1, Req()), __metadata("design:type", Function), __metadata("design:paramtypes", [String, Object]), __metadata("design:returntype", Promise) ], TelescopeController.prototype, "replay", null); __decorate([ Get('batches/:id'), __param(0, Param('id')), __metadata("design:type", Function), __metadata("design:paramtypes", [String]), __metadata("design:returntype", Promise) ], TelescopeController.prototype, "batch", null); __decorate([ Get('tags'), __param(0, Query('prefix')), __metadata("design:type", Function), __metadata("design:paramtypes", [String]), __metadata("design:returntype", Promise) ], TelescopeController.prototype, "tags", null); __decorate([ Get('queues'), __param(0, Query('window')), __metadata("design:type", Function), __metadata("design:paramtypes", [String]), __metadata("design:returntype", Promise) ], TelescopeController.prototype, "queues", null); __decorate([ Get('pulse'), __param(0, Query('window')), __metadata("design:type", Function), __metadata("design:paramtypes", [String]), __metadata("design:returntype", Promise) ], TelescopeController.prototype, "pulseHealth", null); __decorate([ Get('timeseries'), __param(0, Query('window')), __param(1, Query('buckets')), __param(2, Query('type')), __param(3, Query('tag')), __metadata("design:type", Function), __metadata("design:paramtypes", [String, String, String, String]), __metadata("design:returntype", Promise) ], TelescopeController.prototype, "timeseries", null); __decorate([ Get('traces'), __param(0, Query('window')), __param(1, Query('limit')), __metadata("design:type", Function), __metadata("design:paramtypes", [String, String]), __metadata("design:returntype", Promise) ], TelescopeController.prototype, "traces", null); __decorate([ Get('traces/:traceId/waterfall'), __param(0, Param('traceId')), __metadata("design:type", Function), __metadata("design:paramtypes", [String]), __metadata("design:returntype", Promise) ], TelescopeController.prototype, "waterfall", null); __decorate([ Get('stats'), __param(0, Query('type')), __param(1, Query('window')), __param(2, Query('buckets')), __metadata("design:type", Function), __metadata("design:paramtypes", [String, String, String]), __metadata("design:returntype", Promise) ], TelescopeController.prototype, "stats", null); __decorate([ Get('queues/live'), __metadata("design:type", Function), __metadata("design:paramtypes", []), __metadata("design:returntype", Promise) ], TelescopeController.prototype, "liveQueues", null); __decorate([ Get('schedules/live'), __metadata("design:type", Function), __metadata("design:paramtypes", []), __metadata("design:returntype", Promise) ], TelescopeController.prototype, "liveSchedules", null); __decorate([ Get('queues/live/:driver/:queue/counts'), __param(0, Param('driver')), __param(1, Param('queue')), __metadata("design:type", Function), __metadata("design:paramtypes", [String, String]), __metadata("design:returntype", Promise) ], TelescopeController.prototype, "liveCounts", null); __decorate([ Get('queues/live/:driver/:queue/jobs'), __param(0, Param('driver')), __param(1, Param('queue')), __param(2, Query('state')), __param(3, Query('cursor')), __param(4, Query('limit')), __metadata("design:type", Function), __metadata("design:paramtypes", [String, String, String, String, String]), __metadata("design:returntype", Promise) ], TelescopeController.prototype, "liveJobs", null); __decorate([ Get('queues/live/:driver/:queue/jobs/:id'), __param(0, Param('driver')), __param(1, Param('queue')), __param(2, Param('id')), __metadata("design:type", Function), __metadata("design:paramtypes", [String, String, String]), __metadata("design:returntype", Promise) ], TelescopeController.prototype, "liveJob", null); __decorate([ Post('queues/live/:driver/:queue/jobs/:id/:action'), HttpCode(200), UseGuards(TelescopeActionGuard), __param(0, Param('driver')), __param(1, Param('queue')), __param(2, Param('id')), __param(3, Param('action')), __metadata("design:type", Function), __metadata("design:paramtypes", [String, String, String, String]), __metadata("design:returntype", Promise) ], TelescopeController.prototype, "jobAction", null); __decorate([ Post('queues/live/:driver/:queue/actions/:action'), HttpCode(200), UseGuards(TelescopeActionGuard), __param(0, Param('driver')), __param(1, Param('queue')), __param(2, Param('action')), __param(3, Query('state')), __metadata("design:type", Function), __metadata("design:paramtypes", [String, String, String, String]), __metadata("design:returntype", Promise) ], TelescopeController.prototype, "queueAction", null); __decorate([ Post('queues/live/:driver/:queue/enqueue'), HttpCode(200), UseGuards(TelescopeActionGuard), __param(0, Param('driver')), __param(1, Param('queue')), __param(2, Body()), __metadata("design:type", Function), __metadata("design:paramtypes", [String, String, Object]), __metadata("design:returntype", Promise) ], TelescopeController.prototype, "enqueue", null); __decorate([ Get('meta'), __metadata("design:type", Function), __metadata("design:paramtypes", []), __metadata("design:returntype", Promise) ], TelescopeController.prototype, "meta", null); __decorate([ Get('server-stats'), __metadata("design:type", Function), __metadata("design:paramtypes", []), __metadata("design:returntype", Object) ], TelescopeController.prototype, "serverStatsSnapshot", null); __decorate([ Get('server-stats/history'), __metadata("design:type", Function), __metadata("design:paramtypes", []), __metadata("design:returntype", Object) ], TelescopeController.prototype, "serverStatsHistory", null); __decorate([ Get('health'), __metadata("design:type", Function), __metadata("design:paramtypes", []), __metadata("design:returntype", Object) ], TelescopeController.prototype, "health", null); __decorate([ Get('ext/:ext/data/:provider'), __param(0, Param('ext')), __param(1, Param('provider')), __param(2, Query()), __metadata("design:type", Function), __metadata("design:paramtypes", [String, String, Object]), __metadata("design:returntype", Promise) ], TelescopeController.prototype, "extData", null); __decorate([ Get('retention'), __metadata("design:type", Function), __metadata("design:paramtypes", []), __metadata("design:returntype", Object) ], TelescopeController.prototype, "retention", null); __decorate([ Get('prunes'), __metadata("design:type", Function), __metadata("design:paramtypes", []), __metadata("design:returntype", Object) ], TelescopeController.prototype, "prunes", null); __decorate([ Post('retention/prune'), HttpCode(200), __metadata("design:type", Function), __metadata("design:paramtypes", []), __metadata("design:returntype", Promise) ], TelescopeController.prototype, "prune", null); __decorate([ Post('queries/explain'), HttpCode(200), __param(0, Body()), __metadata("design:type", Function), __metadata("design:paramtypes", [Object]), __metadata("design:returntype", Promise) ], TelescopeController.prototype, "explain", null); __decorate([ Post('exceptions/:id/diagnose'), HttpCode(200), __param(0, Param('id')), __param(1, Query('force')), __metadata("design:type", Function), __metadata("design:paramtypes", [String, String]), __metadata("design:returntype", Promise) ], TelescopeController.prototype, "diagnose", null); __decorate([ Get('exceptions/:id/diagnosis'), __param(0, Param('id')), __param(1, Res({ passthrough: true })), __metadata("design:type", Function), __metadata("design:paramtypes", [String, Object]), __metadata("design:returntype", Promise) ], TelescopeController.prototype, "cachedDiagnosis", null); __decorate([ Get('profiles/status'), __metadata("design:type", Function), __metadata("design:paramtypes", []), __metadata("design:returntype", void 0) ], TelescopeController.prototype, "profilesStatus", null); __decorate([ Get('profiles'), __param(0, Query('limit')), __metadata("design:type", Function), __metadata("design:paramtypes", [String]), __metadata("design:returntype", Promise) ], TelescopeController.prototype, "profiles", null); __decorate([ Get('profiles/:id'), __param(0, Param('id')), __metadata("design:type", Function), __metadata("design:paramtypes", [String]), __metadata("design:returntype", Promise) ], TelescopeController.prototype, "profile", null); __decorate([ Post('profiles/arm'), HttpCode(200), __param(0, Body()), __metadata("design:type", Function), __metadata("design:paramtypes", [Object]), __metadata("design:returntype", Object) ], TelescopeController.prototype, "arm", null); __decorate([ Delete('entries'), __metadata("design:type", Function), __metadata("design:paramtypes", []), __metadata("design:returntype", Promise) ], TelescopeController.prototype, "clear", null); TelescopeController = __decorate([ UseGuards(TelescopeGuard), Controller('telescope/api'), __param(0, Inject(TELESCOPE_STORAGE)), __param(1, Inject(TelescopeService)), __param(2, Inject(QueueMetricsService)), __param(3, Inject(TimeseriesService)), __param(4, Inject(TracesService)), __param(5, Inject(StatsService)), __param(6, Inject(ServerStatsService)), __param(7, Inject(PulseService)), __param(8, Inject(ProfilerService)), __param(9, Inject(QueueManagerRegistry)), __param(10, Inject(ScheduleManagerRegistry)), __param(11, Inject(TELESCOPE_OPTIONS)), __param(12, Inject(TELESCOPE_EXTENSIONS)), __param(13, Inject(TELESCOPE_CONFIG)), __param(14, Inject(TelescopePruner)), __metadata("design:paramtypes", [Object, TelescopeService, QueueMetricsService, TimeseriesService, TracesService, StatsService, ServerStatsService, PulseService, ProfilerService, QueueManagerRegistry, ScheduleManagerRegistry, Object, ExtensionRegistry, Object, TelescopePruner, ModuleRef]) ], TelescopeController); export { TelescopeController }; /** * Set an HTTP status on an express-like response without importing express into * the controller's public signatures (we type `@Res` as `unknown`, matching the * auth controller's convention). Narrows via a structural check rather than a * cast so the no-unsafe-typing rule holds; a no-op if the object lacks `.status`. */ function setResponseStatus(response, status) { if (response !== null && typeof response === 'object' && 'status' in response && typeof response.status === 'function') { response.status(status); } } //# sourceMappingURL=telescope.controller.js.map