1use std::path::{Path, PathBuf};
14use std::time::Duration;
15
16use anyhow::Context as _;
17use clap::ValueEnum;
18use rand::distr::{Distribution, StandardUniform};
19use rand::{Rng, RngExt};
20use serde::{Serialize, Serializer};
21
22use crate::rand::Probe;
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
26pub enum ConfigProfile {
27 General,
31 Differential,
36}
37
38impl ConfigProfile {
39 fn is_general(self) -> bool {
41 matches!(self, ConfigProfile::General)
42 }
43}
44
45#[derive(Debug, Clone, Copy)]
48struct GoDuration(Duration);
49
50impl Serialize for GoDuration {
51 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
52 serializer.collect_str(&format_args!("{}ms", self.0.as_millis()))
53 }
54}
55
56#[derive(Debug, Clone, Copy)]
59struct DurationSeconds(Duration);
60
61impl Serialize for DurationSeconds {
62 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
63 serializer.serialize_u64(self.0.as_secs())
64 }
65}
66
67#[derive(Debug, Clone, Copy, Serialize)]
75#[serde(rename_all = "lowercase")]
76pub(crate) enum LogLevel {
77 Error,
79}
80
81#[derive(Debug, Clone, Copy, Serialize)]
83#[serde(rename_all = "lowercase")]
84pub(crate) enum TagCardinality {
85 Low,
88 Orchestrator,
90 High,
92}
93
94impl Distribution<TagCardinality> for StandardUniform {
95 fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> TagCardinality {
96 match rng.random_range(0..3u8) {
97 0 => TagCardinality::Low,
98 1 => TagCardinality::Orchestrator,
99 _ => TagCardinality::High,
100 }
101 }
102}
103
104#[allow(clippy::struct_field_names, clippy::struct_excessive_bools)]
112#[derive(Debug, Serialize)]
113pub(crate) struct DogStatsdConfig {
114 dogstatsd_socket: PathBuf,
116 dogstatsd_buffer_size: u64,
118 dogstatsd_so_rcvbuf: u64,
120 #[serde(skip_serializing_if = "Option::is_none")]
123 dogstatsd_packet_buffer_size: Option<u64>,
124 #[serde(skip_serializing_if = "Option::is_none")]
127 dogstatsd_packet_buffer_flush_timeout: Option<GoDuration>,
128 #[serde(skip_serializing_if = "Option::is_none")]
131 dogstatsd_queue_size: Option<u64>,
132 #[serde(skip_serializing_if = "Option::is_none")]
135 dogstatsd_pipeline_count: Option<u64>,
136 #[serde(skip_serializing_if = "Option::is_none")]
139 dogstatsd_workers_count: Option<u64>,
140 #[serde(skip_serializing_if = "Option::is_none")]
144 dogstatsd_expiry_seconds: Option<DurationSeconds>,
145 #[serde(skip_serializing_if = "Option::is_none")]
149 dogstatsd_context_expiry_seconds: Option<DurationSeconds>,
150 dogstatsd_string_interner_size: u64,
152 #[serde(skip_serializing_if = "Option::is_none")]
155 dogstatsd_mapper_cache_size: Option<u64>,
156 #[serde(skip_serializing_if = "Option::is_none")]
159 dogstatsd_no_aggregation_pipeline_batch_size: Option<u64>,
160 dogstatsd_tag_cardinality: TagCardinality,
162 dogstatsd_non_local_traffic: bool,
164 dogstatsd_origin_detection: bool,
166 dogstatsd_origin_detection_client: bool,
168 dogstatsd_origin_optout_enabled: bool,
170 #[serde(skip_serializing_if = "Option::is_none")]
173 dogstatsd_metrics_stats_enable: Option<bool>,
174 dogstatsd_entity_id_precedence: bool,
176 dogstatsd_no_aggregation_pipeline: bool,
179 dogstatsd_flush_incomplete_buckets: bool,
181 #[serde(skip_serializing_if = "Option::is_none")]
184 dogstatsd_pipeline_autoadjust: Option<bool>,
185 #[serde(skip_serializing_if = "Option::is_none")]
188 dogstatsd_stats_enable: Option<bool>,
189}
190
191impl DogStatsdConfig {
192 fn sample<R: Rng + ?Sized>(rng: &mut R, dogstatsd_socket: &Path, profile: ConfigProfile) -> Self {
195 let general = profile.is_general();
196 Self {
197 dogstatsd_socket: dogstatsd_socket.to_path_buf(),
198 dogstatsd_buffer_size: sample_buffer_size(rng, profile),
199 dogstatsd_so_rcvbuf: Probe::new(0, 34_359_738_368).sample(rng),
200 dogstatsd_packet_buffer_size: general.then(|| Probe::new(1, 10_000_000).sample(rng)),
201 dogstatsd_packet_buffer_flush_timeout: general
202 .then(|| GoDuration(Duration::from_millis(Probe::new(1, 86_400_000).sample(rng)))),
203 dogstatsd_queue_size: general.then(|| Probe::new(1, 10_000_000).sample(rng)),
204 dogstatsd_pipeline_count: general.then(|| Probe::new(1, 1_000_000).sample(rng)),
205 dogstatsd_workers_count: general.then(|| Probe::new(0, 1_000_000).sample(rng)),
206 dogstatsd_expiry_seconds: general
207 .then(|| DurationSeconds(Duration::from_secs(Probe::new(0, 31_536_000_000).sample(rng)))),
208 dogstatsd_context_expiry_seconds: general
209 .then(|| DurationSeconds(Duration::from_secs(Probe::new(0, 31_536_000_000).sample(rng)))),
210 dogstatsd_string_interner_size: Probe::new(1, MAX_STRING_INTERNER_ENTRIES).sample(rng),
211 dogstatsd_mapper_cache_size: general.then(|| Probe::new(0, 100_000_000).sample(rng)),
212 dogstatsd_no_aggregation_pipeline_batch_size: general.then(|| Probe::new(1, 10_000_000).sample(rng)),
213 dogstatsd_tag_cardinality: rng.random(),
214 dogstatsd_non_local_traffic: rng.random(),
215 dogstatsd_origin_detection: rng.random(),
216 dogstatsd_origin_detection_client: rng.random(),
217 dogstatsd_origin_optout_enabled: rng.random(),
218 dogstatsd_metrics_stats_enable: general.then(|| rng.random()),
219 dogstatsd_entity_id_precedence: rng.random(),
220 dogstatsd_no_aggregation_pipeline: rng.random(),
221 dogstatsd_flush_incomplete_buckets: rng.random(),
222 dogstatsd_pipeline_autoadjust: general.then(|| rng.random()),
223 dogstatsd_stats_enable: general.then(|| rng.random()),
224 }
225 }
226}
227
228const MAX_STRING_INTERNER_ENTRIES: u64 = 8_388_608;
234
235fn sample_buffer_size<R: Rng + ?Sized>(rng: &mut R, profile: ConfigProfile) -> u64 {
244 if profile.is_general() && rng.random_ratio(1, 16) {
245 Probe::new(0, 536_870_912).sample(rng)
246 } else {
247 rng.random_range(128..=65_536)
248 }
249}
250
251#[derive(Debug, Serialize)]
256pub struct DatadogConfig {
257 hostname: String,
260 api_key: String,
262 dd_url: String,
264 log_level: LogLevel,
266 #[serde(skip_serializing_if = "Option::is_none")]
272 aggregate_context_limit: Option<u64>,
273 #[serde(flatten)]
275 dogstatsd: DogStatsdConfig,
276}
277
278impl DatadogConfig {
279 #[must_use]
283 pub fn sample<R: Rng + ?Sized>(
284 rng: &mut R, hostname: &str, api_key: &str, dd_url: &str, dogstatsd_socket: &Path, profile: ConfigProfile,
285 ) -> Self {
286 Self {
287 hostname: hostname.to_owned(),
288 api_key: api_key.to_owned(),
289 dd_url: dd_url.to_owned(),
290 log_level: LogLevel::Error,
291 aggregate_context_limit: profile.is_general().then(|| Probe::new(1, 100_000_000).sample(rng)),
292 dogstatsd: DogStatsdConfig::sample(rng, dogstatsd_socket, profile),
293 }
294 }
295
296 pub fn to_yaml(&self) -> anyhow::Result<String> {
303 let mut yaml = serde_yaml::to_string(self).context("serialize datadog.yaml")?;
304 yaml.push_str(STATIC_YAML_TAIL);
305 Ok(yaml)
306 }
307}
308
309const STATIC_YAML_TAIL: &str = "use_dogstatsd: true
311use_v2_api_series: true
312inventories_enabled: false
313enable_metadata_collection: false
314cloud_provider_metadata: []
315";
316
317#[cfg(test)]
318mod tests {
319 use std::convert::Infallible;
320
321 use rand::rand_core::TryRng;
322
323 use super::*;
324
325 #[derive(Debug)]
330 struct SeqRng(u64);
331
332 impl TryRng for SeqRng {
333 type Error = Infallible;
334
335 fn try_next_u32(&mut self) -> Result<u32, Infallible> {
336 let bytes = self.try_next_u64()?.to_le_bytes();
337 Ok(u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
338 }
339
340 fn try_next_u64(&mut self) -> Result<u64, Infallible> {
341 self.0 = self.0.wrapping_add(0x9E37_79B9_7F4A_7C15);
342 let mut z = self.0;
343 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
344 z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
345 Ok(z ^ (z >> 31))
346 }
347
348 fn try_fill_bytes(&mut self, dst: &mut [u8]) -> Result<(), Infallible> {
349 for chunk in dst.chunks_mut(8) {
350 let bytes = self.try_next_u64()?.to_le_bytes();
351 chunk.copy_from_slice(&bytes[..chunk.len()]);
352 }
353 Ok(())
354 }
355 }
356
357 const GENERAL_ONLY_KEYS: &[&str] = &[
361 "aggregate_context_limit",
362 "dogstatsd_packet_buffer_size",
363 "dogstatsd_packet_buffer_flush_timeout",
364 "dogstatsd_queue_size",
365 "dogstatsd_pipeline_count",
366 "dogstatsd_workers_count",
367 "dogstatsd_expiry_seconds",
368 "dogstatsd_context_expiry_seconds",
369 "dogstatsd_mapper_cache_size",
370 "dogstatsd_no_aggregation_pipeline_batch_size",
371 "dogstatsd_metrics_stats_enable",
372 "dogstatsd_pipeline_autoadjust",
373 "dogstatsd_stats_enable",
374 ];
375
376 fn render(profile: ConfigProfile) -> String {
377 let mut rng = SeqRng(0);
378 DatadogConfig::sample(&mut rng, "h", "k", "http://intake:2049", Path::new("/s.sock"), profile)
379 .to_yaml()
380 .expect("render yaml")
381 }
382
383 #[test]
384 fn differential_omits_keys_the_agent_does_not_share() {
385 let yaml = render(ConfigProfile::Differential);
386 for key in GENERAL_ONLY_KEYS {
387 assert!(!yaml.contains(key), "differential config must omit `{key}`:\n{yaml}");
388 }
389 }
390
391 #[test]
392 fn general_keeps_the_full_feral_surface() {
393 let yaml = render(ConfigProfile::General);
394 for key in GENERAL_ONLY_KEYS {
395 assert!(yaml.contains(key), "general config must include `{key}`:\n{yaml}");
396 }
397 }
398
399 #[test]
400 fn differential_keeps_keys_both_targets_share() {
401 let yaml = render(ConfigProfile::Differential);
402 for key in [
403 "dogstatsd_string_interner_size",
404 "dogstatsd_non_local_traffic",
405 "dogstatsd_no_aggregation_pipeline",
406 ] {
407 assert!(
408 yaml.contains(key),
409 "differential config dropped shared key `{key}`:\n{yaml}"
410 );
411 }
412 }
413
414 #[test]
415 fn log_level_is_an_unambiguous_scalar_for_both_profiles() {
416 for profile in [ConfigProfile::General, ConfigProfile::Differential] {
417 let yaml = render(profile);
418 assert!(
419 yaml.contains("log_level: error"),
420 "missing `log_level: error` in:\n{yaml}"
421 );
422 }
423 }
424}