UNPKG

@dudousxd/nestjs-telescope

Version:

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

262 lines 12.7 kB
import type { Entry } from '../entry/entry.js'; import type { ProfilingOptions, ResolvedProfilingConfig } from '../profiling/profiling-config.js'; import type { TelescopePruneLock } from '../prune/prune-lock.js'; import type { RedactOptions } from '../redaction/redact.js'; import type { StorageProvider } from '../storage/storage-provider.js'; import type { Tagger } from '../tagging/tagger.js'; import type { TraceContextProvider } from '../trace/trace-context-provider.js'; export type Duration = number | string; /** * Export-before-prune: hand doomed entries to a host-owned `sink` (S3, a data * lake, cold storage…) BEFORE the pruner deletes them, so retention shrinks the * live store without losing the data that matters. * * The contract is "archive THEN delete, per type, per cycle": * - Only entry types listed in `types` are archived; every other type prunes * normally and is unaffected by archiving. * - For each archived type the pruner fetches entries older than THAT type's * cutoff (its `perType` override, else the global `after`) and streams them to * `sink` in `batchSize` chunks. The type's entries are deleted ONLY after the * sink resolves for all of its batches. * - If the sink throws/rejects, that type is NOT deleted this cycle (the doomed * entries survive to be retried next cycle); the error is logged (rate-limited * to once per cycle) and the rest of the prune cycle continues. A failing sink * can never crash the host or stop the pruner. * - Work per type per cycle is bounded (see {@link ArchiveOptions.maxBatchesPerCycle}); * any remainder is picked up on the next tick. */ export interface ArchiveOptions { /** Entry types to archive before pruning. Types not listed prune normally. */ types: string[]; /** * Receives a batch of doomed entries. MUST resolve only once the batch is * durably stored; a rejection makes the pruner keep those entries (retry next * cycle). Runs OUTSIDE the host request path — it may do slow network I/O. */ sink: (entries: Entry[]) => Promise<void>; /** Entries handed to `sink` per call. Default 500. */ batchSize?: number; /** * Hard cap on the number of `sink` batches per archived type per cycle, so a * large backlog can never make one tick do unbounded work (which would stall * the unref'd timer and pile up memory). Leftover is archived next cycle. * Default 10. */ maxBatchesPerCycle?: number; } export interface PruneOptions { after: Duration; keepLast?: number; intervalMs?: number; /** * Per-entry-type retention overrides. Each key is an entry type (e.g. * `'exception'`, `'request'`) and the value is a {@link Duration} cutoff for * THAT type only. Types absent from this map fall back to the global `after`, * so omitting `perType` reproduces the exact pre-existing global behaviour. * * Each cutoff is validated at module init the same way `after` is — an * unparseable duration is a boot error, not a silent runtime skip. * * @example keep exceptions for a week, everything else for the global default * ```ts * prune: { after: '5m', intervalMs: 60_000, perType: { exception: '7d' } } * ``` */ perType?: Record<string, Duration>; /** * Rows deleted per bounded `DELETE` when the storage provider supports batched * pruning (`pruneScopedBatch`). Default `1000`. * * This is a LOCK-DURATION knob, not a throughput knob. One unbounded delete * over a big table can hold row locks for tens of minutes and every other * writer queues behind it; a thousand-row delete commits in milliseconds and * releases them. Larger values amortise round-trips but lengthen the lock * each statement holds — which is the thing being fixed. Smaller values are * gentler and slower. 1000 keeps the id list well inside every driver's bound * parameter limit. */ batchSize?: number; /** * Hard cap on batches per prune SCOPE per cycle. Default `50` — so ~50k rows * per scope per cycle at the default `batchSize`. * * Without a ceiling, a table that has fallen badly behind turns a single tick * into an hour-long loop, which is the failure mode batching is meant to end, * merely spelled differently. With one, a backlog drains over several cycles * and every cycle has a predictable, bounded cost. Reaching the ceiling logs a * warning; the levers are this value, `batchSize`, and `intervalMs`. */ maxBatchesPerCycle?: number; /** * Pause between consecutive batches, in ms. Default `50`. Set `0` to delete * back-to-back. * * Only paid BETWEEN batches, so a store that drains in one batch — the healthy * steady state — never pauses at all and this default costs nothing. It exists * for the unhealthy case: on a small instance with a fixed IOPS budget, a tight * delete loop can starve the application even though no single statement holds * locks for long. The pause bounds the pruner's duty cycle. At the defaults a * fully saturated cycle adds ~2.5s of pausing, against a 60s interval. */ batchPauseMs?: number; /** * Cross-process prune lock, so a FLEET prunes once per cycle instead of once * per pod. The per-process in-flight guard cannot see other replicas; this can. * * - omitted / `true` — use the storage-backed lease when the configured * provider supports one (`tryAcquireLease` + `releaseLease`), otherwise no * locking. This is the default. * - `false` — never lock. Every replica prunes on its own schedule (the * behaviour before this option existed). * - a {@link TelescopePruneLock} — use this host-supplied implementation, e.g. * one backed by a job engine's singleton mutex. Read the interface's doc * before writing one; the contract is short but it is exact. * * The lock is ADVISORY. Losing it costs a duplicated delete, never a wrong * result, so the pruner fails OPEN: if the lock mechanism itself is broken it * warns and prunes anyway rather than letting retention silently stop. */ lock?: boolean | TelescopePruneLock; /** * Lease TTL for the prune lock, in ms. Defaults to `max(intervalMs * 3, 60_000)`. * * The TTL only matters when a holder DIES mid-cycle (a normal cycle releases in * a `finally`), so it is the answer to "how long may the whole fleet go without * pruning after one pod is killed". Three intervals is short enough to recover * quickly and long enough that a slow-but-alive cycle does not routinely lose * its own lease. Losing it early is not an error — the second pruner simply * deletes rows the first is already deleting. */ lockTtlMs?: number; } export interface RecorderTuning { bufferSize?: number; /** Consumed by the NestJS integration layer's flush scheduler; the core Recorder itself does not start a timer. */ flushIntervalMs?: number; /** Consumed by the NestJS integration layer's flush scheduler; the core Recorder itself does not start a timer. */ flushBatchSize?: number; /** * Backoff before the single bounded retry of a failed `storage.store()` batch. * On the first rejection the Recorder waits this long, then retries ONCE; a * second failure drops the batch (`storeFailedDropped`). Default `1000`ms. */ retryDelayMs?: number; } /** * Tail-sampling rule for a single entry type. Keeps `rate` of the noise but * always retains the entries that matter — errors and slow ones. */ export interface SamplingRule { /** Base keep-rate 0–1 applied to ordinary entries of this type. */ rate: number; /** When true, always keep entries that look like errors (see {@link isErrorEntry}). */ keepErrors?: boolean; /** When set, always keep entries whose `durationMs` is at least this value. */ keepSlowMs?: number; } /** * Per-type sampling configuration. Each type maps to either a bare keep-rate * (uniform down-sampling, unchanged behaviour) or a {@link SamplingRule} object * (tail-sampling: keep a fraction but always retain errors / slow entries). */ export type SamplingConfig = Record<string, number | SamplingRule>; /** Author-facing options. NestJS-specific fields (watchers, authorizer, path) are * layered on in the Nest integration package; this shape is the agnostic subset. */ export interface TelescopeCoreOptions { enabled?: boolean; storage?: StorageProvider; redact?: RedactOptions; /** * Per-entry-type keep rate (0–1). A bare number is normalised to `{ default: <rate> }`, * which applies to every entry type that lacks a specific rate override. * * A per-type value may also be a {@link SamplingRule} object to tail-sample: * keep `rate` of the noise but always retain errors (`keepErrors`) and slow * entries (`keepSlowMs`). Bare-number entries keep their exact prior behaviour. */ sampling?: number | SamplingConfig; recorder?: RecorderTuning; prune?: PruneOptions; /** * Export captured entries to a host-owned sink right before the pruner deletes * them. See {@link ArchiveOptions}. Only meaningful alongside `prune`: with no * pruning nothing is ever doomed, so nothing is archived. */ archive?: ArchiveOptions; taggers?: Tagger[]; instanceId?: string; filter?: (entry: Entry) => boolean; /** * On-demand CPU flamegraph profiling. STRICTLY opt-in and OFF by default; when * absent or `{ enabled: false }` the profiler is never constructed, the Node * `inspector` module is never loaded, and the request path is untouched beyond * a single boolean check. See {@link ProfilingOptions}. */ profiling?: ProfilingOptions; /** Optional ambient trace-context source (e.g. OtelTraceContextProvider). */ traceContext?: TraceContextProvider; /** UI trace-link URL template with {traceId}/{spanId} placeholders. */ traceLink?: string; /** * Mount path for the dashboard + API (no leading/trailing slash needed). * Defaults to `'telescope'` — when unset everything behaves exactly as before * (dashboard at `/telescope`, API at `/telescope/api`). Set e.g. * `'observability'` for `/observability` + `/observability/api`. */ path?: string; } export interface ResolvedCoreConfig { enabled: boolean; /** Normalized mount segment (no leading/trailing slash). Default `'telescope'`. */ path: string; redact: RedactOptions; sampling: SamplingConfig; recorder: Required<RecorderTuning>; prune?: { afterMs: number; keepLast?: number; intervalMs: number; /** * Resolved per-type cutoffs in ms, keyed by entry type. Empty when the host * supplied no `perType` overrides (the common case), in which case the * pruner runs a single global cycle exactly as before. */ perTypeMs: Record<string, number>; /** * Batched-delete tuning, all REQUIRED here even though every author-facing * field is optional: `resolveConfig` is the single place defaults are * applied, so a future field added to `PruneOptions` and forgotten there is * a compile error rather than an `undefined` that reaches the delete loop. */ batchSize: number; maxBatchesPerCycle: number; batchPauseMs: number; lockTtlMs: number; /** * Whether cross-process locking is wanted at all. `false` when the host set * `prune.lock: false`; otherwise `true` — which still yields no locking when * neither a host lock nor a lease-capable provider is available. */ lockEnabled: boolean; /** Host-supplied lock. Absent means "use the storage lease if there is one". */ lock?: TelescopePruneLock; }; /** * Resolved archive config (with `batchSize`/`maxBatchesPerCycle` defaulted), * or absent when the host configured no `archive`. */ archive?: { types: Set<string>; sink: (entries: Entry[]) => Promise<void>; batchSize: number; maxBatchesPerCycle: number; }; taggers: Tagger[]; instanceId: string; filter?: (entry: Entry) => boolean; /** Fully-defaulted profiling config. Always present; `enabled: false` by default. */ profiling: ResolvedProfilingConfig; traceContext?: TraceContextProvider; traceLink?: string; } //# sourceMappingURL=options.d.ts.map