Skip to main content

telemetric_client/
client.rs

1use std::{
2    io::Write as _,
3    sync::atomic::{AtomicBool, Ordering},
4};
5
6use anyhow::anyhow;
7use futures::StreamExt;
8use telemetric_messages as messages;
9use yaaral::http::{self, HttpClientInterface, Response};
10
11/// A telemetric client for reporting measurements to a server
12pub struct Client<RT: HttpClientInterface> {
13    runtime: RT,
14    uri: String,
15    software_id: String,
16    reporting_enabled: AtomicBool,
17    app_info: (String, String, String),
18}
19
20impl<RT: HttpClientInterface> Client<RT> {
21    /// Create a new telemetric client
22    /// app_info takes the form (qualifier, organization, application) and is used to locate where to store the telemetric data
23    pub fn new(
24        runtime: RT,
25        uri: impl Into<String>,
26        software_id: impl Into<String>,
27        app_info: (impl Into<String>, impl Into<String>, impl Into<String>),
28    ) -> Self {
29        Self {
30            runtime,
31            uri: uri.into(),
32            software_id: software_id.into(),
33            reporting_enabled: true.into(),
34            app_info: (app_info.0.into(), app_info.1.into(), app_info.2.into()),
35        }
36    }
37    /// Enable reporting of telemetric data
38    pub fn enable_reporting(&self) {
39        self.reporting_enabled.store(true, Ordering::Relaxed);
40    }
41    /// Disable reporting of telemetric data
42    pub fn disable_reporting(&self) {
43        self.reporting_enabled.store(false, Ordering::Relaxed);
44    }
45    /// Report a measurement to the server
46    pub async fn report(&self, key: String, value: impl serde::Serialize) -> anyhow::Result<()> {
47        if !self.reporting_enabled.load(Ordering::Relaxed) {
48            return Ok(());
49        }
50        let measurement = messages::Measurement {
51            key,
52            software_id: self.software_id.clone(),
53            value: serde_json::to_value(value)?,
54        };
55        let request =
56            http::Request::from_uri(format!("{}/api/report", self.uri)).json(&measurement)?;
57        let respnse = self.runtime.wpost(request).await;
58        if respnse.status().is_success() {
59            let mut stream = respnse.into_stream();
60            let mut buf = Vec::new();
61            while let Some(chunk) = stream.next().await {
62                let chunk = chunk.map_err(|e| anyhow!("{:?}", e))?;
63                buf.extend_from_slice(&chunk);
64            }
65            let answer: messages::Answer = serde_json::from_slice(&buf)?;
66            if answer.success {
67                // Save UUID to uuids.txt
68                let app_data_dir = directories::ProjectDirs::from(
69                    &self.app_info.0,
70                    &self.app_info.1,
71                    &self.app_info.2,
72                )
73                .ok_or(anyhow!("Cannot locate app directory."))?;
74                let data_dir = app_data_dir.data_dir();
75                std::fs::create_dir_all(data_dir)?;
76
77                let uuid_file = data_dir.join("uuids.txt");
78                let mut uuid_file = std::fs::OpenOptions::new()
79                    .create(true)
80                    .append(true)
81                    .open(uuid_file)?;
82                writeln!(uuid_file, "{}", answer.uuid)?;
83
84                // Save request data to data.yaml
85                let data_file = data_dir.join("data.yaml");
86                let mut data_file = std::fs::OpenOptions::new()
87                    .create(true)
88                    .append(true)
89                    .open(data_file)?;
90
91                let yaml_data = serde_json::to_string(&measurement)?;
92                writeln!(data_file, "- {}", yaml_data)?;
93            } else {
94                return Err(anyhow!("Server error: {:?}", answer.message));
95            }
96            Ok(())
97        } else {
98            Err(anyhow!("Request error: {}", respnse.status()))
99        }
100    }
101}