1use std::{
5 sync::{
6 atomic::{AtomicU64, Ordering},
7 Arc, Mutex,
8 },
9 time,
10};
11
12use crate::span_concentrator::{FlushableConcentrator, SpanConcentrator};
13use async_trait::async_trait;
14use futures::stream::FuturesUnordered;
15use futures::StreamExt as _;
16use libdd_capabilities::{HttpClientCapability, MaybeSend, SleepCapability};
17use libdd_common::{Endpoint, MutexExt};
18use libdd_shared_runtime::Worker;
19use libdd_trace_protobuf::pb;
20use libdd_trace_utils::send_with_retry::{
21 send_with_retry, CompressionStrategy, RetryBackoffType, RetryStrategy,
22};
23use libdd_trace_utils::trace_utils::TracerHeaderTags;
24use libdd_trace_utils::tracer_metadata::TracerMetadata;
25use std::fmt::Debug;
26use tracing::error;
27
28pub const STATS_ENDPOINT_PATH: &str = "/v0.6/stats";
29
30pub const COLLAPSED_SPANS_HEALTH_METRIC: &str = "datadog.tracer.stats.collapsed_spans";
32
33pub const COLLAPSED_SPANS_TELEMETRY_METRIC: &str = "tracers.stats_collapsed_spans";
35
36#[derive(Clone, Default, Debug)]
38pub struct StatsMetadata {
39 pub hostname: String,
40 pub env: String,
41 pub app_version: String,
42 pub runtime_id: String,
43 pub language: String,
44 pub lang_version: String,
45 pub lang_interpreter: String,
46 pub lang_vendor: String,
47 pub tracer_version: String,
48 pub git_commit_sha: String,
49 pub process_tags: String,
50 pub service: String,
51}
52
53impl<'a> From<&'a StatsMetadata> for TracerHeaderTags<'a> {
54 fn from(m: &'a StatsMetadata) -> TracerHeaderTags<'a> {
55 TracerHeaderTags {
56 lang: &m.language,
57 lang_version: &m.lang_version,
58 lang_interpreter: &m.lang_interpreter,
59 lang_vendor: &m.lang_vendor,
60 tracer_version: &m.tracer_version,
61 ..Default::default()
62 }
63 }
64}
65
66impl From<TracerMetadata> for StatsMetadata {
67 fn from(m: TracerMetadata) -> StatsMetadata {
68 StatsMetadata {
69 hostname: m.hostname,
70 env: m.env,
71 app_version: m.app_version,
72 runtime_id: m.runtime_id,
73 language: m.language,
74 lang_version: m.language_version,
75 lang_interpreter: m.language_interpreter,
76 lang_vendor: m.language_interpreter_vendor,
77 tracer_version: m.tracer_version,
78 git_commit_sha: m.git_commit_sha,
79 process_tags: m.process_tags,
80 service: m.service,
81 }
82 }
83}
84
85#[derive(Debug)]
90pub struct StatsExporter<
91 Cap: HttpClientCapability + SleepCapability + MaybeSend + Sync + 'static,
92 Con: FlushableConcentrator = SpanConcentrator,
93> {
94 flush_interval: time::Duration,
95 concentrator: Arc<Mutex<Con>>,
96 endpoint: Endpoint,
97 meta: StatsMetadata,
98 sequence_id: AtomicU64,
99 capabilities: Cap,
100 #[cfg(feature = "stats-obfuscation")]
101 supported_obfuscation_version: &'static str,
102 #[cfg(feature = "telemetry")]
104 telemetry: Option<(
105 libdd_telemetry::worker::TelemetryWorkerHandle<Cap>,
106 libdd_telemetry::metrics::ContextKey,
107 )>,
108 #[cfg(feature = "dogstatsd")]
110 dogstatsd: Option<libdd_dogstatsd_client::DogStatsDClient>,
111}
112
113impl<
114 Cap: HttpClientCapability + SleepCapability + MaybeSend + Sync + 'static,
115 Con: FlushableConcentrator,
116 > StatsExporter<Cap, Con>
117{
118 #[allow(clippy::too_many_arguments)]
126 pub fn new(
127 flush_interval: time::Duration,
128 concentrator: Arc<Mutex<Con>>,
129 meta: StatsMetadata,
130 endpoint: Endpoint,
131 capabilities: Cap,
132 #[cfg(feature = "stats-obfuscation")] supported_obfuscation_version: &'static str,
133 #[cfg(feature = "telemetry")] telemetry: Option<
134 libdd_telemetry::worker::TelemetryWorkerHandle<Cap>,
135 >,
136 #[cfg(feature = "dogstatsd")] dogstatsd: Option<libdd_dogstatsd_client::DogStatsDClient>,
137 ) -> Self {
138 #[cfg(feature = "telemetry")]
139 let telemetry = telemetry.map(|handle| {
140 let key = handle.register_metric_context(
141 COLLAPSED_SPANS_TELEMETRY_METRIC.to_string(),
142 vec![],
143 libdd_telemetry::data::metrics::MetricType::Count,
144 true,
145 libdd_telemetry::data::metrics::MetricNamespace::Tracers,
146 );
147 (handle, key)
148 });
149 Self {
150 flush_interval,
151 concentrator,
152 endpoint,
153 meta,
154 sequence_id: AtomicU64::new(0),
155 capabilities,
156 #[cfg(feature = "stats-obfuscation")]
157 supported_obfuscation_version,
158 #[cfg(feature = "telemetry")]
159 telemetry,
160 #[cfg(feature = "dogstatsd")]
161 dogstatsd,
162 }
163 }
164
165 pub async fn send(&self, force_flush: bool) -> anyhow::Result<bool> {
182 let flush = {
183 let mut concentrator = self.concentrator.lock_or_panic();
184 concentrator.flush_buckets(force_flush)
185 };
186
187 #[cfg(feature = "telemetry")]
188 if let Some((handle, key)) = &self.telemetry {
189 if flush.collapsed_spans > 0 {
190 let _ = handle.add_point(
191 flush.collapsed_spans as f64,
192 key,
193 vec![libdd_common::tag!("collapsed_spans", "whole_key")],
194 );
195 }
196 flush.collapsed_fields_metrics.emit_telemetry(handle, key);
197 }
198
199 #[cfg(feature = "dogstatsd")]
200 if let Some(client) = &self.dogstatsd {
201 if flush.collapsed_spans > 0 {
202 client.send(vec![libdd_dogstatsd_client::DogStatsDAction::Count(
203 COLLAPSED_SPANS_HEALTH_METRIC,
204 flush.collapsed_spans as i64,
205 [libdd_common::tag!("collapsed_spans", "whole_key")].iter(),
206 )]);
207 }
208 flush.collapsed_fields_metrics.emit_dogstatsd(client);
209 }
210
211 let futures = FuturesUnordered::new();
212
213 if !flush.obfuscated_buckets.is_empty() {
214 futures.push(self.send_payload(flush.obfuscated_buckets, true));
215 }
216
217 if !flush.unobfuscated_buckets.is_empty() {
218 futures.push(self.send_payload(flush.unobfuscated_buckets, false));
219 }
220
221 let sent_stats = !futures.is_empty();
222
223 futures
224 .collect::<Vec<anyhow::Result<()>>>()
225 .await
226 .into_iter()
227 .collect::<anyhow::Result<()>>()?;
228
229 Ok(sent_stats)
230 }
231
232 async fn send_payload(
237 &self,
238 buckets: Vec<pb::ClientStatsBucket>,
239 #[cfg_attr(not(feature = "stats-obfuscation"), allow(unused))] obfuscated: bool,
240 ) -> anyhow::Result<()> {
241 let sequence = self.sequence_id.fetch_add(1, Ordering::Relaxed);
242 let payload = encode_stats_payload(&self.meta, sequence, buckets);
243 let body = rmp_serde::encode::to_vec_named(&payload)?;
244
245 let mut headers: http::HeaderMap = TracerHeaderTags::from(&self.meta).into();
246 headers.insert(
247 http::header::CONTENT_TYPE,
248 libdd_common::header::APPLICATION_MSGPACK,
249 );
250 #[cfg(feature = "stats-obfuscation")]
251 if obfuscated {
252 headers.insert(
253 http::HeaderName::from_static("datadog-obfuscation-version"),
254 http::HeaderValue::from_static(self.supported_obfuscation_version),
255 );
256 }
257
258 let result = send_with_retry(
259 &self.capabilities,
260 &self.endpoint,
261 body,
262 &headers,
263 &RetryStrategy::new(0, 0, RetryBackoffType::Constant, None),
264 CompressionStrategy::None,
265 )
266 .await;
267
268 match result {
269 Ok(_) => Ok(()),
270 Err(err) => {
271 error!(?err, "Error with the StatsExporter when sending stats");
272 anyhow::bail!("Failed to send stats: {err}");
273 }
274 }
275 }
276}
277
278#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
279#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
280impl<
281 Cap: HttpClientCapability + SleepCapability + MaybeSend + Sync + 'static,
282 Con: FlushableConcentrator + Send + Debug,
283 > Worker for StatsExporter<Cap, Con>
284{
285 async fn trigger(&mut self) {
286 self.capabilities.sleep(self.flush_interval).await;
287 }
288
289 async fn run(&mut self) {
291 let _ = self.send(false).await; }
293
294 fn reset(&mut self) {
295 let _ = self.concentrator.lock_or_panic().flush_buckets(true);
296 self.sequence_id.store(0, Ordering::Relaxed);
297 }
298
299 async fn shutdown(&mut self) {
300 let _ = self.send(true).await;
301 }
302}
303
304fn encode_stats_payload(
305 meta: &StatsMetadata,
306 sequence: u64,
307 buckets: Vec<pb::ClientStatsBucket>,
308) -> pb::ClientStatsPayload {
309 pb::ClientStatsPayload {
310 hostname: meta.hostname.clone(),
311 env: if meta.env.is_empty() {
312 "unknown-env".to_string()
313 } else {
314 meta.env.clone()
315 },
316 version: meta.app_version.clone(),
317 runtime_id: meta.runtime_id.clone(),
318 sequence,
319 service: meta.service.clone(),
320 stats: buckets,
321 git_commit_sha: meta.git_commit_sha.clone(),
322 process_tags: meta.process_tags.clone(),
323 container_id: String::new(),
325 tags: Vec::new(),
326 agent_aggregation: String::new(),
327 image_tag: String::new(),
328 process_tags_hash: 0,
329 lang: String::new(),
330 tracer_version: String::new(),
331 }
332}
333
334pub fn stats_url_from_agent_url(agent_url: &str) -> anyhow::Result<http::Uri> {
336 let mut parts = agent_url.parse::<http::Uri>()?.into_parts();
337 parts.path_and_query = Some(http::uri::PathAndQuery::from_static(STATS_ENDPOINT_PATH));
338 Ok(http::Uri::from_parts(parts)?)
339}
340
341#[cfg(test)]
342mod tests {
343 use super::*;
344 use httpmock::prelude::*;
345 use httpmock::MockServer;
346 use libdd_capabilities_impl::NativeCapabilities;
347 use libdd_shared_runtime::{BlockingRuntime, ForkSafeRuntime, SharedRuntime};
348 use libdd_trace_utils::span::{trace_utils, v04::SpanSlice};
349 use libdd_trace_utils::test_utils::poll_for_mock_hit;
350 use time::Duration;
351 use time::SystemTime;
352
353 fn is_send<T: Send>() {}
354 fn is_sync<T: Sync>() {}
355
356 const BUCKETS_DURATION: Duration = Duration::from_secs(10);
357
358 #[test]
360 fn test_stats_exporter_sync_send() {
361 let _ = is_send::<StatsExporter<NativeCapabilities>>;
362 let _ = is_sync::<StatsExporter<NativeCapabilities>>;
363 }
364
365 fn get_test_metadata() -> StatsMetadata {
366 StatsMetadata {
367 hostname: "libdatadog-test".into(),
368 env: "test".into(),
369 app_version: "0.0.0".into(),
370 language: "rust".into(),
371 tracer_version: "0.0.0".into(),
372 runtime_id: "e39d6d12-0752-489f-b488-cf80006c0378".into(),
373 process_tags: "key1:value1,key2:value2".into(),
374 ..Default::default()
375 }
376 }
377
378 fn get_test_concentrator() -> SpanConcentrator {
379 get_test_concentrator_with_obfuscation_config(
380 #[cfg(feature = "stats-obfuscation")]
381 None,
382 )
383 }
384
385 fn get_test_concentrator_with_obfuscation_config(
386 #[cfg(feature = "stats-obfuscation")] obfuscation_config: Option<
387 crate::span_concentrator::SharedStatsComputationObfuscationConfig,
388 >,
389 ) -> SpanConcentrator {
390 let mut concentrator = SpanConcentrator::new(
391 BUCKETS_DURATION,
392 SystemTime::now() - BUCKETS_DURATION * 3,
394 vec![],
395 vec![],
396 None,
397 vec![],
398 #[cfg(feature = "stats-obfuscation")]
399 obfuscation_config,
400 );
401 let mut trace = vec![];
402
403 for i in 1..100 {
404 trace.push(SpanSlice {
405 service: "libdatadog-test",
406 duration: i,
407 ..Default::default()
408 })
409 }
410
411 trace_utils::compute_top_level_span(trace.as_mut_slice());
412
413 for span in trace.iter() {
414 concentrator.add_span(span);
415 }
416 concentrator
417 }
418
419 #[cfg_attr(miri, ignore)]
420 #[tokio::test]
421 async fn test_send_stats() {
422 let server = MockServer::start_async().await;
423
424 let mock = server
425 .mock_async(|when, then| {
426 when.method(POST)
427 .header("Content-type", "application/msgpack")
428 .path("/v0.6/stats")
429 .body_includes("libdatadog-test")
430 .body_includes("key1:value1,key2:value2");
431 then.status(200).body("");
432 })
433 .await;
434
435 let stats_exporter = StatsExporter::<NativeCapabilities>::new(
436 BUCKETS_DURATION,
437 Arc::new(Mutex::new(get_test_concentrator())),
438 get_test_metadata(),
439 Endpoint::from_url(stats_url_from_agent_url(&server.url("/")).unwrap()),
440 NativeCapabilities::new_client(),
441 #[cfg(feature = "stats-obfuscation")]
442 "1",
443 #[cfg(feature = "telemetry")]
444 None,
445 #[cfg(feature = "dogstatsd")]
446 None,
447 );
448
449 let send_status = stats_exporter.send(true).await;
450 send_status.unwrap();
451
452 mock.assert_async().await;
453 }
454
455 #[cfg_attr(miri, ignore)]
456 #[tokio::test]
457 async fn test_send_stats_fail() {
458 let server = MockServer::start_async().await;
459
460 let mut mock = server
461 .mock_async(|_when, then| {
462 then.status(503)
463 .header("content-type", "application/json")
464 .body(r#"{"status":"error"}"#);
465 })
466 .await;
467
468 let stats_exporter = StatsExporter::<NativeCapabilities>::new(
469 BUCKETS_DURATION,
470 Arc::new(Mutex::new(get_test_concentrator())),
471 get_test_metadata(),
472 Endpoint::from_url(stats_url_from_agent_url(&server.url("/")).unwrap()),
473 NativeCapabilities::new_client(),
474 #[cfg(feature = "stats-obfuscation")]
475 "1",
476 #[cfg(feature = "telemetry")]
477 None,
478 #[cfg(feature = "dogstatsd")]
479 None,
480 );
481
482 let send_status = stats_exporter.send(true).await;
483 send_status.unwrap_err();
484
485 assert!(
486 poll_for_mock_hit(&mut mock, 10, 100, 1, true).await,
487 "Expected a single attempt with no retries"
488 );
489 }
490
491 #[cfg_attr(miri, ignore)]
492 #[test]
493 fn test_run() {
494 let shared_runtime = ForkSafeRuntime::new().expect("Failed to create runtime");
495
496 let server = MockServer::start();
497
498 let mut mock = server.mock(|when, then| {
499 when.method(POST)
500 .header("Content-type", "application/msgpack")
501 .path("/v0.6/stats")
502 .body_includes("libdatadog-test")
503 .body_includes("key1:value1,key2:value2");
504 then.status(200).body("");
505 });
506
507 let caps = NativeCapabilities::new();
508 let stats_exporter = StatsExporter::<NativeCapabilities>::new(
509 Duration::from_secs(1),
511 Arc::new(Mutex::new(get_test_concentrator())),
512 get_test_metadata(),
513 Endpoint::from_url(stats_url_from_agent_url(&server.url("/")).unwrap()),
514 caps.clone(),
515 #[cfg(feature = "stats-obfuscation")]
516 "1",
517 #[cfg(feature = "telemetry")]
518 None,
519 #[cfg(feature = "dogstatsd")]
520 None,
521 );
522 let _handle = shared_runtime
523 .spawn_worker(stats_exporter, true)
524 .expect("Failed to spawn worker");
525
526 std::thread::sleep(Duration::from_secs(1));
528
529 assert!(
530 shared_runtime
531 .block_on(poll_for_mock_hit(&mut mock, 10, 100, 1, false))
532 .expect("Failed to use runtime"),
533 "Expected max retry attempts"
534 );
535 }
536
537 #[cfg_attr(miri, ignore)]
538 #[test]
539 fn test_worker_shutdown() {
540 let shared_runtime = ForkSafeRuntime::new().expect("Failed to create runtime");
541
542 let server = MockServer::start();
543
544 let mut mock = server.mock(|when, then| {
545 when.method(POST)
546 .header("Content-type", "application/msgpack")
547 .path("/v0.6/stats")
548 .body_includes("libdatadog-test")
549 .body_includes("key1:value1,key2:value2");
550 then.status(200).body("");
551 });
552
553 let buckets_duration = Duration::from_secs(10);
554
555 let caps = NativeCapabilities::new();
556 let stats_exporter = StatsExporter::<NativeCapabilities>::new(
557 buckets_duration,
558 Arc::new(Mutex::new(get_test_concentrator())),
559 get_test_metadata(),
560 Endpoint::from_url(stats_url_from_agent_url(&server.url("/")).unwrap()),
561 caps.clone(),
562 #[cfg(feature = "stats-obfuscation")]
563 "1",
564 #[cfg(feature = "telemetry")]
565 None,
566 #[cfg(feature = "dogstatsd")]
567 None,
568 );
569
570 let _handle = shared_runtime
571 .spawn_worker(stats_exporter, true)
572 .expect("Failed to spawn worker");
573
574 shared_runtime.shutdown(None).unwrap();
575
576 assert!(
577 shared_runtime
578 .block_on(poll_for_mock_hit(&mut mock, 10, 100, 1, false))
579 .expect("Failed to get runtime"),
580 "Expected max retry attempts"
581 );
582 }
583
584 #[test]
585 fn test_encode_stats_payload_defaults_empty_env() {
586 let mut meta_with_empty_env = get_test_metadata();
588 meta_with_empty_env.env = "".to_string();
589
590 let buckets = vec![];
591 let payload = encode_stats_payload(&meta_with_empty_env, 1, buckets.clone());
592
593 assert_eq!(
594 payload.env, "unknown-env",
595 "Empty env should default to 'unknown-env'"
596 );
597
598 let meta_with_env = get_test_metadata();
600 let payload_with_env = encode_stats_payload(&meta_with_env, 2, buckets);
601
602 assert_eq!(
603 payload_with_env.env, "test",
604 "Non-empty env should be preserved"
605 );
606 }
607 #[cfg(feature = "stats-obfuscation")]
608 #[cfg_attr(miri, ignore)]
609 #[tokio::test]
610 async fn test_send_stats_with_obfuscation_header() {
611 use crate::span_concentrator::StatsComputationObfuscationConfig;
612 use arc_swap::ArcSwap;
613
614 let server = MockServer::start_async().await;
615
616 let mock = server
617 .mock_async(|when, then| {
618 when.method(POST)
619 .header("Content-type", "application/msgpack")
620 .header("datadog-obfuscation-version", "1")
621 .path("/v0.6/stats")
622 .body_includes("libdatadog-test");
623 then.status(200).body("");
624 })
625 .await;
626
627 let concentrator = get_test_concentrator_with_obfuscation_config(Some(Arc::new(
628 ArcSwap::from_pointee(StatsComputationObfuscationConfig {
629 enabled: true,
630 ..Default::default()
631 }),
632 )));
633
634 let stats_exporter = StatsExporter::new(
635 BUCKETS_DURATION,
636 Arc::new(Mutex::new(concentrator)),
637 get_test_metadata(),
638 Endpoint::from_url(stats_url_from_agent_url(&server.url("/")).unwrap()),
639 NativeCapabilities::new_client(),
640 #[cfg(feature = "stats-obfuscation")]
641 "1",
642 #[cfg(feature = "telemetry")]
643 None,
644 #[cfg(feature = "dogstatsd")]
645 None,
646 );
647
648 let send_status = stats_exporter.send(true).await;
649 send_status.unwrap();
650
651 mock.assert_async().await;
652 }
653
654 #[cfg(any(feature = "telemetry", feature = "dogstatsd"))]
660 fn get_collapsed_concentrator(per_key_collapsed: bool) -> SpanConcentrator {
661 use crate::span_concentrator::CardinalityLimitConfig;
662 use libdd_trace_utils::span::{
663 trace_utils,
664 v04::{SpanSlice, VecMap},
665 };
666
667 let mut cardinality_limit_config = CardinalityLimitConfig {
668 whole_key_limit: 2, ..Default::default()
670 };
671 if per_key_collapsed {
672 cardinality_limit_config.resource_limit = 1;
673 cardinality_limit_config.http_endpoint_limit = 1;
674 }
675 let mut concentrator = SpanConcentrator::new(
676 BUCKETS_DURATION,
677 SystemTime::now(),
678 vec![],
679 vec![],
680 Some(cardinality_limit_config),
681 vec![],
682 #[cfg(feature = "stats-obfuscation")]
683 None,
684 );
685
686 let mut trace = vec![
687 SpanSlice {
688 service: "svc-a",
689 resource: "resource-a",
690 duration: 10,
691 meta: VecMap::from_iter([("http.endpoint", "/")]),
692 ..Default::default()
693 },
694 SpanSlice {
696 service: "svc-a",
697 resource: "resource-b",
698 duration: 20,
699 meta: VecMap::from_iter([("http.endpoint", "/")]),
700 ..Default::default()
701 },
702 SpanSlice {
704 service: "svc-b",
705 resource: "resource-c",
706 duration: 20,
707 meta: VecMap::from_iter([("http.endpoint", "/hello.txt")]),
708 ..Default::default()
709 },
710 SpanSlice {
712 service: "svc-b",
713 resource: "resource-b",
714 duration: 20,
715 ..Default::default()
716 },
717 ];
718 trace_utils::compute_top_level_span(trace.as_mut_slice());
719 for span in &trace {
720 concentrator.add_span(span);
721 }
722 concentrator
723 }
724
725 #[cfg(feature = "dogstatsd")]
727 #[cfg_attr(miri, ignore)]
728 #[tokio::test]
729 async fn test_no_emission_when_zero() {
730 use std::net;
731
732 let server = MockServer::start_async().await;
733 server
734 .mock_async(|_when, then| {
735 then.status(200).body("");
736 })
737 .await;
738
739 let socket = net::UdpSocket::bind("127.0.0.1:0").expect("failed to bind UDP socket");
741 socket
742 .set_read_timeout(Some(std::time::Duration::from_millis(200)))
743 .unwrap();
744 let addr = socket.local_addr().unwrap().to_string();
745
746 let dogstatsd_client =
747 libdd_dogstatsd_client::DogStatsDClient::new(libdd_common::Endpoint::from_slice(&addr))
748 .expect("failed to create dogstatsd client");
749
750 let stats_exporter = StatsExporter::<NativeCapabilities>::new(
752 BUCKETS_DURATION,
753 Arc::new(Mutex::new(get_test_concentrator())),
754 get_test_metadata(),
755 Endpoint::from_url(stats_url_from_agent_url(&server.url("/")).unwrap()),
756 NativeCapabilities::new_client(),
757 #[cfg(feature = "stats-obfuscation")]
758 "1",
759 #[cfg(feature = "telemetry")]
760 None,
761 Some(dogstatsd_client),
762 );
763
764 stats_exporter.send(true).await.unwrap();
765
766 let mut buf = [0u8; 256];
768 let result = socket.recv(&mut buf);
769 assert!(
770 result.is_err(),
771 "No DogStatsD datagram expected when collapsed_spans == 0. Got {}",
772 std::str::from_utf8(&buf[..result.unwrap()]).unwrap()
773 );
774 }
775
776 #[cfg(feature = "dogstatsd")]
778 #[cfg_attr(miri, ignore)]
779 #[tokio::test]
780 async fn test_collapsed_spans_dogstatsd() {
781 use std::net;
782
783 let server = MockServer::start_async().await;
784 server
785 .mock_async(|_when, then| {
786 then.status(200).body("");
787 })
788 .await;
789
790 let socket = net::UdpSocket::bind("127.0.0.1:0").expect("failed to bind UDP socket");
791 socket
792 .set_read_timeout(Some(std::time::Duration::from_millis(500)))
793 .unwrap();
794 let addr = socket.local_addr().unwrap().to_string();
795
796 let dogstatsd_client =
797 libdd_dogstatsd_client::DogStatsDClient::new(libdd_common::Endpoint::from_slice(&addr))
798 .expect("failed to create dogstatsd client");
799
800 let stats_exporter = StatsExporter::<NativeCapabilities>::new(
801 BUCKETS_DURATION,
802 Arc::new(Mutex::new(get_collapsed_concentrator(false))),
803 get_test_metadata(),
804 Endpoint::from_url(stats_url_from_agent_url(&server.url("/")).unwrap()),
805 NativeCapabilities::new_client(),
806 #[cfg(feature = "stats-obfuscation")]
807 "1",
808 #[cfg(feature = "telemetry")]
809 None,
810 Some(dogstatsd_client),
811 );
812
813 stats_exporter.send(true).await.unwrap();
814
815 let mut buf = [0u8; 256];
816 let n = socket
817 .recv(&mut buf)
818 .expect("expected a DogStatsD datagram");
819 let datagram = std::str::from_utf8(&buf[..n]).expect("valid utf-8");
820 assert_eq!(
821 datagram, "datadog.tracer.stats.collapsed_spans:2|c|#collapsed_spans:whole_key",
822 "DogStatsD datagram must match the expected format"
823 );
824 }
825
826 #[cfg(feature = "dogstatsd")]
829 #[cfg_attr(miri, ignore)]
830 #[tokio::test]
831 async fn test_collapsed_spans_per_key_dogstatsd() {
832 use std::net;
833
834 let server = MockServer::start_async().await;
835 server
836 .mock_async(|_when, then| {
837 then.status(200).body("");
838 })
839 .await;
840
841 let socket = net::UdpSocket::bind("127.0.0.1:0").expect("failed to bind UDP socket");
842 socket
843 .set_read_timeout(Some(std::time::Duration::from_millis(500)))
844 .unwrap();
845 let addr = socket.local_addr().unwrap().to_string();
846
847 let dogstatsd_client =
848 libdd_dogstatsd_client::DogStatsDClient::new(libdd_common::Endpoint::from_slice(&addr))
849 .expect("failed to create dogstatsd client");
850
851 let stats_exporter = StatsExporter::<NativeCapabilities>::new(
852 BUCKETS_DURATION,
853 Arc::new(Mutex::new(get_collapsed_concentrator(true))),
854 get_test_metadata(),
855 Endpoint::from_url(stats_url_from_agent_url(&server.url("/")).unwrap()),
856 NativeCapabilities::new_client(),
857 #[cfg(feature = "stats-obfuscation")]
858 "1",
859 #[cfg(feature = "telemetry")]
860 None,
861 Some(dogstatsd_client),
862 );
863
864 stats_exporter.send(true).await.unwrap();
865
866 let mut buf = [0u8; 256];
867
868 let n = socket
870 .recv(&mut buf)
871 .expect("expected a DogStatsD datagram");
872 let datagram = std::str::from_utf8(&buf[..n]).expect("valid utf-8");
873 assert_eq!(
874 datagram, "datadog.tracer.stats.collapsed_spans:2|c|#collapsed_spans:whole_key",
875 "DogStatsD datagram must match the expected format"
876 );
877
878 let n = socket
880 .recv(&mut buf)
881 .expect("expected a DogStatsD datagram");
882 let datagram = std::str::from_utf8(&buf[..n]).expect("valid utf-8");
883 assert_eq!(
884 datagram, "datadog.tracer.stats.collapsed_spans:1|c|#collapsed_spans:resource",
885 "DogStatsD datagram must match the expected format"
886 );
887 let n = socket
888 .recv(&mut buf)
889 .expect("expected a DogStatsD datagram");
890 let datagram = std::str::from_utf8(&buf[..n]).expect("valid utf-8");
891 assert_eq!(
892 datagram, "datadog.tracer.stats.collapsed_spans:2|c|#collapsed_spans:resource,collapsed_spans:http_endpoint",
893 "DogStatsD datagram must match the expected format"
894 );
895 }
896
897 #[cfg(feature = "telemetry")]
900 #[cfg_attr(miri, ignore)]
901 #[tokio::test]
902 async fn test_collapsed_spans_telemetry() {
903 use libdd_telemetry::worker::TelemetryWorkerBuilder;
904
905 let server = MockServer::start_async().await;
906 server
907 .mock_async(|_when, then| {
908 then.status(200).body("");
909 })
910 .await;
911
912 let (handle, _join_handle) = TelemetryWorkerBuilder::new(
913 "test-host".to_string(),
914 "test-service".to_string(),
915 "rust".to_string(),
916 "1.0".to_string(),
917 "0.0.0".to_string(),
918 )
919 .spawn();
920
921 let stats_exporter = StatsExporter::<NativeCapabilities>::new(
922 BUCKETS_DURATION,
923 Arc::new(Mutex::new(get_collapsed_concentrator(true))),
924 get_test_metadata(),
925 Endpoint::from_url(stats_url_from_agent_url(&server.url("/")).unwrap()),
926 NativeCapabilities::new_client(),
927 #[cfg(feature = "stats-obfuscation")]
928 "1",
929 #[cfg(feature = "telemetry")]
930 Some(handle),
931 #[cfg(feature = "dogstatsd")]
932 None,
933 );
934
935 stats_exporter.send(true).await.unwrap();
936
937 let stats_exporter_ref = &stats_exporter;
938 let (handle_ref, _key) = stats_exporter_ref
939 .telemetry
940 .as_ref()
941 .expect("telemetry must be set");
942 let receiver = handle_ref.stats().expect("failed to request stats");
943 let stats = receiver.await.expect("failed to receive stats");
944 assert_eq!(
949 stats.metric_contexts, 1,
950 "exactly one metric context (COLLAPSED_SPANS_METRIC) should be registered"
951 );
952 assert_eq!(
953 stats.metric_buckets.buckets, 3,
954 "exactly 3 metric bucket expected after one whole-key collapsed-spans, one resource key collapsed-span and one resource key+http_endpoint key emissions"
955 );
956 }
957}