actix_analytics/
analytics.rs

1use actix_rt::spawn;
2use actix_web::{
3    dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform},
4    http::header::{HeaderValue, HOST, USER_AGENT},
5    Error,
6};
7use chrono::Utc;
8use futures::future::LocalBoxFuture;
9use lazy_static::lazy_static;
10use reqwest::Client;
11use serde::Serialize;
12use std::sync::{Arc, Mutex};
13use std::{
14    future::{ready, Ready},
15    time::Instant,
16};
17
18#[derive(Debug, Clone, Serialize)]
19struct RequestData {
20    hostname: String,
21    ip_address: String,
22    path: String,
23    user_agent: String,
24    method: String,
25    response_time: u32,
26    status: u16,
27    user_id: String,
28    created_at: String,
29}
30impl RequestData {
31    pub fn new(
32        hostname: String,
33        ip_address: String,
34        path: String,
35        user_agent: String,
36        method: String,
37        response_time: u32,
38        status: u16,
39        user_id: String,
40        created_at: String,
41    ) -> Self {
42        Self {
43            hostname,
44            ip_address,
45            path,
46            user_agent,
47            method,
48            response_time,
49            status,
50            user_id,
51            created_at,
52        }
53    }
54}
55
56type StringMapper = dyn for<'a> Fn(&ServiceRequest) -> String + Send + Sync;
57
58#[derive(Clone)]
59struct Config {
60    privacy_level: i32,
61    server_url: String,
62    get_hostname: Arc<StringMapper>,
63    get_ip_address: Arc<StringMapper>,
64    get_path: Arc<StringMapper>,
65    get_user_agent: Arc<StringMapper>,
66    get_user_id: Arc<StringMapper>,
67}
68
69impl Default for Config {
70    fn default() -> Self {
71        Self {
72            privacy_level: 0,
73            server_url: String::from("https://www.apianalytics-server.com/"),
74            get_hostname: Arc::new(get_hostname),
75            get_ip_address: Arc::new(get_ip_address),
76            get_path: Arc::new(get_path),
77            get_user_agent: Arc::new(get_user_agent),
78            get_user_id: Arc::new(get_user_id),
79        }
80    }
81}
82
83fn get_hostname(req: &ServiceRequest) -> String {
84    req.headers()
85        .get(HOST)
86        .map(|x| x.to_string())
87        .unwrap_or_default()
88}
89
90fn get_ip_address(req: &ServiceRequest) -> String {
91    if let Some(val) = req.peer_addr() {
92        return val.ip().to_string();
93    };
94    String::new()
95}
96
97fn get_path(req: &ServiceRequest) -> String {
98    req.path().to_string()
99}
100
101fn get_user_agent(req: &ServiceRequest) -> String {
102    req
103            .headers()
104            .get(USER_AGENT)
105            .map(|x| x.to_string())
106            .unwrap_or_default()
107}
108
109fn get_user_id(_req: &ServiceRequest) -> String {
110    String::new()
111}
112
113pub struct Analytics {
114    api_key: String,
115    config: Config,
116}
117
118impl Analytics {
119    pub fn new(api_key: String) -> Self {
120        Self {
121            api_key,
122            config: Config::default(),
123        }
124    }
125
126    pub fn with_privacy_level(mut self, privacy_level: i32) -> Self {
127        self.config.privacy_level = privacy_level;
128        self
129    }
130
131    pub fn with_server_url(mut self, server_url: String) -> Self {
132        if server_url.ends_with("/") {
133            self.config.server_url = server_url;
134        } else {
135            self.config.server_url = server_url + "/";
136        }
137        self
138    }
139
140    pub fn with_hostname_mapper<F>(mut self, mapper: F) -> Self
141    where
142        F: Fn(&ServiceRequest) -> String + Send + Sync + 'static,
143    {
144        self.config.get_hostname = Arc::new(mapper);
145        self
146    }
147
148    pub fn with_ip_address_mapper<F>(mut self, mapper: F) -> Self
149    where
150        F: Fn(&ServiceRequest) -> String + Send + Sync + 'static,
151    {
152        self.config.get_ip_address = Arc::new(mapper);
153        self
154    }
155
156    pub fn with_path_mapper<F>(mut self, mapper: F) -> Self
157    where
158        F: Fn(&ServiceRequest) -> String + Send + Sync + 'static,
159    {
160        self.config.get_path = Arc::new(mapper);
161        self
162    }
163
164    pub fn with_user_agent_mapper<F>(mut self, mapper: F) -> Self
165    where
166        F: Fn(&ServiceRequest) -> String + Send + Sync + 'static,
167    {
168        self.config.get_user_agent = Arc::new(mapper);
169        self
170    }
171}
172
173impl<S, B> Transform<S, ServiceRequest> for Analytics
174where
175    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
176    S::Future: 'static,
177    B: 'static,
178{
179    type Response = ServiceResponse<B>;
180    type Error = Error;
181    type InitError = ();
182    type Transform = AnalyticsMiddleware<S>;
183    type Future = Ready<Result<Self::Transform, Self::InitError>>;
184
185    fn new_transform(&self, service: S) -> Self::Future {
186        ready(Ok(AnalyticsMiddleware {
187            api_key: Arc::new(self.api_key.clone()),
188            config: Arc::new(self.config.clone()),
189            service,
190        }))
191    }
192}
193
194pub struct AnalyticsMiddleware<S> {
195    api_key: Arc<String>,
196    config: Arc<Config>,
197    service: S,
198}
199
200pub trait HeaderValueExt {
201    fn to_string(&self) -> String;
202}
203
204impl HeaderValueExt for HeaderValue {
205    fn to_string(&self) -> String {
206        self.to_str().unwrap_or_default().to_string()
207    }
208}
209
210lazy_static! {
211    static ref REQUESTS: Mutex<Vec<RequestData>> = Mutex::new(vec![]);
212    static ref LAST_POSTED: Mutex<Instant> = Mutex::new(Instant::now());
213}
214
215#[derive(Debug, Clone, Serialize)]
216struct Payload {
217    api_key: String,
218    requests: Vec<RequestData>,
219    framework: String,
220    privacy_level: i32,
221}
222
223impl Payload {
224    pub fn new(api_key: String, requests: Vec<RequestData>, privacy_level: i32) -> Self {
225        Self {
226            api_key,
227            requests,
228            framework: String::from("Actix"),
229            privacy_level,
230        }
231    }
232}
233
234async fn post_requests(data: Payload, server_url: String) {
235    let _ = Client::new()
236        .post(server_url + "api/log-request")
237        .json(&data)
238        .send()
239        .await;
240}
241
242async fn log_request(api_key: &str, request_data: RequestData, config: &Config) {
243    REQUESTS.lock().unwrap().push(request_data);
244    if LAST_POSTED.lock().unwrap().elapsed().as_secs_f64() > 60.0 {
245        let payload = Payload::new(
246            api_key.to_owned(),
247            REQUESTS.lock().unwrap().to_vec(),
248            config.privacy_level,
249        );
250        let server_url = config.server_url.to_owned();
251        REQUESTS.lock().unwrap().clear();
252        post_requests(payload, server_url).await;
253        *LAST_POSTED.lock().unwrap() = Instant::now();
254    }
255}
256
257impl<S, B> Service<ServiceRequest> for AnalyticsMiddleware<S>
258where
259    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
260    S::Future: 'static,
261    B: 'static,
262{
263    type Response = ServiceResponse<B>;
264    type Error = Error;
265    type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
266
267    forward_ready!(service);
268
269    fn call(&self, req: ServiceRequest) -> Self::Future {
270        let start = Instant::now();
271
272        let api_key = Arc::clone(&self.api_key);
273        let config = Arc::clone(&self.config);
274        let hostname = (self.config.get_hostname)(&req);
275        let ip_address = (self.config.get_ip_address)(&req);
276        let path = (self.config.get_path)(&req);
277        let method = req.method().to_string();
278        let user_agent = (self.config.get_hostname)(&req);
279        let user_id = (self.config.get_user_id)(&req);
280
281        let future = self.service.call(req);
282
283        Box::pin(async move {
284            let res = future.await?;
285            let elapsed = start.elapsed().as_millis();
286
287            let request_data = RequestData::new(
288                hostname,
289                ip_address,
290                path,
291                user_agent,
292                method,
293                elapsed.try_into().unwrap(),
294                res.status().as_u16(),
295                user_id,
296                Utc::now().to_rfc3339(),
297            );
298
299            spawn(async move { log_request(&api_key, request_data, &config).await });
300
301            Ok(res)
302        })
303    }
304}