///|
import {
"moonrockz/moon-lander-logging" @logging,
"moonrockz/moon-lander-logging/formatting",
}| Part | What it is |
|---|---|
| LogTimestamp | A UTC time in milliseconds, from 1970-01-01T00:00:00.000Z to 9999-12-31T23:59:59.999Z |
| LogRecord | One captured event and its time. It does not change after capture |
| Theme | Literate (ANSI colors) or Plain (no ANSI codes) |
| format_console | One console event, for example [14:23:07.012Z INF] app: Started |
| format_text | One event of a readable log file, with the full UTC date |
| format_json_line | One JSON Lines record of schema version 1 |
| format_json_object_line | One JSON Lines record of schema version 2, with named properties |
| utf8_length | The UTF-8 bytes of a rendered record, for size limits |
///|
test "capture an event with its time" {
let memory = @logging.MemoryLogger::new()
@logging.with_category(memory, "workspace.loader").info(
"Loaded {Count} files from {Path}",
[3, "src"],
)
let time = @formatting.LogTimestamp::from_epoch_millis(1793110987024L)
let record = @formatting.LogRecord::capture(memory.events()[0], time)
assert_eq(record.message(), "Loaded 3 files from src")
assert_eq(record.timestamp().to_iso8601(), "2026-10-27T14:23:07.024Z")
assert_eq(record.timestamp().time_of_day(), "14:23:07.024Z")
assert_eq(record.timestamp().compact_date(), "20261027")
}| Layout | Example |
|---|---|
| Console | [14:23:07.012Z INF] workspace.loader: Loaded 3 files from src |
| Readable file | 2026-10-27T14:23:07.024Z [INF] workspace.loader: Loaded 3 files from src |
///|
suberror BuildError {
BuildError(String)
}
///|
impl Show for BuildError with output(self, logger) {
let BuildError(text) = self
logger.write_string(text)
}
///|
test "readable text with an error" {
let memory = @logging.MemoryLogger::new()
@logging.with_category(memory, "build.compiler").error(
"Compilation failed for {File}",
["app.mbt"],
error=BuildError(
"CompilerError: Unknown identifier calculate_total\n at app.mbt:42",
),
)
let time = @formatting.LogTimestamp::from_epoch_millis(1793110987041L)
let record = @formatting.LogRecord::capture(memory.events()[0], time)
inspect(@formatting.format_text(record), content=(
#|2026-10-27T14:23:07.041Z [ERR] build.compiler: Compilation failed for app.mbt
#| CompilerError: Unknown identifier calculate_total
#| at app.mbt:42
#|
))
inspect(@formatting.format_console(record, Plain), content=(
#|[14:23:07.041Z ERR] build.compiler: Compilation failed for app.mbt
#| CompilerError: Unknown identifier calculate_total
#| at app.mbt:42
#|
))
}///|
test "an escape character becomes visible text" {
let memory = @logging.MemoryLogger::new()
(memory : &@logging.Logger).warn("Input {Value}", ["\u{1b}[2Jcleared\r"])
let record = @formatting.LogRecord::capture(
memory.events()[0],
@formatting.LogTimestamp::earliest(),
)
inspect(@formatting.format_console(record, Plain), content=(
#|[00:00:00.000Z WRN] Input \x1b[2Jcleared\x0d
#|
))
}| Part | Literate style |
|---|---|
| Time, brackets and category | Bright black (90) |
| Literal message text | White (37) |
| Number value | Bright magenta (95) |
| String value | Bright cyan (96) |
| Boolean value | Bright blue (94) |
| Null value | Blue (34) |
| Array or object value | Bright green (92) |
| TRC / DBG / INF | Bright black / white / bright white |
| WRN | Bright yellow (93) |
| ERR / CRT | Bright white on red (97;41); CRT is also bold |
| Error text | Bright red (91) |
///|
test "a literate number value has its own style" {
let memory = @logging.MemoryLogger::new()
(memory : &@logging.Logger).info("Loaded {Count} files", [3])
let record = @formatting.LogRecord::capture(
memory.events()[0],
@formatting.LogTimestamp::earliest(),
)
let literate = @formatting.format_console(record, Literate)
assert_true(literate.contains("\u{1b}[95m3\u{1b}[0m"))
assert_true(literate.has_suffix("\u{1b}[0m\n"))
assert_false(@formatting.format_console(record, Plain).contains("\u{1b}"))
}| Key | Value |
|---|---|
| schemaVersion | The number 1 |
| timestamp | The captured UTC time, for example "2026-10-27T14:23:07.024Z" |
| level | The level name, for example "Information" |
| category | The category, or null |
| messageTemplate | The template as the caller wrote it |
| message | The message text |
| properties | An array of {"name": ..., "value": ...} objects, in hole order |
| error | The Show text of the error, or null |
///|
test "one JSON Lines record" {
let memory = @logging.MemoryLogger::new()
@logging.with_category(memory, "workspace.loader").info(
"Loaded {Count} files from {Path}",
[3, "src"],
)
let time = @formatting.LogTimestamp::from_epoch_millis(1793110987024L)
let record = @formatting.LogRecord::capture(memory.events()[0], time)
inspect(@formatting.format_json_line(record), content=(
#|{"schemaVersion":1,"timestamp":"2026-10-27T14:23:07.024Z","level":"Information","category":"workspace.loader","messageTemplate":"Loaded {Count} files from {Path}","message":"Loaded 3 files from src","properties":[{"name":"Count","value":3},{"name":"Path","value":"src"}],"error":null}
#|
))
}///|
test "named JSON with complete duplicate history" {
let memory = @logging.MemoryLogger::new()
(memory : &@logging.Logger).info("{path} {path} {level}", [
"/repo/morphir.toml", "/repo/app/morphir.toml", "event value",
])
let record = @formatting.LogRecord::capture(memory.events()[0], @formatting.LogTimestamp::earliest())
let line = @formatting.format_json_object_line(record)
guard @json.parse(line) is { "schemaVersion": 2, "level": "Information", "properties": { "path": "/repo/app/morphir.toml", "level": "event value", .. }, "duplicateProperties": { "path": ["/repo/morphir.toml", "/repo/app/morphir.toml"], .. }, .. } else { fail(line) }
}///|
test "count the bytes of a record" {
assert_eq(@formatting.utf8_length("ok\n"), 3L)
assert_eq(@formatting.utf8_length("é✓😀\n"), 10L)
}pub struct LogRecord {
// private fields
}impl Show for LogTimestamp| Key | Value |
|---|---|
| schemaVersion | The number 1 |
| timestamp | The UTC time, for example "2026-10-27T14:23:07.024Z" |
| level | The level name, for example "Information" |
| category | The category, or null |
| messageTemplate | The template as the caller wrote it |
| message | The message text |
| properties | An array of {"name": ..., "value": ...} objects in hole order, with each repeated name and the original JSON value |
| error | The Show text of the error, or null |
fn utf8_length(text : String) -> Int64Install
Download zipStructured logging with a bring-your-own logger trait for the Moon-Lander framework