@@ -10,12 +10,13 @@ are invasive.
1010
1111This document is the specification for an abstract DogStatsD Agent. We assert
1212that 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.
13+ shaped and that the aggregation model of ADP is accurate to the reference
14+ implementation of Datadog Agent DogStatsD.
1515
16- The differential scenario adds one narrower oracle . For the same generated
16+ The differential scenario adds two narrower oracles . For the same generated
1717configuration and workload, ADP and the Datadog Agent must eventually report the
18- same metric contexts.
18+ same metric contexts, and each shared context must carry the same aggregation
19+ curve on both lanes.
1920
2021## On Decoding and Correctness
2122
@@ -50,6 +51,27 @@ that will happen -- the goal is to _find faults_. Many payloads below, for
5051example Pyld26, are vacuous on a properly functional Datadog Agent and will only
5152fire in the prescence of a misbehaving Agent.
5253
54+ ## Endpoints
55+
56+ This intake supports many endpoints. The following table lists them, their
57+ supported methods and, briefly, their purpose.
58+
59+ | Method | Path | Purpose |
60+ | --------| ----------------------------------------| ----------------------------------------------------------------------------|
61+ | POST | ` /api/v2/series ` | v2 metric series |
62+ | POST | ` /api/intake/metrics/v3/series ` | v3 native series |
63+ | POST | ` /api/beta/sketches ` | Distribution sketches |
64+ | POST | ` /api/v1/events_batch ` | event batches, currently catch and discard |
65+ | POST | ` /api/v1/events ` | JSON events, currently catch and discard |
66+ | POST | ` /intake/ ` | events and metadata, currently catch and discard |
67+ | POST | ` /api/v1/check_run ` | service checks, currently catch and discard |
68+ | GET | ` /api/v1/validate ` | Datadog Agent connectivity probe |
69+ | POST | ` /antithesis/metrics/contexts ` | Contexts oracle: computes symmetric difference of lane contexts, see below |
70+ | POST | ` /antithesis/metrics/frechet_distance ` | Series oracle: computes Frechet distance of lane time series, see below |
71+ | GET | ` /contexts?n=N ` | Serves the load generator bounded ` N ` contexts |
72+
73+ All other paths respond with a 404 for every method.
74+
5375## Properties
5476
5577### Payloads
@@ -141,26 +163,139 @@ These properties hold exclusively for v3:
141163| Pyld58 | Origin | OriginInfo Triples | ` len(dictOriginInfo) ` is a multiple of 3 |
142164| Pyld59 | Metadata | Resources Even | payload ` Metadata.resources ` has even length |
143165
144- ### Differential context capture
166+ ### Differential Equivalence
145167
146- The differential scenario uses the same intake binary for both lanes:
168+ The differential scenario compares ADP and Datadog Agent on the same input
169+ stream, confirming that they are "roughly equivalent". What this means varies by
170+ the precise check, discussed below. The differential scenario uses the same
171+ intake for both lanes. Each check POSTs its parameters and the intake makes the
172+ Antithesis SDK calls, see below.
147173
148- - Datadog Agent lane: ` POST /api/intake/metrics/v3/series ` and ` POST /api/beta/sketches `
149- - ADP lane: ` POST /api/v2/series ` and ` POST /api/beta/sketches `
150- - Private control API: ` GET /antithesis/metrics/agent `
151- - Private control API: ` GET /antithesis/metrics/adp `
174+ #### Contexts
152175
153176For context equivalence, a metric context is:
154177
155178- metric name
156179- canonical tag list
157180- metric type
158181
159- The intake folds each captured metric down to its canonical context and stores the
160- deduplicated set per lane, but it does not compare them. The control API returns those context
161- sets. The differential workload command fetches both sets and owns the Antithesis assertion for
162- eventual equivalence.
182+ The intake exposes ` /antithesis/metrics/contexts ` . A POST to this endpoint
183+ computes the [ symmetric
184+ difference] ( https://en.wikipedia.org/wiki/Symmetric_difference ) of the observed
185+ contexts per-lane to that point. If a context C enters on lane A at time T-0 it
186+ will be emitted for all subsequent times, even if C never enters on lane A
187+ again. _ Contexts do not expire and we do not tally how often contexts have
188+ arrived._ Call the symmetric difference ` D ` . Let ` age ` be the difference between
189+ the current time -- from intake's frame of reference -- and the timestamp that
190+ context first ingressed with. We claim that:
191+
192+ * _ eventually_ for every member ` m ` in ` D ` ` age <= acceptable_flush_delay `
193+ * _ finally_ ` D == {} ` after waiting for a period of ` acceptable_flush_delay ` once load is quiescent
194+
195+ The POST body sets calculation parameters, which are:
196+
197+ * ` acceptable_flush_delay ` -- number of seconds before which both lanes are allowed to diverge
198+ * ` phase ` -- ` eventually ` or ` finally ` , which check posted
199+
200+ The ` phase ` picks the assertion name, either
201+ ` differential.contexts_eventually_equivalent ` or
202+ ` differential.contexts_finally_converged ` . It does not change the condition. The
203+ two phases claim different things and a report that merged them could not tell a
204+ lane that diverges under load from one that never converges.
205+
206+ These are transmitted by eventually/finally checks and are a matter of scenario
207+ configuration, ultimately.
208+
209+ #### Series
210+
211+ The concern of this section is the equivalence of time series of a context,
212+ which we'll call 'series' for shorthand. Our goal is to demonstrate that both
213+ lanes, if given the same input stream, _ aggregate_ to an equivalent
214+ aggregation. Implied in this are two concepts, first, the operations by which
215+ aggregation happens per kind and, second, the definition of equivalence.
216+
217+ Points are stored raw, per lane, with a ` seq ` number to distinguish points that
218+ arrive at the same time interval. Conceptually they are stored as tuples:
219+
220+ ` (name, tagset, kind, timestamp, seq, interval, value) `
221+
222+ ` timestamp ` is the time recorded from the ingress frame of reference. Recording
223+ from intake's frame of reference subjects stored points to network jitter
224+ effects, which we wish to avoid. For convenience we do not store any known self-telemetry, so for
225+ instance Datadog Agent lane's ` datadog.* ` is not stored.
226+
227+ Queries over the point storage are done in terms of a bucketing width ` w ` , a
228+ fold operation per ` kind ` and a 'resubmit' rule to break ties on a timestamp:
229+ keep last, keep first by ` seq ` or summation. Queries are executed per-lane, that
230+ is, a query must be made over one lane's store and then the other. Queries are
231+ executed like so:
232+
233+ 0 . Collapse points sharing a timestamp by the resubmit rule.
234+ 1 . Assign each point to a bucket ` k = floor(timestamp / w) ` .
235+ 2 . Fold bucket points by the kind's fold operation, which are:
236+ * ` count ` -- ` sum `
237+ * ` rate ` -- ` sum(value * interval) / sum(interval) `
238+ * ` gauge ` -- last by timestamp
239+ * ` sketch ` -- dd-sketch merge, then projection to scalar series: count, sum, min, max, p75, p95 and p99
240+ * ` other ` -- none, drop
241+ 3 . Finally, buckets without values are filled like so:
242+ * ` count ` -- 0-valued
243+ * ` rate ` -- 0-valued
244+ * ` gauge ` -- carry forward previous value
245+ * ` sketch ` -- count series is 0-valued, quantile series are not emitted
246+
247+ Note, for sketches we require that both lanes maintain the same bin
248+ quantization. We consider this a difference if they do not, that is, a failure
249+ of equivalence.
250+
251+ The equivalence comparison is then done like so. First, truncate both series to
252+ the range both lanes could have contributed to so far:
253+
254+ ```
255+ k_start = max(first bucket on A, first bucket on B)
256+ k_end = floor(min(newest_A, newest_B) / w) - 1
257+ ```
258+
259+ Note that ` k_end ` drops a bucket to avoid reading out buckets that are still
260+ filling. Distance ` d ` is:
261+
262+ ` d(a,b) = |b-a| / max(|a|,|b|) `
263+
264+ where ` a ` is value of lane A for a bucket and ` b ` is the value of lane B for
265+ that same bucket, with ` d(x,x) = 0 ` by definition. The Fréchet measure is
266+ defined over pairs of buckets, one from each lane. Let ` k ` index lane A's
267+ buckets and ` k' ` lane B's, running from ` k_start ` to ` k_end ` . Then:
268+
269+ ```
270+ F(k_start, k') = d(A_k_start, B_k')
271+ F(k, k') = max( d(A_k, B_k'), min(F(k-1,k'), F(k-1,k'-1), F(k,k'-1)) )
272+ ```
273+
274+ A pair is admissible only when ` |k - k'| <= W ` where ` W ` is the 'leash width' in
275+ buckets. An inadmissible pair is not present in the calculation. Note that `F(k,
276+ k')` is not the distance between buckets ` k` and ` k'` it is the running
277+ best-so-far result, that is, of the walks that were possible to reach ` k ` and
278+ ` k' ` what is the smallest required 'leash'? We say that both lanes are
279+ equivalent if ` min(F(k_end, k')) < equivalence_threshold ` for all ` k' ` .
280+
281+ This means then that ` W ` and ` equivalence_threshold ` have outsized influence on
282+ the calculation. As of this writing we hold ` W=1 ` and
283+ ` equivalence_threshold=0.02 ` until such time as empirical results suggest
284+ different values are warranted.
285+
286+ The intake exposes ` /antithesis/metrics/frechet_distance ` . A POST to this
287+ endpoint runs the query described above for both lanes and makes necessary
288+ Antithesis SDK calls. The POST body sets distance calculation parameters, which
289+ are:
290+
291+ * ` bucket_width ` -- ` w ` from above, the bucketing width in seconds
292+ * ` leash_width ` -- ` W ` from above, the 'leash' width in buckets
293+ * ` equivalence_threshold ` -- the value ` min(F(k_end, k')) ` is compared with
294+ * ` phase ` -- ` eventually ` or ` finally ` , which check posted
295+
296+ As with contexts, the ` phase ` picks the assertion name, either
297+ ` differential.series_eventually_equivalent ` or
298+ ` differential.series_finally_converged ` .
163299
164- The context oracle intentionally does not assert aggregate values, sketch values, event payloads, or
165- service-check payloads. Those remain covered by the normal workload generation and payload structural
166- assertions rather than by the context-equivalence check.
300+ These are transmitted by eventually/finally checks -- similar to how context
301+ above works -- and are a matter of scenario configuration, ultimately.
0 commit comments