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 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}