use std::env;
use std::error::Error;
use std::fmt;
pub use tracing::{self, Level};
pub use tracing::{debug, error, info, trace, warn};
pub use tracing::{debug_span, error_span, info_span, span, trace_span};
pub use tracing::{event, event_enabled};
pub use tracing::{Event, Span, Value};
pub use tracing::Instrument;
pub use tracing::field;
pub const LOG_FILTER_ENV: &str = "LGWKS_LOG";
pub const RUST_LOG_FILTER_ENV: &str = "RUST_LOG";
pub const LOG_FORMAT_ENV: &str = "LGWKS_LOG_FORMAT";
pub const DEFAULT_FILTER: &str = "info";
pub const OTEL_SCHEMA_URL: &str = "https://opentelemetry.io/schemas/1.27.0";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum DebugFormat {
Compact,
Pretty,
Json,
}
impl DebugFormat {
pub fn from_env() -> Result<Self, DebugInstallError> {
match env_string(LOG_FORMAT_ENV)? {
Some(value) => Self::parse(&value).ok_or(DebugInstallError::InvalidFormat { value }),
None => Ok(Self::Compact),
}
}
#[must_use]
pub fn parse(value: &str) -> Option<Self> {
if value.eq_ignore_ascii_case("compact") {
Some(Self::Compact)
} else if value.eq_ignore_ascii_case("pretty") {
Some(Self::Pretty)
} else if value.eq_ignore_ascii_case("json") {
Some(Self::Json)
} else {
None
}
}
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Compact => "compact",
Self::Pretty => "pretty",
Self::Json => "json",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct DebugConfig {
service_name: String,
filter: String,
format: DebugFormat,
ansi: bool,
targets: bool,
thread_names: bool,
thread_ids: bool,
}
impl DebugConfig {
#[must_use]
pub fn new(service_name: impl Into<String>) -> Self {
Self {
service_name: service_name.into(),
filter: DEFAULT_FILTER.to_owned(),
format: DebugFormat::Compact,
ansi: true,
targets: true,
thread_names: true,
thread_ids: false,
}
}
pub fn from_env(service_name: impl Into<String>) -> Result<Self, DebugInstallError> {
let filter = env_string(LOG_FILTER_ENV)?
.or(env_string(RUST_LOG_FILTER_ENV)?)
.unwrap_or_else(|| DEFAULT_FILTER.to_owned());
let format = DebugFormat::from_env()?;
Ok(Self::new(service_name)
.with_filter(filter)
.with_format(format))
}
#[must_use]
pub fn with_filter(mut self, filter: impl Into<String>) -> Self {
self.filter = filter.into();
self
}
#[must_use]
pub const fn with_format(mut self, format: DebugFormat) -> Self {
self.format = format;
self
}
#[must_use]
pub const fn with_ansi(mut self, ansi: bool) -> Self {
self.ansi = ansi;
self
}
#[must_use]
pub const fn with_targets(mut self, targets: bool) -> Self {
self.targets = targets;
self
}
#[must_use]
pub const fn with_thread_names(mut self, thread_names: bool) -> Self {
self.thread_names = thread_names;
self
}
#[must_use]
pub const fn with_thread_ids(mut self, thread_ids: bool) -> Self {
self.thread_ids = thread_ids;
self
}
#[must_use]
pub fn service_name(&self) -> &str {
&self.service_name
}
#[must_use]
pub fn filter(&self) -> &str {
&self.filter
}
#[must_use]
pub const fn format(&self) -> DebugFormat {
self.format
}
pub fn install(self) -> Result<(), DebugInstallError> {
self.validate()?;
let filter = tracing_subscriber::EnvFilter::try_new(&self.filter).map_err(|source| {
DebugInstallError::InvalidFilter {
value: self.filter.clone(),
source,
}
})?;
let service_name = self.service_name.clone();
let format = self.format;
let builder = tracing_subscriber::fmt()
.with_writer(std::io::stderr)
.with_env_filter(filter)
.with_ansi(self.ansi)
.with_target(self.targets)
.with_thread_names(self.thread_names)
.with_thread_ids(self.thread_ids);
let subscriber: Box<dyn tracing::Subscriber + Send + Sync> = match format {
DebugFormat::Compact => Box::new(builder.compact().finish()),
DebugFormat::Pretty => Box::new(builder.pretty().finish()),
DebugFormat::Json => Box::new(builder.json().flatten_event(true).finish()),
};
tracing::subscriber::set_global_default(subscriber)
.map_err(|source| DebugInstallError::InstallFailed { source })?;
info!(
service_name = service_name.as_str(),
otel_schema_url = OTEL_SCHEMA_URL,
debugger_format = format.as_str(),
"debugger installed"
);
Ok(())
}
fn validate(&self) -> Result<(), DebugInstallError> {
if self.service_name.trim().is_empty() {
let refusal = Err(DebugInstallError::MissingServiceName);
#[cfg(feature = "trace")]
crate::trace::debug!(error = ?refusal.as_ref().err(), "validate: returning an error to the caller");
return refusal;
}
Ok(())
}
}
pub fn install_default(service_name: impl Into<String>) -> Result<(), DebugInstallError> {
DebugConfig::from_env(service_name)?.install()
}
#[derive(Debug)]
#[non_exhaustive]
pub enum DebugInstallError {
MissingServiceName,
InvalidEnvironment {
variable: &'static str,
source: env::VarError,
},
InvalidFormat {
value: String,
},
InvalidFilter {
value: String,
source: tracing_subscriber::filter::ParseError,
},
InstallFailed {
source: tracing::subscriber::SetGlobalDefaultError,
},
}
impl fmt::Display for DebugInstallError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
Self::MissingServiceName => write!(formatter, "debugger service name is empty"),
Self::InvalidEnvironment { variable, .. } => {
write!(formatter, "{variable} is not valid Unicode")
}
Self::InvalidFormat { ref value } => write!(
formatter,
"{LOG_FORMAT_ENV} value {value:?} is not one of compact, pretty, json"
),
Self::InvalidFilter { ref value, .. } => {
write!(formatter, "trace filter {value:?} is invalid")
}
Self::InstallFailed { .. } => {
write!(
formatter,
"a global tracing subscriber is already installed"
)
}
}
}
}
impl Error for DebugInstallError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match *self {
Self::InvalidEnvironment { ref source, .. } => Some(source),
Self::InvalidFilter { ref source, .. } => Some(source),
Self::InstallFailed { ref source } => Some(source),
Self::MissingServiceName | Self::InvalidFormat { .. } => None,
}
}
}
fn env_string(variable: &'static str) -> Result<Option<String>, DebugInstallError> {
match env::var(variable) {
Ok(value) if value.trim().is_empty() => Ok(None),
Ok(value) => Ok(Some(value)),
Err(env::VarError::NotPresent) => Ok(None),
Err(source) => Err(DebugInstallError::InvalidEnvironment { variable, source }),
}
}
#[cfg(test)]
mod tests {
use super::{DebugConfig, DebugFormat, DebugInstallError, LOG_FORMAT_ENV};
type TestResult = Result<(), Box<dyn std::error::Error>>;
#[test]
fn format_parser_accepts_the_documented_names() {
assert_eq!(
DebugFormat::parse("compact"),
Some(DebugFormat::Compact),
"compact format should parse"
);
assert_eq!(
DebugFormat::parse("PRETTY"),
Some(DebugFormat::Pretty),
"format names are case-insensitive"
);
assert_eq!(
DebugFormat::parse("json"),
Some(DebugFormat::Json),
"json format should parse"
);
assert_eq!(
DebugFormat::parse("yaml"),
None,
"undocumented formats are refused"
);
}
#[test]
fn config_refuses_an_empty_service_name() {
let error = DebugConfig::new(" ").install().err();
assert!(
matches!(error, Some(DebugInstallError::MissingServiceName)),
"empty service names must not install a nameless telemetry source: {error:?}"
);
}
#[test]
fn invalid_filter_is_reported_as_filter_error() -> TestResult {
let error = DebugConfig::new("filter-test")
.with_filter("[")
.install()
.err()
.ok_or("invalid filter unexpectedly installed")?;
assert!(
matches!(error, DebugInstallError::InvalidFilter { .. }),
"invalid filters should fail before global subscriber install: {error:?}"
);
Ok(())
}
#[test]
fn format_names_are_stable_for_the_doctor() {
assert_eq!(
DebugFormat::Json.as_str(),
"json",
"doctor output should use stable format names"
);
assert_eq!(
LOG_FORMAT_ENV, "LGWKS_LOG_FORMAT",
"format environment variable is the documented one"
);
}
}