From 7399383d90ca389d61ee0240b2023d9354d8d49d Mon Sep 17 00:00:00 2001 From: jaronoff97 Date: Wed, 1 Jul 2026 12:45:24 -0400 Subject: [PATCH 1/5] extensions --- examples/extensions/README.md | 30 ++++++ examples/extensions/dump-waste-to-s3.yaml | 20 ++++ proto/tero/policy/v1/extension.proto | 66 +++++++++++++ proto/tero/policy/v1/policy.proto | 17 ++++ proto/tero/policy/v1/tero_extensions.proto | 41 ++++++++ spec.md | 110 +++++++++++++++++++++ 6 files changed, 284 insertions(+) create mode 100644 examples/extensions/README.md create mode 100644 examples/extensions/dump-waste-to-s3.yaml create mode 100644 proto/tero/policy/v1/extension.proto create mode 100644 proto/tero/policy/v1/tero_extensions.proto diff --git a/examples/extensions/README.md b/examples/extensions/README.md new file mode 100644 index 0000000..9249e11 --- /dev/null +++ b/examples/extensions/README.md @@ -0,0 +1,30 @@ +# Extensions + +Extensions attach implementation-specific behavior to a standard policy. Core +matching and keep/transform semantics are unchanged — the engine matches +telemetry with the policy's `log`/`metric`/`trace` target, then hands matched +records to the handler registered for the extension `type`. + +Extensions are generic: `type`, `version`, and an opaque `config`. The engine +does not interpret `config` — it routes matched records to the handler for +`type`, which parses `config` itself. + +Destinations are **not** defined inside the policy. An extension target (an S3 +bucket, a downstream OTLP endpoint, etc.) is pre-configured — either locally on +the implementation or broadcast from a provider via +`SyncResponse.extension_configs`. A routing extension's `config` references the +target by `(kind, name)`; the reference lives inside `config`, not as a +first-class field. + +See the [Extensions section of the spec](../../spec.md#extensions) for the full +rules. + +## This Example + +- `dump-waste-to-s3.yaml` — sample checkout errors to `0.01%` and route the + matched records to the pre-configured `s3` target named `eu-bucket`. + +The matching `eu-bucket` target (with its region/bucket/prefix config) is +configured out of band; the policy only names it. A client advertises which +targets it can reach via `ClientMetadata.supported_extensions`, so a provider +only sends policies pointing at reachable targets. diff --git a/examples/extensions/dump-waste-to-s3.yaml b/examples/extensions/dump-waste-to-s3.yaml new file mode 100644 index 0000000..cfa9198 --- /dev/null +++ b/examples/extensions/dump-waste-to-s3.yaml @@ -0,0 +1,20 @@ +id: dump-waste-to-s3 +name: Dump sampled-out checkout errors to S3 + +log: + match: + - log_field: body + regex: "[error|failed].*$" + - resource_attribute: ["service.name"] + regex: checkout-api + keep: ".01%" + +# The destination is pre-configured (locally or broadcast via +# SyncResponse.extension_configs) and referenced here by kind + name. +extensions: + - type: com.usetero/s3-dump + version: 1.0.0 + config: + target: + kind: s3 + name: eu-bucket diff --git a/proto/tero/policy/v1/extension.proto b/proto/tero/policy/v1/extension.proto new file mode 100644 index 0000000..fcb1362 --- /dev/null +++ b/proto/tero/policy/v1/extension.proto @@ -0,0 +1,66 @@ +syntax = "proto3"; + +package tero.policy.v1; + +option go_package = "github.com/usetero/policy/gen/go/tero/policy/v1"; + +// ============================================================================= +// Extensions +// ============================================================================= +// +// Extensions are a fully generic mechanism for attaching implementation-specific +// behavior to a policy. The policy engine matches telemetry using the core +// target (log/metric/trace), then hands matched records to the handler +// registered for the extension `type`. All extension payloads are opaque bytes: +// the engine never interprets them, so any implementation can define its own +// extension types without changing the core schema. +// +// tero's own extension types (e.g. the s3-dump destination) live in +// tero_extensions.proto. + +// Extension attaches implementation-specific behavior to a policy. +// +// Implementations MUST declare which extension types they support. An extension +// with an unrecognized type is handled per the spec (fail-open, or reject policy +// load when the extension is required for the policy's behavior). +message Extension { + // Type identifier using reverse-FQDN notation (e.g. "com.usetero/s3-dump"). + // Implementations use this to route to the correct handler. + string type = 1; + + // Version of the extension schema (semver, e.g. "1.0.0"). + string version = 2; + + // Opaque, type-defined configuration payload, deserialized by the handler + // registered for `type`. The policy engine does not interpret it. + bytes config = 3; +} + +// ExtensionCapability declares a client's support for one extension type. The +// repeated `config` entries are opaque, type-defined capability descriptors — +// for example, the destinations the client can route to. The policy engine does +// not interpret them; the extension handler does. Providers use this to avoid +// sending policies whose extensions the client cannot satisfy. +message ExtensionCapability { + // Extension type identifier (e.g. "com.usetero/s3-dump"). + string type = 1; + + // Minimum supported extension version (semver, inclusive). + string min_version = 2; + + // Opaque, type-defined capability descriptors. For tero's s3-dump these are + // serialized ExtensionTargetRefs (see tero_extensions.proto). + repeated bytes config = 3; +} + +// ExtensionConfig broadcasts opaque, type-defined configuration for one +// extension type to clients (via SyncResponse.extension_configs). Each `config` +// entry is parsed by the handler registered for `type`. For tero's s3-dump +// these are serialized ExtensionTargets (see tero_extensions.proto). +message ExtensionConfig { + // Extension type identifier (e.g. "com.usetero/s3-dump"). + string type = 1; + + // Opaque, type-defined configuration payloads. + repeated bytes config = 2; +} diff --git a/proto/tero/policy/v1/policy.proto b/proto/tero/policy/v1/policy.proto index 02d8d8a..b1ddbca 100644 --- a/proto/tero/policy/v1/policy.proto +++ b/proto/tero/policy/v1/policy.proto @@ -4,6 +4,7 @@ package tero.policy.v1; import "google/api/annotations.proto"; import "opentelemetry/proto/common/v1/common.proto"; +import "tero/policy/v1/extension.proto"; import "tero/policy/v1/log.proto"; import "tero/policy/v1/metric.proto"; import "tero/policy/v1/trace.proto"; @@ -49,6 +50,10 @@ message Policy { MetricTarget metric = 11; TraceTarget trace = 12; } + + // Implementation-specific extensions attached to this policy. Each extension + // references a pre-configured ExtensionTarget by (kind, name). + repeated Extension extensions = 20; } // ============================================================================= @@ -83,6 +88,11 @@ message ClientMetadata { // * service.namespace // * service.version repeated opentelemetry.proto.common.v1.KeyValue resource_attributes = 3; + + // Extension types this client supports, and the target kinds/names it can + // route each type to. Providers SHOULD only send policies whose extensions + // reference targets advertised here. + repeated ExtensionCapability supported_extensions = 4; } // TransformStageStatus reports hits and misses for a single transform stage. @@ -160,6 +170,13 @@ message SyncResponse { // Error message if sync failed string error_message = 6; + + // Opaque, type-defined extension configuration broadcast to the client, keyed + // by extension type. The policy engine does not interpret it; each extension + // handler parses its own entries. For tero's s3-dump this carries the + // available ExtensionTargets. A client merges these with any locally + // configured extension state. + repeated ExtensionConfig extension_configs = 7; } // ============================================================================= diff --git a/proto/tero/policy/v1/tero_extensions.proto b/proto/tero/policy/v1/tero_extensions.proto new file mode 100644 index 0000000..2df6401 --- /dev/null +++ b/proto/tero/policy/v1/tero_extensions.proto @@ -0,0 +1,41 @@ +syntax = "proto3"; + +package tero.policy.v1; + +option go_package = "github.com/usetero/policy/gen/go/tero/policy/v1"; + +// ============================================================================= +// tero Extension Types +// ============================================================================= +// +// Concrete extension types defined by tero, carried inside the opaque `config` +// bytes of the generic extension messages in extension.proto. These are not +// part of the core policy mechanism — other implementations define their own. + +// ExtensionTarget is a pre-configured, named destination an extension can route +// telemetry to (for example, an S3 bucket or a downstream OTLP endpoint). +// +// Targets are configured out of band on the implementation, or broadcast to +// clients via SyncResponse.extension_configs (serialized into the s3-dump +// extension's config). Policies reference a target by (kind, name) via +// ExtensionTargetRef and never carry its configuration inline. +message ExtensionTarget { + // Destination kind, e.g. "s3" or "otlp". + string kind = 1; + + // Name, unique within a kind, e.g. "eu-bucket". Used as the reference key. + string name = 2; + + // Opaque, kind-defined configuration (e.g. region, bucket, prefix). + bytes config = 3; +} + +// ExtensionTargetRef references a pre-configured ExtensionTarget by identity. +// Carried inside an Extension's config (to name where it routes) and inside an +// ExtensionCapability's config (to advertise reachable destinations). The +// referenced target MUST be known to the implementation — locally configured or +// received via SyncResponse.extension_configs. +message ExtensionTargetRef { + string kind = 1; + string name = 2; +} diff --git a/spec.md b/spec.md index c0e4870..9b4fbab 100644 --- a/spec.md +++ b/spec.md @@ -62,6 +62,7 @@ A policy MAY contain the following fields: | `created_at_unix_nano` | fixed64 | - | Creation timestamp in Unix epoch nanoseconds. | | `modified_at_unix_nano` | fixed64 | - | Last modification timestamp in Unix epoch nanoseconds. | | `labels` | KeyValue[] | empty | Metadata labels for routing and organization. | +| `extensions` | Extension[] | empty | Implementation-specific behavior. See [Extensions](#extensions). | ### Target @@ -1195,6 +1196,113 @@ trace: percentage: 100.0 ``` +## Extensions + +Extensions attach optional, implementation-specific behavior to a policy without +changing core semantics. Core matching and keep/transform decisions are +unaffected: the engine matches telemetry using the policy's target +(`log`/`metric`/`trace`), then hands matched records to the handler registered +for each extension `type`. A common use is routing matched telemetry to an +external destination (for example, dumping sampled-out records to S3). + +The extension mechanism is fully generic. Every extension payload is opaque to +the policy engine — it routes matched records to the handler registered for the +`type` and never interprets the payload. This lets any implementation define its +own extension types without changing the core schema. + +### Declaring an Extension + +A policy declares one or more extensions: + +| Field | Type | Description | +| --------- | ------ | ------------------------------------------------------------- | +| `type` | string | Reverse-FQDN extension identifier (e.g. `com.usetero/s3-dump`). | +| `version` | string | Extension schema version (semver). | +| `config` | object | Opaque, type-defined configuration, parsed by the handler. | + +```yaml +extensions: + - type: com.usetero/s3-dump + version: 1.0.0 + config: { ... } # defined entirely by the extension type +``` + +A client advertises which extension types it supports — and per type, opaque +type-defined capability descriptors — via `ClientMetadata.supported_extensions` +(`type`, `min_version`, and a list of opaque `config` entries). A provider MAY +also broadcast opaque type-defined configuration to clients via +`SyncResponse.extension_configs`. Both are keyed by extension `type`; their +contents are defined by the extension, not this specification. + +### Extension Input + +An extension receives every record its policy **matched** (passed all matchers), +paired with the policy's `keep` verdict for that record — `kept`, `sampled_out`, +or `dropped`. Records the policy did **not** match are never delivered to its +extensions. + +Matching, not keep, is the filter. The keep verdict is a label: it tells the +extension what happened to the record on the main pipeline but does not gate +delivery. A `keep: none` (drop) policy therefore delivers all of its matched +records to the extension, even though every one of them is dropped downstream. +This is what makes "dump waste" work — `com.usetero/s3-dump` on a `keep: .01%` +policy receives all matched records and archives the ~99.99% that were +`sampled_out`. + +The extension is a side-channel off the matched set. It MUST NOT change the +`keep`/`transform` outcome applied to the surviving pipeline. The record is +delivered as matched (before this policy's `transform`); post-transform +delivery, if needed, is defined by the extension `type`. + +### Rules + +1. `extensions` is OPTIONAL. A policy MAY declare multiple extensions (for + example, to dump to both disk and S3). +2. Each extension MUST specify a `type`, a `version`, and a `config`. The + `config` is opaque to the policy engine and defined by the extension `type`. +3. An extension MUST NOT redefine or alter core policy semantics (`match`, + `keep`, `transform`); it only observes the matched set (see + [Extension Input](#extension-input)) and acts on it out of band. +4. Implementations MUST explicitly document which extension `type`/`version` + pairs they support. +5. If a policy declares an extension whose `type` is unsupported (or whose + `config` cannot be satisfied) and that extension is required for the policy's + intended behavior, the implementation SHOULD reject the policy load with a + clear error reported via `PolicySyncStatus.errors`. Otherwise the extension + is skipped (fail-open) and core matching/keep/transform still applies. +6. Extension behavior SHOULD execute off the hot path using non-blocking + operations so core telemetry processing is not delayed. + +### tero Extension: `com.usetero/s3-dump` + +This is a concrete extension type defined by tero, illustrating the generic +mechanism above. It routes matched telemetry to a pre-configured **destination** +(an "extension target") so credentials and connection details stay out of +policies and one destination can be reused across many policies. + +A target is a named destination — `kind` (e.g. `s3`), `name` (unique within a +kind, e.g. `eu-bucket`), and opaque kind-defined `config` (region, bucket, +prefix). It is identified by the `(kind, name)` pair and becomes available to a +client either by local configuration or by broadcast via +`SyncResponse.extension_configs`. + +The policy's extension `config` carries only a reference — `{ kind, name }` — +to a target: + +```yaml +extensions: + - type: com.usetero/s3-dump + version: 1.0.0 + config: + target: + kind: s3 + name: eu-bucket +``` + +A client advertises the targets it can reach as the capability descriptors of +`ClientMetadata.supported_extensions` (each descriptor is a `{ kind, name }` +reference), so a provider only sends policies pointing at reachable targets. + ## Conformance An implementation conforms to this specification if it: @@ -1209,6 +1317,8 @@ An implementation conforms to this specification if it: 8. Validates policies per the [compilation error](#compilation-errors) rules, keeps invalid policies inert without aborting valid policies in the same batch, and reports compilation errors via `PolicySyncStatus.errors`. +9. Handles [extensions](#extensions) per the stated rules for any extension + `type` it supports, and documents which `type`/`version` pairs it supports. Implementations MAY support a subset of features (e.g., omit rate limiting) but MUST clearly document unsupported features. From 935db35dde7909cad0c994142ada046b352be8ad Mon Sep 17 00:00:00 2001 From: jaronoff97 Date: Mon, 6 Jul 2026 12:37:45 -0400 Subject: [PATCH 2/5] add bit about traffic sent --- examples/extensions/README.md | 5 ++- examples/extensions/dump-waste-to-s3.yaml | 2 + proto/tero/policy/v1/extension.proto | 29 ++++++++++++ spec.md | 54 ++++++++++++++--------- 4 files changed, 68 insertions(+), 22 deletions(-) diff --git a/examples/extensions/README.md b/examples/extensions/README.md index 9249e11..0dd4dd9 100644 --- a/examples/extensions/README.md +++ b/examples/extensions/README.md @@ -21,8 +21,9 @@ rules. ## This Example -- `dump-waste-to-s3.yaml` — sample checkout errors to `0.01%` and route the - matched records to the pre-configured `s3` target named `eu-bucket`. +- `dump-waste-to-s3.yaml` — sample checkout errors to `0.01%` and, via + `mode: dropped`, route the sampled-out records (the "waste") to the + pre-configured `s3` target named `eu-bucket`. The matching `eu-bucket` target (with its region/bucket/prefix config) is configured out of band; the policy only names it. A client advertises which diff --git a/examples/extensions/dump-waste-to-s3.yaml b/examples/extensions/dump-waste-to-s3.yaml index cfa9198..91efa36 100644 --- a/examples/extensions/dump-waste-to-s3.yaml +++ b/examples/extensions/dump-waste-to-s3.yaml @@ -9,11 +9,13 @@ log: regex: checkout-api keep: ".01%" +# mode: dropped delivers only the records this policy sampled out (the "waste"). # The destination is pre-configured (locally or broadcast via # SyncResponse.extension_configs) and referenced here by kind + name. extensions: - type: com.usetero/s3-dump version: 1.0.0 + mode: dropped config: target: kind: s3 diff --git a/proto/tero/policy/v1/extension.proto b/proto/tero/policy/v1/extension.proto index fcb1362..19d1fc3 100644 --- a/proto/tero/policy/v1/extension.proto +++ b/proto/tero/policy/v1/extension.proto @@ -18,6 +18,31 @@ option go_package = "github.com/usetero/policy/gen/go/tero/policy/v1"; // tero's own extension types (e.g. the s3-dump destination) live in // tero_extensions.proto. +// ExtensionMode selects which slice of the telemetry stream the engine delivers +// to an extension, relative to its policy. {KEPT, DROPPED, UNMATCHED} are +// disjoint and partition the stream; MATCHED and ALL are convenience unions. +enum ExtensionMode { + EXTENSION_MODE_UNSPECIFIED = 0; // treated as MATCHED + + // Matched the policy and survived the keep stage. + EXTENSION_MODE_KEPT = 1; + + // Matched the policy but was removed by the keep stage (dropped, sampled out, + // or rate limited). Its keep verdict is the record's final pipeline outcome, + // which another matching policy may have caused. This is the "dump waste" mode. + EXTENSION_MODE_DROPPED = 2; + + // Did not match the policy's matchers. + EXTENSION_MODE_UNMATCHED = 3; + + // Matched the policy, regardless of keep outcome (KEPT + DROPPED). Default. + EXTENSION_MODE_MATCHED = 4; + + // Every record of the signal type, regardless of match or keep + // (KEPT + DROPPED + UNMATCHED). + EXTENSION_MODE_ALL = 5; +} + // Extension attaches implementation-specific behavior to a policy. // // Implementations MUST declare which extension types they support. An extension @@ -34,6 +59,10 @@ message Extension { // Opaque, type-defined configuration payload, deserialized by the handler // registered for `type`. The policy engine does not interpret it. bytes config = 3; + + // Which slice of traffic the engine delivers to this extension. Defaults to + // MATCHED. + ExtensionMode mode = 4; } // ExtensionCapability declares a client's support for one extension type. The diff --git a/spec.md b/spec.md index 9b4fbab..89848d6 100644 --- a/spec.md +++ b/spec.md @@ -1219,11 +1219,13 @@ A policy declares one or more extensions: | `type` | string | Reverse-FQDN extension identifier (e.g. `com.usetero/s3-dump`). | | `version` | string | Extension schema version (semver). | | `config` | object | Opaque, type-defined configuration, parsed by the handler. | +| `mode` | enum | Which slice of traffic the engine delivers. Defaults to `matched`. See [Extension Input](#extension-input). | ```yaml extensions: - type: com.usetero/s3-dump version: 1.0.0 + mode: dropped config: { ... } # defined entirely by the extension type ``` @@ -1236,23 +1238,33 @@ contents are defined by the extension, not this specification. ### Extension Input -An extension receives every record its policy **matched** (passed all matchers), -paired with the policy's `keep` verdict for that record — `kept`, `sampled_out`, -or `dropped`. Records the policy did **not** match are never delivered to its -extensions. - -Matching, not keep, is the filter. The keep verdict is a label: it tells the -extension what happened to the record on the main pipeline but does not gate -delivery. A `keep: none` (drop) policy therefore delivers all of its matched -records to the extension, even though every one of them is dropped downstream. -This is what makes "dump waste" work — `com.usetero/s3-dump` on a `keep: .01%` -policy receives all matched records and archives the ~99.99% that were -`sampled_out`. - -The extension is a side-channel off the matched set. It MUST NOT change the -`keep`/`transform` outcome applied to the surviving pipeline. The record is -delivered as matched (before this policy's `transform`); post-transform -delivery, if needed, is defined by the extension `type`. +An extension's `mode` selects which slice of the telemetry stream the engine +delivers to it, relative to the extension's policy. Each record is classified on +two independent axes — whether it **matched** the policy's matchers, and its +final **keep** outcome — and a mode names a slice of that classification: + +| Mode | Records delivered | +| ----------- | ----------------------------------------------------- | +| `kept` | Matched the policy and survived the keep stage. | +| `dropped` | Matched the policy but was removed by keep (dropped, sampled out, or rate limited). | +| `unmatched` | Did not match the policy's matchers. | +| `matched` | Matched the policy, regardless of keep (`kept` + `dropped`). **Default.** | +| `all` | Every record of the signal type (`kept` + `dropped` + `unmatched`). | + +`kept`, `dropped`, and `unmatched` are disjoint and together cover the whole +stream; `matched` and `all` are convenience unions. + +The keep outcome is the record's **final pipeline outcome** — the most +restrictive result across all matching policies — so `dropped` captures records +removed even by a *different* policy. This is what makes "dump waste" work: a +`com.usetero/s3-dump` extension with `mode: dropped` on a `keep: .01%` policy +receives exactly the ~99.99% that were sampled out. + +Delivery is a side-channel: the engine copies the selected records to the +extension and MUST NOT let the extension change the `keep`/`transform` outcome +applied to the surviving pipeline. Records are delivered as matched (before this +policy's `transform`); post-transform delivery, if needed, is defined by the +extension `type`. ### Rules @@ -1261,8 +1273,8 @@ delivery, if needed, is defined by the extension `type`. 2. Each extension MUST specify a `type`, a `version`, and a `config`. The `config` is opaque to the policy engine and defined by the extension `type`. 3. An extension MUST NOT redefine or alter core policy semantics (`match`, - `keep`, `transform`); it only observes the matched set (see - [Extension Input](#extension-input)) and acts on it out of band. + `keep`, `transform`); it only observes the slice of traffic named by its + `mode` (see [Extension Input](#extension-input)) and acts on it out of band. 4. Implementations MUST explicitly document which extension `type`/`version` pairs they support. 5. If a policy declares an extension whose `type` is unsupported (or whose @@ -1287,12 +1299,14 @@ client either by local configuration or by broadcast via `SyncResponse.extension_configs`. The policy's extension `config` carries only a reference — `{ kind, name }` — -to a target: +to a target. Pairing it with `mode: dropped` archives the records this policy +sampled out (the "waste"): ```yaml extensions: - type: com.usetero/s3-dump version: 1.0.0 + mode: dropped config: target: kind: s3 From 61e9a6e74df2238357414443ee9cf66439c80505 Mon Sep 17 00:00:00 2001 From: jaronoff97 Date: Mon, 6 Jul 2026 14:14:04 -0400 Subject: [PATCH 3/5] describe where in pipeline extensions sit --- examples/extensions/README.md | 9 +++-- proto/tero/policy/v1/extension.proto | 9 +++-- spec.md | 58 ++++++++++++++++------------ 3 files changed, 43 insertions(+), 33 deletions(-) diff --git a/examples/extensions/README.md b/examples/extensions/README.md index 0dd4dd9..2152195 100644 --- a/examples/extensions/README.md +++ b/examples/extensions/README.md @@ -2,12 +2,13 @@ Extensions attach implementation-specific behavior to a standard policy. Core matching and keep/transform semantics are unchanged — the engine matches -telemetry with the policy's `log`/`metric`/`trace` target, then hands matched -records to the handler registered for the extension `type`. +telemetry with the policy's `log`/`metric`/`trace` target, computes the final +keep outcome, then dispatches selected records to the handler registered for the +extension `type` before transforms run. Extensions are generic: `type`, `version`, and an opaque `config`. The engine -does not interpret `config` — it routes matched records to the handler for -`type`, which parses `config` itself. +does not interpret `config` — it routes records selected by `mode` to the +handler for `type`, which parses `config` itself. Destinations are **not** defined inside the policy. An extension target (an S3 bucket, a downstream OTLP endpoint, etc.) is pre-configured — either locally on diff --git a/proto/tero/policy/v1/extension.proto b/proto/tero/policy/v1/extension.proto index 19d1fc3..809a766 100644 --- a/proto/tero/policy/v1/extension.proto +++ b/proto/tero/policy/v1/extension.proto @@ -10,10 +10,11 @@ option go_package = "github.com/usetero/policy/gen/go/tero/policy/v1"; // // Extensions are a fully generic mechanism for attaching implementation-specific // behavior to a policy. The policy engine matches telemetry using the core -// target (log/metric/trace), then hands matched records to the handler -// registered for the extension `type`. All extension payloads are opaque bytes: -// the engine never interprets them, so any implementation can define its own -// extension types without changing the core schema. +// target (log/metric/trace), computes the final keep outcome, then dispatches +// selected records to the handler registered for the extension `type` before +// transforms run. All extension payloads are opaque bytes: the engine never +// interprets them, so any implementation can define its own extension types +// without changing the core schema. // // tero's own extension types (e.g. the s3-dump destination) live in // tero_extensions.proto. diff --git a/spec.md b/spec.md index 89848d6..2ed9bc7 100644 --- a/spec.md +++ b/spec.md @@ -871,30 +871,37 @@ to enable multi-stage sampling: ## Policy Stages -Policies execute in two fixed stages: +Policies execute in two fixed stages, with extension dispatch between them: ``` -┌─────────────────────────────────────────────────────────────────┐ -│ │ -│ Telemetry ──▶ Match ──┬──▶ Keep ──▶ Transform ──▶ Out │ -│ │ │ -│ │ ┌─────────────────┐ │ -│ │ │ 1. Remove │ │ -│ │ │ 2. Redact │ │ -│ │ │ 3. Rename │ │ -│ │ │ 4. Add │ │ -│ │ └─────────────────┘ │ -│ │ │ -│ └── (policies matched in parallel) │ -│ │ -└─────────────────────────────────────────────────────────────────┘ +┌──────────────────────────────────────────────────────────────────────────┐ +│ │ +│ Telemetry ──▶ Match ──┬──▶ Keep ──▶ Extensions ──▶ Transform ──▶ Out │ +│ │ │ +│ │ ┌─────────────────┐ │ +│ │ │ 1. Remove │ │ +│ │ │ 2. Redact │ │ +│ │ │ 3. Rename │ │ +│ │ │ 4. Add │ │ +│ │ └─────────────────┘ │ +│ │ │ +│ └── (policies matched in parallel) │ +│ │ +└──────────────────────────────────────────────────────────────────────────┘ ``` ### Stage 1: Keep All matching policies contribute their `keep` values. The runtime evaluates these values and applies the most restrictive result. If telemetry is dropped or -sampled out, processing stops. +sampled out, it does not continue to the transform stage. + +### Extension Dispatch + +After the runtime computes the final keep outcome, it classifies records for +each extension by that extension's `mode` and dispatches copies to the registered +extension handler. Extension dispatch observes pre-transform records and MUST NOT +change the final keep outcome or any transform result. ### Stage 2: Transform @@ -1201,14 +1208,15 @@ trace: Extensions attach optional, implementation-specific behavior to a policy without changing core semantics. Core matching and keep/transform decisions are unaffected: the engine matches telemetry using the policy's target -(`log`/`metric`/`trace`), then hands matched records to the handler registered -for each extension `type`. A common use is routing matched telemetry to an -external destination (for example, dumping sampled-out records to S3). +(`log`/`metric`/`trace`), computes the final keep outcome, then dispatches +selected records to the handler registered for each extension `type` before +transforming surviving telemetry. A common use is routing matched telemetry to +an external destination (for example, dumping sampled-out records to S3). The extension mechanism is fully generic. Every extension payload is opaque to -the policy engine — it routes matched records to the handler registered for the -`type` and never interprets the payload. This lets any implementation define its -own extension types without changing the core schema. +the policy engine — it routes records selected by `mode` to the handler +registered for the `type` and never interprets the payload. This lets any +implementation define its own extension types without changing the core schema. ### Declaring an Extension @@ -1262,9 +1270,9 @@ receives exactly the ~99.99% that were sampled out. Delivery is a side-channel: the engine copies the selected records to the extension and MUST NOT let the extension change the `keep`/`transform` outcome -applied to the surviving pipeline. Records are delivered as matched (before this -policy's `transform`); post-transform delivery, if needed, is defined by the -extension `type`. +applied to the surviving pipeline. Records are delivered after the final keep +outcome is known and before any transform operations run; post-transform +delivery, if needed, is defined by the extension `type`. ### Rules From ed3e395a856972409e03025317b4eb855a8b9135 Mon Sep 17 00:00:00 2001 From: jaronoff97 Date: Mon, 6 Jul 2026 14:19:31 -0400 Subject: [PATCH 4/5] fmt --- spec.md | 212 ++++++++++++++++++++++++++++---------------------------- 1 file changed, 106 insertions(+), 106 deletions(-) diff --git a/spec.md b/spec.md index 2ed9bc7..8ec72ac 100644 --- a/spec.md +++ b/spec.md @@ -55,14 +55,14 @@ A policy MUST contain the following fields: A policy MAY contain the following fields: -| Field | Type | Default | Description | -| ----------------------- | ---------- | ------- | ------------------------------------------------------ | -| `description` | string | empty | Explanation of the policy's purpose. | -| `enabled` | boolean | `true` | Whether the policy is active. | -| `created_at_unix_nano` | fixed64 | - | Creation timestamp in Unix epoch nanoseconds. | -| `modified_at_unix_nano` | fixed64 | - | Last modification timestamp in Unix epoch nanoseconds. | -| `labels` | KeyValue[] | empty | Metadata labels for routing and organization. | -| `extensions` | Extension[] | empty | Implementation-specific behavior. See [Extensions](#extensions). | +| Field | Type | Default | Description | +| ----------------------- | ----------- | ------- | ---------------------------------------------------------------- | +| `description` | string | empty | Explanation of the policy's purpose. | +| `enabled` | boolean | `true` | Whether the policy is active. | +| `created_at_unix_nano` | fixed64 | - | Creation timestamp in Unix epoch nanoseconds. | +| `modified_at_unix_nano` | fixed64 | - | Last modification timestamp in Unix epoch nanoseconds. | +| `labels` | KeyValue[] | empty | Metadata labels for routing and organization. | +| `extensions` | Extension[] | empty | Implementation-specific behavior. See [Extensions](#extensions). | ### Target @@ -176,33 +176,33 @@ output. ##### LogField Enum Values -| Value | Type | Description | -| ------------------------------- | ------ | ---------------------------------------------------- | -| `LOG_FIELD_BODY` | string | The log message body. | -| `LOG_FIELD_SEVERITY_TEXT` | string | Severity as string (e.g., "DEBUG", "INFO", "ERROR"). | +| Value | Type | Description | +| ------------------------------- | ------ | --------------------------------------------------------------------------------------------- | +| `LOG_FIELD_BODY` | string | The log message body. | +| `LOG_FIELD_SEVERITY_TEXT` | string | Severity as string (e.g., "DEBUG", "INFO", "ERROR"). | | `LOG_FIELD_TRACE_ID` | bytes | Associated trace identifier. See [Bytes and Identifier Fields](#bytes-and-identifier-fields). | -| `LOG_FIELD_SPAN_ID` | bytes | Associated span identifier. See [Bytes and Identifier Fields](#bytes-and-identifier-fields). | -| `LOG_FIELD_EVENT_NAME` | string | Event name for event logs. | -| `LOG_FIELD_RESOURCE_SCHEMA_URL` | string | Resource schema URL. | -| `LOG_FIELD_SCOPE_SCHEMA_URL` | string | Scope schema URL. | +| `LOG_FIELD_SPAN_ID` | bytes | Associated span identifier. See [Bytes and Identifier Fields](#bytes-and-identifier-fields). | +| `LOG_FIELD_EVENT_NAME` | string | Event name for event logs. | +| `LOG_FIELD_RESOURCE_SCHEMA_URL` | string | Resource schema URL. | +| `LOG_FIELD_SCOPE_SCHEMA_URL` | string | Scope schema URL. | #### Match Types A matcher MUST specify exactly one match type: -| Type | Value Type | Description | -| ------------- | ---------- | -------------------------------------------------------------- | -| `exact` | string | String field value MUST equal the specified string exactly. | -| `regex` | string | String field value MUST match the regular expression. | -| `exists` | boolean | If `true`, field MUST exist. If `false`, field MUST NOT exist. | -| `starts_with` | string | String field value MUST begin with the specified literal string. | -| `ends_with` | string | String field value MUST end with the specified literal string. | -| `contains` | string | String field value MUST contain the specified literal substring. | -| `equals` | Value | Non-string field value MUST equal the typed value. See [Typed and Comparison Matching](#typed-and-comparison-matching). | -| `gt` | NumericValue | Numeric field value MUST be greater than the value. | -| `gte` | NumericValue | Numeric field value MUST be greater than or equal to the value. | -| `lt` | NumericValue | Numeric field value MUST be less than the value. | -| `lte` | NumericValue | Numeric field value MUST be less than or equal to the value. | +| Type | Value Type | Description | +| ------------- | ------------ | ----------------------------------------------------------------------------------------------------------------------- | +| `exact` | string | String field value MUST equal the specified string exactly. | +| `regex` | string | String field value MUST match the regular expression. | +| `exists` | boolean | If `true`, field MUST exist. If `false`, field MUST NOT exist. | +| `starts_with` | string | String field value MUST begin with the specified literal string. | +| `ends_with` | string | String field value MUST end with the specified literal string. | +| `contains` | string | String field value MUST contain the specified literal substring. | +| `equals` | Value | Non-string field value MUST equal the typed value. See [Typed and Comparison Matching](#typed-and-comparison-matching). | +| `gt` | NumericValue | Numeric field value MUST be greater than the value. | +| `gte` | NumericValue | Numeric field value MUST be greater than or equal to the value. | +| `lt` | NumericValue | Numeric field value MUST be less than the value. | +| `lte` | NumericValue | Numeric field value MUST be less than or equal to the value. | Regular expressions MUST use [RE2 syntax](https://github.com/google/re2/wiki/Syntax) for cross-implementation @@ -241,10 +241,10 @@ NumericValue { Two distinct types are used deliberately: because the comparison matchers take a `NumericValue`, comparing against a non-numeric value (a bool or bytes) is unrepresentable in the schema rather than something that must be rejected during -compilation. `Value` has no string variant for the same reason — string -equality is expressed with `exact`. `int_value` preserves full 64-bit precision -for large integer fields (for example, nanosecond timestamps) that a `double` -cannot represent exactly. +compilation. `Value` has no string variant for the same reason — string equality +is expressed with `exact`. `int_value` preserves full 64-bit precision for large +integer fields (for example, nanosecond timestamps) that a `double` cannot +represent exactly. > **Future direction:** `exact` is expected to be deprecated. Once `Value` gains > a string variant, all equality — string and non-string — will be expressed @@ -273,11 +273,11 @@ YAML/JSON. For shorthand, the literal's type determines the `Value` variant: ```yaml # Shorthand — type inferred from the literal - log_attribute: ["http.response.status_code"] - gte: 500 # int + gte: 500 # int - log_attribute: ["sampling.ratio"] - lt: 0.5 # double + lt: 0.5 # double - log_attribute: ["deprecated"] - equals: true # bool + equals: true # bool # Canonical proto form - log_attribute: ["http.response.status_code"] @@ -324,10 +324,10 @@ encoding cost is paid once per policy rather than once per telemetry record. # value is supplied explicitly via equals. - log_attribute: ["raw.token"] equals: - hex_value: "deadbeef" # hex string (readable) + hex_value: "deadbeef" # hex string (readable) - log_attribute: ["raw.token"] equals: - bytes_value: "3q2+7w==" # proto-native base64 + bytes_value: "3q2+7w==" # proto-native base64 ``` `hex_value` and `bytes_value` are two encodings of the same bytes; an @@ -346,14 +346,14 @@ are interchangeable. - The numeric comparison matchers (`gt`, `gte`, `lt`, `lte`) do not apply to bytes. -**Type coercion and performance.** Implementations SHOULD always coerce between a -field's declared type and the matcher literal — for example, decoding a hex +**Type coercion and performance.** Implementations SHOULD always coerce between +a field's declared type and the matcher literal — for example, decoding a hex string to bytes for a bytes-typed field — so a correctly authored policy matches regardless of how the literal was written. For performance, policies SHOULD also include a string match type where practical: implementations commonly optimize -string and regular-expression matching with a dedicated multi-pattern engine, and -a string matcher lets that fast path pre-filter records before a typed comparison -runs. +string and regular-expression matching with a dedicated multi-pattern engine, +and a string matcher lets that fast path pre-filter records before a typed +comparison runs. #### Case Insensitivity @@ -627,19 +627,19 @@ See [AttributePath](#attributepath) for path syntax. A matcher MUST specify exactly one match type (except when using `metric_type`, which performs implicit equality): -| Type | Value Type | Description | -| ------------- | ---------- | -------------------------------------------------------------- | -| `exact` | string | String field value MUST equal the specified string exactly. | -| `regex` | string | String field value MUST match the regular expression. | -| `exists` | boolean | If `true`, field MUST exist. If `false`, field MUST NOT exist. | -| `starts_with` | string | String field value MUST begin with the specified literal string. | -| `ends_with` | string | String field value MUST end with the specified literal string. | -| `contains` | string | String field value MUST contain the specified literal substring. | -| `equals` | Value | Non-string field value MUST equal the typed value. See [Typed and Comparison Matching](#typed-and-comparison-matching). | -| `gt` | NumericValue | Numeric field value MUST be greater than the value. | -| `gte` | NumericValue | Numeric field value MUST be greater than or equal to the value. | -| `lt` | NumericValue | Numeric field value MUST be less than the value. | -| `lte` | NumericValue | Numeric field value MUST be less than or equal to the value. | +| Type | Value Type | Description | +| ------------- | ------------ | ----------------------------------------------------------------------------------------------------------------------- | +| `exact` | string | String field value MUST equal the specified string exactly. | +| `regex` | string | String field value MUST match the regular expression. | +| `exists` | boolean | If `true`, field MUST exist. If `false`, field MUST NOT exist. | +| `starts_with` | string | String field value MUST begin with the specified literal string. | +| `ends_with` | string | String field value MUST end with the specified literal string. | +| `contains` | string | String field value MUST contain the specified literal substring. | +| `equals` | Value | Non-string field value MUST equal the typed value. See [Typed and Comparison Matching](#typed-and-comparison-matching). | +| `gt` | NumericValue | Numeric field value MUST be greater than the value. | +| `gte` | NumericValue | Numeric field value MUST be greater than or equal to the value. | +| `lt` | NumericValue | Numeric field value MUST be less than the value. | +| `lte` | NumericValue | Numeric field value MUST be less than or equal to the value. | Regular expressions MUST use [RE2 syntax](https://github.com/google/re2/wiki/Syntax) for cross-implementation @@ -704,33 +704,33 @@ TraceMatcher { A matcher MUST specify exactly one field selector: -| Selector | Type | Description | -| -------------------- | ------------------- | ------------------------------------------------ | -| `trace_field` | TraceField enum | Well-known span field. | -| `span_attribute` | AttributePath | Span attribute by path. | -| `resource_attribute` | AttributePath | Resource attribute by path. | -| `scope_attribute` | AttributePath | Instrumentation scope attribute by path. | -| `span_kind` | SpanKind enum | Span kind (implicit equality match). | -| `span_status` | SpanStatusCode enum | Span status code (implicit equality match). | -| `event_name` | string | Event name (matches if span contains the event). | -| `event_attribute` | AttributePath | Event attribute path (matches if present). | +| Selector | Type | Description | +| -------------------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | +| `trace_field` | TraceField enum | Well-known span field. | +| `span_attribute` | AttributePath | Span attribute by path. | +| `resource_attribute` | AttributePath | Resource attribute by path. | +| `scope_attribute` | AttributePath | Instrumentation scope attribute by path. | +| `span_kind` | SpanKind enum | Span kind (implicit equality match). | +| `span_status` | SpanStatusCode enum | Span status code (implicit equality match). | +| `event_name` | string | Event name (matches if span contains the event). | +| `event_attribute` | AttributePath | Event attribute path (matches if present). | | `link_trace_id` | string (hex) | Link trace ID, authored as hex and matched as bytes (matches if span has link). See [Bytes and Identifier Fields](#bytes-and-identifier-fields). | See [AttributePath](#attributepath) for path syntax. ##### TraceField Enum Values -| Value | Type | Description | -| --------------------------------- | ------ | ------------------------------ | -| `TRACE_FIELD_NAME` | string | The span name. | -| `TRACE_FIELD_TRACE_ID` | bytes | The trace identifier. See [Bytes and Identifier Fields](#bytes-and-identifier-fields). | -| `TRACE_FIELD_SPAN_ID` | bytes | The span identifier. See [Bytes and Identifier Fields](#bytes-and-identifier-fields). | +| Value | Type | Description | +| --------------------------------- | ------ | -------------------------------------------------------------------------------------------- | +| `TRACE_FIELD_NAME` | string | The span name. | +| `TRACE_FIELD_TRACE_ID` | bytes | The trace identifier. See [Bytes and Identifier Fields](#bytes-and-identifier-fields). | +| `TRACE_FIELD_SPAN_ID` | bytes | The span identifier. See [Bytes and Identifier Fields](#bytes-and-identifier-fields). | | `TRACE_FIELD_PARENT_SPAN_ID` | bytes | The parent span identifier. See [Bytes and Identifier Fields](#bytes-and-identifier-fields). | -| `TRACE_FIELD_TRACE_STATE` | string | The W3C tracestate. | -| `TRACE_FIELD_RESOURCE_SCHEMA_URL` | string | Resource schema URL. | -| `TRACE_FIELD_SCOPE_SCHEMA_URL` | string | Scope schema URL. | -| `TRACE_FIELD_SCOPE_NAME` | string | Instrumentation scope name. | -| `TRACE_FIELD_SCOPE_VERSION` | string | Instrumentation scope version. | +| `TRACE_FIELD_TRACE_STATE` | string | The W3C tracestate. | +| `TRACE_FIELD_RESOURCE_SCHEMA_URL` | string | Resource schema URL. | +| `TRACE_FIELD_SCOPE_SCHEMA_URL` | string | Scope schema URL. | +| `TRACE_FIELD_SCOPE_NAME` | string | Instrumentation scope name. | +| `TRACE_FIELD_SCOPE_VERSION` | string | Instrumentation scope version. | ##### SpanKind Enum Values @@ -755,19 +755,19 @@ See [AttributePath](#attributepath) for path syntax. A matcher MUST specify exactly one match type (except when using `span_kind` or `span_status`, which perform implicit equality): -| Type | Value Type | Description | -| ------------- | ---------- | -------------------------------------------------------------- | -| `exact` | string | String field value MUST equal the specified string exactly. | -| `regex` | string | String field value MUST match the regular expression. | -| `exists` | boolean | If `true`, field MUST exist. If `false`, field MUST NOT exist. | -| `starts_with` | string | String field value MUST begin with the specified literal string. | -| `ends_with` | string | String field value MUST end with the specified literal string. | -| `contains` | string | String field value MUST contain the specified literal substring. | -| `equals` | Value | Non-string field value MUST equal the typed value. See [Typed and Comparison Matching](#typed-and-comparison-matching). | -| `gt` | NumericValue | Numeric field value MUST be greater than the value. | -| `gte` | NumericValue | Numeric field value MUST be greater than or equal to the value. | -| `lt` | NumericValue | Numeric field value MUST be less than the value. | -| `lte` | NumericValue | Numeric field value MUST be less than or equal to the value. | +| Type | Value Type | Description | +| ------------- | ------------ | ----------------------------------------------------------------------------------------------------------------------- | +| `exact` | string | String field value MUST equal the specified string exactly. | +| `regex` | string | String field value MUST match the regular expression. | +| `exists` | boolean | If `true`, field MUST exist. If `false`, field MUST NOT exist. | +| `starts_with` | string | String field value MUST begin with the specified literal string. | +| `ends_with` | string | String field value MUST end with the specified literal string. | +| `contains` | string | String field value MUST contain the specified literal substring. | +| `equals` | Value | Non-string field value MUST equal the typed value. See [Typed and Comparison Matching](#typed-and-comparison-matching). | +| `gt` | NumericValue | Numeric field value MUST be greater than the value. | +| `gte` | NumericValue | Numeric field value MUST be greater than or equal to the value. | +| `lt` | NumericValue | Numeric field value MUST be less than the value. | +| `lte` | NumericValue | Numeric field value MUST be less than or equal to the value. | Regular expressions MUST use [RE2 syntax](https://github.com/google/re2/wiki/Syntax) for cross-implementation @@ -899,9 +899,9 @@ sampled out, it does not continue to the transform stage. ### Extension Dispatch After the runtime computes the final keep outcome, it classifies records for -each extension by that extension's `mode` and dispatches copies to the registered -extension handler. Extension dispatch observes pre-transform records and MUST NOT -change the final keep outcome or any transform result. +each extension by that extension's `mode` and dispatches copies to the +registered extension handler. Extension dispatch observes pre-transform records +and MUST NOT change the final keep outcome or any transform result. ### Stage 2: Transform @@ -1222,11 +1222,11 @@ implementation define its own extension types without changing the core schema. A policy declares one or more extensions: -| Field | Type | Description | -| --------- | ------ | ------------------------------------------------------------- | -| `type` | string | Reverse-FQDN extension identifier (e.g. `com.usetero/s3-dump`). | -| `version` | string | Extension schema version (semver). | -| `config` | object | Opaque, type-defined configuration, parsed by the handler. | +| Field | Type | Description | +| --------- | ------ | ----------------------------------------------------------------------------------------------------------- | +| `type` | string | Reverse-FQDN extension identifier (e.g. `com.usetero/s3-dump`). | +| `version` | string | Extension schema version (semver). | +| `config` | object | Opaque, type-defined configuration, parsed by the handler. | | `mode` | enum | Which slice of traffic the engine delivers. Defaults to `matched`. See [Extension Input](#extension-input). | ```yaml @@ -1251,20 +1251,20 @@ delivers to it, relative to the extension's policy. Each record is classified on two independent axes — whether it **matched** the policy's matchers, and its final **keep** outcome — and a mode names a slice of that classification: -| Mode | Records delivered | -| ----------- | ----------------------------------------------------- | -| `kept` | Matched the policy and survived the keep stage. | +| Mode | Records delivered | +| ----------- | ----------------------------------------------------------------------------------- | +| `kept` | Matched the policy and survived the keep stage. | | `dropped` | Matched the policy but was removed by keep (dropped, sampled out, or rate limited). | -| `unmatched` | Did not match the policy's matchers. | -| `matched` | Matched the policy, regardless of keep (`kept` + `dropped`). **Default.** | -| `all` | Every record of the signal type (`kept` + `dropped` + `unmatched`). | +| `unmatched` | Did not match the policy's matchers. | +| `matched` | Matched the policy, regardless of keep (`kept` + `dropped`). **Default.** | +| `all` | Every record of the signal type (`kept` + `dropped` + `unmatched`). | `kept`, `dropped`, and `unmatched` are disjoint and together cover the whole stream; `matched` and `all` are convenience unions. The keep outcome is the record's **final pipeline outcome** — the most restrictive result across all matching policies — so `dropped` captures records -removed even by a *different* policy. This is what makes "dump waste" work: a +removed even by a _different_ policy. This is what makes "dump waste" work: a `com.usetero/s3-dump` extension with `mode: dropped` on a `keep: .01%` policy receives exactly the ~99.99% that were sampled out. @@ -1306,8 +1306,8 @@ prefix). It is identified by the `(kind, name)` pair and becomes available to a client either by local configuration or by broadcast via `SyncResponse.extension_configs`. -The policy's extension `config` carries only a reference — `{ kind, name }` — -to a target. Pairing it with `mode: dropped` archives the records this policy +The policy's extension `config` carries only a reference — `{ kind, name }` — to +a target. Pairing it with `mode: dropped` archives the records this policy sampled out (the "waste"): ```yaml From 7fb731a2d8c3d26bf03c13ce7c8b32f5960a73a1 Mon Sep 17 00:00:00 2001 From: jaronoff97 Date: Mon, 6 Jul 2026 14:35:52 -0400 Subject: [PATCH 5/5] feat: deprecate exact, move it to be in typed value --- README.md | 4 +- examples/cost-control/README.md | 13 ++- .../foundation/drop-cache-hit-logs.yaml | 2 +- .../foundation/rate-limit-batch-job-logs.yaml | 2 +- .../rate-limit-error-logs-1-per-5m.yaml | 2 +- .../sample-api-logs-by-request.yaml | 2 +- .../checkout-api/drop-cart-item-added.yaml | 2 +- .../checkout-api/drop-request-received.yaml | 2 +- .../checkout-api/sample-order-validated.yaml | 2 +- .../user-service/drop-cache-hit.yaml | 2 +- .../user-service/drop-session-heartbeat.yaml | 2 +- examples/pci-compliance/README.md | 4 +- .../foundation/drop-payment-debug.yaml | 2 +- .../checkout-api/redact-order-submitted.yaml | 2 +- .../payment-api/redact-payment-processed.yaml | 2 +- .../redact-transaction-logged.yaml | 2 +- .../foundation/keep-spans-for-trace.yaml | 5 +- .../foundation/sample-by-environment.yaml | 2 +- .../keep-order-submission-spans.yaml | 4 +- .../checkout-api/sample-cart-operations.yaml | 2 +- .../checkout-api/sample-inventory-checks.yaml | 4 +- .../payment-api/drop-heartbeat-spans.yaml | 4 +- .../payment-api/keep-payment-spans.yaml | 2 +- .../payment-api/sample-validation-spans.yaml | 2 +- proto/tero/policy/v1/log.proto | 24 ++-- proto/tero/policy/v1/metric.proto | 20 ++-- proto/tero/policy/v1/shared.proto | 14 +-- proto/tero/policy/v1/trace.proto | 26 +++-- spec.md | 103 +++++++++--------- 29 files changed, 139 insertions(+), 120 deletions(-) diff --git a/README.md b/README.md index fc75da2..e3b03b8 100644 --- a/README.md +++ b/README.md @@ -28,9 +28,9 @@ name: Drop checkout debug logs log: match: - resource_attribute: ["service.name"] - exact: checkout-api + equals: checkout-api - log_field: LOG_FIELD_SEVERITY_TEXT - exact: DEBUG + equals: DEBUG keep: none ``` diff --git a/examples/cost-control/README.md b/examples/cost-control/README.md index 467aa79..3733bb9 100644 --- a/examples/cost-control/README.md +++ b/examples/cost-control/README.md @@ -78,7 +78,7 @@ name: Rate limit repeated error logs to 1 per 5 minutes per request log: match: - log_field: severity_text - exact: ERROR + equals: ERROR keep: "1/5m" sample_key: log_attribute: ["request_id"] @@ -86,10 +86,11 @@ log: #### Typed and comparison matching -The string match types (`exact`, `regex`, `contains`, ...) only see string -values. To match numbers and booleans, use the typed `equals` matcher or the -numeric comparators `gt` / `gte` / `lt` / `lte`. The value's type is inferred -from the literal — `500` is an int, `0.1` is a double, `true` is a bool. +Use `equals` for typed equality across strings, numbers, booleans, and bytes. +The pattern matchers (`regex`, `contains`, ...) only see string values. To match +numeric ranges, use the typed comparators `gt` / `gte` / `lt` / `lte`. The +value's type is inferred from the literal — `500` is an int, `0.1` is a double, +`true` is a bool. Keep only error logs by dropping the successful status-code range: @@ -134,7 +135,7 @@ name: Sample order validated logs log: match: - resource_attribute: ["service.name"] - exact: checkout-api + equals: checkout-api - log_field: body regex: "order validated" keep: 50% diff --git a/examples/cost-control/foundation/drop-cache-hit-logs.yaml b/examples/cost-control/foundation/drop-cache-hit-logs.yaml index 93941c6..1d6222b 100644 --- a/examples/cost-control/foundation/drop-cache-hit-logs.yaml +++ b/examples/cost-control/foundation/drop-cache-hit-logs.yaml @@ -3,7 +3,7 @@ name: Drop logs flagged as cache hits description: > Cache hits are high volume and low value. This matches the boolean attribute directly with equals instead of regexing a stringified flag — equals: true - matches a real boolean value, which the string match types cannot see. + matches a real boolean value, which the string pattern matchers cannot see. log: match: diff --git a/examples/cost-control/foundation/rate-limit-batch-job-logs.yaml b/examples/cost-control/foundation/rate-limit-batch-job-logs.yaml index 09c0fac..dc1c2d9 100644 --- a/examples/cost-control/foundation/rate-limit-batch-job-logs.yaml +++ b/examples/cost-control/foundation/rate-limit-batch-job-logs.yaml @@ -4,7 +4,7 @@ name: Rate limit batch job logs to 100/s per job log: match: - resource_attribute: ["service.name"] - exact: batch-processor + equals: batch-processor keep: "100/s" sample_key: log_attribute: ["job_id"] diff --git a/examples/cost-control/foundation/rate-limit-error-logs-1-per-5m.yaml b/examples/cost-control/foundation/rate-limit-error-logs-1-per-5m.yaml index deff4ea..59a4c5a 100644 --- a/examples/cost-control/foundation/rate-limit-error-logs-1-per-5m.yaml +++ b/examples/cost-control/foundation/rate-limit-error-logs-1-per-5m.yaml @@ -4,7 +4,7 @@ name: Rate limit repeated error logs to 1 per 5 minutes per request log: match: - log_field: severity_text - exact: ERROR + equals: ERROR keep: "1/5m" sample_key: log_attribute: ["request_id"] diff --git a/examples/cost-control/foundation/sample-api-logs-by-request.yaml b/examples/cost-control/foundation/sample-api-logs-by-request.yaml index c816b2f..141202e 100644 --- a/examples/cost-control/foundation/sample-api-logs-by-request.yaml +++ b/examples/cost-control/foundation/sample-api-logs-by-request.yaml @@ -4,7 +4,7 @@ name: Sample API logs at 10% by request log: match: - resource_attribute: ["service.name"] - exact: api-gateway + equals: api-gateway keep: "10%" sample_key: log_attribute: ["request_id"] diff --git a/examples/cost-control/generated/checkout-api/drop-cart-item-added.yaml b/examples/cost-control/generated/checkout-api/drop-cart-item-added.yaml index 56fdf5b..1ccdd69 100644 --- a/examples/cost-control/generated/checkout-api/drop-cart-item-added.yaml +++ b/examples/cost-control/generated/checkout-api/drop-cart-item-added.yaml @@ -4,7 +4,7 @@ name: Drop cart item added logs log: match: - resource_attribute: ["service.name"] - exact: checkout-api + equals: checkout-api - log_field: body regex: "item added to cart" keep: none diff --git a/examples/cost-control/generated/checkout-api/drop-request-received.yaml b/examples/cost-control/generated/checkout-api/drop-request-received.yaml index 2a7a060..7fc8e04 100644 --- a/examples/cost-control/generated/checkout-api/drop-request-received.yaml +++ b/examples/cost-control/generated/checkout-api/drop-request-received.yaml @@ -4,7 +4,7 @@ name: Drop request received logs log: match: - resource_attribute: ["service.name"] - exact: checkout-api + equals: checkout-api - log_field: body regex: "request received" keep: none diff --git a/examples/cost-control/generated/checkout-api/sample-order-validated.yaml b/examples/cost-control/generated/checkout-api/sample-order-validated.yaml index 7267dd8..8197109 100644 --- a/examples/cost-control/generated/checkout-api/sample-order-validated.yaml +++ b/examples/cost-control/generated/checkout-api/sample-order-validated.yaml @@ -4,7 +4,7 @@ name: Sample order validated logs log: match: - resource_attribute: ["service.name"] - exact: checkout-api + equals: checkout-api - log_field: body regex: "order validated" keep: 50% diff --git a/examples/cost-control/generated/user-service/drop-cache-hit.yaml b/examples/cost-control/generated/user-service/drop-cache-hit.yaml index 0172232..b278a6c 100644 --- a/examples/cost-control/generated/user-service/drop-cache-hit.yaml +++ b/examples/cost-control/generated/user-service/drop-cache-hit.yaml @@ -4,7 +4,7 @@ name: Drop cache hit logs log: match: - resource_attribute: ["service.name"] - exact: user-service + equals: user-service - log_field: body regex: "cache hit for user" keep: none diff --git a/examples/cost-control/generated/user-service/drop-session-heartbeat.yaml b/examples/cost-control/generated/user-service/drop-session-heartbeat.yaml index 4dbb6e2..d7f39fc 100644 --- a/examples/cost-control/generated/user-service/drop-session-heartbeat.yaml +++ b/examples/cost-control/generated/user-service/drop-session-heartbeat.yaml @@ -4,7 +4,7 @@ name: Drop session heartbeat logs log: match: - resource_attribute: ["service.name"] - exact: user-service + equals: user-service - log_field: body regex: "session heartbeat" keep: none diff --git a/examples/pci-compliance/README.md b/examples/pci-compliance/README.md index d4cb571..77c3d31 100644 --- a/examples/pci-compliance/README.md +++ b/examples/pci-compliance/README.md @@ -60,7 +60,7 @@ log: - resource_attribute: ["service.name"] regex: "^payment-" - log_field: severity_text - exact: DEBUG + equals: DEBUG keep: none ``` @@ -89,7 +89,7 @@ name: Redact transaction logged events log: match: - resource_attribute: ["service.name"] - exact: payment-api + equals: payment-api - log_field: body regex: "transaction logged" transform: diff --git a/examples/pci-compliance/foundation/drop-payment-debug.yaml b/examples/pci-compliance/foundation/drop-payment-debug.yaml index bdc6d7c..8a9348e 100644 --- a/examples/pci-compliance/foundation/drop-payment-debug.yaml +++ b/examples/pci-compliance/foundation/drop-payment-debug.yaml @@ -6,5 +6,5 @@ log: - resource_attribute: ["service.name"] regex: "^payment-" - log_field: severity_text - exact: DEBUG + equals: DEBUG keep: none diff --git a/examples/pci-compliance/generated/checkout-api/redact-order-submitted.yaml b/examples/pci-compliance/generated/checkout-api/redact-order-submitted.yaml index 1dda346..3927284 100644 --- a/examples/pci-compliance/generated/checkout-api/redact-order-submitted.yaml +++ b/examples/pci-compliance/generated/checkout-api/redact-order-submitted.yaml @@ -4,7 +4,7 @@ name: Redact order submitted events log: match: - resource_attribute: ["service.name"] - exact: checkout-api + equals: checkout-api - log_field: body regex: "order submitted" transform: diff --git a/examples/pci-compliance/generated/payment-api/redact-payment-processed.yaml b/examples/pci-compliance/generated/payment-api/redact-payment-processed.yaml index 9d4a582..4498799 100644 --- a/examples/pci-compliance/generated/payment-api/redact-payment-processed.yaml +++ b/examples/pci-compliance/generated/payment-api/redact-payment-processed.yaml @@ -4,7 +4,7 @@ name: Redact payment processed events log: match: - resource_attribute: ["service.name"] - exact: payment-api + equals: payment-api - log_field: body regex: "payment processed" transform: diff --git a/examples/pci-compliance/generated/payment-api/redact-transaction-logged.yaml b/examples/pci-compliance/generated/payment-api/redact-transaction-logged.yaml index fe306f1..024b4e1 100644 --- a/examples/pci-compliance/generated/payment-api/redact-transaction-logged.yaml +++ b/examples/pci-compliance/generated/payment-api/redact-transaction-logged.yaml @@ -4,7 +4,7 @@ name: Redact transaction logged events log: match: - resource_attribute: ["service.name"] - exact: payment-api + equals: payment-api - log_field: body regex: "transaction logged" transform: diff --git a/examples/trace-sampling/foundation/keep-spans-for-trace.yaml b/examples/trace-sampling/foundation/keep-spans-for-trace.yaml index 0c455d9..9acbbe7 100644 --- a/examples/trace-sampling/foundation/keep-spans-for-trace.yaml +++ b/examples/trace-sampling/foundation/keep-spans-for-trace.yaml @@ -4,11 +4,12 @@ description: > Retains every span belonging to one trace id while debugging an incident. trace_id is a bytes field, so the hex literal is decoded to raw bytes once at policy-load time and compared as bytes — no per-span hex encoding required. - The exact matcher is case-insensitive on the hex input. + hex_value is case-insensitive on input. trace: match: - trace_field: TRACE_FIELD_TRACE_ID - exact: "4bf92f3577b34da6a3ce929d0e0e4736" + equals: + hex_value: "4bf92f3577b34da6a3ce929d0e0e4736" keep: percentage: 100.0 diff --git a/examples/trace-sampling/foundation/sample-by-environment.yaml b/examples/trace-sampling/foundation/sample-by-environment.yaml index 688b063..23bd90f 100644 --- a/examples/trace-sampling/foundation/sample-by-environment.yaml +++ b/examples/trace-sampling/foundation/sample-by-environment.yaml @@ -7,6 +7,6 @@ description: > trace: match: - resource_attribute: ["deployment.environment"] - exact: staging + equals: staging keep: percentage: 50.0 diff --git a/examples/trace-sampling/generated/checkout-api/keep-order-submission-spans.yaml b/examples/trace-sampling/generated/checkout-api/keep-order-submission-spans.yaml index d2b1f7e..ab8147b 100644 --- a/examples/trace-sampling/generated/checkout-api/keep-order-submission-spans.yaml +++ b/examples/trace-sampling/generated/checkout-api/keep-order-submission-spans.yaml @@ -7,8 +7,8 @@ description: > trace: match: - resource_attribute: ["service.name"] - exact: checkout-api + equals: checkout-api - trace_field: name - exact: SubmitOrder + equals: SubmitOrder keep: percentage: 100.0 diff --git a/examples/trace-sampling/generated/checkout-api/sample-cart-operations.yaml b/examples/trace-sampling/generated/checkout-api/sample-cart-operations.yaml index 115ac9f..ccaad72 100644 --- a/examples/trace-sampling/generated/checkout-api/sample-cart-operations.yaml +++ b/examples/trace-sampling/generated/checkout-api/sample-cart-operations.yaml @@ -7,7 +7,7 @@ description: > trace: match: - resource_attribute: ["service.name"] - exact: checkout-api + equals: checkout-api - trace_field: name regex: "^(AddToCart|RemoveFromCart|UpdateCart)" keep: diff --git a/examples/trace-sampling/generated/checkout-api/sample-inventory-checks.yaml b/examples/trace-sampling/generated/checkout-api/sample-inventory-checks.yaml index 2b90572..78f561e 100644 --- a/examples/trace-sampling/generated/checkout-api/sample-inventory-checks.yaml +++ b/examples/trace-sampling/generated/checkout-api/sample-inventory-checks.yaml @@ -7,10 +7,10 @@ description: > trace: match: - resource_attribute: ["service.name"] - exact: checkout-api + equals: checkout-api - span_kind: CLIENT - span_attribute: ["rpc.service"] - exact: InventoryService + equals: InventoryService keep: percentage: 10.0 mode: proportional diff --git a/examples/trace-sampling/generated/payment-api/drop-heartbeat-spans.yaml b/examples/trace-sampling/generated/payment-api/drop-heartbeat-spans.yaml index bce9c20..6e7d08f 100644 --- a/examples/trace-sampling/generated/payment-api/drop-heartbeat-spans.yaml +++ b/examples/trace-sampling/generated/payment-api/drop-heartbeat-spans.yaml @@ -7,8 +7,8 @@ description: > trace: match: - resource_attribute: ["service.name"] - exact: payment-api + equals: payment-api - trace_field: name - exact: PaymentGatewayHeartbeat + equals: PaymentGatewayHeartbeat keep: percentage: 0.0 diff --git a/examples/trace-sampling/generated/payment-api/keep-payment-spans.yaml b/examples/trace-sampling/generated/payment-api/keep-payment-spans.yaml index 655878c..cb09e19 100644 --- a/examples/trace-sampling/generated/payment-api/keep-payment-spans.yaml +++ b/examples/trace-sampling/generated/payment-api/keep-payment-spans.yaml @@ -7,7 +7,7 @@ description: > trace: match: - resource_attribute: ["service.name"] - exact: payment-api + equals: payment-api - trace_field: name regex: "^(ProcessPayment|AuthorizePayment|CapturePayment|RefundPayment)" keep: diff --git a/examples/trace-sampling/generated/payment-api/sample-validation-spans.yaml b/examples/trace-sampling/generated/payment-api/sample-validation-spans.yaml index f2ee42d..ae85684 100644 --- a/examples/trace-sampling/generated/payment-api/sample-validation-spans.yaml +++ b/examples/trace-sampling/generated/payment-api/sample-validation-spans.yaml @@ -7,7 +7,7 @@ description: > trace: match: - resource_attribute: ["service.name"] - exact: payment-api + equals: payment-api - trace_field: name regex: "^Validate" - span_status: OK diff --git a/proto/tero/policy/v1/log.proto b/proto/tero/policy/v1/log.proto index f7d9c4e..c1e0576 100644 --- a/proto/tero/policy/v1/log.proto +++ b/proto/tero/policy/v1/log.proto @@ -79,8 +79,8 @@ enum LogField { LOG_FIELD_BODY = 1; LOG_FIELD_SEVERITY_TEXT = 2; // trace_id and span_id are bytes. They are authored as lowercase hex and - // matched as raw bytes (exact/equals), or as their hex rendering for string - // match types. See the Value message and the spec's + // matched as raw bytes with equals.hex_value, or as their hex rendering for + // string pattern match types. See the Value message and the spec's // "Bytes and Identifier Fields" section. LOG_FIELD_TRACE_ID = 3; LOG_FIELD_SPAN_ID = 4; @@ -124,12 +124,17 @@ message LogMatcher { // Match type. Exactly one must be set. // - // The string match types (exact, regex, starts_with, ends_with, contains) - // operate only on string field values. To match non-string values, use the - // typed equals matcher or the numeric comparison matchers (gt, gte, lt, lte). + // Use the typed equals matcher for equality. The exact field is deprecated and + // will move to reserved in a future version after wire-format users migrate to + // equals.string_value. + // + // The string pattern match types (regex, starts_with, ends_with, contains) + // operate only on string field values. Use the numeric comparison matchers + // (gt, gte, lt, lte) for ranges. oneof match { - // Exact string match (string field values only) - string exact = 10; + // Deprecated: use equals.string_value instead. This field will move to + // reserved in a future version. + string exact = 10 [deprecated = true]; // Regular expression match (string field values only) string regex = 11; @@ -146,7 +151,7 @@ message LogMatcher { // Literal substring match (string field values only) string contains = 15; - // Typed equality for non-string field values (bool, int, double, bytes) + // Typed equality for string, bool, int, double, or bytes field values. Value equals = 22; // Numeric greater-than comparison (int/double field values) @@ -165,7 +170,8 @@ message LogMatcher { // If true, inverts the match result bool negate = 20; - // If true, applies case-insensitive matching to all match types + // If true, applies case-insensitive matching to equals.string_value and the + // string pattern match types. bool case_insensitive = 21; } diff --git a/proto/tero/policy/v1/metric.proto b/proto/tero/policy/v1/metric.proto index 84998b7..35a8db4 100644 --- a/proto/tero/policy/v1/metric.proto +++ b/proto/tero/policy/v1/metric.proto @@ -100,12 +100,17 @@ message MetricMatcher { // Match type. Exactly one must be set. // Note: For metric_type field, only exists is valid (type equality is implicit). // - // The string match types (exact, regex, starts_with, ends_with, contains) - // operate only on string field values. To match non-string values, use the - // typed equals matcher or the numeric comparison matchers (gt, gte, lt, lte). + // Use the typed equals matcher for equality. The exact field is deprecated and + // will move to reserved in a future version after wire-format users migrate to + // equals.string_value. + // + // The string pattern match types (regex, starts_with, ends_with, contains) + // operate only on string field values. Use the numeric comparison matchers + // (gt, gte, lt, lte) for ranges. oneof match { - // Exact string match (string field values only) - string exact = 10; + // Deprecated: use equals.string_value instead. This field will move to + // reserved in a future version. + string exact = 10 [deprecated = true]; // Regular expression match (string field values only) string regex = 11; @@ -122,7 +127,7 @@ message MetricMatcher { // Literal substring match (string field values only) string contains = 15; - // Typed equality for non-string field values (bool, int, double, bytes) + // Typed equality for string, bool, int, double, or bytes field values. Value equals = 22; // Numeric greater-than comparison (int/double field values) @@ -141,6 +146,7 @@ message MetricMatcher { // If true, inverts the match result bool negate = 20; - // If true, applies case-insensitive matching to all match types + // If true, applies case-insensitive matching to equals.string_value and the + // string pattern match types. bool case_insensitive = 21; } diff --git a/proto/tero/policy/v1/shared.proto b/proto/tero/policy/v1/shared.proto index c2856e0..260cead 100644 --- a/proto/tero/policy/v1/shared.proto +++ b/proto/tero/policy/v1/shared.proto @@ -51,9 +51,7 @@ message AttributePath { repeated string path = 1; } -// Value carries a typed, non-string scalar for the `equals` matcher. String -// equality is expressed with the `exact` match type, so this message -// intentionally has no string variant. +// Value carries a typed scalar for the `equals` matcher. // // `equals` matches when the field value has the same type and value as the set // variant. Integer and floating-point values are compared in a single numeric @@ -68,19 +66,16 @@ message AttributePath { // equals: true -> bool_value // equals: 200 -> int_value // equals: 0.5 -> double_value +// equals: "foo" -> string_value // // Bytes are authored either as proto-native base64 (`bytes_value`) or, more // readably, as a lowercase-hex string (`hex_value`). The two are equivalent — // hex_value is decoded to bytes at policy-compile time and yields the same bytes // as the corresponding bytes_value — but hex_value keeps identifiers // (trace/span ids) readable in the canonical proto/JSON form instead of base64. -// A bare string literal (e.g. `equals: "foo"`) MUST be rejected — use `exact` -// for strings. // -// Future direction: the `exact` match type is expected to be deprecated. Once a -// string variant is added to this message, all equality (string and non-string) -// will be expressed through `equals`, with `exact` kept only for backward -// compatibility. +// String equality SHOULD be authored with string_value. The older matcher-level +// `exact` field is deprecated and will move to reserved in a future version. message Value { // Exactly one must be set. oneof value { @@ -93,6 +88,7 @@ message Value { // input, canonically lowercase). Decoded to bytes at policy-compile time; // an invalid hex string MUST be rejected. string hex_value = 5; + string string_value = 6; } } diff --git a/proto/tero/policy/v1/trace.proto b/proto/tero/policy/v1/trace.proto index 0d65ba3..792751b 100644 --- a/proto/tero/policy/v1/trace.proto +++ b/proto/tero/policy/v1/trace.proto @@ -32,9 +32,9 @@ enum TraceField { // Span fields TRACE_FIELD_NAME = 1; // trace_id, span_id, and parent_span_id are bytes. They are authored as - // lowercase hex and matched as raw bytes (exact/equals), or as their hex - // rendering for string match types. See the Value message and the spec's - // "Bytes and Identifier Fields" section. + // lowercase hex and matched as raw bytes with equals.hex_value, or as their + // hex rendering for string pattern match types. See the Value message and the + // spec's "Bytes and Identifier Fields" section. TRACE_FIELD_TRACE_ID = 2; TRACE_FIELD_SPAN_ID = 3; TRACE_FIELD_PARENT_SPAN_ID = 4; @@ -119,12 +119,17 @@ message TraceMatcher { // Match type. Exactly one must be set. // Note: For span_kind and span_status fields, only exists is valid (equality is implicit). // - // The string match types (exact, regex, starts_with, ends_with, contains) - // operate only on string field values. To match non-string values, use the - // typed equals matcher or the numeric comparison matchers (gt, gte, lt, lte). + // Use the typed equals matcher for equality. The exact field is deprecated and + // will move to reserved in a future version after wire-format users migrate to + // equals.string_value. + // + // The string pattern match types (regex, starts_with, ends_with, contains) + // operate only on string field values. Use the numeric comparison matchers + // (gt, gte, lt, lte) for ranges. oneof match { - // Exact string match (string field values only) - string exact = 10; + // Deprecated: use equals.string_value instead. This field will move to + // reserved in a future version. + string exact = 10 [deprecated = true]; // Regular expression match (string field values only) string regex = 11; @@ -141,7 +146,7 @@ message TraceMatcher { // Literal substring match (string field values only) string contains = 15; - // Typed equality for non-string field values (bool, int, double, bytes) + // Typed equality for string, bool, int, double, or bytes field values. Value equals = 22; // Numeric greater-than comparison (int/double field values) @@ -160,7 +165,8 @@ message TraceMatcher { // If true, inverts the match result bool negate = 20; - // If true, applies case-insensitive matching to all match types + // If true, applies case-insensitive matching to equals.string_value and the + // string pattern match types. bool case_insensitive = 21; } diff --git a/spec.md b/spec.md index 8ec72ac..0fbf729 100644 --- a/spec.md +++ b/spec.md @@ -192,13 +192,13 @@ A matcher MUST specify exactly one match type: | Type | Value Type | Description | | ------------- | ------------ | ----------------------------------------------------------------------------------------------------------------------- | -| `exact` | string | String field value MUST equal the specified string exactly. | +| `exact` | string | Deprecated string equality. Use `equals.string_value`; `exact` will move to reserved in a future version. | | `regex` | string | String field value MUST match the regular expression. | | `exists` | boolean | If `true`, field MUST exist. If `false`, field MUST NOT exist. | | `starts_with` | string | String field value MUST begin with the specified literal string. | | `ends_with` | string | String field value MUST end with the specified literal string. | | `contains` | string | String field value MUST contain the specified literal substring. | -| `equals` | Value | Non-string field value MUST equal the typed value. See [Typed and Comparison Matching](#typed-and-comparison-matching). | +| `equals` | Value | Field value MUST equal the typed value. See [Typed and Comparison Matching](#typed-and-comparison-matching). | | `gt` | NumericValue | Numeric field value MUST be greater than the value. | | `gte` | NumericValue | Numeric field value MUST be greater than or equal to the value. | | `lt` | NumericValue | Numeric field value MUST be less than the value. | @@ -211,15 +211,18 @@ consistency. #### Typed and Comparison Matching The `exact`, `regex`, `starts_with`, `ends_with`, and `contains` match types -operate on the field's **string** value. For most non-string values (bool, int, -double) they never match; use the typed `equals` matcher or the numeric -comparison matchers `gt`, `gte`, `lt`, and `lte` instead. Bytes-typed fields are -the exception and have dedicated handling — see +operate on the field's **string** value. For non-string values they never match; +use the typed `equals` matcher or the numeric comparison matchers `gt`, `gte`, +`lt`, and `lte` instead. Bytes-typed fields have dedicated handling — see [Bytes and Identifier Fields](#bytes-and-identifier-fields). -These matchers carry a typed value rather than a string. `equals` uses `Value`, -which holds any non-string scalar; the comparison matchers use `NumericValue`, -which holds only a number: +`exact` is deprecated. It remains in the wire format for progressive migration, +but new policies SHOULD use `equals` with a string value. The `exact` field will +move to reserved in a future version after existing wire-format users migrate. + +Equality and comparison matchers carry typed values. `equals` uses `Value`, +which holds any scalar; the comparison matchers use `NumericValue`, which holds +only a number: ``` Value { @@ -229,6 +232,7 @@ Value { double_value: double bytes_value: bytes // raw bytes (base64 in JSON) hex_value: string // bytes as a hex string (readable; see below) + string_value: string } NumericValue { @@ -241,29 +245,24 @@ NumericValue { Two distinct types are used deliberately: because the comparison matchers take a `NumericValue`, comparing against a non-numeric value (a bool or bytes) is unrepresentable in the schema rather than something that must be rejected during -compilation. `Value` has no string variant for the same reason — string equality -is expressed with `exact`. `int_value` preserves full 64-bit precision for large -integer fields (for example, nanosecond timestamps) that a `double` cannot -represent exactly. - -> **Future direction:** `exact` is expected to be deprecated. Once `Value` gains -> a string variant, all equality — string and non-string — will be expressed -> through a single `equals` matcher, and `exact` will be retained only for -> backward compatibility. New policies SHOULD treat `exact` and `equals` as the -> same concept differing only by the value's type. +compilation. `int_value` preserves full 64-bit precision for large integer +fields (for example, nanosecond timestamps) that a `double` cannot represent +exactly. **Semantics:** - `equals` matches if and only if the field value has the same type and value as - the supplied `Value`. Integer and floating-point values are compared in a - single numeric domain, so an `int_value` matcher MAY match a `double` field - with an equal numeric value and vice versa. All other type pairings (for - example, an `int_value` matcher against a string field) MUST NOT match. + the supplied `Value`. `exact` is equivalent to `equals.string_value` for + compatibility. Integer and floating-point values are compared in a single + numeric domain, so an `int_value` matcher MAY match a `double` field with an + equal numeric value and vice versa. All other type pairings (for example, an + `int_value` matcher against a string field) MUST NOT match. - `gt`, `gte`, `lt`, and `lte` perform numeric comparison against `int` and `double` field values. A non-numeric field value MUST NOT match. - A type mismatch is a non-match, never a runtime error (fail-open). -- `case_insensitive` has no effect on `equals` or the comparison matchers; it - applies only to the string match types. +- `case_insensitive` applies to `exact`, `equals.string_value`, and the string + pattern match types. It has no effect on other `equals` variants or the + comparison matchers. - `negate` inverts the result, as with any matcher. **Authoring:** As with [AttributePath](#attributepath), implementations MUST @@ -278,6 +277,8 @@ YAML/JSON. For shorthand, the literal's type determines the `Value` variant: lt: 0.5 # double - log_attribute: ["deprecated"] equals: true # bool +- resource_attribute: ["service.name"] + equals: checkout-api # string # Canonical proto form - log_attribute: ["http.response.status_code"] @@ -289,10 +290,9 @@ Bytes are supplied either as the proto-native base64 `bytes_value` or, more readably, as a hex string in `hex_value`; see [Bytes and Identifier Fields](#bytes-and-identifier-fields). -**Validation:** A string value supplied to `equals` (use `exact` instead), and a -bool or bytes value supplied to a comparison matcher, are not representable in -`Value`/`NumericValue` and MUST be rejected when unmarshaling. Per -[Compilation Errors](#compilation-errors), a policy is also invalid if a +**Validation:** A bool, string, or bytes value supplied to a comparison matcher +is not representable in `NumericValue` and MUST be rejected when unmarshaling. +Per [Compilation Errors](#compilation-errors), a policy is also invalid if a `hex_value` is not valid hexadecimal (non-hex characters or an odd number of digits), or if the typed value is left unset (an empty `Value`/`NumericValue`). @@ -315,10 +315,10 @@ encoding cost is paid once per policy rather than once per telemetry record. **Authoring.** A bytes-typed field is matched as follows: ```yaml -# Well-known identifier field: a plain string literal is decoded as hex, -# because the field's declared type is bytes (no decoration needed). +# Well-known identifier field: hex_value is decoded once and compared as bytes. - trace_field: TRACE_FIELD_SPAN_ID - exact: "8a3f0e1234567890" + equals: + hex_value: "8a3f0e1234567890" # Arbitrary bytes attribute: the type is not known from the field, so the bytes # value is supplied explicitly via equals. @@ -336,9 +336,11 @@ are interchangeable. **Matching semantics on a bytes-typed field:** -- `exact` and `equals` decode the hex (or base64) literal once and compare - `bytes == bytes`. Hex input is case-insensitive; the comparison is over raw - bytes, so length and value are checked exactly. +- `equals.bytes_value` and `equals.hex_value` decode the literal once and + compare `bytes == bytes`. Hex input is case-insensitive; the comparison is + over raw bytes, so length and value are checked exactly. The deprecated + `exact` matcher MAY still be accepted for well-known bytes fields during + migration, but new policies SHOULD use `equals.hex_value`. - `exists` is unchanged. - `regex`, `starts_with`, `ends_with`, and `contains` treat the field as a string, matching against its canonical lowercase-hex rendering. This keeps @@ -357,8 +359,8 @@ comparison runs. #### Case Insensitivity -If `case_insensitive` is `true`, the match is performed without regard to case. -This applies to all match types including `exact`, `regex`, `starts_with`, +If `case_insensitive` is `true`, string matching is performed without regard to +case. This applies to `exact`, `equals.string_value`, `regex`, `starts_with`, `ends_with`, and `contains`. #### Negation @@ -629,13 +631,13 @@ which performs implicit equality): | Type | Value Type | Description | | ------------- | ------------ | ----------------------------------------------------------------------------------------------------------------------- | -| `exact` | string | String field value MUST equal the specified string exactly. | +| `exact` | string | Deprecated string equality. Use `equals.string_value`; `exact` will move to reserved in a future version. | | `regex` | string | String field value MUST match the regular expression. | | `exists` | boolean | If `true`, field MUST exist. If `false`, field MUST NOT exist. | | `starts_with` | string | String field value MUST begin with the specified literal string. | | `ends_with` | string | String field value MUST end with the specified literal string. | | `contains` | string | String field value MUST contain the specified literal substring. | -| `equals` | Value | Non-string field value MUST equal the typed value. See [Typed and Comparison Matching](#typed-and-comparison-matching). | +| `equals` | Value | Field value MUST equal the typed value. See [Typed and Comparison Matching](#typed-and-comparison-matching). | | `gt` | NumericValue | Numeric field value MUST be greater than the value. | | `gte` | NumericValue | Numeric field value MUST be greater than or equal to the value. | | `lt` | NumericValue | Numeric field value MUST be less than the value. | @@ -647,8 +649,8 @@ consistency. #### Case Insensitivity -If `case_insensitive` is `true`, the match is performed without regard to case. -This applies to all match types including `exact`, `regex`, `starts_with`, +If `case_insensitive` is `true`, string matching is performed without regard to +case. This applies to `exact`, `equals.string_value`, `regex`, `starts_with`, `ends_with`, and `contains`. #### Negation @@ -757,13 +759,13 @@ A matcher MUST specify exactly one match type (except when using `span_kind` or | Type | Value Type | Description | | ------------- | ------------ | ----------------------------------------------------------------------------------------------------------------------- | -| `exact` | string | String field value MUST equal the specified string exactly. | +| `exact` | string | Deprecated string equality. Use `equals.string_value`; `exact` will move to reserved in a future version. | | `regex` | string | String field value MUST match the regular expression. | | `exists` | boolean | If `true`, field MUST exist. If `false`, field MUST NOT exist. | | `starts_with` | string | String field value MUST begin with the specified literal string. | | `ends_with` | string | String field value MUST end with the specified literal string. | | `contains` | string | String field value MUST contain the specified literal substring. | -| `equals` | Value | Non-string field value MUST equal the typed value. See [Typed and Comparison Matching](#typed-and-comparison-matching). | +| `equals` | Value | Field value MUST equal the typed value. See [Typed and Comparison Matching](#typed-and-comparison-matching). | | `gt` | NumericValue | Numeric field value MUST be greater than the value. | | `gte` | NumericValue | Numeric field value MUST be greater than or equal to the value. | | `lt` | NumericValue | Numeric field value MUST be less than the value. | @@ -775,8 +777,8 @@ consistency. #### Case Insensitivity -If `case_insensitive` is `true`, the match is performed without regard to case. -This applies to all match types including `exact`, `regex`, `starts_with`, +If `case_insensitive` is `true`, string matching is performed without regard to +case. This applies to `exact`, `equals.string_value`, `regex`, `starts_with`, `ends_with`, and `contains`. #### Negation @@ -1081,9 +1083,9 @@ enabled: true log: match: - resource_attribute: ["service.name"] - exact: checkout-api + equals: checkout-api - log_field: LOG_FIELD_SEVERITY_TEXT - exact: DEBUG + equals: DEBUG keep: none ``` @@ -1095,7 +1097,7 @@ name: Redact PII from payment service log: match: - resource_attribute: ["service.name"] - exact: payment-api + equals: payment-api transform: remove: - log_attribute: ["user.password"] @@ -1196,9 +1198,10 @@ id: keep-spans-for-trace name: Keep all spans for one trace trace: match: - # trace_id is bytes; the hex literal is decoded once and compared as bytes + # trace_id is bytes; hex_value is decoded once and compared as bytes - trace_field: TRACE_FIELD_TRACE_ID - exact: "4bf92f3577b34da6a3ce929d0e0e4736" + equals: + hex_value: "4bf92f3577b34da6a3ce929d0e0e4736" keep: percentage: 100.0 ```