///|
/// Log severity (Ruby `::Logger::Severity`).
pub(all) enum Severity {
Debug
Info
Warn
Error
Fatal
Unknown
} derive(Eq, Debug)
///|
pub fn Severity::to_int(self : Severity) -> Int {
match self {
Debug => 0
Info => 1
Warn => 2
Error => 3
Fatal => 4
Unknown => 5
}
}
///|
/// Label used by the basic formatter (`WARN` → `WARNING`, `FATAL` → `FAILED`).
pub fn Severity::label(self : Severity) -> String {
match self {
Debug => "DEBUG"
Info => "INFO"
Warn => "WARNING"
Error => "ERROR"
Fatal => "FAILED"
Unknown => "ANY"
}
}
///|
/// A log message with optional source context (Ruby `message_with_context`).
pub(all) struct LogMessage {
severity : Severity
text : String
source_location : Cursor?
include_location : Cursor?
} derive(Debug)
///|
/// `path: line N: text` when a source location is present (Ruby `AutoFormattingMessage#inspect`).
pub fn LogMessage::to_string(self : LogMessage) -> String {
match self.source_location {
Some(loc) => "\{loc.line_info()}: \{self.text}"
None => self.text
}
}
///|
/// A logger. Messages below `level` are dropped (except by a memory logger,
/// which records everything, like Ruby's `MemoryLogger`).
pub struct Logger {
mut level : Severity
mut max_severity : Severity?
priv sink : (LogMessage) -> Unit
priv record_all : Bool
priv report_all : Bool
}
///|
/// Creates a logger that forwards messages at or above `level` to `sink`.
pub fn Logger::new(
sink : (LogMessage) -> Unit,
level? : Severity = Warn,
) -> Logger {
{ level, max_severity: None, sink, record_all: false, report_all: false, }
}
///|
/// A logger that discards messages but tracks the maximum severity.
pub fn Logger::null() -> Logger {
{
level: Unknown,
max_severity: None,
sink: _ => (),
record_all: false,
report_all: false,
}
}
///|
/// A logger that records every message into `messages`. Like Ruby's
/// `MemoryLogger`, its level is `Unknown`; unlike Ruby, `is_debug`/`is_info`
/// report true so that callers capturing messages for later replay (see
/// `io.run_with_files`) see debug and info messages too.
pub fn Logger::memory(messages : Array[LogMessage]) -> Logger {
{
level: Unknown,
max_severity: None,
sink: m => messages.push(m),
record_all: true,
report_all: true,
}
}
///|
/// A logger that records every message into `messages` (whatever its
/// severity, like a hook on Ruby's `Logger#add`) while `is_debug`/`is_info`
/// answer according to `level`, as for a Ruby logger with that level.
pub fn Logger::recording(
messages : Array[LogMessage],
level~ : Severity,
) -> Logger {
{
level,
max_severity: None,
sink: m => messages.push(m),
record_all: true,
report_all: false,
}
}
///|
/// Formats a message like Ruby's `Asciidoctor::Logger::BasicFormatter`.
pub fn format_log_message(m : LogMessage) -> String {
"asciidoctor: \{m.severity.label()}: \{m.to_string()}"
}
///|
pub fn Logger::add(self : Logger, m : LogMessage) -> Unit {
match self.max_severity {
Some(s) if s.to_int() >= m.severity.to_int() => ()
_ => self.max_severity = Some(m.severity)
}
if self.record_all || m.severity.to_int() >= self.level.to_int() {
(self.sink)(m)
}
}
///|
/// Sets the minimum severity of messages forwarded to the sink.
pub fn Logger::set_level(self : Logger, level : Severity) -> Unit {
self.level = level
}
///|
pub fn Logger::is_debug(self : Logger) -> Bool {
self.report_all || self.level.to_int() <= 0
}
///|
pub fn Logger::is_info(self : Logger) -> Bool {
self.report_all || self.level.to_int() <= 1
}
///|
let current_logger : Ref[Logger] = { val: Logger::null(), }
///|
/// The global logger (Ruby `LoggerManager.logger`).
pub fn logger() -> Logger {
current_logger.val
}
///|
/// Replaces the global logger; returns the previous one.
pub fn set_logger(l : Logger) -> Logger {
let old = current_logger.val
current_logger.val = l
old
}
///|
/// Runs `f` with a memory logger installed and returns the recorded messages.
pub fn[T] with_memory_logger(
f : () -> T raise?,
) -> (T, Array[LogMessage]) raise? {
let messages = []
let old = set_logger(Logger::memory(messages))
errdefer set_logger(old) |> ignore
let r = f()
set_logger(old) |> ignore
(r, messages)
}
///|
fn log_msg(
severity : Severity,
text : String,
source_location? : Cursor,
include_location? : Cursor,
) -> Unit {
logger().add({ severity, text, source_location, include_location, })
}
///|
fn log_warn(
text : String,
source_location? : Cursor,
include_location? : Cursor,
) -> Unit {
log_msg(Warn, text, source_location?, include_location?)
}
///|
fn log_error(
text : String,
source_location? : Cursor,
include_location? : Cursor,
) -> Unit {
log_msg(Error, text, source_location?, include_location?)
}
///|
fn log_info(text : String, source_location? : Cursor) -> Unit {
log_msg(Info, text, source_location?)
}
///|
fn log_debug(text : String, source_location? : Cursor) -> Unit {
log_msg(Debug, text, source_location?)
}
///|
/// Keys of one-time actions already performed in this process (Ruby keeps such
/// state in class variables, e.g. warning once that a gem is unavailable).
let once_keys : Map[String, Bool] = Map([])
///|
/// Returns true the first time it is called with `key` in this process.
fn once_per_process(key : String) -> Bool {
if once_keys.contains(key) {
false
} else {
once_keys[key] = true
true
}
}
///|
/// Snapshot of the process-wide one-time state (see `restore_process_state`).
pub fn snapshot_process_state() -> Array[String] {
once_keys.keys().collect()
}
///|
/// Restores the process-wide one-time state. Used by drivers that rerun the
/// pipeline (e.g. `@io`) so each run observes the same state.
pub fn restore_process_state(keys : Array[String]) -> Unit {
once_keys.clear()
for k in keys {
once_keys[k] = true
}
}