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