use std::borrow::Cow;
use std::env;
use std::fmt::{self, Display};
use std::path::PathBuf;
use std::str::FromStr;
use relay_common::impl_str_serde;
use sentry::types::Dsn;
use serde::{Deserialize, Serialize};
use tracing::level_filters::LevelFilter;
use tracing_subscriber::{prelude::*, EnvFilter, Layer};
#[cfg(feature = "dashboard")]
use crate::dashboard;
const RELEASE: &str = std::env!("RELAY_RELEASE");
include!(concat!(env!("OUT_DIR"), "/constants.gen.rs"));
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum LogFormat {
Auto,
Pretty,
Simplified,
Json,
}
#[derive(Clone, Debug)]
pub struct LevelParseError(String);
impl Display for LevelParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
r#"error parsing "{}" as level: expected one of "error", "warn", "info", "debug", "trace", "off""#,
self.0
)
}
}
#[derive(Clone, Copy, Debug)]
pub enum Level {
Error,
Warn,
Info,
Debug,
Trace,
Off,
}
impl_str_serde!(Level, "The logging level.");
impl Level {
pub const fn level_filter(&self) -> LevelFilter {
match self {
Level::Error => LevelFilter::ERROR,
Level::Warn => LevelFilter::WARN,
Level::Info => LevelFilter::INFO,
Level::Debug => LevelFilter::DEBUG,
Level::Trace => LevelFilter::TRACE,
Level::Off => LevelFilter::OFF,
}
}
}
impl Display for Level {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", format!("{:?}", self).to_lowercase())
}
}
impl FromStr for Level {
type Err = LevelParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let result = match s {
"" => Level::Error,
s if s.eq_ignore_ascii_case("error") => Level::Error,
s if s.eq_ignore_ascii_case("warn") => Level::Warn,
s if s.eq_ignore_ascii_case("info") => Level::Info,
s if s.eq_ignore_ascii_case("debug") => Level::Debug,
s if s.eq_ignore_ascii_case("trace") => Level::Trace,
s if s.eq_ignore_ascii_case("off") => Level::Off,
s => return Err(LevelParseError(s.into())),
};
Ok(result)
}
}
impl std::error::Error for LevelParseError {}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(default)]
pub struct LogConfig {
pub level: Level,
pub format: LogFormat,
pub enable_backtraces: bool,
pub traces_sample_rate: f32,
}
impl LogConfig {
pub const fn level_filter(&self) -> LevelFilter {
self.level.level_filter()
}
}
impl Default for LogConfig {
fn default() -> Self {
Self {
level: Level::Info,
format: LogFormat::Auto,
enable_backtraces: false,
#[cfg(debug_assertions)]
traces_sample_rate: 1.0,
#[cfg(not(debug_assertions))]
traces_sample_rate: 0.0,
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(default)]
pub struct SentryConfig {
pub dsn: Option<Dsn>,
pub enabled: bool,
pub environment: Option<Cow<'static, str>>,
pub _crash_db: Option<PathBuf>,
}
impl SentryConfig {
pub fn enabled_dsn(&self) -> Option<&Dsn> {
self.dsn.as_ref().filter(|_| self.enabled)
}
}
impl Default for SentryConfig {
fn default() -> Self {
Self {
dsn: "https://0cc4a37e5aab4da58366266a87a95740@sentry.io/1269704"
.parse()
.ok(),
enabled: false,
environment: None,
_crash_db: None,
}
}
}
#[cfg(feature = "crash-handler")]
fn capture_native_envelope(data: &[u8]) {
if let Some(client) = sentry::Hub::main().client() {
match sentry::Envelope::from_bytes_raw(data.to_owned()) {
Ok(envelope) => client.send_envelope(envelope),
Err(error) => {
let error = &error as &dyn std::error::Error;
crate::error!(error, "failed to capture crash")
}
}
} else {
crate::error!("failed to capture crash: no sentry client registered");
}
}
fn get_default_filters() -> EnvFilter {
let mut env_filter = EnvFilter::new(
"INFO,\
sqlx=WARN,\
tower_http=TRACE,\
trust_dns_proto=WARN,\
",
);
for name in CRATE_NAMES {
env_filter = env_filter.add_directive(format!("{name}=TRACE").parse().unwrap());
}
env_filter
}
pub fn init(config: &LogConfig, sentry: &SentryConfig) {
if config.enable_backtraces {
env::set_var("RUST_BACKTRACE", "full");
}
let subscriber = tracing_subscriber::fmt::layer()
.with_writer(std::io::stderr)
.with_target(true);
let format = match (config.format, console::user_attended()) {
(LogFormat::Auto, true) | (LogFormat::Pretty, _) => {
subscriber.compact().without_time().boxed()
}
(LogFormat::Auto, false) | (LogFormat::Simplified, _) => {
subscriber.with_ansi(false).boxed()
}
(LogFormat::Json, _) => subscriber
.json()
.flatten_event(true)
.with_current_span(true)
.with_span_list(true)
.with_file(true)
.with_line_number(true)
.boxed(),
};
let logs_subscriber = tracing_subscriber::registry()
.with(format.with_filter(config.level_filter()))
.with(sentry::integrations::tracing::layer())
.with(match env::var(EnvFilter::DEFAULT_ENV) {
Ok(value) => EnvFilter::new(value),
Err(_) => get_default_filters(),
});
#[cfg(feature = "dashboard")]
let logs_subscriber = logs_subscriber.with(dashboard::dashboard_subscriber());
logs_subscriber.init();
if let Some(dsn) = sentry.enabled_dsn() {
let guard = sentry::init(sentry::ClientOptions {
dsn: Some(dsn).cloned(),
in_app_include: vec!["relay"],
release: Some(RELEASE.into()),
attach_stacktrace: config.enable_backtraces,
environment: sentry.environment.clone(),
traces_sample_rate: config.traces_sample_rate,
..Default::default()
});
std::mem::forget(guard);
}
#[cfg(feature = "crash-handler")]
{
if let Some(dsn) = sentry.enabled_dsn().map(|d| d.to_string()) {
if let Some(db) = sentry._crash_db.as_deref() {
crate::info!("initializing crash handler in {}", db.display());
relay_crash::CrashHandler::new(dsn.as_str(), db)
.transport(capture_native_envelope)
.release(Some(RELEASE))
.environment(sentry.environment.as_deref())
.install();
}
}
}
}