use std::fmt::{self, Debug, Formatter};
use tracing::{info, instrument, warn};
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
use tracing_subscriber::{EnvFilter, Layer};
use crate::env::{self, EnvError};
use crate::environment::Environment;
pub const ENV_SENTRY_DSN: &str = "WSB_SENTRY_SERVER_DSN";
pub const DEFAULT_LOG_FILTER: &str = "info";
#[derive(Debug, thiserror::Error)]
pub enum ObservabilityError {
#[error(
"environment variable `{ENV_SENTRY_DSN}` is required in production; \
error monitoring cannot be disabled there"
)]
MissingDsnInProduction,
#[error(transparent)]
Env(#[from] EnvError),
}
#[derive(Clone)]
pub struct Observability {
dsn: Option<String>,
environment: Environment,
log_filter: Option<String>,
release: String,
}
pub const UNKNOWN_RELEASE: &str = "unknown";
impl Observability {
#[must_use]
pub fn new(environment: Environment, dsn: impl Into<String>) -> Self {
Self {
dsn: Some(dsn.into()),
environment,
log_filter: None,
release: String::from(UNKNOWN_RELEASE),
}
}
#[must_use]
pub fn none(environment: Environment) -> Self {
Self {
dsn: None,
environment,
log_filter: None,
release: String::from(UNKNOWN_RELEASE),
}
}
pub fn from_env(environment: Environment) -> Result<Self, ObservabilityError> {
let dsn: String = env::required(ENV_SENTRY_DSN)?;
Ok(Self {
dsn: Some(dsn),
environment,
log_filter: None,
release: String::from(UNKNOWN_RELEASE),
})
}
#[must_use]
pub fn with_release(mut self, release: impl Into<String>) -> Self {
self.release = release.into();
self
}
#[must_use]
pub fn with_log_filter(mut self, log_filter: impl Into<String>) -> Self {
self.log_filter = Some(log_filter.into());
self
}
#[must_use]
pub const fn has_error_monitoring(&self) -> bool {
self.dsn.is_some()
}
#[must_use]
pub const fn environment(&self) -> Environment {
self.environment
}
#[must_use]
#[instrument(skip_all)]
pub fn init(self) -> ObservabilityGuard {
let sentry_guard: Option<sentry::ClientInitGuard> = self.dsn.as_ref().map(|dsn| {
let mut options: sentry::ClientOptions = sentry::ClientOptions::default();
options.release = Some(self.release.clone().into());
options.environment = Some(self.environment.as_str().into());
options.attach_stacktrace = true;
sentry::init((dsn.clone(), options))
});
let fallback: &str = self.log_filter.as_deref().unwrap_or(DEFAULT_LOG_FILTER);
let directives: String =
std::env::var(EnvFilter::DEFAULT_ENV).unwrap_or_else(|_| fallback.to_string());
let filter: EnvFilter =
EnvFilter::try_new(&directives).unwrap_or_else(|_| EnvFilter::new(DEFAULT_LOG_FILTER));
let format = if self.environment.is_production() {
tracing_subscriber::fmt::layer()
.json()
.flatten_event(true)
.with_current_span(true)
.with_span_list(false)
.with_ansi(false)
.with_filter(filter)
.boxed()
} else {
tracing_subscriber::fmt::layer().with_filter(filter).boxed()
};
tracing_subscriber::registry()
.with(format)
.with(sentry::integrations::tracing::layer())
.init();
info!(
release = %self.release,
environment = %self.environment,
log_filter = %directives,
"starting"
);
if sentry_guard.is_none() {
warn!("error monitoring is not configured; nothing will reach Sentry");
}
ObservabilityGuard {
sentry: sentry_guard,
}
}
}
impl Debug for Observability {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.debug_struct("Observability")
.field("environment", &self.environment)
.field("release", &self.release)
.field("error_monitoring", &self.dsn.is_some())
.field("log_filter", &self.log_filter)
.finish()
}
}
pub struct ObservabilityGuard {
sentry: Option<sentry::ClientInitGuard>,
}
impl Debug for ObservabilityGuard {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.debug_struct("ObservabilityGuard")
.field("error_monitoring", &self.sentry.is_some())
.finish()
}
}
#[cfg(test)]
mod tests {
use super::Observability;
use crate::environment::Environment;
#[test]
fn debug_output_never_contains_the_dsn() {
let observability: Observability =
Observability::new(Environment::Production, "https://sup3rs3cret@example.com/1");
let actual: String = format!("{observability:?}");
assert!(!actual.contains("sup3rs3cret"));
assert!(actual.contains("error_monitoring: true"));
}
#[test]
fn a_custom_log_filter_is_retained() {
let observability: Observability =
Observability::none(Environment::Local).with_log_filter("debug");
let actual: String = format!("{observability:?}");
assert!(actual.contains("debug"));
}
}