Skip to content

Conformance test suite for validating Policy specification implementations.

License

Notifications You must be signed in to change notification settings

usetero/policy-conformance

Repository files navigation

policy-conformance

Conformance test suite for Policy spec implementations. Verifies that policy evaluation engines in Go, Rust, and Zig produce identical results when given the same policies and OpenTelemetry input data.

Repository structure

.
├── Taskfile.yml              # Test harness (build, test, bench, clean)
├── runners/
│   ├── go/                   # Go conformance runner (policy-go)
│   ├── rs/                   # Rust conformance runner (policy-rs)
│   └── zig/                  # Zig conformance runner (policy-zig)
├── server/                   # HTTP/gRPC conformance server (Go)
├── testcases/                # 184 test case directories
│   ├── logs_*/               # Log signal tests
│   ├── metrics_*/            # Metric signal tests
│   ├── traces_*/             # Trace signal tests
│   └── compound_*/           # Multi-signal / multi-batch tests
└── bin/                      # Hermit-managed toolchain

Test case layout

Each test case is a directory under testcases/ containing:

Simple test (single input/output):

testcases/logs_severity_drop/
├── policies.json             # Policy definitions
├── input.json                # OTLP JSON input
├── expected.json             # Expected OTLP JSON output
└── expected_stats.json       # Expected match statistics

The harness runs a simple test as follows:

  1. Detect signal type from the directory name prefix (logs_* → log, metrics_* → metric, traces_* → trace)
  2. Invoke the runner:
    ./runner-go --policies policies.json --input input.json \
                --output output_go.json --stats stats_go.json --signal log
    
  3. Normalize both expected.json and output_go.json with jq (strip null fields, coerce numeric strings to numbers, sort keys)
  4. Diff the normalized output against expected.json — any difference is a failure
  5. Diff stats_go.json against expected_stats.json — any difference is a failure
  6. Report PASS or FAIL (on failure, print the diff)

Compound test (multiple batches, stats checked once after all batches):

testcases/compound_mixed_signals/
├── policies.json             # Policy definitions (may span signals)
├── input_1.json              # Batch 1 (e.g., logs)
├── expected_1.json           # Expected output for batch 1
├── input_2.json              # Batch 2 (e.g., metrics)
├── expected_2.json           # Expected output for batch 2
├── input_3.json              # Batch 3 (e.g., traces)
├── expected_3.json           # Expected output for batch 3
└── expected_stats.json       # Merged stats across all batches

The harness runs a compound test as follows:

  1. Iterate over input_N.json files sorted numerically
  2. For each batch N:
    1. Detect signal type from the JSON content (resourceLogs → log, resourceMetrics → metric, resourceSpans → trace)
    2. Invoke the runner with input_N.json, writing output_N_go.json and stats_N_go.json
    3. Normalize and diff output_N_go.json against expected_N.json
  3. After all batches, merge per-batch stats files with jq (sum hits and misses per policy_id)
  4. Diff the merged stats against expected_stats.json
  5. Report PASS or FAIL (on failure, print diffs for each failing batch and/or stats)

Runners

All three runners implement the same CLI interface:

runner-{go,rs,zig} \
  --policies policies.json \
  --input input.json \
  --output output.json \
  --stats stats.json \
  --signal {log,metric,trace}

For HTTP/gRPC mode, --policies is replaced with --server URL or --grpc ADDR.

Runner Language Policy engine Protobuf codec
runner-go Go policy-go protojson
runner-rs Rust policy-rs serde + custom OTel types
runner-zig Zig policy-zig Native proto JSON codec

Prerequisites

  • Task (provided via bin/)
  • Go 1.26+
  • Rust (stable)
  • Zig 0.15+
  • Hyperscan/Vectorscan (task ci:setup installs it)
  • jq (provided via bin/)

Running tests

Build everything

task build       # Build all runners + conformance server

File-based provider (CLI mode)

Runs each runner as a CLI process that reads policies from a local JSON file:

task test        # Run all 3 runners
task test:go     # Go only
task test:rs     # Rust only
task test:zig    # Zig only

HTTP provider (server mode)

Runs the conformance server per test case, then uses the runner's --server flag to fetch policies over HTTP:

task test:http       # All 3 runners via HTTP
task test:http:go    # Go only
task test:http:rs    # Rust only
task test:http:zig   # Zig only

What happens for each http test case:

  1. Starts the conformance server on a random port:
    ./conformance-server --policies testcases/X/policies.json --http-port 0 --grpc-port 0
    
  2. Reads HTTP_PORT=N and GRPC_PORT=N from stdout (via FIFO, no sleeps)
  3. Invokes the runner:
    ./runner-go --server http://localhost:PORT/v1/policy/sync \
                --input testcases/X/input.json \
                --output testcases/X/output_go_http.json \
                --signal log
    
  4. Fetches accumulated stats from GET /stats
  5. Shuts down the server via GET /shutdown
  6. Diffs output and stats (stats normalization strips misses and zero-hit policies since the server only reports {policy_id, hits} for matched policies)

gRPC provider

Same as HTTP but uses --grpc flag:

task test:grpc       # Go + Rust via gRPC
task test:grpc:go
task test:grpc:rs

Note: The Zig runner does not support gRPC.

Other commands

task test:repeat TC=traces_sampling_50pct N=100 R=go   # Repeat one test N times
task bench                                              # Benchmark all runners with hyperfine
task clean                                              # Remove build artifacts and outputs

Known issues

  • traces_event_attribute and traces_link_trace_id are unimplemented across all runners

Test case catalog

All 184 test cases listed below pass for all three runners (Go, Rust, Zig) in both file-based and HTTP modes unless noted otherwise.

Logs — matching

Test case Description Go Zig Rust
logs_all_dropped All log records match a drop policy; output is empty
logs_attribute_match Match log records by log attribute exact value
logs_case_insensitive_ends_with Case-insensitive ends_with matcher on severity
logs_case_insensitive_exact Case-insensitive exact matcher on body
logs_case_insensitive_regex Case-insensitive regex matcher on body
logs_case_insensitive_starts_with Case-insensitive starts_with matcher on severity
logs_contains_ci Case-insensitive contains matcher on body
logs_contains_cs Case-sensitive contains matcher on body
logs_empty_input Empty input (no log records) produces empty output
logs_empty_vs_missing_field Distinguish between empty string and missing/null field
logs_enabled_false Disabled policy (enabled: false) is skipped entirely
logs_enabled_false_with_transforms Disabled policy with transforms; transforms must not fire
logs_ends_with ends_with matcher on severity text
logs_event_name_field Match on the event_name log field
logs_exact_drop Exact match on severity drops matching records
logs_exists exists: true matcher on log attribute
logs_exists_false exists: false matcher — match when attribute is absent
logs_keep_all_default Policy with keep: "all" passes all matched records through
logs_multiple_matchers Policy with multiple matchers (AND logic)
logs_multiple_policies_most_restrictive Most-restrictive keep wins when multiple policies match
logs_multiple_resources Multiple resources in input processed independently
logs_negated_match negate: true inverts a matcher
logs_nested_attribute Match on nested attribute path (e.g., ["http", "method"])
logs_nested_attribute_deep Match on deeply nested attribute path (3+ levels)
logs_no_match No policy matches; all records pass through unmodified
logs_overlapping_policies Multiple policies match the same record
logs_policy_ordering_determinism Policies evaluated in deterministic (alphanumeric by ID) order
logs_regex_drop Regex matcher drops matching records
logs_resource_attr Match on resource attribute
logs_resource_schema_url Match on resource schema URL
logs_scope_attr Match on scope attribute
logs_scope_schema_url Match on scope schema URL
logs_severity_drop Drop by severity text exact match
logs_span_id_field Match on the span_id log field
logs_starts_with starts_with matcher on severity text
logs_three_matchers Three matchers combined in a single policy (AND)
logs_trace_id_field Match on the trace_id log field

Logs — sampling and rate limiting

Test case Description Go Zig Rust
logs_rate_limit Rate limit to N records per second
logs_rate_limit_drop_overlap keep: "none" overrides rate limit on the same record
logs_rate_limit_per_minute Rate limit specified as N per minute
logs_sample_key_attribute Sampling keyed by log attribute value
logs_sample_key_resource_attr Sampling keyed by resource attribute value
logs_sample_key_scope_attr Sampling keyed by scope attribute value
logs_sampling_10pct 10% sampling rate
logs_sampling_25pct 25% sampling rate
logs_sampling_50pct 50% sampling rate
logs_sampling_75pct 75% sampling rate
logs_sampling_drop_overlap keep: "none" overrides sampling on the same record

Logs — transforms

Test case Description Go Zig Rust
logs_transform_add_attr_upsert_absent Add attribute with upsert: true when field absent (insert)
logs_transform_add_attribute Add a new log attribute
logs_transform_add_body Add body to log record when body is null
logs_transform_add_body_no_upsert_exists Add body without upsert when body exists (no-op)
logs_transform_add_body_upsert_exists Add body with upsert: true when body exists (overwrite)
logs_transform_add_no_upsert Add attribute with upsert: false when field exists (no-op)
logs_transform_add_no_upsert_new_field Add attribute with upsert: false when field absent (insert)
logs_transform_add_resource_attr Add a new resource attribute
logs_transform_add_scope_attr Add a new scope attribute
logs_transform_add_upsert Add attribute with upsert: true overwrites existing value
logs_transform_drop_skips_transform Transforms are not applied to records dropped by keep: "none"
logs_transform_execution_order Transforms execute in spec order: remove → redact → rename → add
logs_transform_multiple_policies Multiple policies each apply their own transforms
logs_transform_multiple_same_field Multiple transforms targeting the same field in one policy
logs_transform_redact_attribute Redact a log attribute value with [REDACTED]
logs_transform_redact_body Redact the log body field
logs_transform_redact_nonexistent Redact a non-existent field (no-op)
logs_transform_redact_resource_attr Redact a resource attribute
logs_transform_redact_scope_attr Redact a scope attribute
logs_transform_remove_attribute Remove a log attribute
logs_transform_remove_body Remove the log body field
logs_transform_remove_nonexistent Remove a non-existent field (no-op)
logs_transform_remove_resource_attr Remove a resource attribute
logs_transform_remove_scope_attr Remove a scope attribute
logs_transform_rename_attribute Rename a log attribute
logs_transform_rename_no_upsert Rename with upsert: false when target exists (no-op)
logs_transform_rename_nonexistent Rename a non-existent source attribute (no-op)
logs_transform_rename_resource_attr Rename a resource attribute
logs_transform_rename_scope_attr Rename a scope attribute
logs_transform_rename_source_absent Rename when source attribute absent, upsert: false (no-op)
logs_transform_rename_target_absent Rename when target absent, upsert: false (normal rename)
logs_transform_rename_upsert Rename with upsert: true overwrites existing target
logs_transform_rename_upsert_source_absent Rename with upsert: true when source absent (no-op)
logs_transform_rename_upsert_target_absent Rename with upsert: true when target absent (rename)
logs_transform_with_rate_limit Transforms applied to records that survive rate limiting
logs_transform_with_sampling Transforms applied to records that survive sampling

Metrics

Test case Description Go Zig Rust
metrics_aggregation_temporality Match by aggregation temporality (delta/cumulative)
metrics_case_insensitive Case-insensitive metric name matching
metrics_cumulative_temporality Match cumulative temporality specifically
metrics_description Match by metric description field
metrics_drop_by_attr Drop metrics by datapoint attribute
metrics_drop_by_name Drop metrics by name exact match
metrics_empty_input Empty input (no metrics) produces empty output
metrics_ends_with ends_with matcher on metric name
metrics_exists exists: true matcher on datapoint attribute
metrics_exists_false exists: false matcher on datapoint attribute
metrics_exponential_histogram_type Match exponential histogram metric type
metrics_histogram_type Match histogram metric type
metrics_keep Basic keep policy for metrics
metrics_multiple_matchers Multiple matchers combined (AND logic) for metrics
metrics_multiple_policies Multiple metric policies evaluated together
metrics_multiple_resources Multiple resources in metric input
metrics_negate Negated matcher for metrics
metrics_negate_temporality Negated temporality matcher
metrics_negate_type Negated metric type matcher
metrics_overlapping_miss Overlapping policies where one misses
metrics_resource_attr Match on resource attribute for metrics
metrics_resource_schema_url Match on resource schema URL for metrics
metrics_scope_attr Match on scope attribute for metrics
metrics_scope_name Match on scope name for metrics
metrics_scope_schema_url Match on scope schema URL for metrics
metrics_scope_version Match on scope version for metrics
metrics_starts_with starts_with matcher on metric name
metrics_sum_type Match sum metric type
metrics_summary_type Match summary metric type
metrics_three_policies Three metric policies evaluated together
metrics_type_filter Filter metrics by type (gauge/sum/histogram)
metrics_unit Match by metric unit field

Traces — matching

Test case Description Go Zig Rust
traces_case_insensitive Case-insensitive span name matching
traces_drop_0pct Drop at 0% (all sampled out)
traces_empty_input Empty input (no spans) produces empty output
traces_error_vs_health Distinguish error spans from health check spans
traces_event_name Match on span event name
traces_exists exists: true matcher on span attribute
traces_exists_false exists: false matcher on span attribute
traces_multiple_matchers Multiple matchers combined (AND) for traces
traces_multiple_resources Multiple resources in trace input
traces_name_contains contains matcher on span name
traces_name_ends_with ends_with matcher on span name
traces_name_regex Regex matcher on span name
traces_name_starts_with starts_with matcher on span name
traces_negate Negated matcher for traces
traces_negate_span_kind Negated span kind matcher
traces_negate_span_status Negated span status matcher
traces_overlapping Overlapping trace policies on the same spans
traces_parent_span_id Match on parent span ID field
traces_resource_attr Match on resource attribute for traces
traces_scope_attr Match on scope attribute for traces
traces_scope_name Match on scope name for traces
traces_scope_schema_url Match on scope schema URL for traces
traces_scope_version Match on scope version for traces
traces_span_attribute Match on span attribute exact value
traces_span_attribute_contains contains matcher on span attribute
traces_span_kind Match on span kind (server)
traces_span_kind_client Match on span kind (client)
traces_span_kind_consumer Match on span kind (consumer)
traces_span_kind_producer Match on span kind (producer)
traces_span_status_error Match on span status = error
traces_span_status_ok Match on span status = ok
traces_span_status_unset Match on span status = unset
traces_trace_state Match on trace state field

Traces — sampling

Test case Description Go Zig Rust
traces_keep_100pct Keep at 100% (all sampled in, writes th:0 tracestate)
traces_sampling_10pct 10% trace sampling with deterministic hash
traces_sampling_25pct 25% trace sampling
traces_sampling_50pct 50% trace sampling
traces_sampling_75pct 75% trace sampling
traces_sampling_equalizing Equalizing sampling algorithm
traces_sampling_fail_closed fail_closed: true drops spans without valid trace ID
traces_sampling_precision High-precision sampling threshold encoding
traces_sampling_proportional Proportional sampling algorithm

Traces — tracestate

Test case Description Go Zig Rust
traces_tracestate_equalizing_incoming_th Equalizing sampler with pre-existing th in tracestate
traces_tracestate_fail_closed_true Fail-closed behavior with tracestate present
traces_tracestate_mixed Mixed tracestate scenarios (some with, some without)
traces_tracestate_overwrite_ot Overwrite existing ot vendor key in tracestate
traces_tracestate_preserve_vendors Preserve non-ot vendor keys in tracestate
traces_tracestate_proportional_incoming_th Proportional sampler with pre-existing th in tracestate
traces_tracestate_rv_consistency_check Randomness value consistency check in tracestate
traces_tracestate_rv_randomness Random value (rv) written to tracestate
traces_tracestate_write_basic Basic tracestate write with sampling threshold

Compound tests

Test case Description Go Zig Rust
compound_all_keep_types All keep types (all/none/sample/rate_limit) in one policy set
compound_conflicting_keeps Conflicting keep decisions across policies; most restrictive wins
compound_datapoint_attr_types Datapoint attribute matching across histogram, summary, gauge
compound_disabled_mixed Mix of enabled and disabled policies; disabled transforms must not fire
compound_double_negation exists: false + negate: true semantics
compound_empty_vs_missing Empty string vs null/absent field behavior across signals
compound_many_policies_fanout 50+ policies each targeting a different service; verifies no cross-contamination
compound_mixed_signals Policies spanning logs, metrics, and traces in one set
compound_negation_overlap Policies with negated matchers creating complex intersections
compound_nested_attributes Nested attribute paths across all signal types
compound_rate_limit_most_restrictive keep: "none" overrides rate limit; confirms none > rate_limit
compound_regex_edge_cases Character classes, anchoring, alternation, UUID patterns, escaped brackets
compound_sampling_interactions Trace sampling with fail_closed, 0% drop overriding sampling
compound_scope_isolation Resource/scope attribute transforms are isolated per scope
compound_transform_chain Cross-policy transform visibility (transforms applied after all matching)
compound_transform_ordering_alphanumeric Policies evaluated in alphanumeric ID order, not array order
compound_transforms_across_policies Multiple policies with add/redact/rename transforms on same records

Summary

Signal Tests Go Zig Rust
Logs 84 84 84 84
Metrics 32 32 32 32
Traces 51 51 51 51
Compound 17 16 17 16
Total 184 183 184 183

About

Conformance test suite for validating Policy specification implementations.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors