/** * Cost calculation helpers. * * Two layers: * - Back-compat layer (bytesToCost, bytesToGb, parsePrometheusValue): * unchanged signatures, used by existing tools (savings, top-patterns, * event-lookup, trend, services, investigate, etc). * - X% commitment layer (projectAction, projectActionRange, * COST_MODEL_BY_DESTINATION, getDestinationCostModel, annualizeDollars): * splits ingest vs storage, models per-destination compact ratios with * uncertainty bands, and degrades savings for small events where * envelope overhead dominates. * * Compact-ratio numbers come from the ES/Splunk PoC findings: * - Elasticsearch pruned (compactable fields excluded from _source): * 45-73% reduction range. Modeled as compact_ratio 0.30..0.40. * - Elasticsearch unpruned: ~45-55% post/pre. Returned via * getDestinationCostModel(dest, {esPruned:false}). * - Splunk envelope-in-event: ~92% reduction on the OUTER stream. * Modeled as 0.08..0.15. * - Datadog/CW/Azure/GCP/Sumo/Coralogix/ClickHouse: no-op. compact_ratio = * 1.0..1.0; a caveat is emitted by callers. On ClickHouse the reason is * measured rather than structural: the text index and the column codecs * already absorb almost all of it, so compaction moves about 7% of table * bytes, and table bytes are not where a ClickHouse bill lives. The lever * there is rows that never enter, priced through the `compute` term below. * * Small-event degradation: below `small_event_floor_bytes` (default 100), * envelope overhead linearly degrades the compact ratio toward 1.0. At * avgSize == floor → baseRatio. At avgSize → 0 → ratio → 1.0. */ import type { SiemId } from './siem/pricing.js'; export declare const S3_STORAGE_PER_GB_MONTH = 0.023; /** Convert bytes to cost in dollars at the given $/GB rate. */ export declare function bytesToCost(bytes: number, costPerGb: number): number; /** Convert bytes to GB. */ export declare function bytesToGb(bytes: number): number; /** Parse a Prometheus value (always a string) to a number. */ export declare function parsePrometheusValue(result: { value?: [number, string]; }): number; /** * What the destination bills on. * - uncompressed-ingest: Splunk (bytes-into-indexer at uncompressed size) * - compressed-ingest: Datadog, CloudWatch, GCP Logging, Sumo, Azure * (bytes accepted by the API; vendor compresses * post-receipt) * - indexed-uncompressed: Elasticsearch (the _source / index footprint) * - stored-month: ClickHouse, S3-backed offload (per GB-month) */ export type BillingBasis = 'uncompressed-ingest' | 'compressed-ingest' | 'indexed-uncompressed' | 'stored-month'; /** * How (or whether) compaction lands at this destination. * - no-op: destination cannot accept encoded events (Datadog & * friends). compact_ratio fixed at 1.0; caller warns. * - envelope: Splunk-style encode-in-event; query-time expand. * - index-pruned: ES with `_source.excludes` of compactable fields. * - index-unpruned: ES without pruning (savings come from value-level * rewrite, not index pruning). */ export type CompactMode = 'no-op' | 'envelope' | 'index-pruned' | 'index-unpruned'; /** * Cheaper storage/ingest tier that tier_down routes events to. * When present in a destination's cost model, projectActionWithRatio can * compute meaningful dollar savings for tier_down (rather than returning * zero with a caveat about "rule not yet configured"). */ export interface TierDownTargetTier { /** Human-readable tier name, e.g. "CloudWatch Logs Infrequent Access". */ name: string; /** Cheaper ingest rate for this tier ($/GB). */ ingest_rate_usd_per_gb: number; /** Cheaper storage rate for this tier ($/GB-month). */ storage_rate_usd_per_gb_month: number; } /** * A destination whose bill is COMPUTE, not bytes accepted or bytes stored. * * ClickHouse is the only one modeled this way. Its storage line is small and * its ingest line is zero, so pricing it on bytes alone reads as if the bill * were somewhere it is not. What moves a ClickHouse bill is rows that never * enter: insert and merge CPU follows row count, faster than linearly, because * a row never written is not paid for once and is not paid for again on every * merge that would have carried it. * * `curve` maps rows KEPT (as a fraction of what arrives today) to insert-plus- * merge CPU as a fraction of today's. Points are measured; between them this * model interpolates linearly and nothing else. * * Compute is bought in whole units within the autoscaler's bounds, so a CPU * drop is worth nothing until a whole unit can go, and never below the floor. * `unit_step` and `min_units` carry that. */ export interface DestinationComputeTerm { /** What the CPU curve is indexed on. Rows inserted is the only measured one. */ basis: 'rows-inserted'; /** List price of one compute unit, $/hour. */ unit_usd_per_hour: number; /** True when the platform bills whole units (round up), not fractions. */ unit_step: boolean; /** Lowest unit count the service can run at. Never billed below this. */ min_units: number; /** * [rowsKeptFraction, cpuFraction] points, ascending by rowsKeptFraction. * Endpoints at 0 and 1 are required so every input is bracketed. */ curve: Array<[number, number]>; } /** * What the rows-kept fraction was actually derived from on THIS call. * * - 'rows-inserted': a real row or event count. The basis the curve was * measured on, so the answer means what it says. * - 'bytes-removed-as-rows-proxy': bytes stood in for rows because no count * was available. Fine when the removed patterns have about the average * event size, and wrong in proportion to how far they do not: removing * long lines takes more bytes than rows and understates the compute saving, * removing short ones overstates it. */ export type ComputeBasis = 'rows-inserted' | 'bytes-removed-as-rows-proxy'; /** What a compute term says about one action, on one estate. */ export interface ComputeSavingProjection { basis: ComputeBasis; /** Rows still inserted after the action, as a fraction of today's rows. */ rows_kept_fraction: number; /** Insert-plus-merge CPU after the action, as a fraction of today's. */ cpu_fraction: number; /** * Compute units billed before and after, whole units, floored at min_units. * Present only when the caller supplied current units or a monthly spend. */ units_before?: number; units_after?: number; /** (units_before - units_after) x unit_usd_per_hour x 730. */ saving_usd_month?: number; /** * Share of today's compute bill this action removes. When units are known * this is (units_before - units_after) / units_before, which is the stepped * answer and can be 0 at the floor. When they are not, it is 1 - cpu_fraction, * which is the unstepped shape of the curve and nothing more. */ saving_fraction: number; /** Always true. No ClickHouse compute dollar in this codebase is measured on the customer's estate. */ modeled: true; note: string; } export interface DestinationCostModel { destination: SiemId; /** $/GB billed at ingest. */ ingest_per_gb: number; /** * How to LABEL ingest_per_gb when quoting the model to a human. Default * 'ingest'. Datadog is 'all-in': its real ingest meter is ~$0.10/GB and the * money is per-million-event indexing, so the $2.50 figure is a blend — * calling that blend "ingest" reads as not knowing the platform. */ ingest_label?: string; /** $/GB-month billed for retention. */ storage_per_gb_month: number; billing_basis: BillingBasis; compact_mode: CompactMode; /** * What the customer must have for compact to be real here — the app, plugin, * or view that expands compacted events again, with its platform and version * constraint. Rendered on any plan that prices compact: a lever whose * prerequisite is unstated is a lever we are guessing at. */ compact_requires?: string; /** Same contract for tier_down: the tier, and what enables it. */ tier_down_requires?: string; /** * Why compact is not offered here, when the reason is NOT "the destination * cannot accept encoded events". Serverless-style platforms accept them * happily and have nowhere to install an expander, which would leave the * customer with unreadable events — not a keep-everything lever. */ compact_unavailable_reason?: string; /** * Ratio of POST-compact bytes / PRE-compact bytes for the destination's * billed measure. Lower = better savings. Range describes uncertainty. */ compact_ratio_low: number; compact_ratio_high: number; /** * Body-size below which compaction efficiency degrades (envelope overhead * dominates). Default 100 bytes. */ small_event_floor_bytes: number; /** * Cheaper tier that tier_down can route events to. When present, * tier_down savings are computed as the delta between standard and tier * rates. When absent, tier_down produces bytes_out=bytes_in with a caveat. */ tier_down_target_tier?: TierDownTargetTier; /** * Additional cheaper tiers the destination offers beyond the default * tier_down_target_tier, in order of increasing aggression. The engine is * unaware of which one a deployment uses: the MCP picks the target plan when * it generates the forwarder recipe, and every tier_down event for that * deployment lands in that one chosen plan (no per-pattern split). Consumed by * the offload recipe generator (renderOffloadSection), which emits a * provisioning recipe per plan (the default tier_down_target_tier plus each * alternative here). estimate_savings prices the default target tier; pricing a * caller-selected alternative is a planned follow-up. Example (Azure): default * = Basic Logs; alt = [Auxiliary Logs]. */ tier_down_alt_tiers?: TierDownTargetTier[]; /** * Present only where the bill is compute rather than bytes. ClickHouse only. * Do not add one to a destination that bills per GB accepted or per GB * stored: there the byte projection already IS the bill, and a compute term * would double-count it. */ compute?: DestinationComputeTerm; } /** * Provenance tag for any dollar value the pipeline emits. * - 'list_price' — derived from vendor list $/GB (lib/siem/pricing). * - 'customer_supplied' — caller passed an explicit override rate. * - 'unset' — no rate available; value is a placeholder (0/null). */ export type DollarSource = 'list_price' | 'customer_supplied' | 'unset'; /** * Envelope shape that every dollar field in an envelope MUST use. * * The plain-English `disclosure` rides alongside `value` so renderers cannot * print a list-price number without the "may differ depending on discounts, * commits, or contract tier" caveat. `disclosure` is null only when the rate * came from the customer (no caveat needed). */ export interface DisclosedDollarValue { value: number; source: DollarSource; /** Plain-English disclosure. null iff source === 'customer_supplied'. */ disclosure: string | null; } /** * Build a DisclosedDollarValue. Single source-of-truth constructor — renderers * NEVER inline an object literal of this shape. * * - source='customer_supplied' → disclosure=null (caller owns the rate). * - source='unset' → disclosure='(no $/GB rate configured)'. * - source='list_price' → disclosure carries the SIEM label + list * rate + "may differ" caveat. */ export declare function buildDisclosedDollarValue(value: number, source: DollarSource, siemLabel: string | null, listRatePerGb: number | null): DisclosedDollarValue; /** Internal alias used during the migration. Renderers should call buildDisclosedDollarValue. */ export declare const makeDisclosedDollar: typeof buildDisclosedDollarValue; export interface SavingsProjection { bytes_in: number; /** Post-action bytes leaving forwarder toward destination. */ bytes_out: number; ingest_dollars: number | null; /** For the retention window the caller supplies (default 1 month). */ storage_dollars: number | null; total_dollars: number | null; /** * For `offload` only: the residual cost the customer pays to store the * offloaded bytes in their own object store ($/window). 0 for every other * action. Already NETTED into total_dollars, so savings = baseline - total is * net of S3. Surfaced separately so renderers can show "saved $X (net of $Y * S3 storage)". */ s3_storage_dollars?: number; /** Disclosed-value mirror of total_dollars. Always populated when total_dollars is non-null. */ total_dollars_disclosed?: DisclosedDollarValue | null; /** Disclosed-value mirror of ingest_dollars. */ ingest_dollars_disclosed?: DisclosedDollarValue | null; /** Disclosed-value mirror of storage_dollars. */ storage_dollars_disclosed?: DisclosedDollarValue | null; basis: BillingBasis; confidence: 'low' | 'expected' | 'high'; /** Always populated. 0..100. */ percent_reduction: number; /** Origin of each axis' rate. */ rate_source: { ingest: 'list' | 'customer_supplied' | 'unset'; storage: 'list' | 'customer_supplied' | 'unset'; }; /** * Present only on a destination with a `compute` term (ClickHouse), and only * for the actions that keep rows out of the cluster: offload, drop, sample. * The byte axis above still carries the storage saving; this carries the * compute one, which is the larger number and the modeled one. */ compute_saving?: ComputeSavingProjection; /** * True when any dollar on this projection rests on a model rather than on the * destination's own meter. Set on every ClickHouse projection, because the * compute term is a curve fitted to one capture and a unit floor that is * ASSUMED. Renderers must carry the word "modeled" wherever they print these. */ modeled?: boolean; notes?: string[]; } /** * Headline-shaped projection consumed by percent-first tool surfaces. Mixes * percent (always present) with optional dollar overlays gated on whether the * caller could supply a rate (customer-supplied) or fall back to vendors.json * list. When neither is available, dollars are omitted entirely. */ export interface SavingsHeadline { percent: { low: number; expected: number; high: number; }; bytes: { in: number; out_expected: number; }; dollars?: { list_low?: number; list_expected?: number; list_high?: number; customer_low?: number; customer_expected?: number; customer_high?: number; }; /** * Disclosed-value mirror of `dollars`. Every numeric cell above is also * available here wrapped in DisclosedDollarValue so renderers can call * fmtDisclosedDollar without re-resolving rate_source + listRate. */ dollars_disclosed?: { list_low?: DisclosedDollarValue; list_expected?: DisclosedDollarValue; list_high?: DisclosedDollarValue; customer_low?: DisclosedDollarValue; customer_expected?: DisclosedDollarValue; customer_high?: DisclosedDollarValue; }; rate_source: 'list_price' | 'customer_supplied' | 'unset'; range?: { low: SavingsProjection; expected: SavingsProjection; high: SavingsProjection; }; } export type Action = 'pass' | 'sample' | 'compact' | 'tier_down' | 'offload' | 'drop'; /** * Per-destination cost & compaction model. * * Note: $/GB ingest values intentionally match * DEFAULT_ANALYZER_COST_PER_GB from lib/siem/pricing.ts (single source of * truth: comsite vendors.json). Storage numbers are estimates: * - Splunk: ~$0.10/GB-month retained (varies wildly by tier). * - ES: $0.05/GB-month at hot-tier list pricing. * - CH self-hosted: $0.023/GB-month (S3-backed object cost). * - CW: $0.03/GB-month. * - Azure Logs: $0.12/GB-month (interactive). * - GCP Logging: $0.01/GB-month (after 30d free). * - Sumo: $0.02/GB-month (continuous tier). * - Datadog: $0 storage (commodity included; pure ingest billing). */ export declare const COST_MODEL_BY_DESTINATION: Record; /** Stable identity for the action-hierarchy table. Superset of SiemId. */ export type DestinationKey = SiemId | 'splunk_cloud' | 'elasticsearch_self' | 'elasticsearch_managed' | 'opensearch_self' | 'opensearch_managed' | 'newrelic' | 'honeycomb' | 'grafana_cloud_logs' | 'loki' | 'generic'; export declare const DEFAULT_ACTION_BY_DESTINATION: Record; /** * Return the destination's preferred action at the given level (1-based). * Level 1 = first lever to pull; level 2 = fallback when level-1 is * unavailable. Unknown destinations and out-of-range levels fall back to * 'offload' (the safe single-lever default). */ export declare function getDefaultActionForDestination(destination: string, level?: number): Action; /** * Return the full ordered hierarchy of allowed default actions for a * destination. Used by the offload-section renderer to gate which * down-tier / compact sub-sections are relevant (e.g. Datadog Flex only * shows when 'tier_down' is allowed on 'datadog'). */ export declare function getAllowedActionsForDestination(destination: string): Action[]; /** * THE predicate for "does `compact` keep the line queryable in this * destination, in place". * * True only where the destination has a real compaction mechanism: Splunk * (envelope) and self-hosted Elasticsearch/OpenSearch (index-pruned). * Everywhere else `compact_mode` is `no-op` with ratio 1.0, and claiming * in-place compaction there is a false statement about the customer's own * platform. * * Every surface that renders or gates compaction language routes through * this. Without it the renderer carries rival notions of the same fact: * unconditional prose in four places, a hardcoded * `splunk || elasticsearch || clickhouse` gating the measured-ratio * section, and an allowed-actions lookup in the action selector — which * lets a CloudWatch report assert in-place compaction in its opening * paragraph while the section demonstrating it is skipped, so the section * numbering jumps 5 to 7. `test/compaction-claim-drift.test.ts` fails if * a rival notion appears. */ /** * The actions available on a destination GIVEN THE DEPLOYMENT — the single * source of truth for availability, so the lever choice and the plan's action * set can never disagree. * * The base list omits compaction on the Elasticsearch/OpenSearch family * because the expander is the l1es plugin, installable only on nodes the * customer controls. A CONFIRMED self-managed deployment adds it back. * Serverless never qualifies: its compact_mode is 'no-op' precisely because * there is no plugin surface, so the guard below refuses it there too. */ export declare function getAvailableActions(destination: SiemId, opts?: { selfManaged?: boolean; }): Action[]; export declare function compactsInPlace(destination: string): boolean; export declare function getDestinationCostModel(dest: SiemId, opts?: { esPruned?: boolean; }): DestinationCostModel; /** * Below the floor, envelope overhead linearly degrades compact savings: * the effective ratio walks from baseRatio (at floor) toward 1.0 (at 0). * Above the floor, returns baseRatio unchanged. * * Exposed for testing; not part of the v1 stable surface. */ export declare function degradeRatioForSmallEvents(baseRatio: number, avgSize?: number, floor?: number): number; /** Hours in a billing month, the figure every compute dollar here is built on. */ export declare const HOURS_PER_MONTH = 730; /** * Insert-plus-merge CPU as a fraction of today's, for a given fraction of * today's rows kept. Linear between measured points; identity outside them, * which cannot happen once a curve carries its 0 and 1 endpoints. * * Exposed for testing and for surfaces that want the shape without the money. */ export declare function cpuFractionForRowsKept(curve: Array<[number, number]>, rowsKeptFraction: number): number; /** * Model what an action does to a compute-billed destination's bill. * * Whole units, floored: the platform bills capacity in units within the * autoscaler's bounds, so lower CPU is worth nothing until a whole unit can go * and never below the floor. A small estate already at the floor saves zero, * and this returns zero rather than a fractional dollar the invoice will not * show. * * Without a current unit count or a monthly spend there is no dollar to give, * so this returns the fraction and says what to supply. */ export declare function projectComputeSaving(compute: DestinationComputeTerm, rowsKeptFraction: number, opts?: { current_units?: number; monthly_spend_usd?: number; /** What the fraction came from. Defaults to the curve's own basis. */ basis?: ComputeBasis; }): ComputeSavingProjection; export interface ProjectActionArgs { action: Action; bytes_in: number; avg_event_size_bytes?: number; /** For action='sample', e.g. 10 means keep 1 in 10. Default 10. */ sample_n?: number; destination: SiemId; /** Default 1 month. */ retention_months?: number; esPruned?: boolean; /** * Which tier_down plan to price. Matches (case-insensitive substring) the * name of the destination's default tier_down_target_tier or any * tier_down_alt_tiers entry (e.g. "auxiliary" → Azure Auxiliary Logs). Omit * for the default target tier (e.g. Azure Basic Logs). No effect on * destinations whose tier_down has no alternative tiers. */ tier_down_plan?: string; /** * Optional customer-supplied rate overrides. When present, the * corresponding rate_source axis flips to 'customer_supplied'. When the * destination has no list rate AND no override is supplied, that axis * collapses to `null` dollars + `rate_source = 'unset'`. */ customer_rate?: { ingest_per_gb_override?: number; storage_per_gb_month_override?: number; /** * Customer's offload-bucket storage rate ($/GB-month) used to net the * `offload` action. Defaults to S3 Standard (S3_STORAGE_PER_GB_MONTH) when * absent. Pass the cheaper tier (IA / Glacier) when the offload bucket uses * it. */ s3_per_gb_month_override?: number; }; /** * Measured per-service compact ratio (optimized_bytes / input_bytes, in * [0.02, 1.0]) from the engine's own `emitted_events_optimized_size_total`. * When present AND the destination compacts in `envelope` mode (Splunk, * where the on-wire encoded size IS the billed size), this replaces the * static destination band for action='compact' so the projection reflects * the service's real compressibility instead of a destination-wide guess. * Ignored on index-pruned (ES) destinations, where the wire ratio diverges * from the billed index size; those keep the static band for the dollar * projection. Ignored on ClickHouse too, where compact is a no-op. The value already * reflects realized small-event overhead, so it is NOT re-degraded. */ compact_ratio_override?: number; /** * Compute-billed destinations only (ClickHouse). The service's CURRENT * compute unit count. Supply it, or `monthly_compute_spend_usd`, to get a * dollar compute saving instead of a fraction. */ current_compute_units?: number; /** * Compute-billed destinations only. The service's current monthly compute * spend in dollars. Converted to units at the model's unit price when * `current_compute_units` is absent. */ monthly_compute_spend_usd?: number; /** * Rows still inserted after the action, as a fraction of the cluster's * current rows. Supply it when `bytes_in` is one slice of a larger estate. * When absent, the compute term ASSUMES `bytes_in` is everything the cluster * takes today and reads rows kept off the byte reduction. */ rows_kept_fraction?: number; } /** * Compute reduction as a 0..100 percent. Always non-negative and clamped to * 100. When passBytes is 0, returns 0 (nothing to reduce, no inflation). * * Scalar form returns the same value across all three confidence axes so * callers can splat into a triplet uniformly. */ export declare function percentReduction(passBytes: number, actionBytes: number | { low: number; expected: number; high: number; }): { low: number; expected: number; high: number; }; /** * Resolve which tier_down tier to price for a destination. Without a selector, * returns the default tier_down_target_tier (e.g. Azure Basic Logs). A selector * matches (case-insensitive substring) the name of the default tier or any * tier_down_alt_tiers entry (e.g. "auxiliary" → Azure Auxiliary Logs). Returns * undefined only when the destination has no tier_down tier at all. The engine * is unaware of the plan; this only prices the caller-selected one. */ export declare function resolveTierDownTier(model: DestinationCostModel, planSelector?: string | null): TierDownTargetTier | undefined; /** * Project the destination cost of one (action, bytes_in) pair using the * expected (mid-band) compact ratio for the destination. * * Examples: * projectAction({ action:'compact', bytes_in:1e9, destination:'splunk' }) * → total_dollars ≈ 1.0 * 6 * 0.115 ≈ $0.69 (i.e. ~88.5% savings on $6). * projectAction({ action:'compact', bytes_in:1e9, destination:'datadog' }) * → bytes_out === bytes_in, notes includes * 'compact not supported on datadog'. */ export declare function projectAction(args: ProjectActionArgs): SavingsProjection; /** * Project low / expected / high savings using the destination's compact * ratio uncertainty band. All three legs are degraded by the small-event * curve when avg_event_size_bytes is supplied. * * 'low' = least savings = compact_ratio_high (more bytes through) * 'high' = most savings = compact_ratio_low (fewer bytes through) */ export declare function projectActionRange(args: ProjectActionArgs): { low: SavingsProjection; expected: SavingsProjection; high: SavingsProjection; percent_reduction_low: number; percent_reduction_expected: number; percent_reduction_high: number; rate_source: SavingsProjection['rate_source']; }; /** * Percent-first headline wrapper around projectActionRange. * * Threads `effective_ingest_per_gb` (if supplied) through as an ingest * override on the underlying projection. Top-level `rate_source` collapses * the per-axis sources into a single tag callers can render: * - 'customer_supplied' if any axis was overridden * - 'list_price' if any axis used the vendor list rate (and none was * overridden) * - 'unset' if neither axis has a rate at all * * Both `dollars.list_*` and `dollars.customer_*` can be populated in mixed * cases (e.g. customer overrides ingest only; storage still on list). */ export declare function projectSavings(args: ProjectActionArgs & { effective_ingest_per_gb?: number; }): SavingsHeadline; /** * Annualize a window of dollars: e.g. 7-day spend × 365/7. * Returns 0 if windowDays <= 0. */ export declare function annualizeDollars(windowDollars: number, windowDays: number): number;