lockbook_server_lib/
metrics.rs1use 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}