Structured logging with a bring-your-own logger trait for the Moon-Lander framework
///|
import {
"moonrockz/moon-lander-logging" @logging,
}| Part | What it is |
|---|---|
| Logger | The trait that a backend implements. log gets each event; enabled filters levels and enables each level by default |
| Level | Trace, Debug, Information, Warning, Error, Critical, from the lowest to the highest |
| LogEvent | One immutable event: level, category, template, properties and error |
| MessageSegment | One part of the message: literal Text or a bound Value with its JSON type |
| Level methods | trace, debug, info, warn, error and critical on a &Logger |
| MemoryLogger | A logger that keeps each event, for tests and examples |
///|
struct PrintLogger {}
///|
impl @logging.Logger for PrintLogger with log(_self, event) {
println(event.to_line())
}
///|
test "bring your own logger" {
let logger : &@logging.Logger = PrintLogger::{ }
logger.info("Build started for {Target}", ["wasm"])
}///|
fn build(logger : &@logging.Logger) -> Unit {
logger.info("Build started", [])
logger.warn("Cache is empty", [])
}
///|
test "read the events of a library function" {
let memory = @logging.MemoryLogger::new()
build(memory)
let levels = memory.events().map(event => event.level)
assert_eq(levels, [Information, Warning])
}///|
test "a template keeps its values as data" {
let memory = @logging.MemoryLogger::new()
let logger : &@logging.Logger = memory
let path = "src"
logger.info("Loaded {Count} files from {Path}", [3, Json(path)])
let event = memory.events()[0]
assert_eq(event.template, "Loaded {Count} files from {Path}")
assert_eq(event.properties(), [("Count", 3), ("Path", "src")])
assert_eq(event.message(), "Loaded 3 files from src")
}| Template part | Meaning |
|---|---|
| {Name} | A hole. The name holds ASCII letters, digits and _ |
| {{ and }} | A literal { and a literal } |
| Any other brace | Literal text, for example an unclosed {Name or an empty {} |
| Case | Result |
|---|---|
| A hole has no argument | No property; the message keeps the hole text, for example {Target} |
| An argument has no hole | The argument is dropped |
| One name in two holes | Each hole takes its own argument; the message shows the first value in both |
| Segment | What it holds |
|---|---|
| Text(text) | Literal text. A hole with no argument stays here as its text, for example {Target} |
| Value(name~, value~) | A bound hole: its name and its JSON value |
///|
test "the segments of a message" {
let memory = @logging.MemoryLogger::new()
(memory : &@logging.Logger).info("Loaded {Count} files from {Path}", [
3, "src",
])
let event = memory.events()[0]
let segments = event.segments()
assert_eq(segments, [
Text("Loaded "),
Value(name="Count", value=3),
Text(" files from "),
Value(name="Path", value="src"),
])
let text = segments.map(segment => segment.text()).join("")
assert_eq(text, event.message())
}| Function | Result |
|---|---|
| null_logger() | Enables no level and drops each event |
| MemoryLogger::new() | Keeps each event; events() gives a copy |
| min_level(logger, level) | Passes only the events at level or higher that logger enables |
| fan_out(loggers) | Gives each event to each logger in the list that enables its level |
| with_category(logger, name) | Sets the category name on each event |
///|
test "chain the combinators" {
let memory = @logging.MemoryLogger::new()
let logger = memory
|> @logging.min_level(Information)
|> @logging.with_category("morphir.build")
logger.debug("Dropped", [])
logger.info("Loaded {Count} files", [3])
let events = memory.events()
assert_eq(events.length(), 1)
assert_eq(events[0].category, Some("morphir.build"))
}///|
test "two backends with their own levels" {
let console = @logging.MemoryLogger::new()
let errors = @logging.MemoryLogger::new()
let logger = @logging.fan_out([console, @logging.min_level(errors, Error)])
logger.warn("Disk {Name} is low", ["C"])
assert_eq(console.events().length(), 1)
assert_eq(errors.events().length(), 0)
}///|
struct WarningsOnly {
mut written : Int
}
///|
impl @logging.Logger for WarningsOnly with enabled(_self, level) {
level >= Warning
}
///|
impl @logging.Logger for WarningsOnly with log(self, _event) {
self.written += 1
}
///|
test "a disabled level builds no event" {
let backend = WarningsOnly::{ written: 0 }
let logger : &@logging.Logger = backend
logger.debug("Not built", [])
logger.error("Built", [])
assert_eq(backend.written, 1)
}pub struct LogEvent {
level : Level
category : String?
template : String
error : Error?
// private fields
}pub struct MemoryLogger {
// private fields
}impl Logger for MemoryLoggerInstall
Download zipStructured logging with a bring-your-own logger trait for the Moon-Lander framework