import type { SamplingConfig } from '../config/options.js';
import type { ContextAccessor } from '../context/context-accessor.js';
import type { TelescopeContext } from '../context/telescope-context.js';
import type { Entry, RecordInput } from '../entry/entry.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 DropReason = 'overflow' | 'store-failed' | 'record-error';
/**
 * Cheap, hot-path-safe self-observability counters describing the Recorder's
 * own behaviour. Every field is a plain integer/number snapshot — no per-call
 * timing is taken on the synchronous `record()` path. Flush timing is measured
 * off the host path inside `flush()`.
 */
export interface RecorderSelfMetrics {
    /** Total entries that passed sampling+filter and were buffered. */
    recorded: number;
    /** Ring-buffer capacity. */
    bufferSize: number;
    /** Entries currently held in the ring. */
    bufferUsed: number;
    /** Maximum `bufferUsed` ever observed. */
    bufferHighWater: number;
    /** Number of flushes that drained at least one entry. */
    flushes: number;
    /** Cumulative entries drained across all flushes. */
    flushedEntries: number;
    /**
     * Number of flush batches that failed their first `store()` and were retried
     * once. A non-zero, climbing value means the storage backend is flaky; pair
     * it with `storeFailedDropped` to see how many retries still ended in a drop.
     */
    retriedFlushes: number;
    /** Wall-clock duration of the most recent draining flush, or null. */
    lastFlushMs: number | null;
    /** Largest flush duration ever observed, or null. */
    maxFlushMs: number | null;
    /** Cumulative flush duration across all draining flushes. */
    totalFlushMs: number;
    /** Entries evicted because the ring was full. */
    overflowDropped: number;
    /** Entries dropped because the storage provider rejected a batch. */
    storeFailedDropped: number;
    /** Sum of all drop buckets (overflow + store-failed + record-error). */
    droppedCount: number;
    /**
     * Entries whose content hit a redaction bound and was clipped (depth, string,
     * array, or node budget). A non-zero, climbing value means hosts are capturing
     * fat content (e.g. ORM entities) — a signal to project lighter content or add
     * per-type `sampling`. The entry is still recorded; only its payload is bounded.
     */
    truncatedCount: number;
}
export interface RecorderOptions {
    storage: StorageProvider;
    context: TelescopeContext;
    instanceId: string;
    taggers: Tagger[];
    redact: RedactOptions;
    /**
     * Per-type sampling. A value may be a bare keep-rate (0..1) or a
     * {@link SamplingRule} object that additionally keeps errors / slow entries.
     * Missing type ⇒ falls back to `default`, else keep.
     */
    sampling: SamplingConfig;
    bufferSize: number;
    /**
     * Maximum entries handed to a single `storage.store()` call. When set and a
     * flush drains more than this, the drained entries are sliced into sequential
     * chunks of at most this size, each stored (with its own bounded retry) in
     * oldest→newest order. Bounds the per-store payload so a large flush can't
     * spike storage memory/latency. When unset (or >= the drained count) the whole
     * batch is stored in one call, preserving the original behaviour.
     */
    flushBatchSize?: number;
    /**
     * Backoff (ms) before the single bounded retry of a failed `store()` batch.
     * Defaults to 1000ms. The retry happens inside the same in-flight flush
     * promise, so the `flushing` serialization still prevents pileups.
     */
    retryDelayMs?: number;
    now?: () => number;
    random?: () => number;
    /**
     * Injectable delay seam used between the failed first store and its retry.
     * Defaults to an unref'd `setTimeout` so it never keeps the event loop alive.
     * Tests inject a resolved/controllable promise for determinism.
     */
    delay?: (ms: number) => Promise<void>;
    idFactory: () => string;
    /**
     * Called (inside a try/catch) whenever entries are dropped, with the count
     * and reason. A faulty hook will not propagate into the Recorder.
     *
     * Reasons:
     * - `'overflow'`      — the ring buffer was full; the oldest entry was evicted.
     * - `'store-failed'`  — the storage provider rejected a batch; the batch is dropped.
     * - `'record-error'`  — an unexpected error occurred inside `record()`.
     */
    onDrop?: (count: number, reason: DropReason) => void;
    /**
     * Final allow/deny predicate applied to the enriched entry; returning false
     * excludes it (an intentional exclusion, not counted as a drop).
     */
    filter?: (entry: Entry) => boolean;
    /** Optional ambient trace-context source; read once per recorded entry. */
    traceContext?: TraceContextProvider;
    /**
     * Optional, soft-detected `@dudousxd/nestjs-context` accessor (structurally
     * mirrored as {@link ContextAccessor}). When present it enriches each recorded
     * entry as a SECONDARY correlation source, additive to the OTel
     * {@link traceContext}:
     *
     * - **traceId precedence**: an explicit {@link RecordInput.traceId} (when the
     *   watcher already knows its own correlation id) wins over everything. Else
     *   OTel wins, and the context `traceId()` is used ONLY as a FALLBACK when
     *   {@link traceContext} did not yield one for this entry. An existing OTel
     *   trace id is never clobbered, so cross-lib correlation with
     *   durable/notifications (which share nestjs-context) kicks in only when OTel
     *   is absent.
     * - **user/tenant tags**: when available, `user:<Type>#<id>` and
     *   `tenant:<id>` tags are appended (before taggers run, so taggers/filters
     *   can see them) — letting the dashboard group/filter by user and tenant.
     *
     * Read defensively once per entry; a misbehaving accessor degrades to no
     * enrichment and never throws into `record()`.
     */
    contextAccessor?: ContextAccessor;
    /**
     * Best-effort hook fired with the entries a flush JUST persisted (after a
     * successful `store()`, before the next flush). Powers per-flush alert
     * evaluation (the `new-exception` rule) without coupling the Recorder to the
     * alerter. Called inside a try/catch — a faulty hook is swallowed and can never
     * break the flush or the host. NOT called for a batch that failed to store.
     */
    onFlushStored?: (entries: Entry[]) => void | Promise<void>;
    /**
     * Best-effort hook fired with the RAW input of EVERY `record()` call, BEFORE
     * the pause check and sampling — so a metrics consumer sees complete counts
     * even when the entry is sampled out or dropped under overload. Called inside
     * its own try/catch; a throw is swallowed and never counted as a drop, never
     * breaks the synchronous record path. Powers the OTel/Prometheus metrics tap.
     */
    onRecorded?: (input: RecordInput) => void;
}
/**
 * Buffers {@link Entry} objects in a fixed-capacity O(1) ring buffer and
 * periodically flushes them to a {@link StorageProvider}.
 *
 * **Overflow policy** — overflow drops the OLDEST buffered entry (so under
 * sustained overload a batch may be stored without its earliest entries);
 * recent activity is preferred.
 *
 * **Storage failures** — when `store()` rejects, the drained batch is retried
 * exactly ONCE after a bounded backoff (`retryDelayMs`, default 1000ms). Only a
 * second failure drops the batch (fail-open, never grow). The retry runs inside
 * the single in-flight `flushing` promise, so failed batches never pile up and
 * the ring keeps absorbing/evicting meanwhile — memory stays bounded. Drops are
 * surfaced via `onDrop` and the `storeFailedDropped` / `droppedCount` counters;
 * each retried batch increments the `retriedFlushes` self-metric.
 */
export declare class Recorder {
    private readonly options;
    private readonly ring;
    /** Index of the oldest entry in the ring. */
    private head;
    /** Number of valid entries currently held. */
    private count;
    private overflowDrops;
    private storeFailedDrops;
    private recordErrorDrops;
    private recordedCount;
    private highWaterCount;
    private flushCount;
    private flushedEntriesCount;
    /** Flush batches that failed their first store() and were retried once. */
    private retriedFlushCount;
    private lastFlushDurationMs;
    private maxFlushDurationMs;
    private totalFlushDurationMs;
    /** Entries whose content was clipped by a redaction bound (incident guard). */
    private truncatedEntryCount;
    private flushing;
    /**
     * When paused, `record()` becomes a no-op (the entry is dropped, counted as an
     * overflow drop) so a telescope under load can never amplify an incident. Set
     * by the overhead guard when event-loop lag crosses its threshold; cleared
     * when lag recovers. Flushing continues so the buffer still drains.
     */
    private paused;
    private readonly now;
    private readonly random;
    private readonly delay;
    private readonly retryDelayMs;
    /**
     * Redaction key/path Sets compiled ONCE at boot from `options.redact`. Config
     * is immutable after construction, so rebuilding these per entry (in the
     * hottest function) was pure waste — they are precompiled here and reused on
     * every `enrich()` via {@link redactBoundedWith}.
     */
    private readonly redactSpec;
    constructor(options: RecorderOptions);
    get overflowDropped(): number;
    get storeFailedDropped(): number;
    /** Sum of all drop buckets (overflow + store-failed + record-error). */
    get droppedCount(): number;
    /**
     * Number of ring slots still holding an entry reference. After a flush this
     * MUST equal `bufferUsed` (only the live, not-yet-drained entries), never the
     * stale entries a previous flush left behind.
     *
     * @internal Test-only seam to assert `drain()` nulls drained slots so fat
     * entries don't linger in the ring after a flush. Not part of the public API.
     */
    get retainedSlotCount(): number;
    /**
     * Snapshot of the Recorder's own behaviour. All fields are cheap integer
     * counters accumulated on the hot path plus off-path flush timings — no
     * per-record timing is taken, so reading this never taxes `record()`.
     */
    getSelfMetrics(): RecorderSelfMetrics;
    /**
     * On-demand micro-benchmark of the synchronous capture path (sampling check +
     * enrich + filter) on a representative input, WITHOUT enqueuing into the ring
     * or touching storage. Returns the mean nanoseconds per call. Lives here so
     * the "cost per capture" figure is honest yet never instruments live records.
     */
    benchmarkRecordCost(iterations: number): number;
    /**
     * Runs the same sampling+enrich+filter logic as `record()` but discards the
     * enriched entry instead of buffering it. Used only by the benchmark.
     */
    private measureCaptureOnce;
    /**
     * Whether capture is currently paused by the overhead guard. While paused,
     * `record()` drops new entries (counted as an overflow drop) but flushing
     * continues so the buffer drains.
     */
    get isPaused(): boolean;
    /** Pause capture: `record()` becomes a dropping no-op until {@link resume}. */
    pause(): void;
    /** Resume capture after a {@link pause}. */
    resume(): void;
    /** Synchronous, O(1), never throws into the caller. */
    record(input: RecordInput): void;
    /**
     * Drains the buffer and persists via {@link StorageProvider.store}.
     * Concurrent calls share the same in-flight promise — entries added
     * while a flush is running are picked up on the next flush.
     */
    flush(): Promise<void>;
    /**
     * Persists a drained batch, chunked by `flushBatchSize` when that bounds the
     * batch. Each chunk is stored sequentially (oldest→newest) with its own
     * bounded retry; per-chunk rollups + the `onFlushStored` hook fire only for
     * chunks that actually persisted (matching the whole-batch semantics — an
     * alert never fires for a dropped chunk). A chunk's failure does not abort the
     * remaining chunks.
     */
    private storeDrained;
    /**
     * Stores one chunk with bounded retry, then fires per-chunk rollups + the
     * flush hook only when it persisted. Shared by the chunked and whole-batch
     * paths so the post-store side effects are identical.
     */
    private storeChunk;
    /**
     * Persists `drained` with ONE bounded retry. On the first `store()` rejection
     * the Recorder waits `retryDelayMs` (default 1000ms) and retries exactly once;
     * a second failure drops the batch (`storeFailedDropped` + `store-failed`).
     *
     * Hard bounds preserved: this runs INSIDE the single in-flight `flushing`
     * promise, so no second failed batch can ever be queued concurrently, and the
     * ring keeps absorbing/evicting meanwhile — memory stays bounded. Returns
     * whether the batch was ultimately persisted.
     */
    private storeWithRetry;
    /**
     * After a successful entry store, pre-aggregate the same batch into the
     * rollup layer when the storage also implements the {@link RollupStore} SPI.
     * The entries are already persisted, so a rollup failure must NOT be counted
     * as a store failure — it is swallowed independently. Never throws into the
     * flush chain.
     */
    private recordRollupsAfterStore;
    /**
     * Invoke the `onFlushStored` hook with the just-persisted batch. Awaited inside
     * the flush chain so the hook's work (e.g. per-flush alert evaluation) settles
     * before `flush()` resolves, but wrapped so a rejection/throw is swallowed — the
     * entries are already stored and a hook bug must never break the flush.
     */
    private notifyFlushStored;
    /**
     * Invoke the `onRecorded` tap with the raw input. Isolated try/catch: a tap
     * failure is swallowed and is NOT a drop — capture continues unaffected.
     */
    private notifyRecorded;
    /**
     * Tail-sampling decision. Delegates to the shared resolver so the same logic
     * backs both the live path and the benchmark. The hot-path cost is shallow
     * field reads (type, tags, durationMs, content.statusCode/failed) — no walk.
     */
    private passesSampling;
    private enrich;
    /**
     * Reads the optional, soft-detected {@link ContextAccessor} once. Returns the
     * context fallback `traceId` (or `null`) and any `user:`/`tenant:` tags. Every
     * accessor call is wrapped so a misbehaving accessor degrades to empty
     * enrichment and can never throw into `record()` (mirrors the OTel read).
     */
    private readContextEnrichment;
    /**
     * O(1) ring-buffer push. On overflow the oldest entry is evicted so that
     * recent activity is always preserved.
     */
    private push;
    /**
     * Drains all entries from the ring in oldest→newest order and resets it.
     * Returns a plain array for hand-off to storage.
     */
    private drain;
    /**
     * Records off-path flush self-metrics. Only invoked from `flush()`, which has
     * already guaranteed `drainedCount >= 1`, so every call here counts a flush
     * that drained at least one entry.
     */
    private recordFlushMetrics;
    /** Calls `onDrop` inside a try/catch so a faulty hook cannot escape. */
    private notifyDrop;
}
//# sourceMappingURL=recorder.d.ts.map