crisp_status_reporter/
lib.rs1#[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 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 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 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 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 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 let payload_json = serde_json::to_vec(&payload).or(Err(()))?;
193
194 let request_uri = Uri::try_from(self.report_uri.as_str()).or(Err(()))?;
196
197 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 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 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}