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