1use axum::{body::Body, extract::ConnectInfo, http::Request, response::Response};
2use chrono::Utc;
3use std::future::Future;
4use http::{
5 header::{HeaderValue, HOST, USER_AGENT},
6 Extensions, HeaderMap,
7};
8use reqwest::Client;
9use serde::Serialize;
10use std::{
11 net::{IpAddr, SocketAddr},
12 pin::Pin,
13 sync::Arc,
14 task::{Context, Poll},
15 time::{Duration, Instant},
16};
17use tokio::sync::RwLock;
18use tokio::time::interval;
19use tower::{Layer, Service};
20
21#[derive(Debug, Clone, Serialize)]
22struct RequestData {
23 hostname: String,
24 ip_address: Option<String>,
25 path: String,
26 user_agent: String,
27 method: String,
28 response_time: u32,
29 status: u16,
30 user_id: Option<String>,
31 created_at: String,
32}
33
34type StringMapper = dyn for<'a> Fn(&Request<Body>) -> String + Send + Sync;
35
36#[derive(Clone)]
37struct Config {
38 privacy_level: i32,
39 server_url: String,
40 get_hostname: Arc<StringMapper>,
41 get_ip_address: Arc<StringMapper>,
42 get_path: Arc<StringMapper>,
43 get_user_agent: Arc<StringMapper>,
44 get_user_id: Arc<StringMapper>,
45}
46
47impl Default for Config {
48 fn default() -> Self {
49 Self {
50 privacy_level: 0,
51 server_url: "https://www.apianalytics-server.com/".to_string(),
52 get_hostname: Arc::new(get_hostname),
53 get_ip_address: Arc::new(get_ip_address),
54 get_path: Arc::new(get_path),
55 get_user_agent: Arc::new(get_user_agent),
56 get_user_id: Arc::new(get_user_id),
57 }
58 }
59}
60
61pub trait HeaderValueExt {
62 fn to_string(&self) -> String;
63}
64
65impl HeaderValueExt for HeaderValue {
66 fn to_string(&self) -> String {
67 self.to_str().unwrap_or_default().to_string()
68 }
69}
70
71fn get_hostname(req: &Request<Body>) -> String {
72 req.headers()
73 .get(HOST)
74 .map(|x| x.to_string())
75 .unwrap_or_default()
76}
77
78fn get_ip_address(req: &Request<Body>) -> String {
79 let headers = req.headers();
80 let extensions = req.extensions();
81 if let Some(val) = ip_from_cf_connecting_ip(headers) {
82 return val.to_string();
83 }
84 if let Some(val) = ip_from_x_forwarded_for(headers) {
85 return val.to_string();
86 }
87 if let Some(val) = ip_from_x_real_ip(headers) {
88 return val.to_string();
89 }
90 if let Some(val) = ip_from_connect_info(extensions) {
91 return val.to_string();
92 }
93 String::new()
94}
95
96fn ip_from_cf_connecting_ip(headers: &HeaderMap) -> Option<IpAddr> {
97 headers
98 .get("cf-connecting-ip")
99 .and_then(|hv| hv.to_str().ok())
100 .and_then(|s| s.trim().parse::<IpAddr>().ok())
101}
102
103fn ip_from_x_forwarded_for(headers: &HeaderMap) -> Option<IpAddr> {
104 headers
105 .get("x-forwarded-for")
106 .and_then(|hv| hv.to_str().ok())
107 .and_then(|s| s.split(',').find_map(|s| s.trim().parse::<IpAddr>().ok()))
108}
109
110fn ip_from_x_real_ip(headers: &HeaderMap) -> Option<IpAddr> {
111 headers
112 .get("x-real-ip")
113 .and_then(|hv| hv.to_str().ok())
114 .and_then(|s| s.parse::<IpAddr>().ok())
115}
116
117fn ip_from_connect_info(extensions: &Extensions) -> Option<IpAddr> {
118 extensions
119 .get::<ConnectInfo<SocketAddr>>()
120 .map(|ConnectInfo(addr)| addr.ip())
121}
122
123fn get_path(req: &Request<Body>) -> String {
124 req.uri().path().to_owned()
125}
126
127fn get_user_agent(req: &Request<Body>) -> String {
128 req.headers()
129 .get(USER_AGENT)
130 .map(|x| x.to_string())
131 .unwrap_or_default()
132}
133
134fn get_user_id(_req: &Request<Body>) -> String {
135 String::new()
136}
137
138#[derive(Clone)]
139pub struct Analytics {
140 config: Config,
141 requests: Arc<RwLock<Vec<RequestData>>>,
142}
143
144impl Analytics {
145 pub fn new(api_key: String) -> Self {
146 let requests: Arc<RwLock<Vec<RequestData>>> = Arc::new(RwLock::new(vec![]));
147 let requests_clone = Arc::clone(&requests);
148 let config = Config::default();
149 let privacy_level = config.privacy_level;
150 let server_url = config.server_url.clone();
151
152 tokio::spawn(async move {
153 let client = Client::builder()
154 .timeout(Duration::from_secs(10))
155 .build()
156 .unwrap_or_else(|_| Client::new());
157
158 let mut ticker = interval(Duration::from_secs(60));
159 ticker.tick().await; loop {
161 ticker.tick().await;
162 let batch = {
163 let mut reqs = requests_clone.write().await;
164 std::mem::take(&mut *reqs)
165 };
166 if !batch.is_empty() {
167 let payload = Payload::new(api_key.clone(), batch, privacy_level);
168 post_requests(&client, payload, &server_url).await;
169 }
170 }
171 });
172
173 Self { config, requests }
174 }
175
176 pub fn with_privacy_level(mut self, privacy_level: i32) -> Self {
177 self.config.privacy_level = privacy_level;
178 self
179 }
180
181 pub fn with_server_url(mut self, server_url: String) -> Self {
182 self.config.server_url = if server_url.ends_with('/') {
183 server_url
184 } else {
185 server_url + "/"
186 };
187 self
188 }
189
190 pub fn with_hostname_mapper<F>(mut self, mapper: F) -> Self
191 where
192 F: Fn(&Request<Body>) -> String + Send + Sync + 'static,
193 {
194 self.config.get_hostname = Arc::new(mapper);
195 self
196 }
197
198 pub fn with_ip_address_mapper<F>(mut self, mapper: F) -> Self
199 where
200 F: Fn(&Request<Body>) -> String + Send + Sync + 'static,
201 {
202 self.config.get_ip_address = Arc::new(mapper);
203 self
204 }
205
206 pub fn with_path_mapper<F>(mut self, mapper: F) -> Self
207 where
208 F: Fn(&Request<Body>) -> String + Send + Sync + 'static,
209 {
210 self.config.get_path = Arc::new(mapper);
211 self
212 }
213
214 pub fn with_user_agent_mapper<F>(mut self, mapper: F) -> Self
215 where
216 F: Fn(&Request<Body>) -> String + Send + Sync + 'static,
217 {
218 self.config.get_user_agent = Arc::new(mapper);
219 self
220 }
221
222 pub fn with_user_id_mapper<F>(mut self, mapper: F) -> Self
223 where
224 F: Fn(&Request<Body>) -> String + Send + Sync + 'static,
225 {
226 self.config.get_user_id = Arc::new(mapper);
227 self
228 }
229}
230
231impl<S> Layer<S> for Analytics {
232 type Service = AnalyticsMiddleware<S>;
233
234 fn layer(&self, inner: S) -> Self::Service {
235 AnalyticsMiddleware {
236 config: Arc::new(self.config.clone()),
237 requests: Arc::clone(&self.requests),
238 inner,
239 }
240 }
241}
242
243#[derive(Clone)]
244pub struct AnalyticsMiddleware<S> {
245 config: Arc<Config>,
246 requests: Arc<RwLock<Vec<RequestData>>>,
247 inner: S,
248}
249
250#[derive(Debug, Clone, Serialize)]
251struct Payload {
252 api_key: String,
253 requests: Vec<RequestData>,
254 framework: String,
255 privacy_level: i32,
256}
257
258impl Payload {
259 pub fn new(api_key: String, requests: Vec<RequestData>, privacy_level: i32) -> Self {
260 Self {
261 api_key,
262 requests,
263 framework: "Axum".to_string(),
264 privacy_level,
265 }
266 }
267}
268
269async fn post_requests(client: &Client, data: Payload, server_url: &str) {
270 let url = format!("{}api/log-request", server_url);
271 match client.post(url).json(&data).send().await {
272 Ok(resp) if resp.status().is_success() => {}
273 Ok(resp) => {
274 eprintln!(
275 "Failed to send analytics data. Server responded with status: {}",
276 resp.status()
277 );
278 }
279 Err(err) => {
280 eprintln!("Error sending analytics data: {}", err);
281 }
282 }
283}
284
285impl<S> Service<Request<Body>> for AnalyticsMiddleware<S>
286where
287 S: Service<Request<Body>, Response = Response> + Clone + Send + Sync + 'static,
288 S::Future: Send + 'static,
289{
290 type Response = S::Response;
291 type Error = S::Error;
292 type Future =
293 Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send + 'static>>;
294
295 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
296 self.inner.poll_ready(cx)
297 }
298
299 fn call(&mut self, req: Request<Body>) -> Self::Future {
300 let now = Instant::now();
301
302 let hostname = (self.config.get_hostname)(&req);
303 let ip_address = if self.config.privacy_level >= 2 {
304 None
305 } else {
306 let val = (self.config.get_ip_address)(&req);
307 if val.is_empty() { None } else { Some(val) }
308 };
309 let path = (self.config.get_path)(&req);
310 let method = req.method().to_string();
311 let user_agent = (self.config.get_user_agent)(&req);
312 let user_id = {
313 let val = (self.config.get_user_id)(&req);
314 if val.is_empty() { None } else { Some(val) }
315 };
316 let requests = Arc::clone(&self.requests);
317
318 let future = self.inner.call(req);
319
320 Box::pin(async move {
321 let res: Response = future.await?;
322 let response_time = now.elapsed().as_millis().min(u32::MAX as u128) as u32;
323
324 let request_data = RequestData {
325 hostname,
326 ip_address,
327 path,
328 user_agent,
329 method,
330 response_time,
331 status: res.status().as_u16(),
332 user_id,
333 created_at: Utc::now().to_rfc3339(),
334 };
335
336 requests.write().await.push(request_data);
337
338 Ok(res)
339 })
340 }
341}
342
343#[cfg(test)]
344mod tests {
345 use super::*;
346 use std::str::FromStr;
347
348 fn make_headers(pairs: &[(&str, &str)]) -> HeaderMap {
349 let mut map = HeaderMap::new();
350 for (name, value) in pairs {
351 map.insert(
352 http::header::HeaderName::from_str(name).unwrap(),
353 http::header::HeaderValue::from_str(value).unwrap(),
354 );
355 }
356 map
357 }
358
359 #[test]
360 fn test_cf_connecting_ip() {
361 let headers = make_headers(&[("cf-connecting-ip", "203.0.113.1")]);
362 assert_eq!(ip_from_cf_connecting_ip(&headers), Some("203.0.113.1".parse().unwrap()));
363 }
364
365 #[test]
366 fn test_x_forwarded_for_single() {
367 let headers = make_headers(&[("x-forwarded-for", "203.0.113.1")]);
368 assert_eq!(ip_from_x_forwarded_for(&headers), Some("203.0.113.1".parse().unwrap()));
369 }
370
371 #[test]
372 fn test_x_forwarded_for_returns_first_ip() {
373 let headers = make_headers(&[("x-forwarded-for", "203.0.113.1, 10.0.0.1, 192.168.1.1")]);
375 assert_eq!(ip_from_x_forwarded_for(&headers), Some("203.0.113.1".parse().unwrap()));
376 }
377
378 #[test]
379 fn test_x_real_ip() {
380 let headers = make_headers(&[("x-real-ip", "203.0.113.1")]);
381 assert_eq!(ip_from_x_real_ip(&headers), Some("203.0.113.1".parse().unwrap()));
382 }
383
384 #[test]
385 fn test_invalid_ip_returns_none() {
386 let headers = make_headers(&[("x-real-ip", "not-an-ip")]);
387 assert_eq!(ip_from_x_real_ip(&headers), None);
388 }
389
390 #[test]
391 fn test_cf_takes_priority_over_forwarded() {
392 let headers = make_headers(&[
393 ("cf-connecting-ip", "1.1.1.1"),
394 ("x-forwarded-for", "2.2.2.2"),
395 ]);
396 assert_eq!(ip_from_cf_connecting_ip(&headers), Some("1.1.1.1".parse().unwrap()));
397 assert_eq!(ip_from_x_forwarded_for(&headers), Some("2.2.2.2".parse().unwrap()));
398 }
399
400 #[test]
401 fn test_ipv6_address() {
402 let headers = make_headers(&[("x-real-ip", "2001:db8::1")]);
403 assert_eq!(ip_from_x_real_ip(&headers), Some("2001:db8::1".parse().unwrap()));
404 }
405
406 #[test]
407 fn test_missing_header_returns_none() {
408 let headers = HeaderMap::new();
409 assert_eq!(ip_from_cf_connecting_ip(&headers), None);
410 assert_eq!(ip_from_x_forwarded_for(&headers), None);
411 assert_eq!(ip_from_x_real_ip(&headers), None);
412 }
413}