lockbook_server_lib/
loggers.rs1use std::backtrace::Backtrace;
2use std::fmt::{Debug, Write};
3use std::time::SystemTime;
4use std::{env, panic};
5
6use serde::Serialize;
7
8use tokio::runtime::Handle;
9
10use sha2::{Digest, Sha256};
11
12use tracing::field::{Field, Visit};
13use tracing::metadata::LevelFilter;
14use tracing::{Event, Subscriber};
15use tracing_appender::rolling::RollingFileAppender;
16use tracing_gcp::GcpLayer;
17use tracing_subscriber::filter::FilterFn;
18use tracing_subscriber::layer::Context;
19use tracing_subscriber::prelude::*;
20use tracing_subscriber::{Layer, filter, fmt};
21
22use pagerduty_rs::eventsv2async::EventsV2;
23use pagerduty_rs::types::{AlertTrigger, AlertTriggerPayload, Event as PagerEvent, Severity};
24
25use crate::CARGO_PKG_VERSION;
26use crate::config::Config;
27
28static LOG_FILE: &str = "lockbook_server.log";
29
30pub fn init(config: &Config) {
31 let log_level = env::var("LOG_LEVEL")
32 .ok()
33 .and_then(|s| s.as_str().parse().ok())
34 .unwrap_or(LevelFilter::DEBUG);
35 let subscriber = tracing_subscriber::Registry::default()
36 .with(
38 fmt::Layer::new()
39 .pretty()
40 .with_target(false)
41 .with_filter(log_level)
42 .with_filter(server_logs()),
43 )
44 .with(
46 GcpLayer::init_with_writer(file_logger(config))
47 .with_filter(LevelFilter::DEBUG)
48 .with_filter(server_logs()),
49 )
50 .with(
52 PDLogger::new(config)
53 .with_filter(LevelFilter::ERROR)
54 .with_filter(server_logs()),
55 );
56
57 tracing::subscriber::set_global_default(subscriber).unwrap();
58 panic_hook();
59}
60
61fn file_logger(config: &Config) -> RollingFileAppender {
62 tracing_appender::rolling::never(&config.server.log_path, LOG_FILE)
63}
64
65fn server_logs() -> FilterFn {
66 filter::filter_fn(|metadata| {
67 metadata.target().starts_with("lockbook")
68 || metadata.target().starts_with("dbrs")
69 || metadata.target().starts_with("lb_rs")
70 })
71}
72
73struct PDLogger {
74 config: Config,
75 handle: Handle,
76}
77
78impl<S: Subscriber> Layer<S> for PDLogger {
79 fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) {
80 self.page(AlertDetails::new(event));
81 }
82}
83
84impl PDLogger {
85 fn new(config: &Config) -> Self {
86 let handle = Handle::current();
87 let config = config.clone();
88 Self { config, handle }
89 }
90
91 fn page(&self, details: AlertDetails) {
92 let env = self.config.server.env.to_string();
93 match &self.config.server.pd_api_key {
94 Some(api_key) => send_to_pagerduty(&self.handle, env, api_key, details),
95 None => eprintln!("WOULD PAGE: {}", details.message),
96 }
97 }
98}
99
100fn send_to_pagerduty(handle: &Handle, env: String, api_key: &str, alert: AlertDetails) {
101 let events = EventsV2::new(String::from(api_key), Some("lockbook-server".to_string())).unwrap();
102 let message = alert.message.clone();
103 let event = PagerEvent::AlertTrigger(AlertTrigger {
104 payload: AlertTriggerPayload {
105 severity: Severity::Error,
106 summary: message.clone(),
107 source: env,
108 timestamp: Some(SystemTime::now().into()),
109 component: None,
110 group: None,
111 class: None,
112 custom_details: Some(alert),
113 },
114 dedup_key: Some(dedup_key(&message)),
115 images: None,
116 links: None,
117 client: None,
118 client_url: None,
119 });
120
121 tokio::task::block_in_place(move || {
123 futures::executor::block_on(async {
124 handle
125 .spawn(async move {
126 events
127 .event(event)
128 .await
129 .err()
130 .map(|err| eprintln!("Failed reporting event to PagerDuty! {err}"))
131 })
132 .await
133 .err()
134 .map(|err| eprintln!("Failed spawning task in Tokio runtime! {err}"))
135 })
136 });
137}
138
139#[derive(Serialize, Default, Clone)]
140struct AlertDetails {
141 message: String,
142 logger: String,
143 file: Option<String>,
144 line: Option<String>,
145 build: String,
146}
147
148impl AlertDetails {
149 fn new(event: &Event) -> Self {
150 let mut details = Self::default();
151 let record = event.metadata();
152
153 event.record(&mut details);
155
156 details.logger = record.target().to_string();
158 details.file = record.file().map(|file| file.to_string());
159 details.line = record.line().map(|line| line.to_string());
160 details.build = CARGO_PKG_VERSION.to_string();
161
162 details
163 }
164}
165
166impl Visit for AlertDetails {
167 fn record_debug(&mut self, field: &Field, value: &dyn Debug) {
168 if field.name() == "message" {
169 write!(self.message, "{value:?}").unwrap();
170 }
171 }
172}
173
174fn dedup_key(record: &str) -> String {
175 let mut hasher = Sha256::new();
176 hasher.update(record);
177 let result = hasher.finalize();
178 base64::encode(result)
179}
180
181fn panic_hook() {
182 panic::set_hook(Box::new(move |panic_info| {
183 let bt = Backtrace::force_capture();
184 tracing::error!("panic detected: {panic_info} {}", bt);
185 }));
186}