/** * Inline computations for the v3 `log10x_top_patterns` layout: * * - **Badge classification** — per-row trajectory label (NEW / ACUTE / * GROWING / STABLE / SHRINKING) derived from current-vs-baseline * bytes. Uses a 3-window baseline at offsets 7d/14d/21d so the * badge maps 1:1 to what log10x_whats_changing would say for the * same hash. * - **Service breadth** — count of distinct services emitting each * hash, used to gate the "show service breakdown" CTA to multi- * service rows only. * - **Datadog analyzer snippet** — lifted from `exclusion-filter.ts`'s * `generateHashFilter('datadog', 'config', ...)`. Folded inline only * when the env's analyzer is Datadog (where ingest-exclusion is * pre-meter, so an exact-tenx_hash query saves the metered moment). * For Splunk we surface a CTA with a post-license caveat instead of * folding inline — Splunk transforms.conf nullQueue drops AFTER * license consumption, so the customer expectation of parity with * the forwarder drop would mislead. * - **Health banner** — lightweight degraded-state detection visible * to top_patterns without invoking log10x_doctor. Surfaces when the * engine metric is empty / events metric is null. Skips a full * doctor pass (too heavy for every top_patterns call). */ import * as pql from './promql.js'; import type { EnvConfig } from './environments.js'; import { type DepCheckResult } from './siem/deps/index.js'; export type { TrendDelta } from './trend-delta.js'; /** Trajectory state label for a pattern row. Formerly called `Badge`. */ export type Badge = 'NEW' | 'ACUTE' | 'GROWING' | 'STABLE' | 'SHRINKING'; /** * Classify a pattern's trajectory based on current-window bytes vs * baseline (average of N prior-period bytes). Thresholds match the * intuition the Reader uses when scanning: * * - NEW — pattern's first-seen is more recent than the * baseline window AND no baseline data exists * - ACUTE — delta/baseline > 0.5 (more than 50% above its own * baseline rate — a real spike, decision changes) * - GROWING — 0.05 < delta/baseline ≤ 0.5 (trending up but not a * spike — same decision as STABLE for most purposes) * - STABLE — |delta/baseline| ≤ 0.05 (within noise) OR baseline * query returned empty for a pattern older than the * baseline window (engine knew about it, baseline data * just isn't there — likely a labeling difference, not * a real "new" event) * - SHRINKING — delta/baseline < -0.05 * * The 5% deadband is intentional — Prometheus baselines have natural * jitter from edge buffering and Reporter aggregation timing; below 5% * the change is more likely noise than signal. * * The first-seen cross-check matters in envs where pattern names * include unstable identifiers (otel-collector adds span IDs, version * strings, etc.) — every pattern looks "new" on a 7d baseline because * the *name* changed even though the underlying event class didn't. * Only mark NEW when first-seen is more recent than the baseline. */ export interface BadgeInfo { kind: Badge; /** Signed ratio vs baseline average. e.g. +0.85 = +85% above baseline. * null when no baseline data exists (NEW case). */ ratio: number | null; /** Echoed from input — used to render "new (17h)" with the age. */ firstSeenAgeSeconds: number | null; } export declare function classifyBadge(currentBytes: number, baselineSamples: number[], firstSeenAgeSeconds?: number | null, baselineWindowSeconds?: number): BadgeInfo; /** * Derive a Badge state from a trend_delta percent value and the * pattern's first-seen age. * * This makes `state` strictly derived from `trend_delta.value` (the * source of truth), replacing the older classifyBadge() derivation * that went directly from baseline-bytes comparison. * * Thresholds: * NEW — firstSeenAgeSeconds < 7d (regardless of delta) * ACUTE — TODO: requires 1h delta input; not yet implemented. * Reserve the branch for when a separate 1h-delta field * is available on the row. * GROWING — deltaPct > 15 * SHRINKING — deltaPct < -15 * STABLE — |deltaPct| <= 15 (inclusive at ±15) * * Note: ACUTE cannot be derived from the WoW delta alone — it signals * a short-window spike (last 1h vs prior 1h) that is orthogonal to the * week-over-week trend. Until a 1h-delta input is wired, callers should * treat ACUTE as a TODO and fall through to GROWING/STABLE/SHRINKING. */ export declare function classifyStateFromDelta(deltaPct: number, ageSeconds: number | null): Badge; /** * Render the badge in the meaningful form for the new list shape. * Replaces the old `—` / `↑` glyphs that gave the Reader nothing * actionable. Now each badge carries either the actual percent change * vs baseline, the actual first-seen age, or a plain word — whichever * is the most concrete claim we can defensibly make. * * ACUTE → "acute spike (+85% vs baseline)" * GROWING → "+15% vs baseline" * SHRINKING → "−12% vs baseline" * STABLE → "stable (within ±5%)" * NEW (known age) → "new (since 17h ago)" * NEW (unknown age) → "new (not in baseline)" */ export declare function fmtBadge(b: Badge): string; /** * Preferred renderer — takes the full BadgeInfo so it can include the * actual percent change / first-seen age in the output. Use this * everywhere the row has access to the full info. */ export declare function fmtBadgeInfo(info: BadgeInfo): string; /** * Fetch baseline bytes for the same (pattern, service, severity) * triples that topPatternsFull surfaced, at offsets 7d / 14d / 21d. * Returns a Map keyed by `${pattern}|${service}|${severity}` * so the caller can compute the badge per row by joining on identity. * * Three parallel queries. Worst-case ~1-3s on a healthy Prometheus. * Empty results per offset (returns no baseline samples for a hash) are * fine — `classifyBadge` returns NEW when baselineSamples is empty. */ export declare function fetchBaselineBytes(env: EnvConfig, filters: Record, metricsEnv: string, range: string, offsetDays?: number[]): Promise>; /** * For a list of hashes, count the distinct services emitting each one. * Uses a single grouped-by-(hash,service) query and post-processes * locally — 1 PromQL call vs N. Returns Map. * * Drives the "show service breakdown" CTA: skipped when count <= 1, * surfaced when count >= 2 (multi-service rows where the dominant * service in the row header doesn't tell the whole story). */ export declare function fetchServiceBreadth(env: EnvConfig, metricsEnv: string, range: string, hashes: string[]): Promise>; /** * Per-hash dependency-check pass for the env's analyzer. * * Runs `checkDeps` in parallel for each hash. Each call hits the * analyzer's read-only APIs (DescribeAlarms / DescribeMetricFilters / * ListDashboards on CloudWatch; equivalents for Splunk / Datadog / * Elasticsearch). Per-vendor pagination is bounded inside the module. * * Returns null when no analyzer is detected or the analyzer isn't in * the supported subset — the caller renders no dep badge in that case. * Returns Map on success (some entries may have * `error` set if the per-hash scan failed; the renderer handles that * shape). * * Note on precision: the matcher is `allTokensMatchExact`, token-AND on * discrete tokens, mirroring the templater's tokenization. Catches saved * searches / alerts whose body contains every pattern token as a discrete * token. Misses references by `tenx_hash` value (hash-only refs). * False-positives possible on common-word patterns. The renderer surfaces * this caveat inline. */ export declare function fetchDepsPerHash(analyzer: string | null, hashes: Array<{ hash: string; service: string; severity: string; }>): Promise | null>; /** * Datadog ingest-exclusion query for an exact tenx_hash match. The * query goes into Logs > Configuration > Indexes > Exclusion Filters; * the drop happens pre-meter, so the customer's metered ingest cost is * actually reduced (unlike Splunk transforms.conf nullQueue, which * drops post-license and only saves indexer storage). * * Lifted from `exclusion-filter.ts`'s `generateHashFilter('datadog', * 'config', ...)` — kept here as a small standalone helper so * top_patterns can fold it inline without importing the full * pattern_mitigate tool's surface. */ export declare function datadogAnalyzerQuery(hash: string, service: string, severity: string): string; /** * Lightweight degraded-state detection. Surfaces when the engine * metric tier appears unhealthy *from what top_patterns can already * see* — empty total-bytes-in-scope, missing events metric, etc. * * Does NOT run log10x_doctor (which is a multi-second multi-API probe * unsuitable for the hero tool's hot path). Returns null when no * banner is warranted; otherwise returns a short markdown string the * renderer pins to the top of the output. */ export interface HealthSignals { totalBytes: number; patternCountTotal?: number; eventsAvailable: boolean; } export declare function healthBanner(s: HealthSignals): string | null;