1use chrono::DateTime;
2use chrono::Datelike;
3use chrono::Local;
4use chrono::SecondsFormat;
5use chrono::TimeZone;
6use chrono::Utc;
7use lazy_static::lazy_static;
8use serde::Deserialize;
9use serde::Serialize;
10use serde_json::json;
11use snafu::Snafu;
12use std::collections::HashMap;
13use std::string::ToString;
14use std::sync::RwLock;
15use std::sync::atomic::AtomicBool;
16use std::sync::mpsc;
17use std::thread;
18use std::time::Duration;
19use strum_macros::Display;
20use strum_macros::EnumString;
21
22lazy_static! {
23 static ref LOG: RwLock<Option<std::sync::mpsc::SyncSender<LogMessage>>> = RwLock::new(None);
24}
25lazy_static! {
26 static ref SHOULD_LOOP: AtomicBool = AtomicBool::new(false);
27}
28lazy_static! {
29 static ref IS_FLUSHED: AtomicBool = AtomicBool::new(false);
30}
31lazy_static! {
32 static ref LOG_TO_CONSOLE: AtomicBool = AtomicBool::new(false);
33}
34lazy_static! {
35 static ref LOG_TO_ELASTIC: AtomicBool = AtomicBool::new(false);
36}
37
38static WAIT: Duration = Duration::from_secs(1);
39
40#[derive(Debug, Snafu)]
41pub enum Error {
42 #[snafu(display("Request failed: {}", inner))]
43 RequestFailed { inner: reqwest::Error },
44 #[snafu(display("Failed to deserialize log API reply: {}", inner))]
45 DeserializationFailed { inner: reqwest::Error },
46 #[snafu(display(
47 "Some, or all chunked logs, were not accepted by the log API: {}",
48 errors
49 ))]
50 SomeLogsWereNotAccepted { errors: String },
51 #[snafu(display("The log API rejected the whole chunked log request: {}", errors))]
52 ApiRejectedLogPayload { errors: String },
53}
54
55#[derive(Display, Debug, PartialEq, Eq, EnumString)]
56pub enum LogLevel {
57 #[strum(to_string = "DBG")]
58 Debug,
59 #[strum(to_string = "INF")]
60 Information,
61 #[strum(to_string = "WRN")]
62 Warning,
63 #[strum(to_string = "ERR")]
64 Error,
65 #[strum(to_string = "FTL")]
66 Fatal,
67}
68
69#[derive(Serialize, Deserialize, Display, PartialEq, Eq, Hash, EnumString, Clone)]
70pub enum LogEnvironment {
71 Production,
72 Development,
73}
74
75pub struct LogMessage {
76 level: LogLevel,
77 message_template: String,
78 message: String,
79 fields: HashMap<String, String>,
80}
81
82#[derive(Serialize)]
83struct LogInnerIndex {
84 #[serde(rename(serialize = "_index"))]
85 index: String,
86}
87
88#[derive(Serialize)]
89struct LogIndex {
90 index: LogInnerIndex,
91}
92
93#[derive(Deserialize, Serialize, Debug)]
94struct ElasticErrorCause {
95 r#type: String,
96 reason: String,
97}
98
99#[derive(Deserialize, Serialize, Debug)]
100struct ElasticError {
101 caused_by: ElasticErrorCause,
102}
103
104#[derive(Serialize, Deserialize)]
105struct ElasticSingleLogStatus {
106 #[serde(rename(deserialize = "_index"))]
107 index: String,
108 status: u16,
109 #[serde(rename(deserialize = "_id"))]
110 id: String,
111 error: Option<ElasticError>,
112}
113
114#[derive(serde::Deserialize, Serialize)]
115struct ElasticSingleReplyIndex {
116 index: ElasticSingleLogStatus,
117}
118
119#[derive(serde::Deserialize, Serialize)]
120struct ElasticReply {
121 errors: bool,
122 items: Vec<ElasticSingleReplyIndex>,
123}
124
125#[derive(serde::Deserialize, Serialize)]
126struct ElasticErrorReplyError {
127 reason: String,
128 r#type: String,
129}
130
131#[derive(serde::Deserialize, Serialize)]
132struct ElasticErrorReply {
133 error: ElasticErrorReplyError,
134 status: u16,
135}
136
137pub fn create_console_message<T>(level: &LogLevel, message: &str, time: &DateTime<T>) -> String
138where
139 T: TimeZone,
140{
141 let time_str = time.time().format("%H:%M:%S");
142 format!("[{} {}]: {}", time_str, level, message)
143}
144
145pub fn log<S>(level: LogLevel, template: &str, fields: HashMap<S, String>)
146where
147 S: std::fmt::Display,
148{
149 let mut message = template.to_string();
150 for (key, value) in fields.iter() {
151 message = message.replacen(format!("{{{}}}", key).as_str(), value.as_str(), 1);
152 }
153
154 let mut fields_copy = HashMap::<String, String>::with_capacity(fields.len());
155 for (key, value) in fields.into_iter() {
156 fields_copy.insert(key.to_string(), value);
157 }
158
159 let log_msg = LogMessage {
160 level,
161 message_template: template.to_string(),
162 message,
163 fields: fields_copy,
164 };
165
166 {
167 let logger = LOG.read().unwrap();
168 if let Some(logger_) = &*logger {
169 logger_
170 .send(log_msg)
171 .expect("Relastic queue has stopped working");
172 return; }
174 }
175 let time = chrono::Local::now();
176 if LOG_TO_ELASTIC.load(std::sync::atomic::Ordering::SeqCst) {
177 println!(
178 "{}",
179 create_console_message(
180 &LogLevel::Warning,
181 "Could not log to elastic. Logging to console as fallback",
182 &time
183 )
184 );
185 }
186 println!(
187 "{}",
188 create_console_message(&log_msg.level, &log_msg.message, &time)
189 );
190}
191
192pub fn get_matches(input: &str) -> std::collections::VecDeque<String> {
193 let mut matches = std::collections::VecDeque::new();
194 let mut word = std::string::String::new();
195 for c in input.chars() {
196 match c {
197 '{' => {
198 word.clear();
200 }
201 '}' => {
202 matches.push_back(word.clone());
203 word.clear();
204 }
205 _ => {
206 word.push(c);
207 }
208 }
209 }
210 matches
211}
212
213#[macro_export]
214macro_rules! log_debug {
215 ($fmt: literal, $($arg:expr),*) => {
216 {
217 let mut matches = $crate::log::get_matches($fmt);
218 let mut map = std::collections::HashMap::new();
219 $(
220 if let Some(log_variable_name) = matches.pop_front()
221 {
222 map.insert(log_variable_name, $arg.to_string());
223 }
224 )*
225
226 $crate::log::log($crate::log::LogLevel::Debug, $fmt, map);
227 }
228 }
229}
230
231#[macro_export]
232macro_rules! log_information {
233 ($fmt: literal, $($arg:expr),*) => {
234 {
235 let mut matches = $crate::log::get_matches($fmt);
236 let mut map = std::collections::HashMap::new();
237 $(
238 if let Some(log_variable_name) = matches.pop_front()
239 {
240 map.insert(log_variable_name, $arg.to_string());
241 }
242 )*
243
244 $crate::log::log($crate::log::LogLevel::Information, $fmt, map);
245 }
246 }
247}
248
249#[macro_export]
250macro_rules! log_warning {
251 ($fmt: literal, $($arg:expr),*) => {
252 {
253 let mut matches = $crate::log::get_matches($fmt);
254 let mut map = std::collections::HashMap::new();
255 $(
256 if let Some(log_variable_name) = matches.pop_front()
257 {
258 map.insert(log_variable_name, $arg.to_string());
259 }
260 )*
261
262 $crate::log::log($crate::log::LogLevel::Warning, $fmt, map);
263 }
264 }
265}
266
267#[macro_export]
268macro_rules! log_error {
269 ($fmt: literal, $($arg:expr),*) => {
270 {
271 let mut matches = $crate::log::get_matches($fmt);
272 let mut map = std::collections::HashMap::new();
273 $(
274 if let Some(log_variable_name) = matches.pop_front()
275 {
276 map.insert(log_variable_name, $arg.to_string());
277 }
278 )*
279
280 $crate::log::log($crate::log::LogLevel::Error, $fmt, map);
281 }
282 }
283}
284
285#[macro_export]
286macro_rules! log_fatal {
287 ($fmt: literal, $($arg:expr),*) => {
288 {
289 let mut matches = $crate::log::get_matches($fmt);
290 let mut map = std::collections::HashMap::new();
291 $(
292 if let Some(log_variable_name) = matches.pop_front()
293 {
294 map.insert(log_variable_name, $arg.to_string());
295 }
296 )*
297
298 $crate::log::log($crate::log::LogLevel::Fatal, $fmt, map);
299 }
300 }
301}
302
303pub fn information(template: &str, fields: HashMap<&str, String>) {
316 log(LogLevel::Information, template, fields)
317}
318
319pub fn error(template: &str, fields: HashMap<&str, String>) {
332 log(LogLevel::Error, template, fields)
333}
334
335pub fn debug(template: &str, fields: HashMap<&str, String>) {
347 log(LogLevel::Debug, template, fields)
348}
349
350pub fn fatal(template: &str, fields: HashMap<&str, String>) {
362 log(LogLevel::Fatal, template, fields)
363}
364
365pub fn warning(template: &str, fields: HashMap<&str, String>) {
378 log(LogLevel::Warning, template, fields)
379}
380
381pub fn flush() {
384 println!("Waiting for log grace-period to expire...");
385 thread::sleep(WAIT);
386 println!("Closing log transceiver");
387 {
388 let mut sender = LOG.write().unwrap();
389 *sender = None;
390 }
391 println!("Flushing logs");
392 SHOULD_LOOP.store(false, std::sync::atomic::Ordering::SeqCst);
393 while !IS_FLUSHED.load(std::sync::atomic::Ordering::SeqCst) {
394 println!("Waiting for logs to flush...");
395 thread::sleep(WAIT);
396 }
397 println!("Flushed logs");
398}
399
400#[derive(Serialize)]
401struct LogPayload {
402 #[serde(rename(serialize = "@timestamp"))]
403 timestamp: String,
404 level: String,
405 #[serde(rename(serialize = "messageTemplate"))]
406 message_template: String,
407 message: String,
408 fields: HashMap<String, String>,
409}
410
411#[derive(Clone, PartialEq)]
415pub struct ElasticConfig {
416 pub username: String,
418 pub password: String,
420 pub url: String,
422 pub environment: LogEnvironment,
424 pub application_name: String,
426 pub log_to_console: Option<bool>,
428}
429
430fn concat_fields(
431 default_fields: &HashMap<String, String>,
432 extra_fields: HashMap<String, String>,
433) -> HashMap<String, String> {
434 let mut all_fields = HashMap::new();
435 for (key, value) in default_fields.iter() {
436 all_fields.insert(key.to_owned(), value.to_owned());
437 }
438 for (key, value) in extra_fields.into_iter() {
439 all_fields.insert(key, value);
440 }
441 all_fields
442}
443
444#[cfg(test)]
445pub fn test_post_to_elastic(elastic_credentials: ElasticConfig, json: String) -> Result<(), Error> {
446 let client = reqwest::blocking::Client::new();
447 post_to_elastic(client, elastic_credentials, json)
448}
449
450fn post_to_elastic(
451 client: reqwest::blocking::Client,
452 elastic_credentials: ElasticConfig,
453 json: String,
454) -> Result<(), Error> {
455 let result = client
456 .post(format!("{}/_bulk", elastic_credentials.url))
457 .basic_auth(
458 elastic_credentials.username,
459 Some(elastic_credentials.password),
460 )
461 .header("Content-Type", "application/json")
462 .header("Accept", "application/json")
463 .body(json.to_owned())
464 .send()
465 .map_err(|x| Error::RequestFailed { inner: x })?;
466 if result.status().is_success() {
467 let ok_result = result
468 .json::<ElasticReply>()
469 .map_err(|x| Error::DeserializationFailed { inner: x })?;
470 let mut errors = Vec::<String>::new();
471 for index in ok_result.items.into_iter() {
472 if let Some(some_error) = index.index.error {
473 errors.push(format!(
474 "[{}]: {}",
475 some_error.caused_by.r#type, some_error.caused_by.reason
476 ));
477 }
478 }
479 if !errors.is_empty() {
480 return Err(Error::SomeLogsWereNotAccepted {
481 errors: errors.join("\n"),
482 });
483 }
484 return Ok(());
485 }
486
487 let err_result = result.json::<ElasticErrorReply>();
488 if let Ok(some_err_result) = err_result {
489 Err(Error::ApiRejectedLogPayload {
490 errors: json!(some_err_result).to_string(),
491 })
492 } else {
493 Err(Error::ApiRejectedLogPayload {
494 errors: format!("[Unhandled Error]: Payload {}", json),
495 })
496 }
497}
498
499fn serialize_to_chunks(log_chunks: Vec<(LogIndex, LogPayload)>) -> String {
500 let mut chunks = Vec::<String>::new();
501 for log_chunk in log_chunks.into_iter() {
502 chunks.push(json!(log_chunk.0).to_string());
503 chunks.push(json!(log_chunk.1).to_string());
504 }
505 chunks.push("\n".to_string());
507 chunks.join("\n")
508}
509
510fn log_to_elastic(
511 client: reqwest::blocking::Client,
512 elastic_credentials: ElasticConfig,
513 log_chunks: Vec<(LogIndex, LogPayload)>,
514) {
515 let json = serialize_to_chunks(log_chunks);
516 let result = post_to_elastic(client, elastic_credentials, json);
517 if let Err(result_err) = result {
518 eprintln!("error: {}", result_err)
519 }
520}
521
522pub fn setup_console_log() {
523 LOG_TO_CONSOLE.store(true, std::sync::atomic::Ordering::SeqCst);
524 SHOULD_LOOP.store(false, std::sync::atomic::Ordering::SeqCst);
525 IS_FLUSHED.store(true, std::sync::atomic::Ordering::SeqCst);
526 LOG_TO_ELASTIC.store(false, std::sync::atomic::Ordering::SeqCst);
527 println!(
528 "{}",
529 create_console_message(
530 &LogLevel::Information,
531 "Relastic initialized to log to console",
532 &Local::now()
533 )
534 );
535}
536
537pub fn setup_elastic_log(config: ElasticConfig, buffer_size: usize) {
562 LOG_TO_CONSOLE.store(
563 config.log_to_console.unwrap_or(true),
564 std::sync::atomic::Ordering::SeqCst,
565 );
566 LOG_TO_ELASTIC.store(true, std::sync::atomic::Ordering::SeqCst);
567 let client = reqwest::blocking::Client::new();
568 let (tx, rx) = mpsc::sync_channel::<LogMessage>(buffer_size);
569 {
570 let mut logger = LOG.write().unwrap();
571 *logger = Some(tx);
572 }
573 let app_name_lowercase = config.application_name.to_lowercase();
574 let env_str_lowercase = config.environment.to_owned().to_string().to_lowercase();
575 let default_fields = HashMap::from([
576 ("ApplicationName".to_string(), app_name_lowercase.to_owned()),
577 (
578 "HostingEnvironment".to_string(),
579 env_str_lowercase.to_owned(),
580 ),
581 ]);
582
583 SHOULD_LOOP.store(true, std::sync::atomic::Ordering::SeqCst);
584
585 thread::spawn(move || {
586 loop {
587 let mut payload_chunks = Vec::<(LogIndex, LogPayload)>::new();
588 let next_logs = rx.recv_timeout(WAIT);
589 if next_logs.is_err() {
590 if !SHOULD_LOOP.load(std::sync::atomic::Ordering::SeqCst) {
592 IS_FLUSHED.store(true, std::sync::atomic::Ordering::SeqCst);
593 return;
594 }
595 continue;
596 }
597 for received in next_logs.into_iter() {
598 let dt = Utc::now();
599 let str_index: String = format!(
600 "{}-{}-{}.{}",
601 app_name_lowercase,
602 env_str_lowercase,
603 dt.date_naive().year(),
604 dt.date_naive().month()
605 );
606 let index = LogIndex {
607 index: LogInnerIndex { index: str_index },
608 };
609 let log_payload = LogPayload {
610 timestamp: dt.to_rfc3339_opts(SecondsFormat::Nanos, true),
611 level: received.level.to_string(),
612 message_template: received.message_template,
613 message: received.message,
614 fields: concat_fields(&default_fields, received.fields),
615 };
616
617 if received.level == LogLevel::Debug
619 && config.environment == LogEnvironment::Production
620 {
621 continue;
622 }
623 if LOG_TO_CONSOLE.load(std::sync::atomic::Ordering::SeqCst) {
625 let local_time = Local::now();
626 println!(
627 "{}",
628 create_console_message(&received.level, &log_payload.message, &local_time)
629 );
630 }
631 payload_chunks.push((index, log_payload));
632 }
633 if !payload_chunks.is_empty() {
634 log_to_elastic(client.to_owned(), config.clone(), payload_chunks);
635 }
636 if !SHOULD_LOOP.load(std::sync::atomic::Ordering::SeqCst) {
637 IS_FLUSHED.store(true, std::sync::atomic::Ordering::SeqCst);
638 return;
639 }
640 }
641 });
642}