// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0
///|
/// How severe a record is.
///
/// A level is a value rather than an enumeration so an application can define
/// its own — `Level::{ name: "notice", value: 35, colour: Blue }` — and have it
/// work in filters, calls and layouts alike. Only `value` counts when levels are
/// compared; `name` is what filters and labels read.
pub(all) struct Level {
name : String
value : Int32
colour : Colour
}
///|
/// Step-by-step detail.
pub let trace : Level = { name: "trace", value: 10, colour: Colour::Cyan, }
///|
/// Diagnostics for developers.
pub let debug : Level = { name: "debug", value: 20, colour: Colour::Magenta, }
///|
/// Normal operation.
pub let info : Level = { name: "info", value: 30, colour: Colour::Green, }
///|
/// Something unexpected that the program survived.
pub let warn : Level = { name: "warn", value: 40, colour: Colour::Yellow, }
///|
/// A failure that needs attention.
pub let error : Level = { name: "error", value: 50, colour: Colour::Red, }
///|
/// A failure the program cannot carry on from.
pub let fatal : Level = { name: "fatal", value: 60, colour: Colour::Alarm, }
///|
/// The six built-in levels, quietest first.
pub fn built_in() -> Array[Level] {
[trace, debug, info, warn, error, fatal]
}
///|
/// Its number.
pub fn Level::value(self : Level) -> Int32 {
self.value
}
///|
/// Its lower-case name, as filters, labels and configuration files write it.
pub fn Level::name(self : Level) -> String {
self.name
}
///|
/// Its colour in a terminal.
pub fn Level::colour(self : Level) -> Colour {
self.colour
}
///|
/// The same level under another name, which is how a profile's vocabulary
/// reaches a JSON document.
pub fn Level::named(self : Level, name : String) -> Level {
{ name, value: self.value, colour: self.colour, }
}
///|
/// Whether this level passes a minimum.
pub fn Level::at_least(self : Level, min : Level) -> Bool {
self.value >= min.value
}
///|
/// A level by name or by number, in any capitalisation.
///
/// `warning` is `warn`, `critical` is `fatal`, and `0` to `255` is a level by
/// value, so a level defined by an application is still spelled the same way
/// once it reaches a filter.
pub fn Level::parse(name : StringView) -> Level raise Refused {
let s = name.to_lower()
match s {
"trace" => trace
"debug" => debug
"info" => info
"warn" | "warning" => warn
"error" | "err" => error
"fatal" | "critical" => fatal
_ =>
match number(s) {
Some(n) if n >= 0 && n <= 255 =>
{ name: s.to_owned(), value: n, colour: Colour::Plain, }
_ =>
raise refuse(
0,
name.to_owned(),
"trace, debug, info, warn, error, fatal or 0 to 255",
)
}
}
}
///|
/// A decimal number, or `None` when `s` holds anything else.
fn number(s : StringView) -> Int32? {
if s.length() == 0 {
return None
}
let mut n = 0
for c in s {
if c < '0' || c > '9' {
return None
}
n = n * 10 + (c.to_int() - 48)
}
Some(n)
}
///|
/// How a profile's logger writes this level.
///
/// A pattern preset uses its own logger's vocabulary; every other layout uses
/// the profile in force. Python's `WARNING` and `CRITICAL`, and `slog`'s
/// numeric names for the two levels it has no word for, are the only
/// differences — which is the point of a profile.
pub fn Level::label(self : Level, profile : Profile) -> String {
match self.value {
40 => if profile == Profile::Python { "WARNING" } else { "WARN" }
60 =>
if profile == Profile::Python {
"CRITICAL"
} else if profile == Profile::Slog {
"ERROR+4"
} else {
"FATAL"
}
10 => if profile == Profile::Slog { "DEBUG-4" } else { "TRACE" }
20 => "DEBUG"
30 => "INFO"
50 => "ERROR"
_ => self.name.to_upper()
}
}
///|
/// The syslog severity, for GELF and anything else shaped like syslog.
pub fn Level::syslog(self : Level) -> Int32 {
if self.value <= 20 {
7
} else if self.value <= 30 {
6
} else if self.value <= 40 {
4
} else if self.value <= 50 {
3
} else {
2
}
}
///|
/// The number Logstash writes, 5000 to 40000.
pub fn Level::logstash(self : Level) -> Int32 {
if self.value <= 10 {
5000
} else if self.value <= 20 {
10000
} else if self.value <= 30 {
20000
} else if self.value <= 40 {
30000
} else {
40000
}
}
///|
/// The severity number of the OpenTelemetry log model.
pub fn Level::otel(self : Level) -> Int32 {
if self.value <= 10 {
1
} else if self.value <= 20 {
5
} else if self.value <= 30 {
9
} else if self.value <= 40 {
13
} else if self.value <= 50 {
17
} else {
21
}
}
///|
pub impl Eq for Level with fn equal(self, other) {
self.value == other.value
}
///|
pub extend Level with Eq::{equal, not_equal}
///|
pub impl Compare for Level with fn compare(self, other) {
if self.value < other.value {
-1
} else if self.value > other.value {
1
} else {
0
}
}
///|
pub extend Level with Compare::{compare, op_ge, op_gt, op_le, op_lt}
///|
pub impl Show for Level with fn output(self, logger) {
logger.write_string(self.name)
}
///|
pub extend Level with Show::{output, to_string}