/** * Shared pattern-extraction lib — run a batch of events through the engine. * * Used by log10x_resolve_batch and log10x_poc_from_siem_submit. Both tools feed * raw log lines through this and get back a structured list of per-pattern * records with aggregated counts, bytes, severity, service, and sample * event bodies. * * Always runs the local 10x engine (a locally-installed `tenx` binary, or * local Docker via LOG10X_TENX_MODE=docker). Events never leave the machine. * * Under the hood this uses the existing dev-cli + cli-output-parser * machinery so the engine contract stays single-source. */ import { type RoundTripResult } from './dev-cli.js'; import { type ReceiverBytesBasis } from './receiver-bytes.js'; export interface ExtractedPattern { /** * Engine's internal templateHash — the templater's structural * fingerprint of the field-set. Used as an INTERNAL join key * between encoded events and their template body. NEVER use this * as a user-facing identity or in mute YAML: the receiver does not * match against templateHash (it matches `symbolMessage` or * `tenx_hash`). Mute targets must use `symbolMessage` (preferred) * or `tenxHash` (patternHash) below. */ hash: string; /** * Engine-emitted Reporter-tier pattern name (symbol-lookup result). * Bound per-event via the `pattern=` anchor on the encoded line * when the engine's `apps/mcp/stdout` config emits it. This is the * default receiver match key (`compactReceiverFieldNames: * [symbolMessage]`) and the preferred user-facing identity. */ symbolMessage?: string; /** * Engine-emitted xxHash64 (11-char base64url) of `symbolMessage`, * carried per-event via the `patternHash=` anchor. Same key the * forwarder enrichment writes to the `tenx_hash` field on events. * Acts as the secondary mute target when symbolMessage isn't * available (receiver must be configured with * `compactReceiverFieldNames: [tenx_hash]`). */ tenxHash?: string; /** Template body with `$` marking variable slots. */ template: string; /** * Dominant service label, from the envelope of the events bound to this * pattern. A record is bound to its input line by position, re-synchronised * on the template's literal ends where the engine dropped a line; a record * whose ends match no line nearby stays unbound and carries no service. */ service?: string; /** Dominant severity (uppercase standard form). */ severity?: string; /** Number of events in this batch matching the pattern. */ count: number; /** Total bytes across all matching events. */ bytes: number; /** * Bytes of the records as they arrived, before any unwrapping, summed over * the bound events: the size a forwarder sends and a platform bills. Only * present when the caller supplied `lineRawBytes`. */ rawBytes?: number; /** * Total bytes that would ship over the wire if these events were * encoded by the engine. Sum of the raw `encoded.log` line bytes * (`~hash,val,val,...` + newline) across this pattern's events. * Measured, not estimated. Used to compute the real compact-byte * ratio in Section 6. * * Optional: missing on older CLI builds without anchored encoded * layout. Consumers treat missing as 0. */ encodedBytes?: number; /** One representative raw event (from the first encoded match). */ sampleEvent: string; /** Per-slot captured values (slot name or positional index → distinct values observed, capped at 20 samples). */ variables: Record; /** * Per-slot TRUE distinct-value count, not capped. The `variables` * field holds at most 20 sample values to keep payloads bounded; * this field carries the real cardinality measurement from the * templater. Use this for unbounded-slot detection — `variables[k].length` * is the sample size, not the cardinality. */ slotDistinctCounts?: Record; /** Epoch ms of the earliest event seen in the pulled window. `undefined` when no envelope timestamps were available. */ firstSeenMs?: number; /** Epoch ms of the latest event seen in the pulled window. */ lastSeenMs?: number; /** * Per-hour event counts within the pulled window. Keyed by epoch-hour * (Math.floor(timestampMs / 3_600_000)). Used by `poc-enrichers` to * compute growth rate, last-24h-vs-window-average acceleration, and * a 24-bucket trajectory. */ eventsByHour?: Record; /** * Events and bytes per envelope (service, status, split attribute) value, * from the records bound to their input line. Present only when the caller * asked for `cells`. */ cells?: PatternCell[]; /** * Bytes the Receiver's regulator counts for these events, summed: each * record as the forwarder sends it, metadata included, continuation lines * joined into the head record (see `receiver-bytes.ts`). */ receiverBytes?: number; /** * The largest `receiverBytes` within any 240-second interval, when the * events carry times: the busiest window whatever its alignment, so a cap * set from it holds wherever the Receiver's window falls. */ receiverPeakWindowBytes?: number; /** The extraction's timed span in seconds, which the peak's rate is taken over. */ receiverSpanSeconds?: number; /** * The bytes a cap compares against, over the capture: the Receiver's * counter for this statement (its message pattern in its container, every * template that shares them), at its busiest window's rate over the timed * span when that is higher than its total. A cap set from it holds that * window. Templates of one counter carry the same value, so rows that merge * take the larger, never the sum. */ receiverCapBytes?: number; } /** One envelope combination a pattern's events arrived with. */ export interface PatternCell { service?: string; /** The status as the platform stored it (Datadog: `info`, `warn`, `error`, ...). */ status?: string; /** A parsed attribute from SPLIT_ATTRIBUTES, when the event carried one. */ attribute?: { name: string; value: string; }; count: number; bytes: number; } /** * Parsed attributes that name the emitting code inside a service. Datadog's * Java integration pipelines (Kafka among them) parse the logger into the * standard attribute `logger.name`, so a filter per value is the native split * below the service. */ export declare const SPLIT_ATTRIBUTES: readonly ["logger.name"]; /** Services whose Datadog integration log pipeline parses the log4j logger at the end of a line into `logger.name`. */ export declare const LOGGER_NAME_PIPELINE_SERVICES: readonly ["kafka"]; /** * The `logger.name` Datadog's Kafka log pipeline parses from an event that * reached the file before intake: the class in parentheses that ends the first * of the event's lines to end that way (the head line before a stack trace, * the last line of a multi-line message). Undefined for other services and for * events with no such line. */ export declare function pipelineSplitAttribute(service: string | undefined, lines: readonly string[]): { name: string; value: string; } | undefined; export interface ExtractedPatterns { patterns: ExtractedPattern[]; totalEvents: number; totalBytes: number; /** How `receiverBytes` was reached: measured on the forwarder's records, or the payload plus an assumed metadata size. */ receiverBytesBasis?: ReceiverBytesBasis; /** The metadata bytes assumed per record on the assumed path. */ recordOverheadBytes?: number; /** Seconds between the first and last event time, when the events carry times. */ receiverTimedSpanSeconds?: number; /** Events whose split attribute came from `pipelineSplitAttribute`, when it was passed. */ pipelineParsedEvents?: number; /** Number of raw input lines the caller passed in. */ inputLineCount: number; /** Wall time spent in the engine (CLI). */ templaterWallTimeMs: number; /** Always `local_cli` — the engine runs on the caller's machine. */ executionMode: 'local_cli'; /** * Which engine build actually produced the numbers: `tenx 1.1.32 (edge)` * for the host binary, `docker log10x/pipeline-10x:latest` otherwise. * * Recorded because the docker default tag is mutable. A report generated * against one `:latest` was not reproducible against another, and the * output said nothing about which build had run. Surfaced in the report's * methodology block so the engine identity travels with the numbers. */ engineBuild?: string; /** * Fraction of patterns that resolved a severity, 0..1. Feeds the * fail-closed guard: when the engine emits no severity, every pattern * reads as reducible and the ERROR safety rail cannot fire. */ severityCoverage: number; /** * Whether the engine's records reconciled against the input lines by count * alone. When false, binding re-synchronised on template ends; the two * counts below say how many records were bound and how many were not. */ positionalBindingExact: boolean; /** Input lines submitted, and the line count the engine's records account for. */ inputLinesSubmitted: number; inputLinesAccountedFor: number; /** Records bound to an input line, records whose binding re-synchronised, and records left unbound. */ recordsBound?: number; recordsResynced?: number; recordsUnbound?: number; /** * The decode pass over this run's output compared with its input, when * `verifyRoundTrip` was asked for. */ roundTrip?: RoundTripResult; } export interface ExtractPatternsOptions { /** * When true, route the engine run through the file-output engine app * (@apps/mcp-file) * instead of the stdout-based @apps/mcp. The CLI writes templates, * encoded events, and aggregated rows to disk; the parser reads * them after the process exits. Scales to multi-million-event pulls * because no stdout buffering is involved. * * Use this for SIEM POC pulls. Default false to preserve existing * `resolve_batch` semantics for small paste-style inputs. */ useFileOutput?: boolean; /** * When true (with `useFileOutput=true`), * split the input into chunks and run multiple tenx processes in * parallel. Each chunk gets its own LOG10X_MCP_RUNTIME_NAME so * output directories don't clash; outputs are merged by * templateHash + tenx_hash. Cuts wall time roughly linearly with * core count on multi-million-event pulls. * * Default false. Enable for SIEM POC pulls > 100MB. */ chunkParallel?: boolean; /** Target chunk size in bytes when chunkParallel=true. Default 32MB. */ chunkTargetBytes?: number; /** Parallelism cap when chunkParallel=true. Default min(cpus-1, 8). */ chunkParallelism?: number; /** * When true, coerceObjectToLine does NOT recurse into nested JSON. * For events whose structured-field variance lives in the envelope * (e.g., CloudWatch fluentd-wrapped events with kubernetes labels * embedded in the message field), the recursive descent would strip * the envelope and lose all that signal. With preserveEnvelope=true, * the function stops after the first unwrap and returns the JSON * string of the envelope so the templater sees the full structure * and extracts slots from across it. * Default: false (back-compat). */ preserveEnvelope?: boolean; /** * Hint: the upstream caller already knows the bucket's pattern_hash * (e.g., pattern_examples queries by it). Older engine builds do not * anchor `patternHash=` on encoded lines, so `rec.tenxHash` ends up * undefined and the defensive value-based filter on the slot loop * cannot fire. Pass this hint to feed the filter regardless. * Default: undefined. */ bucketHashHint?: string; /** * The on-disk byte size of each event, aligned with `events`, for callers * that unwrapped a record before handing it over. Summed per pattern into * `rawBytes`. */ lineRawBytes?: number[]; /** Keep per-pattern envelope cells (`ExtractedPattern.cells`). */ cells?: boolean; /** * With `cells`: the split attribute a platform pipeline would parse from an * event's lines, for events whose envelope carries none (records read before * the platform's intake). Called with the event's service and lines. */ pipelineSplitAttribute?: (service: string | undefined, lines: readonly string[]) => { name: string; value: string; } | undefined; /** * The forwarder's records, aligned with `events`: each record's compact * JSON bytes, and its payload's bytes as a JSON string body. With both, * `receiverBytes` is measured; without, it is the payload plus * `recordOverheadBytes` per record. */ lineRecordBytes?: Array; linePayloadJsonBytes?: Array; /** Metadata bytes assumed per record when the records are not supplied. Default `ASSUMED_RECORD_OVERHEAD_BYTES`. */ recordOverheadBytes?: number; /** Each event's time in epoch ms, aligned with `events`; a gap takes the previous event's time. */ lineTimesMs?: Array; /** * Decode the compact records the engine wrote and compare them with the * text it was given, one more engine run. Not run on a chunked run, where * the output is spread over several directories. */ verifyRoundTrip?: boolean; } /** * The literal text at a template's two ends, with the engine's escaping * undone: `$` marks a variable slot, `$(...)` a timestamp slot, and `/` * escapes the next character. `prefix` is the text before the first slot * and `suffix` the text after the last one; a template with no slot has the * whole text as both. */ export declare function templateEnds(template: string): { prefix: string; suffix: string; lines: number; }; /** * Bind each encoded record to the input line it starts on. Records come back * in input order, each spanning the lines its template folds, so a cursor * walk binds them by position. Where the engine dropped a line the walk * re-synchronises on the template's literal ends: the text before the first * slot starts the record's first line and the text after the last slot ends * its last line. A record whose ends match nothing within `lookahead` lines * stays unbound and the cursor moves on by the record's span. */ export declare function bindRecordsToLines(records: Array<{ templateHash: string; }>, templates: Map, lines: string[], lookahead?: number): { head: Array; bound: number; resyncs: number; unbound: number; }; /** * Templatize a batch of events and return structured pattern records. * * `events` may contain raw strings, objects with a `text`/`message`/`log` * field, or JSON lines. Everything gets stringified into newline-separated * raw text before submission. */ export declare function extractPatterns(events: unknown[], opts?: ExtractPatternsOptions): Promise; /** * Collapse patterns that share the same `symbolMessage`. * * Why this exists: the engine emits one templateHash per distinct * field-set, but several field-sets can resolve to the same Reporter-tier * `symbolMessage` (e.g., the same log line with and without a trailing * variable slot, or audit logs that differ only by which optional fields * are populated). For a user-facing top-cost view the templateHash * distinction is noise: ten rows all named "Kind Event ApiVersion …" * with slightly different identities. From an action standpoint they * also collapse to one mute target (the receiver matches on * `symbolMessage`, not on templateHash). * * Pass each `ExtractedPattern[]` through this before rendering or * enriching. Patterns without a `symbolMessage` (older CLI) are left * as-is, keyed by their templateHash. */ export declare function collapseBySymbolMessage(patterns: ExtractedPattern[]): ExtractedPattern[]; /** One input line of an event, with its forwarder record's sizes when known. */ export interface ReceiverLine { line: string; recordBytes?: number; payloadJsonBytes?: number; } /** * One event's size as the Receiver counts it: one record, the head line's * whole record and then each continuation line's payload joined with an * escaped newline. A line with no record of its own counts as its text as a * JSON string body, plus `overhead` for the head. */ export declare function eventReceiverSize(lines: ReceiverLine[], overhead?: number): number; /** * The largest sum of bytes within any 240-second interval of the events' * times, or undefined when no event carries a time. */ export declare function slidingWindowPeak(events: Array<[number, number]>): number | undefined; /** Add one event to a pattern's cells, keyed by its envelope's service, status and split attribute. */ export declare function addPatternCell(cells: Map, env: { service?: string; severity?: string; attribute?: { name: string; value: string; }; }, bytes: number): void; /** The envelope fields a cell is keyed by, read from one pulled event. */ export declare function envelopeCellOf(event: Record): { service?: string; severity?: string; attribute?: { name: string; value: string; }; }; /** The event time an envelope carries, read from the same fields the enrichment reads. */ export declare function envelopeTimestampMs(obj: Record): number | undefined;