Skip to main content

libdd_telemetry/worker/
mod.rs

1// Copyright 2021-Present Datadog, Inc. https://www.datadoghq.com/
2// SPDX-License-Identifier: Apache-2.0
3
4pub mod http_client;
5mod scheduler;
6pub mod store;
7
8use crate::{
9    config::Config,
10    data::{self, Application, Dependency, Endpoint, Host, Integration, Log, Payload, Telemetry},
11    metrics::{ContextKey, MetricBuckets, MetricContexts},
12};
13
14use async_trait::async_trait;
15use libdd_common::{http_common, tag::Tag};
16use libdd_shared_runtime::Worker;
17
18use std::iter::Sum;
19use std::ops::Add;
20use std::{
21    collections::hash_map::DefaultHasher,
22    hash::{Hash, Hasher},
23    ops::ControlFlow,
24    sync::{
25        atomic::{AtomicU64, Ordering},
26        Arc, Condvar, Mutex,
27    },
28    time,
29};
30use std::{collections::HashSet, fmt::Debug, time::Duration};
31
32use crate::metrics::MetricBucketStats;
33use futures::{
34    channel::oneshot,
35    future::{self},
36};
37use http::{header, HeaderValue};
38use serde::{Deserialize, Serialize};
39use tokio::{
40    runtime::{self, Handle},
41    sync::mpsc,
42    task::JoinHandle,
43};
44use tokio_util::sync::CancellationToken;
45use tracing::debug;
46
47const CONTINUE: ControlFlow<()> = ControlFlow::Continue(());
48const BREAK: ControlFlow<()> = ControlFlow::Break(());
49
50fn time_now() -> f64 {
51    #[allow(clippy::unwrap_used)]
52    std::time::SystemTime::UNIX_EPOCH
53        .elapsed()
54        .unwrap_or_default()
55        .as_secs_f64()
56}
57
58macro_rules! telemetry_worker_log {
59    ($worker:expr , ERROR , $fmt_str:tt, $($arg:tt)*) => {
60        {
61            debug!(
62                worker.runtime_id = %$worker.runtime_id,
63                worker.debug_logging = $worker.config.telemetry_debug_logging_enabled,
64                $fmt_str,
65                $($arg)*
66            );
67            if $worker.config.telemetry_debug_logging_enabled {
68                eprintln!(concat!("{}: Telemetry worker ERROR: ", $fmt_str), time_now(), $($arg)*);
69            }
70        }
71    };
72    ($worker:expr , DEBUG , $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                println!(concat!("{}: Telemetry worker DEBUG: ", $fmt_str), time_now(), $($arg)*);
82            }
83        }
84    };
85}
86
87#[derive(Debug, Serialize, Deserialize)]
88pub enum TelemetryActions {
89    AddPoint((f64, ContextKey, Vec<Tag>)),
90    AddConfig(data::Configuration),
91    AddDependency(Dependency),
92    AddIntegration(Integration),
93    AddLog((LogIdentifier, Log)),
94    AddEndpoint(Endpoint),
95    Lifecycle(LifecycleAction),
96    #[serde(skip)]
97    CollectStats(oneshot::Sender<TelemetryWorkerStats>),
98}
99
100#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
101pub enum LifecycleAction {
102    Start,
103    Stop,
104    FlushMetricAggr,
105    FlushData,
106    ExtendedHeartbeat,
107}
108
109/// Identifies a logging location uniquely
110///
111/// The identifier is a single 64 bit integer to save space an memory
112/// and to be able to generic on the way different languages handle
113#[derive(Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
114pub struct LogIdentifier {
115    // Collisions? Never heard of them
116    pub identifier: u64,
117}
118
119// Holds the current state of the telemetry worker
120#[derive(Debug)]
121struct TelemetryWorkerData {
122    started: bool,
123    dependencies: store::Store<Dependency>,
124    configurations: store::Store<data::Configuration>,
125    integrations: store::Store<data::Integration>,
126    endpoints: HashSet<data::Endpoint>,
127    logs: store::QueueHashMap<LogIdentifier, Log>,
128    metric_contexts: MetricContexts,
129    metric_buckets: MetricBuckets,
130    host: Host,
131    app: Application,
132}
133
134pub struct TelemetryWorker {
135    flavor: TelemetryWorkerFlavor,
136    config: Config,
137    mailbox: mpsc::Receiver<TelemetryActions>,
138    cancellation_token: CancellationToken,
139    seq_id: AtomicU64,
140    runtime_id: String,
141    client: Box<dyn http_client::HttpClient + Sync + Send>,
142    metrics_flush_interval: Duration,
143    deadlines: scheduler::Scheduler<LifecycleAction>,
144    data: TelemetryWorkerData,
145    next_action: Option<TelemetryActions>,
146    stopped: bool,
147}
148impl Debug for TelemetryWorker {
149    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
150        f.debug_struct("TelemetryWorker")
151            .field("flavor", &self.flavor)
152            .field("config", &self.config)
153            .field("mailbox", &self.mailbox)
154            .field("cancellation_token", &self.cancellation_token)
155            .field("seq_id", &self.seq_id)
156            .field("runtime_id", &self.runtime_id)
157            .field("metrics_flush_interval", &self.metrics_flush_interval)
158            .field("deadlines", &self.deadlines)
159            .field("data", &self.data)
160            .finish()
161    }
162}
163
164#[async_trait]
165impl Worker for TelemetryWorker {
166    async fn trigger(&mut self) {
167        if self.next_action.is_some() {
168            // An action is already available and hasn't been executed
169            return;
170        }
171        if self.stopped {
172            // Channel is closed and Stop has already been dispatched. Park forever to avoid
173            // a hot loop re-emitting Lifecycle::Stop on every iteration; the runtime will
174            // tear the worker down via the handle.
175            debug!(
176                worker.runtime_id = %self.runtime_id,
177                "Telemetry worker mailbox closed; parking until shutdown"
178            );
179            std::future::pending::<()>().await;
180        }
181        // Wait for the next action and store it
182        let action = self.recv_next_action().await;
183        self.next_action = Some(action);
184    }
185
186    // Processes a single action from the state machine
187    async fn run(&mut self) {
188        // Take the action that was stored by trigger()
189        if let Some(action) = self.next_action.take() {
190            debug!(
191                worker.runtime_id = %self.runtime_id,
192                action = ?action,
193                "Received telemetry action"
194            );
195
196            // When running as a [libdd_shared_runtime::Worker] Shutdown is handled by stopping the
197            // Worker from the handle and not by sending stop action
198            let _action_result = match self.flavor {
199                TelemetryWorkerFlavor::Full => self.dispatch_action(action).await,
200                TelemetryWorkerFlavor::MetricsLogs => {
201                    self.dispatch_metrics_logs_action(action).await
202                }
203            };
204        }
205    }
206
207    /// Reset the worker state in the child process after a fork.
208    ///
209    /// Discards inherited pending telemetry state and dedupe history without sending anything, and
210    /// drains the mailbox so that actions queued before the fork are not processed by the
211    /// child.
212    fn reset(&mut self) {
213        // Drain all actions queued in the mailbox before the fork.
214        while self.mailbox.try_recv().is_ok() {}
215
216        // Discard any action that was staged by the last trigger() call.
217        self.next_action = None;
218
219        // Clear all unbuffered telemetry data; the child must not send pre-fork data.
220        self.data.logs = store::QueueHashMap::default();
221        self.data.metric_buckets = MetricBuckets::default();
222        self.data.dependencies.clear();
223        self.data.integrations.clear();
224        self.data.configurations.clear();
225        self.data.endpoints.clear();
226    }
227
228    async fn shutdown(&mut self) {
229        let stop_action = TelemetryActions::Lifecycle(LifecycleAction::Stop);
230        let _action_result = match self.flavor {
231            TelemetryWorkerFlavor::Full => self.dispatch_action(stop_action).await,
232            TelemetryWorkerFlavor::MetricsLogs => {
233                self.dispatch_metrics_logs_action(stop_action).await
234            }
235        };
236    }
237}
238
239#[derive(Debug, Default, Serialize, Deserialize)]
240pub struct TelemetryWorkerStats {
241    pub dependencies_stored: u32,
242    pub dependencies_unflushed: u32,
243    pub configurations_stored: u32,
244    pub configurations_unflushed: u32,
245    pub integrations_stored: u32,
246    pub integrations_unflushed: u32,
247    pub logs: u32,
248    pub metric_contexts: u32,
249    pub metric_buckets: MetricBucketStats,
250}
251
252impl Add for TelemetryWorkerStats {
253    type Output = Self;
254
255    fn add(self, rhs: Self) -> Self::Output {
256        TelemetryWorkerStats {
257            dependencies_stored: self.dependencies_stored + rhs.dependencies_stored,
258            dependencies_unflushed: self.dependencies_unflushed + rhs.dependencies_unflushed,
259            configurations_stored: self.configurations_stored + rhs.configurations_stored,
260            configurations_unflushed: self.configurations_unflushed + rhs.configurations_unflushed,
261            integrations_stored: self.integrations_stored + rhs.integrations_stored,
262            integrations_unflushed: self.integrations_unflushed + rhs.integrations_unflushed,
263            logs: self.logs + rhs.logs,
264            metric_contexts: self.metric_contexts + rhs.metric_contexts,
265            metric_buckets: MetricBucketStats {
266                buckets: self.metric_buckets.buckets + rhs.metric_buckets.buckets,
267                series: self.metric_buckets.series + rhs.metric_buckets.series,
268                series_points: self.metric_buckets.series_points + rhs.metric_buckets.series_points,
269                distributions: self.metric_buckets.distributions + rhs.metric_buckets.distributions,
270                distributions_points: self.metric_buckets.distributions_points
271                    + rhs.metric_buckets.distributions_points,
272            },
273        }
274    }
275}
276
277impl Sum for TelemetryWorkerStats {
278    fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
279        iter.fold(Self::default(), |a, b| a + b)
280    }
281}
282
283mod serialize {
284    use crate::data;
285    use http::HeaderValue;
286    #[allow(clippy::declare_interior_mutable_const)]
287    pub const CONTENT_TYPE_VALUE: HeaderValue = libdd_common::header::APPLICATION_JSON;
288    pub fn serialize(telemetry: &data::Telemetry) -> anyhow::Result<Vec<u8>> {
289        Ok(serde_json::to_vec(telemetry)?)
290    }
291}
292
293impl TelemetryWorker {
294    fn log_err(&self, err: &anyhow::Error) {
295        telemetry_worker_log!(self, ERROR, "{}", err);
296    }
297
298    async fn recv_next_action(&mut self) -> TelemetryActions {
299        let action = if let Some((deadline, deadline_action)) = self.deadlines.next_deadline() {
300            // If deadline passed, directly return associated action
301            if deadline
302                .checked_duration_since(time::Instant::now())
303                .is_none()
304            {
305                return TelemetryActions::Lifecycle(*deadline_action);
306            };
307
308            // Otherwise run it in a timeout against the mailbox
309            match tokio::time::timeout_at(deadline.into(), self.mailbox.recv()).await {
310                Ok(mailbox_action) => mailbox_action,
311                Err(_) => Some(TelemetryActions::Lifecycle(*deadline_action)),
312            }
313        } else {
314            self.mailbox.recv().await
315        };
316
317        // if no action is received, then it means the channel is stopped
318        action.unwrap_or_else(|| {
319            // the worker handle no longer lives - we must remove restartable here to avoid leaks
320            self.config.restartable = false;
321            self.stopped = true;
322            TelemetryActions::Lifecycle(LifecycleAction::Stop)
323        })
324    }
325
326    async fn dispatch_metrics_logs_action(&mut self, action: TelemetryActions) -> ControlFlow<()> {
327        telemetry_worker_log!(self, DEBUG, "Handling metric action {:?}", action);
328        use LifecycleAction::*;
329        use TelemetryActions::*;
330        match action {
331            Lifecycle(Start) => {
332                if !self.data.started {
333                    #[allow(clippy::unwrap_used)]
334                    self.deadlines
335                        .schedule_event(LifecycleAction::FlushMetricAggr)
336                        .unwrap();
337
338                    #[allow(clippy::unwrap_used)]
339                    self.deadlines
340                        .schedule_event(LifecycleAction::FlushData)
341                        .unwrap();
342                    self.data.started = true;
343                }
344            }
345            AddLog((identifier, log)) => {
346                let (l, new) = self.data.logs.get_mut_or_insert(identifier, log);
347                if !new {
348                    l.count += 1;
349                }
350            }
351            AddPoint((point, key, extra_tags)) => {
352                self.data.metric_buckets.add_point(key, point, extra_tags)
353            }
354            Lifecycle(FlushMetricAggr) => {
355                self.data.metric_buckets.flush_aggregates();
356
357                #[allow(clippy::unwrap_used)]
358                self.deadlines
359                    .schedule_event(LifecycleAction::FlushMetricAggr)
360                    .unwrap();
361            }
362            Lifecycle(FlushData) => {
363                if !(self.data.started || self.config.restartable) {
364                    return CONTINUE;
365                }
366
367                #[allow(clippy::unwrap_used)]
368                self.deadlines
369                    .schedule_event(LifecycleAction::FlushData)
370                    .unwrap();
371
372                let batch = self.build_observability_batch();
373                if !batch.is_empty() {
374                    let payload = data::Payload::MessageBatch(batch);
375                    match self.send_payload(&payload).await {
376                        Ok(()) => self.payload_sent_success(&payload),
377                        Err(e) => self.log_err(&e),
378                    }
379                }
380            }
381            AddConfig(_)
382            | AddDependency(_)
383            | AddIntegration(_)
384            | AddEndpoint(_)
385            | Lifecycle(ExtendedHeartbeat) => {}
386            Lifecycle(Stop) => {
387                if !self.data.started {
388                    return BREAK;
389                }
390                self.data.metric_buckets.flush_aggregates();
391
392                let batch = self.build_observability_batch();
393                if !batch.is_empty() {
394                    let payload = data::Payload::MessageBatch(batch);
395                    match self.send_payload(&payload).await {
396                        Ok(()) => {
397                            if self.config.restartable {
398                                self.payload_sent_success(&payload)
399                            }
400                        }
401                        Err(e) => self.log_err(&e),
402                    }
403                }
404
405                self.data.started = false;
406                if !self.config.restartable {
407                    self.deadlines.clear_pending();
408                }
409                return BREAK;
410            }
411            CollectStats(stats_sender) => {
412                stats_sender.send(self.stats()).ok();
413            }
414        };
415        CONTINUE
416    }
417
418    async fn dispatch_action(&mut self, action: TelemetryActions) -> ControlFlow<()> {
419        telemetry_worker_log!(self, DEBUG, "Handling action {:?}", action);
420
421        use LifecycleAction::*;
422        use TelemetryActions::*;
423        match action {
424            Lifecycle(Start) => {
425                if !self.data.started {
426                    let app_started = data::Payload::AppStarted(self.build_app_started());
427                    match self.send_payload(&app_started).await {
428                        Ok(()) => self.payload_sent_success(&app_started),
429                        Err(err) => self.log_err(&err),
430                    }
431
432                    #[allow(clippy::unwrap_used)]
433                    self.deadlines
434                        .schedule_event(LifecycleAction::FlushMetricAggr)
435                        .unwrap();
436
437                    #[allow(clippy::unwrap_used)]
438                    // flush data should be last to previously flushed metrics are sent
439                    self.deadlines
440                        .schedule_event(LifecycleAction::FlushData)
441                        .unwrap();
442
443                    #[allow(clippy::unwrap_used)]
444                    self.deadlines
445                        .schedule_event(LifecycleAction::ExtendedHeartbeat)
446                        .unwrap();
447                    self.data.started = true;
448                }
449            }
450            AddDependency(dep) => self.data.dependencies.insert(dep),
451            AddIntegration(integration) => self.data.integrations.insert(integration),
452            AddConfig(cfg) => self.data.configurations.insert(cfg),
453            AddEndpoint(endpoint) => {
454                self.data.endpoints.insert(endpoint);
455            }
456            AddLog((identifier, log)) => {
457                let (l, new) = self.data.logs.get_mut_or_insert(identifier, log);
458                if !new {
459                    l.count += 1;
460                }
461            }
462            AddPoint((point, key, extra_tags)) => {
463                self.data.metric_buckets.add_point(key, point, extra_tags)
464            }
465            Lifecycle(FlushMetricAggr) => {
466                self.data.metric_buckets.flush_aggregates();
467
468                #[allow(clippy::unwrap_used)]
469                self.deadlines
470                    .schedule_event(LifecycleAction::FlushMetricAggr)
471                    .unwrap();
472            }
473            Lifecycle(FlushData) => {
474                if !(self.data.started || self.config.restartable) {
475                    return CONTINUE;
476                }
477
478                #[allow(clippy::unwrap_used)]
479                self.deadlines
480                    .schedule_event(LifecycleAction::FlushData)
481                    .unwrap();
482
483                let mut batch = self.build_app_events_batch();
484                let payload = if batch.is_empty() {
485                    data::Payload::AppHeartbeat(())
486                } else {
487                    batch.push(data::Payload::AppHeartbeat(()));
488                    data::Payload::MessageBatch(batch)
489                };
490                match self.send_payload(&payload).await {
491                    Ok(()) => self.payload_sent_success(&payload),
492                    Err(err) => self.log_err(&err),
493                }
494
495                let batch = self.build_observability_batch();
496                if !batch.is_empty() {
497                    let payload = data::Payload::MessageBatch(batch);
498                    match self.send_payload(&payload).await {
499                        Ok(()) => self.payload_sent_success(&payload),
500                        Err(err) => self.log_err(&err),
501                    }
502                }
503            }
504            Lifecycle(ExtendedHeartbeat) => {
505                self.data.dependencies.unflush_stored();
506                self.data.integrations.unflush_stored();
507                self.data.configurations.unflush_stored();
508
509                let extended_hb = data::Payload::AppExtendedHeartbeat(self.build_app_started());
510                match self.send_payload(&extended_hb).await {
511                    Ok(()) => self.payload_sent_success(&extended_hb),
512                    Err(err) => self.log_err(&err),
513                }
514                // Only re-schedule self. Resetting `FlushData` here would replace its
515                // existing deadline with `now + heartbeat_interval`, starving FlushData
516                // when `extended_heartbeat_interval < heartbeat_interval` because each
517                // ExtendedHeartbeat firing pushes FlushData out before it can fire.
518                #[allow(clippy::unwrap_used)]
519                self.deadlines
520                    .schedule_event(LifecycleAction::ExtendedHeartbeat)
521                    .unwrap();
522            }
523            Lifecycle(Stop) => {
524                if !self.data.started {
525                    return BREAK;
526                }
527                self.data.metric_buckets.flush_aggregates();
528
529                let mut app_events = self.build_app_events_batch();
530                app_events.push(data::Payload::AppClosing(()));
531
532                let observability_events = self.build_observability_batch();
533
534                let mut payloads = vec![data::Payload::MessageBatch(app_events)];
535                if !observability_events.is_empty() {
536                    payloads.push(data::Payload::MessageBatch(observability_events));
537                }
538
539                let self_arc = Arc::new(tokio::sync::RwLock::new(&mut *self));
540                let futures = payloads.into_iter().map(|payload| {
541                    let self_arc = self_arc.clone();
542                    async move {
543                        // This is different from the non-functional:
544                        // match self_arc.read().await.send_payload(&payload).await { ... }
545                        // presumably because the temp read guard would live till end of match
546                        let res = {
547                            let self_rguard = self_arc.read().await;
548                            self_rguard.send_payload(&payload).await
549                        };
550                        match res {
551                            Ok(()) => self_arc.write().await.payload_sent_success(&payload),
552                            Err(err) => self_arc.read().await.log_err(&err),
553                        }
554                    }
555                });
556                future::join_all(futures).await;
557
558                self.data.started = false;
559                if !self.config.restartable {
560                    self.deadlines.clear_pending();
561                }
562
563                return BREAK;
564            }
565            CollectStats(stats_sender) => {
566                stats_sender.send(self.stats()).ok();
567            }
568        }
569
570        CONTINUE
571    }
572
573    // Builds telemetry payloads containing lifecycle events
574    fn build_app_events_batch(&mut self) -> Vec<Payload> {
575        let mut payloads = Vec::new();
576
577        if self.data.dependencies.flush_not_empty() {
578            payloads.push(data::Payload::AppDependenciesLoaded(
579                data::AppDependenciesLoaded {
580                    dependencies: self.data.dependencies.unflushed().cloned().collect(),
581                },
582            ))
583        }
584        if self.data.integrations.flush_not_empty() {
585            payloads.push(data::Payload::AppIntegrationsChange(
586                data::AppIntegrationsChange {
587                    integrations: self.data.integrations.unflushed().cloned().collect(),
588                },
589            ))
590        }
591        if self.data.configurations.flush_not_empty() {
592            payloads.push(data::Payload::AppClientConfigurationChange(
593                data::AppClientConfigurationChange {
594                    configuration: self.data.configurations.unflushed().cloned().collect(),
595                },
596            ))
597        }
598        if !self.data.endpoints.is_empty() {
599            payloads.push(data::Payload::AppEndpoints(data::AppEndpoints {
600                is_first: true,
601                endpoints: self
602                    .data
603                    .endpoints
604                    .iter()
605                    .map(|e| e.to_json_value().unwrap_or_default())
606                    .filter(|e| e.is_object())
607                    .collect(),
608            }));
609        }
610        payloads
611    }
612
613    // Builds telemetry payloads containing logs, metrics and distributions
614    fn build_observability_batch(&mut self) -> Vec<Payload> {
615        let mut payloads = Vec::new();
616
617        let logs = self.build_logs();
618        if !logs.logs.is_empty() {
619            payloads.push(data::Payload::Logs(logs));
620        }
621        let metrics = self.build_metrics_series();
622        if !metrics.series.is_empty() {
623            payloads.push(data::Payload::GenerateMetrics(metrics))
624        }
625        let distributions = self.build_metrics_distributions();
626        if !distributions.series.is_empty() {
627            payloads.push(data::Payload::Sketches(distributions))
628        }
629        payloads
630    }
631
632    fn build_metrics_distributions(&mut self) -> data::Distributions {
633        let mut series = Vec::new();
634        let context_guard = self.data.metric_contexts.lock();
635        for (context_key, extra_tags, points) in self.data.metric_buckets.flush_distributions() {
636            let Some(context) = context_guard.read(context_key) else {
637                telemetry_worker_log!(self, ERROR, "Context not found for key {:?}", context_key);
638                continue;
639            };
640            let mut tags = extra_tags;
641            tags.extend(context.tags.iter().cloned());
642            series.push(data::metrics::Distribution {
643                namespace: context.namespace,
644                metric: context.name.clone(),
645                tags,
646                sketch: data::metrics::SerializedSketch::B64 {
647                    sketch_b64: base64::Engine::encode(
648                        &base64::engine::general_purpose::STANDARD,
649                        points.encode_to_vec(),
650                    ),
651                },
652                common: context.common,
653                _type: context.metric_type,
654                interval: self.metrics_flush_interval.as_secs(),
655            });
656        }
657        data::Distributions { series }
658    }
659
660    fn build_metrics_series(&mut self) -> data::GenerateMetrics {
661        let mut series = Vec::new();
662        let context_guard = self.data.metric_contexts.lock();
663        for (context_key, extra_tags, points) in self.data.metric_buckets.flush_series() {
664            let Some(context) = context_guard.read(context_key) else {
665                telemetry_worker_log!(self, ERROR, "Context not found for key {:?}", context_key);
666                continue;
667            };
668
669            let mut tags = extra_tags;
670            tags.extend(context.tags.iter().cloned());
671            series.push(data::metrics::Serie {
672                namespace: context.namespace,
673                metric: context.name.clone(),
674                tags,
675                points,
676                common: context.common,
677                _type: context.metric_type,
678                interval: self.metrics_flush_interval.as_secs(),
679            });
680        }
681
682        data::GenerateMetrics { series }
683    }
684
685    fn build_app_started(&mut self) -> data::AppStarted {
686        data::AppStarted {
687            configuration: self.data.configurations.unflushed().cloned().collect(),
688            dependencies: self.data.dependencies.unflushed().cloned().collect(),
689            integrations: self.data.integrations.unflushed().cloned().collect(),
690        }
691    }
692
693    fn app_started_sent_success(&mut self, p: &data::AppStarted) {
694        self.data
695            .configurations
696            .removed_flushed(p.configuration.len());
697        self.data.dependencies.removed_flushed(p.dependencies.len());
698        self.data.integrations.removed_flushed(p.integrations.len());
699    }
700
701    fn payload_sent_success(&mut self, payload: &data::Payload) {
702        use data::Payload::*;
703        match payload {
704            AppStarted(p) => self.app_started_sent_success(p),
705            AppExtendedHeartbeat(p) => self.app_started_sent_success(p),
706            AppDependenciesLoaded(p) => {
707                self.data.dependencies.removed_flushed(p.dependencies.len())
708            }
709            AppIntegrationsChange(p) => {
710                self.data.integrations.removed_flushed(p.integrations.len())
711            }
712            AppClientConfigurationChange(p) => self
713                .data
714                .configurations
715                .removed_flushed(p.configuration.len()),
716            AppEndpoints(_) => self.data.endpoints.clear(),
717            MessageBatch(batch) => {
718                for p in batch {
719                    self.payload_sent_success(p);
720                }
721            }
722            Logs(p) => {
723                for _ in &p.logs {
724                    self.data.logs.pop_front();
725                }
726            }
727            AppHeartbeat(()) | AppClosing(()) => {}
728            GenerateMetrics(_) | Sketches(_) => {}
729        }
730    }
731
732    fn build_logs(&self) -> data::Logs {
733        // TODO: change the data model to take a &[Log] so don't have to clone data here
734        let logs = self.data.logs.iter().map(|(_, l)| l.clone()).collect();
735        data::Logs { logs }
736    }
737
738    fn next_seq_id(&self) -> u64 {
739        self.seq_id.fetch_add(1, Ordering::Release)
740    }
741
742    async fn send_payload(&self, payload: &data::Payload) -> anyhow::Result<()> {
743        debug!(
744            worker.runtime_id = %self.runtime_id,
745            payload.type = payload.request_type(),
746            seq_id = self.seq_id.load(Ordering::Acquire),
747            "Sending telemetry payload"
748        );
749        let req = self.build_request(payload)?;
750        let result = self.send_request(req).await;
751        match &result {
752            Ok(resp) => debug!(
753                worker.runtime_id = %self.runtime_id,
754                payload.type = payload.request_type(),
755                response.status = resp.status().as_u16(),
756                "Successfully sent telemetry payload"
757            ),
758            Err(e) => debug!(
759                worker.runtime_id = %self.runtime_id,
760                payload.type = payload.request_type(),
761                error = ?e,
762                "Failed to send telemetry payload"
763            ),
764        }
765        Ok(())
766    }
767
768    fn build_request(&self, payload: &data::Payload) -> anyhow::Result<http_common::HttpRequest> {
769        let seq_id = self.next_seq_id();
770        let tel = Telemetry {
771            api_version: data::ApiVersion::V2,
772            tracer_time: time::SystemTime::UNIX_EPOCH
773                .elapsed()
774                .map_or(0, |d| d.as_secs()),
775            runtime_id: &self.runtime_id,
776            seq_id,
777            host: &self.data.host,
778            origin: None,
779            application: &self.data.app,
780            payload,
781        };
782
783        telemetry_worker_log!(self, DEBUG, "Prepared payload: {:?}", tel);
784
785        let req = http_client::request_builder(&self.config)?
786            .method(http::Method::POST)
787            .header(header::CONTENT_TYPE, serialize::CONTENT_TYPE_VALUE)
788            .header(
789                http_client::header::REQUEST_TYPE,
790                HeaderValue::from_static(payload.request_type()),
791            )
792            .header(
793                http_client::header::API_VERSION,
794                HeaderValue::from_static(data::ApiVersion::V2.to_str()),
795            )
796            .header(
797                http_client::header::LIBRARY_LANGUAGE,
798                tel.application.language_name.clone(),
799            )
800            .header(
801                http_client::header::LIBRARY_VERSION,
802                tel.application.tracer_version.clone(),
803            );
804        let req = http_client::add_instrumentation_session_headers(
805            req,
806            self.config.session_id.as_deref(),
807            self.config.parent_session_id.as_deref(),
808            self.config.root_session_id.as_deref(),
809        );
810
811        let body = http_common::Body::from(serialize::serialize(&tel)?);
812        Ok(req.body(body)?)
813    }
814
815    async fn send_request(
816        &self,
817        req: http_common::HttpRequest,
818    ) -> Result<http_common::HttpResponse, http_common::Error> {
819        let timeout_ms = if let Some(endpoint) = self.config.endpoint.as_ref() {
820            endpoint.timeout_ms
821        } else {
822            libdd_common::Endpoint::DEFAULT_TIMEOUT
823        };
824
825        debug!(
826            worker.runtime_id = %self.runtime_id,
827            http.timeout_ms = timeout_ms,
828            "Sending HTTP request"
829        );
830
831        tokio::select! {
832            _ = self.cancellation_token.cancelled() => {
833                debug!(
834                    worker.runtime_id = %self.runtime_id,
835                    "Telemetry request cancelled"
836                );
837                Err(http_common::Error::Other(anyhow::anyhow!("Request cancelled")))
838            },
839            _ = tokio::time::sleep(time::Duration::from_millis(timeout_ms)) => {
840                debug!(
841                    worker.runtime_id = %self.runtime_id,
842                    http.timeout_ms = timeout_ms,
843                    "Telemetry request timed out"
844                );
845                Err(http_common::Error::Other(anyhow::anyhow!("Request timed out")))
846            },
847            r = self.client.request(req) => {
848                match r {
849                    Ok(resp) => {
850                        Ok(resp)
851                    }
852                    Err(e) => {
853                        Err(e)
854                    },
855                }
856            }
857        }
858    }
859
860    fn stats(&self) -> TelemetryWorkerStats {
861        TelemetryWorkerStats {
862            dependencies_stored: self.data.dependencies.len_stored() as u32,
863            dependencies_unflushed: self.data.dependencies.len_unflushed() as u32,
864            configurations_stored: self.data.configurations.len_stored() as u32,
865            configurations_unflushed: self.data.configurations.len_unflushed() as u32,
866            integrations_stored: self.data.integrations.len_stored() as u32,
867            integrations_unflushed: self.data.integrations.len_unflushed() as u32,
868            logs: self.data.logs.len() as u32,
869            metric_contexts: self.data.metric_contexts.lock().len() as u32,
870            metric_buckets: self.data.metric_buckets.stats(),
871        }
872    }
873
874    // Runs a state machine that waits for actions, either from the worker's
875    // mailbox, or scheduled actions from the worker's deadline object.
876    async fn run_loop(mut self) {
877        debug!(
878            worker.flavor = ?self.flavor,
879            worker.runtime_id = %self.runtime_id,
880            "Starting telemetry worker"
881        );
882
883        loop {
884            if self.cancellation_token.is_cancelled() {
885                debug!(
886                    worker.runtime_id = %self.runtime_id,
887                    "Telemetry worker cancelled, shutting down"
888                );
889                return;
890            }
891
892            let action = self.recv_next_action().await;
893            debug!(
894                worker.runtime_id = %self.runtime_id,
895                action = ?action,
896                "Received telemetry action"
897            );
898
899            let action_result = match self.flavor {
900                TelemetryWorkerFlavor::Full => self.dispatch_action(action).await,
901                TelemetryWorkerFlavor::MetricsLogs => {
902                    self.dispatch_metrics_logs_action(action).await
903                }
904            };
905
906            match action_result {
907                ControlFlow::Continue(()) => {}
908                ControlFlow::Break(()) => {
909                    debug!(
910                        worker.runtime_id = %self.runtime_id,
911                        worker.restartable = self.config.restartable,
912                        "Telemetry worker received break signal"
913                    );
914                    if !self.config.restartable {
915                        break;
916                    }
917                }
918            };
919        }
920
921        debug!(
922            worker.runtime_id = %self.runtime_id,
923            "Telemetry worker stopped"
924        );
925    }
926}
927
928#[derive(Debug)]
929struct InnerTelemetryShutdown {
930    is_shutdown: Mutex<bool>,
931    condvar: Condvar,
932}
933
934impl InnerTelemetryShutdown {
935    fn wait_for_shutdown(&self) {
936        drop(
937            #[allow(clippy::unwrap_used)]
938            self.condvar
939                .wait_while(self.is_shutdown.lock().unwrap(), |is_shutdown| {
940                    !*is_shutdown
941                })
942                .unwrap(),
943        )
944    }
945
946    #[allow(clippy::unwrap_used)]
947    fn shutdown_finished(&self) {
948        *self.is_shutdown.lock().unwrap() = true;
949        self.condvar.notify_all();
950    }
951}
952
953#[derive(Clone, Debug)]
954/// TelemetryWorkerHandle is a handle which allows interactions with the telemetry worker.
955/// The handle is safe to use across threads.
956///
957/// The worker won't send data to the agent until you call `TelemetryWorkerHandle::send_start`
958///
959/// To stop the worker, call `TelemetryWorkerHandle::send_stop` which trigger flush asynchronously
960/// then `TelemetryWorkerHandle::wait_for_shutdown`
961pub struct TelemetryWorkerHandle {
962    sender: mpsc::Sender<TelemetryActions>,
963    shutdown: Arc<InnerTelemetryShutdown>,
964    cancellation_token: CancellationToken,
965    // Used to spawn cancellation tasks. Should be None when running as a SharedRuntime worker,
966    // since the runtime is not guaranteed to exist for the lifetime of the worker.
967    runtime: Option<runtime::Handle>,
968
969    contexts: MetricContexts,
970}
971
972impl TelemetryWorkerHandle {
973    pub fn register_metric_context(
974        &self,
975        name: String,
976        tags: Vec<Tag>,
977        metric_type: data::metrics::MetricType,
978        common: bool,
979        namespace: data::metrics::MetricNamespace,
980    ) -> ContextKey {
981        self.contexts
982            .register_metric_context(name, tags, metric_type, common, namespace)
983    }
984
985    pub fn try_send_msg(&self, msg: TelemetryActions) -> anyhow::Result<()> {
986        Ok(self.sender.try_send(msg)?)
987    }
988
989    pub async fn send_msg(&self, msg: TelemetryActions) -> anyhow::Result<()> {
990        Ok(self.sender.send(msg).await?)
991    }
992
993    pub async fn send_msgs<T>(&self, msgs: T) -> anyhow::Result<()>
994    where
995        T: IntoIterator<Item = TelemetryActions>,
996    {
997        for msg in msgs {
998            self.sender.send(msg).await?;
999        }
1000
1001        Ok(())
1002    }
1003
1004    pub async fn send_msg_timeout(
1005        &self,
1006        msg: TelemetryActions,
1007        timeout: time::Duration,
1008    ) -> anyhow::Result<()> {
1009        Ok(self.sender.send_timeout(msg, timeout).await?)
1010    }
1011
1012    pub fn send_start(&self) -> anyhow::Result<()> {
1013        Ok(self
1014            .sender
1015            .try_send(TelemetryActions::Lifecycle(LifecycleAction::Start))?)
1016    }
1017
1018    pub fn send_stop(&self) -> anyhow::Result<()> {
1019        Ok(self
1020            .sender
1021            .try_send(TelemetryActions::Lifecycle(LifecycleAction::Stop))?)
1022    }
1023
1024    fn cancel_requests_with_deadline(&self, deadline: time::Instant) {
1025        let Some(runtime) = &self.runtime else {
1026            tracing::error!("Cannot schedule cancellation deadline: no runtime handle available");
1027            return;
1028        };
1029        let token = self.cancellation_token.clone();
1030        let f = async move {
1031            tokio::time::sleep_until(deadline.into()).await;
1032            token.cancel()
1033        };
1034        runtime.spawn(f);
1035    }
1036
1037    pub fn wait_for_shutdown_deadline(&self, deadline: time::Instant) {
1038        self.cancel_requests_with_deadline(deadline);
1039        self.wait_for_shutdown()
1040    }
1041
1042    pub fn add_dependency(&self, name: String, version: Option<String>) -> anyhow::Result<()> {
1043        self.sender
1044            .try_send(TelemetryActions::AddDependency(Dependency {
1045                name,
1046                version,
1047            }))?;
1048        Ok(())
1049    }
1050
1051    pub fn add_integration(
1052        &self,
1053        name: String,
1054        enabled: bool,
1055        version: Option<String>,
1056        compatible: Option<bool>,
1057        auto_enabled: Option<bool>,
1058    ) -> anyhow::Result<()> {
1059        self.sender
1060            .try_send(TelemetryActions::AddIntegration(Integration {
1061                name,
1062                version,
1063                compatible,
1064                enabled,
1065                auto_enabled,
1066            }))?;
1067        Ok(())
1068    }
1069
1070    pub fn add_log<T: Hash>(
1071        &self,
1072        identifier: T,
1073        message: String,
1074        level: data::LogLevel,
1075        stack_trace: Option<String>,
1076    ) -> anyhow::Result<()> {
1077        let mut hasher = DefaultHasher::new();
1078        identifier.hash(&mut hasher);
1079        self.sender.try_send(TelemetryActions::AddLog((
1080            LogIdentifier {
1081                identifier: hasher.finish(),
1082            },
1083            data::Log {
1084                message,
1085                level,
1086                stack_trace,
1087                count: 1,
1088                tags: String::new(),
1089                is_sensitive: false,
1090                is_crash: false,
1091            },
1092        )))?;
1093        Ok(())
1094    }
1095
1096    pub fn add_point(
1097        &self,
1098        value: f64,
1099        context: &ContextKey,
1100        extra_tags: Vec<Tag>,
1101    ) -> anyhow::Result<()> {
1102        self.sender
1103            .try_send(TelemetryActions::AddPoint((value, *context, extra_tags)))?;
1104        Ok(())
1105    }
1106
1107    pub fn wait_for_shutdown(&self) {
1108        self.shutdown.wait_for_shutdown();
1109    }
1110
1111    pub fn stats(&self) -> anyhow::Result<oneshot::Receiver<TelemetryWorkerStats>> {
1112        let (sender, receiver) = oneshot::channel();
1113        self.sender
1114            .try_send(TelemetryActions::CollectStats(sender))?;
1115        Ok(receiver)
1116    }
1117}
1118
1119/// How many dependencies/integrations/configs we keep in memory at most
1120pub const MAX_ITEMS: usize = 5000;
1121
1122#[derive(Debug, Default, Clone, Copy)]
1123pub enum TelemetryWorkerFlavor {
1124    /// Send all telemetry messages including lifecycle events like app-started, heartbeats,
1125    /// dependencies and configurations
1126    #[default]
1127    Full,
1128    /// Only send telemetry data not tied to the lifecycle of the app like logs and metrics
1129    MetricsLogs,
1130}
1131
1132pub struct TelemetryWorkerBuilder {
1133    pub host: Host,
1134    pub application: Application,
1135    pub runtime_id: Option<String>,
1136    pub dependencies: store::Store<data::Dependency>,
1137    pub integrations: store::Store<data::Integration>,
1138    pub configurations: store::Store<data::Configuration>,
1139    pub endpoints: HashSet<data::Endpoint>,
1140    pub native_deps: bool,
1141    pub rust_shared_lib_deps: bool,
1142    pub config: Config,
1143    pub flavor: TelemetryWorkerFlavor,
1144}
1145
1146impl TelemetryWorkerBuilder {
1147    /// Creates a new telemetry worker builder and infer host information automatically
1148    pub fn new_fetch_host(
1149        service_name: String,
1150        language_name: String,
1151        language_version: String,
1152        tracer_version: String,
1153    ) -> Self {
1154        Self {
1155            host: crate::build_host(),
1156            ..Self::new(
1157                String::new(),
1158                service_name,
1159                language_name,
1160                language_version,
1161                tracer_version,
1162            )
1163        }
1164    }
1165
1166    /// Creates a new telemetry worker builder with the given hostname
1167    pub fn new(
1168        hostname: String,
1169        service_name: String,
1170        language_name: String,
1171        language_version: String,
1172        tracer_version: String,
1173    ) -> Self {
1174        Self {
1175            host: Host {
1176                hostname,
1177                ..Default::default()
1178            },
1179            application: Application {
1180                service_name,
1181                language_name,
1182                language_version,
1183                tracer_version,
1184                ..Default::default()
1185            },
1186            runtime_id: None,
1187            dependencies: store::Store::new(MAX_ITEMS),
1188            integrations: store::Store::new(MAX_ITEMS),
1189            configurations: store::Store::new(MAX_ITEMS),
1190            endpoints: HashSet::new(),
1191            native_deps: true,
1192            rust_shared_lib_deps: false,
1193            config: Config::default(),
1194            flavor: TelemetryWorkerFlavor::default(),
1195        }
1196    }
1197
1198    /// Build the corresponding worker and its handle.
1199    ///
1200    /// The optional runtime handle is stored in the worker handle and should be the one used to run
1201    /// the worker task cancellation deadlines. Pass `None` when the worker will be run via a
1202    /// [`SharedRuntime`](libdd_shared_runtime::SharedRuntime).
1203    pub fn build_worker(
1204        self,
1205        tokio_runtime: Option<Handle>,
1206    ) -> (TelemetryWorkerHandle, TelemetryWorker) {
1207        let (tx, mailbox) = mpsc::channel(5000);
1208        let shutdown = Arc::new(InnerTelemetryShutdown {
1209            is_shutdown: Mutex::new(false),
1210            condvar: Condvar::new(),
1211        });
1212        let contexts = MetricContexts::default();
1213        let token = CancellationToken::new();
1214        let config = self.config;
1215        let telemetry_heartbeat_interval = config.telemetry_heartbeat_interval;
1216        let telemetry_extended_heartbeat_interval = config.telemetry_extended_heartbeat_interval;
1217        let client = http_client::from_config(&config);
1218
1219        let metrics_flush_interval =
1220            telemetry_heartbeat_interval.min(MetricBuckets::METRICS_FLUSH_INTERVAL);
1221
1222        #[allow(clippy::unwrap_used)]
1223        let worker = TelemetryWorker {
1224            flavor: self.flavor,
1225            data: TelemetryWorkerData {
1226                started: false,
1227                dependencies: self.dependencies,
1228                integrations: self.integrations,
1229                configurations: self.configurations,
1230                endpoints: self.endpoints,
1231                logs: store::QueueHashMap::default(),
1232                metric_contexts: contexts.clone(),
1233                metric_buckets: MetricBuckets::default(),
1234                host: self.host,
1235                app: self.application,
1236            },
1237            config,
1238            mailbox,
1239            seq_id: AtomicU64::new(1),
1240            runtime_id: self
1241                .runtime_id
1242                .unwrap_or_else(|| uuid::Uuid::new_v4().to_string()),
1243            client,
1244            metrics_flush_interval,
1245            deadlines: scheduler::Scheduler::new(vec![
1246                (metrics_flush_interval, LifecycleAction::FlushMetricAggr),
1247                (telemetry_heartbeat_interval, LifecycleAction::FlushData),
1248                (
1249                    telemetry_extended_heartbeat_interval,
1250                    LifecycleAction::ExtendedHeartbeat,
1251                ),
1252            ]),
1253            cancellation_token: token.clone(),
1254            next_action: None,
1255            stopped: false,
1256        };
1257
1258        (
1259            TelemetryWorkerHandle {
1260                sender: tx,
1261                shutdown,
1262                cancellation_token: token,
1263                runtime: tokio_runtime,
1264
1265                contexts,
1266            },
1267            worker,
1268        )
1269    }
1270
1271    /// Spawns a telemetry worker task in the current tokio runtime
1272    /// The worker will capture a reference to the runtime and use it to run its tasks
1273    pub fn spawn(self) -> (TelemetryWorkerHandle, JoinHandle<()>) {
1274        let tokio_runtime = tokio::runtime::Handle::current();
1275
1276        let (worker_handle, worker) = self.build_worker(Some(tokio_runtime.clone()));
1277
1278        let join_handle = tokio_runtime.spawn(async move { worker.run_loop().await });
1279
1280        (worker_handle, join_handle)
1281    }
1282
1283    /// Spawns a telemetry worker in a new thread and returns a handle to interact with it
1284    pub fn run(self) -> anyhow::Result<TelemetryWorkerHandle> {
1285        let runtime = tokio::runtime::Builder::new_current_thread()
1286            .enable_all()
1287            .build()?;
1288        let (handle, worker) = self.build_worker(Some(runtime.handle().clone()));
1289        let notify_shutdown = handle.shutdown.clone();
1290        std::thread::spawn(move || {
1291            runtime.block_on(worker.run_loop());
1292            runtime.shutdown_background();
1293            notify_shutdown.shutdown_finished();
1294        });
1295
1296        Ok(handle)
1297    }
1298}
1299
1300#[cfg(test)]
1301mod tests {
1302    use crate::data::Payload;
1303    use crate::worker::http_client::header::{
1304        DD_PARENT_SESSION_ID, DD_ROOT_SESSION_ID, DD_SESSION_ID,
1305    };
1306    use crate::worker::{
1307        LifecycleAction, TelemetryActions, TelemetryWorker, TelemetryWorkerBuilder,
1308        TelemetryWorkerFlavor, TelemetryWorkerHandle,
1309    };
1310    use libdd_common::{http_common, Endpoint};
1311    use tokio::runtime::Runtime;
1312
1313    fn is_send<T: Send>(_: T) {}
1314    fn is_sync<T: Sync>(_: T) {}
1315
1316    #[test]
1317    fn test_handle_sync_send() {
1318        #[allow(clippy::redundant_closure)]
1319        let _ = |h: TelemetryWorkerHandle| is_send(h);
1320        #[allow(clippy::redundant_closure)]
1321        let _ = |h: TelemetryWorkerHandle| is_sync(h);
1322    }
1323
1324    fn test_worker(
1325        session_id: Option<String>,
1326        root_session_id: Option<String>,
1327        parent_session_id: Option<String>,
1328    ) -> TelemetryWorker {
1329        let mut b = TelemetryWorkerBuilder::new(
1330            "h".into(),
1331            "svc".into(),
1332            "lang".into(),
1333            "1".into(),
1334            "tv".into(),
1335        );
1336        b.config
1337            .set_endpoint(Endpoint::from_slice("http://127.0.0.1:1"))
1338            .unwrap();
1339        b.runtime_id = Some("rid".into());
1340        b.config.session_id = session_id;
1341        b.config.parent_session_id = parent_session_id;
1342        b.config.root_session_id = root_session_id;
1343        let rt = Runtime::new().unwrap();
1344        b.build_worker(Some(rt.handle().clone())).1
1345    }
1346
1347    #[test]
1348    fn telemetry_http_includes_dd_session_id() {
1349        let req = test_worker(Some("sess".into()), None, None)
1350            .build_request(&Payload::AppHeartbeat(()))
1351            .unwrap();
1352        assert_eq!(
1353            req.headers().get(DD_SESSION_ID).unwrap().to_str().unwrap(),
1354            "sess"
1355        );
1356        assert!(req.headers().get(DD_ROOT_SESSION_ID).is_none());
1357        assert!(req.headers().get(DD_PARENT_SESSION_ID).is_none());
1358    }
1359
1360    #[test]
1361    fn telemetry_http_omits_root_session_id_when_same_as_session_id() {
1362        let req = test_worker(
1363            Some("sess-id".into()),
1364            Some("sess-id".into()),
1365            Some("parent".into()),
1366        )
1367        .build_request(&Payload::AppHeartbeat(()))
1368        .unwrap();
1369        assert_eq!(
1370            req.headers().get(DD_SESSION_ID).unwrap().to_str().unwrap(),
1371            "sess-id"
1372        );
1373        assert!(req.headers().get(DD_ROOT_SESSION_ID).is_none());
1374        assert_eq!(
1375            req.headers()
1376                .get(DD_PARENT_SESSION_ID)
1377                .unwrap()
1378                .to_str()
1379                .unwrap(),
1380            "parent"
1381        );
1382    }
1383
1384    #[test]
1385    fn telemetry_http_omits_parent_session_id_when_same_as_session_id() {
1386        let req = test_worker(
1387            Some("sess-id".into()),
1388            Some("root".into()),
1389            Some("sess-id".into()),
1390        )
1391        .build_request(&Payload::AppHeartbeat(()))
1392        .unwrap();
1393        assert_eq!(
1394            req.headers().get(DD_SESSION_ID).unwrap().to_str().unwrap(),
1395            "sess-id"
1396        );
1397        assert_eq!(
1398            req.headers()
1399                .get(DD_ROOT_SESSION_ID)
1400                .unwrap()
1401                .to_str()
1402                .unwrap(),
1403            "root"
1404        );
1405        assert!(req.headers().get(DD_PARENT_SESSION_ID).is_none());
1406    }
1407
1408    #[test]
1409    fn telemetry_http_omits_session_family_without_valid_session_id() {
1410        let assert_no_session_headers = |req: &http_common::HttpRequest| {
1411            assert!(req.headers().get(DD_SESSION_ID).is_none());
1412            assert!(req.headers().get(DD_ROOT_SESSION_ID).is_none());
1413            assert!(req.headers().get(DD_PARENT_SESSION_ID).is_none());
1414        };
1415
1416        let req = test_worker(None, Some("root".into()), Some("parent".into()))
1417            .build_request(&Payload::AppHeartbeat(()))
1418            .unwrap();
1419        assert_no_session_headers(&req);
1420
1421        let req = test_worker(
1422            Some(String::new()),
1423            Some("root".into()),
1424            Some("parent".into()),
1425        )
1426        .build_request(&Payload::AppHeartbeat(()))
1427        .unwrap();
1428        assert_no_session_headers(&req);
1429    }
1430
1431    #[test]
1432    fn telemetry_http_includes_dd_session_root_and_parent_session_ids() {
1433        let req = test_worker(
1434            Some("sess".into()),
1435            Some("root".into()),
1436            Some("parent".into()),
1437        )
1438        .build_request(&Payload::AppHeartbeat(()))
1439        .unwrap();
1440        assert_eq!(
1441            req.headers().get(DD_SESSION_ID).unwrap().to_str().unwrap(),
1442            "sess"
1443        );
1444        assert_eq!(
1445            req.headers()
1446                .get(DD_ROOT_SESSION_ID)
1447                .unwrap()
1448                .to_str()
1449                .unwrap(),
1450            "root"
1451        );
1452        assert_eq!(
1453            req.headers()
1454                .get(DD_PARENT_SESSION_ID)
1455                .unwrap()
1456                .to_str()
1457                .unwrap(),
1458            "parent"
1459        );
1460    }
1461
1462    fn build_test_worker_with_flavor(flavor: TelemetryWorkerFlavor) -> TelemetryWorker {
1463        let mut b = TelemetryWorkerBuilder::new(
1464            "h".into(),
1465            "svc".into(),
1466            "lang".into(),
1467            "1".into(),
1468            "tv".into(),
1469        );
1470        b.config
1471            .set_endpoint(Endpoint::from_slice("http://127.0.0.1:1"))
1472            .unwrap();
1473        b.runtime_id = Some("rid".into());
1474        b.flavor = flavor;
1475        b.build_worker(Some(tokio::runtime::Handle::current())).1
1476    }
1477
1478    /// Every event with a delay must be scheduled on Start; otherwise it sits in
1479    /// `delays` forever and its handler never fires. Walking `delays` (rather than
1480    /// enumerating variants) guards against future periodic actions regressing.
1481    #[tokio::test]
1482    #[cfg_attr(miri, ignore)] // reqwest in dispatch_action
1483    async fn full_flavor_start_schedules_every_periodic_action() {
1484        let mut worker = build_test_worker_with_flavor(TelemetryWorkerFlavor::Full);
1485
1486        let _ = worker
1487            .dispatch_action(TelemetryActions::Lifecycle(LifecycleAction::Start))
1488            .await;
1489
1490        let delays: Vec<LifecycleAction> =
1491            worker.deadlines.delays.iter().map(|(_, k)| *k).collect();
1492        let scheduled: Vec<LifecycleAction> =
1493            worker.deadlines.deadlines.iter().map(|(_, k)| *k).collect();
1494
1495        assert!(!delays.is_empty(), "scheduler should have periodic actions");
1496        for ev in &delays {
1497            assert!(
1498                scheduled.contains(ev),
1499                "{ev:?} has a delay but was not scheduled on Start; scheduled={scheduled:?}",
1500            );
1501        }
1502    }
1503
1504    /// `MetricsLogs` flavor intentionally excludes lifecycle events. Negative guard
1505    /// so any future change emitting them from this flavor has to update the test.
1506    #[tokio::test]
1507    #[cfg_attr(miri, ignore)] // reqwest in build_worker
1508    async fn metrics_logs_flavor_start_does_not_schedule_extended_heartbeat() {
1509        let mut worker = build_test_worker_with_flavor(TelemetryWorkerFlavor::MetricsLogs);
1510
1511        let _ = worker
1512            .dispatch_metrics_logs_action(TelemetryActions::Lifecycle(LifecycleAction::Start))
1513            .await;
1514
1515        let scheduled: Vec<LifecycleAction> =
1516            worker.deadlines.deadlines.iter().map(|(_, k)| *k).collect();
1517
1518        assert!(scheduled.contains(&LifecycleAction::FlushMetricAggr));
1519        assert!(scheduled.contains(&LifecycleAction::FlushData));
1520        assert!(
1521            !scheduled.contains(&LifecycleAction::ExtendedHeartbeat),
1522            "MetricsLogs should not schedule ExtendedHeartbeat; scheduled={scheduled:?}",
1523        );
1524    }
1525
1526    /// Regression: when `extended_heartbeat_interval < heartbeat_interval`, the
1527    /// ExtendedHeartbeat handler must not reset FlushData's deadline. If it did, each
1528    /// firing would push FlushData to `now + heartbeat_interval` and the next
1529    /// (sooner) ExtendedHeartbeat would push it again — starving FlushData forever.
1530    #[tokio::test]
1531    #[cfg_attr(miri, ignore)] // reqwest in dispatch_action
1532    async fn extended_heartbeat_does_not_reset_flush_data() {
1533        let mut worker = build_test_worker_with_flavor(TelemetryWorkerFlavor::Full);
1534
1535        let _ = worker
1536            .dispatch_action(TelemetryActions::Lifecycle(LifecycleAction::Start))
1537            .await;
1538
1539        let flush_data_before = worker
1540            .deadlines
1541            .deadlines
1542            .iter()
1543            .find(|(_, k)| *k == LifecycleAction::FlushData)
1544            .map(|(d, _)| *d)
1545            .expect("FlushData scheduled on Start");
1546
1547        let _ = worker
1548            .dispatch_action(TelemetryActions::Lifecycle(
1549                LifecycleAction::ExtendedHeartbeat,
1550            ))
1551            .await;
1552
1553        let flush_data_after = worker
1554            .deadlines
1555            .deadlines
1556            .iter()
1557            .find(|(_, k)| *k == LifecycleAction::FlushData)
1558            .map(|(d, _)| *d)
1559            .expect("FlushData should still be scheduled after ExtendedHeartbeat fires");
1560
1561        assert_eq!(
1562            flush_data_before, flush_data_after,
1563            "ExtendedHeartbeat must not reset FlushData's deadline",
1564        );
1565    }
1566
1567    mod reset {
1568        use super::super::*;
1569        use crate::data::{
1570            metrics::{MetricNamespace, MetricType},
1571            Configuration, ConfigurationOrigin, Dependency, Endpoint, Integration, Log, LogLevel,
1572        };
1573        use libdd_shared_runtime::Worker;
1574
1575        fn build_test_worker() -> (TelemetryWorkerHandle, TelemetryWorker) {
1576            let builder = TelemetryWorkerBuilder::new(
1577                "hostname".to_string(),
1578                "test-service".to_string(),
1579                "rust".to_string(),
1580                "1.0.0".to_string(),
1581                "1.0.0".to_string(),
1582            );
1583            // build_worker requires a tokio Handle; tests using this must be #[tokio::test]
1584            builder.build_worker(Some(tokio::runtime::Handle::current()))
1585        }
1586
1587        fn make_log(id: u64, message: &str) -> (LogIdentifier, Log) {
1588            (
1589                LogIdentifier { identifier: id },
1590                Log {
1591                    message: message.to_string(),
1592                    level: LogLevel::Warn,
1593                    stack_trace: None,
1594                    count: 1,
1595                    tags: String::new(),
1596                    is_sensitive: false,
1597                    is_crash: false,
1598                },
1599            )
1600        }
1601
1602        /// After reset(), pending buffered telemetry and dedupe history is cleared.
1603        #[tokio::test]
1604        async fn test_reset_clears_buffered_data() {
1605            let (handle, mut worker) = build_test_worker();
1606
1607            // Populate every data field that reset() should clear.
1608            worker.data.dependencies.insert(Dependency {
1609                name: "dep".to_string(),
1610                version: None,
1611            });
1612            worker.data.integrations.insert(Integration {
1613                name: "integration".to_string(),
1614                version: None,
1615                enabled: true,
1616                compatible: None,
1617                auto_enabled: None,
1618            });
1619            worker.data.configurations.insert(Configuration {
1620                name: "cfg".to_string(),
1621                value: "true".to_string(),
1622                origin: ConfigurationOrigin::Code,
1623                config_id: None,
1624                seq_id: None,
1625            });
1626            worker.data.endpoints.insert(Endpoint {
1627                operation_name: "GET /health".to_string(),
1628                resource_name: "/health".to_string(),
1629                ..Default::default()
1630            });
1631            let (id, log) = make_log(42, "msg");
1632            worker.data.logs.get_mut_or_insert(id, log);
1633
1634            // Register a metric context and add a data point.
1635            let key = handle.register_metric_context(
1636                "test.metric".to_string(),
1637                vec![],
1638                MetricType::Count,
1639                false,
1640                MetricNamespace::Tracers,
1641            );
1642            worker.data.metric_buckets.add_point(key, 1.0, vec![]);
1643
1644            worker.reset();
1645
1646            let stats = worker.stats();
1647            assert_eq!(
1648                stats.dependencies_stored, 0,
1649                "dependency dedupe history should be cleared"
1650            );
1651            assert_eq!(
1652                stats.dependencies_unflushed, 0,
1653                "dependency pending queue should be cleared"
1654            );
1655            assert_eq!(
1656                stats.integrations_stored, 0,
1657                "integration dedupe history should be cleared"
1658            );
1659            assert_eq!(
1660                stats.integrations_unflushed, 0,
1661                "integration pending queue should be cleared"
1662            );
1663            assert_eq!(
1664                stats.configurations_stored, 0,
1665                "configuration dedupe history should be cleared"
1666            );
1667            assert_eq!(
1668                stats.configurations_unflushed, 0,
1669                "configuration pending queue should be cleared"
1670            );
1671            assert_eq!(stats.logs, 0, "logs should be cleared");
1672            assert_eq!(
1673                stats.metric_buckets.buckets, 0,
1674                "metric buckets should be cleared"
1675            );
1676            assert_eq!(
1677                stats.metric_buckets.series, 0,
1678                "metric series should be cleared"
1679            );
1680            assert!(
1681                worker.data.endpoints.is_empty(),
1682                "endpoints should be cleared"
1683            );
1684            assert!(worker.next_action.is_none(), "next_action should be None");
1685        }
1686
1687        /// After reset(), actions queued in the mailbox before the fork are discarded.
1688        #[tokio::test]
1689        async fn test_reset_drains_mailbox() {
1690            let (handle, mut worker) = build_test_worker();
1691
1692            // Enqueue several actions that should be discarded.
1693            handle
1694                .try_send_msg(TelemetryActions::AddDependency(Dependency {
1695                    name: "dep".to_string(),
1696                    version: None,
1697                }))
1698                .unwrap();
1699            let (id, log) = make_log(1, "pre-fork log");
1700            handle
1701                .try_send_msg(TelemetryActions::AddLog((id, log)))
1702                .unwrap();
1703
1704            // Stage one action as if trigger() had already stored it.
1705            worker.next_action = Some(TelemetryActions::Lifecycle(LifecycleAction::Start));
1706
1707            worker.reset();
1708
1709            // The mailbox must be empty and next_action cleared.
1710            assert!(
1711                worker.mailbox.try_recv().is_err(),
1712                "mailbox should be empty"
1713            );
1714            assert!(worker.next_action.is_none(), "next_action should be None");
1715            // None of the queued actions should have been applied to pending state.
1716            let stats = worker.stats();
1717            assert_eq!(
1718                stats.dependencies_stored, 0,
1719                "queued AddDependency must not be applied"
1720            );
1721            assert_eq!(
1722                stats.dependencies_unflushed, 0,
1723                "queued AddDependency must not be pending"
1724            );
1725            assert_eq!(stats.logs, 0, "queued AddLog must be discarded");
1726        }
1727
1728        /// After reset(), the worker accepts new telemetry and processes it normally.
1729        #[tokio::test]
1730        async fn test_worker_accepts_new_data_after_reset() {
1731            let (handle, mut worker) = build_test_worker();
1732            worker.flavor = TelemetryWorkerFlavor::MetricsLogs;
1733
1734            // Populate state before reset – this data must not survive.
1735            let (id, log) = make_log(99, "pre-fork");
1736            worker.data.logs.get_mut_or_insert(id, log);
1737
1738            worker.reset();
1739
1740            // Send a new log from the child side.
1741            let (id2, log2) = make_log(1, "post-fork");
1742            handle
1743                .try_send_msg(TelemetryActions::AddLog((id2, log2)))
1744                .unwrap();
1745
1746            // Simulate one trigger() + run() cycle.
1747            worker.trigger().await;
1748            worker.run().await;
1749
1750            let stats = worker.stats();
1751            // Only the new post-fork log should be buffered.
1752            assert_eq!(stats.logs, 1, "only post-fork log should be present");
1753        }
1754
1755        /// After reset(), lifecycle state needed to keep periodic flushing alive is preserved.
1756        #[tokio::test]
1757        async fn test_reset_preserves_started_and_deadlines() {
1758            let (_handle, mut worker) = build_test_worker();
1759
1760            worker.data.started = true;
1761            worker
1762                .deadlines
1763                .schedule_event(LifecycleAction::FlushMetricAggr)
1764                .unwrap();
1765            worker
1766                .deadlines
1767                .schedule_event(LifecycleAction::FlushData)
1768                .unwrap();
1769
1770            let deadlines_before = worker.deadlines.deadlines.clone();
1771
1772            worker.reset();
1773
1774            assert!(worker.data.started, "started flag should be preserved");
1775            assert_eq!(
1776                worker.deadlines.deadlines.len(),
1777                deadlines_before.len(),
1778                "scheduled deadlines should be preserved"
1779            );
1780            for ((_, actual), (_, expected)) in worker
1781                .deadlines
1782                .deadlines
1783                .iter()
1784                .zip(deadlines_before.iter())
1785            {
1786                assert_eq!(
1787                    actual, expected,
1788                    "deadline kinds should be preserved across reset"
1789                );
1790            }
1791        }
1792    }
1793
1794    #[cfg_attr(miri, ignore)]
1795    #[test]
1796    fn test_channel_close_flushes_and_parks_via_shared_runtime() {
1797        use httpmock::prelude::*;
1798        use libdd_common::Endpoint;
1799        use libdd_shared_runtime::SharedRuntime;
1800        use std::time::Duration;
1801
1802        const TELEMETRY_PATH: &str = "/telemetry/proxy/api/v2/apmtelemetry";
1803
1804        let server = MockServer::start();
1805        let mock = server.mock(|when, then| {
1806            when.method(POST).path(TELEMETRY_PATH);
1807            then.status(202).body("");
1808        });
1809
1810        let mut builder = TelemetryWorkerBuilder::new(
1811            "host".into(),
1812            "svc".into(),
1813            "lang".into(),
1814            "1".into(),
1815            "tv".into(),
1816        );
1817        builder
1818            .config
1819            .set_endpoint(Endpoint::from_slice(&server.url("/")))
1820            .unwrap();
1821        builder.runtime_id = Some("rid".into());
1822
1823        let shared_runtime = SharedRuntime::new().expect("SharedRuntime::new");
1824        let runtime_handle = shared_runtime
1825            .block_on(async { tokio::runtime::Handle::current() })
1826            .expect("runtime handle");
1827        let (telemetry_handle, worker) = builder.build_worker(Some(runtime_handle));
1828
1829        let _worker_handle = shared_runtime
1830            .spawn_worker(worker, false)
1831            .expect("spawn_worker");
1832
1833        // Drive the worker into the started state so Stop has work to flush.
1834        telemetry_handle.send_start().expect("send_start");
1835
1836        // Wait for the AppStarted batch so we know the worker reached started == true
1837        // before we close the channel.
1838        for _ in 0..50 {
1839            if mock.calls() >= 1 {
1840                break;
1841            }
1842            std::thread::sleep(Duration::from_millis(20));
1843        }
1844        assert!(
1845            mock.calls() >= 1,
1846            "worker should POST at least once after Start"
1847        );
1848
1849        // Close the mailbox by dropping the handle.
1850        let hits_before_close = mock.calls();
1851        drop(telemetry_handle);
1852
1853        // The worker must dispatch Lifecycle::Stop, which flushes a final batch, then park.
1854        for _ in 0..50 {
1855            if mock.calls() > hits_before_close {
1856                break;
1857            }
1858            std::thread::sleep(Duration::from_millis(20));
1859        }
1860        assert!(
1861            mock.calls() > hits_before_close,
1862            "worker should flush a final batch after the channel is closed"
1863        );
1864
1865        // Once parked, the worker must stop POSTing. Sample for a while and require the
1866        // hit count to stabilise (proves no Stop-emit loop).
1867        let stable_hits = mock.calls();
1868        std::thread::sleep(Duration::from_millis(300));
1869        assert_eq!(
1870            mock.calls(),
1871            stable_hits,
1872            "worker must stop POSTing after parking; observed {} extra hits",
1873            mock.calls().saturating_sub(stable_hits),
1874        );
1875    }
1876}