@dudousxd/nestjs-telescope
Version:
Laravel Telescope-style observability console for NestJS — core: watchers, recorder, correlation, SQLite store, headless API.
262 lines • 12.7 kB
TypeScript
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