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