///|
/// Logging configuration with a root level and category-specific overrides.
///
/// Category overrides are hierarchical: `app.db.query` falls back to `app.db`,
/// then `app`, then the logger's root level.
struct Config {
mut level : Level
levels : Map[String, Level]
}
///|
/// Create a configuration with the given root level.
pub fn Config::Config(level? : Level = Info) -> Config {
Config::{ level, levels: {} }
}
///|
/// Errors raised when parsing or updating logger configuration.
pub(all) suberror ConfigError {
/// The category name is empty or contains invalid dot placement.
InvalidCategory(String)
/// The configuration string contains an empty or malformed directive.
InvalidDirective(String)
/// The level name is not one of the supported `Level` values.
InvalidLevel(String)
}
///|
/// Read configuration from an environment variable.
///
/// `env` defaults to `MOON_XLOG`. The value is a comma-separated list of
/// directives: either a root level such as `debug`, or a category override such
/// as `app.db=trace`.
pub fn Config::from_env(
env? : String = "MOON_XLOG",
level? : Level = Info,
) -> Config raise ConfigError {
let config = Config(level~)
guard @env.get_env_var(env) is Some(spec) else { config }
config.parse(spec)
config
}
///|
/// Set a level override for a category.
///
/// Category names must be non-empty dot-separated segments, with no leading
/// dot, trailing dot, or empty segment.
pub fn Config::set_category_level(
self : Config,
category : String,
level : Level,
) -> Unit raise ConfigError {
guard is_valid_category(category) else {
raise ConfigError::InvalidCategory(category)
}
self.levels[category] = level
}
///|
fn Config::parse(self : Config, spec : String) -> Unit raise ConfigError {
if spec.is_blank() {
return
}
for raw in spec.split(",") {
let directive = raw.trim()
guard !directive.is_blank() else {
raise ConfigError::InvalidDirective(raw.to_owned())
}
match directive.split_once("=") {
Some((category, level)) => {
let category = category.trim().to_owned()
let level = level.trim()
let level = parse_level(level)
self.set_category_level(category, level)
}
None => self.level = parse_level(directive)
}
}
}
///|
fn parse_level(level : StringView) -> Level raise ConfigError {
match level {
['F' | 'f', 'A' | 'a', 'T' | 't', 'A' | 'a', 'L' | 'l'] => Fatal
['E' | 'e', 'R' | 'r', 'R' | 'r', 'O' | 'o', 'R' | 'r'] => Error
['W' | 'w', 'A' | 'a', 'R' | 'r', 'N' | 'n'] => Warn
['I' | 'i', 'N' | 'n', 'F' | 'f', 'O' | 'o'] => Info
['D' | 'd', 'E' | 'e', 'B' | 'b', 'U' | 'u', 'G' | 'g'] => Debug
['T' | 't', 'R' | 'r', 'A' | 'a', 'C' | 'c', 'E' | 'e'] => Trace
_ => raise ConfigError::InvalidLevel(level.to_owned())
}
}
///|
fn is_valid_category(category : String) -> Bool {
!category.is_empty() &&
!category.has_prefix(".") &&
!category.has_suffix(".") &&
!category.contains("..")
}
///|
fn Config::get_category_level(self : Config, category : String) -> Level? {
if self.levels.get(category) is Some(level) {
return Some(level)
}
guard category.rev_split_once(".") is Some((parent, _)) else { None }
self.get_category_level(parent.to_owned())
}