Skip to content

Commit 50aea35

Browse files
committed
DiffEq -- Only context
This draft PR introduces a new differential equivalence scenario focused only on context totals for now. There's enough change in here that I will pull alterations down-stack, more to come later.
1 parent 9899f1c commit 50aea35

42 files changed

Lines changed: 1706 additions & 195 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Cargo.lock

Lines changed: 17 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ members = [
3535
"lib/stringtheory",
3636
"test/antithesis/harness",
3737
"test/antithesis/intake",
38+
"test/antithesis/scenarios/differential",
3839
"test/antithesis/scenarios/general",
3940
]
4041
resolver = "2"

Makefile

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ export ADP_APP_VERSION_AUTO := $(shell cat bin/agent-data-plane/Cargo.toml | gre
1919
export ADP_APP_VERSION := $(or $(ADP_APP_VERSION),$(ADP_APP_VERSION_AUTO))
2020
export ADP_APP_BUILD_TIME := $(APP_BUILD_TIME)
2121

22+
# Datadog Agent version used by the project-wide comparison image.
23+
export DATADOG_AGENT_VERSION := $(shell cat .datadog-agent-version)
24+
2225
# SPDX license-list-data tag used by package-adp-host to harvest THIRD-PARTY-* license texts.
2326
# Pinned to match docker/Dockerfile.agent-data-plane so the host-built tarball ships the same
2427
# set of license texts as the linux Docker artifact; bump in lockstep with the Dockerfile.
@@ -201,6 +204,7 @@ build-datadog-agent-image: build-adp-image ## Builds the converged Datadog Agent
201204
@docker build \
202205
--tag saluki-images/datadog-agent:testing-devel \
203206
--tag local.dev/saluki-images/datadog-agent:testing-devel \
207+
--build-arg "DD_AGENT_VERSION=$(DATADOG_AGENT_VERSION)-jmx" \
204208
--build-arg ADP_IMAGE=saluki-images/agent-data-plane:testing-devel \
205209
--file ./docker/Dockerfile.datadog-agent \
206210
.
@@ -211,6 +215,7 @@ build-datadog-agent-image-release: build-adp-image-release ## Builds the converg
211215
@docker build \
212216
--tag saluki-images/datadog-agent:testing-release \
213217
--tag local.dev/saluki-images/datadog-agent:testing-release \
218+
--build-arg "DD_AGENT_VERSION=$(DATADOG_AGENT_VERSION)-jmx" \
214219
--build-arg ADP_IMAGE=saluki-images/agent-data-plane:testing-release \
215220
--file ./docker/Dockerfile.datadog-agent \
216221
.
@@ -753,6 +758,8 @@ endif
753758

754759
ANTITHESIS_CONFIG_DIR := test/antithesis/scenarios/general
755760
ANTITHESIS_COMPOSE_FILE := $(ANTITHESIS_CONFIG_DIR)/docker-compose.yaml
761+
ANTITHESIS_DIFFERENTIAL_CONFIG_DIR := test/antithesis/scenarios/differential
762+
ANTITHESIS_DIFFERENTIAL_COMPOSE_FILE := $(ANTITHESIS_DIFFERENTIAL_CONFIG_DIR)/docker-compose.yaml
756763

757764
.PHONY: check-antithesis-tools
758765
check-antithesis-tools:
@@ -765,12 +772,23 @@ antithesis-build: ## Builds the Antithesis harness container images
765772
@echo "[*] Building Antithesis harness images..."
766773
@docker compose -f $(ANTITHESIS_COMPOSE_FILE) build
767774

775+
.PHONY: antithesis-build-differential
776+
antithesis-build-differential: build-datadog-agent-image-release ## Builds the differential Antithesis harness images
777+
@echo "[*] Building differential Antithesis harness images..."
778+
@docker compose -f $(ANTITHESIS_DIFFERENTIAL_COMPOSE_FILE) build
779+
768780
.PHONY: antithesis-validate
769781
antithesis-validate: check-antithesis-tools antithesis-build
770782
antithesis-validate: ## Validates the Antithesis harness: builds images, runs 'snouty validate'
771783
@echo "[*] Validating Antithesis harness with snouty..."
772784
@snouty validate $(ANTITHESIS_CONFIG_DIR)
773785

786+
.PHONY: antithesis-validate-differential
787+
antithesis-validate-differential: check-antithesis-tools antithesis-build-differential
788+
antithesis-validate-differential: ## Validates the differential Antithesis harness
789+
@echo "[*] Validating differential Antithesis harness with snouty..."
790+
@snouty validate $(ANTITHESIS_DIFFERENTIAL_CONFIG_DIR)
791+
774792
##@ Profiling
775793

776794
.PHONY: profile-run-blackhole

test/antithesis/AGENTS.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,15 @@ testing. Antithesis will not run any test commands until it receives this event.
3939
(`antithesis-scenario-general`) owns general-only test commands under
4040
`src/bin/`. Snouty will push tagged images, consume this directory, and
4141
launch the run.
42+
- `scenarios/differential/` — the A/B scenario that runs ADP and the Datadog
43+
Agent side by side against one shared sampled config and asserts they emit the
44+
same metric context inventory for an identical DogStatsD stream. The Agent is
45+
normative. Its layout mirrors `general/`: `Dockerfile`, `docker-compose.yaml`,
46+
per-service build inputs, and a `README.md` describing the oracle and the
47+
private control API. Its Cargo package (`antithesis-scenario-differential`)
48+
owns the differential test commands under `src/bin/`. Build and validate it
49+
with `make antithesis-build-differential` and `make
50+
antithesis-validate-differential`.
4251

4352
**scratchbook**
4453

test/antithesis/harness/src/bin/first_sample_config/main.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
1-
//! Antithesis `first_` command: sample this timeline's `datadog.yaml` and release ADP.
1+
//! Antithesis `first_` command: sample this timeline's `datadog.yaml` and release blocked targets.
22
//!
33
//! Runs once per execution path after `setup_complete`, so the sample (see
44
//! [`harness::config`], Antithesis SDK randomness) is a post-snapshot, per-timeline
55
//! decision Antithesis branches. Writes the config to the shared `agent-config`
6-
//! volume then a `ready` sentinel the blocked ADP entrypoint waits on; running
7-
//! upstream of ADP's boot is what makes each timeline boot under its own config.
6+
//! volume then a `ready` sentinel the blocked target entrypoints wait on; running
7+
//! upstream of target boot is what makes each timeline boot under its own config.
88
//! Deployment fields come from the environment (see [`Cli`]).
99
1010
use std::fs;
1111
use std::path::PathBuf;
1212

1313
use antithesis_sdk::prelude::*;
1414
use antithesis_sdk::random::AntithesisRng;
15-
use anyhow::Context as _;
15+
use anyhow::Context;
1616
use clap::Parser;
1717
use harness::config::{ConfigProfile, DatadogConfig};
1818
use rand::rand_core::UnwrapErr;
@@ -23,7 +23,7 @@ use serde_json::json;
2323
#[command(name = "first_sample_config")]
2424
struct Cli {
2525
/// Directory to write `datadog.yaml` and the `ready` sentinel into (shared
26-
/// `agent-config` volume; the ADP container reads it).
26+
/// `agent-config` volume; blocked target containers read it).
2727
#[arg(long, env = "CONFIG_DIR", default_value = "/agent-config")]
2828
config_dir: PathBuf,
2929
/// Which `datadog.yaml` variation to sample. The differential scenario sets
@@ -72,7 +72,7 @@ fn main() -> anyhow::Result<()> {
7272
let details = serde_json::to_value(&config).unwrap_or_else(|e| json!({ "serialize_error": e.to_string() }));
7373
assert_reachable!("first_sample_config.config_sampled", &details);
7474

75-
// Release ADP: it blocks on this sentinel, then boots under the config above.
75+
// Release blocked targets: they wait on this sentinel, then boot under the config above.
7676
let ready_path = cli.config_dir.join("ready");
7777
fs::write(&ready_path, b"ready\n").with_context(|| format!("write sentinel {}", ready_path.display()))?;
7878
Ok(())

test/antithesis/harness/src/config.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,7 @@ const MAX_STRING_INTERNER_ENTRIES: u64 = 8_388_608;
239239
/// usually realistic but rarely tiny or wild to probe the truncation edge. A
240240
/// sampled `0` leaves ADP no room past the 4-byte length prefix, so it drops
241241
/// every packet before decode — useful when ADP runs alone, but it would make
242-
/// the differential targets diverge for a reason the oracle is not testing.
242+
/// the differential targets diverge for a reason the scenario is not testing.
243243
fn sample_buffer_size<R: Rng + ?Sized>(rng: &mut R, profile: ConfigProfile) -> u64 {
244244
if profile.is_general() && rng.random_ratio(1, 16) {
245245
Probe::new(0, 536_870_912).sample(rng)

test/antithesis/harness/src/lib.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
//! Shared helpers for Antithesis test commands.
22
3+
use std::time::Duration;
4+
35
pub mod config;
46
#[cfg(unix)]
57
pub mod driver;
68
pub mod payload;
79
pub mod rand;
10+
11+
/// How long a context may take to appear on both lanes before it counts as a
12+
/// divergence.
13+
pub const ACCEPTABLE_FLUSH_DELAY: Duration = Duration::from_secs(30);

test/antithesis/harness/src/payload/dogstatsd.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ fn choose_message<R: Rng + ?Sized>(rng: &mut R) -> Message {
102102
}
103103

104104
/// Write one `DogStatsD` message of a sampled type to `buf` at the given vibe.
105+
///
105106
/// Returns the packed value count when a multi-value metric was emitted, else
106107
/// `None`.
107108
pub fn send<R: Rng + ?Sized>(rng: &mut R, buf: &mut Vec<u8>, vibe: Vibe) -> Option<usize> {

test/antithesis/intake/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ mime = { workspace = true }
2424
protobuf = { workspace = true }
2525
serde = { workspace = true, features = ["derive"] }
2626
serde_json = { workspace = true }
27+
stele = { workspace = true }
2728
tokio = { workspace = true, features = [
2829
"macros",
2930
"net",
@@ -48,3 +49,6 @@ tracing-subscriber = { workspace = true, features = [
4849
"std",
4950
"tracing-log",
5051
] }
52+
53+
[dev-dependencies]
54+
proptest = { workspace = true }

test/antithesis/intake/README.md

Lines changed: 66 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,76 @@
11
# A property asserting intake for Datadog Agent-like artifacts
22

3-
This intake asserts structural and aggregation properties on payloads from
4-
Datadog Agent-like programs and is intended to mimic the constraints set by
5-
Datadog Intake API. This implementation is forked from the other intake in this
6-
project and may be later merged back up, although the goals to make aggregation
7-
assertions here is invasive.
3+
This intake asserts structural properties on payloads from Datadog Agent-like programs and is intended
4+
to mimic the constraints set by Datadog Intake API. This implementation is forked from the other intake
5+
in this project and may be later merged back up, although the goals to make aggregation assertions here
6+
are invasive.
87

9-
### How This Project Works
8+
## How this project works
109

11-
This document is the 'specification' for an abstract DogStatsD Agent. We assert
12-
that for any given input stream ADP emits to the intake data that is correctly
13-
shaped and, in a future update, that the aggregation model of ADP is accurate to
14-
the reference implementation of Datadog Agent DogStatsD.
10+
This document is the specification for an abstract DogStatsD Agent. We assert that for any given input
11+
stream ADP emits to the intake data that is correctly shaped and, in a future update, that the
12+
aggregation model of ADP is accurate to the reference implementation of Datadog Agent DogStatsD.
1513

16-
# Properties
14+
The differential scenario adds one narrower oracle. For the same generated configuration and workload,
15+
ADP and the Datadog Agent must eventually report the same metric contexts.
1716

18-
## Payloads
17+
## Properties
1918

20-
The Agent emits outputs to Datadog intake endpoints as payloads. The current
21-
specification covers `/api/v2/series` only. The Agent also emits to
22-
`/api/v3/series`, which a future revision will add.
19+
### Payloads
2320

24-
In this section we define properties that hold for `/api/v2/series` payloads
25-
irrespective of load generation profile. Precisely, a 'payload' is an HTTP
26-
envelope wrap around the compressed bytes of a
21+
The Agent emits outputs to Datadog intake endpoints as payloads. The current specification covers
22+
`/api/v2/series` only. The Agent also emits to `/api/v3/series`, which a future revision will add.
23+
24+
In this section we define properties that hold for `/api/v2/series` payloads irrespective of load
25+
generation profile. Precisely, a payload is an HTTP envelope around the compressed bytes of a
2726
[`MetricPayload`](https://github.com/DataDog/agent-payload/blob/0a5f9ebbbe9c2a1f1e671467511f6189d3a3b443/proto/metrics/agent_payload.proto#L30-L72).
2827

29-
Some properties reference rig-controlled parameters. `MaxTags(orgID)` and
30-
`MaxResources(orgID)` are per-org caps with defaults 100 and 500 respectively.
31-
32-
| Number | Category | Name | Description |
33-
|--------|---------------|------------------------|----------------------------------------------------------------|
34-
| Pyld01 | Envelope | Content-Type | `Content-Type` in `{application/x-protobuf, application/json}` |
35-
| Pyld02 | Envelope | Content-Encoding | `Content-Encoding` in `{deflate, gzip, zstd, identity}` |
36-
| Pyld03 | Envelope | API Key | `DD-Api-Key` header present and non-empty |
37-
| Pyld05 | Bytes | Compressed Size | body < 500 KiB compressed |
38-
| Pyld06 | Bytes | Uncompressed Size | body <= 5 MiB uncompressed |
39-
| Pyld07 | MetricPayload | Decode | body decodes as v2 `MetricPayload` via `rust-protobuf`. |
40-
| Pyld08 | MetricPayload | Point Count | total points <= configured `serializer_max_series_points_per_payload` |
41-
| Pyld09 | MetricSeries | Metric Non-Empty | `MetricSeries.metric` is non-empty |
42-
| Pyld10 | MetricSeries | Metric Length | `len(metric) <= 350` bytes |
43-
| Pyld11 | MetricSeries | Metric Alphabetic | `metric` contains at least one ASCII alphabetic char |
44-
| Pyld12 | MetricSeries | Type Enum | `type` in `{COUNT, RATE, GAUGE}` |
45-
| Pyld13 | MetricSeries | Tag Count | `len(tags) <= MaxTags(orgID)` |
46-
| Pyld14 | MetricSeries | Tag Prefix Reserved | no tag starts with `device:` or `dd.internal.resource:` |
47-
| Pyld15 | MetricSeries | Per-Series Point Count | `len(points) <=` configured `serializer_max_series_points_per_payload` |
48-
| Pyld16 | MetricSeries | Origin Populated | `origin.{product, category, service}` enum-valid |
49-
| Pyld17 | Resource | Host Resource Resolved | every series resolves a non-empty `(type="host")` resource and all series in a payload share one host |
50-
| Pyld18 | Resource | Resource Count | `len(resources) <= MaxResources(orgID)` |
51-
| Pyld19 | Resource | Host Name Length | host `name <= 255` bytes |
52-
| Pyld20 | MetricPoint | Value Not-NaN | `value` is not NaN |
53-
| Pyld21 | MetricPoint | Timestamp Future Bound | `timestamp <= intake_now + 600s` |
54-
| Pyld22 | Bytes | Content-Length | `Content-Length` absent or value equals body byte count |
28+
Some properties reference rig-controlled parameters. `MaxTags(orgID)` and `MaxResources(orgID)` are
29+
per-org caps with defaults 100 and 500 respectively.
30+
31+
| Number | Category | Name | Description |
32+
| --- | --- | --- | --- |
33+
| Pyld01 | Envelope | Content-Type | `Content-Type` in `{application/x-protobuf, application/json}` |
34+
| Pyld02 | Envelope | Content-Encoding | `Content-Encoding` in `{deflate, gzip, zstd, identity}` |
35+
| Pyld03 | Envelope | API Key | `DD-Api-Key` header present and non-empty |
36+
| Pyld05 | Bytes | Compressed Size | body < 500 KiB compressed |
37+
| Pyld06 | Bytes | Uncompressed Size | body <= 5 MiB uncompressed |
38+
| Pyld07 | MetricPayload | Decode | body decodes as v2 `MetricPayload` via `rust-protobuf` |
39+
| Pyld08 | MetricPayload | Point Count | total points <= configured `serializer_max_series_points_per_payload` |
40+
| Pyld09 | MetricSeries | Metric Non-Empty | `MetricSeries.metric` is non-empty |
41+
| Pyld10 | MetricSeries | Metric Length | `len(metric) <= 350` bytes |
42+
| Pyld11 | MetricSeries | Metric Alphabetic | `metric` contains at least one ASCII alphabetic char |
43+
| Pyld12 | MetricSeries | Type Enum | `type` in `{COUNT, RATE, GAUGE}` |
44+
| Pyld13 | MetricSeries | Tag Count | `len(tags) <= MaxTags(orgID)` |
45+
| Pyld14 | MetricSeries | Tag Prefix Reserved | no tag starts with `device:` or `dd.internal.resource:` |
46+
| Pyld15 | MetricSeries | Per-Series Point Count | `len(points) <=` configured `serializer_max_series_points_per_payload` |
47+
| Pyld16 | MetricSeries | Origin Populated | `origin.{product, category, service}` enum-valid |
48+
| Pyld17 | Resource | Host Resource Resolved | every series resolves a non-empty `(type="host")` resource and all series in a payload share one host |
49+
| Pyld18 | Resource | Resource Count | `len(resources) <= MaxResources(orgID)` |
50+
| Pyld19 | Resource | Host Name Length | host `name <= 255` bytes |
51+
| Pyld20 | MetricPoint | Value Not-NaN | `value` is not NaN |
52+
| Pyld21 | MetricPoint | Timestamp Future Bound | `timestamp <= intake_now + 600s` |
53+
| Pyld22 | Bytes | Content-Length | `Content-Length` absent or value equals body byte count |
54+
55+
### Differential context capture
56+
57+
The differential scenario uses the same intake binary for both lanes:
58+
59+
- Datadog Agent lane: `POST /api/v2/series`
60+
- ADP lane: `POST /api/v2/series`
61+
- Private control API: `GET /antithesis/metrics/agent`
62+
- Private control API: `GET /antithesis/metrics/adp`
63+
64+
For context equivalence, a metric context is a `stele::MetricContext`:
65+
66+
- metric name
67+
- canonical tag list
68+
69+
The intake folds each captured metric down to its canonical `stele::MetricContext` and stores the
70+
deduplicated set per lane, but it does not compare them. The control API returns those context
71+
sets. The differential workload command fetches both sets and owns the Antithesis assertion for
72+
eventual equivalence.
73+
74+
The context oracle intentionally does not assert aggregate values, sketch values, event payloads, or
75+
service-check payloads. Those remain covered by the normal workload generation and payload structural
76+
assertions rather than by the context-equivalence check.

0 commit comments

Comments
 (0)