Skip to main content

lockbook_server_lib/
loggers.rs

1use 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        // Logger for stdout (local development)
37        .with(
38            fmt::Layer::new()
39                .pretty()
40                .with_target(false)
41                .with_filter(log_level)
42                .with_filter(server_logs()),
43        )
44        // Writes to the specified file in a format that gcp understands
45        .with(
46            GcpLayer::init_with_writer(file_logger(config))
47                .with_filter(LevelFilter::DEBUG)
48                .with_filter(server_logs()),
49        )
50        // Logger for disaster response (any error logs sent to pagerduty)
51        .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    // https://github.com/neonphog/tokio_safe_block_on/blob/074d40929ccab649b0dcc83a4ebdbdcb70b317fb/src/lib.rs#L72-L86
122    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        // Populate the message field
154        event.record(&mut details);
155
156        // Populate the other fields
157        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}