/** * Policy loader — parses and validates a `policy.yaml` file for the * `tenx-recur` CLI tick runner. * * The schema mirrors what `scheduler-manifest-emitter.ts` emits via * `emitPolicyYaml()`. Validation is intentionally lenient — unknown fields * are ignored so policies written by newer versions of the setup wizard * still load correctly on an older CLI binary. * * Minimal YAML parser note: we parse the policy.yaml with a hand-rolled * line-by-line parser rather than pulling in a heavy YAML library. The * policy format is simple (no anchors, no multi-document, no block * scalars) so this is safe and keeps the dependency footprint small. * * Policy shape (all fields with defaults): * * schema_version: "1.0" * * reduction: * target_services: [] # empty = all services * target_percent: 30 # integer 1-95 * exceptions: [] # service names never touched * min_delta_pp: 2 # minimum savings change before commit * lookback_window: "24h" # PromQL range for the volume query * severity_rules: # per-severity action overrides * ERROR: keep # kept at floor regardless of volume * INFO: auto # auto = apply cost policy * DEBUG: auto * WARN: auto * * schedule: * preset: daily-03utc * cron_utc: "0 3 * * *" * scheduler: k8s_cron * * config_plane: * repo: https://github.com/acme/log10x-config * env_id: */ /** Datadog Standard indexing at list, 15-day retention, $ per million events. */ export declare const DATADOG_STANDARD_LIST_USD_PER_MILLION: number; /** * Per-severity rules the tick applies. Compaction is not one of them: it is a * per-source switch (the sidecar's `optimize` flag), set at install from a * measured ratio, not a per-pattern row the tick writes. */ export type SeverityAction = 'keep' | 'auto' | 'drop' | 'sample'; export interface SeverityRules { ERROR?: SeverityAction; CRITICAL?: SeverityAction; WARN?: SeverityAction; INFO?: SeverityAction; DEBUG?: SeverityAction; TRACE?: SeverityAction; } export interface ConfigPlane { /** * URL or local filesystem path to the customer gitops config repo. * The CLI clones/pulls from here, writes updated CSVs, and opens a PR * (or pushes directly, depending on the `commit_strategy` field below). */ repo: string; /** * Log10x env ID — scopes the PromQL metric queries. * Falls back to LOG10X_ENV_ID when absent. */ env_id?: string; /** * How the CLI commits changes. * 'pr' — open a GitHub PR via `gh pr create` (default). * 'direct_push' — push directly to the specified branch. */ commit_strategy?: 'pr' | 'direct_push'; /** Branch to push to (direct_push) or base branch for the PR. Default: main. */ base_branch?: string; /** * Budget policies only: where the run writes the Receiver's files. * 'configmap' — the policy ConfigMap the Receiver reads (in-cluster CronJob). * 'gitops' — `repo`, for the engine's gitops pull. */ delivery?: 'configmap' | 'gitops'; /** Namespace of the policy ConfigMap (configmap delivery). Default: the pod's namespace. */ namespace?: string; /** Name of the policy ConfigMap (configmap delivery). Default: log10x-action-intent. */ configmap?: string; /** Gitops delivery: the branch the run commits to directly and the engine pulls. Default: log10x-policy. */ policy_branch?: string; } /** * A Datadog Standard indexing budget held month to date (the budget run). * X is the whole account's Standard bill. */ export interface DollarBudget { /** X: dollars a month of Standard indexing, the whole account. */ usd_monthly: number; /** The account's Standard indexing rate, $ per million events. */ standard_usd_per_million: number; /** Index names that bill as Standard. Empty: every index counts. */ standard_indexes: string[]; /** Error lines over a limit that pass per statement per node per window. */ keep_count: number; /** Where a service's lines over its limit go when it names none. */ default_action: 'tier_down' | 'offload'; /** Per-service destination for lines over the limit. */ service_actions: Record; /** * Dollars a month of Standard for a named service (the container), held * month to date against its own number. The rest of the account is held * to usd_monthly minus their sum. */ service_budgets: Record; /** Nodes each service's pods run on, when the run cannot discover them (gitops delivery). */ nodes_by_container: Record; /** Receivers in the fleet, for the `*` row, when the run cannot discover them. */ nodes_total?: number; } /** * Rows written into a ClickHouse logs table held at a rate (the rows run). The * run reads the rows ClickHouse wrote in the last hour from its own query log and * each statement's lines an hour from the Receiver's counters, and rewrites the * per-statement limits and service totals, in events, so the excess goes to the * account's own bucket before ClickHouse receives it. */ export interface RowsBudget { /** R: rows an hour into the logs table; undefined: `auto`, the meter's last complete hour at the first applied run. */ rows_per_hour?: number; /** `database.table` the meter counts. */ table: string; /** Cluster name for `clusterAllReplicas` on ClickHouse Cloud (each node keeps its own part_log); undefined reads the one node. */ cluster?: string; /** Prometheus label carrying the container (the regulator's containerField). */ container_label: string; /** Minutes of the meter's window; 60 in production. A shorter window scales the reads to an hour, for a time-scaled test rig. */ meter_minutes: number; /** Rows an hour other writers put into the table; the rest of the meter is the Receiver's. Default 0. */ outside_rows_per_hour: number; /** Error lines over a limit that pass per statement per node per window. */ keep_count: number; default_action: 'offload'; service_actions: Record; nodes_by_container: Record; nodes_total?: number; } /** * An Elasticsearch hot tier held under a size (the hot-tier run). The run * reads the hot nodes' store and the Receiver's own indices every hour and * rewrites the per-statement limits, in bytes, so growth goes to the frozen * tier instead of filling hot. */ export interface HotTierBudget { /** S in GB of index store on the hot nodes; undefined: `capacity_share` of their disk. */ gb?: number; /** Share of the hot nodes' disk to hold under when `gb` is not set. Default 0.8 (Elasticsearch's low watermark is 85%). */ capacity_share: number; /** Hours a kept line stays on hot; default read from the ILM policy on the kept alias. */ retention_hours?: number; /** Write alias of the kept lines (routeState pass). */ pass_alias: string; /** Write alias of the tier_down lines, rolled to frozen by ILM. */ tier_down_alias: string; /** Field holding the container in the Receiver's documents. */ container_field: string; /** Error lines over a limit that pass per statement per node per window. */ keep_count: number; default_action: 'tier_down' | 'offload'; service_actions: Record; nodes_by_container: Record; nodes_total?: number; } /** A named service's own number, and the `service` tag Datadog's usage metric carries for it. */ export interface ServiceBudget { usd: number; /** Default: the container name. */ datadog_service: string; } /** As written: `payments: 4000`, or `payments: {usd: 4000, datadog_service: payments-api}`. */ export type ServiceBudgetInput = number | { usd: number; datadog_service?: string; }; export interface Policy { schema_version: string; /** Services the policy targets. Empty = all services. */ target_services: string[]; /** Desired savings target (1-95). */ target_percent: number; /** * VOLUME BUDGET (thermostat): keep monthly ingest for the targeted scope at * or under this many GB/mo. When present it REPLACES target_percent as the * tick's goal: the tick scales the budget to its lookback window and cuts * only the overage — already under budget means every pattern passes. */ budget_gb_monthly?: number; /** * DOLLAR BUDGET on Datadog Standard indexing, held month to date. When * present the tick is the budget run (caps, actions, totals per service), * not the mute-list tick. */ budget_usd?: DollarBudget; /** ROWS RUN on ClickHouse: hold the rows written into the logs table at a rate an hour. */ rows?: RowsBudget; /** * HOT-TIER BUDGET on Elasticsearch: hold the hot nodes' store under a size. * When present the tick is the hot-tier run (caps, actions, totals in bytes). */ hot_tier?: HotTierBudget; /** Services the policy must never touch. */ exceptions: string[]; /** * Minimum change (percentage points) before a new CSV is committed. * Prevents noisy churn on small week-to-week fluctuations. */ min_delta_pp: number; /** * PromQL range window for the volume query (e.g. "24h", "7d"). * This is the lookback the tick uses when calling top_patterns. */ lookback_window: string; /** Per-severity action overrides. */ severity_rules: SeverityRules; cron_utc?: string; scheduler?: string; config_plane: ConfigPlane; } export declare class PolicyLoadError extends Error { readonly cause?: unknown | undefined; constructor(message: string, cause?: unknown | undefined); } /** * Parse a `policy.yaml` string into a validated `Policy` object. * * Missing fields receive sensible defaults so policies written by an older * setup wizard still load without error. The only required field is * `config_plane.repo` — the tick has nowhere to write without it. * * @throws PolicyLoadError on structural problems (missing repo, out-of-range * target_percent, negative min_delta_pp, etc.). */ export declare function parsePolicyYaml(text: string): Policy; /** * Why a set of per-service budgets cannot be held, or undefined when it can: * their sum must leave something of X for the rest of the account, and a * service the policy never touches cannot be held to a number. */ export declare function serviceBudgetsProblem(budgets: Record, usdMonthly: number | undefined, exceptions: readonly string[]): string | undefined; export declare function serviceBudgetUsd(b: ServiceBudgetInput | ServiceBudget): number;