1use serde::Serialize;
2use std::collections::HashMap;
3use std::vec::Vec;
4
5use gethostname::gethostname;
6use reqwest;
7
8const BASE_URL: &str = "https://logs.logdna.com/logs/ingest";
9
10type Line = HashMap<String, String>;
11type Lines = Vec<Line>;
12
13#[derive(Serialize)]
14struct Body {
15 lines: Lines,
16}
17
18pub struct Logger {
19 blocking_client: reqwest::blocking::Client,
20 client: reqwest::Client,
21 apikey: String,
22 hostname: String,
23 tags: String,
24 app: String,
25}
26
27impl Logger {
28 pub fn new(apikey: String, tags: String, app: String) -> Self {
30 let blocking_client = reqwest::blocking::Client::new();
31 let client = reqwest::Client::new();
32 let hostname = gethostname().into_string().unwrap();
33
34 Self {
35 blocking_client,
36 client,
37 apikey,
38 hostname,
39 tags,
40 app,
41 }
42 }
43
44 pub fn blocking_log(
46 &self,
47 log_line: String,
48 level: String,
49 ) -> Result<reqwest::blocking::Response, reqwest::Error> {
50 let timestamp = std::time::SystemTime::now()
51 .duration_since(std::time::UNIX_EPOCH)
52 .unwrap()
53 .as_millis();
54
55 let query = format!(
56 "hostname={}×tamp={}&tags={}",
57 self.hostname, timestamp, self.tags
58 );
59
60 let url = format!("{}?{}", BASE_URL, query);
61
62 let mut lines = Lines::new();
63
64 let mut line = Line::new();
65
66 let app = self.app.clone();
67
68 line.insert("line".to_string(), log_line);
69 line.insert("app".to_string(), app);
70 line.insert("level".to_string(), level);
71 line.insert("timestamp".to_string(), timestamp.to_string());
72
73 lines.push(line);
74
75 let body = Body { lines };
76
77 let res = self
78 .blocking_client
79 .post(&url)
80 .header("Content-Type", "application/json")
81 .header("apikey", &self.apikey)
82 .json(&body)
83 .send();
84
85 return res;
86 }
87
88 pub async fn log(
90 &self,
91 log_line: String,
92 level: String,
93 ) -> Result<reqwest::Response, reqwest::Error> {
94 let timestamp = std::time::SystemTime::now()
96 .duration_since(std::time::UNIX_EPOCH)
97 .unwrap()
98 .as_millis();
99
100 let query = format!(
101 "hostname={}×tamp={}&tags={}",
102 self.hostname, timestamp, self.tags
103 );
104
105 let url = format!("{}?{}", BASE_URL, query);
106
107 let mut lines = Lines::new();
108
109 let mut line = Line::new();
110
111 let app = self.app.clone();
113
114 line.insert("line".to_string(), log_line);
115 line.insert("app".to_string(), app);
116 line.insert("level".to_string(), level);
117 line.insert("timestamp".to_string(), timestamp.to_string());
118
119 lines.push(line);
120
121 let body = Body { lines };
122
123 let res = self
124 .client
125 .post(&url)
126 .header("Content-Type", "application/json")
127 .header("apikey", &self.apikey)
128 .json(&body)
129 .send()
130 .await;
131
132 return res;
133 }
134}
135
136#[cfg(test)]
137mod tests {
138
139 use std::env;
140
141 use crate::Logger;
142
143 fn getenv(key: &str) -> String {
144 match env::var(key) {
145 Ok(val) => val,
146 Err(_) => panic!("{} is not set", key),
147 }
148 }
149
150 #[test]
151 fn test_constructor() {
152 let apikey = getenv("LOGDNA_APIKEY");
153 let tags = getenv("LOGDNA_TAGS");
154 Logger::new(apikey, tags, "test".to_string());
155 }
156
157 macro_rules! aw {
158 ($e:expr) => {
159 tokio_test::block_on($e)
160 };
161 }
162
163 #[test]
164 fn test_log() {
165 let apikey = getenv("LOGDNA_APIKEY");
166 let tags = getenv("LOGDNA_TAGS");
167 let logger = Logger::new(apikey, tags, "test".to_string());
168
169 let res = logger.log("test log".to_string(), "info".to_string());
170
171 assert!(aw!(res).is_ok());
172 }
173
174 #[test]
175 fn test_blocking_log() {
176 let apikey = getenv("LOGDNA_APIKEY");
177 let tags = getenv("LOGDNA_TAGS");
178 let logger = Logger::new(apikey, tags, "test".to_string());
179
180 let res = logger.blocking_log("test blocking log".to_string(), "info".to_string());
181
182 assert!(res.is_ok());
183 }
184}