use backtrace::Backtrace;
use lazy_static::lazy_static;
pub use log::{error, info, warn};
pub use sentry::{ClientInitGuard, Envelope, integrations::log::SentryLogger, protocol::*, release_name, end_session, end_session_with_status};
use serde_derive::Serialize;
use simplelog::{ColorChoice, CombinedLogger, LevelFilter, SharedLogger, TermLogger, TerminalMode};
use std::borrow::Cow;
use std::fs::{DirBuilder, File};
use std::io::{BufWriter, Write};
use std::{panic, panic::PanicHookInfo};
use std::path::Path;
use std::sync::{Arc, RwLock};
use crate::error::Result;
use crate::utils::current_time;
const VERSION: &str = env!("CARGO_PKG_VERSION");
lazy_static! {
pub static ref SENTRY_DSN: Arc<RwLock<String>> = Arc::new(RwLock::new(String::new()));
}
#[derive(Debug, Serialize)]
pub struct Logger {
name: String,
crate_version: String,
build_type: String,
operating_system: String,
explanation: String,
backtrace: String,
}
impl Logger {
pub fn init(logging_path: &Path, verbose: bool, set_logger: bool, release: Option<Cow<'static, str>>) -> Result<ClientInitGuard> {
if let Some(parent_folder) = logging_path.parent() {
DirBuilder::new().recursive(true).create(parent_folder)?;
}
let log_level = if verbose {
LevelFilter::Info
} else {
LevelFilter::Warn
};
if set_logger {
let loggers: Vec<Box<dyn SharedLogger + 'static>> = vec![TermLogger::new(log_level, simplelog::Config::default(), TerminalMode::Mixed, ColorChoice::Auto)];
let combined_logger = CombinedLogger::new(loggers);
let logger = SentryLogger::with_dest(combined_logger);
log::set_max_level(log_level);
log::set_boxed_logger(Box::new(logger))?;
}
let dsn = if cfg!(debug_assertions) { String::new() } else { SENTRY_DSN.read().unwrap().to_string() };
let client_options = sentry::ClientOptions {
release: release.clone(),
sample_rate: 1.0,
auto_session_tracking: true,
..Default::default()
};
let sentry_guard = sentry::init((dsn, client_options));
let sentry_enabled = sentry_guard.is_enabled();
let orig_hook = panic::take_hook();
let logging_path = logging_path.to_owned();
panic::set_hook(Box::new(move |info: &PanicHookInfo| {
warn!("Panic detected. Generating backtraces and crash logs...");
let data = Self::new(info, VERSION);
if data.save(&logging_path).is_err() {
error!("Failed to generate crash log.");
}
orig_hook(info);
if sentry_enabled {
end_session_with_status(SessionStatus::Crashed)
}
}));
info!("Logger initialized.");
Ok(sentry_guard)
}
pub fn new(panic_info: &PanicHookInfo, version: &str) -> Self {
let info = os_info::get();
let operating_system = format!("OS: {}\nVersion: {}", info.os_type(), info.version());
let mut explanation = String::new();
if let Some(payload) = panic_info.payload().downcast_ref::<&str>() {
explanation.push_str(&format!("Cause: {}\n", &payload));
}
match panic_info.location() {
Some(location) => explanation.push_str(&format!("Panic occurred in file '{}' at line {}\n", location.file(), location.line())),
None => explanation.push_str("Panic location unknown.\n"),
}
Self {
name: env!("CARGO_PKG_NAME").to_owned(),
crate_version: version.to_owned(),
build_type: if cfg!(debug_assertions) { "Debug" } else { "Release" }.to_owned(),
operating_system,
explanation,
backtrace: format!("{:#?}", Backtrace::new()),
}
}
pub fn save(&self, path: &Path) -> Result<()> {
let file_path = path.join(format!("error-report-{}.toml", current_time()?));
let mut file = BufWriter::new(File::create(file_path)?);
file.write_all(toml::to_string_pretty(&self)?.as_bytes())?;
Ok(())
}
pub fn send_event(sentry_guard: &ClientInitGuard, level: Level, message: &str, data: Option<(&str, &[u8])>) -> Result<()> {
if sentry_guard.is_enabled() {
let mut event = Event::new();
event.level = level;
event.message = Some(message.to_string());
let mut envelope = Envelope::from(event);
if let Some((filename, buffer)) = data {
let attatchment = Attachment {
buffer: buffer.to_vec(),
filename: filename.to_owned(),
content_type: Some("application/json".to_owned()),
ty: None
};
envelope.add_item(EnvelopeItem::Attachment(attatchment));
}
sentry_guard.send_envelope(envelope);
}
Ok(())
}
}