Skip to main content

datadog_agent_runtime/remote_agent/
mod.rs

1//! Remote agent registration and the services that the Datadog Agent queries.
2//!
3//! [`RemoteAgentBootstrap`] registers the process with the Datadog Agent over the Remote Agent Registry, keeps that
4//! registration alive, and creates everything that depends on it: the configuration stream, and the workers that serve
5//! the status, flare, and telemetry services and report diagnostic events back to the Datadog Agent.
6
7use std::collections::HashMap;
8use std::sync::OnceLock;
9use std::{sync::Arc, time::Duration};
10
11use async_trait::async_trait;
12use chrono::{DateTime, Utc};
13use datadog_agent_commons::ipc::{
14    client::RemoteAgentClient,
15    config::RemoteAgentClientConfiguration,
16    session::{SessionId, SessionIdHandle},
17};
18use datadog_protos::agent::v1::{
19    event::Details as RemoteAgentEventDetails, Event as RemoteAgentEvent, InvalidApiKeyEvent,
20    ReportRemoteAgentEventRequest,
21};
22use datadog_protos::agent::{
23    config_event,
24    flare::v1::{flare_provider_server::*, *},
25    status::v1::{status_provider_server::*, *},
26    telemetry::v1::{get_telemetry_response::*, telemetry_provider_server::*, *},
27    ConfigSetting as AgentConfigSetting, ConfigSnapshot,
28};
29use futures::StreamExt;
30use process_memory::Querier as MemoryQuerier;
31use prost_types::value::Kind;
32use saluki_api::{DynamicRoute, EndpointType};
33use saluki_common::sync::shutdown::ShutdownHandle;
34use saluki_common::task::spawn_traced_named;
35use saluki_config::dynamic::{ConfigSetting, ConfigUpdate, Provenance};
36use saluki_core::{
37    diagnostic::{subscribe_events, DiagnosticCollector, DiagnosticDetails, DiagnosticEvent},
38    observability::metrics::{
39        get_shared_metrics_state, AggregatedMetricsProcessor, AggregatedMetricsState, Reflector, RemapperRule,
40        TelemetryProcessor,
41    },
42    runtime::{
43        state::{DataspaceRegistry, DataspaceUpdate, IdentifierFilter, Subscription},
44        InitializationError, Supervisable, SupervisorFuture,
45    },
46};
47use saluki_error::{generic_error, GenericError};
48use saluki_io::net::{GrpcTargetAddress, ListenAddress};
49use serde_json::{Map, Value};
50use tokio::task::spawn_blocking;
51use tokio::time::{timeout, Instant};
52use tokio::{
53    select,
54    sync::{mpsc, oneshot, Mutex},
55    time::{interval, MissedTickBehavior},
56};
57use tonic::{server::NamedService, service::Routes, Status};
58use tracing::{debug, error, info, warn};
59
60mod status;
61pub use self::status::{StatusBuilder, StatusSectionProvider, StatusSectionWriter};
62
63const DEFAULT_REFRESH_INTERVAL: Duration = Duration::from_secs(30);
64const REFRESH_FAILED_RETRY_INTERVAL: Duration = Duration::from_secs(5);
65
66const SESSION_ID_METADATA_KEY: &str = "session_id";
67
68/// Remote agent initialization.
69///
70/// This helper type is used to coordinate the initialization of remote agent state by registering to the Core Agent and
71/// acquiring the necessary information to allow initialization of the remote agent itself to proceed.
72pub struct RemoteAgentBootstrap {
73    client: RemoteAgentClient,
74    session_id: SessionIdHandle,
75    internal_metrics: Reflector<AggregatedMetricsProcessor>,
76    dataspace: Arc<OnceLock<DataspaceRegistry>>,
77}
78
79impl RemoteAgentBootstrap {
80    /// Creates a new `RemoteAgentBootstrap` from the given configurations.
81    ///
82    /// A remote agent client is created and immediately attempts to register with the Core Agent, advertising
83    /// `secure_api_listen_address` as the address where the Core Agent reaches the status, flare, and telemetry
84    /// services. This function does not return until the first registration attempt finishes, whether successful or
85    /// not.
86    ///
87    /// Registration runs in a background task that this function spawns outside of any supervision tree. The task
88    /// refreshes the registration periodically, and registers again whenever a refresh fails, for the rest of the
89    /// process's lifetime: it doesn't stop on shutdown, or when the returned `RemoteAgentBootstrap` is dropped.
90    ///
91    /// # Errors
92    ///
93    /// Returns an error if `secure_api_listen_address` isn't a valid gRPC target address, if the client can't connect
94    /// to the Core Agent, or if the first registration attempt fails.
95    ///
96    /// If the first registration attempt fails, the background task keeps running and retries registration
97    /// periodically, so it may still register with the Core Agent later. Treat this error as fatal rather than calling
98    /// this function again: each call starts another background task, and each task registers its own session.
99    pub async fn new(
100        client_config: &RemoteAgentClientConfiguration, secure_api_listen_address: &ListenAddress,
101    ) -> Result<Self, GenericError> {
102        let api_listen_addr = GrpcTargetAddress::try_from_listen_addr(secure_api_listen_address)
103            .ok_or_else(|| generic_error!("Failed to get valid gRPC target address from secure API listen address."))?;
104
105        // Generate our remote agent state, which is mostly fixed but has a few dynamic bits.
106        let service_names = vec![
107            <StatusProviderServer<()> as NamedService>::NAME.to_string(),
108            <FlareProviderServer<()> as NamedService>::NAME.to_string(),
109            <TelemetryProviderServer<()> as NamedService>::NAME.to_string(),
110        ];
111
112        let (state, init_reg_rx) = RemoteAgentState::new(api_listen_addr, service_names);
113        let session_id = state.session_id.clone();
114
115        // Create our client, and then immediately start the registration loop.
116        //
117        // Wait for the result of the initial registration attempt before proceeding.
118        let client = RemoteAgentClient::connect(client_config).await?;
119        let task_name = format!("{}-remote-agent-task", saluki_metadata::get_app_details().identifier());
120        spawn_traced_named(task_name, run_remote_agent_registration_loop(client.clone(), state));
121
122        match init_reg_rx.await {
123            Ok(Ok(())) => (),
124            Ok(Err(e)) => return Err(e),
125            Err(_) => {
126                return Err(generic_error!(
127                    "Failed to initialize remote agent state. Registration task failed unexpectedly."
128                ))
129            }
130        }
131
132        Ok(Self {
133            client,
134            session_id,
135            internal_metrics: get_shared_metrics_state(),
136            dataspace: Arc::new(OnceLock::new()),
137        })
138    }
139
140    fn build_impl(
141        &self, status_sections: Vec<Arc<dyn StatusSectionProvider>>, remapper_rules: Vec<RemapperRule>,
142    ) -> RemoteAgentImpl {
143        RemoteAgentImpl {
144            started: Utc::now(),
145            internal_metrics: self.internal_metrics.clone(),
146            processor: telemetry_processor(remapper_rules),
147            status_sections,
148            session_id: self.session_id.clone(),
149            dataspace: Arc::clone(&self.dataspace),
150        }
151    }
152
153    /// Creates a worker that captures the dataspace from the supervisor context and makes it available to the
154    /// diagnostic artifact collection service.
155    ///
156    /// This worker must be added to the supervision tree. When it initializes, it runs inside a supervisor process
157    /// where the dataspace task-local is set, and fills the shared [`OnceLock`] so that `get_flare_files` can
158    /// collect diagnostic artifacts at request time. Without this, the gRPC handler tasks spawned by tonic would not
159    /// have access to the dataspace since tonic's `tokio::spawn` does not propagate task-locals.
160    pub fn create_dataspace_anchor(&self) -> DataspaceAnchorWorker {
161        DataspaceAnchorWorker {
162            dataspace: Arc::clone(&self.dataspace),
163        }
164    }
165
166    /// Creates a worker that reports diagnostic events to the Core Agent as remote agent events.
167    pub fn create_event_reporter(&self) -> RemoteAgentEventReporter {
168        RemoteAgentEventReporter {
169            client: self.client.clone(),
170            session_id: self.session_id.clone(),
171        }
172    }
173
174    /// Creates a worker that serves the status, flare, and telemetry services on the privileged API endpoint.
175    ///
176    /// The status service always reports the built-in main section fields (version, Git commit, architecture, start
177    /// time, and the Agent version this build was compiled against), followed by the fields written by each of the
178    /// given `status_sections`, in order.
179    ///
180    /// The telemetry service reports the internal metrics that match the given `telemetry_rules`, renamed to the names
181    /// the Datadog Agent expects. Metrics that match no rule are not reported, so if `telemetry_rules` is empty, the
182    /// service reports no metrics at all.
183    pub fn create_services_worker(
184        &self, status_sections: Vec<Arc<dyn StatusSectionProvider>>, telemetry_rules: Vec<RemapperRule>,
185    ) -> RemoteAgentServicesWorker {
186        let routes = Routes::new(StatusProviderServer::new(self.build_impl(status_sections, Vec::new())))
187            .add_service(FlareProviderServer::new(self.build_impl(Vec::new(), Vec::new())))
188            .add_service(TelemetryProviderServer::new(
189                self.build_impl(Vec::new(), telemetry_rules),
190            ));
191
192        RemoteAgentServicesWorker { routes }
193    }
194
195    /// Creates a config stream that receives configuration events from the Core Agent.
196    ///
197    /// The stream is fed by a background task that this method spawns outside of any supervision tree. The task
198    /// reconnects whenever the stream from the Core Agent ends, and doesn't stop on shutdown. It only stops when it
199    /// tries to deliver an update after the returned receiver is dropped.
200    pub fn create_config_stream(&self) -> mpsc::Receiver<ConfigUpdate> {
201        let (sender, receiver) = mpsc::channel(100);
202
203        let client = self.client.clone();
204        let session_id = self.session_id.clone();
205
206        tokio::spawn(run_config_stream_event_loop(client, sender, session_id));
207
208        receiver
209    }
210}
211
212struct RemoteAgentState {
213    pid: u32,
214    display_name: String,
215    flavor: String,
216    api_listen_addr: String,
217    session_id: SessionIdHandle,
218    service_names: Vec<String>,
219    initial_registration_tx: Option<oneshot::Sender<Result<(), GenericError>>>,
220}
221
222impl RemoteAgentState {
223    fn new(
224        api_listen_addr: GrpcTargetAddress, service_names: Vec<String>,
225    ) -> (Self, oneshot::Receiver<Result<(), GenericError>>) {
226        let app_details = saluki_metadata::get_app_details();
227        let display_name = app_details.full_name().to_string();
228        let flavor = app_details.full_name().replace(" ", "_").to_lowercase();
229
230        let (init_reg_tx, init_reg_rx) = oneshot::channel();
231
232        let state = Self {
233            pid: std::process::id(),
234            display_name,
235            flavor,
236            api_listen_addr: api_listen_addr.to_string(),
237            session_id: SessionIdHandle::empty(),
238            service_names,
239            initial_registration_tx: Some(init_reg_tx),
240        };
241
242        (state, init_reg_rx)
243    }
244}
245
246async fn run_remote_agent_registration_loop(mut client: RemoteAgentClient, mut state: RemoteAgentState) {
247    let mut loop_timer = interval(DEFAULT_REFRESH_INTERVAL);
248    loop_timer.set_missed_tick_behavior(MissedTickBehavior::Delay);
249
250    debug!("Remote Agent registration task started.");
251
252    loop {
253        loop_timer.tick().await;
254
255        match state.session_id.get() {
256            Some(session_id) => {
257                debug!(%session_id, "Refreshing registration with Datadog Agent.");
258
259                if client.refresh_remote_agent(&session_id).await.is_err() {
260                    loop_timer.reset_after(REFRESH_FAILED_RETRY_INTERVAL);
261                    state.session_id.update(None);
262                    warn!("Failed to refresh registration with the Datadog Agent. Resetting session ID and attempting to re-register shortly.");
263
264                    continue;
265                }
266            }
267            None => {
268                match client
269                    .register_remote_agent(
270                        state.pid,
271                        &state.display_name,
272                        &state.flavor,
273                        &state.api_listen_addr,
274                        state.service_names.clone(),
275                    )
276                    .await
277                {
278                    Ok(resp) => {
279                        let resp = resp.into_inner();
280                        let new_session_id = match SessionId::new(&resp.session_id) {
281                            Ok(session_id) => session_id,
282                            Err(e) => {
283                                warn!(error = %e, "Received invalid session ID from Datadog Agent after registation. Registration will be retried periodically in the background.");
284                                loop_timer.reset_after(DEFAULT_REFRESH_INTERVAL);
285                                continue;
286                            }
287                        };
288                        let new_refresh_interval = resp.recommended_refresh_interval_secs;
289                        info!(session_id = %new_session_id, "Successfully registered with the Datadog Agent. Refreshing every {} seconds.", new_refresh_interval);
290
291                        state.session_id.update(Some(new_session_id));
292                        loop_timer.reset_after(Duration::from_secs(new_refresh_interval as u64));
293
294                        if let Some(tx) = state.initial_registration_tx.take() {
295                            let _ = tx.send(Ok(()));
296                        }
297                    }
298                    Err(e) => {
299                        warn!(error = %e, "Failed to register with the Datadog Agent. Registration will be retried periodically in the background.");
300                        loop_timer.reset_after(DEFAULT_REFRESH_INTERVAL);
301
302                        if let Some(tx) = state.initial_registration_tx.take() {
303                            let _ = tx.send(Err(e));
304                        }
305                    }
306                }
307            }
308        }
309    }
310}
311
312async fn run_config_stream_event_loop(
313    mut client: RemoteAgentClient, sender: mpsc::Sender<ConfigUpdate>, session_id: SessionIdHandle,
314) {
315    loop {
316        debug!("Establishing a new config stream connection to the Core Agent.");
317
318        // Read the current session ID.
319        //
320        // We do this every loop since it can change in the background due to re-registration.
321        let current_session_id = session_id.wait_for_update().await;
322
323        let mut stream = client.stream_config_events(&current_session_id);
324        while let Some(result) = stream.next().await {
325            match result {
326                Ok(event) => {
327                    let update = match event.event {
328                        Some(config_event::Event::Snapshot(snapshot)) => {
329                            Some(ConfigUpdate::Snapshot(snapshot_to_settings(&snapshot)))
330                        }
331                        Some(config_event::Event::Update(update)) => update
332                            .setting
333                            .as_ref()
334                            .map(|setting| ConfigUpdate::Partial(setting_to_config_setting(setting))),
335                        None => {
336                            error!("Received a configuration update event with no data.");
337                            None
338                        }
339                    };
340
341                    if let Some(update) = update {
342                        if sender.send(update).await.is_err() {
343                            warn!("Dynamic configuration channel closed. Config stream shutting down.");
344                            return;
345                        }
346                    }
347                }
348                Err(e) => {
349                    error!("Error while reading config event stream: {}.", e);
350                }
351            }
352        }
353
354        debug!("Config stream ended, retrying in 5 seconds...");
355        tokio::time::sleep(Duration::from_secs(5)).await;
356    }
357}
358
359/// Sources that indicate the Agent supplied the value rather than an operator.
360const AGENT_DEFAULT_SOURCE: &str = "default";
361/// A value that was not set by the user nor does the schema define default value for.
362const AGENT_DECLARED_ONLY_SOURCE: &str = "schema";
363const AGENT_UNSET_SOURCES: [&str; 2] = [AGENT_DEFAULT_SOURCE, AGENT_DECLARED_ONLY_SOURCE];
364
365/// Converts a setting from the Agent's RPC wire protocol to our `ConfigSetting` type.
366fn setting_to_config_setting(setting: &AgentConfigSetting) -> ConfigSetting {
367    let provenance = if AGENT_UNSET_SOURCES.contains(&setting.source.as_str()) {
368        Provenance::Default
369    } else {
370        Provenance::Explicit
371    };
372
373    ConfigSetting::new(
374        setting.key.clone(),
375        proto_value_to_serde_value(&setting.value),
376        provenance,
377    )
378}
379
380/// Converts a `ConfigSnapshot` into the settings it carries.
381fn snapshot_to_settings(snapshot: &ConfigSnapshot) -> Vec<ConfigSetting> {
382    snapshot.settings.iter().map(setting_to_config_setting).collect()
383}
384
385/// Recursively converts a `google::protobuf::Value` into a `serde_json::Value`.
386fn proto_value_to_serde_value(proto_val: &Option<prost_types::Value>) -> Value {
387    let Some(kind) = proto_val.as_ref().and_then(|v| v.kind.as_ref()) else {
388        return Value::Null;
389    };
390
391    match kind {
392        Kind::NullValue(_) => Value::Null,
393        Kind::NumberValue(n) => {
394            if n.fract() == 0.0 && *n >= i64::MIN as f64 && *n <= i64::MAX as f64 {
395                Value::from(*n as i64)
396            } else {
397                Value::from(*n)
398            }
399        }
400        Kind::StringValue(s) => Value::String(s.clone()),
401        Kind::BoolValue(b) => Value::Bool(*b),
402        Kind::StructValue(s) => {
403            let json_map: Map<String, Value> = s
404                .fields
405                .iter()
406                .map(|(k, v)| (k.clone(), proto_value_to_serde_value(&Some(v.clone()))))
407                .collect();
408            Value::Object(json_map)
409        }
410        Kind::ListValue(l) => {
411            let json_list: Vec<Value> = l
412                .values
413                .iter()
414                .map(|v| proto_value_to_serde_value(&Some(v.clone())))
415                .collect();
416            Value::Array(json_list)
417        }
418    }
419}
420
421/// The implementation behind the status, flare, and telemetry services that the Datadog Agent queries.
422struct RemoteAgentImpl {
423    started: DateTime<Utc>,
424    internal_metrics: Reflector<AggregatedMetricsProcessor>,
425    processor: Option<Mutex<TelemetryProcessor>>,
426    status_sections: Vec<Arc<dyn StatusSectionProvider>>,
427    session_id: SessionIdHandle,
428    dataspace: Arc<OnceLock<DataspaceRegistry>>,
429}
430
431/// Creates the processor behind the telemetry service, or `None` if there are no remapper rules.
432///
433/// A `TelemetryProcessor` without rules renders every metric under its raw name, so we skip it entirely rather than
434/// report metrics that no rule selected.
435fn telemetry_processor(remapper_rules: Vec<RemapperRule>) -> Option<Mutex<TelemetryProcessor>> {
436    (!remapper_rules.is_empty()).then(|| Mutex::new(TelemetryProcessor::new().with_remapper_rules(remapper_rules)))
437}
438
439/// Renders the telemetry payload for the given internal metrics.
440///
441/// Without a processor, the payload is empty.
442async fn render_telemetry(processor: Option<&Mutex<TelemetryProcessor>>, state: &AggregatedMetricsState) -> String {
443    match processor {
444        Some(processor) => processor.lock().await.process(state),
445        None => String::new(),
446    }
447}
448
449impl RemoteAgentImpl {
450    async fn session_id_middleware<Resp, Next>(&self, next: Next) -> Result<tonic::Response<Resp>, Status>
451    where
452        Next: AsyncFnOnce() -> Result<tonic::Response<Resp>, Status>,
453    {
454        let metadata_session_id = self
455            .session_id
456            .get()
457            .ok_or(Status::failed_precondition(
458                "session ID not set; must be registered with Core Agent",
459            ))?
460            .to_grpc_header_value();
461
462        next().await.map(|mut resp| {
463            resp.metadata_mut().append(SESSION_ID_METADATA_KEY, metadata_session_id);
464            resp
465        })
466    }
467}
468
469#[async_trait]
470impl StatusProvider for RemoteAgentImpl {
471    async fn get_status_details(
472        &self, _request: tonic::Request<GetStatusDetailsRequest>,
473    ) -> Result<tonic::Response<GetStatusDetailsResponse>, Status> {
474        return self
475            .session_id_middleware(async || {
476                let app_details = saluki_metadata::get_app_details();
477
478                let mut builder = StatusBuilder::new();
479                builder
480                    .main_section()
481                    .set_field("Version", app_details.version().raw())
482                    .set_field("Git Commit", app_details.git_hash())
483                    .set_field("Architecture", app_details.target_arch())
484                    .set_field("Started", self.started.to_rfc3339());
485
486                // Surface the Agent version this build was compiled against, for debugging version-gated behavior.
487                if let Some(agent_version) = datadog_agent_commons::agent_version::version_string() {
488                    builder
489                        .main_section()
490                        .set_field("Built Against Agent Version", agent_version);
491                }
492
493                for section in &self.status_sections {
494                    section.write_status(&mut builder);
495                }
496
497                Ok(tonic::Response::new(builder.into_response()))
498            })
499            .await;
500    }
501}
502
503#[async_trait]
504impl TelemetryProvider for RemoteAgentImpl {
505    async fn get_telemetry(
506        &self, _request: tonic::Request<GetTelemetryRequest>,
507    ) -> Result<tonic::Response<GetTelemetryResponse>, Status> {
508        return self
509            .session_id_middleware(async || {
510                let prom_text = render_telemetry(self.processor.as_ref(), self.internal_metrics.state()).await;
511
512                Ok(tonic::Response::new(GetTelemetryResponse {
513                    payload: Some(Payload::PromText(prom_text)),
514                }))
515            })
516            .await;
517    }
518}
519
520/// Timeout for the tokio task dump step within [`FlareProvider::get_flare_files`].
521///
522/// `Handle::dump()` may never resolve if a runtime worker is blocked for more than 250 ms. We use
523/// a conservative 5-second budget so that a hung runtime still allows the other diagnostic artifacts
524/// to be collected and returned.
525#[cfg(all(
526    feature = "taskdump",
527    target_os = "linux",
528    any(
529        target_arch = "aarch64",
530        target_arch = "x86",
531        target_arch = "x86_64",
532        target_arch = "s390x"
533    )
534))]
535const TASK_DUMP_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
536
537/// Timeout for collecting all component-owned diagnostic artifacts.
538///
539/// Each `collect()` closure is run on a blocking thread via `spawn_blocking`. If the overall
540/// budget is exhausted, collection stops and whatever artifacts were gathered are returned.
541/// This ensures that a slow or deadlocked component cannot stall the entire response.
542const DIAGNOSTIC_COLLECT_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(4);
543
544/// Maximum size in bytes for a single diagnostic artifact.
545///
546/// Artifacts that exceed this limit are truncated and a truncation marker is appended so that
547/// readers know the content is incomplete. High-cardinality hosts can produce very large workload
548/// tag dumps; this cap keeps the overall archive size predictable.
549const DIAGNOSTIC_ARTIFACT_MAX_BYTES: usize = 5 * 1024 * 1024; // 5 MiB
550
551/// Truncation marker appended to diagnostic artifacts that exceed [`DIAGNOSTIC_ARTIFACT_MAX_BYTES`] in size.
552const DIAGNOSTIC_TRUNCATION_MARKER: &[u8] = b"\n\n[... truncated: artifact exceeded the 5 MiB per-file limit ...]\n";
553
554/// Caps `data` to [`DIAGNOSTIC_ARTIFACT_MAX_BYTES`], appending [`DIAGNOSTIC_TRUNCATION_MARKER`] if truncated.
555fn cap_artifact_data(mut data: Vec<u8>) -> Vec<u8> {
556    if data.len() > DIAGNOSTIC_ARTIFACT_MAX_BYTES {
557        data.truncate(DIAGNOSTIC_ARTIFACT_MAX_BYTES);
558        data.extend_from_slice(DIAGNOSTIC_TRUNCATION_MARKER);
559    }
560    data
561}
562
563/// Returns the number of open file descriptors for the current process.
564///
565/// Reads from `/proc/self/fd` on Linux. Returns `"unavailable"` on other platforms.
566#[cfg(target_os = "linux")]
567fn fd_count() -> String {
568    std::fs::read_dir("/proc/self/fd")
569        .map(|entries| entries.count().to_string())
570        .unwrap_or_else(|_| "unavailable".to_string())
571}
572
573#[cfg(not(target_os = "linux"))]
574fn fd_count() -> String {
575    "unavailable".to_string()
576}
577
578/// Returns the number of threads in the current process.
579///
580/// Reads the `Threads:` field from `/proc/self/status` on Linux. Returns `"unavailable"` on other
581/// platforms.
582#[cfg(target_os = "linux")]
583fn thread_count() -> String {
584    std::fs::read_to_string("/proc/self/status")
585        .ok()
586        .and_then(|s| {
587            s.lines()
588                .find(|l| l.starts_with("Threads:"))
589                .and_then(|l| l.split_whitespace().nth(1).map(str::to_string))
590        })
591        .unwrap_or_else(|| "unavailable".to_string())
592}
593
594#[cfg(not(target_os = "linux"))]
595fn thread_count() -> String {
596    "unavailable".to_string()
597}
598
599#[async_trait]
600impl FlareProvider for RemoteAgentImpl {
601    async fn get_flare_files(
602        &self, _request: tonic::Request<GetFlareFilesRequest>,
603    ) -> Result<tonic::Response<GetFlareFilesResponse>, Status> {
604        return self
605            .session_id_middleware(async || {
606                let mut files: HashMap<String, Vec<u8>> = HashMap::new();
607
608                // Grab and run all asserted diagnostic collectors.
609                if let Some(dataspace) = self.dataspace.get() {
610                    let collectors = dataspace.current_values::<DiagnosticCollector>(IdentifierFilter::all());
611                    let total_collectors = collectors.len();
612                    let deadline = Instant::now() + DIAGNOSTIC_COLLECT_TIMEOUT;
613
614                    for collector in collectors {
615                        if Instant::now() >= deadline {
616                            break;
617                        }
618
619                        let artifact_name = collector.artifact_name().to_string();
620                        let remaining = deadline.saturating_duration_since(Instant::now());
621
622                        let collect = timeout(
623                            remaining,
624                            spawn_blocking(move || collector.collect()),
625                        );
626                        let artifact_data = match collect.await {
627                            Ok(Ok(data)) => data,
628                            Ok(Err(e)) => format!("collection panicked for '{artifact_name}': {e}").into_bytes(),
629                            Err(_) => format!("collection timed out for '{artifact_name}'").into_bytes(),
630                        };
631
632                        files.insert(artifact_name, cap_artifact_data(artifact_data));
633                    }
634
635                    if files.len() < total_collectors {
636                        warn!("Diagnostic artifact collection timed out; collected {}/{total_collectors} artifacts.", files.len());
637                    }
638                }
639
640                // Process-level artifacts.
641                //
642                // These don't belong to any individual component — they describe the process
643                // itself. They stay hardwired here since there is no natural component owner to
644                // assert a handle for them.
645
646                // Process info: pid, uptime, RSS, args, fd count, thread count.
647                let pid = std::process::id();
648                let uptime = Utc::now().signed_duration_since(self.started);
649                let args: Vec<String> = std::env::args().collect();
650                let rss_display = MemoryQuerier::default()
651                    .resident_set_size()
652                    .map(|b| b.to_string())
653                    .unwrap_or_else(|| "unavailable".to_string());
654                let process_info = format!(
655                    "pid: {pid}\nuptime_seconds: {uptime}\nrss_bytes: {rss_display}\nargs: {args:?}\nfd_count: {fd_count}\nthread_count: {thread_count}\n",
656                    uptime = uptime.num_seconds(),
657                    fd_count = fd_count(),
658                    thread_count = thread_count(),
659                );
660                files.insert("runtime_debug_info.log".to_string(), cap_artifact_data(process_info.into_bytes()));
661
662                // Tokio task dump (only with the `taskdump` feature enabled, on targets where Tokio supports task dumps).
663                //
664                // Wrapped in an explicit timeout because `Handle::dump()`
665                // may never resolve if a runtime worker is blocked for
666                // more than 250ms
667                #[cfg(all(
668                    feature = "taskdump",
669                    target_os = "linux",
670                    any(
671                        target_arch = "aarch64",
672                        target_arch = "x86",
673                        target_arch = "x86_64",
674                        target_arch = "s390x"
675                    )
676                ))]
677                {
678                    let task_dump = match timeout(
679                        TASK_DUMP_TIMEOUT,
680                        tokio::runtime::Handle::current().dump(),
681                    )
682                    .await
683                    {
684                        Ok(dump) => {
685                            let mut output = String::new();
686                            for (i, task) in dump.tasks().iter().enumerate() {
687                                output.push_str(&format!("task {i}:\n{}\n", task.trace()));
688                            }
689                            output
690                        }
691                        Err(_) => format!(
692                            "task dump timed out after {}ms — a runtime worker may be blocked",
693                            TASK_DUMP_TIMEOUT.as_millis()
694                        ),
695                    };
696                    files.insert("runtime-dump.txt".to_string(), cap_artifact_data(task_dump.into_bytes()));
697                }
698
699                Ok(tonic::Response::new(GetFlareFilesResponse { files }))
700            })
701            .await;
702    }
703}
704
705/// A worker that captures the dataspace from the supervisor context and stores it in a shared
706/// [`OnceLock`] for use by the diagnostic artifact collection service.
707///
708/// The gRPC handler runs in tasks spawned by tonic, which do not inherit tokio task-locals.
709/// This worker bridges that gap by capturing the dataspace during its own `initialize()` call,
710/// which runs inside the supervisor process where the dataspace is available.
711pub struct DataspaceAnchorWorker {
712    dataspace: Arc<OnceLock<DataspaceRegistry>>,
713}
714
715#[async_trait]
716impl Supervisable for DataspaceAnchorWorker {
717    fn name(&self) -> &str {
718        "flare-dataspace-anchor"
719    }
720
721    async fn initialize(&self, process_shutdown: ShutdownHandle) -> Result<SupervisorFuture, InitializationError> {
722        if let Some(ds) = DataspaceRegistry::try_current() {
723            let _ = self.dataspace.set(ds);
724        }
725
726        Ok(Box::pin(async move {
727            process_shutdown.await;
728            Ok(())
729        }))
730    }
731}
732
733/// A worker that serves the status, flare, and telemetry services on the privileged API endpoint.
734///
735/// The services are asserted as a dynamic gRPC route, so the privileged API endpoint in the same supervision tree serves
736/// them for as long as this worker runs.
737pub struct RemoteAgentServicesWorker {
738    routes: Routes,
739}
740
741#[async_trait]
742impl Supervisable for RemoteAgentServicesWorker {
743    fn name(&self) -> &str {
744        "remote-agent-services"
745    }
746
747    async fn initialize(&self, process_shutdown: ShutdownHandle) -> Result<SupervisorFuture, InitializationError> {
748        let route = DynamicRoute::grpc(EndpointType::Privileged, self.routes.clone());
749
750        Ok(Box::pin(async move {
751            let dataspace =
752                DataspaceRegistry::try_current().ok_or_else(|| generic_error!("Dataspace not available."))?;
753
754            dataspace.assert(route, "remote-agent-services");
755
756            process_shutdown.await;
757            Ok(())
758        }))
759    }
760}
761
762/// A worker that reports diagnostic events to the Core Agent as remote agent events.
763///
764/// It subscribes to [`DiagnosticEvent`]s emitted anywhere in the process converts the ones it recognizes into the
765/// Datadog Agent's remote agent event representation, and reports them to the Core Agent over the remote agent gRPC
766/// API. `InvalidApiKey` events are rate limited, so a sustained stream of them cannot flood the Core Agent.
767pub struct RemoteAgentEventReporter {
768    client: RemoteAgentClient,
769    session_id: SessionIdHandle,
770}
771
772#[async_trait]
773impl Supervisable for RemoteAgentEventReporter {
774    fn name(&self) -> &str {
775        "remote_agent_event_reporter"
776    }
777
778    async fn initialize(&self, process_shutdown: ShutdownHandle) -> Result<SupervisorFuture, InitializationError> {
779        let client = self.client.clone();
780        let session_id = self.session_id.clone();
781
782        Ok(Box::pin(async move {
783            let subscription = subscribe_events(IdentifierFilter::all())
784                .map_err(|e| generic_error!("Failed to subscribe to diagnostic events: {}", e))?;
785
786            select! {
787                _ = process_shutdown => Ok(()),
788                result = run_event_reporter(client, session_id, subscription) => result,
789            }
790        }))
791    }
792}
793
794async fn run_event_reporter(
795    mut client: RemoteAgentClient, session_id: SessionIdHandle, mut subscription: Subscription<DiagnosticEvent>,
796) -> Result<(), GenericError> {
797    debug!("Remote Agent event reporter started.");
798
799    let mut invalid_api_key_reports = InvalidApiKeyReportThrottle::default();
800
801    while let Some(update) = subscription.recv().await {
802        let DataspaceUpdate::Message(_, event) = update else {
803            continue;
804        };
805
806        let Some(remote_agent_event) = diagnostic_to_remote_agent_event(&event) else {
807            debug!("Received diagnostic event with no remote agent event mapping; skipping.");
808            continue;
809        };
810
811        let Some(session_id) = session_id.get() else {
812            debug!("No active remote agent session; dropping diagnostic event.");
813            continue;
814        };
815
816        let invalid_api_key = matches!(event.details(), &DiagnosticDetails::InvalidApiKey);
817        if invalid_api_key && !invalid_api_key_reports.is_allowed() {
818            debug!("Throttling an InvalidApiKey event report.");
819            continue;
820        }
821
822        let request = ReportRemoteAgentEventRequest {
823            session_id: session_id.as_str().to_string(),
824            events: vec![remote_agent_event],
825        };
826
827        match client.report_remote_agent_event(request).await {
828            Ok(_) => {
829                // The window opens only on delivery, so a failed report never holds back the next one.
830                if invalid_api_key {
831                    invalid_api_key_reports.record();
832                }
833            }
834            Err(e) => {
835                warn!(error = %e, "Failed to report remote agent event to the Datadog Agent.");
836            }
837        }
838    }
839
840    debug!("Remote Agent event reporter stopped.");
841    Ok(())
842}
843
844/// Minimum time between `InvalidApiKey` reports.
845const INVALID_API_KEY_REPORT_INTERVAL: Duration = Duration::from_secs(30);
846
847/// Rate limits `InvalidApiKey` reports.
848///
849/// Checking and recording are separate so that only a delivered report opens the window: a report that never
850/// reaches the Core Agent leaves the next one free to send immediately.
851#[derive(Debug, Default)]
852struct InvalidApiKeyReportThrottle {
853    last_reported: Option<Instant>,
854}
855
856impl InvalidApiKeyReportThrottle {
857    fn is_allowed(&self) -> bool {
858        self.last_reported
859            .is_none_or(|last| last.elapsed() >= INVALID_API_KEY_REPORT_INTERVAL)
860    }
861
862    fn record(&mut self) {
863        self.last_reported = Some(Instant::now());
864    }
865}
866
867/// Converts a [`DiagnosticEvent`] into the Datadog Agent's remote agent event representation.
868///
869/// Returns `None` for diagnostic details that have no corresponding remote agent event variant, in which case the
870/// event is not reported.
871fn diagnostic_to_remote_agent_event(event: &DiagnosticEvent) -> Option<RemoteAgentEvent> {
872    let details = match event.details() {
873        DiagnosticDetails::InvalidApiKey => RemoteAgentEventDetails::InvalidApiKey(InvalidApiKeyEvent {}),
874        // `DiagnosticDetails` is `#[non_exhaustive]`; any future variant without a remote agent mapping is skipped.
875        _ => return None,
876    };
877
878    Some(RemoteAgentEvent {
879        message: event.message().to_string(),
880        details: Some(details),
881    })
882}
883
884#[cfg(test)]
885mod tests {
886    use saluki_core::{
887        data_model::event::{
888            metric::{context::Context, Metric},
889            Event,
890        },
891        observability::metrics::{MetricsSnapshot, Processor as _},
892    };
893
894    use super::*;
895
896    #[test]
897    fn cap_artifact_data_leaves_small_payloads_unchanged() {
898        let input = b"hello world".to_vec();
899        let output = cap_artifact_data(input.clone());
900        assert_eq!(output, input);
901    }
902
903    #[test]
904    fn cap_artifact_data_truncates_at_limit_and_appends_marker() {
905        let input = vec![b'x'; DIAGNOSTIC_ARTIFACT_MAX_BYTES + 1024];
906        let output = cap_artifact_data(input);
907
908        // Total length is capped at the limit plus the marker.
909        assert_eq!(
910            output.len(),
911            DIAGNOSTIC_ARTIFACT_MAX_BYTES + DIAGNOSTIC_TRUNCATION_MARKER.len()
912        );
913
914        // The first DIAGNOSTIC_ARTIFACT_MAX_BYTES bytes are all 'x'.
915        assert!(output[..DIAGNOSTIC_ARTIFACT_MAX_BYTES].iter().all(|&b| b == b'x'));
916
917        // The marker is appended at the end.
918        assert!(output.ends_with(DIAGNOSTIC_TRUNCATION_MARKER));
919    }
920
921    #[test]
922    fn cap_artifact_data_at_exact_limit_is_not_truncated() {
923        let input = vec![b'y'; DIAGNOSTIC_ARTIFACT_MAX_BYTES];
924        let output = cap_artifact_data(input.clone());
925        assert_eq!(output, input);
926    }
927
928    fn metrics_state(names: &[&'static str]) -> AggregatedMetricsState {
929        let processor = AggregatedMetricsProcessor;
930        let state = processor.build_initial_state();
931        let upserts = names
932            .iter()
933            .map(|name| Event::Metric(Metric::counter(Context::from_static_parts(name, &[]), 1.0)))
934            .collect();
935        processor.process(
936            MetricsSnapshot {
937                upserts,
938                evictions: Vec::new(),
939            },
940            &state,
941        );
942        state
943    }
944
945    #[tokio::test]
946    async fn telemetry_without_remapper_rules_is_empty() {
947        let state = metrics_state(&["test.events_received_total"]);
948        let processor = telemetry_processor(Vec::new());
949
950        assert_eq!(render_telemetry(processor.as_ref(), &state).await, "");
951    }
952
953    #[tokio::test]
954    async fn telemetry_only_reports_metrics_matched_by_remapper_rules() {
955        let state = metrics_state(&["test.events_received_total", "test.unmatched_total"]);
956        let processor = telemetry_processor(vec![RemapperRule::by_name(
957            "test.events_received_total",
958            "events_received",
959        )]);
960
961        let prom_text = render_telemetry(processor.as_ref(), &state).await;
962        assert!(prom_text.contains("events_received 1"), "{prom_text}");
963        assert!(!prom_text.contains("unmatched"), "{prom_text}");
964    }
965
966    fn agent_setting(source: &str, key: &str, value: &str) -> AgentConfigSetting {
967        AgentConfigSetting {
968            source: source.to_string(),
969            key: key.to_string(),
970            value: Some(prost_types::Value {
971                kind: Some(Kind::StringValue(value.to_string())),
972            }),
973        }
974    }
975
976    #[test]
977    fn an_agent_default_is_marked_as_a_default() {
978        let setting = setting_to_config_setting(&agent_setting(
979            AGENT_DEFAULT_SOURCE,
980            "dd_url",
981            "https://app.datadoghq.com",
982        ));
983
984        assert_eq!(setting.key, "dd_url");
985        assert_eq!(setting.value, Value::from("https://app.datadoghq.com"));
986        assert_eq!(setting.provenance, Provenance::Default);
987    }
988
989    #[test]
990    fn a_schema_setting_is_marked_as_a_default() {
991        let setting = setting_to_config_setting(&AgentConfigSetting {
992            source: "schema".to_string(),
993            key: "api_key".to_string(),
994            value: None,
995        });
996
997        assert_eq!(setting.value, Value::Null);
998        assert_eq!(setting.provenance, Provenance::Default);
999    }
1000
1001    #[test]
1002    fn null_values_are_preserved_with_their_provenance() {
1003        for source in [AGENT_DEFAULT_SOURCE, "schema", "file", "remote-config"] {
1004            let setting = setting_to_config_setting(&AgentConfigSetting {
1005                source: source.to_string(),
1006                key: "api_key".to_string(),
1007                value: Some(prost_types::Value {
1008                    kind: Some(Kind::NullValue(0)),
1009                }),
1010            });
1011
1012            let expected_provenance = if [AGENT_DEFAULT_SOURCE, "schema"].contains(&source) {
1013                Provenance::Default
1014            } else {
1015                Provenance::Explicit
1016            };
1017            assert_eq!(setting.value, Value::Null);
1018            assert_eq!(setting.provenance, expected_provenance, "source {source}");
1019        }
1020    }
1021
1022    #[test]
1023    fn an_empty_string_value_is_kept_with_its_provenance() {
1024        // An empty string is still a value; provenance comes from its source, not its content.
1025        for (source, provenance) in [
1026            ("file", Provenance::Explicit),
1027            ("default", Provenance::Default),
1028            ("schema", Provenance::Default),
1029        ] {
1030            let setting = setting_to_config_setting(&agent_setting(source, "site", ""));
1031
1032            assert_eq!(setting.value, Value::from(""));
1033            assert_eq!(setting.provenance, provenance);
1034        }
1035    }
1036
1037    #[test]
1038    fn operator_supplied_sources_are_marked_as_explicit() {
1039        // Unknown sources are treated as explicit inputs rather than defaults.
1040        for source in [
1041            "file",
1042            "environment-variable",
1043            "remote-config",
1044            "cli",
1045            "source-from-the-future",
1046        ] {
1047            let setting = setting_to_config_setting(&agent_setting(source, "dd_url", "https://app.datadoghq.eu"));
1048
1049            assert_eq!(
1050                setting.provenance,
1051                Provenance::Explicit,
1052                "source {source} should be explicit"
1053            );
1054        }
1055    }
1056
1057    #[test]
1058    fn snapshot_settings_keep_order_values_and_provenance() {
1059        let snapshot = ConfigSnapshot {
1060            origin: "core-agent".to_string(),
1061            sequence_id: 1,
1062            settings: vec![
1063                agent_setting("file", "site", "datadoghq.eu"),
1064                agent_setting("default", "dd_url", "https://app.datadoghq.com"),
1065                agent_setting(AGENT_DECLARED_ONLY_SOURCE, "api_key", ""),
1066            ],
1067        };
1068
1069        let settings = snapshot_to_settings(&snapshot);
1070
1071        assert_eq!(
1072            settings,
1073            vec![
1074                ConfigSetting::explicit("site", Value::from("datadoghq.eu")),
1075                ConfigSetting::new("dd_url", Value::from("https://app.datadoghq.com"), Provenance::Default),
1076                ConfigSetting::new("api_key", Value::from(""), Provenance::Default),
1077            ]
1078        );
1079    }
1080
1081    #[test]
1082    fn diagnostic_to_remote_agent_event_maps_invalid_api_key() {
1083        let event = DiagnosticEvent::new("credentials rejected", DiagnosticDetails::InvalidApiKey);
1084        let converted =
1085            diagnostic_to_remote_agent_event(&event).expect("InvalidApiKey should map to a remote agent event");
1086
1087        assert_eq!(converted.message, "credentials rejected");
1088        assert!(matches!(
1089            converted.details,
1090            Some(RemoteAgentEventDetails::InvalidApiKey(_))
1091        ));
1092    }
1093
1094    #[tokio::test(start_paused = true)]
1095    async fn invalid_api_key_reports_are_throttled_to_thirty_seconds() {
1096        let mut throttle = InvalidApiKeyReportThrottle::default();
1097
1098        assert!(throttle.is_allowed());
1099        throttle.record();
1100        assert!(!throttle.is_allowed());
1101
1102        tokio::time::advance(Duration::from_secs(30)).await;
1103        assert!(throttle.is_allowed());
1104        throttle.record();
1105        assert!(!throttle.is_allowed());
1106    }
1107
1108    #[tokio::test(start_paused = true)]
1109    async fn a_report_slightly_before_the_window_elapses_is_held() {
1110        let mut throttle = InvalidApiKeyReportThrottle::default();
1111
1112        throttle.record();
1113        tokio::time::advance(Duration::from_secs(29)).await;
1114        assert!(!throttle.is_allowed());
1115    }
1116
1117    #[test]
1118    fn a_report_that_was_never_delivered_does_not_open_the_throttle_window() {
1119        let mut throttle = InvalidApiKeyReportThrottle::default();
1120
1121        assert!(throttle.is_allowed());
1122        assert!(throttle.is_allowed());
1123        assert!(throttle.is_allowed());
1124
1125        throttle.record();
1126        assert!(!throttle.is_allowed());
1127    }
1128}