1pub mod http_client;
5pub mod metric_ring;
6mod scheduler;
7pub mod store;
8
9use crate::{
10 config::Config,
11 data::{
12 self, Application, Dependency, Endpoint, Host, Integration, Log, Payload, ProductState,
13 Telemetry,
14 },
15 metrics::{ContextKey, MetricBuckets, MetricContexts},
16};
17
18use crate::worker::metric_ring::MetricRing;
19
20use async_trait::async_trait;
21use bytes::Bytes;
22use libdd_capabilities::{HttpClientCapability, HttpError, MaybeSend, SleepCapability};
23use libdd_common::tag::Tag;
24use libdd_shared_runtime::Worker;
25
26use std::iter::Sum;
27use std::marker::PhantomData;
28use std::ops::Add;
29use std::{
30 collections::hash_map::DefaultHasher,
31 hash::{Hash, Hasher},
32 ops::ControlFlow,
33 sync::{
34 atomic::{AtomicU64, Ordering},
35 Arc,
36 },
37};
38use std::{collections::HashSet, fmt::Debug, time::Duration};
39use web_time as time;
44
45#[cfg(not(target_arch = "wasm32"))]
46use std::sync::{Condvar, Mutex};
47
48use crate::metrics::MetricBucketStats;
49use futures::channel::oneshot;
50use http::{header, HeaderValue};
51use serde::{Deserialize, Serialize};
52use tokio::sync::mpsc;
53#[cfg(not(target_arch = "wasm32"))]
54use tokio::{runtime, task::JoinHandle};
55use tokio_util::sync::CancellationToken;
56use tracing::debug;
57
58const CONTINUE: ControlFlow<()> = ControlFlow::Continue(());
59const BREAK: ControlFlow<()> = ControlFlow::Break(());
60
61fn time_now() -> f64 {
62 time::SystemTime::UNIX_EPOCH
63 .elapsed()
64 .unwrap_or_default()
65 .as_secs_f64()
66}
67
68macro_rules! telemetry_worker_log {
69 ($worker:expr , ERROR , $fmt_str:tt, $($arg:tt)*) => {
70 {
71 debug!(
72 worker.runtime_id = %$worker.runtime_id,
73 worker.debug_logging = $worker.config.telemetry_debug_logging_enabled,
74 $fmt_str,
75 $($arg)*
76 );
77 if $worker.config.telemetry_debug_logging_enabled {
78 eprintln!(concat!("{}: Telemetry worker ERROR: ", $fmt_str), time_now(), $($arg)*);
79 }
80 }
81 };
82 ($worker:expr , DEBUG , $fmt_str:tt, $($arg:tt)*) => {
83 {
84 debug!(
85 worker.runtime_id = %$worker.runtime_id,
86 worker.debug_logging = $worker.config.telemetry_debug_logging_enabled,
87 $fmt_str,
88 $($arg)*
89 );
90 if $worker.config.telemetry_debug_logging_enabled {
91 eprintln!(concat!("{}: Telemetry worker DEBUG: ", $fmt_str), time_now(), $($arg)*);
92 }
93 }
94 };
95}
96
97#[derive(Debug, Serialize, Deserialize)]
98pub enum TelemetryActions {
99 AddPoint((f64, ContextKey, Vec<Tag>)),
100 AddConfig(data::Configuration),
101 AddDependency(Dependency),
102 AddIntegration(Integration),
103 AddProductChange((String, ProductState)),
104 AddLog((LogIdentifier, Log)),
105 AddEndpoint(Endpoint),
106 Lifecycle(LifecycleAction),
107 #[serde(skip)]
108 CollectStats(oneshot::Sender<TelemetryWorkerStats>),
109}
110
111#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
112pub enum LifecycleAction {
113 Start,
114 Stop,
115 FlushMetricAggr,
116 FlushData,
117 ExtendedHeartbeat,
118}
119
120#[derive(Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
125pub struct LogIdentifier {
126 pub identifier: u64,
128}
129
130#[derive(Debug)]
132struct TelemetryWorkerData {
133 started: bool,
134 dependencies: store::Store<data::Dependency, data::DependencyKey>,
135 configurations: store::Store<data::Configuration>,
136 integrations: store::Store<data::Integration>,
137 endpoints: store::Store<data::Endpoint>,
138 endpoints_is_first: bool,
139 products: std::collections::HashMap<String, ProductState>,
140 products_pending: HashSet<String>,
141 logs: store::QueueHashMap<LogIdentifier, Log>,
142 metric_contexts: MetricContexts,
143 metric_buckets: MetricBuckets,
144 host: Host,
145 app: Application,
146 install_signature: Option<data::InstallSignature>,
147}
148
149pub struct TelemetryWorker<C: HttpClientCapability + SleepCapability + MaybeSend + Sync + 'static> {
152 flavor: TelemetryWorkerFlavor,
153 config: Config,
154 mailbox: mpsc::Receiver<TelemetryActions>,
155 cancellation_token: CancellationToken,
156 seq_id: AtomicU64,
157 runtime_id: String,
158 capabilities: C,
159 metrics_flush_interval: Duration,
160 deadlines: scheduler::Scheduler<LifecycleAction>,
161 data: TelemetryWorkerData,
162 next_action: Option<TelemetryActions>,
163 stopped: bool,
164 metric_ring: Arc<MetricRing>,
167}
168
169impl<C: HttpClientCapability + SleepCapability + MaybeSend + Sync + 'static> Debug
170 for TelemetryWorker<C>
171{
172 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
173 f.debug_struct("TelemetryWorker")
174 .field("flavor", &self.flavor)
175 .field("config", &self.config)
176 .field("mailbox", &self.mailbox)
177 .field("cancellation_token", &self.cancellation_token)
178 .field("seq_id", &self.seq_id)
179 .field("runtime_id", &self.runtime_id)
180 .field("metrics_flush_interval", &self.metrics_flush_interval)
181 .field("deadlines", &self.deadlines)
182 .field("data", &self.data)
183 .finish()
184 }
185}
186
187#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
188#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
189impl<C: HttpClientCapability + SleepCapability + MaybeSend + Sync + 'static> Worker
190 for TelemetryWorker<C>
191{
192 async fn trigger(&mut self) {
193 if self.next_action.is_some() {
194 return;
196 }
197 if self.stopped {
198 debug!(
202 worker.runtime_id = %self.runtime_id,
203 "Telemetry worker mailbox closed; parking until shutdown"
204 );
205 std::future::pending::<()>().await;
206 }
207 let action = self.recv_next_action().await;
209 self.next_action = Some(action);
210 }
211
212 async fn run(&mut self) {
214 if let Some(action) = self.next_action.take() {
216 debug!(
217 worker.runtime_id = %self.runtime_id,
218 action = ?action,
219 "Received telemetry action"
220 );
221
222 let _action_result = match self.flavor {
225 TelemetryWorkerFlavor::Full => self.dispatch_action(action).await,
226 TelemetryWorkerFlavor::MetricsLogs => {
227 self.dispatch_metrics_logs_action(action).await
228 }
229 };
230 }
231 }
232
233 fn reset(&mut self) {
239 while self.mailbox.try_recv().is_ok() {}
241
242 self.next_action = None;
244
245 self.data.logs = store::QueueHashMap::default();
247 self.data.metric_buckets = MetricBuckets::default();
248 self.metric_ring.drain(|_, _, _| {});
250 self.data.dependencies.clear();
251 self.data.integrations.clear();
252 self.data.configurations.clear();
253 self.data.endpoints.clear();
254 self.data.endpoints_is_first = true;
255 self.data.products.clear();
256 self.data.products_pending.clear();
257 }
258
259 async fn shutdown(&mut self) {
260 for _ in 0..self.mailbox.len() {
263 if let Ok(action) = self.mailbox.try_recv() {
264 let _ = match self.flavor {
265 TelemetryWorkerFlavor::Full => self.dispatch_action(action).await,
266 TelemetryWorkerFlavor::MetricsLogs => {
267 self.dispatch_metrics_logs_action(action).await
268 }
269 };
270 }
271 }
272
273 let stop_action = TelemetryActions::Lifecycle(LifecycleAction::Stop);
274 let _action_result = match self.flavor {
275 TelemetryWorkerFlavor::Full => self.dispatch_action(stop_action).await,
276 TelemetryWorkerFlavor::MetricsLogs => {
277 self.dispatch_metrics_logs_action(stop_action).await
278 }
279 };
280 }
281}
282
283#[derive(Debug, Default, Serialize, Deserialize)]
284pub struct TelemetryWorkerStats {
285 pub dependencies_stored: u32,
286 pub dependencies_unflushed: u32,
287 pub configurations_stored: u32,
288 pub configurations_unflushed: u32,
289 pub integrations_stored: u32,
290 pub integrations_unflushed: u32,
291 pub logs: u32,
292 pub metric_contexts: u32,
293 pub metric_buckets: MetricBucketStats,
294}
295
296impl Add for TelemetryWorkerStats {
297 type Output = Self;
298
299 fn add(self, rhs: Self) -> Self::Output {
300 TelemetryWorkerStats {
301 dependencies_stored: self.dependencies_stored + rhs.dependencies_stored,
302 dependencies_unflushed: self.dependencies_unflushed + rhs.dependencies_unflushed,
303 configurations_stored: self.configurations_stored + rhs.configurations_stored,
304 configurations_unflushed: self.configurations_unflushed + rhs.configurations_unflushed,
305 integrations_stored: self.integrations_stored + rhs.integrations_stored,
306 integrations_unflushed: self.integrations_unflushed + rhs.integrations_unflushed,
307 logs: self.logs + rhs.logs,
308 metric_contexts: self.metric_contexts + rhs.metric_contexts,
309 metric_buckets: MetricBucketStats {
310 buckets: self.metric_buckets.buckets + rhs.metric_buckets.buckets,
311 series: self.metric_buckets.series + rhs.metric_buckets.series,
312 series_points: self.metric_buckets.series_points + rhs.metric_buckets.series_points,
313 distributions: self.metric_buckets.distributions + rhs.metric_buckets.distributions,
314 distributions_points: self.metric_buckets.distributions_points
315 + rhs.metric_buckets.distributions_points,
316 },
317 }
318 }
319}
320
321impl Sum for TelemetryWorkerStats {
322 fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
323 iter.fold(Self::default(), |a, b| a + b)
324 }
325}
326
327mod serialize {
328 use crate::data;
329 use http::HeaderValue;
330 #[allow(clippy::declare_interior_mutable_const)]
331 pub const CONTENT_TYPE_VALUE: HeaderValue = libdd_common::header::APPLICATION_JSON;
332 pub fn serialize(telemetry: &data::Telemetry) -> anyhow::Result<Vec<u8>> {
333 Ok(serde_json::to_vec(telemetry)?)
334 }
335}
336
337impl<C: HttpClientCapability + SleepCapability + MaybeSend + Sync + 'static> TelemetryWorker<C> {
338 fn log_err(&self, err: &anyhow::Error) {
339 telemetry_worker_log!(self, ERROR, "{}", err);
340 }
341
342 fn drain_metric_ring(&mut self) {
344 let ring = self.metric_ring.clone();
347 let buckets = &mut self.data.metric_buckets;
348 ring.drain(|value, key, extra_tags| buckets.add_point(key, value, extra_tags));
349 }
350
351 fn flush_metric_aggregates(&mut self) {
353 self.drain_metric_ring();
354 self.data.metric_buckets.flush_aggregates();
355 }
356
357 async fn recv_next_action(&mut self) -> TelemetryActions {
358 loop {
359 self.drain_metric_ring();
361
362 let action = if let Some((deadline, deadline_action)) = self.deadlines.next_deadline() {
363 let deadline_action = *deadline_action;
364 let Some(remaining) = deadline.checked_duration_since(time::Instant::now()) else {
369 if let Ok(mailbox_action) = self.mailbox.try_recv() {
370 return mailbox_action;
371 }
372 return TelemetryActions::Lifecycle(deadline_action);
373 };
374
375 let sleeper = <C as SleepCapability>::new();
376 let ring = self.metric_ring.clone();
377 tokio::select! {
378 biased;
379 mailbox_action = self.mailbox.recv() => mailbox_action,
380 _ = sleeper.sleep(remaining) => Some(TelemetryActions::Lifecycle(deadline_action)),
381 _ = ring.notified() => continue,
383 }
384 } else {
385 let ring = self.metric_ring.clone();
386 tokio::select! {
387 biased;
388 mailbox_action = self.mailbox.recv() => mailbox_action,
389 _ = ring.notified() => continue,
390 }
391 };
392
393 return action.unwrap_or_else(|| {
395 self.config.restartable = false;
397 self.stopped = true;
398 TelemetryActions::Lifecycle(LifecycleAction::Stop)
399 });
400 }
401 }
402
403 async fn dispatch_metrics_logs_action(&mut self, action: TelemetryActions) -> ControlFlow<()> {
404 telemetry_worker_log!(self, DEBUG, "Handling metric action {:?}", action);
405 use LifecycleAction::*;
406 use TelemetryActions::*;
407 match action {
408 Lifecycle(Start) => {
409 if !self.data.started {
410 #[allow(clippy::unwrap_used)]
411 self.deadlines
412 .schedule_event(LifecycleAction::FlushMetricAggr)
413 .unwrap();
414
415 #[allow(clippy::unwrap_used)]
416 self.deadlines
417 .schedule_event(LifecycleAction::FlushData)
418 .unwrap();
419 self.data.started = true;
420 }
421 }
422 AddLog((identifier, log)) => {
423 let (l, new) = self.data.logs.get_mut_or_insert(identifier, log);
424 if !new {
425 l.count += 1;
426 }
427 }
428 AddPoint((point, key, extra_tags)) => {
429 self.data.metric_buckets.add_point(key, point, extra_tags)
430 }
431 Lifecycle(FlushMetricAggr) => {
432 self.flush_metric_aggregates();
433
434 #[allow(clippy::unwrap_used)]
435 self.deadlines
436 .schedule_event(LifecycleAction::FlushMetricAggr)
437 .unwrap();
438 }
439 Lifecycle(FlushData) => {
440 if !(self.data.started || self.config.restartable) {
441 return CONTINUE;
442 }
443
444 #[allow(clippy::unwrap_used)]
445 self.deadlines
446 .schedule_event(LifecycleAction::FlushData)
447 .unwrap();
448
449 let batch = self.build_observability_batch();
450 if !batch.is_empty() {
451 let payload = data::Payload::MessageBatch(batch);
452 match self.send_payload(&payload).await {
453 Ok(()) => self.payload_sent_success(&payload),
454 Err(e) => self.log_err(&e),
455 }
456 }
457 }
458 AddConfig(_)
459 | AddDependency(_)
460 | AddIntegration(_)
461 | AddProductChange(_)
462 | AddEndpoint(_)
463 | Lifecycle(ExtendedHeartbeat) => {}
464 Lifecycle(Stop) => {
465 if !self.data.started {
466 return BREAK;
467 }
468 self.flush_metric_aggregates();
469
470 let batch = self.build_observability_batch();
471 if !batch.is_empty() {
472 let payload = data::Payload::MessageBatch(batch);
473 match self.send_payload(&payload).await {
474 Ok(()) => {
475 if self.config.restartable {
476 self.payload_sent_success(&payload)
477 }
478 }
479 Err(e) => self.log_err(&e),
480 }
481 }
482
483 self.data.started = false;
484 if !self.config.restartable {
485 self.deadlines.clear_pending();
486 }
487 return BREAK;
488 }
489 CollectStats(stats_sender) => {
490 stats_sender.send(self.stats()).ok();
491 }
492 };
493 CONTINUE
494 }
495
496 async fn dispatch_action(&mut self, action: TelemetryActions) -> ControlFlow<()> {
497 telemetry_worker_log!(self, DEBUG, "Handling action {:?}", action);
498
499 use LifecycleAction::*;
500 use TelemetryActions::*;
501 match action {
502 Lifecycle(Start) => {
503 if !self.data.started {
504 if self.config.emit_app_lifecycle {
505 let app_started = data::Payload::AppStarted(self.build_app_started());
506 match self.send_payload(&app_started).await {
507 Ok(()) => self.payload_sent_success(&app_started),
508 Err(err) => self.log_err(&err),
509 }
510 }
511
512 #[allow(clippy::unwrap_used)]
513 self.deadlines
514 .schedule_event(LifecycleAction::FlushMetricAggr)
515 .unwrap();
516
517 #[allow(clippy::unwrap_used)]
518 self.deadlines
520 .schedule_event(LifecycleAction::FlushData)
521 .unwrap();
522
523 #[allow(clippy::unwrap_used)]
524 self.deadlines
525 .schedule_event(LifecycleAction::ExtendedHeartbeat)
526 .unwrap();
527 self.data.started = true;
528 }
529 }
530 AddDependency(dep) => self.data.dependencies.insert(dep),
531 AddIntegration(integration) => self.data.integrations.insert(integration),
532 AddProductChange((name, state)) => {
533 self.data.products.insert(name.clone(), state);
534 self.data.products_pending.insert(name);
535 }
536 AddConfig(cfg) => self.data.configurations.insert(cfg),
537 AddEndpoint(endpoint) => {
538 self.data.endpoints.insert(endpoint);
539 }
540 AddLog((identifier, log)) => {
541 let (l, new) = self.data.logs.get_mut_or_insert(identifier, log);
542 if !new {
543 l.count += 1;
544 }
545 }
546 AddPoint((point, key, extra_tags)) => {
547 self.data.metric_buckets.add_point(key, point, extra_tags)
548 }
549 Lifecycle(FlushMetricAggr) => {
550 self.flush_metric_aggregates();
551
552 #[allow(clippy::unwrap_used)]
553 self.deadlines
554 .schedule_event(LifecycleAction::FlushMetricAggr)
555 .unwrap();
556 }
557 Lifecycle(FlushData) => {
558 if !(self.data.started || self.config.restartable) {
559 return CONTINUE;
560 }
561
562 #[allow(clippy::unwrap_used)]
563 self.deadlines
564 .schedule_event(LifecycleAction::FlushData)
565 .unwrap();
566
567 let mut batch = self.build_app_events_batch();
568 let payload = if batch.is_empty() {
569 data::Payload::AppHeartbeat(())
570 } else {
571 batch.push(data::Payload::AppHeartbeat(()));
572 data::Payload::MessageBatch(batch)
573 };
574 match self.send_payload(&payload).await {
575 Ok(()) => self.payload_sent_success(&payload),
576 Err(err) => self.log_err(&err),
577 }
578
579 let batch = self.build_observability_batch();
580 if !batch.is_empty() {
581 let payload = data::Payload::MessageBatch(batch);
582 match self.send_payload(&payload).await {
583 Ok(()) => self.payload_sent_success(&payload),
584 Err(err) => self.log_err(&err),
585 }
586 }
587 }
588 Lifecycle(ExtendedHeartbeat) => {
589 let delta = self.build_app_events_batch();
591 if !delta.is_empty() {
592 let payload = data::Payload::MessageBatch(delta);
593 match self.send_payload(&payload).await {
594 Ok(()) => self.payload_sent_success(&payload),
595 Err(err) => self.log_err(&err),
596 }
597 }
598
599 self.data.dependencies.unflush_stored();
600 self.data.integrations.unflush_stored();
601 self.data.configurations.unflush_stored();
602
603 let extended_hb =
604 data::Payload::AppExtendedHeartbeat(self.build_extended_heartbeat());
605 match self.send_payload(&extended_hb).await {
606 Ok(()) => self.payload_sent_success(&extended_hb),
607 Err(err) => self.log_err(&err),
608 }
609
610 if !self.data.products.is_empty() {
611 let products = self
612 .data
613 .products
614 .iter()
615 .map(|(name, state)| (name.clone(), state.clone()))
616 .collect();
617 let product_change =
618 data::Payload::AppProductChange(data::AppProductChange { products });
619 match self.send_payload(&product_change).await {
620 Ok(()) => self.payload_sent_success(&product_change),
621 Err(err) => self.log_err(&err),
622 }
623 }
624 #[allow(clippy::unwrap_used)]
629 self.deadlines
630 .schedule_event(LifecycleAction::ExtendedHeartbeat)
631 .unwrap();
632 }
633 Lifecycle(Stop) => {
634 if !self.data.started {
635 return BREAK;
636 }
637 self.flush_metric_aggregates();
638
639 let mut app_events = self.build_app_events_batch();
640 app_events.extend(self.build_observability_batch());
641 if self.config.emit_app_lifecycle {
642 app_events.push(data::Payload::AppClosing(()));
643 }
644
645 let payload = data::Payload::MessageBatch(app_events);
646 match self.send_payload(&payload).await {
647 Ok(()) => self.payload_sent_success(&payload),
648 Err(err) => self.log_err(&err),
649 }
650
651 self.data.started = false;
652 if !self.config.restartable {
653 self.deadlines.clear_pending();
654 }
655
656 return BREAK;
657 }
658 CollectStats(stats_sender) => {
659 stats_sender.send(self.stats()).ok();
660 }
661 }
662
663 CONTINUE
664 }
665
666 fn build_app_events_batch(&mut self) -> Vec<Payload> {
668 let mut payloads = Vec::new();
669
670 if self.data.dependencies.flush_not_empty() {
671 payloads.push(data::Payload::AppDependenciesLoaded(
672 data::AppDependenciesLoaded {
673 dependencies: self.data.dependencies.unflushed().cloned().collect(),
674 },
675 ))
676 }
677 if self.data.integrations.flush_not_empty() {
678 payloads.push(data::Payload::AppIntegrationsChange(
679 data::AppIntegrationsChange {
680 integrations: self.data.integrations.unflushed().cloned().collect(),
681 },
682 ))
683 }
684 if !self.data.products_pending.is_empty() {
685 let products = self
686 .data
687 .products_pending
688 .iter()
689 .filter_map(|name| {
690 self.data
691 .products
692 .get(name)
693 .map(|state| (name.clone(), state.clone()))
694 })
695 .collect();
696 payloads.push(data::Payload::AppProductChange(data::AppProductChange {
697 products,
698 }))
699 }
700 if self.data.configurations.flush_not_empty() {
701 payloads.push(data::Payload::AppClientConfigurationChange(
702 data::AppClientConfigurationChange {
703 configuration: self.data.configurations.unflushed().cloned().collect(),
704 },
705 ))
706 }
707 if self.data.endpoints.flush_not_empty() {
708 payloads.push(data::Payload::AppEndpoints(data::AppEndpoints {
709 is_first: self.data.endpoints_is_first,
710 endpoints: self
713 .data
714 .endpoints
715 .unflushed()
716 .take(self.config.endpoints_message_limit as usize)
717 .map(|e| e.to_json_value().unwrap_or_default())
718 .filter(|e| e.is_object())
719 .collect(),
720 }));
721 }
722 payloads
723 }
724
725 fn build_observability_batch(&mut self) -> Vec<Payload> {
727 let mut payloads = Vec::new();
728
729 let logs = self.build_logs();
730 if !logs.logs.is_empty() {
731 payloads.push(data::Payload::Logs(logs));
732 }
733 let metrics = self.build_metrics_series();
734 if !metrics.series.is_empty() {
735 payloads.push(data::Payload::GenerateMetrics(metrics))
736 }
737 let distributions = self.build_metrics_distributions();
738 if !distributions.series.is_empty() {
739 payloads.push(data::Payload::Sketches(distributions))
740 }
741 payloads
742 }
743
744 fn build_metrics_distributions(&mut self) -> data::Distributions {
745 let mut series = Vec::new();
746 let context_guard = self.data.metric_contexts.lock();
747 for (context_key, extra_tags, points) in self.data.metric_buckets.flush_distributions() {
748 let Some(context) = context_guard.read(context_key) else {
749 telemetry_worker_log!(self, ERROR, "Context not found for key {:?}", context_key);
750 continue;
751 };
752 let mut tags = extra_tags;
753 tags.extend(context.tags.iter().cloned());
754 series.push(data::metrics::Distribution {
755 namespace: context.namespace,
756 metric: context.name.clone(),
757 tags,
758 sketch: data::metrics::SerializedSketch::B64 {
759 sketch_b64: base64::Engine::encode(
760 &base64::engine::general_purpose::STANDARD,
761 points.encode_to_vec(),
762 ),
763 },
764 common: context.common,
765 _type: context.metric_type,
766 interval: self.metrics_flush_interval.as_secs(),
767 });
768 }
769 data::Distributions { series }
770 }
771
772 fn build_metrics_series(&mut self) -> data::GenerateMetrics {
773 let mut series = Vec::new();
774 let context_guard = self.data.metric_contexts.lock();
775 for (context_key, extra_tags, points) in self.data.metric_buckets.flush_series() {
776 let Some(context) = context_guard.read(context_key) else {
777 telemetry_worker_log!(self, ERROR, "Context not found for key {:?}", context_key);
778 continue;
779 };
780
781 let mut tags = extra_tags;
782 tags.extend(context.tags.iter().cloned());
783 series.push(data::metrics::Serie {
784 namespace: context.namespace,
785 metric: context.name.clone(),
786 tags,
787 points,
788 common: context.common,
789 _type: context.metric_type,
790 interval: self.metrics_flush_interval.as_secs(),
791 });
792 }
793
794 data::GenerateMetrics { series }
795 }
796
797 fn build_app_started(&mut self) -> data::AppStarted {
798 data::AppStarted {
801 configuration: self.data.configurations.unflushed().cloned().collect(),
802 dependencies: Vec::new(),
803 integrations: Vec::new(),
804 install_signature: self.data.install_signature.clone(),
805 products: self.data.products.clone(),
806 error: None,
807 }
808 }
809
810 fn build_extended_heartbeat(&mut self) -> data::AppStarted {
811 data::AppStarted {
812 configuration: self.data.configurations.unflushed().cloned().collect(),
813 dependencies: self.data.dependencies.unflushed().cloned().collect(),
814 integrations: self.data.integrations.unflushed().cloned().collect(),
815 install_signature: self.data.install_signature.clone(),
816 products: self.data.products.clone(),
817 error: None,
818 }
819 }
820
821 fn app_started_sent_success(&mut self, p: &data::AppStarted) {
822 self.data
823 .configurations
824 .removed_flushed(p.configuration.len());
825 self.data.dependencies.removed_flushed(p.dependencies.len());
826 self.data.integrations.removed_flushed(p.integrations.len());
827 self.data.products_pending.clear();
828 }
829
830 fn payload_sent_success(&mut self, payload: &data::Payload) {
831 use data::Payload::*;
832 match payload {
833 AppStarted(p) => self.app_started_sent_success(p),
834 AppExtendedHeartbeat(p) => self.app_started_sent_success(p),
835 AppDependenciesLoaded(p) => {
836 self.data.dependencies.removed_flushed(p.dependencies.len())
837 }
838 AppIntegrationsChange(p) => {
839 self.data.integrations.removed_flushed(p.integrations.len())
840 }
841 AppProductChange(p) => {
842 for name in p.products.keys() {
843 self.data.products_pending.remove(name);
844 }
845 }
846 AppClientConfigurationChange(p) => self
847 .data
848 .configurations
849 .removed_flushed(p.configuration.len()),
850 AppEndpoints(p) => {
851 self.data.endpoints.removed_flushed(p.endpoints.len());
854 self.data.endpoints_is_first = false;
855 }
856 MessageBatch(batch) => {
857 for p in batch {
858 self.payload_sent_success(p);
859 }
860 }
861 Logs(p) => {
862 for _ in &p.logs {
863 self.data.logs.pop_front();
864 }
865 }
866 AppHeartbeat(()) | AppClosing(()) => {}
867 GenerateMetrics(_) | Sketches(_) => {}
868 }
869 }
870
871 fn build_logs(&self) -> data::Logs {
872 let logs = self.data.logs.iter().map(|(_, l)| l.clone()).collect();
874 data::Logs { logs }
875 }
876
877 fn next_seq_id(&self) -> u64 {
878 self.seq_id.fetch_add(1, Ordering::Release)
879 }
880
881 async fn send_payload(&self, payload: &data::Payload) -> anyhow::Result<()> {
882 debug!(
883 worker.runtime_id = %self.runtime_id,
884 payload.type = payload.request_type(),
885 seq_id = self.seq_id.load(Ordering::Acquire),
886 "Sending telemetry payload"
887 );
888 let req = self.build_request(payload)?;
889 let result = self.send_request(req).await;
890 match &result {
891 Ok(resp) => debug!(
892 worker.runtime_id = %self.runtime_id,
893 payload.type = payload.request_type(),
894 response.status = resp.status().as_u16(),
895 "Successfully sent telemetry payload"
896 ),
897 Err(e) => debug!(
898 worker.runtime_id = %self.runtime_id,
899 payload.type = payload.request_type(),
900 error = ?e,
901 "Failed to send telemetry payload"
902 ),
903 }
904 Ok(())
905 }
906
907 fn build_request(&self, payload: &data::Payload) -> anyhow::Result<http::Request<Bytes>> {
908 let seq_id = self.next_seq_id();
909 let tel = Telemetry {
910 api_version: data::ApiVersion::V2,
911 tracer_time: time::SystemTime::UNIX_EPOCH
912 .elapsed()
913 .map_or(0, |d| d.as_secs()),
914 runtime_id: &self.runtime_id,
915 seq_id,
916 host: &self.data.host,
917 origin: None,
918 application: &self.data.app,
919 payload,
920 };
921
922 telemetry_worker_log!(self, DEBUG, "Prepared payload: {:?}", tel);
923
924 let req = http_client::request_builder(&self.config)?
925 .method(http::Method::POST)
926 .header(header::CONTENT_TYPE, serialize::CONTENT_TYPE_VALUE)
927 .header(
928 http_client::header::REQUEST_TYPE,
929 HeaderValue::from_static(payload.request_type()),
930 )
931 .header(
932 http_client::header::API_VERSION,
933 HeaderValue::from_static(data::ApiVersion::V2.to_str()),
934 )
935 .header(
936 http_client::header::LIBRARY_LANGUAGE,
937 tel.application.language_name.clone(),
938 )
939 .header(
940 http_client::header::LIBRARY_VERSION,
941 tel.application.tracer_version.clone(),
942 );
943 let req = http_client::add_instrumentation_session_headers(
944 req,
945 self.config.session_id.as_deref(),
946 self.config.parent_session_id.as_deref(),
947 self.config.root_session_id.as_deref(),
948 );
949
950 let body = Bytes::from(serialize::serialize(&tel)?);
951 Ok(req.body(body)?)
952 }
953
954 async fn send_request(
955 &self,
956 req: http::Request<Bytes>,
957 ) -> Result<http::Response<Bytes>, HttpError> {
958 let timeout_ms = if let Some(endpoint) = self.config.endpoint.as_ref() {
959 endpoint.timeout_ms
960 } else {
961 libdd_common::Endpoint::DEFAULT_TIMEOUT
962 };
963 let timeout = time::Duration::from_millis(timeout_ms);
964
965 debug!(
966 worker.runtime_id = %self.runtime_id,
967 http.timeout_ms = timeout_ms,
968 "Sending HTTP request"
969 );
970
971 let sleeper = <C as SleepCapability>::new();
972 tokio::select! {
973 _ = self.cancellation_token.cancelled() => {
974 debug!(
975 worker.runtime_id = %self.runtime_id,
976 "Telemetry request cancelled"
977 );
978 Err(HttpError::Other(anyhow::anyhow!("Request cancelled")))
979 },
980 _ = sleeper.sleep(timeout) => {
981 debug!(
982 worker.runtime_id = %self.runtime_id,
983 http.timeout_ms = timeout_ms,
984 "Telemetry request timed out"
985 );
986 Err(HttpError::Other(anyhow::anyhow!("Request timed out")))
987 },
988 r = self.capabilities.request(req) => r,
989 }
990 }
991
992 fn stats(&self) -> TelemetryWorkerStats {
993 TelemetryWorkerStats {
994 dependencies_stored: self.data.dependencies.len_stored() as u32,
995 dependencies_unflushed: self.data.dependencies.len_unflushed() as u32,
996 configurations_stored: self.data.configurations.len_stored() as u32,
997 configurations_unflushed: self.data.configurations.len_unflushed() as u32,
998 integrations_stored: self.data.integrations.len_stored() as u32,
999 integrations_unflushed: self.data.integrations.len_unflushed() as u32,
1000 logs: self.data.logs.len() as u32,
1001 metric_contexts: self.data.metric_contexts.lock().len() as u32,
1002 metric_buckets: self.data.metric_buckets.stats(),
1003 }
1004 }
1005
1006 async fn run_loop(mut self) {
1009 debug!(
1010 worker.flavor = ?self.flavor,
1011 worker.runtime_id = %self.runtime_id,
1012 "Starting telemetry worker"
1013 );
1014
1015 loop {
1016 if self.cancellation_token.is_cancelled() {
1017 debug!(
1018 worker.runtime_id = %self.runtime_id,
1019 "Telemetry worker cancelled, shutting down"
1020 );
1021 return;
1022 }
1023
1024 let action = self.recv_next_action().await;
1025 debug!(
1026 worker.runtime_id = %self.runtime_id,
1027 action = ?action,
1028 "Received telemetry action"
1029 );
1030
1031 let action_result = match self.flavor {
1032 TelemetryWorkerFlavor::Full => self.dispatch_action(action).await,
1033 TelemetryWorkerFlavor::MetricsLogs => {
1034 self.dispatch_metrics_logs_action(action).await
1035 }
1036 };
1037
1038 match action_result {
1039 ControlFlow::Continue(()) => {}
1040 ControlFlow::Break(()) => {
1041 debug!(
1042 worker.runtime_id = %self.runtime_id,
1043 worker.restartable = self.config.restartable,
1044 "Telemetry worker received break signal"
1045 );
1046 if !self.config.restartable {
1047 break;
1048 }
1049 }
1050 };
1051 }
1052
1053 debug!(
1054 worker.runtime_id = %self.runtime_id,
1055 "Telemetry worker stopped"
1056 );
1057 }
1058}
1059
1060#[cfg(not(target_arch = "wasm32"))]
1061#[derive(Debug)]
1062struct InnerTelemetryShutdown {
1063 is_shutdown: Mutex<bool>,
1064 condvar: Condvar,
1065}
1066
1067#[cfg(not(target_arch = "wasm32"))]
1068impl InnerTelemetryShutdown {
1069 fn wait_for_shutdown(&self) {
1070 drop(
1071 #[allow(clippy::unwrap_used)]
1072 self.condvar
1073 .wait_while(self.is_shutdown.lock().unwrap(), |is_shutdown| {
1074 !*is_shutdown
1075 })
1076 .unwrap(),
1077 )
1078 }
1079
1080 #[allow(clippy::unwrap_used)]
1081 fn shutdown_finished(&self) {
1082 *self.is_shutdown.lock().unwrap() = true;
1083 self.condvar.notify_all();
1084 }
1085}
1086
1087pub struct TelemetryWorkerHandle<
1096 C: HttpClientCapability + SleepCapability + MaybeSend + Sync + 'static,
1097> {
1098 sender: mpsc::Sender<TelemetryActions>,
1099 #[cfg(not(target_arch = "wasm32"))]
1100 shutdown: Arc<InnerTelemetryShutdown>,
1101 cancellation_token: CancellationToken,
1102 #[cfg(not(target_arch = "wasm32"))]
1103 runtime: Option<runtime::Handle>,
1104 contexts: MetricContexts,
1105 metric_ring: Arc<MetricRing>,
1107 _phantom: PhantomData<fn() -> C>,
1108}
1109
1110impl<C: HttpClientCapability + SleepCapability + MaybeSend + Sync + 'static> Clone
1111 for TelemetryWorkerHandle<C>
1112{
1113 fn clone(&self) -> Self {
1114 Self {
1115 sender: self.sender.clone(),
1116 #[cfg(not(target_arch = "wasm32"))]
1117 shutdown: self.shutdown.clone(),
1118 cancellation_token: self.cancellation_token.clone(),
1119 #[cfg(not(target_arch = "wasm32"))]
1120 runtime: self.runtime.clone(),
1121 contexts: self.contexts.clone(),
1122 metric_ring: self.metric_ring.clone(),
1123 _phantom: PhantomData,
1124 }
1125 }
1126}
1127
1128impl<C: HttpClientCapability + SleepCapability + MaybeSend + Sync + 'static> Debug
1129 for TelemetryWorkerHandle<C>
1130{
1131 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1132 f.debug_struct("TelemetryWorkerHandle")
1133 .field("sender", &self.sender)
1134 .field("cancellation_token", &self.cancellation_token)
1135 .finish()
1136 }
1137}
1138
1139#[cfg(not(target_arch = "wasm32"))]
1140fn schedule_deferred_cancel<F>(runtime: Option<&runtime::Handle>, future: F)
1141where
1142 F: core::future::Future<Output = ()> + Send + 'static,
1143{
1144 let Some(rt) = runtime else {
1145 tracing::error!("Cannot schedule cancellation deadline: no runtime handle available");
1146 return;
1147 };
1148 rt.spawn(future);
1149}
1150
1151impl<C: HttpClientCapability + SleepCapability + MaybeSend + Sync + 'static>
1152 TelemetryWorkerHandle<C>
1153{
1154 pub fn register_metric_context(
1155 &self,
1156 name: String,
1157 tags: Vec<Tag>,
1158 metric_type: data::metrics::MetricType,
1159 common: bool,
1160 namespace: data::metrics::MetricNamespace,
1161 ) -> ContextKey {
1162 self.contexts
1163 .register_metric_context(name, tags, metric_type, common, namespace)
1164 }
1165
1166 pub fn try_send_msg(&self, msg: TelemetryActions) -> anyhow::Result<()> {
1167 Ok(self.sender.try_send(msg)?)
1168 }
1169
1170 pub async fn send_msg(&self, msg: TelemetryActions) -> anyhow::Result<()> {
1171 Ok(self.sender.send(msg).await?)
1172 }
1173
1174 pub async fn send_msgs<T>(&self, msgs: T) -> anyhow::Result<()>
1175 where
1176 T: IntoIterator<Item = TelemetryActions>,
1177 {
1178 for msg in msgs {
1179 self.sender.send(msg).await?;
1180 }
1181
1182 Ok(())
1183 }
1184
1185 pub async fn send_msg_timeout(
1186 &self,
1187 msg: TelemetryActions,
1188 timeout: time::Duration,
1189 ) -> anyhow::Result<()> {
1190 Ok(self.sender.send_timeout(msg, timeout).await?)
1191 }
1192
1193 pub fn send_start(&self) -> anyhow::Result<()> {
1194 Ok(self
1195 .sender
1196 .try_send(TelemetryActions::Lifecycle(LifecycleAction::Start))?)
1197 }
1198
1199 pub fn send_stop(&self) -> anyhow::Result<()> {
1200 Ok(self
1201 .sender
1202 .try_send(TelemetryActions::Lifecycle(LifecycleAction::Stop))?)
1203 }
1204
1205 #[cfg(not(target_arch = "wasm32"))]
1207 pub fn cancel_requests_with_deadline(&self, deadline: time::Instant) {
1208 let token = self.cancellation_token.clone();
1209 let remaining = deadline.saturating_duration_since(time::Instant::now());
1210 let sleeper = <C as SleepCapability>::new();
1211 let future = async move {
1212 sleeper.sleep(remaining).await;
1213 token.cancel();
1214 };
1215 schedule_deferred_cancel(self.runtime.as_ref(), future);
1216 }
1217
1218 #[cfg(not(target_arch = "wasm32"))]
1221 pub fn wait_for_shutdown_deadline(&self, deadline: time::Instant) {
1222 self.cancel_requests_with_deadline(deadline);
1223 self.wait_for_shutdown()
1224 }
1225
1226 pub fn add_dependency(
1227 &self,
1228 name: String,
1229 version: Option<String>,
1230 metadata: Option<Vec<data::DependencyMetadata>>,
1231 ) -> anyhow::Result<()> {
1232 self.sender
1233 .try_send(TelemetryActions::AddDependency(Dependency {
1234 name,
1235 version,
1236 hash: None,
1237 metadata,
1238 }))?;
1239 Ok(())
1240 }
1241
1242 pub fn add_product_change(
1243 &self,
1244 product: String,
1245 enabled: bool,
1246 version: Option<String>,
1247 ) -> anyhow::Result<()> {
1248 self.sender.try_send(TelemetryActions::AddProductChange((
1249 product,
1250 ProductState {
1251 enabled,
1252 version,
1253 error: None,
1254 },
1255 )))?;
1256 Ok(())
1257 }
1258
1259 pub fn add_integration(
1260 &self,
1261 name: String,
1262 enabled: bool,
1263 version: Option<String>,
1264 compatible: Option<bool>,
1265 auto_enabled: Option<bool>,
1266 error: Option<String>,
1267 ) -> anyhow::Result<()> {
1268 self.sender
1269 .try_send(TelemetryActions::AddIntegration(Integration {
1270 name,
1271 version,
1272 compatible,
1273 enabled,
1274 auto_enabled,
1275 error,
1276 }))?;
1277 Ok(())
1278 }
1279
1280 pub fn add_log<T: Hash>(
1281 &self,
1282 identifier: T,
1283 message: String,
1284 level: data::LogLevel,
1285 stack_trace: Option<String>,
1286 ) -> anyhow::Result<()> {
1287 let mut hasher = DefaultHasher::new();
1288 identifier.hash(&mut hasher);
1289 self.sender.try_send(TelemetryActions::AddLog((
1290 LogIdentifier {
1291 identifier: hasher.finish(),
1292 },
1293 data::Log {
1294 message,
1295 level,
1296 stack_trace,
1297 count: 1,
1298 tags: String::new(),
1299 is_sensitive: false,
1300 is_crash: false,
1301 },
1302 )))?;
1303 Ok(())
1304 }
1305
1306 pub fn add_point(
1307 &self,
1308 value: f64,
1309 context: &ContextKey,
1310 extra_tags: Vec<Tag>,
1311 ) -> anyhow::Result<()> {
1312 self.metric_ring.push(value, *context, extra_tags);
1315 Ok(())
1316 }
1317
1318 #[cfg(not(target_arch = "wasm32"))]
1319 pub fn wait_for_shutdown(&self) {
1320 self.shutdown.wait_for_shutdown();
1321 }
1322
1323 pub fn stats(&self) -> anyhow::Result<oneshot::Receiver<TelemetryWorkerStats>> {
1324 let (sender, receiver) = oneshot::channel();
1325 self.sender
1326 .try_send(TelemetryActions::CollectStats(sender))?;
1327 Ok(receiver)
1328 }
1329}
1330
1331pub const MAX_ITEMS: usize = 5000;
1333
1334#[derive(Debug, Default, Clone, Copy)]
1335pub enum TelemetryWorkerFlavor {
1336 #[default]
1339 Full,
1340 MetricsLogs,
1342}
1343
1344pub struct TelemetryWorkerBuilder {
1345 pub host: Host,
1346 pub application: Application,
1347 pub runtime_id: Option<String>,
1348 pub dependencies: store::Store<data::Dependency, data::DependencyKey>,
1349 pub integrations: store::Store<data::Integration>,
1350 pub configurations: store::Store<data::Configuration>,
1351 pub endpoints: store::Store<data::Endpoint>,
1352 pub native_deps: bool,
1353 pub rust_shared_lib_deps: bool,
1354 pub config: Config,
1355 pub flavor: TelemetryWorkerFlavor,
1356 pub install_signature: Option<data::InstallSignature>,
1357}
1358
1359impl TelemetryWorkerBuilder {
1360 pub fn new_fetch_host(
1362 service_name: String,
1363 language_name: String,
1364 language_version: String,
1365 tracer_version: String,
1366 ) -> Self {
1367 Self {
1368 host: crate::build_host(),
1369 ..Self::new(
1370 String::new(),
1371 service_name,
1372 language_name,
1373 language_version,
1374 tracer_version,
1375 )
1376 }
1377 }
1378
1379 pub fn new(
1381 hostname: String,
1382 service_name: String,
1383 language_name: String,
1384 language_version: String,
1385 tracer_version: String,
1386 ) -> Self {
1387 Self {
1388 host: Host {
1389 hostname,
1390 ..Default::default()
1391 },
1392 application: Application {
1393 service_name,
1394 language_name,
1395 language_version,
1396 tracer_version,
1397 ..Default::default()
1398 },
1399 runtime_id: None,
1400 dependencies: store::Store::new(MAX_ITEMS),
1401 integrations: store::Store::new(MAX_ITEMS),
1402 configurations: store::Store::new(MAX_ITEMS),
1403 endpoints: store::Store::new(10000),
1404 native_deps: true,
1405 rust_shared_lib_deps: false,
1406 config: Config::default(),
1407 flavor: TelemetryWorkerFlavor::default(),
1408 install_signature: None,
1409 }
1410 }
1411
1412 pub fn build_worker<C: HttpClientCapability + SleepCapability + MaybeSend + Sync + 'static>(
1414 self,
1415 #[cfg(not(target_arch = "wasm32"))] tokio_runtime: Option<runtime::Handle>,
1416 ) -> (TelemetryWorkerHandle<C>, TelemetryWorker<C>) {
1417 let (tx, mailbox) = mpsc::channel(5000);
1418 #[cfg(not(target_arch = "wasm32"))]
1419 let shutdown = Arc::new(InnerTelemetryShutdown {
1420 is_shutdown: Mutex::new(false),
1421 condvar: Condvar::new(),
1422 });
1423 let contexts = MetricContexts::default();
1424 let metric_ring = Arc::new(MetricRing::new());
1425 let token = CancellationToken::new();
1426 let config = self.config;
1427 let telemetry_heartbeat_interval = config.telemetry_heartbeat_interval;
1428 let telemetry_extended_heartbeat_interval = config.telemetry_extended_heartbeat_interval;
1429 let capabilities = C::new_without_connection_pooling();
1430
1431 let metrics_flush_interval =
1432 telemetry_heartbeat_interval.min(MetricBuckets::METRICS_FLUSH_INTERVAL);
1433
1434 let worker = TelemetryWorker {
1435 flavor: self.flavor,
1436 data: TelemetryWorkerData {
1437 started: false,
1438 dependencies: self.dependencies,
1439 integrations: self.integrations,
1440 configurations: self.configurations,
1441 endpoints: self.endpoints,
1442 endpoints_is_first: true,
1443 products: std::collections::HashMap::new(),
1444 products_pending: HashSet::new(),
1445 logs: store::QueueHashMap::default(),
1446 metric_contexts: contexts.clone(),
1447 metric_buckets: MetricBuckets::default(),
1448 host: self.host,
1449 app: self.application,
1450 install_signature: self.install_signature,
1451 },
1452 config,
1453 mailbox,
1454 seq_id: AtomicU64::new(1),
1455 runtime_id: self
1456 .runtime_id
1457 .unwrap_or_else(|| uuid::Uuid::new_v4().to_string()),
1458 capabilities,
1459 metrics_flush_interval,
1460 deadlines: scheduler::Scheduler::new(vec![
1461 (metrics_flush_interval, LifecycleAction::FlushMetricAggr),
1462 (telemetry_heartbeat_interval, LifecycleAction::FlushData),
1463 (
1464 telemetry_extended_heartbeat_interval,
1465 LifecycleAction::ExtendedHeartbeat,
1466 ),
1467 ]),
1468 cancellation_token: token.clone(),
1469 next_action: None,
1470 stopped: false,
1471 metric_ring: metric_ring.clone(),
1472 };
1473
1474 (
1475 TelemetryWorkerHandle {
1476 sender: tx,
1477 #[cfg(not(target_arch = "wasm32"))]
1478 shutdown,
1479 cancellation_token: token,
1480 #[cfg(not(target_arch = "wasm32"))]
1481 runtime: tokio_runtime,
1482 contexts,
1483 metric_ring,
1484 _phantom: PhantomData,
1485 },
1486 worker,
1487 )
1488 }
1489
1490 #[cfg(not(target_arch = "wasm32"))]
1492 pub fn spawn<C: HttpClientCapability + SleepCapability + MaybeSend + Sync + 'static>(
1493 self,
1494 ) -> (TelemetryWorkerHandle<C>, JoinHandle<()>) {
1495 let tokio_runtime = tokio::runtime::Handle::current();
1496
1497 let (worker_handle, worker) = self.build_worker::<C>(Some(tokio_runtime.clone()));
1498
1499 let join_handle = tokio_runtime.spawn(async move { worker.run_loop().await });
1500
1501 (worker_handle, join_handle)
1502 }
1503
1504 #[cfg(not(target_arch = "wasm32"))]
1506 pub fn run<C: HttpClientCapability + SleepCapability + MaybeSend + Sync + 'static>(
1507 self,
1508 ) -> anyhow::Result<TelemetryWorkerHandle<C>> {
1509 let runtime = tokio::runtime::Builder::new_current_thread()
1510 .enable_all()
1511 .build()?;
1512 let (handle, worker) = self.build_worker::<C>(Some(runtime.handle().clone()));
1513 let notify_shutdown = handle.shutdown.clone();
1514 std::thread::spawn(move || {
1515 runtime.block_on(worker.run_loop());
1516 runtime.shutdown_background();
1517 notify_shutdown.shutdown_finished();
1518 });
1519
1520 Ok(handle)
1521 }
1522}
1523
1524#[cfg(test)]
1525mod tests {
1526 use crate::config::TelemetryEndpoint;
1527 use crate::data::Payload;
1528 use crate::worker::http_client::header::{
1529 DD_PARENT_SESSION_ID, DD_ROOT_SESSION_ID, DD_SESSION_ID,
1530 };
1531 use crate::worker::{
1532 LifecycleAction, TelemetryActions, TelemetryWorker, TelemetryWorkerBuilder,
1533 TelemetryWorkerFlavor, TelemetryWorkerHandle,
1534 };
1535 use libdd_capabilities_impl::NativeCapabilities;
1536 use tokio::runtime::Runtime;
1537
1538 fn is_send<T: Send>(_: T) {}
1539 fn is_sync<T: Sync>(_: T) {}
1540
1541 #[test]
1542 fn test_handle_sync_send() {
1543 #[allow(clippy::redundant_closure)]
1544 let _ = |h: TelemetryWorkerHandle<NativeCapabilities>| is_send(h);
1545 #[allow(clippy::redundant_closure)]
1546 let _ = |h: TelemetryWorkerHandle<NativeCapabilities>| is_sync(h);
1547 }
1548
1549 fn test_worker(
1550 session_id: Option<String>,
1551 root_session_id: Option<String>,
1552 parent_session_id: Option<String>,
1553 ) -> TelemetryWorker<NativeCapabilities> {
1554 let mut b = TelemetryWorkerBuilder::new(
1555 "h".into(),
1556 "svc".into(),
1557 "lang".into(),
1558 "1".into(),
1559 "tv".into(),
1560 );
1561 b.config
1562 .set_endpoint(TelemetryEndpoint {
1563 url: Some("http://127.0.0.1:1".to_owned()),
1564 ..Default::default()
1565 })
1566 .unwrap();
1567 b.runtime_id = Some("rid".into());
1568 b.config.session_id = session_id;
1569 b.config.parent_session_id = parent_session_id;
1570 b.config.root_session_id = root_session_id;
1571 let rt = Runtime::new().unwrap();
1572 b.build_worker::<NativeCapabilities>(Some(rt.handle().clone()))
1573 .1
1574 }
1575
1576 #[cfg_attr(miri, ignore)] #[test]
1578 fn telemetry_http_includes_dd_session_id() {
1579 let req = test_worker(Some("sess".into()), None, None)
1580 .build_request(&Payload::AppHeartbeat(()))
1581 .unwrap();
1582 assert_eq!(
1583 req.headers().get(DD_SESSION_ID).unwrap().to_str().unwrap(),
1584 "sess"
1585 );
1586 assert!(req.headers().get(DD_ROOT_SESSION_ID).is_none());
1587 assert!(req.headers().get(DD_PARENT_SESSION_ID).is_none());
1588 }
1589
1590 #[cfg_attr(miri, ignore)] #[test]
1592 fn telemetry_http_omits_root_session_id_when_same_as_session_id() {
1593 let req = test_worker(
1594 Some("sess-id".into()),
1595 Some("sess-id".into()),
1596 Some("parent".into()),
1597 )
1598 .build_request(&Payload::AppHeartbeat(()))
1599 .unwrap();
1600 assert_eq!(
1601 req.headers().get(DD_SESSION_ID).unwrap().to_str().unwrap(),
1602 "sess-id"
1603 );
1604 assert!(req.headers().get(DD_ROOT_SESSION_ID).is_none());
1605 assert_eq!(
1606 req.headers()
1607 .get(DD_PARENT_SESSION_ID)
1608 .unwrap()
1609 .to_str()
1610 .unwrap(),
1611 "parent"
1612 );
1613 }
1614
1615 #[cfg_attr(miri, ignore)] #[test]
1617 fn telemetry_http_omits_parent_session_id_when_same_as_session_id() {
1618 let req = test_worker(
1619 Some("sess-id".into()),
1620 Some("root".into()),
1621 Some("sess-id".into()),
1622 )
1623 .build_request(&Payload::AppHeartbeat(()))
1624 .unwrap();
1625 assert_eq!(
1626 req.headers().get(DD_SESSION_ID).unwrap().to_str().unwrap(),
1627 "sess-id"
1628 );
1629 assert_eq!(
1630 req.headers()
1631 .get(DD_ROOT_SESSION_ID)
1632 .unwrap()
1633 .to_str()
1634 .unwrap(),
1635 "root"
1636 );
1637 assert!(req.headers().get(DD_PARENT_SESSION_ID).is_none());
1638 }
1639
1640 #[cfg_attr(miri, ignore)] #[test]
1642 fn telemetry_http_omits_session_family_without_valid_session_id() {
1643 let assert_no_session_headers = |req: &http::Request<bytes::Bytes>| {
1644 assert!(req.headers().get(DD_SESSION_ID).is_none());
1645 assert!(req.headers().get(DD_ROOT_SESSION_ID).is_none());
1646 assert!(req.headers().get(DD_PARENT_SESSION_ID).is_none());
1647 };
1648
1649 let req = test_worker(None, Some("root".into()), Some("parent".into()))
1650 .build_request(&Payload::AppHeartbeat(()))
1651 .unwrap();
1652 assert_no_session_headers(&req);
1653
1654 let req = test_worker(
1655 Some(String::new()),
1656 Some("root".into()),
1657 Some("parent".into()),
1658 )
1659 .build_request(&Payload::AppHeartbeat(()))
1660 .unwrap();
1661 assert_no_session_headers(&req);
1662 }
1663
1664 #[cfg_attr(miri, ignore)] #[test]
1666 fn telemetry_http_includes_dd_session_root_and_parent_session_ids() {
1667 let req = test_worker(
1668 Some("sess".into()),
1669 Some("root".into()),
1670 Some("parent".into()),
1671 )
1672 .build_request(&Payload::AppHeartbeat(()))
1673 .unwrap();
1674 assert_eq!(
1675 req.headers().get(DD_SESSION_ID).unwrap().to_str().unwrap(),
1676 "sess"
1677 );
1678 assert_eq!(
1679 req.headers()
1680 .get(DD_ROOT_SESSION_ID)
1681 .unwrap()
1682 .to_str()
1683 .unwrap(),
1684 "root"
1685 );
1686 assert_eq!(
1687 req.headers()
1688 .get(DD_PARENT_SESSION_ID)
1689 .unwrap()
1690 .to_str()
1691 .unwrap(),
1692 "parent"
1693 );
1694 }
1695
1696 fn build_test_worker_with_flavor(
1697 flavor: TelemetryWorkerFlavor,
1698 ) -> TelemetryWorker<NativeCapabilities> {
1699 let mut b = TelemetryWorkerBuilder::new(
1700 "h".into(),
1701 "svc".into(),
1702 "lang".into(),
1703 "1".into(),
1704 "tv".into(),
1705 );
1706 b.config
1707 .set_endpoint(TelemetryEndpoint {
1708 url: Some("http://127.0.0.1:1".to_owned()),
1709 ..Default::default()
1710 })
1711 .unwrap();
1712 b.runtime_id = Some("rid".into());
1713 b.flavor = flavor;
1714 b.build_worker::<NativeCapabilities>(Some(tokio::runtime::Handle::current()))
1715 .1
1716 }
1717
1718 #[tokio::test]
1722 #[cfg_attr(miri, ignore)] async fn endpoints_message_limit_chunks_payloads_and_flags_only_the_first() {
1724 let mut worker = build_test_worker_with_flavor(TelemetryWorkerFlavor::Full);
1725 worker.config.endpoints_message_limit = 2;
1726
1727 for i in 0..5 {
1728 worker.data.endpoints.insert(crate::data::Endpoint {
1729 operation_name: "http.request".to_string(),
1730 resource_name: format!("GET /r{i}"),
1731 ..Default::default()
1732 });
1733 }
1734
1735 let mut chunks = Vec::new();
1736 while worker.data.endpoints.flush_not_empty() {
1738 let payloads = worker.build_app_events_batch();
1739 let endpoints = payloads
1740 .iter()
1741 .find_map(|p| match p {
1742 crate::data::Payload::AppEndpoints(e) => Some(e),
1743 _ => None,
1744 })
1745 .expect("an app-endpoints payload while endpoints are queued");
1746 chunks.push((endpoints.is_first, endpoints.endpoints.len()));
1747 let sent = crate::data::Payload::AppEndpoints(crate::data::AppEndpoints {
1748 is_first: endpoints.is_first,
1749 endpoints: endpoints.endpoints.clone(),
1750 });
1751 worker.payload_sent_success(&sent);
1752 }
1753
1754 assert_eq!(
1755 chunks,
1756 vec![(true, 2), (false, 2), (false, 1)],
1757 "5 endpoints at a limit of 2 should be 2+2+1 with is_first only on the first payload"
1758 );
1759 }
1760
1761 #[tokio::test]
1765 #[cfg_attr(miri, ignore)] async fn full_flavor_start_schedules_every_periodic_action() {
1767 let mut worker = build_test_worker_with_flavor(TelemetryWorkerFlavor::Full);
1768
1769 let _ = worker
1770 .dispatch_action(TelemetryActions::Lifecycle(LifecycleAction::Start))
1771 .await;
1772
1773 let delays: Vec<LifecycleAction> =
1774 worker.deadlines.delays.iter().map(|(_, k)| *k).collect();
1775 let scheduled: Vec<LifecycleAction> =
1776 worker.deadlines.deadlines.iter().map(|(_, k)| *k).collect();
1777
1778 assert!(!delays.is_empty(), "scheduler should have periodic actions");
1779 for ev in &delays {
1780 assert!(
1781 scheduled.contains(ev),
1782 "{ev:?} has a delay but was not scheduled on Start; scheduled={scheduled:?}",
1783 );
1784 }
1785 }
1786
1787 #[tokio::test]
1790 #[cfg_attr(miri, ignore)] async fn metrics_logs_flavor_start_does_not_schedule_extended_heartbeat() {
1792 let mut worker = build_test_worker_with_flavor(TelemetryWorkerFlavor::MetricsLogs);
1793
1794 let _ = worker
1795 .dispatch_metrics_logs_action(TelemetryActions::Lifecycle(LifecycleAction::Start))
1796 .await;
1797
1798 let scheduled: Vec<LifecycleAction> =
1799 worker.deadlines.deadlines.iter().map(|(_, k)| *k).collect();
1800
1801 assert!(scheduled.contains(&LifecycleAction::FlushMetricAggr));
1802 assert!(scheduled.contains(&LifecycleAction::FlushData));
1803 assert!(
1804 !scheduled.contains(&LifecycleAction::ExtendedHeartbeat),
1805 "MetricsLogs should not schedule ExtendedHeartbeat; scheduled={scheduled:?}",
1806 );
1807 }
1808
1809 #[tokio::test]
1814 #[cfg_attr(miri, ignore)] async fn extended_heartbeat_does_not_reset_flush_data() {
1816 let mut worker = build_test_worker_with_flavor(TelemetryWorkerFlavor::Full);
1817
1818 let _ = worker
1819 .dispatch_action(TelemetryActions::Lifecycle(LifecycleAction::Start))
1820 .await;
1821
1822 let flush_data_before = worker
1823 .deadlines
1824 .deadlines
1825 .iter()
1826 .find(|(_, k)| *k == LifecycleAction::FlushData)
1827 .map(|(d, _)| *d)
1828 .expect("FlushData scheduled on Start");
1829
1830 let _ = worker
1831 .dispatch_action(TelemetryActions::Lifecycle(
1832 LifecycleAction::ExtendedHeartbeat,
1833 ))
1834 .await;
1835
1836 let flush_data_after = worker
1837 .deadlines
1838 .deadlines
1839 .iter()
1840 .find(|(_, k)| *k == LifecycleAction::FlushData)
1841 .map(|(d, _)| *d)
1842 .expect("FlushData should still be scheduled after ExtendedHeartbeat fires");
1843
1844 assert_eq!(
1845 flush_data_before, flush_data_after,
1846 "ExtendedHeartbeat must not reset FlushData's deadline",
1847 );
1848 }
1849
1850 #[cfg_attr(miri, ignore)]
1856 #[tokio::test]
1857 async fn app_started_omits_dependencies_and_integrations() {
1858 let mut worker = build_test_worker_with_flavor(TelemetryWorkerFlavor::Full);
1859
1860 let _ = worker
1861 .dispatch_action(TelemetryActions::AddDependency(crate::data::Dependency {
1862 name: "monolog/monolog".into(),
1863 version: Some("3.5.0".into()),
1864 ..Default::default()
1865 }))
1866 .await;
1867 let _ = worker
1868 .dispatch_action(TelemetryActions::AddIntegration(crate::data::Integration {
1869 name: "curl".into(),
1870 enabled: true,
1871 ..Default::default()
1872 }))
1873 .await;
1874
1875 let app_started = worker.build_app_started();
1876 assert!(
1877 app_started.dependencies.is_empty(),
1878 "app-started must not carry dependencies; the intake rejects the whole payload",
1879 );
1880 assert!(
1881 app_started.integrations.is_empty(),
1882 "app-started must not carry integrations; the intake rejects the whole payload",
1883 );
1884
1885 let batch = worker.build_app_events_batch();
1887 assert!(
1888 batch.iter().any(|p| matches!(
1889 p,
1890 crate::data::Payload::AppDependenciesLoaded(d) if !d.dependencies.is_empty()
1891 )),
1892 "dependencies registered before Start must still be reported via \
1893 app-dependencies-loaded, got {batch:?}",
1894 );
1895 assert!(
1896 batch.iter().any(|p| matches!(
1897 p,
1898 crate::data::Payload::AppIntegrationsChange(i) if !i.integrations.is_empty()
1899 )),
1900 "integrations registered before Start must still be reported via \
1901 app-integrations-change, got {batch:?}",
1902 );
1903 }
1904
1905 #[cfg_attr(miri, ignore)]
1908 #[tokio::test]
1909 async fn extended_heartbeat_carries_dependencies_and_integrations() {
1910 let mut worker = build_test_worker_with_flavor(TelemetryWorkerFlavor::Full);
1911
1912 let _ = worker
1913 .dispatch_action(TelemetryActions::AddDependency(crate::data::Dependency {
1914 name: "monolog/monolog".into(),
1915 version: Some("3.5.0".into()),
1916 ..Default::default()
1917 }))
1918 .await;
1919 let _ = worker
1920 .dispatch_action(TelemetryActions::AddIntegration(crate::data::Integration {
1921 name: "curl".into(),
1922 enabled: true,
1923 ..Default::default()
1924 }))
1925 .await;
1926
1927 let hb = worker.build_extended_heartbeat();
1928 assert_eq!(1, hb.dependencies.len(), "{hb:?}");
1929 assert_eq!(1, hb.integrations.len(), "{hb:?}");
1930 }
1931
1932 mod reset {
1933 use super::super::*;
1934 use crate::data::{
1935 metrics::{MetricNamespace, MetricType},
1936 Configuration, ConfigurationOrigin, Dependency, Endpoint, Integration, Log, LogLevel,
1937 };
1938 use libdd_capabilities_impl::NativeCapabilities;
1939 use libdd_shared_runtime::Worker;
1940
1941 fn build_test_worker() -> (
1942 TelemetryWorkerHandle<NativeCapabilities>,
1943 TelemetryWorker<NativeCapabilities>,
1944 ) {
1945 let builder = TelemetryWorkerBuilder::new(
1946 "hostname".to_string(),
1947 "test-service".to_string(),
1948 "rust".to_string(),
1949 "1.0.0".to_string(),
1950 "1.0.0".to_string(),
1951 );
1952 builder.build_worker::<NativeCapabilities>(Some(tokio::runtime::Handle::current()))
1954 }
1955
1956 fn make_log(id: u64, message: &str) -> (LogIdentifier, Log) {
1957 (
1958 LogIdentifier { identifier: id },
1959 Log {
1960 message: message.to_string(),
1961 level: LogLevel::Warn,
1962 stack_trace: None,
1963 count: 1,
1964 tags: String::new(),
1965 is_sensitive: false,
1966 is_crash: false,
1967 },
1968 )
1969 }
1970
1971 #[cfg_attr(miri, ignore)] #[tokio::test]
1974 async fn test_reset_clears_buffered_data() {
1975 let (handle, mut worker) = build_test_worker();
1976
1977 worker.data.dependencies.insert(Dependency {
1979 name: "dep".to_string(),
1980 ..Default::default()
1981 });
1982 worker.data.integrations.insert(Integration {
1983 name: "integration".to_string(),
1984 version: None,
1985 enabled: true,
1986 compatible: None,
1987 auto_enabled: None,
1988 ..Default::default()
1989 });
1990 worker.data.configurations.insert(Configuration {
1991 name: "cfg".to_string(),
1992 value: Some("true".to_string()),
1993 origin: ConfigurationOrigin::Code,
1994 config_id: None,
1995 seq_id: None,
1996 });
1997 worker.data.endpoints.insert(Endpoint {
1998 operation_name: "GET /health".to_string(),
1999 resource_name: "/health".to_string(),
2000 ..Default::default()
2001 });
2002 let (id, log) = make_log(42, "msg");
2003 worker.data.logs.get_mut_or_insert(id, log);
2004
2005 let key = handle.register_metric_context(
2007 "test.metric".to_string(),
2008 vec![],
2009 MetricType::Count,
2010 false,
2011 MetricNamespace::Tracers,
2012 );
2013 worker.data.metric_buckets.add_point(key, 1.0, vec![]);
2014
2015 worker.reset();
2016
2017 let stats = worker.stats();
2018 assert_eq!(
2019 stats.dependencies_stored, 0,
2020 "dependency dedupe history should be cleared"
2021 );
2022 assert_eq!(
2023 stats.dependencies_unflushed, 0,
2024 "dependency pending queue should be cleared"
2025 );
2026 assert_eq!(
2027 stats.integrations_stored, 0,
2028 "integration dedupe history should be cleared"
2029 );
2030 assert_eq!(
2031 stats.integrations_unflushed, 0,
2032 "integration pending queue should be cleared"
2033 );
2034 assert_eq!(
2035 stats.configurations_stored, 0,
2036 "configuration dedupe history should be cleared"
2037 );
2038 assert_eq!(
2039 stats.configurations_unflushed, 0,
2040 "configuration pending queue should be cleared"
2041 );
2042 assert_eq!(stats.logs, 0, "logs should be cleared");
2043 assert_eq!(
2044 stats.metric_buckets.buckets, 0,
2045 "metric buckets should be cleared"
2046 );
2047 assert_eq!(
2048 stats.metric_buckets.series, 0,
2049 "metric series should be cleared"
2050 );
2051 assert_eq!(
2052 worker.data.endpoints.len_stored(),
2053 0,
2054 "endpoints should be cleared"
2055 );
2056 assert!(
2057 worker.data.endpoints_is_first,
2058 "the child's first app-endpoints payload is a first one again"
2059 );
2060 assert!(worker.next_action.is_none(), "next_action should be None");
2061 }
2062
2063 #[cfg_attr(miri, ignore)] #[tokio::test]
2066 async fn test_reset_drains_mailbox() {
2067 let (handle, mut worker) = build_test_worker();
2068
2069 handle
2071 .try_send_msg(TelemetryActions::AddDependency(Dependency {
2072 name: "dep".to_string(),
2073 ..Default::default()
2074 }))
2075 .unwrap();
2076 let (id, log) = make_log(1, "pre-fork log");
2077 handle
2078 .try_send_msg(TelemetryActions::AddLog((id, log)))
2079 .unwrap();
2080
2081 worker.next_action = Some(TelemetryActions::Lifecycle(LifecycleAction::Start));
2083
2084 worker.reset();
2085
2086 assert!(
2088 worker.mailbox.try_recv().is_err(),
2089 "mailbox should be empty"
2090 );
2091 assert!(worker.next_action.is_none(), "next_action should be None");
2092 let stats = worker.stats();
2094 assert_eq!(
2095 stats.dependencies_stored, 0,
2096 "queued AddDependency must not be applied"
2097 );
2098 assert_eq!(
2099 stats.dependencies_unflushed, 0,
2100 "queued AddDependency must not be pending"
2101 );
2102 assert_eq!(stats.logs, 0, "queued AddLog must be discarded");
2103 }
2104
2105 #[cfg_attr(miri, ignore)] #[tokio::test]
2108 async fn test_worker_accepts_new_data_after_reset() {
2109 let (handle, mut worker) = build_test_worker();
2110 worker.flavor = TelemetryWorkerFlavor::MetricsLogs;
2111
2112 let (id, log) = make_log(99, "pre-fork");
2114 worker.data.logs.get_mut_or_insert(id, log);
2115
2116 worker.reset();
2117
2118 let (id2, log2) = make_log(1, "post-fork");
2120 handle
2121 .try_send_msg(TelemetryActions::AddLog((id2, log2)))
2122 .unwrap();
2123
2124 worker.trigger().await;
2126 worker.run().await;
2127
2128 let stats = worker.stats();
2129 assert_eq!(stats.logs, 1, "only post-fork log should be present");
2131 }
2132
2133 #[cfg_attr(miri, ignore)] #[tokio::test]
2136 async fn test_reset_preserves_started_and_deadlines() {
2137 let (_handle, mut worker) = build_test_worker();
2138
2139 worker.data.started = true;
2140 worker
2141 .deadlines
2142 .schedule_event(LifecycleAction::FlushMetricAggr)
2143 .unwrap();
2144 worker
2145 .deadlines
2146 .schedule_event(LifecycleAction::FlushData)
2147 .unwrap();
2148
2149 let deadlines_before = worker.deadlines.deadlines.clone();
2150
2151 worker.reset();
2152
2153 assert!(worker.data.started, "started flag should be preserved");
2154 assert_eq!(
2155 worker.deadlines.deadlines.len(),
2156 deadlines_before.len(),
2157 "scheduled deadlines should be preserved"
2158 );
2159 for ((_, actual), (_, expected)) in worker
2160 .deadlines
2161 .deadlines
2162 .iter()
2163 .zip(deadlines_before.iter())
2164 {
2165 assert_eq!(
2166 actual, expected,
2167 "deadline kinds should be preserved across reset"
2168 );
2169 }
2170 }
2171 }
2172
2173 #[cfg_attr(miri, ignore)]
2174 #[test]
2175 fn test_channel_close_flushes_and_parks_via_shared_runtime() {
2176 use httpmock::prelude::*;
2177 use libdd_shared_runtime::{BlockingRuntime, ForkSafeRuntime, SharedRuntime};
2178 use std::time::Duration;
2179
2180 const TELEMETRY_PATH: &str = "/telemetry/proxy/api/v2/apmtelemetry";
2181
2182 let server = MockServer::start();
2183 let mock = server.mock(|when, then| {
2184 when.method(POST).path(TELEMETRY_PATH);
2185 then.status(202).body("");
2186 });
2187
2188 let mut builder = TelemetryWorkerBuilder::new(
2189 "host".into(),
2190 "svc".into(),
2191 "lang".into(),
2192 "1".into(),
2193 "tv".into(),
2194 );
2195 builder
2196 .config
2197 .set_endpoint(TelemetryEndpoint {
2198 url: Some(server.url("/")),
2199 ..Default::default()
2200 })
2201 .unwrap();
2202 builder.runtime_id = Some("rid".into());
2203
2204 let shared_runtime = ForkSafeRuntime::new().expect("ForkSafeRuntime::new");
2205 let runtime_handle = shared_runtime
2206 .block_on(async { tokio::runtime::Handle::current() })
2207 .expect("runtime handle");
2208 let (telemetry_handle, worker) =
2209 builder.build_worker::<NativeCapabilities>(Some(runtime_handle));
2210
2211 let _worker_handle = shared_runtime
2212 .spawn_worker(worker, false)
2213 .expect("spawn_worker");
2214
2215 telemetry_handle.send_start().expect("send_start");
2217
2218 for _ in 0..50 {
2221 if mock.calls() >= 1 {
2222 break;
2223 }
2224 std::thread::sleep(Duration::from_millis(20));
2225 }
2226 assert!(
2227 mock.calls() >= 1,
2228 "worker should POST at least once after Start"
2229 );
2230
2231 let hits_before_close = mock.calls();
2233 drop(telemetry_handle);
2234
2235 for _ in 0..50 {
2237 if mock.calls() > hits_before_close {
2238 break;
2239 }
2240 std::thread::sleep(Duration::from_millis(20));
2241 }
2242 assert!(
2243 mock.calls() > hits_before_close,
2244 "worker should flush a final batch after the channel is closed"
2245 );
2246
2247 let stable_hits = mock.calls();
2250 std::thread::sleep(Duration::from_millis(300));
2251 assert_eq!(
2252 mock.calls(),
2253 stable_hits,
2254 "worker must stop POSTing after parking; observed {} extra hits",
2255 mock.calls().saturating_sub(stable_hits),
2256 );
2257 }
2258
2259 #[cfg_attr(miri, ignore)]
2263 #[tokio::test]
2264 async fn shutdown_drains_pending_actions_before_stop() {
2265 use crate::data::metrics::{MetricNamespace, MetricType};
2266 use httpmock::prelude::*;
2267
2268 const TELEMETRY_PATH: &str = "/telemetry/proxy/api/v2/apmtelemetry";
2269 const METRIC_NAME: &str = "regression.drain_before_stop";
2270
2271 let server = MockServer::start();
2272 let metric_mock = server.mock(|when, then| {
2273 when.method(POST)
2274 .path(TELEMETRY_PATH)
2275 .body_includes(format!(r#""metric":"{METRIC_NAME}""#));
2276 then.status(202).body("");
2277 });
2278 let _lifecycle = server.mock(|when, then| {
2280 when.method(POST).path(TELEMETRY_PATH);
2281 then.status(202).body("");
2282 });
2283
2284 let mut builder = TelemetryWorkerBuilder::new(
2285 "host".into(),
2286 "svc".into(),
2287 "lang".into(),
2288 "1".into(),
2289 "tv".into(),
2290 );
2291 builder
2292 .config
2293 .set_endpoint(TelemetryEndpoint {
2294 url: Some(server.url("/")),
2295 ..Default::default()
2296 })
2297 .unwrap();
2298 builder.runtime_id = Some("rid".into());
2299 let (handle, mut worker) =
2300 builder.build_worker::<NativeCapabilities>(Some(tokio::runtime::Handle::current()));
2301
2302 let context = handle.register_metric_context(
2303 METRIC_NAME.into(),
2304 Vec::new(),
2305 MetricType::Count,
2306 false,
2307 MetricNamespace::Tracers,
2308 );
2309
2310 let _ = worker
2312 .dispatch_action(TelemetryActions::Lifecycle(LifecycleAction::Start))
2313 .await;
2314
2315 handle
2316 .add_point(1.0, &context, Vec::new())
2317 .expect("add_point");
2318
2319 <TelemetryWorker<_> as libdd_shared_runtime::Worker>::shutdown(&mut worker).await;
2320
2321 metric_mock.assert_calls(1);
2322 }
2323}