autotel
Version:
Write Once, Observe Anywhere
399 lines (397 loc) • 14.6 kB
JavaScript
import * as nodeFs from "node:fs";
import path from "node:path";
//#region src/policy.ts
/**
* Telemetry Policies (experimental)
*
* Implements the applier side of OTEP 4738 (`oteps/4738-telemetry-policy.md`):
* small, independent, fail-open rules that decide what telemetry is kept and
* how it is transformed. Policies are portable — the same JSON runs here, in a
* Collector, or in any other conforming implementation.
*
* This is an *applier*, not an engine: policies compile down to the hooks
* autotel already has (`spanFilter`, log record processors). No new pipeline.
*
* Supported stages:
* - `trace.keep.percentage` — deterministic per-trace probabilistic sampling
* - `log.keep` — `"all"` / `"none"` / a percentage
* - `log.transform` — `remove` → `redact` → `rename` → `add` (spec order)
*
* Unsupported (policy is skipped, telemetry is not — per spec fail-open):
* - `metric` targets
* - `trace.keep.mode` / `sampling_precision` / `hash_seed` / `fail_closed`
* (OTEP 235 consistent-probability sampling; autotel's samplers are not
* consistent-probability yet)
* - `event_attribute` / `link_trace_id` matchers
*
* @example
* ```typescript
* init({ service: 'api', policies: './policies' })
* ```
*
* ```json
* {
* "id": "drop-debug-logs",
* "log": { "match": [{ "log_field": "severity_text", "regex": "^(DEBUG|TRACE)$" }], "keep": "none" }
* }
* ```
*/
/**
* Values longer than this are not regex-matched (the matcher yields no match).
*
* The spec mandates RE2 for cross-implementation consistency; Node's `RegExp`
* backtracks, so an untrusted log body plus a pathological pattern is a DoS.
* Capping input bounds the damage without pulling in an RE2 binding.
*
* ponytail: length cap instead of a real RE2 engine — swap in a linear-time
* matcher if policies ever run against fully untrusted patterns.
*/
const MAX_MATCH_LENGTH = 4096;
let activePolicies = [];
const warned = /* @__PURE__ */ new Set();
function warnOnce(key, message) {
if (warned.has(key)) return;
warned.add(key);
console.warn(`[autotel] ${message}`);
}
const MATCH_OPERATORS = [
"exact",
"regex",
"exists",
"starts_with",
"ends_with",
"contains"
];
const FIELD_SELECTORS = [
"log_field",
"trace_field",
"span_attribute",
"log_attribute",
"resource_attribute",
"scope_attribute",
"span_kind",
"span_status"
];
function countSet(source, keys) {
return keys.filter((key) => source[key] !== void 0).length;
}
/**
* Returns a reason string if the policy cannot be applied, or undefined if it can.
*
* Per spec an implementation MAY support a subset of stages but MUST skip the
* policy — never the telemetry — when it meets one it does not understand.
*/
function unsupportedReason(policy) {
if (!policy || typeof policy.id !== "string" || policy.id === "") return "policy requires an id";
if (policy.metric !== void 0) return "metric targets are not supported";
if (countSet(policy, ["trace", "log"]) !== 1) return "exactly one target (trace or log) must be set";
const target = policy.trace ?? policy.log;
if (!Array.isArray(target?.match) || target.match.length === 0) return "at least one matcher is required";
for (const matcher of target.match) {
if (countSet(matcher, FIELD_SELECTORS) !== 1) return "each matcher must set exactly one field selector";
if (countSet(matcher, MATCH_OPERATORS) !== 1) return "each matcher must set exactly one match operator";
}
if (policy.trace?.keep) {
const { percentage, mode, sampling_precision, hash_seed, fail_closed } = policy.trace.keep;
if (mode !== void 0 || sampling_precision !== void 0 || hash_seed !== void 0 || fail_closed !== void 0) return "trace keep supports \"percentage\" only (consistent-probability sampling modes are not implemented)";
if (percentage !== void 0 && (typeof percentage !== "number" || Number.isNaN(percentage) || percentage < 0 || percentage > 100)) return "trace keep percentage must be between 0 and 100";
}
}
/**
* Replace the active policy set.
*
* Disabled policies are dropped (spec: they MUST be treated as if they do not
* exist) and unsupported ones are skipped with a warning.
*/
function setPolicies(policies) {
const accepted = [];
for (const policy of policies ?? []) {
if (policy?.enabled === false) continue;
const reason = unsupportedReason(policy);
if (reason) {
warnOnce(`skip:${policy?.id ?? "unknown"}:${reason}`, `policy "${policy?.id ?? "unknown"}" skipped: ${reason}`);
continue;
}
accepted.push(policy);
}
activePolicies = accepted;
}
/** The currently active (validated, enabled) policies. */
function getPolicies() {
return activePolicies;
}
function clearPolicies() {
activePolicies = [];
warned.clear();
}
const regexCache = /* @__PURE__ */ new Map();
function compileRegex(pattern, caseInsensitive) {
const key = `${caseInsensitive ? "i" : ""}