1use std::collections::{btree_map::Entry, BTreeMap, BTreeSet};
6use std::sync::{Arc, Mutex};
7use std::time::{SystemTime, UNIX_EPOCH};
8
9use datadog_protos::metrics::metric_payload::{MetricSeries, MetricType, Resource};
10use datadog_protos::metrics::{MetricPayload, SketchPayload};
11use serde::{Deserialize, Serialize};
12
13use crate::lenient_decode::V3Series;
14
15const SELF_TELEMETRY_PREFIX: &str = "datadog.";
16
17#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
18#[serde(rename_all = "snake_case")]
19pub(crate) enum Target {
20 Agent,
21 Adp,
22}
23
24impl Target {
25 #[must_use]
26 pub(crate) fn parse(value: &str) -> Option<Self> {
27 match value {
28 "agent" => Some(Self::Agent),
29 "adp" => Some(Self::Adp),
30 _ => None,
31 }
32 }
33
34 pub(crate) fn as_str(self) -> &'static str {
35 match self {
36 Self::Agent => "agent",
37 Self::Adp => "adp",
38 }
39 }
40}
41
42#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
44#[serde(rename_all = "snake_case")]
45pub(crate) enum MetricKind {
46 Count,
47 Rate,
48 Gauge,
49 Sketch,
50 Other,
54}
55
56impl MetricKind {
57 fn of(type_: MetricType) -> Self {
61 match type_ {
62 MetricType::COUNT => Self::Count,
63 MetricType::RATE => Self::Rate,
64 MetricType::GAUGE => Self::Gauge,
65 MetricType::UNSPECIFIED => Self::Other,
66 }
67 }
68}
69
70#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
71pub(crate) struct EpochSeconds(i64);
72
73impl EpochSeconds {
74 pub(crate) const fn from_epoch_secs(secs: i64) -> Self {
75 Self(secs)
76 }
77
78 pub(crate) fn secs(self) -> i64 {
80 self.0
81 }
82
83 pub(crate) fn now() -> Option<Self> {
86 let secs = SystemTime::now().duration_since(UNIX_EPOCH).ok()?.as_secs();
87 i64::try_from(secs).ok().map(Self)
88 }
89}
90
91#[derive(Clone, Debug, PartialEq)]
95pub(crate) enum BucketValue {
96 Scalar(f64),
98 Sketch(SketchValue),
100}
101
102#[derive(Clone, Debug, PartialEq)]
104pub(crate) struct SketchValue {
105 pub(crate) count: i64,
106 pub(crate) sum: f64,
107 pub(crate) min: f64,
108 pub(crate) max: f64,
109 pub(crate) bins: Vec<(i32, u32)>,
110}
111
112#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
114pub(crate) struct Context {
115 pub(crate) name: String,
116 pub(crate) tagset: BTreeSet<String>,
117 pub(crate) kind: MetricKind,
118}
119
120#[derive(Clone, Debug, Deserialize, Serialize)]
122pub(crate) struct ContextAt {
123 #[serde(flatten)]
124 pub(crate) context: Context,
125 pub(crate) first_seen: EpochSeconds,
126}
127
128#[derive(Clone, Debug, Deserialize, Serialize)]
130pub(crate) struct LaneView {
131 pub(crate) now: EpochSeconds,
132 pub(crate) contexts: Vec<ContextAt>,
133}
134
135#[derive(Debug, Default)]
136struct Lanes {
137 seen: BTreeMap<(Target, Context), EpochSeconds>,
138}
139
140impl Lanes {
141 fn record(&mut self, target: Target, contexts: &[Context], now: EpochSeconds) -> usize {
142 let mut added = 0;
143 for context in contexts {
144 if context.name.starts_with(SELF_TELEMETRY_PREFIX) {
145 continue;
146 }
147 if let Entry::Vacant(slot) = self.seen.entry((target, context.clone())) {
148 slot.insert(now);
149 added += 1;
150 }
151 }
152 added
153 }
154
155 fn contexts(&self, target: Target) -> Vec<ContextAt> {
156 self.seen
157 .iter()
158 .filter(|((lane, _), _)| *lane == target)
159 .map(|((_, context), &first_seen)| ContextAt {
160 context: context.clone(),
161 first_seen,
162 })
163 .collect()
164 }
165}
166
167#[derive(Clone, Debug, Default)]
170pub struct State {
171 lanes: Arc<Mutex<Lanes>>,
172}
173
174impl State {
175 #[must_use]
177 pub fn new() -> Self {
178 Self::default()
179 }
180
181 pub(crate) fn record_series_v2(&self, target: Target, payload: MetricPayload, now: EpochSeconds) -> usize {
182 let contexts = observe_series(payload, now.secs());
183 self.with_lanes(|lanes| lanes.record(target, &contexts, now))
184 }
185
186 pub(crate) fn record_sketches(&self, target: Target, payload: SketchPayload, now: EpochSeconds) -> usize {
187 let contexts = observe_sketches(payload);
188 self.with_lanes(|lanes| lanes.record(target, &contexts, now))
189 }
190
191 pub(crate) fn record_series_v3(&self, target: Target, series: Vec<V3Series>, now: EpochSeconds) -> usize {
192 let contexts = observe_series_v3(series, now.secs());
193 self.with_lanes(|lanes| lanes.record(target, &contexts, now))
194 }
195
196 pub(crate) fn contexts(&self, target: Target) -> Vec<ContextAt> {
197 self.with_lanes(|lanes| lanes.contexts(target))
198 }
199
200 fn with_lanes<T>(&self, f: impl FnOnce(&mut Lanes) -> T) -> T {
201 f(&mut self.lanes.lock().expect("capture lock poisoned"))
202 }
203}
204
205pub(crate) const MAX_METRIC_NAME_LEN: usize = 350;
207pub(crate) const MAX_TAG_COUNT: usize = 100;
212pub(crate) const MAX_RESOURCE_COUNT: usize = 500;
216pub(crate) const MAX_HOST_NAME_LEN: usize = 255;
218const MAX_SECONDS_IN_FUTURE: i64 = 600;
221
222fn scalar_point_kept(value: &BucketValue, bucket_start: u64, now_secs: i64) -> bool {
228 match value {
229 BucketValue::Scalar(v) => {
230 !v.is_nan() && i128::from(bucket_start) <= i128::from(now_secs) + i128::from(MAX_SECONDS_IN_FUTURE)
231 }
232 BucketValue::Sketch(_) => true,
233 }
234}
235
236pub(crate) fn metric_name_kept(name: &str) -> bool {
239 !name.is_empty() && name.len() <= MAX_METRIC_NAME_LEN && name.bytes().any(|b| b.is_ascii_alphabetic())
240}
241
242pub(crate) fn series_kept_by_intake(series: &MetricSeries) -> bool {
248 let host_ok = series
249 .resources
250 .iter()
251 .find(|r| r.type_() == "host")
252 .is_none_or(|host| host.name().len() <= MAX_HOST_NAME_LEN);
253 metric_name_kept(series.metric.as_str())
254 && series.tags.len() <= MAX_TAG_COUNT
255 && series.resources.len() <= MAX_RESOURCE_COUNT
256 && host_ok
257}
258
259fn tagset_with_host(tags: &[String], host: Option<&str>) -> BTreeSet<String> {
262 let mut tagset: BTreeSet<String> = tags.iter().cloned().collect();
263 if let Some(host) = host {
264 if !host.is_empty() {
265 tagset.insert(format!("host:{host}"));
266 }
267 }
268 tagset
269}
270
271fn observe_series(payload: MetricPayload, now_secs: i64) -> Vec<Context> {
277 let mut contexts = Vec::new();
278 for series in payload.series {
279 if !series_kept_by_intake(&series) {
280 continue;
281 }
282 let host = series
283 .resources
284 .iter()
285 .find(|r| r.type_() == "host")
286 .map(Resource::name);
287 let has_point = series.points.iter().any(|point| {
288 u64::try_from(point.timestamp)
289 .is_ok_and(|ts| scalar_point_kept(&BucketValue::Scalar(point.value), ts, now_secs))
290 });
291 if !has_point {
292 continue;
293 }
294 contexts.push(Context {
295 name: series.metric.clone(),
296 tagset: tagset_with_host(&series.tags, host),
297 kind: MetricKind::of(series.type_()),
298 });
299 }
300 contexts
301}
302
303fn observe_sketches(payload: SketchPayload) -> Vec<Context> {
314 let mut contexts = Vec::new();
315 for sketch in payload.sketches {
316 if !metric_name_kept(sketch.metric())
318 || sketch.tags.len() > MAX_TAG_COUNT
319 || sketch.host().len() > MAX_HOST_NAME_LEN
320 {
321 continue;
322 }
323 let has_point = sketch.dogsketches.iter().any(|d| u64::try_from(d.ts).is_ok())
324 || sketch.distributions.iter().any(|d| u64::try_from(d.ts).is_ok());
325 if !has_point {
326 continue;
327 }
328 contexts.push(Context {
329 name: sketch.metric.clone(),
330 tagset: tagset_with_host(&sketch.tags, Some(sketch.host())),
331 kind: MetricKind::Sketch,
332 });
333 }
334 contexts
335}
336
337fn observe_series_v3(series: Vec<V3Series>, now_secs: i64) -> Vec<Context> {
342 series
343 .into_iter()
344 .filter(|s| {
345 s.points
346 .iter()
347 .any(|(ts, value)| scalar_point_kept(value, *ts, now_secs))
348 })
349 .map(|s| Context {
350 name: s.name,
351 tagset: s.tags.into_iter().collect(),
352 kind: s.kind,
353 })
354 .collect()
355}
356
357#[cfg(test)]
358mod tests;