Skip to main content

crisp_status_reporter/
lib.rs

1//! rs-crisp-status-reporter Crisp Status Reporter for Rust.
2
3#[macro_use]
4extern crate log;
5#[macro_use]
6extern crate serde_derive;
7
8use std::cmp::max;
9use std::convert::TryFrom;
10use std::io;
11use std::thread;
12use std::time::Duration;
13
14use base64::engine::general_purpose::STANDARD as base64_encoder;
15use base64::Engine;
16use http_req::{
17    request::{Method, Request},
18    uri::Uri,
19};
20use serde_json;
21use sys_info::{cpu_num, loadavg, mem_info};
22
23static LOG_NAME: &'static str = "Crisp Status Reporter";
24static REPORT_URI: &'static str = "https://report.crisp.watch/v1";
25
26pub const HTTP_CLIENT_TIMEOUT: Duration = Duration::from_secs(10);
27
28pub struct Reporter<'a> {
29    token: &'a str,
30    service_id: Option<&'a str>,
31    node_id: Option<&'a str>,
32    replica_id: Option<&'a str>,
33    interval: Duration,
34}
35
36pub struct ReporterBuilder<'a> {
37    reporter: Reporter<'a>,
38}
39
40struct ReporterManager {
41    report_uri: String,
42    replica_id: String,
43    interval: Duration,
44    useragent: String,
45    authorization: String,
46}
47
48#[derive(Serialize, Debug)]
49struct ReportPayload<'a> {
50    replica_id: &'a str,
51    interval: u64,
52    load: ReportPayloadLoad,
53}
54
55#[derive(Serialize, Debug)]
56struct ReportPayloadLoad {
57    cpu: f32,
58    ram: f32,
59}
60
61impl<'a> Reporter<'a> {
62    pub fn new(token: &'a str) -> ReporterBuilder<'a> {
63        ReporterBuilder {
64            reporter: Reporter {
65                token: token,
66                service_id: None,
67                node_id: None,
68                replica_id: None,
69                interval: Duration::from_secs(30),
70            },
71        }
72    }
73
74    pub fn run(&self) -> Result<(), ()> {
75        debug!("{}: Will run using URL: {}", LOG_NAME, REPORT_URI);
76
77        // Build thread manager context?
78        match (self.service_id, self.node_id, self.replica_id) {
79            (Some(service_id), Some(node_id), Some(replica_id)) => {
80                let manager = ReporterManager {
81                    report_uri: format!("{}/report/{}/{}/", REPORT_URI, service_id, node_id),
82                    replica_id: replica_id.to_owned(),
83                    interval: self.interval,
84
85                    useragent: format!(
86                        "rs-{}/{}",
87                        env!("CARGO_PKG_NAME"),
88                        env!("CARGO_PKG_VERSION")
89                    ),
90
91                    authorization: format!(
92                        "Basic {}",
93                        base64_encoder.encode(&format!(":{}", self.token))
94                    ),
95                };
96
97                // Spawn thread
98                thread::Builder::new()
99                    .name("crisp-status-reporter".to_string())
100                    .spawn(move || manager.run())
101                    .or(Err(()))
102                    .and(Ok(()))
103            }
104            _ => Err(()),
105        }
106    }
107}
108
109impl<'a> ReporterBuilder<'a> {
110    pub fn build(self) -> Reporter<'a> {
111        if self.reporter.service_id.is_none() {
112            panic!("missing service_id");
113        }
114        if self.reporter.node_id.is_none() {
115            panic!("missing node_id");
116        }
117        if self.reporter.replica_id.is_none() {
118            panic!("missing replica_id");
119        }
120
121        self.reporter
122    }
123
124    pub fn service_id(mut self, service_id: &'a str) -> ReporterBuilder<'a> {
125        self.reporter.service_id = Some(service_id);
126
127        self
128    }
129
130    pub fn node_id(mut self, node_id: &'a str) -> ReporterBuilder<'a> {
131        self.reporter.node_id = Some(node_id);
132
133        self
134    }
135
136    pub fn replica_id(mut self, replica_id: &'a str) -> ReporterBuilder<'a> {
137        self.reporter.replica_id = Some(replica_id);
138
139        self
140    }
141
142    pub fn interval(mut self, interval: Duration) -> ReporterBuilder<'a> {
143        self.reporter.interval = interval;
144
145        self
146    }
147}
148
149impl ReporterManager {
150    pub fn run(&self) {
151        debug!("{}: Now running", LOG_NAME);
152
153        // Schedule first report after 10 seconds
154        thread::sleep(Duration::from_secs(10));
155
156        loop {
157            if self.report().is_err() == true {
158                warn!(
159                    "{}: Last report failed, trying again sooner than usual",
160                    LOG_NAME
161                );
162
163                // Try reporting again after half the interval (this report failed)
164                thread::sleep(self.interval / 2);
165
166                self.report().ok();
167            }
168
169            thread::sleep(self.interval);
170        }
171    }
172
173    fn report(&self) -> Result<(), ()> {
174        debug!("{}: Will dispatch request", LOG_NAME);
175
176        // Generate report payload
177        let payload = ReportPayload {
178            replica_id: &self.replica_id,
179            interval: self.interval.as_secs(),
180            load: ReportPayloadLoad {
181                cpu: Self::get_load_cpu(),
182                ram: Self::get_load_ram(),
183            },
184        };
185
186        debug!(
187            "{}: Will send request to URL: {} with payload: {:?}",
188            LOG_NAME, &self.report_uri, payload
189        );
190
191        // Encode payload to string
192        let payload_json = serde_json::to_vec(&payload).or(Err(()))?;
193
194        // Generate request URI
195        let request_uri = Uri::try_from(self.report_uri.as_str()).or(Err(()))?;
196
197        // Acquire report response
198        let mut response_sink = io::sink();
199
200        let response = Request::new(&request_uri)
201            .connect_timeout(Some(HTTP_CLIENT_TIMEOUT))
202            .read_timeout(Some(HTTP_CLIENT_TIMEOUT))
203            .write_timeout(Some(HTTP_CLIENT_TIMEOUT))
204            .method(Method::POST)
205            .header("User-Agent", &self.useragent)
206            .header("Authorization", &self.authorization)
207            .header("Content-Type", "application/json")
208            .header("Content-Length", &payload_json.len())
209            .body(&payload_json)
210            .send(&mut response_sink);
211
212        match response {
213            Ok(response) => {
214                let status_code = response.status_code();
215
216                if status_code.is_success() {
217                    debug!("{}: Request succeeded", LOG_NAME);
218
219                    // Return with success
220                    return Ok(());
221                } else {
222                    warn!("{}: Got non-OK status code: {}", LOG_NAME, status_code);
223                }
224            }
225            Err(err) => error!("{}: Failed dispatching request: {}", LOG_NAME, err),
226        }
227
228        // Return with error
229        Err(())
230    }
231
232    fn get_load_cpu() -> f32 {
233        match (cpu_num(), loadavg()) {
234            (Ok(cpu_num_value), Ok(loadavg_value)) => {
235                (loadavg_value.one / (max(cpu_num_value, 1) as f64)) as f32
236            }
237            _ => 0.00,
238        }
239    }
240
241    fn get_load_ram() -> f32 {
242        if let Ok(mem_info_value) = mem_info() {
243            1.00 - ((mem_info_value.avail as f32) / (mem_info_value.total as f32))
244        } else {
245            0.00
246        }
247    }
248}