async_profiler_agent/reporter/
local.rs

1// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4//! A reporter that reports into a directory.
5
6use async_trait::async_trait;
7use chrono::SecondsFormat;
8use std::path::PathBuf;
9use std::time::SystemTime;
10use thiserror::Error;
11
12use crate::metadata::ReportMetadata;
13
14use super::Reporter;
15
16#[derive(Error, Debug)]
17enum LocalReporterError {
18    #[error("{0}")]
19    IoError(#[from] std::io::Error),
20}
21
22/// A reporter that reports into a directory.
23///
24/// The files are reported with the filename `yyyy-mm-ddTHH-MM-SSZ.jfr`
25///
26/// It does not currently use the metadata, so if you are using
27/// [LocalReporter] alone, rather than inside a [MultiReporter], you
28/// can just use [AgentMetadata::NoMetadata] as metadata.
29///
30/// [AgentMetadata::NoMetadata]: crate::metadata::AgentMetadata::NoMetadata
31/// [MultiReporter]: crate::reporter::multi::MultiReporter
32///
33/// ### Example
34///
35/// ```
36/// # use async_profiler_agent::metadata::AgentMetadata;
37/// # use async_profiler_agent::profiler::{ProfilerBuilder, SpawnError};
38/// # #[tokio::main]
39/// # async fn main() -> Result<(), SpawnError> {
40/// let profiler = ProfilerBuilder::default()
41///    .with_local_reporter("/tmp/profiles")
42///    .build();
43/// # if false { // don't spawn the profiler in doctests
44/// profiler.spawn()?;
45/// # }
46/// # Ok(())
47/// # }
48/// ```
49#[derive(Debug)]
50pub struct LocalReporter {
51    directory: PathBuf,
52}
53
54impl LocalReporter {
55    /// Instantiate a new LocalReporter writing into the provided directory.
56    pub fn new(directory: impl Into<PathBuf>) -> Self {
57        LocalReporter {
58            directory: directory.into(),
59        }
60    }
61
62    /// Writes the jfr file to disk.
63    async fn report_profiling_data(
64        &self,
65        jfr: Vec<u8>,
66        _metadata_obj: &ReportMetadata<'_>,
67    ) -> Result<(), std::io::Error> {
68        let time: chrono::DateTime<chrono::Utc> = SystemTime::now().into();
69        let time = time
70            .to_rfc3339_opts(SecondsFormat::Secs, true)
71            .replace(":", "-");
72        tracing::debug!("reporting {time}.jfr");
73        let file_name = format!("{time}.jfr");
74        tokio::fs::write(self.directory.join(file_name), jfr).await?;
75        Ok(())
76    }
77}
78
79#[async_trait]
80impl Reporter for LocalReporter {
81    async fn report(
82        &self,
83        jfr: Vec<u8>,
84        metadata: &ReportMetadata,
85    ) -> Result<(), Box<dyn std::error::Error + Send>> {
86        self.report_profiling_data(jfr, metadata)
87            .await
88            .map_err(|e| Box::new(LocalReporterError::IoError(e)) as _)
89    }
90}
91
92#[cfg(test)]
93mod test {
94    use std::path::Path;
95
96    use crate::{
97        metadata::DUMMY_METADATA,
98        reporter::{Reporter, local::LocalReporter},
99    };
100
101    #[tokio::test]
102    async fn test_local_reporter() {
103        let dir = tempfile::tempdir().unwrap();
104        let reporter = LocalReporter::new(dir.path());
105        reporter
106            .report(b"JFR".into(), &DUMMY_METADATA)
107            .await
108            .unwrap();
109        let jfr_file = std::fs::read_dir(dir.path())
110            .unwrap()
111            .flat_map(|f| f.ok())
112            .filter(|f| {
113                Path::new(&f.file_name())
114                    .extension()
115                    .is_some_and(|e| e == "jfr")
116            })
117            .next()
118            .unwrap();
119        assert_eq!(tokio::fs::read(jfr_file.path()).await.unwrap(), b"JFR");
120    }
121}