UNPKG

@dudousxd/nestjs-telescope

Version:

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

155 lines 7.07 kB
// packages/core/src/config/resolve-config.ts import { hostname } from 'node:os'; import { z } from 'zod'; import { resolveProfilingConfig } from '../profiling/profiling-config.js'; import { BUILTIN_TAGGERS } from '../tagging/tagger.js'; import { normalizeTelescopePath } from './normalize-path.js'; import { durationToMs } from './parse-duration.js'; const RATE = z.number().min(0).max(1); const SAMPLING_RULE = z.object({ rate: RATE, keepErrors: z.boolean().optional(), keepSlowMs: z.number().nonnegative().optional(), }); const PER_TYPE_SAMPLING = z.union([RATE, SAMPLING_RULE]); const optionsSchema = z.object({ enabled: z.boolean().default(true), sampling: z.union([RATE, z.record(PER_TYPE_SAMPLING)]).optional(), recorder: z .object({ bufferSize: z.number().int().positive().default(10_000), flushIntervalMs: z.number().int().positive().default(1_000), flushBatchSize: z.number().int().positive().default(500), retryDelayMs: z.number().int().nonnegative().default(1_000), }) .default({}), prune: z .object({ after: z.union([z.number().positive(), z.string()]), keepLast: z.number().int().nonnegative().optional(), intervalMs: z.number().int().positive().default(60_000), perType: z.record(z.union([z.number().positive(), z.string()])).optional(), batchSize: z.number().int().positive().default(1_000), maxBatchesPerCycle: z.number().int().positive().default(50), batchPauseMs: z.number().int().nonnegative().default(50), lockTtlMs: z.number().int().positive().optional(), // `lock` is a boolean OR a runtime object with a method; zod could only // assert "callable", and the precise TelescopePruneLock shape is already // enforced by PruneOptions at the call site. Left out of the schema (zod // strips it) and read from the typed options object below, exactly as // `archive.sink` is. }) .optional(), archive: z .object({ types: z.array(z.string()).nonempty(), // The sink is a runtime function; zod can only assert it is callable. The // precise `(entries: Entry[]) => Promise<void>` shape is enforced by the // TelescopeCoreOptions type at the call site, so we copy it through from // the original options object below rather than from the parsed value. sink: z.function(), batchSize: z.number().int().positive().default(500), maxBatchesPerCycle: z.number().int().positive().default(10), }) .optional(), instanceId: z.string().optional(), }); /** * Builds a clean {@link SamplingRule}, omitting optional keys that are absent * rather than setting them to `undefined` — required under * `exactOptionalPropertyTypes`, since zod types optionals as `T | undefined`. */ function toSamplingRule(rule) { const normalized = { rate: rule.rate }; if (rule.keepErrors !== undefined) { normalized.keepErrors = rule.keepErrors; } if (rule.keepSlowMs !== undefined) { normalized.keepSlowMs = rule.keepSlowMs; } return normalized; } function normalizeSampling(sampling) { if (sampling === undefined) return {}; if (typeof sampling === 'number') return { default: sampling }; const normalized = {}; for (const [type, rule] of Object.entries(sampling)) { normalized[type] = typeof rule === 'number' ? rule : toSamplingRule(rule); } return normalized; } export function resolveConfig(options) { const parsed = optionsSchema.parse(options); const resolved = { enabled: parsed.enabled, path: normalizeTelescopePath(options.path), redact: options.redact ?? {}, sampling: normalizeSampling(parsed.sampling), recorder: parsed.recorder, taggers: [...BUILTIN_TAGGERS, ...(options.taggers ?? [])], instanceId: parsed.instanceId ?? hostname(), profiling: resolveProfilingConfig(options.profiling), ...(options.traceContext ? { traceContext: options.traceContext } : {}), ...(options.traceLink ? { traceLink: options.traceLink } : {}), }; if (parsed.prune) { // Resolve per-type overrides through the SAME duration parser as `after`, so // a bad per-type duration fails the boot here rather than silently skipping // that type at prune time. Absent `perType` yields `{}` → identical to the // pre-existing single-cutoff behaviour. const perTypeMs = {}; if (parsed.prune.perType) { for (const [type, duration] of Object.entries(parsed.prune.perType)) { perTypeMs[type] = durationToMs(duration); } } // `lock: false` disables locking outright; a lock OBJECT is the host's own // implementation; anything else (omitted, or `true`) means "use the storage // lease if the provider has one". `lock` is typed `boolean | // TelescopePruneLock`, so the boolean check narrows the object arm with no // assertion. const lockOption = options.prune?.lock; const lockEnabled = lockOption !== false; const hostLock = typeof lockOption === 'boolean' ? undefined : lockOption; const pruneEntry = { afterMs: durationToMs(parsed.prune.after), intervalMs: parsed.prune.intervalMs, perTypeMs, batchSize: parsed.prune.batchSize, maxBatchesPerCycle: parsed.prune.maxBatchesPerCycle, batchPauseMs: parsed.prune.batchPauseMs, // Derived rather than a flat literal default: the TTL is only meaningful // relative to the tick rate — it answers "how long may the fleet go // unpruned after a holder is killed" — so it tracks `intervalMs`, with a // floor so a very fast interval cannot produce a lease too short to // outlive its own cycle. lockTtlMs: parsed.prune.lockTtlMs ?? Math.max(parsed.prune.intervalMs * 3, 60_000), lockEnabled, }; if (hostLock !== undefined) { pruneEntry.lock = hostLock; } if (parsed.prune.keepLast !== undefined) { pruneEntry.keepLast = parsed.prune.keepLast; } resolved.prune = pruneEntry; } // `archive` carries a runtime `sink` function whose precise signature zod can't // express; take the function from the original (typed) options object so the // resolved config keeps the exact `(Entry[]) => Promise<void>` type, no cast. if (parsed.archive && options.archive) { resolved.archive = { types: new Set(parsed.archive.types), sink: options.archive.sink, batchSize: parsed.archive.batchSize, maxBatchesPerCycle: parsed.archive.maxBatchesPerCycle, }; } if (options.filter !== undefined) { resolved.filter = options.filter; } return resolved; } //# sourceMappingURL=resolve-config.js.map