@@ -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,127 @@ 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+
199+ These are transmitted by eventually/finally checks and are a matter of scenario
200+ configuration, ultimately.
201+
202+ #### Series
203+
204+ The concern of this section is the equivalence of time series of a context,
205+ which we'll call 'series' for shorthand. Our goal is to demonstrate that both
206+ lanes, if given the same input stream, _ aggregate_ to an equivalent
207+ aggregation. Implied in this are two concepts, first, the operations by which
208+ aggregation happens per kind and, second, the definition of equivalence.
209+
210+ Points are stored raw, per lane, with a ` seq ` number to distinguish points that
211+ arrive at the same time interval. Conceptually they are stored as tuples:
212+
213+ ` (name, tagset, kind, timestamp, seq, interval, value) `
214+
215+ ` timestamp ` is the time recorded from the ingress frame of reference. Recording
216+ from intake's frame of reference subjects stored points to network jitter
217+ effects, which we wish to avoid. For convenience we do not store any known self-telemetry, so for
218+ instance Datadog Agent lane's ` datadog.* ` is not stored.
219+
220+ Queries over the point storage are done in terms of a bucketing width ` w ` , a
221+ fold operation per ` kind ` and a 'resubmit' rule to break ties on a timestamp:
222+ keep last, keep first by ` seq ` or summation. Queries are executed per-lane, that
223+ is, a query must be made over one lane's store and then the other. Queries are
224+ executed like so:
225+
226+ 0 . Collapse points sharing a timestamp by the resubmit rule.
227+ 1 . Assign each point to a bucket ` k = floor(timestamp / w) ` .
228+ 2 . Fold bucket points by the kind's fold operation, which are:
229+ * ` count ` -- ` sum `
230+ * ` rate ` -- ` sum(value * interval) / sum(interval) `
231+ * ` gauge ` -- last by timestamp
232+ * ` sketch ` -- dd-sketch merge, then projection to scalar series: count, sum, min, max, p75, p95 and p99
233+ * ` other ` -- none, drop
234+ 3 . Finally, buckets without values are filled like so:
235+ * ` count ` -- 0-valued
236+ * ` rate ` -- 0-valued
237+ * ` gauge ` -- carry forward previous value
238+ * ` sketch ` -- count series is 0-valued, quantile series are not emitted
239+
240+ Note, for sketches we require that both lanes maintain the same bin
241+ quantization. We consider this a difference if they do not, that is, a failure
242+ of equivalence.
243+
244+ The equivalence comparison is then done like so. First, truncate both series to
245+ the range both lanes could have contributed to so far:
246+
247+ ```
248+ k_start = max(first bucket on A, first bucket on B)
249+ k_end = floor(min(newest_A, newest_B) / w) - 1
250+ ```
251+
252+ Note that ` k_end ` drops a bucket to avoid reading out buckets that are still
253+ filling. Distance ` d ` is:
254+
255+ ` d(a,b) = |b-a| / max(|a|,|b|) `
256+
257+ where ` a ` is value of lane A for a bucket and ` b ` is the value of lane B for
258+ that same bucket, with ` d(x,x) = 0 ` by definition. The Fréchet measure is
259+ defined over pairs of buckets, one from each lane. Let ` k ` index lane A's
260+ buckets and ` k' ` lane B's, running from ` k_start ` to ` k_end ` . Then:
261+
262+ ```
263+ F(k_start, k') = d(A_k_start, B_k')
264+ F(k, k') = max( d(A_k, B_k'), min(F(k-1,k'), F(k-1,k'-1), F(k,k'-1)) )
265+ ```
266+
267+ A pair is admissible only when ` |k - k'| <= W ` where ` W ` is the 'leash width' in
268+ buckets. An inadmissible pair is not present in the calculation. Note that `F(k,
269+ k')` is not the distance between buckets ` k` and ` k'` it is the running
270+ best-so-far result, that is, of the walks that were possible to reach ` k ` and
271+ ` k' ` what is the smallest required 'leash'? We say that both lanes are
272+ equivalent if ` min(F(k_end, k')) < equivalence_threshold ` for all ` k' ` .
273+
274+ This means then that ` W ` and ` equivalence_threshold ` have outsized influence on
275+ the calculation. As of this writing we hold ` W=1 ` and
276+ ` equivalence_threshold=0.02 ` until such time as empirical results suggest
277+ different values are warranted.
278+
279+ The intake exposes ` /antithesis/metrics/frechet_distance ` . A POST to this
280+ endpoint runs the query described above for both lanes and makes necessary
281+ Antithesis SDK calls. The POST body sets distance calculation parameters, which
282+ are:
283+
284+ * ` bucket_width ` -- ` w ` from above, the bucketing width in seconds
285+ * ` leash_width ` -- ` W ` from above, the 'leash' width in buckets
286+ * ` equivalence_threshold ` -- the value ` min(F(k_end, k')) ` is compared with
163287
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.
288+ These are transmitted by eventually/finally checks -- similar to how context
289+ above works -- and are a matter of scenario configuration, ultimately.
0 commit comments