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                            let _ = METRICS_USAGE_BY_USER_VEC.remove_label_values(&[&username]);
149                            let _ = EGRESS_BY_USER.remove_label_values(&[&username]);
150                            let _ = ACTIVITY_BY_USER.remove_label_values(&[&username]);
151                            let _ = AGE_BY_USER.remove_label_values(&[&username]);
152                            continue;
153                        }
154                        Some(user_info) => user_info,
155                    };
156
157                    if user_info.is_user_active {
158                        active_users += 1;
159                    }
160                    if user_info.is_user_sharer_or_sharee {
161                        share_feature_users += 1;
162                    }
163
164                    total_documents += user_info.total_documents;
165                    total_bytes += user_info.total_bytes;
166
167                    if user_info.total_bytes > 50_000 {
168                        METRICS_USAGE_BY_USER_VEC
169                            .with_label_values(&[&username])
170                            .set(user_info.total_bytes);
171                    } else {
172                        other_usage += user_info.total_bytes;
173                    }
174
175                    if user_info.total_egress > 100_000 {
176                        EGRESS_BY_USER
177                            .with_label_values(&[&username])
178                            .set(user_info.total_egress);
179                    } else {
180                        other_egress += user_info.total_egress;
181                    }
182
183                    ACTIVITY_BY_USER
184                        .with_label_values(&[&username])
185                        .set(if user_info.is_user_active_v2 { 1 } else { 0 });
186
187                    AGE_BY_USER
188                        .with_label_values(&[&username])
189                        .set(user_info.account_age);
190
191                    let billing_info = Self::get_user_billing_info(&db, &owner)?;
192
193                    if billing_info.is_premium() {
194                        premium_users += 1;
195
196                        match billing_info.billing_platform {
197                            None => {
198                                return Err(internal!(
199                                    "Could not retrieve billing platform although it was used moments before."
200                                ));
201                            }
202                            Some(billing_platform) => match billing_platform {
203                                BillingPlatform::GooglePlay { .. } => {
204                                    premium_google_play_users += 1
205                                }
206                                BillingPlatform::Stripe { .. } => premium_stripe_users += 1,
207                                BillingPlatform::AppStore { .. } => premium_app_store_users += 1,
208                            },
209                        }
210                    }
211                    drop(db);
212                }
213
214                tokio::time::sleep(self.config.metrics.time_between_metrics).await;
215            }
216            METRICS_USAGE_BY_USER_VEC
217                .with_label_values(&["OTHER"])
218                .set(other_usage);
219            EGRESS_BY_USER
220                .with_label_values(&["OTHER"])
221                .set(other_egress);
222
223            METRICS_STATISTICS
224                .total_users
225                .set(total_users_ever - deleted_users);
226
227            METRICS_STATISTICS.total_documents.set(total_documents);
228            METRICS_STATISTICS.active_users.set(active_users);
229            METRICS_STATISTICS.deleted_users.set(deleted_users);
230            METRICS_STATISTICS.total_document_bytes.set(total_bytes);
231            METRICS_STATISTICS
232                .total_egress_bytes
233                .set(server_wide_egress as i64);
234            METRICS_STATISTICS
235                .share_feature_users
236                .set(share_feature_users);
237            METRICS_STATISTICS.premium_users.set(premium_users);
238
239            METRICS_PREMIUM_USERS_BY_PAYMENT_PLATFORM_VEC
240                .with_label_values(&[STRIPE_LABEL_NAME])
241                .set(premium_stripe_users);
242            METRICS_PREMIUM_USERS_BY_PAYMENT_PLATFORM_VEC
243                .with_label_values(&[GOOGLE_PLAY_LABEL_NAME])
244                .set(premium_google_play_users);
245            METRICS_PREMIUM_USERS_BY_PAYMENT_PLATFORM_VEC
246                .with_label_values(&[APP_STORE_LABEL_NAME])
247                .set(premium_app_store_users);
248
249            info!("metrics refresh finished");
250            tokio::time::sleep(self.config.metrics.time_between_metrics_refresh).await;
251        }
252    }
253    pub fn get_user_billing_info(
254        db: &ServerDb, owner: &Owner,
255    ) -> Result<SubscriptionProfile, ServerError<MetricsError>> {
256        let account =
257            db.accounts.get().get(owner).ok_or_else(|| {
258                internal!("Could not get user's account during metrics {:?}", owner)
259            })?;
260
261        Ok(account.billing_info.clone())
262    }
263
264    pub fn get_user_info(
265        db: &mut ServerDb, owner: Owner,
266    ) -> Result<Option<UserInfo>, ServerError<MetricsError>> {
267        if db.owned_files.get().get(&owner).is_none() {
268            return Ok(None);
269        }
270
271        let mut tree = ServerTree::new(
272            owner,
273            &mut db.owned_files,
274            &mut db.shared_files,
275            &mut db.file_children,
276            &mut db.metas,
277        )?
278        .to_lazy();
279
280        let mut ids = Vec::new();
281
282        for id in tree.ids() {
283            if !tree.calculate_deleted(&id)? {
284                ids.push(id);
285            }
286        }
287
288        let is_user_sharer_or_sharee = tree
289            .all_files()?
290            .iter()
291            .any(|k| k.owner() != owner || k.is_shared());
292
293        let root_creation_timestamp =
294            if let Some(root_creation_timestamp) = tree.all_files()?.iter().find(|f| f.is_root()) {
295                root_creation_timestamp.file.timestamped_value.timestamp
296            } else {
297                return Ok(None);
298            };
299
300        let account_age = get_time().0 - root_creation_timestamp;
301
302        let last_seen = *db
303            .last_seen
304            .get()
305            .get(&owner)
306            .unwrap_or(&(root_creation_timestamp as u64));
307
308        let total_egress = db
309            .egress_by_owner
310            .get()
311            .get(&owner)
312            .cloned()
313            .unwrap_or_default()
314            .all_bandwidth() as i64;
315
316        let time_two_days_ago = get_time().0 as u64 - TWO_DAYS_IN_MILLIS as u64;
317        let last_seen_since_account_creation = last_seen as i64 - root_creation_timestamp;
318        let delay_buffer_time = 5000;
319        let not_the_welcome_doc = last_seen_since_account_creation > delay_buffer_time;
320        let is_user_active = not_the_welcome_doc && last_seen > time_two_days_ago;
321        let time_one_hour_ago = get_time().0 as u64 - TWO_HOURS_IN_MILLIS as u64;
322        let is_user_active_v2 = not_the_welcome_doc && last_seen > time_one_hour_ago;
323
324        let total_bytes = tree.calculate_usage(owner).unwrap_or_default();
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}