Skip to main content

lockbook_server_lib/
metrics.rs

1use crate::billing::app_store_client::AppStoreClient;
2use crate::billing::billing_model::{BillingPlatform, SubscriptionProfile};
3use crate::billing::google_play_client::GooglePlayClient;
4use crate::billing::stripe_client::StripeClient;
5use crate::document_service::DocumentService;
6use crate::schema::ServerDb;
7use crate::{ServerError, ServerState};
8use lazy_static::lazy_static;
9use lb_rs::model::clock::get_time;
10use lb_rs::model::file_like::FileLike;
11use lb_rs::model::file_metadata::Owner;
12use lb_rs::model::server_tree::ServerTree;
13use lb_rs::model::tree_like::TreeLike;
14use prometheus::{IntGaugeVec, register_int_gauge_vec};
15use prometheus_static_metric::make_static_metric;
16use std::fmt::Debug;
17use tracing::*;
18
19pub struct UserInfo {
20    account_age: i64,
21    total_documents: i64,
22    total_bytes: i64,
23    total_egress: i64,
24    is_user_active: bool,
25    is_user_active_v2: bool,
26    is_user_sharer_or_sharee: bool,
27}
28
29make_static_metric! {
30    pub struct MetricsStatistics: IntGauge {
31        "type" => {
32            total_users,
33            share_feature_users,
34            premium_users,
35            active_users,
36            deleted_users,
37            total_documents,
38            total_document_bytes,
39            total_egress_bytes,
40        },
41    }
42}
43
44lazy_static! {
45    pub static ref METRICS_COUNTERS_VEC: IntGaugeVec = register_int_gauge_vec!(
46        "lockbook_metrics_counters",
47        "Lockbook's basic metrics of users and files derived from redis",
48        &["type"]
49    )
50    .unwrap();
51    pub static ref METRICS_STATISTICS: MetricsStatistics =
52        MetricsStatistics::from(&METRICS_COUNTERS_VEC);
53    pub static ref METRICS_USAGE_BY_USER_VEC: IntGaugeVec = register_int_gauge_vec!(
54        "lockbook_metrics_usage_by_user",
55        "Lockbook's total usage by user",
56        &["username"]
57    )
58    .unwrap();
59    pub static ref ACTIVITY_BY_USER: IntGaugeVec = register_int_gauge_vec!(
60        "lockbook_activity_by_user",
61        "Lockbook's active users",
62        &["username"]
63    )
64    .unwrap();
65    pub static ref EGRESS_BY_USER: IntGaugeVec = register_int_gauge_vec!(
66        "lockbook_egress_by_user",
67        "Lockbook's egress by user",
68        &["username"]
69    )
70    .unwrap();
71    pub static ref AGE_BY_USER: IntGaugeVec =
72        register_int_gauge_vec!("lockbook_age_by_user", "Lockbook's account ages", &["username"])
73            .unwrap();
74    pub static ref METRICS_PREMIUM_USERS_BY_PAYMENT_PLATFORM_VEC: IntGaugeVec =
75        register_int_gauge_vec!(
76            "lockbook_premium_users_by_payment_platform",
77            "Lockbook's total number of premium users by payment platform",
78            &["platform"]
79        )
80        .unwrap();
81}
82
83const STRIPE_LABEL_NAME: &str = "stripe";
84const GOOGLE_PLAY_LABEL_NAME: &str = "google-play";
85const APP_STORE_LABEL_NAME: &str = "app-store";
86
87#[derive(Debug)]
88pub enum MetricsError {}
89
90const TWO_DAYS_IN_MILLIS: u128 = 1000 * 60 * 60 * 24 * 2;
91const TWO_HOURS_IN_MILLIS: u128 = 1000 * 60 * 60;
92
93impl<S, A, G, D> ServerState<S, A, G, D>
94where
95    S: StripeClient,
96    A: AppStoreClient,
97    G: GooglePlayClient,
98    D: DocumentService,
99{
100    pub fn start_metrics_worker(&self) {
101        let state_clone = self.clone();
102
103        tokio::spawn(async move {
104            info!("Started capturing metrics");
105
106            if let Err(e) = state_clone.start_metrics_loop().await {
107                error!("interrupting metrics loop due to error: {:?}", e)
108            }
109        });
110    }
111
112    pub async fn start_metrics_loop(self) -> Result<(), ServerError<MetricsError>> {
113        loop {
114            info!("Metrics refresh started");
115
116            let public_keys_and_usernames = self.index_db.lock().await.usernames.get().clone();
117            let server_wide_egress = self
118                .index_db
119                .lock()
120                .await
121                .server_egress
122                .get()
123                .map(|total| total.all_bandwidth())
124                .unwrap_or_default();
125
126            let total_users_ever = public_keys_and_usernames.len() as i64;
127            let mut total_documents = 0;
128            let mut total_bytes = 0;
129            let mut active_users = 0;
130            let mut deleted_users = 0;
131            let mut share_feature_users = 0;
132            let mut other_usage = 0;
133            let mut other_egress = 0;
134
135            let mut premium_users = 0;
136            let mut premium_stripe_users = 0;
137            let mut premium_google_play_users = 0;
138            let mut premium_app_store_users = 0;
139
140            for (username, owner) in public_keys_and_usernames {
141                {
142                    let mut db = self.index_db.lock().await;
143                    let maybe_user_info = Self::get_user_info(&mut db, owner)?;
144
145                    let user_info = match maybe_user_info {
146                        None => {
147                            deleted_users += 1;
148                            continue;
149                        }
150                        Some(user_info) => user_info,
151                    };
152
153                    if user_info.is_user_active {
154                        active_users += 1;
155                    }
156                    if user_info.is_user_sharer_or_sharee {
157                        share_feature_users += 1;
158                    }
159
160                    total_documents += user_info.total_documents;
161                    total_bytes += user_info.total_bytes;
162
163                    if user_info.total_bytes > 50_000 {
164                        METRICS_USAGE_BY_USER_VEC
165                            .with_label_values(&[&username])
166                            .set(user_info.total_bytes);
167                    } else {
168                        other_usage += user_info.total_bytes;
169                    }
170
171                    if user_info.total_egress > 100_000 {
172                        EGRESS_BY_USER
173                            .with_label_values(&[&username])
174                            .set(user_info.total_egress);
175                    } else {
176                        other_egress += user_info.total_egress;
177                    }
178
179                    ACTIVITY_BY_USER
180                        .with_label_values(&[&username])
181                        .set(if user_info.is_user_active_v2 { 1 } else { 0 });
182
183                    AGE_BY_USER
184                        .with_label_values(&[&username])
185                        .set(user_info.account_age);
186
187                    let billing_info = Self::get_user_billing_info(&db, &owner)?;
188
189                    if billing_info.is_premium() {
190                        premium_users += 1;
191
192                        match billing_info.billing_platform {
193                            None => {
194                                return Err(internal!(
195                                    "Could not retrieve billing platform although it was used moments before."
196                                ));
197                            }
198                            Some(billing_platform) => match billing_platform {
199                                BillingPlatform::GooglePlay { .. } => {
200                                    premium_google_play_users += 1
201                                }
202                                BillingPlatform::Stripe { .. } => premium_stripe_users += 1,
203                                BillingPlatform::AppStore { .. } => premium_app_store_users += 1,
204                            },
205                        }
206                    }
207                    drop(db);
208                }
209
210                tokio::time::sleep(self.config.metrics.time_between_metrics).await;
211            }
212            METRICS_USAGE_BY_USER_VEC
213                .with_label_values(&["OTHER"])
214                .set(other_usage);
215            EGRESS_BY_USER
216                .with_label_values(&["OTHER"])
217                .set(other_egress);
218
219            METRICS_STATISTICS
220                .total_users
221                .set(total_users_ever - deleted_users);
222
223            METRICS_STATISTICS.total_documents.set(total_documents);
224            METRICS_STATISTICS.active_users.set(active_users);
225            METRICS_STATISTICS.deleted_users.set(deleted_users);
226            METRICS_STATISTICS.total_document_bytes.set(total_bytes);
227            METRICS_STATISTICS
228                .total_egress_bytes
229                .set(server_wide_egress as i64);
230            METRICS_STATISTICS
231                .share_feature_users
232                .set(share_feature_users);
233            METRICS_STATISTICS.premium_users.set(premium_users);
234
235            METRICS_PREMIUM_USERS_BY_PAYMENT_PLATFORM_VEC
236                .with_label_values(&[STRIPE_LABEL_NAME])
237                .set(premium_stripe_users);
238            METRICS_PREMIUM_USERS_BY_PAYMENT_PLATFORM_VEC
239                .with_label_values(&[GOOGLE_PLAY_LABEL_NAME])
240                .set(premium_google_play_users);
241            METRICS_PREMIUM_USERS_BY_PAYMENT_PLATFORM_VEC
242                .with_label_values(&[APP_STORE_LABEL_NAME])
243                .set(premium_app_store_users);
244
245            info!("metrics refresh finished");
246            tokio::time::sleep(self.config.metrics.time_between_metrics_refresh).await;
247        }
248    }
249    pub fn get_user_billing_info(
250        db: &ServerDb, owner: &Owner,
251    ) -> Result<SubscriptionProfile, ServerError<MetricsError>> {
252        let account =
253            db.accounts.get().get(owner).ok_or_else(|| {
254                internal!("Could not get user's account during metrics {:?}", owner)
255            })?;
256
257        Ok(account.billing_info.clone())
258    }
259
260    pub fn get_user_info(
261        db: &mut ServerDb, owner: Owner,
262    ) -> Result<Option<UserInfo>, ServerError<MetricsError>> {
263        if db.owned_files.get().get(&owner).is_none() {
264            return Ok(None);
265        }
266
267        let mut tree = ServerTree::new(
268            owner,
269            &mut db.owned_files,
270            &mut db.shared_files,
271            &mut db.file_children,
272            &mut db.metas,
273        )?
274        .to_lazy();
275
276        let mut ids = Vec::new();
277
278        for id in tree.ids() {
279            if !tree.calculate_deleted(&id)? {
280                ids.push(id);
281            }
282        }
283
284        let is_user_sharer_or_sharee = tree
285            .all_files()?
286            .iter()
287            .any(|k| k.owner() != owner || k.is_shared());
288
289        let root_creation_timestamp =
290            if let Some(root_creation_timestamp) = tree.all_files()?.iter().find(|f| f.is_root()) {
291                root_creation_timestamp.file.timestamped_value.timestamp
292            } else {
293                return Ok(None);
294            };
295
296        let account_age = get_time().0 - root_creation_timestamp;
297
298        let last_seen = *db
299            .last_seen
300            .get()
301            .get(&owner)
302            .unwrap_or(&(root_creation_timestamp as u64));
303
304        let total_egress = db
305            .egress_by_owner
306            .get()
307            .get(&owner)
308            .cloned()
309            .unwrap_or_default()
310            .all_bandwidth() as i64;
311
312        let time_two_days_ago = get_time().0 as u64 - TWO_DAYS_IN_MILLIS as u64;
313        let last_seen_since_account_creation = last_seen as i64 - root_creation_timestamp;
314        let delay_buffer_time = 5000;
315        let not_the_welcome_doc = last_seen_since_account_creation > delay_buffer_time;
316        let is_user_active = not_the_welcome_doc && last_seen > time_two_days_ago;
317        let time_one_hour_ago = get_time().0 as u64 - TWO_HOURS_IN_MILLIS as u64;
318        let is_user_active_v2 = not_the_welcome_doc && last_seen > time_one_hour_ago;
319
320        let total_bytes: u64 = Self::get_usage_helper(&mut tree)
321            .unwrap_or_default()
322            .iter()
323            .map(|f| f.size_bytes)
324            .sum();
325
326        let total_documents = if let Some(owned_files) = db.owned_files.get().get(&owner) {
327            owned_files.len() as i64
328        } else {
329            return Ok(None);
330        };
331
332        Ok(Some(UserInfo {
333            total_documents,
334            total_bytes: total_bytes as i64,
335            is_user_active,
336            is_user_sharer_or_sharee,
337            is_user_active_v2,
338            total_egress,
339            account_age,
340        }))
341    }
342}