@@ -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
@@ -143,27 +165,149 @@ These properties hold exclusively for v3:
143165| Pyld58 | Origin | OriginInfo Triples | ` len(dictOriginInfo) ` is a multiple of 3 |
144166| Pyld59 | Metadata | Resources Even | payload ` Metadata.resources ` has even length |
145167
146- ### Differential context capture
168+ ### Differential Equivalence
147169
148- The differential scenario uses the same intake binary for both lanes. Both take the series API the
149- timeline sampled, so a lane splitting off the other's encoding is a finding:
170+ The differential scenario compares ADP and Datadog Agent on the same input
171+ stream, confirming that they are "roughly equivalent". What this means varies by
172+ the precise check, discussed below. The differential scenario uses the same
173+ intake for both lanes. Each check POSTs its parameters and the intake makes the
174+ Antithesis SDK calls, see below.
150175
151- - Datadog Agent lane: ` POST /api/v2/ series` or ` POST /api/intake/metrics/v3/series ` , plus ` POST /api/beta/sketches `
152- - ADP lane: the same two series routes, plus ` POST /api/beta/sketches `
153- - Private control API: ` GET /antithesis/metrics/agent `
154- - Private control API: ` GET /antithesis/metrics/adp `
176+ Both lanes take the series API the timeline sampled, so a lane that ships the
177+ other's encoding is a finding rather than a configured difference.
178+
179+ #### Contexts
155180
156181For context equivalence, a metric context is:
157182
158183- metric name
159184- canonical tag list
160185- metric type
161186
162- The intake folds each captured metric down to its canonical context and stores the
163- deduplicated set per lane, but it does not compare them. The control API returns those context
164- sets. The differential workload command fetches both sets and owns the Antithesis assertion for
165- eventual equivalence.
187+ The intake exposes ` /antithesis/metrics/contexts ` . A POST to this endpoint
188+ computes the [ symmetric
189+ difference] ( https://en.wikipedia.org/wiki/Symmetric_difference ) of the observed
190+ contexts per-lane to that point. If a context C enters on lane A at time T-0 it
191+ will be emitted for all subsequent times, even if C never enters on lane A
192+ again. _ Contexts do not expire and we do not tally how often contexts have
193+ arrived._ Call the symmetric difference ` D ` . Let ` age ` be the difference between
194+ the current time -- from intake's frame of reference -- and the timestamp that
195+ context first ingressed with. We claim that:
196+
197+ * _ eventually_ for every member ` m ` in ` D ` ` age <= acceptable_flush_delay `
198+ * _ finally_ ` D == {} ` after waiting for a period of ` acceptable_flush_delay ` once load is quiescent
199+
200+ The POST body sets calculation parameters, which are:
201+
202+ * ` acceptable_flush_delay ` -- number of seconds before which both lanes are allowed to diverge
203+ * ` phase ` -- ` eventually ` or ` finally ` , which check posted
204+
205+ The ` phase ` picks the assertion name, either
206+ ` differential.contexts_eventually_equivalent ` or
207+ ` differential.contexts_finally_converged ` , and the predicate. The ` eventually_ `
208+ check asserts no member of ` D ` is delayed, so a member inside
209+ ` acceptable_flush_delay ` is in flight rather than a divergence. The ` finally_ `
210+ check asserts ` D == {} ` and ignores the budget, since load has stopped and a
211+ residual has nothing left to wait for. A report that merged them could not tell a
212+ lane that diverges under load from one that never converges.
213+
214+ These are transmitted by eventually/finally checks and are a matter of scenario
215+ configuration, ultimately.
216+
217+ #### Series
218+
219+ The concern of this section is the equivalence of time series of a context,
220+ which we'll call 'series' for shorthand. Our goal is to demonstrate that both
221+ lanes, if given the same input stream, _ aggregate_ to an equivalent
222+ aggregation. Implied in this are two concepts, first, the operations by which
223+ aggregation happens per kind and, second, the definition of equivalence.
224+
225+ Points are stored raw, per lane, with a ` seq ` number to distinguish points that
226+ arrive at the same time interval. Conceptually they are stored as tuples:
227+
228+ ` (name, tagset, kind, timestamp, seq, interval, value) `
229+
230+ ` timestamp ` is the time recorded from the ingress frame of reference. Recording
231+ from intake's frame of reference subjects stored points to network jitter
232+ effects, which we wish to avoid. For convenience we do not store any known self-telemetry, so for
233+ instance Datadog Agent lane's ` datadog.* ` is not stored.
234+
235+ Queries over the point storage are done in terms of a bucketing width ` w ` , a
236+ fold operation per ` kind ` and a 'resubmit' rule to break ties on a timestamp:
237+ keep last, keep first by ` seq ` or summation. Queries are executed per-lane, that
238+ is, a query must be made over one lane's store and then the other. Queries are
239+ executed like so:
240+
241+ 0 . Collapse points sharing a timestamp by the resubmit rule.
242+ 1 . Assign each point to a bucket ` k = floor(timestamp / w) ` .
243+ 2 . Fold bucket points by the kind's fold operation, which are:
244+ * ` count ` -- ` sum `
245+ * ` rate ` -- ` sum(value * interval) / sum(interval) `
246+ * ` gauge ` -- last by timestamp
247+ * ` sketch ` -- dd-sketch merge, then projection to scalar series: count, sum, min, max, p75, p95 and p99
248+ * ` other ` -- none, drop
249+ 3 . Finally, buckets without values are filled like so:
250+ * ` count ` -- 0-valued
251+ * ` rate ` -- 0-valued
252+ * ` gauge ` -- carry forward previous value
253+ * ` sketch ` -- count series is 0-valued, quantile series are not emitted
254+
255+ Note, for sketches we require that both lanes maintain the same bin
256+ quantization. We consider this a difference if they do not, that is, a failure
257+ of equivalence.
258+
259+ The equivalence comparison is then done like so. First, truncate both series to
260+ the range both lanes could have contributed to so far:
261+
262+ ```
263+ k_start = max(first bucket on A, first bucket on B) + W
264+ k_end = floor(min(newest_A, newest_B) / w) - F - W
265+ ```
266+
267+ ` F ` is 1 in the ` eventually_ ` check and 0 in the ` finally_ ` check. The
268+ ` eventually_ ` check drops a bucket to avoid reading out a bucket that is still
269+ filling. After load stops nothing is filling. Both ends drop a further ` W ` buckets because the two lanes can put the
270+ same input in different buckets. At the cut one lane counts a point the other
271+ placed outside the range. Distance ` d ` is:
272+
273+ ` d(a,b) = |b-a| / max(|a|,|b|) `
274+
275+ where ` a ` is value of lane A for a bucket and ` b ` is the value of lane B for
276+ that same bucket, with ` d(x,x) = 0 ` by definition. The Fréchet measure is
277+ defined over pairs of buckets, one from each lane. Let ` k ` index lane A's
278+ buckets and ` k' ` lane B's, running from ` k_start ` to ` k_end ` . Then:
279+
280+ ```
281+ F(k_start, k_start) = d(A_k_start, B_k_start)
282+ F(k, k') = max( d(A_k, B_k'), min(F(k-1,k'), F(k-1,k'-1), F(k,k'-1)) )
283+ ```
284+
285+ A pair is admissible only when ` |k - k'| <= W ` where ` W ` is the 'leash width' in
286+ buckets. An inadmissible pair is not present in the calculation. Note that `F(k,
287+ k')` is not the distance between buckets ` k` and ` k'` it is the running
288+ best-so-far result, that is, of the walks that were possible to reach ` k ` and
289+ ` k' ` what is the smallest required 'leash'? We say that both lanes are
290+ equivalent if ` F(k_end, k_end) < equivalence_threshold ` . Both ends are fixed, so
291+ every bucket of both lanes is matched to something.
292+
293+ This means then that ` W ` and ` equivalence_threshold ` have outsized influence on
294+ the calculation. As of this writing we hold ` W=1 ` and
295+ ` equivalence_threshold=0.02 ` until such time as empirical results suggest
296+ different values are warranted.
297+
298+ The intake exposes ` /antithesis/metrics/frechet_distance ` . A POST to this
299+ endpoint runs the query described above for both lanes and makes necessary
300+ Antithesis SDK calls. The POST body sets distance calculation parameters, which
301+ are:
302+
303+ * ` bucket_width ` -- ` w ` from above, the bucketing width in seconds
304+ * ` leash_width ` -- ` W ` from above, the 'leash' width in buckets
305+ * ` equivalence_threshold ` -- the value ` F(k_end, k_end) ` is compared with
306+ * ` phase ` -- ` eventually ` or ` finally ` , which check posted
307+
308+ As with contexts, the ` phase ` picks the assertion name, either
309+ ` differential.series_eventually_equivalent ` or
310+ ` differential.series_finally_converged ` .
166311
167- The context oracle intentionally does not assert aggregate values, sketch values, event payloads, or
168- service-check payloads. Those remain covered by the normal workload generation and payload structural
169- assertions rather than by the context-equivalence check.
312+ These are transmitted by eventually/finally checks -- similar to how context
313+ above works -- and are a matter of scenario configuration, ultimately.
0 commit comments