mirror of
https://github.com/Nanaloveyuki/BitLogger.git
synced 2026-05-30 23:52:27 +00:00
🚚 Move bitlogger&bitlogger-async to src& src-async
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
///|
|
||||
/// BitLogger public API surface.
|
||||
@@ -0,0 +1,772 @@
|
||||
test "level filter works" {
|
||||
let logger = Logger::new(console_sink(), min_level=Level::Warn, target="test")
|
||||
inspect(logger.is_enabled(Level::Error), content="true")
|
||||
inspect(logger.is_enabled(Level::Info), content="false")
|
||||
}
|
||||
|
||||
test "context sink merges fields" {
|
||||
let logger = Logger::new(console_sink(), min_level=Level::Info, target="ctx")
|
||||
.with_context_fields([field("service", "bitlogger")])
|
||||
let merged = [field("service", "bitlogger"), field("mode", "test")]
|
||||
inspect(merged.length(), content="2")
|
||||
inspect(merged[0].key, content="service")
|
||||
inspect(merged[1].key, content="mode")
|
||||
logger.info("hello", fields=[field("mode", "test")])
|
||||
}
|
||||
|
||||
test "fields helper builds field arrays ergonomically" {
|
||||
let items = fields([("service", "bitlogger"), ("mode", "test")])
|
||||
inspect(items.length(), content="2")
|
||||
inspect(items[0].key, content="service")
|
||||
inspect(items[0].value, content="bitlogger")
|
||||
inspect(items[1].key, content="mode")
|
||||
inspect(items[1].value, content="test")
|
||||
}
|
||||
|
||||
test "fanout sink can write to plain and json outputs" {
|
||||
let logger = Logger::new(
|
||||
fanout_sink(console_sink(), json_console_sink()),
|
||||
min_level=Level::Info,
|
||||
target="fanout",
|
||||
)
|
||||
inspect(logger.is_enabled(Level::Info), content="true")
|
||||
logger.info("hello", fields=[field("kind", "dual")])
|
||||
}
|
||||
|
||||
test "child logger composes target path" {
|
||||
let logger = Logger::new(console_sink(), min_level=Level::Info, target="app")
|
||||
.child("worker")
|
||||
inspect(logger.target, content="app.worker")
|
||||
}
|
||||
|
||||
test "logger config parser reads core options" {
|
||||
let config = parse_logger_config_text(
|
||||
"{\"min_level\":\"debug\",\"target\":\"service\",\"timestamp\":true}",
|
||||
)
|
||||
inspect(config.min_level.label(), content="DEBUG")
|
||||
inspect(config.target, content="service")
|
||||
inspect(config.timestamp, content="true")
|
||||
}
|
||||
|
||||
test "logger config parser reads formatter and queue options" {
|
||||
let config = parse_logger_config_text(
|
||||
"{\"sink\":{\"kind\":\"text_console\",\"text_formatter\":{\"separator\":\" | \",\"show_timestamp\":false,\"template\":\"[{level}] {message}\",\"color_mode\":\"always\"}},\"queue\":{\"max_pending\":32,\"overflow\":\"DropOldest\"}}",
|
||||
)
|
||||
inspect(match config.sink.kind {
|
||||
SinkKind::TextConsole => "TextConsole"
|
||||
_ => "other"
|
||||
}, content="TextConsole")
|
||||
inspect(config.sink.text_formatter.separator, content=" | ")
|
||||
inspect(config.sink.text_formatter.show_timestamp, content="false")
|
||||
inspect(config.sink.text_formatter.template, content="[{level}] {message}")
|
||||
inspect(color_mode_label(config.sink.text_formatter.color_mode), content="always")
|
||||
match config.queue {
|
||||
Some(queue) => {
|
||||
inspect(queue.max_pending, content="32")
|
||||
inspect(match queue.overflow {
|
||||
QueueOverflowPolicy::DropNewest => "DropNewest"
|
||||
QueueOverflowPolicy::DropOldest => "DropOldest"
|
||||
}, content="DropOldest")
|
||||
}
|
||||
None => inspect(false, content="true")
|
||||
}
|
||||
}
|
||||
|
||||
test "logger config parser reads formatter style tags" {
|
||||
let config = parse_logger_config_text(
|
||||
"{\"sink\":{\"kind\":\"text_console\",\"text_formatter\":{\"color_mode\":\"always\",\"color_support\":\"basic\",\"style_markup\":\"builtin\",\"target_style_markup\":\"builtin\",\"fields_style_markup\":\"disabled\",\"style_tags\":{\"accent\":{\"fg\":\"#4cc9f0\",\"bold\":true},\"panel\":{\"bg\":\"#202020\",\"underline\":true}}}}}",
|
||||
)
|
||||
inspect(style_markup_mode_label(config.sink.text_formatter.style_markup), content="builtin")
|
||||
inspect(color_support_label(config.sink.text_formatter.color_support), content="basic")
|
||||
inspect(style_markup_mode_label(config.sink.text_formatter.target_style_markup), content="builtin")
|
||||
inspect(style_markup_mode_label(config.sink.text_formatter.fields_style_markup), content="disabled")
|
||||
inspect(config.sink.text_formatter.style_tags.length(), content="2")
|
||||
match config.sink.text_formatter.style_tags.get("accent") {
|
||||
Some(style) => {
|
||||
inspect(style.fg.unwrap(), content="#4cc9f0")
|
||||
inspect(style.bold, content="true")
|
||||
inspect(style.bg is None, content="true")
|
||||
}
|
||||
None => inspect(false, content="true")
|
||||
}
|
||||
match config.sink.text_formatter.style_tags.get("panel") {
|
||||
Some(style) => {
|
||||
inspect(style.bg.unwrap(), content="#202020")
|
||||
inspect(style.underline, content="true")
|
||||
}
|
||||
None => inspect(false, content="true")
|
||||
}
|
||||
}
|
||||
|
||||
test "logger config parser reads file rotation options" {
|
||||
let config = parse_logger_config_text(
|
||||
"{\"sink\":{\"kind\":\"file\",\"path\":\"bitlogger.log\",\"rotation\":{\"max_bytes\":128,\"max_backups\":3}}}",
|
||||
)
|
||||
inspect(match config.sink.kind {
|
||||
SinkKind::File => "File"
|
||||
_ => "other"
|
||||
}, content="File")
|
||||
inspect(config.sink.path, content="bitlogger.log")
|
||||
match config.sink.rotation {
|
||||
Some(rotation) => {
|
||||
inspect(rotation.max_bytes, content="128")
|
||||
inspect(rotation.max_backups, content="3")
|
||||
}
|
||||
None => inspect(false, content="true")
|
||||
}
|
||||
}
|
||||
|
||||
test "logger config stringify roundtrips stable fields" {
|
||||
let text = stringify_logger_config(
|
||||
LoggerConfig::new(
|
||||
min_level=Level::Warn,
|
||||
target="api",
|
||||
timestamp=true,
|
||||
sink=SinkConfig::new(
|
||||
kind=SinkKind::TextConsole,
|
||||
text_formatter=TextFormatterConfig::new(
|
||||
show_timestamp=false,
|
||||
show_level=true,
|
||||
show_target=true,
|
||||
show_fields=true,
|
||||
separator=" | ",
|
||||
field_separator=",",
|
||||
template="[{level}] {target} {message}",
|
||||
),
|
||||
),
|
||||
queue=Some(QueueConfig::new(8, overflow=QueueOverflowPolicy::DropNewest)),
|
||||
),
|
||||
)
|
||||
let config = parse_logger_config_text(text)
|
||||
inspect(config.min_level.label(), content="WARN")
|
||||
inspect(config.target, content="api")
|
||||
inspect(config.timestamp, content="true")
|
||||
inspect(config.sink.text_formatter.separator, content=" | ")
|
||||
inspect(config.sink.text_formatter.template, content="[{level}] {target} {message}")
|
||||
}
|
||||
|
||||
test "logger config stringify roundtrips formatter style tags" {
|
||||
let text = stringify_logger_config(
|
||||
LoggerConfig::new(
|
||||
sink=SinkConfig::new(
|
||||
kind=SinkKind::TextConsole,
|
||||
text_formatter=TextFormatterConfig::new(
|
||||
color_mode=ColorMode::Always,
|
||||
color_support=ColorSupport::Basic,
|
||||
style_markup=StyleMarkupMode::Builtin,
|
||||
target_style_markup=StyleMarkupMode::Builtin,
|
||||
fields_style_markup=StyleMarkupMode::Disabled,
|
||||
style_tags={
|
||||
"accent": text_style(fg=Some("#4cc9f0"), bold=true),
|
||||
"panel": text_style(bg=Some("#202020"), dim=true),
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
let config = parse_logger_config_text(text)
|
||||
inspect(color_mode_label(config.sink.text_formatter.color_mode), content="always")
|
||||
inspect(color_support_label(config.sink.text_formatter.color_support), content="basic")
|
||||
inspect(style_markup_mode_label(config.sink.text_formatter.style_markup), content="builtin")
|
||||
inspect(style_markup_mode_label(config.sink.text_formatter.target_style_markup), content="builtin")
|
||||
inspect(style_markup_mode_label(config.sink.text_formatter.fields_style_markup), content="disabled")
|
||||
inspect(config.sink.text_formatter.style_tags.length(), content="2")
|
||||
inspect(config.sink.text_formatter.style_tags.get("accent").unwrap().fg.unwrap(), content="#4cc9f0")
|
||||
inspect(config.sink.text_formatter.style_tags.get("accent").unwrap().bold, content="true")
|
||||
inspect(config.sink.text_formatter.style_tags.get("panel").unwrap().bg.unwrap(), content="#202020")
|
||||
inspect(config.sink.text_formatter.style_tags.get("panel").unwrap().dim, content="true")
|
||||
}
|
||||
|
||||
test "logger config stringify roundtrips file rotation fields" {
|
||||
let text = stringify_logger_config(
|
||||
LoggerConfig::new(
|
||||
sink=SinkConfig::new(
|
||||
kind=SinkKind::File,
|
||||
path="bitlogger.log",
|
||||
rotation=Some(file_rotation(256, max_backups=2)),
|
||||
),
|
||||
),
|
||||
)
|
||||
let config = parse_logger_config_text(text)
|
||||
inspect(config.sink.path, content="bitlogger.log")
|
||||
match config.sink.rotation {
|
||||
Some(rotation) => {
|
||||
inspect(rotation.max_bytes, content="256")
|
||||
inspect(rotation.max_backups, content="2")
|
||||
}
|
||||
None => inspect(false, content="true")
|
||||
}
|
||||
}
|
||||
|
||||
test "config subtype json helpers stringify stable shapes" {
|
||||
inspect(
|
||||
stringify_queue_config(
|
||||
QueueConfig::new(8, overflow=QueueOverflowPolicy::DropOldest),
|
||||
),
|
||||
content="{\"max_pending\":8,\"overflow\":\"DropOldest\"}",
|
||||
)
|
||||
inspect(
|
||||
stringify_text_formatter_config(
|
||||
TextFormatterConfig::new(
|
||||
show_timestamp=false,
|
||||
show_level=true,
|
||||
show_target=false,
|
||||
show_fields=true,
|
||||
separator=" | ",
|
||||
field_separator=",",
|
||||
template="[{level}] {message} :: {fields}",
|
||||
color_mode=ColorMode::Always,
|
||||
color_support=ColorSupport::Basic,
|
||||
style_markup=StyleMarkupMode::Builtin,
|
||||
target_style_markup=StyleMarkupMode::Builtin,
|
||||
fields_style_markup=StyleMarkupMode::Disabled,
|
||||
style_tags={
|
||||
"accent": text_style(fg=Some("#4cc9f0"), bold=true),
|
||||
},
|
||||
),
|
||||
),
|
||||
content="{\"show_timestamp\":false,\"show_level\":true,\"show_target\":false,\"show_fields\":true,\"separator\":\" | \",\"field_separator\":\",\",\"template\":\"[{level}] {message} :: {fields}\",\"color_mode\":\"always\",\"color_support\":\"basic\",\"style_markup\":\"builtin\",\"target_style_markup\":\"builtin\",\"fields_style_markup\":\"disabled\",\"style_tags\":{\"accent\":{\"bold\":true,\"dim\":false,\"italic\":false,\"underline\":false,\"fg\":\"#4cc9f0\"}}}",
|
||||
)
|
||||
inspect(
|
||||
stringify_sink_config(
|
||||
SinkConfig::new(
|
||||
kind=SinkKind::File,
|
||||
path="demo.log",
|
||||
append=false,
|
||||
auto_flush=false,
|
||||
rotation=Some(file_rotation(128, max_backups=2)),
|
||||
text_formatter=TextFormatterConfig::new(show_timestamp=false, color_mode=ColorMode::Auto),
|
||||
),
|
||||
),
|
||||
content="{\"kind\":\"file\",\"path\":\"demo.log\",\"append\":false,\"auto_flush\":false,\"text_formatter\":{\"show_timestamp\":false,\"show_level\":true,\"show_target\":true,\"show_fields\":true,\"separator\":\" \",\"field_separator\":\" \",\"template\":\"\",\"color_mode\":\"auto\",\"color_support\":\"truecolor\",\"style_markup\":\"full\",\"target_style_markup\":\"disabled\",\"fields_style_markup\":\"disabled\"},\"rotation\":{\"max_bytes\":128,\"max_backups\":2}}",
|
||||
)
|
||||
}
|
||||
|
||||
test "config basic color support downgrades hex colors" {
|
||||
let formatter = TextFormatterConfig::new(
|
||||
show_level=false,
|
||||
show_target=false,
|
||||
color_mode=ColorMode::Always,
|
||||
color_support=ColorSupport::Basic,
|
||||
)
|
||||
let rendered = format_text(
|
||||
Record::new(Level::Info, "<#ff0000>hot</> <bg:#010203>bg</>"),
|
||||
formatter=formatter.to_formatter(),
|
||||
)
|
||||
inspect(rendered, content="\u{001b}[31mhot\u{001b}[0m \u{001b}[100mbg\u{001b}[0m")
|
||||
}
|
||||
|
||||
test "config formatter style tags render in built logger" {
|
||||
let formatter = TextFormatterConfig::new(
|
||||
show_timestamp=false,
|
||||
show_target=false,
|
||||
color_mode=ColorMode::Always,
|
||||
style_tags={
|
||||
"accent": text_style(fg=Some("#4cc9f0"), bold=true),
|
||||
},
|
||||
)
|
||||
let rendered = format_text(
|
||||
Record::new(Level::Info, "<accent>tag</>"),
|
||||
formatter=formatter.to_formatter(),
|
||||
)
|
||||
inspect(rendered, content="[\u{001b}[32mINFO\u{001b}[0m] \u{001b}[38;2;76;201;240;1mtag\u{001b}[0m")
|
||||
}
|
||||
|
||||
test "config builtin style markup ignores custom tags" {
|
||||
let formatter = TextFormatterConfig::new(
|
||||
show_level=false,
|
||||
show_target=false,
|
||||
color_mode=ColorMode::Always,
|
||||
style_markup=StyleMarkupMode::Builtin,
|
||||
style_tags={
|
||||
"brand": text_style(fg=Some("#4cc9f0"), bold=true),
|
||||
},
|
||||
)
|
||||
let rendered = format_text(
|
||||
Record::new(Level::Info, "<brand>custom</> <red>builtin</>"),
|
||||
formatter=formatter.to_formatter(),
|
||||
)
|
||||
inspect(rendered, content="<brand>custom</> \u{001b}[31mbuiltin\u{001b}[0m")
|
||||
}
|
||||
|
||||
test "config disabled style markup keeps raw tags" {
|
||||
let formatter = TextFormatterConfig::new(
|
||||
show_level=false,
|
||||
show_target=false,
|
||||
color_mode=ColorMode::Always,
|
||||
style_markup=StyleMarkupMode::Disabled,
|
||||
style_tags={
|
||||
"accent": text_style(fg=Some("#4cc9f0"), bold=true),
|
||||
},
|
||||
)
|
||||
let rendered = format_text(
|
||||
Record::new(Level::Info, "<accent>raw</> <red>tag</>"),
|
||||
formatter=formatter.to_formatter(),
|
||||
)
|
||||
inspect(rendered, content="<accent>raw</> <red>tag</>")
|
||||
}
|
||||
|
||||
test "config target and fields markup modes render separately" {
|
||||
let formatter = TextFormatterConfig::new(
|
||||
show_level=false,
|
||||
color_mode=ColorMode::Always,
|
||||
style_markup=StyleMarkupMode::Disabled,
|
||||
target_style_markup=StyleMarkupMode::Builtin,
|
||||
fields_style_markup=StyleMarkupMode::Builtin,
|
||||
)
|
||||
let rendered = format_text(
|
||||
Record::new(
|
||||
Level::Info,
|
||||
"<danger>message stays raw</>",
|
||||
target="<danger>svc</>",
|
||||
fields=[field("status", "<success>ok</>")],
|
||||
),
|
||||
formatter=formatter.to_formatter(),
|
||||
)
|
||||
inspect(
|
||||
rendered,
|
||||
content="[\u{001b}[34m\u{001b}[31;1msvc\u{001b}[0m\u{001b}[0m] <danger>message stays raw</> \u{001b}[35mstatus=\u{001b}[32;1mok\u{001b}[0m\u{001b}[0m",
|
||||
)
|
||||
}
|
||||
|
||||
test "build logger from config supports queued text console" {
|
||||
let logger = build_logger(
|
||||
LoggerConfig::new(
|
||||
min_level=Level::Debug,
|
||||
target="config.runtime",
|
||||
timestamp=true,
|
||||
sink=SinkConfig::new(
|
||||
kind=SinkKind::TextConsole,
|
||||
text_formatter=TextFormatterConfig::new(show_timestamp=false, separator=" | "),
|
||||
),
|
||||
queue=Some(QueueConfig::new(2, overflow=QueueOverflowPolicy::DropOldest)),
|
||||
),
|
||||
)
|
||||
logger.info("one")
|
||||
logger.info("two")
|
||||
logger.info("three")
|
||||
inspect(logger.pending_count(), content="2")
|
||||
inspect(logger.flush(), content="2")
|
||||
}
|
||||
|
||||
test "configured logger drain supports partial queue draining" {
|
||||
let logger = build_logger(
|
||||
LoggerConfig::new(
|
||||
min_level=Level::Info,
|
||||
target="config.partial",
|
||||
sink=SinkConfig::new(kind=SinkKind::Console),
|
||||
queue=Some(QueueConfig::new(4, overflow=QueueOverflowPolicy::DropNewest)),
|
||||
),
|
||||
)
|
||||
logger.info("one")
|
||||
logger.info("two")
|
||||
logger.info("three")
|
||||
inspect(logger.pending_count(), content="3")
|
||||
inspect(logger.drain(max_items=2), content="2")
|
||||
inspect(logger.pending_count(), content="1")
|
||||
inspect(logger.flush(), content="1")
|
||||
}
|
||||
|
||||
test "configured logger reports dropped_count for bounded queue" {
|
||||
let logger = build_logger(
|
||||
LoggerConfig::new(
|
||||
min_level=Level::Info,
|
||||
target="config.drop",
|
||||
sink=SinkConfig::new(kind=SinkKind::TextConsole),
|
||||
queue=Some(QueueConfig::new(2, overflow=QueueOverflowPolicy::DropOldest)),
|
||||
),
|
||||
)
|
||||
logger.info("one")
|
||||
logger.info("two")
|
||||
logger.info("three")
|
||||
logger.info("four")
|
||||
inspect(logger.pending_count(), content="2")
|
||||
inspect(logger.dropped_count(), content="2")
|
||||
inspect(logger.flush(), content="2")
|
||||
}
|
||||
|
||||
test "configured logger exposes file sink observability helpers" {
|
||||
let logger = build_logger(
|
||||
LoggerConfig::new(
|
||||
sink=SinkConfig::new(
|
||||
kind=SinkKind::File,
|
||||
path="config-file.log",
|
||||
auto_flush=false,
|
||||
rotation=Some(file_rotation(64, max_backups=3)),
|
||||
),
|
||||
),
|
||||
)
|
||||
let state = logger.file_state()
|
||||
let runtime_state = logger.file_runtime_state()
|
||||
inspect(logger.file_available() == native_files_supported(), content="true")
|
||||
inspect(logger.file_path(), content="config-file.log")
|
||||
inspect(state.path, content="config-file.log")
|
||||
inspect(state.available == logger.file_available(), content="true")
|
||||
inspect(state.append == logger.file_append_mode(), content="true")
|
||||
inspect(state.auto_flush == logger.file_auto_flush(), content="true")
|
||||
inspect(logger.file_append_mode(), content="true")
|
||||
inspect(logger.file_auto_flush(), content="false")
|
||||
match runtime_state {
|
||||
Some(snapshot) => {
|
||||
inspect(snapshot.queued, content="false")
|
||||
inspect(snapshot.pending_count, content="0")
|
||||
inspect(snapshot.dropped_count, content="0")
|
||||
inspect(snapshot.file.path, content="config-file.log")
|
||||
}
|
||||
None => inspect(false, content="true")
|
||||
}
|
||||
inspect(logger.file_rotation_enabled(), content="true")
|
||||
match logger.file_rotation_config() {
|
||||
Some(rotation) => {
|
||||
inspect(rotation.max_bytes, content="64")
|
||||
inspect(rotation.max_backups, content="3")
|
||||
}
|
||||
None => inspect(false, content="true")
|
||||
}
|
||||
inspect(logger.file_open_failures(), content=if logger.file_available() { "0" } else { "1" })
|
||||
inspect(logger.file_write_failures(), content="0")
|
||||
inspect(logger.file_flush_failures(), content="0")
|
||||
inspect(logger.file_rotation_failures(), content="0")
|
||||
ignore(logger.close())
|
||||
}
|
||||
|
||||
test "file state json helpers stringify stable snapshots" {
|
||||
let plain = file_sink_state_to_json(
|
||||
FileSinkState::new(
|
||||
"demo.log",
|
||||
available=true,
|
||||
append=false,
|
||||
auto_flush=true,
|
||||
rotation=Some(file_rotation(64, max_backups=2)),
|
||||
open_failures=1,
|
||||
write_failures=2,
|
||||
flush_failures=3,
|
||||
rotation_failures=4,
|
||||
),
|
||||
)
|
||||
inspect(
|
||||
@json_parser.stringify(plain),
|
||||
content="{\"path\":\"demo.log\",\"available\":true,\"append\":false,\"auto_flush\":true,\"open_failures\":1,\"write_failures\":2,\"flush_failures\":3,\"rotation_failures\":4,\"rotation\":{\"max_bytes\":64,\"max_backups\":2}}",
|
||||
)
|
||||
inspect(
|
||||
stringify_file_sink_state(
|
||||
FileSinkState::new(
|
||||
"plain.log",
|
||||
available=false,
|
||||
append=true,
|
||||
auto_flush=false,
|
||||
rotation=None,
|
||||
open_failures=0,
|
||||
write_failures=0,
|
||||
flush_failures=0,
|
||||
rotation_failures=0,
|
||||
),
|
||||
),
|
||||
content="{\"path\":\"plain.log\",\"available\":false,\"append\":true,\"auto_flush\":false,\"open_failures\":0,\"write_failures\":0,\"flush_failures\":0,\"rotation_failures\":0,\"rotation\":null}",
|
||||
)
|
||||
}
|
||||
|
||||
test "runtime file state json helpers stringify queue snapshots" {
|
||||
let json = stringify_runtime_file_state(
|
||||
RuntimeFileState::new(
|
||||
FileSinkState::new(
|
||||
"queue.log",
|
||||
available=true,
|
||||
append=true,
|
||||
auto_flush=false,
|
||||
rotation=None,
|
||||
open_failures=0,
|
||||
write_failures=1,
|
||||
flush_failures=2,
|
||||
rotation_failures=3,
|
||||
),
|
||||
queued=true,
|
||||
pending_count=7,
|
||||
dropped_count=5,
|
||||
),
|
||||
)
|
||||
inspect(
|
||||
json,
|
||||
content="{\"file\":{\"path\":\"queue.log\",\"available\":true,\"append\":true,\"auto_flush\":false,\"open_failures\":0,\"write_failures\":1,\"flush_failures\":2,\"rotation_failures\":3,\"rotation\":null},\"queued\":true,\"pending_count\":7,\"dropped_count\":5}",
|
||||
)
|
||||
}
|
||||
|
||||
test "file sink policy json helpers stringify stable policies" {
|
||||
inspect(
|
||||
@json_parser.stringify(
|
||||
file_sink_policy_to_json(
|
||||
FileSinkPolicy::new(
|
||||
append=false,
|
||||
auto_flush=true,
|
||||
rotation=Some(file_rotation(96, max_backups=3)),
|
||||
),
|
||||
),
|
||||
),
|
||||
content="{\"append\":false,\"auto_flush\":true,\"rotation\":{\"max_bytes\":96,\"max_backups\":3}}",
|
||||
)
|
||||
inspect(
|
||||
stringify_file_sink_policy(
|
||||
FileSinkPolicy::new(append=true, auto_flush=false, rotation=None),
|
||||
),
|
||||
content="{\"append\":true,\"auto_flush\":false,\"rotation\":null}",
|
||||
)
|
||||
}
|
||||
|
||||
test "configured logger reports disabled rotation when file sink has none" {
|
||||
let logger = build_logger(
|
||||
LoggerConfig::new(
|
||||
sink=SinkConfig::new(kind=SinkKind::File, path="config-no-rotation.log"),
|
||||
),
|
||||
)
|
||||
inspect(logger.file_rotation_enabled(), content="false")
|
||||
inspect(logger.file_rotation_config() is None, content="true")
|
||||
match logger.file_runtime_state() {
|
||||
Some(snapshot) => inspect(snapshot.queued, content="false")
|
||||
None => inspect(false, content="true")
|
||||
}
|
||||
ignore(logger.close())
|
||||
}
|
||||
|
||||
test "configured non-file logger has no file runtime state" {
|
||||
let logger = build_logger(
|
||||
LoggerConfig::new(sink=SinkConfig::new(kind=SinkKind::Console)),
|
||||
)
|
||||
inspect(logger.file_runtime_state() is None, content="true")
|
||||
}
|
||||
|
||||
test "configured logger file setters update file sink policy state" {
|
||||
let logger = build_logger(
|
||||
LoggerConfig::new(
|
||||
sink=SinkConfig::new(kind=SinkKind::File, path="config-setters.log"),
|
||||
queue=Some(QueueConfig::new(2, overflow=QueueOverflowPolicy::DropNewest)),
|
||||
),
|
||||
)
|
||||
let default_policy = logger.file_default_policy()
|
||||
inspect(logger.file_append_mode(), content="true")
|
||||
inspect(logger.file_auto_flush(), content="true")
|
||||
inspect(logger.file_rotation_enabled(), content="false")
|
||||
inspect(default_policy.append, content="true")
|
||||
inspect(default_policy.auto_flush, content="true")
|
||||
inspect(default_policy.rotation is None, content="true")
|
||||
inspect(logger.file_policy_matches_default(), content="true")
|
||||
inspect(logger.file_set_append_mode(false), content="true")
|
||||
inspect(logger.file_append_mode(), content="false")
|
||||
inspect(logger.file_set_auto_flush(false), content="true")
|
||||
inspect(logger.file_auto_flush(), content="false")
|
||||
inspect(logger.file_set_rotation(Some(file_rotation(48, max_backups=4))), content="true")
|
||||
inspect(logger.file_rotation_enabled(), content="true")
|
||||
match logger.file_rotation_config() {
|
||||
Some(rotation) => {
|
||||
inspect(rotation.max_bytes, content="48")
|
||||
inspect(rotation.max_backups, content="4")
|
||||
}
|
||||
None => inspect(false, content="true")
|
||||
}
|
||||
inspect(logger.file_clear_rotation(), content="true")
|
||||
inspect(logger.file_rotation_enabled(), content="false")
|
||||
inspect(logger.file_rotation_config() is None, content="true")
|
||||
inspect(logger.file_reopen(), content=if logger.file_available() { "true" } else { "false" })
|
||||
inspect(logger.file_append_mode(), content="false")
|
||||
let state = logger.file_state()
|
||||
let policy = logger.file_policy()
|
||||
inspect(state.append, content="false")
|
||||
inspect(state.auto_flush, content="false")
|
||||
inspect(state.rotation is None, content="true")
|
||||
inspect(policy.append, content="false")
|
||||
inspect(policy.auto_flush, content="false")
|
||||
inspect(policy.rotation is None, content="true")
|
||||
inspect(logger.file_policy_matches_default(), content="false")
|
||||
inspect(logger.file_reset_policy(), content="true")
|
||||
inspect(logger.file_append_mode(), content="true")
|
||||
inspect(logger.file_auto_flush(), content="true")
|
||||
inspect(logger.file_rotation_config() is None, content="true")
|
||||
inspect(logger.file_policy_matches_default(), content="true")
|
||||
ignore(logger.close())
|
||||
}
|
||||
|
||||
test "configured logger reset policy restores configured file defaults" {
|
||||
let logger = build_logger(
|
||||
LoggerConfig::new(
|
||||
sink=SinkConfig::new(
|
||||
kind=SinkKind::File,
|
||||
path="config-reset-policy.log",
|
||||
append=false,
|
||||
auto_flush=false,
|
||||
rotation=Some(file_rotation(36, max_backups=2)),
|
||||
),
|
||||
),
|
||||
)
|
||||
let default_policy = logger.file_default_policy()
|
||||
inspect(default_policy.append, content="false")
|
||||
inspect(default_policy.auto_flush, content="false")
|
||||
inspect(logger.file_policy_matches_default(), content="true")
|
||||
inspect(logger.file_set_append_mode(true), content="true")
|
||||
inspect(logger.file_set_auto_flush(true), content="true")
|
||||
inspect(logger.file_clear_rotation(), content="true")
|
||||
inspect(logger.file_reset_policy(), content="true")
|
||||
inspect(logger.file_append_mode(), content="false")
|
||||
inspect(logger.file_auto_flush(), content="false")
|
||||
inspect(logger.file_policy_matches_default(), content="true")
|
||||
match logger.file_rotation_config() {
|
||||
Some(rotation) => {
|
||||
inspect(rotation.max_bytes, content="36")
|
||||
inspect(rotation.max_backups, content="2")
|
||||
}
|
||||
None => inspect(false, content="true")
|
||||
}
|
||||
ignore(logger.close())
|
||||
}
|
||||
|
||||
test "configured logger set policy applies bundled runtime file policy" {
|
||||
let logger = build_logger(
|
||||
LoggerConfig::new(
|
||||
sink=SinkConfig::new(kind=SinkKind::File, path="config-set-policy.log"),
|
||||
),
|
||||
)
|
||||
let default_policy = logger.file_default_policy()
|
||||
inspect(
|
||||
logger.file_set_policy(
|
||||
FileSinkPolicy::new(
|
||||
append=false,
|
||||
auto_flush=false,
|
||||
rotation=Some(file_rotation(22, max_backups=3)),
|
||||
),
|
||||
),
|
||||
content="true",
|
||||
)
|
||||
let policy = logger.file_policy()
|
||||
inspect(policy.append, content="false")
|
||||
inspect(policy.auto_flush, content="false")
|
||||
match policy.rotation {
|
||||
Some(rotation) => {
|
||||
inspect(rotation.max_bytes, content="22")
|
||||
inspect(rotation.max_backups, content="3")
|
||||
}
|
||||
None => inspect(false, content="true")
|
||||
}
|
||||
inspect(default_policy.append, content="true")
|
||||
inspect(default_policy.auto_flush, content="true")
|
||||
inspect(default_policy.rotation is None, content="true")
|
||||
inspect(logger.file_policy_matches_default(), content="false")
|
||||
ignore(logger.close())
|
||||
}
|
||||
|
||||
test "configured non-file logger cannot reset file policy" {
|
||||
let logger = build_logger(LoggerConfig::new(sink=SinkConfig::new(kind=SinkKind::Console)))
|
||||
inspect(logger.file_reset_policy(), content="false")
|
||||
inspect(logger.file_policy().append, content="false")
|
||||
inspect(logger.file_default_policy().append, content="false")
|
||||
inspect(logger.file_set_policy(FileSinkPolicy::new()), content="false")
|
||||
inspect(logger.file_policy_matches_default(), content="false")
|
||||
}
|
||||
|
||||
test "configured logger can reopen built file sink" {
|
||||
let logger = build_logger(
|
||||
LoggerConfig::new(
|
||||
sink=SinkConfig::new(kind=SinkKind::File, path="config-reopen.log"),
|
||||
),
|
||||
)
|
||||
if logger.file_available() {
|
||||
inspect(logger.close(), content="true")
|
||||
inspect(logger.file_reopen_append(), content="true")
|
||||
inspect(logger.file_available(), content="true")
|
||||
inspect(logger.file_append_mode(), content="true")
|
||||
inspect(logger.file_open_failures(), content="0")
|
||||
logger.info("reopened from config")
|
||||
inspect(logger.file_write_failures(), content="0")
|
||||
inspect(logger.file_reopen_truncate(), content="true")
|
||||
inspect(logger.file_append_mode(), content="false")
|
||||
inspect(logger.file_reopen(), content="true")
|
||||
inspect(logger.file_append_mode(), content="false")
|
||||
inspect(logger.file_reopen_with_current_policy(), content="true")
|
||||
inspect(logger.file_append_mode(), content="false")
|
||||
inspect(logger.file_reopen_append(), content="true")
|
||||
inspect(logger.file_append_mode(), content="true")
|
||||
inspect(logger.file_reset_failure_counters(), content="true")
|
||||
inspect(logger.file_open_failures(), content="0")
|
||||
inspect(logger.file_write_failures(), content="0")
|
||||
inspect(logger.file_flush_failures(), content="0")
|
||||
inspect(logger.file_rotation_failures(), content="0")
|
||||
inspect(logger.close(), content="true")
|
||||
} else {
|
||||
inspect(logger.file_append_mode(), content="true")
|
||||
inspect(logger.file_open_failures(), content="1")
|
||||
logger.info("dropped")
|
||||
inspect(logger.file_write_failures(), content="1")
|
||||
inspect(logger.file_reopen_append(), content="false")
|
||||
inspect(logger.file_open_failures(), content="2")
|
||||
inspect(logger.file_reopen_truncate(), content="false")
|
||||
inspect(logger.file_append_mode(), content="false")
|
||||
inspect(logger.file_reopen_with_current_policy(), content="false")
|
||||
inspect(logger.file_open_failures(), content="4")
|
||||
inspect(logger.file_reopen_append(), content="false")
|
||||
inspect(logger.file_append_mode(), content="true")
|
||||
inspect(logger.file_open_failures(), content="5")
|
||||
inspect(logger.file_reset_failure_counters(), content="true")
|
||||
inspect(logger.file_open_failures(), content="0")
|
||||
inspect(logger.file_write_failures(), content="0")
|
||||
inspect(logger.file_flush_failures(), content="0")
|
||||
inspect(logger.file_rotation_failures(), content="0")
|
||||
}
|
||||
}
|
||||
|
||||
test "configured non-file logger cannot reset file failure counters" {
|
||||
let logger = build_logger(LoggerConfig::new(sink=SinkConfig::new(kind=SinkKind::Console)))
|
||||
inspect(logger.file_reset_failure_counters(), content="false")
|
||||
}
|
||||
|
||||
test "configured logger exposes file flush and close helpers" {
|
||||
let logger = build_logger(
|
||||
LoggerConfig::new(
|
||||
sink=SinkConfig::new(kind=SinkKind::File, path="config-control.log"),
|
||||
),
|
||||
)
|
||||
if logger.file_available() {
|
||||
logger.info("before flush")
|
||||
inspect(logger.file_flush(), content="true")
|
||||
inspect(logger.file_close(), content="true")
|
||||
inspect(logger.file_available(), content="false")
|
||||
inspect(logger.file_flush(), content="false")
|
||||
} else {
|
||||
inspect(logger.file_flush(), content="false")
|
||||
inspect(logger.file_close(), content="false")
|
||||
}
|
||||
}
|
||||
|
||||
test "configured queued file logger flushes queue through file helper" {
|
||||
let logger = build_logger(
|
||||
LoggerConfig::new(
|
||||
sink=SinkConfig::new(kind=SinkKind::File, path="config-queued-file.log"),
|
||||
queue=Some(QueueConfig::new(4, overflow=QueueOverflowPolicy::DropNewest)),
|
||||
),
|
||||
)
|
||||
logger.info("one")
|
||||
logger.info("two")
|
||||
match logger.file_runtime_state() {
|
||||
Some(snapshot) => {
|
||||
inspect(snapshot.queued, content="true")
|
||||
inspect(snapshot.pending_count, content="2")
|
||||
inspect(snapshot.dropped_count, content="0")
|
||||
}
|
||||
None => inspect(false, content="true")
|
||||
}
|
||||
if logger.file_available() {
|
||||
inspect(logger.pending_count(), content="2")
|
||||
inspect(logger.file_flush(), content="true")
|
||||
inspect(logger.pending_count(), content="0")
|
||||
match logger.file_runtime_state() {
|
||||
Some(snapshot) => inspect(snapshot.pending_count, content="0")
|
||||
None => inspect(false, content="true")
|
||||
}
|
||||
inspect(logger.file_close(), content="true")
|
||||
} else {
|
||||
inspect(logger.pending_count(), content="2")
|
||||
inspect(logger.file_flush(), content="false")
|
||||
inspect(logger.pending_count(), content="0")
|
||||
match logger.file_runtime_state() {
|
||||
Some(snapshot) => inspect(snapshot.pending_count, content="0")
|
||||
None => inspect(false, content="true")
|
||||
}
|
||||
inspect(logger.file_close(), content="false")
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,56 @@
|
||||
# BitLogger
|
||||
|
||||
BitLogger is a minimal structured logger for MoonBit.
|
||||
|
||||
BitLogger 是一个使用 MoonBit 编写的结构化日志库.
|
||||
|
||||
## Features / 特性
|
||||
|
||||
- structured logging with levels, targets, and key-value fields
|
||||
- 支持 level、target 与结构化字段的基础日志能力
|
||||
- composable sinks, filters, patches, and queue wrappers
|
||||
- 支持 sink、filter、patch、queue 包装等组合能力
|
||||
- configurable text formatting with template and style tags
|
||||
- 支持 template、style tag 与彩色输出的文本格式化能力
|
||||
- config-driven logger assembly and JSON export / parse helpers
|
||||
- 支持配置驱动组装以及 JSON 解析 / 导出能力
|
||||
- native file sink support with rotation and runtime observability
|
||||
- 支持 native file sink、基础 rotation 与运行时可观测性
|
||||
|
||||
## Example / 示例
|
||||
|
||||
```moonbit
|
||||
test {
|
||||
let logger = Logger::new(console_sink(), min_level=Level::Debug, target="demo")
|
||||
.with_timestamp()
|
||||
.with_context_fields([field("service", "bitlogger")])
|
||||
logger.info("starting", fields=[field("port", "8080")])
|
||||
}
|
||||
```
|
||||
|
||||
Project command note / 项目命令说明:
|
||||
|
||||
- use `moon check` / `moon test` for local project verification
|
||||
- 本地项目校验请使用 `moon check` / `moon test`
|
||||
- `.mbt.md` literate docs still use MoonBit's document-test conventions internally
|
||||
- `.mbt.md` 文档内部仍沿用 MoonBit 的文档测试约定
|
||||
|
||||
## Where To Go Next / 下一步
|
||||
|
||||
- examples / 示例:
|
||||
- `../examples/basic/`
|
||||
- `../examples/async_basic/`
|
||||
- package-level API docs / 单接口 API 文档:
|
||||
- `../docs/api/`
|
||||
- common entry points / 常用入口:
|
||||
- `Logger::new(...)`
|
||||
- `async_logger(...)`
|
||||
- `build_logger(...)`
|
||||
- `build_async_logger(...)`
|
||||
|
||||
## Notes / 说明
|
||||
|
||||
- This README is intentionally minimal and no longer acts as a full API catalog.
|
||||
- 当前 README 仅保留 package 定位、关键特性与最小示例,不再承担完整 API 手册职责。
|
||||
- Detailed API docs now live under `docs/api/` one interface per file.
|
||||
- 详细 API 已迁移到 `docs/api/`,按“一接口一文件”维护。
|
||||
+1197
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,117 @@
|
||||
fn string_to_c_bytes(str : String) -> Bytes {
|
||||
let res : Array[Byte] = []
|
||||
let len = str.length()
|
||||
let mut i = 0
|
||||
while i < len {
|
||||
let mut c = str.code_unit_at(i).to_int()
|
||||
if 0xD800 <= c && c <= 0xDBFF {
|
||||
c -= 0xD800
|
||||
i = i + 1
|
||||
let l = str.code_unit_at(i).to_int() - 0xDC00
|
||||
c = (c << 10) + l + 0x10000
|
||||
}
|
||||
if c < 0x80 {
|
||||
res.push(c.to_byte())
|
||||
} else if c < 0x800 {
|
||||
res.push((0xc0 + (c >> 6)).to_byte())
|
||||
res.push((0x80 + (c & 0x3f)).to_byte())
|
||||
} else if c < 0x10000 {
|
||||
res.push((0xe0 + (c >> 12)).to_byte())
|
||||
res.push((0x80 + ((c >> 6) & 0x3f)).to_byte())
|
||||
res.push((0x80 + (c & 0x3f)).to_byte())
|
||||
} else {
|
||||
res.push((0xf0 + (c >> 18)).to_byte())
|
||||
res.push((0x80 + ((c >> 12) & 0x3f)).to_byte())
|
||||
res.push((0x80 + ((c >> 6) & 0x3f)).to_byte())
|
||||
res.push((0x80 + (c & 0x3f)).to_byte())
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
res.push((0).to_byte())
|
||||
Bytes::from_array(res)
|
||||
}
|
||||
|
||||
#external
|
||||
type NativeFileHandle
|
||||
|
||||
#borrow(path, mode)
|
||||
extern "C" fn file_open_ffi(path : Bytes, mode : Bytes) -> NativeFileHandle = "fopen"
|
||||
|
||||
extern "C" fn file_is_null_ffi(handle : NativeFileHandle) -> Bool = "moonbitlang_async_pointer_is_null"
|
||||
|
||||
#borrow(buffer)
|
||||
extern "C" fn file_write_ffi(
|
||||
buffer : Bytes,
|
||||
size : Int,
|
||||
count : Int,
|
||||
handle : NativeFileHandle,
|
||||
) -> Int = "fwrite"
|
||||
|
||||
extern "C" fn file_flush_ffi(handle : NativeFileHandle) -> Int = "fflush"
|
||||
|
||||
extern "C" fn file_close_ffi(handle : NativeFileHandle) -> Int = "fclose"
|
||||
|
||||
extern "C" fn file_seek_ffi(handle : NativeFileHandle, offset : Int, origin : Int) -> Int = "fseek"
|
||||
|
||||
extern "C" fn file_tell_ffi(handle : NativeFileHandle) -> Int = "ftell"
|
||||
|
||||
#borrow(from_path, to_path)
|
||||
extern "C" fn file_rename_ffi(from_path : Bytes, to_path : Bytes) -> Int = "rename"
|
||||
|
||||
#borrow(path)
|
||||
extern "C" fn file_remove_ffi(path : Bytes) -> Int = "remove"
|
||||
|
||||
pub struct FileHandle {
|
||||
path : String
|
||||
raw : NativeFileHandle
|
||||
}
|
||||
|
||||
fn open_file_handle_internal(path : String, append : Bool) -> FileHandle? {
|
||||
let mode = if append { "ab" } else { "wb" }
|
||||
let raw = file_open_ffi(string_to_c_bytes(path), string_to_c_bytes(mode))
|
||||
if file_is_null_ffi(raw) {
|
||||
None
|
||||
} else {
|
||||
Some({ raw, path })
|
||||
}
|
||||
}
|
||||
|
||||
fn write_file_handle_internal(handle : FileHandle, content : String) -> Bool {
|
||||
let bytes = string_to_c_bytes(content)
|
||||
let written = file_write_ffi(bytes, 1, bytes.length() - 1, handle.raw)
|
||||
written == bytes.length() - 1
|
||||
}
|
||||
|
||||
fn flush_file_handle_internal(handle : FileHandle) -> Bool {
|
||||
file_flush_ffi(handle.raw) == 0
|
||||
}
|
||||
|
||||
fn close_file_handle_internal(handle : FileHandle) -> Bool {
|
||||
file_close_ffi(handle.raw) == 0
|
||||
}
|
||||
|
||||
fn file_size_internal(handle : FileHandle) -> Int {
|
||||
ignore(file_seek_ffi(handle.raw, 0, 2))
|
||||
let size = file_tell_ffi(handle.raw)
|
||||
if size < 0 {
|
||||
0
|
||||
} else {
|
||||
size
|
||||
}
|
||||
}
|
||||
|
||||
fn rename_file_internal(from_path : String, to_path : String) -> Bool {
|
||||
file_rename_ffi(string_to_c_bytes(from_path), string_to_c_bytes(to_path)) == 0
|
||||
}
|
||||
|
||||
fn remove_file_internal(path : String) -> Bool {
|
||||
file_remove_ffi(string_to_c_bytes(path)) == 0
|
||||
}
|
||||
|
||||
fn string_byte_length_internal(content : String) -> Int {
|
||||
string_to_c_bytes(content).length() - 1
|
||||
}
|
||||
|
||||
fn native_files_supported_internal() -> Bool {
|
||||
true
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
pub struct FileHandle {
|
||||
path : String
|
||||
}
|
||||
|
||||
fn open_file_handle_internal(path : String, append : Bool) -> FileHandle? {
|
||||
ignore(append)
|
||||
ignore(path)
|
||||
let _unused : FileHandle = { path: "" }
|
||||
ignore(_unused)
|
||||
None
|
||||
}
|
||||
|
||||
fn write_file_handle_internal(handle : FileHandle, content : String) -> Bool {
|
||||
ignore(handle)
|
||||
ignore(content)
|
||||
false
|
||||
}
|
||||
|
||||
fn flush_file_handle_internal(handle : FileHandle) -> Bool {
|
||||
ignore(handle)
|
||||
false
|
||||
}
|
||||
|
||||
fn close_file_handle_internal(handle : FileHandle) -> Bool {
|
||||
ignore(handle)
|
||||
false
|
||||
}
|
||||
|
||||
fn file_size_internal(handle : FileHandle) -> Int {
|
||||
ignore(handle)
|
||||
0
|
||||
}
|
||||
|
||||
fn rename_file_internal(from_path : String, to_path : String) -> Bool {
|
||||
ignore(from_path)
|
||||
ignore(to_path)
|
||||
false
|
||||
}
|
||||
|
||||
fn remove_file_internal(path : String) -> Bool {
|
||||
ignore(path)
|
||||
false
|
||||
}
|
||||
|
||||
fn string_byte_length_internal(content : String) -> Int {
|
||||
content.length()
|
||||
}
|
||||
|
||||
fn native_files_supported_internal() -> Bool {
|
||||
false
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
pub type RecordPredicate = (Record) -> Bool
|
||||
|
||||
pub fn level_at_least(min_level : Level) -> RecordPredicate {
|
||||
fn(rec) {
|
||||
rec.level.priority() >= min_level.priority()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn target_is(target : String) -> RecordPredicate {
|
||||
fn(rec) {
|
||||
rec.target == target
|
||||
}
|
||||
}
|
||||
|
||||
pub fn target_has_prefix(prefix : String) -> RecordPredicate {
|
||||
fn(rec) {
|
||||
rec.target.has_prefix(prefix)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn message_contains(fragment : String) -> RecordPredicate {
|
||||
fn(rec) {
|
||||
rec.message.contains(fragment)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn has_field(key : String) -> RecordPredicate {
|
||||
fn(rec) {
|
||||
for field in rec.fields {
|
||||
if field.key == key {
|
||||
return true
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn field_equals(key : String, value : String) -> RecordPredicate {
|
||||
fn(rec) {
|
||||
for field in rec.fields {
|
||||
if field.key == key && field.value == value {
|
||||
return true
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn not_(predicate : RecordPredicate) -> RecordPredicate {
|
||||
fn(rec) {
|
||||
!(predicate(rec))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn all_of(predicates : Array[RecordPredicate]) -> RecordPredicate {
|
||||
fn(rec) {
|
||||
for predicate in predicates {
|
||||
if !(predicate(rec)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
pub fn any_of(predicates : Array[RecordPredicate]) -> RecordPredicate {
|
||||
fn(rec) {
|
||||
for predicate in predicates {
|
||||
if predicate(rec) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,905 @@
|
||||
pub type RecordFormatter = (Record) -> String
|
||||
|
||||
pub(all) enum ColorMode {
|
||||
Never
|
||||
Auto
|
||||
Always
|
||||
}
|
||||
|
||||
pub(all) enum ColorSupport {
|
||||
Basic
|
||||
TrueColor
|
||||
}
|
||||
|
||||
pub(all) enum StyleMarkupMode {
|
||||
Disabled
|
||||
Builtin
|
||||
Full
|
||||
}
|
||||
|
||||
pub struct TextStyle {
|
||||
fg : String?
|
||||
bg : String?
|
||||
bold : Bool
|
||||
dim : Bool
|
||||
italic : Bool
|
||||
underline : Bool
|
||||
}
|
||||
|
||||
pub fn text_style(
|
||||
fg~ : String? = None,
|
||||
bg~ : String? = None,
|
||||
bold~ : Bool = false,
|
||||
dim~ : Bool = false,
|
||||
italic~ : Bool = false,
|
||||
underline~ : Bool = false,
|
||||
) -> TextStyle {
|
||||
{ fg, bg, bold, dim, italic, underline }
|
||||
}
|
||||
|
||||
pub struct StyleTagRegistry {
|
||||
entries : Map[String, TextStyle]
|
||||
}
|
||||
|
||||
fn normalize_style_tag_name(name : String) -> String {
|
||||
name.trim().to_lower().to_owned()
|
||||
}
|
||||
|
||||
pub fn style_tag_registry() -> StyleTagRegistry {
|
||||
{ entries: {} }
|
||||
}
|
||||
|
||||
fn merge_text_style(base : TextStyle, overlay : TextStyle) -> TextStyle {
|
||||
{
|
||||
fg: match overlay.fg {
|
||||
Some(_) => overlay.fg
|
||||
None => base.fg
|
||||
},
|
||||
bg: match overlay.bg {
|
||||
Some(_) => overlay.bg
|
||||
None => base.bg
|
||||
},
|
||||
bold: base.bold || overlay.bold,
|
||||
dim: base.dim || overlay.dim,
|
||||
italic: base.italic || overlay.italic,
|
||||
underline: base.underline || overlay.underline,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn StyleTagRegistry::set_tag(
|
||||
self : StyleTagRegistry,
|
||||
name : String,
|
||||
style~ : TextStyle = text_style(),
|
||||
fg~ : String? = None,
|
||||
bg~ : String? = None,
|
||||
bold~ : Bool = false,
|
||||
dim~ : Bool = false,
|
||||
italic~ : Bool = false,
|
||||
underline~ : Bool = false,
|
||||
) -> StyleTagRegistry {
|
||||
let overlay = text_style(fg=fg, bg=bg, bold=bold, dim=dim, italic=italic, underline=underline)
|
||||
self.entries[normalize_style_tag_name(name)] = merge_text_style(style, overlay)
|
||||
self
|
||||
}
|
||||
|
||||
pub fn StyleTagRegistry::get(self : StyleTagRegistry, name : String) -> TextStyle? {
|
||||
self.entries.get(normalize_style_tag_name(name))
|
||||
}
|
||||
|
||||
pub fn StyleTagRegistry::contains(self : StyleTagRegistry, name : String) -> Bool {
|
||||
self.entries.contains(normalize_style_tag_name(name))
|
||||
}
|
||||
|
||||
pub fn StyleTagRegistry::define_alias(
|
||||
self : StyleTagRegistry,
|
||||
name : String,
|
||||
target : String,
|
||||
) -> StyleTagRegistry {
|
||||
match self.get(target) {
|
||||
Some(style) => self.set_tag(name, style=style)
|
||||
None => match global_style_tag_registry().get(target) {
|
||||
Some(style) => self.set_tag(name, style=style)
|
||||
None => match builtin_text_style_for_tag(normalize_style_tag_name(target)) {
|
||||
Some(style) => self.set_tag(name, style=style)
|
||||
None => self
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn default_style_tag_registry() -> StyleTagRegistry {
|
||||
style_tag_registry()
|
||||
.set_tag("black", fg=Some("black"))
|
||||
.set_tag("red", fg=Some("red"))
|
||||
.set_tag("green", fg=Some("green"))
|
||||
.set_tag("yellow", fg=Some("yellow"))
|
||||
.set_tag("blue", fg=Some("blue"))
|
||||
.set_tag("magenta", fg=Some("magenta"))
|
||||
.set_tag("cyan", fg=Some("cyan"))
|
||||
.set_tag("white", fg=Some("white"))
|
||||
.set_tag("bright_black", fg=Some("bright_black"))
|
||||
.set_tag("bright_red", fg=Some("bright_red"))
|
||||
.set_tag("bright_green", fg=Some("bright_green"))
|
||||
.set_tag("bright_yellow", fg=Some("bright_yellow"))
|
||||
.set_tag("bright_blue", fg=Some("bright_blue"))
|
||||
.set_tag("bright_magenta", fg=Some("bright_magenta"))
|
||||
.set_tag("bright_cyan", fg=Some("bright_cyan"))
|
||||
.set_tag("bright_white", fg=Some("bright_white"))
|
||||
.set_tag("accent", fg=Some("bright_cyan"), bold=true)
|
||||
.set_tag("info", fg=Some("cyan"))
|
||||
.set_tag("success", fg=Some("green"), bold=true)
|
||||
.set_tag("warning", fg=Some("yellow"), bold=true)
|
||||
.set_tag("danger", fg=Some("red"), bold=true)
|
||||
.set_tag("muted", fg=Some("bright_black"), dim=true)
|
||||
.set_tag("b", bold=true)
|
||||
.set_tag("dim", dim=true)
|
||||
.set_tag("i", italic=true)
|
||||
.set_tag("u", underline=true)
|
||||
}
|
||||
|
||||
let global_style_tag_registry_ref : Ref[StyleTagRegistry] = Ref::new(style_tag_registry())
|
||||
|
||||
pub fn global_style_tag_registry() -> StyleTagRegistry {
|
||||
global_style_tag_registry_ref.val
|
||||
}
|
||||
|
||||
pub fn set_global_style_tag_registry(registry : StyleTagRegistry) -> Unit {
|
||||
global_style_tag_registry_ref.val = registry
|
||||
}
|
||||
|
||||
pub fn reset_global_style_tag_registry() -> Unit {
|
||||
global_style_tag_registry_ref.val = style_tag_registry()
|
||||
}
|
||||
|
||||
priv struct InlineStyle {
|
||||
fg_code : String?
|
||||
bg_code : String?
|
||||
fg_basic_name : String?
|
||||
bg_basic_name : String?
|
||||
bold : Bool
|
||||
dim : Bool
|
||||
italic : Bool
|
||||
underline : Bool
|
||||
}
|
||||
|
||||
priv struct StyledSegment {
|
||||
text : String
|
||||
style : InlineStyle
|
||||
}
|
||||
|
||||
priv struct StyleFrame {
|
||||
tag : String
|
||||
style : InlineStyle
|
||||
}
|
||||
|
||||
fn code_unit(ch : Char) -> Int {
|
||||
ch.to_int()
|
||||
}
|
||||
|
||||
fn code_unit16(ch : UInt16) -> Int {
|
||||
ch.to_int()
|
||||
}
|
||||
|
||||
pub struct TextFormatter {
|
||||
show_timestamp : Bool
|
||||
show_level : Bool
|
||||
show_target : Bool
|
||||
show_fields : Bool
|
||||
separator : String
|
||||
field_separator : String
|
||||
template : String
|
||||
color_mode : ColorMode
|
||||
color_support : ColorSupport
|
||||
style_markup : StyleMarkupMode
|
||||
target_style_markup : StyleMarkupMode
|
||||
fields_style_markup : StyleMarkupMode
|
||||
style_tags : StyleTagRegistry?
|
||||
}
|
||||
|
||||
pub fn text_formatter(
|
||||
show_timestamp~ : Bool = true,
|
||||
show_level~ : Bool = true,
|
||||
show_target~ : Bool = true,
|
||||
show_fields~ : Bool = true,
|
||||
separator~ : String = " ",
|
||||
field_separator~ : String = " ",
|
||||
template~ : String = "",
|
||||
color_mode~ : ColorMode = ColorMode::Never,
|
||||
color_support~ : ColorSupport = ColorSupport::TrueColor,
|
||||
style_markup~ : StyleMarkupMode = StyleMarkupMode::Full,
|
||||
target_style_markup~ : StyleMarkupMode = StyleMarkupMode::Disabled,
|
||||
fields_style_markup~ : StyleMarkupMode = StyleMarkupMode::Disabled,
|
||||
style_tags~ : StyleTagRegistry? = None,
|
||||
) -> TextFormatter {
|
||||
{
|
||||
show_timestamp,
|
||||
show_level,
|
||||
show_target,
|
||||
show_fields,
|
||||
separator,
|
||||
field_separator,
|
||||
template,
|
||||
color_mode,
|
||||
color_support,
|
||||
style_markup,
|
||||
target_style_markup,
|
||||
fields_style_markup,
|
||||
style_tags,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn color_support_label(support : ColorSupport) -> String {
|
||||
match support {
|
||||
ColorSupport::Basic => "basic"
|
||||
ColorSupport::TrueColor => "truecolor"
|
||||
}
|
||||
}
|
||||
|
||||
pub fn TextFormatter::with_color_support(self : TextFormatter, color_support : ColorSupport) -> TextFormatter {
|
||||
{ ..self, color_support }
|
||||
}
|
||||
|
||||
pub fn style_markup_mode_label(mode : StyleMarkupMode) -> String {
|
||||
match mode {
|
||||
StyleMarkupMode::Disabled => "disabled"
|
||||
StyleMarkupMode::Builtin => "builtin"
|
||||
StyleMarkupMode::Full => "full"
|
||||
}
|
||||
}
|
||||
|
||||
pub fn TextFormatter::with_style_markup(self : TextFormatter, style_markup : StyleMarkupMode) -> TextFormatter {
|
||||
{ ..self, style_markup }
|
||||
}
|
||||
|
||||
pub fn TextFormatter::without_style_markup(self : TextFormatter) -> TextFormatter {
|
||||
{ ..self, style_markup: StyleMarkupMode::Disabled }
|
||||
}
|
||||
|
||||
pub fn TextFormatter::with_target_style_markup(
|
||||
self : TextFormatter,
|
||||
style_markup : StyleMarkupMode,
|
||||
) -> TextFormatter {
|
||||
{ ..self, target_style_markup: style_markup }
|
||||
}
|
||||
|
||||
pub fn TextFormatter::with_fields_style_markup(
|
||||
self : TextFormatter,
|
||||
style_markup : StyleMarkupMode,
|
||||
) -> TextFormatter {
|
||||
{ ..self, fields_style_markup: style_markup }
|
||||
}
|
||||
|
||||
pub fn TextFormatter::with_style_tags(self : TextFormatter, style_tags : StyleTagRegistry) -> TextFormatter {
|
||||
{ ..self, style_tags: Some(style_tags) }
|
||||
}
|
||||
|
||||
pub fn color_mode_label(mode : ColorMode) -> String {
|
||||
match mode {
|
||||
ColorMode::Never => "never"
|
||||
ColorMode::Auto => "auto"
|
||||
ColorMode::Always => "always"
|
||||
}
|
||||
}
|
||||
|
||||
fn use_ansi_color(mode : ColorMode) -> Bool {
|
||||
match mode {
|
||||
ColorMode::Never => false
|
||||
ColorMode::Always => true
|
||||
ColorMode::Auto => match @env.get_env_var("NO_COLOR") {
|
||||
Some(_) => false
|
||||
None => true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ansi_wrap(text : String, code : String, enabled : Bool) -> String {
|
||||
if !enabled || text == "" {
|
||||
text
|
||||
} else {
|
||||
"\u{001b}[\{code}m\{text}\u{001b}[0m"
|
||||
}
|
||||
}
|
||||
|
||||
fn ansi_wrap_with_style(text : String, style : InlineStyle, enabled : Bool) -> String {
|
||||
if !enabled || text == "" {
|
||||
text
|
||||
} else {
|
||||
let codes : Array[String] = []
|
||||
match style.fg_code {
|
||||
Some(code) => codes.push(code)
|
||||
None => ()
|
||||
}
|
||||
match style.bg_code {
|
||||
Some(code) => codes.push(code)
|
||||
None => ()
|
||||
}
|
||||
if style.bold {
|
||||
codes.push("1")
|
||||
}
|
||||
if style.dim {
|
||||
codes.push("2")
|
||||
}
|
||||
if style.italic {
|
||||
codes.push("3")
|
||||
}
|
||||
if style.underline {
|
||||
codes.push("4")
|
||||
}
|
||||
if codes.length() == 0 {
|
||||
text
|
||||
} else {
|
||||
let joined = codes.join(";")
|
||||
"\u{001b}[\{joined}m\{text}\u{001b}[0m"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn default_inline_style() -> InlineStyle {
|
||||
{
|
||||
fg_code: None,
|
||||
bg_code: None,
|
||||
fg_basic_name: None,
|
||||
bg_basic_name: None,
|
||||
bold: false,
|
||||
dim: false,
|
||||
italic: false,
|
||||
underline: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn named_color_code(tag : String) -> String? {
|
||||
match tag {
|
||||
"black" => Some("30")
|
||||
"red" => Some("31")
|
||||
"green" => Some("32")
|
||||
"yellow" => Some("33")
|
||||
"blue" => Some("34")
|
||||
"magenta" => Some("35")
|
||||
"cyan" => Some("36")
|
||||
"white" => Some("37")
|
||||
"bright_black" => Some("90")
|
||||
"bright_red" => Some("91")
|
||||
"bright_green" => Some("92")
|
||||
"bright_yellow" => Some("93")
|
||||
"bright_blue" => Some("94")
|
||||
"bright_magenta" => Some("95")
|
||||
"bright_cyan" => Some("96")
|
||||
"bright_white" => Some("97")
|
||||
_ => None
|
||||
}
|
||||
}
|
||||
|
||||
fn named_bg_color_code(tag : String) -> String? {
|
||||
match tag {
|
||||
"black" => Some("40")
|
||||
"red" => Some("41")
|
||||
"green" => Some("42")
|
||||
"yellow" => Some("43")
|
||||
"blue" => Some("44")
|
||||
"magenta" => Some("45")
|
||||
"cyan" => Some("46")
|
||||
"white" => Some("47")
|
||||
"bright_black" => Some("100")
|
||||
"bright_red" => Some("101")
|
||||
"bright_green" => Some("102")
|
||||
"bright_yellow" => Some("103")
|
||||
"bright_blue" => Some("104")
|
||||
"bright_magenta" => Some("105")
|
||||
"bright_cyan" => Some("106")
|
||||
"bright_white" => Some("107")
|
||||
_ => None
|
||||
}
|
||||
}
|
||||
|
||||
fn basic_name_from_hex(value : String) -> String {
|
||||
let r = hex_pair_to_int(value.unsafe_get(1), value.unsafe_get(2))
|
||||
let g = hex_pair_to_int(value.unsafe_get(3), value.unsafe_get(4))
|
||||
let b = hex_pair_to_int(value.unsafe_get(5), value.unsafe_get(6))
|
||||
let max_value = if r >= g && r >= b { r } else if g >= b { g } else { b }
|
||||
if max_value < 48 {
|
||||
"bright_black"
|
||||
} else {
|
||||
let min_value = if r <= g && r <= b { r } else if g <= b { g } else { b }
|
||||
let spread = max_value - min_value
|
||||
if spread < 24 {
|
||||
if max_value > 170 {
|
||||
"white"
|
||||
} else if max_value > 96 {
|
||||
"bright_black"
|
||||
} else {
|
||||
"black"
|
||||
}
|
||||
} else if r >= g && r >= b {
|
||||
if g > 96 && b < 96 {
|
||||
"yellow"
|
||||
} else if b > 96 && g < 96 {
|
||||
"magenta"
|
||||
} else {
|
||||
"red"
|
||||
}
|
||||
} else if g >= r && g >= b {
|
||||
if r > 96 && b < 96 {
|
||||
"yellow"
|
||||
} else if b > 96 && r < 96 {
|
||||
"cyan"
|
||||
} else {
|
||||
"green"
|
||||
}
|
||||
} else {
|
||||
if r > 96 && g < 96 {
|
||||
"magenta"
|
||||
} else if g > 96 && r < 96 {
|
||||
"cyan"
|
||||
} else {
|
||||
"blue"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_inline_color_code(
|
||||
formatter : TextFormatter,
|
||||
basic_name : String?,
|
||||
truecolor_code : String,
|
||||
) -> String {
|
||||
match formatter.color_support {
|
||||
ColorSupport::TrueColor => truecolor_code
|
||||
ColorSupport::Basic => match basic_name {
|
||||
Some(name) => named_code_from_basic_name(name).unwrap_or(truecolor_code)
|
||||
None => truecolor_code
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn named_code_from_basic_name(name : String) -> String? {
|
||||
named_color_code(name)
|
||||
}
|
||||
|
||||
fn named_bg_code_from_basic_name(name : String) -> String? {
|
||||
named_bg_color_code(name)
|
||||
}
|
||||
|
||||
fn resolve_inline_bg_code(
|
||||
formatter : TextFormatter,
|
||||
basic_name : String?,
|
||||
truecolor_code : String,
|
||||
) -> String {
|
||||
match formatter.color_support {
|
||||
ColorSupport::TrueColor => truecolor_code
|
||||
ColorSupport::Basic => match basic_name {
|
||||
Some(name) => named_bg_code_from_basic_name(name).unwrap_or(truecolor_code)
|
||||
None => truecolor_code
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn is_hex_color(value : String) -> Bool {
|
||||
if value.length() != 7 {
|
||||
return false
|
||||
}
|
||||
if value.unsafe_get(0) != '#' {
|
||||
return false
|
||||
}
|
||||
for i = 1; i < value.length(); i = i + 1 {
|
||||
let ch = value.unsafe_get(i)
|
||||
let is_digit = ch >= '0' && ch <= '9'
|
||||
let is_lower = ch >= 'a' && ch <= 'f'
|
||||
let is_upper = ch >= 'A' && ch <= 'F'
|
||||
if !(is_digit || is_lower || is_upper) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn hex_to_int(ch : UInt16) -> Int {
|
||||
let code = code_unit16(ch)
|
||||
if code >= code_unit('0') && code <= code_unit('9') {
|
||||
code_unit16(ch) - code_unit('0')
|
||||
} else if code >= code_unit('a') && code <= code_unit('f') {
|
||||
code_unit16(ch) - code_unit('a') + 10
|
||||
} else {
|
||||
code_unit16(ch) - code_unit('A') + 10
|
||||
}
|
||||
}
|
||||
|
||||
fn hex_pair_to_int(high : UInt16, low : UInt16) -> Int {
|
||||
hex_to_int(high) * 16 + hex_to_int(low)
|
||||
}
|
||||
|
||||
fn rgb_fg_code(value : String) -> String {
|
||||
let r = hex_pair_to_int(value.unsafe_get(1), value.unsafe_get(2))
|
||||
let g = hex_pair_to_int(value.unsafe_get(3), value.unsafe_get(4))
|
||||
let b = hex_pair_to_int(value.unsafe_get(5), value.unsafe_get(6))
|
||||
"38;2;\{r};\{g};\{b}"
|
||||
}
|
||||
|
||||
fn rgb_bg_code(value : String) -> String {
|
||||
let r = hex_pair_to_int(value.unsafe_get(4), value.unsafe_get(5))
|
||||
let g = hex_pair_to_int(value.unsafe_get(6), value.unsafe_get(7))
|
||||
let b = hex_pair_to_int(value.unsafe_get(8), value.unsafe_get(9))
|
||||
"48;2;\{r};\{g};\{b}"
|
||||
}
|
||||
|
||||
fn rgb_bg_code_from_hex(value : String) -> String {
|
||||
let r = hex_pair_to_int(value.unsafe_get(1), value.unsafe_get(2))
|
||||
let g = hex_pair_to_int(value.unsafe_get(3), value.unsafe_get(4))
|
||||
let b = hex_pair_to_int(value.unsafe_get(5), value.unsafe_get(6))
|
||||
"48;2;\{r};\{g};\{b}"
|
||||
}
|
||||
|
||||
fn inline_style_from_text_style(
|
||||
base : InlineStyle,
|
||||
style : TextStyle,
|
||||
formatter : TextFormatter,
|
||||
) -> InlineStyle? {
|
||||
let fg = match style.fg {
|
||||
None => Some((base.fg_code, base.fg_basic_name))
|
||||
Some(value) => {
|
||||
let normalized = normalize_style_tag_name(value)
|
||||
match named_color_code(normalized) {
|
||||
Some(code) => Some((Some(code), Some(normalized)))
|
||||
None => if is_hex_color(value) {
|
||||
let basic_name = basic_name_from_hex(value)
|
||||
Some((
|
||||
Some(resolve_inline_color_code(formatter, Some(basic_name), rgb_fg_code(value))),
|
||||
Some(basic_name),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let bg = match style.bg {
|
||||
None => Some((base.bg_code, base.bg_basic_name))
|
||||
Some(value) => {
|
||||
let normalized = normalize_style_tag_name(value)
|
||||
match named_bg_color_code(normalized) {
|
||||
Some(code) => {
|
||||
let bg_name = if normalized.has_prefix("bright_") {
|
||||
normalized
|
||||
} else {
|
||||
normalized
|
||||
}
|
||||
Some((Some(code), Some(bg_name)))
|
||||
}
|
||||
None => if is_hex_color(value) {
|
||||
let basic_name = basic_name_from_hex(value)
|
||||
Some((
|
||||
Some(resolve_inline_bg_code(formatter, Some(basic_name), rgb_bg_code_from_hex(value))),
|
||||
Some(basic_name),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
match fg {
|
||||
Some((next_fg_code, next_fg_name)) => match bg {
|
||||
Some((next_bg_code, next_bg_name)) => Some({
|
||||
fg_code: next_fg_code,
|
||||
bg_code: next_bg_code,
|
||||
fg_basic_name: next_fg_name,
|
||||
bg_basic_name: next_bg_name,
|
||||
bold: base.bold || style.bold,
|
||||
dim: base.dim || style.dim,
|
||||
italic: base.italic || style.italic,
|
||||
underline: base.underline || style.underline,
|
||||
})
|
||||
None => None
|
||||
}
|
||||
None => None
|
||||
}
|
||||
}
|
||||
|
||||
fn builtin_text_style_for_tag(tag : String) -> TextStyle? {
|
||||
match normalize_style_tag_name(tag) {
|
||||
"black" => Some(text_style(fg=Some("black")))
|
||||
"red" => Some(text_style(fg=Some("red")))
|
||||
"green" => Some(text_style(fg=Some("green")))
|
||||
"yellow" => Some(text_style(fg=Some("yellow")))
|
||||
"blue" => Some(text_style(fg=Some("blue")))
|
||||
"magenta" => Some(text_style(fg=Some("magenta")))
|
||||
"cyan" => Some(text_style(fg=Some("cyan")))
|
||||
"white" => Some(text_style(fg=Some("white")))
|
||||
"bright_black" => Some(text_style(fg=Some("bright_black")))
|
||||
"bright_red" => Some(text_style(fg=Some("bright_red")))
|
||||
"bright_green" => Some(text_style(fg=Some("bright_green")))
|
||||
"bright_yellow" => Some(text_style(fg=Some("bright_yellow")))
|
||||
"bright_blue" => Some(text_style(fg=Some("bright_blue")))
|
||||
"bright_magenta" => Some(text_style(fg=Some("bright_magenta")))
|
||||
"bright_cyan" => Some(text_style(fg=Some("bright_cyan")))
|
||||
"bright_white" => Some(text_style(fg=Some("bright_white")))
|
||||
"accent" => Some(text_style(fg=Some("bright_cyan"), bold=true))
|
||||
"info" => Some(text_style(fg=Some("cyan")))
|
||||
"success" => Some(text_style(fg=Some("green"), bold=true))
|
||||
"warning" => Some(text_style(fg=Some("yellow"), bold=true))
|
||||
"danger" => Some(text_style(fg=Some("red"), bold=true))
|
||||
"muted" => Some(text_style(fg=Some("bright_black"), dim=true))
|
||||
"b" => Some(text_style(bold=true))
|
||||
"dim" => Some(text_style(dim=true))
|
||||
"i" => Some(text_style(italic=true))
|
||||
"u" => Some(text_style(underline=true))
|
||||
_ => None
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_registered_text_style(tag : String, formatter : TextFormatter) -> TextStyle? {
|
||||
let normalized = normalize_style_tag_name(tag)
|
||||
match formatter.style_markup {
|
||||
StyleMarkupMode::Builtin => return builtin_text_style_for_tag(normalized)
|
||||
_ => ()
|
||||
}
|
||||
match formatter.style_tags {
|
||||
Some(registry) => match registry.get(normalized) {
|
||||
Some(style) => Some(style)
|
||||
None => match global_style_tag_registry().get(normalized) {
|
||||
Some(style) => Some(style)
|
||||
None => builtin_text_style_for_tag(normalized)
|
||||
}
|
||||
}
|
||||
None => match global_style_tag_registry().get(normalized) {
|
||||
Some(style) => Some(style)
|
||||
None => builtin_text_style_for_tag(normalized)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_inline_tag(style : InlineStyle, tag : String, formatter : TextFormatter) -> InlineStyle? {
|
||||
let normalized = normalize_style_tag_name(tag)
|
||||
if is_hex_color(tag) {
|
||||
let basic_name = basic_name_from_hex(tag)
|
||||
Some({
|
||||
..style,
|
||||
fg_code: Some(resolve_inline_color_code(formatter, Some(basic_name), rgb_fg_code(tag))),
|
||||
fg_basic_name: Some(basic_name),
|
||||
})
|
||||
} else if normalized.length() == 10 && normalized.has_prefix("bg:") && is_hex_color(normalized[3:].to_owned()) {
|
||||
let basic_name = basic_name_from_hex(normalized[3:].to_owned())
|
||||
Some({
|
||||
..style,
|
||||
bg_code: Some(resolve_inline_bg_code(formatter, Some(basic_name), rgb_bg_code(normalized))),
|
||||
bg_basic_name: Some(basic_name),
|
||||
})
|
||||
} else {
|
||||
match resolve_registered_text_style(normalized, formatter) {
|
||||
Some(spec) => inline_style_from_text_style(style, spec, formatter)
|
||||
None => None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn push_plain_segment(
|
||||
segments : Array[StyledSegment],
|
||||
buffer : StringBuilder,
|
||||
style : InlineStyle,
|
||||
) -> Unit {
|
||||
let text = buffer.to_string()
|
||||
if text != "" {
|
||||
segments.push({ text, style })
|
||||
buffer.reset()
|
||||
}
|
||||
}
|
||||
|
||||
fn pop_named_style_frame(stack : Array[StyleFrame], tag : String) -> Bool {
|
||||
let normalized = normalize_style_tag_name(tag)
|
||||
if stack.length() <= 1 {
|
||||
return false
|
||||
}
|
||||
for i = stack.length() - 1; i > 0; i = i - 1 {
|
||||
if stack[i].tag == normalized {
|
||||
while stack.length() - 1 >= i {
|
||||
ignore(stack.pop())
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn has_named_style_frame(stack : Array[StyleFrame], tag : String) -> Bool {
|
||||
let normalized = normalize_style_tag_name(tag)
|
||||
if stack.length() <= 1 {
|
||||
return false
|
||||
}
|
||||
for i = stack.length() - 1; i > 0; i = i - 1 {
|
||||
if stack[i].tag == normalized {
|
||||
return true
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn parse_inline_markup(input : String, formatter : TextFormatter) -> Array[StyledSegment] {
|
||||
let segments : Array[StyledSegment] = []
|
||||
let buffer = StringBuilder::new()
|
||||
let stack : Array[StyleFrame] = [{ tag: "", style: default_inline_style() }]
|
||||
let chars = input.to_array()
|
||||
let current_style = fn() { stack[stack.length() - 1].style }
|
||||
let flush = fn() { push_plain_segment(segments, buffer, current_style()) }
|
||||
let append_raw = fn(start : Int, finish : Int) {
|
||||
for i = start; i < finish; i = i + 1 {
|
||||
buffer.write_char(chars.unsafe_get(i))
|
||||
}
|
||||
}
|
||||
let find_tag_end = fn(start : Int) -> Int {
|
||||
for i = start; i < chars.length(); i = i + 1 {
|
||||
if chars.unsafe_get(i) == '>' {
|
||||
return i
|
||||
}
|
||||
}
|
||||
-1
|
||||
}
|
||||
for i = 0; i < chars.length(); {
|
||||
if chars.unsafe_get(i) != '<' {
|
||||
buffer.write_char(chars.unsafe_get(i))
|
||||
continue i + 1
|
||||
}
|
||||
let end = find_tag_end(i + 1)
|
||||
if end == -1 {
|
||||
buffer.write_char(chars[i])
|
||||
continue i + 1
|
||||
}
|
||||
let tag = input[i + 1:end].to_owned()
|
||||
if tag == "/" {
|
||||
if stack.length() > 1 {
|
||||
flush()
|
||||
ignore(stack.pop())
|
||||
} else {
|
||||
append_raw(i, end + 1)
|
||||
}
|
||||
continue end + 1
|
||||
}
|
||||
if tag.has_prefix("/") {
|
||||
let close_tag = tag[1:].to_owned()
|
||||
if close_tag != "" && has_named_style_frame(stack, close_tag) {
|
||||
flush()
|
||||
ignore(pop_named_style_frame(stack, close_tag))
|
||||
} else {
|
||||
append_raw(i, end + 1)
|
||||
}
|
||||
continue end + 1
|
||||
}
|
||||
match apply_inline_tag(current_style(), tag, formatter) {
|
||||
Some(next_style) => {
|
||||
flush()
|
||||
stack.push({ tag: normalize_style_tag_name(tag), style: next_style })
|
||||
}
|
||||
None => append_raw(i, end + 1)
|
||||
}
|
||||
continue end + 1
|
||||
}
|
||||
flush()
|
||||
segments
|
||||
}
|
||||
|
||||
fn render_styled_text(text : String, formatter : TextFormatter, mode : StyleMarkupMode) -> String {
|
||||
match mode {
|
||||
StyleMarkupMode::Disabled => return text
|
||||
_ => ()
|
||||
}
|
||||
let enabled = use_ansi_color(formatter.color_mode)
|
||||
let scoped = { ..formatter, style_markup: mode }
|
||||
let segments = parse_inline_markup(text, scoped)
|
||||
let out = StringBuilder::new()
|
||||
for segment in segments {
|
||||
out.write_string(ansi_wrap_with_style(segment.text, segment.style, enabled))
|
||||
}
|
||||
out.to_string()
|
||||
}
|
||||
|
||||
fn render_inline_markup(message : String, formatter : TextFormatter) -> String {
|
||||
render_styled_text(message, formatter, formatter.style_markup)
|
||||
}
|
||||
|
||||
fn level_ansi_code(level : Level) -> String {
|
||||
match level {
|
||||
Level::Trace => "90"
|
||||
Level::Debug => "36"
|
||||
Level::Info => "32"
|
||||
Level::Warn => "33"
|
||||
Level::Error => "31;1"
|
||||
}
|
||||
}
|
||||
|
||||
fn fields_to_json(fields : Array[Field]) -> Json {
|
||||
let obj : Map[String, Json] = {}
|
||||
for item in fields {
|
||||
obj[item.key] = Json::string(item.value)
|
||||
}
|
||||
Json::object(obj)
|
||||
}
|
||||
|
||||
fn timestamp_text(rec : Record, formatter : TextFormatter) -> String {
|
||||
if formatter.show_timestamp && rec.timestamp_ms != 0UL {
|
||||
ansi_wrap(rec.timestamp_ms.to_string(), "90", use_ansi_color(formatter.color_mode))
|
||||
} else {
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
fn level_text(rec : Record, formatter : TextFormatter) -> String {
|
||||
if formatter.show_level {
|
||||
ansi_wrap(rec.level.label(), level_ansi_code(rec.level), use_ansi_color(formatter.color_mode))
|
||||
} else {
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
fn target_text(rec : Record, formatter : TextFormatter) -> String {
|
||||
if formatter.show_target && rec.target != "" {
|
||||
let rendered = render_styled_text(rec.target, formatter, formatter.target_style_markup)
|
||||
ansi_wrap(rendered, "34", use_ansi_color(formatter.color_mode))
|
||||
} else {
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
fn format_field_text(field : Field, formatter : TextFormatter) -> String {
|
||||
let value = render_styled_text(field.value, formatter, formatter.fields_style_markup)
|
||||
"\{field.key}=\{value}"
|
||||
}
|
||||
|
||||
fn fields_text(rec : Record, formatter : TextFormatter) -> String {
|
||||
if formatter.show_fields && rec.fields.length() != 0 {
|
||||
let content = rec.fields.map(fn(field) { format_field_text(field, formatter) }).join(formatter.field_separator)
|
||||
ansi_wrap(content, "35", use_ansi_color(formatter.color_mode))
|
||||
} else {
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
fn render_template(rec : Record, formatter : TextFormatter) -> String {
|
||||
formatter.template
|
||||
.replace_all(old="{timestamp}", new=timestamp_text(rec, formatter))
|
||||
.replace_all(old="{timestamp_ms}", new=timestamp_text(rec, formatter))
|
||||
.replace_all(old="{level}", new=level_text(rec, formatter))
|
||||
.replace_all(old="{target}", new=target_text(rec, formatter))
|
||||
.replace_all(old="{message}", new=render_inline_markup(rec.message, formatter))
|
||||
.replace_all(old="{fields}", new=fields_text(rec, formatter))
|
||||
.trim()
|
||||
.to_owned()
|
||||
}
|
||||
|
||||
pub fn format_text(rec : Record, formatter~ : TextFormatter = text_formatter()) -> String {
|
||||
if formatter.template != "" {
|
||||
return render_template(rec, formatter)
|
||||
}
|
||||
let parts : Array[String] = []
|
||||
if formatter.show_timestamp && rec.timestamp_ms != 0UL {
|
||||
parts.push("[\{timestamp_text(rec, formatter)}]")
|
||||
}
|
||||
if formatter.show_level {
|
||||
parts.push("[\{level_text(rec, formatter)}]")
|
||||
}
|
||||
if formatter.show_target && rec.target != "" {
|
||||
parts.push("[\{target_text(rec, formatter)}]")
|
||||
}
|
||||
parts.push(render_inline_markup(rec.message, formatter))
|
||||
let base = parts.join(formatter.separator)
|
||||
if !formatter.show_fields || rec.fields.length() == 0 {
|
||||
base
|
||||
} else {
|
||||
let details = fields_text(rec, formatter)
|
||||
"\{base}\{formatter.separator}\{details}"
|
||||
}
|
||||
}
|
||||
|
||||
pub fn format_json(rec : Record) -> String {
|
||||
let obj : Map[String, Json] = {
|
||||
"level": Json::string(rec.level.label()),
|
||||
"message": Json::string(rec.message),
|
||||
"fields": fields_to_json(rec.fields),
|
||||
}
|
||||
if rec.timestamp_ms != 0UL {
|
||||
obj["timestamp_ms"] = rec.timestamp_ms.to_json()
|
||||
}
|
||||
if rec.target == "" {
|
||||
Json::object(obj).stringify()
|
||||
} else {
|
||||
obj["target"] = Json::string(rec.target)
|
||||
Json::object(obj).stringify()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
let default_console_sink : ConsoleSink = console_sink()
|
||||
let default_min_level_ref : Ref[Level] = Ref::new(Level::Info)
|
||||
let default_target_ref : Ref[String] = Ref::new("")
|
||||
|
||||
pub fn set_default_min_level(level : Level) -> Unit {
|
||||
default_min_level_ref.val = level
|
||||
}
|
||||
|
||||
pub fn set_default_target(target : String) -> Unit {
|
||||
default_target_ref.val = target
|
||||
}
|
||||
|
||||
pub fn default_logger() -> Logger[ConsoleSink] {
|
||||
Logger::new(default_console_sink, min_level=default_min_level_ref.val, target=default_target_ref.val)
|
||||
}
|
||||
|
||||
pub fn log(level : Level, message : String, fields~ : Array[Field] = []) -> Unit {
|
||||
default_logger().log(level, message, fields=fields)
|
||||
}
|
||||
|
||||
pub fn trace(message : String, fields~ : Array[Field] = []) -> Unit {
|
||||
default_logger().trace(message, fields=fields)
|
||||
}
|
||||
|
||||
pub fn debug(message : String, fields~ : Array[Field] = []) -> Unit {
|
||||
default_logger().debug(message, fields=fields)
|
||||
}
|
||||
|
||||
pub fn info(message : String, fields~ : Array[Field] = []) -> Unit {
|
||||
default_logger().info(message, fields=fields)
|
||||
}
|
||||
|
||||
pub fn warn(message : String, fields~ : Array[Field] = []) -> Unit {
|
||||
default_logger().warn(message, fields=fields)
|
||||
}
|
||||
|
||||
pub fn error(message : String, fields~ : Array[Field] = []) -> Unit {
|
||||
default_logger().error(message, fields=fields)
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
pub(all) enum Level {
|
||||
Trace
|
||||
Debug
|
||||
Info
|
||||
Warn
|
||||
Error
|
||||
}
|
||||
|
||||
pub fn Level::priority(self : Level) -> Int {
|
||||
match self {
|
||||
Level::Trace => 10
|
||||
Level::Debug => 20
|
||||
Level::Info => 30
|
||||
Level::Warn => 40
|
||||
Level::Error => 50
|
||||
}
|
||||
}
|
||||
|
||||
pub fn Level::label(self : Level) -> String {
|
||||
match self {
|
||||
Level::Trace => "TRACE"
|
||||
Level::Debug => "DEBUG"
|
||||
Level::Info => "INFO"
|
||||
Level::Warn => "WARN"
|
||||
Level::Error => "ERROR"
|
||||
}
|
||||
}
|
||||
|
||||
pub fn Level::enabled(self : Level, min_level : Level) -> Bool {
|
||||
self.priority() >= min_level.priority()
|
||||
}
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
pub struct Logger[S] {
|
||||
min_level : Level
|
||||
sink : S
|
||||
target : String
|
||||
timestamp : Bool
|
||||
}
|
||||
|
||||
pub fn[S] Logger::new(sink : S, min_level~ : Level = Level::Info, target~ : String = "") -> Logger[S] {
|
||||
{ min_level, sink, target, timestamp: false }
|
||||
}
|
||||
|
||||
pub fn[S] Logger::with_target(self : Logger[S], target : String) -> Logger[S] {
|
||||
{ ..self, target }
|
||||
}
|
||||
|
||||
fn combine_targets(parent : String, child : String) -> String {
|
||||
if parent == "" {
|
||||
child
|
||||
} else if child == "" {
|
||||
parent
|
||||
} else {
|
||||
"\{parent}.\{child}"
|
||||
}
|
||||
}
|
||||
|
||||
pub fn[S] Logger::child(self : Logger[S], target : String) -> Logger[S] {
|
||||
{ ..self, target: combine_targets(self.target, target) }
|
||||
}
|
||||
|
||||
pub fn[S] Logger::with_context_fields(self : Logger[S], fields : Array[Field]) -> Logger[ContextSink[S]] {
|
||||
{
|
||||
min_level: self.min_level,
|
||||
sink: ContextSink::{ sink: self.sink, context_fields: fields },
|
||||
target: self.target,
|
||||
timestamp: self.timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn[S] Logger::bind(self : Logger[S], fields : Array[Field]) -> Logger[ContextSink[S]] {
|
||||
self.with_context_fields(fields)
|
||||
}
|
||||
|
||||
pub fn[S] Logger::with_filter(self : Logger[S], predicate : (Record) -> Bool) -> Logger[FilterSink[S]] {
|
||||
{
|
||||
min_level: self.min_level,
|
||||
sink: filter_sink(self.sink, predicate),
|
||||
target: self.target,
|
||||
timestamp: self.timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn[S] Logger::with_patch(self : Logger[S], patch : RecordPatch) -> Logger[PatchSink[S]] {
|
||||
{
|
||||
min_level: self.min_level,
|
||||
sink: patch_sink(self.sink, patch),
|
||||
target: self.target,
|
||||
timestamp: self.timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn[S] Logger::with_queue(
|
||||
self : Logger[S],
|
||||
max_pending~ : Int = 0,
|
||||
overflow~ : QueueOverflowPolicy = QueueOverflowPolicy::DropNewest,
|
||||
) -> Logger[QueuedSink[S]] {
|
||||
{
|
||||
min_level: self.min_level,
|
||||
sink: queued_sink(self.sink, max_pending=max_pending, overflow=overflow),
|
||||
target: self.target,
|
||||
timestamp: self.timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn[S] Logger::with_min_level(self : Logger[S], min_level : Level) -> Logger[S] {
|
||||
{ ..self, min_level }
|
||||
}
|
||||
|
||||
pub fn[S] Logger::with_timestamp(self : Logger[S], enabled~ : Bool = true) -> Logger[S] {
|
||||
{ ..self, timestamp: enabled }
|
||||
}
|
||||
|
||||
pub fn[S] Logger::is_enabled(self : Logger[S], level : Level) -> Bool {
|
||||
level.enabled(self.min_level)
|
||||
}
|
||||
|
||||
pub fn[S : Sink] Logger::log(
|
||||
self : Logger[S],
|
||||
level : Level,
|
||||
message : String,
|
||||
fields~ : Array[Field] = [],
|
||||
target? : String = "",
|
||||
) -> Unit {
|
||||
if !self.is_enabled(level) {
|
||||
()
|
||||
} else {
|
||||
let actual_target = if target == "" { self.target } else { target }
|
||||
let timestamp_ms = if self.timestamp { @env.now() } else { 0UL }
|
||||
self.sink.write(
|
||||
record(level, message, timestamp_ms=timestamp_ms, target=actual_target, fields=fields),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn[S : Sink] Logger::trace(self : Logger[S], message : String, fields~ : Array[Field] = []) -> Unit {
|
||||
self.log(Level::Trace, message, fields=fields)
|
||||
}
|
||||
|
||||
pub fn[S : Sink] Logger::debug(self : Logger[S], message : String, fields~ : Array[Field] = []) -> Unit {
|
||||
self.log(Level::Debug, message, fields=fields)
|
||||
}
|
||||
|
||||
pub fn[S : Sink] Logger::info(self : Logger[S], message : String, fields~ : Array[Field] = []) -> Unit {
|
||||
self.log(Level::Info, message, fields=fields)
|
||||
}
|
||||
|
||||
pub fn[S : Sink] Logger::warn(self : Logger[S], message : String, fields~ : Array[Field] = []) -> Unit {
|
||||
self.log(Level::Warn, message, fields=fields)
|
||||
}
|
||||
|
||||
pub fn[S : Sink] Logger::error(self : Logger[S], message : String, fields~ : Array[Field] = []) -> Unit {
|
||||
self.log(Level::Error, message, fields=fields)
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import {
|
||||
"maria/json_parser" @json_parser,
|
||||
"moonbitlang/core/array",
|
||||
"moonbitlang/core/builtin",
|
||||
"moonbitlang/core/env" @env,
|
||||
"moonbitlang/core/json",
|
||||
"moonbitlang/core/queue" @queue,
|
||||
"moonbitlang/core/ref",
|
||||
}
|
||||
|
||||
options(
|
||||
targets: {
|
||||
"file_backend_native.mbt": [ "native", "llvm" ],
|
||||
"file_backend_stub.mbt": [ "js", "wasm", "wasm-gc" ],
|
||||
},
|
||||
)
|
||||
@@ -0,0 +1,69 @@
|
||||
pub type RecordPatch = (Record) -> Record
|
||||
|
||||
pub fn identity_patch() -> RecordPatch {
|
||||
fn(rec) { rec }
|
||||
}
|
||||
|
||||
pub fn set_target(target : String) -> RecordPatch {
|
||||
fn(rec) {
|
||||
{ ..rec, target }
|
||||
}
|
||||
}
|
||||
|
||||
pub fn prefix_message(prefix : String) -> RecordPatch {
|
||||
fn(rec) {
|
||||
{ ..rec, message: "\{prefix}\{rec.message}" }
|
||||
}
|
||||
}
|
||||
|
||||
pub fn append_fields(extra_fields : Array[Field]) -> RecordPatch {
|
||||
fn(rec) {
|
||||
if extra_fields.length() == 0 {
|
||||
rec
|
||||
} else if rec.fields.length() == 0 {
|
||||
{ ..rec, fields: extra_fields }
|
||||
} else {
|
||||
{ ..rec, fields: rec.fields + extra_fields }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn redact_field(key : String, placeholder~ : String = "***") -> RecordPatch {
|
||||
fn(rec) {
|
||||
{
|
||||
..rec,
|
||||
fields: rec.fields.map(fn(field) {
|
||||
if field.key == key {
|
||||
{ ..field, value: placeholder }
|
||||
} else {
|
||||
field
|
||||
}
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn redact_fields(keys : Array[String], placeholder~ : String = "***") -> RecordPatch {
|
||||
fn(rec) {
|
||||
{
|
||||
..rec,
|
||||
fields: rec.fields.map(fn(field) {
|
||||
if keys.contains(field.key) {
|
||||
{ ..field, value: placeholder }
|
||||
} else {
|
||||
field
|
||||
}
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn compose_patches(patches : Array[RecordPatch]) -> RecordPatch {
|
||||
fn(rec) {
|
||||
let mut current = rec
|
||||
for patch in patches {
|
||||
current = patch(current)
|
||||
}
|
||||
current
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
pub struct Field {
|
||||
key : String
|
||||
value : String
|
||||
}
|
||||
|
||||
pub fn field(key : String, value : String) -> Field {
|
||||
{ key, value }
|
||||
}
|
||||
|
||||
pub fn fields(entries : Array[(String, String)]) -> Array[Field] {
|
||||
entries.map(fn(entry) {
|
||||
field(entry.0, entry.1)
|
||||
})
|
||||
}
|
||||
|
||||
pub struct Record {
|
||||
level : Level
|
||||
timestamp_ms : UInt64
|
||||
target : String
|
||||
message : String
|
||||
fields : Array[Field]
|
||||
}
|
||||
|
||||
pub fn Record::new(
|
||||
level : Level,
|
||||
message : String,
|
||||
timestamp_ms~ : UInt64 = 0UL,
|
||||
target~ : String = "",
|
||||
fields~ : Array[Field] = [],
|
||||
) -> Record {
|
||||
{ level, timestamp_ms, target, message, fields }
|
||||
}
|
||||
|
||||
fn record(
|
||||
level : Level,
|
||||
message : String,
|
||||
timestamp_ms~ : UInt64 = 0UL,
|
||||
target~ : String = "",
|
||||
fields~ : Array[Field] = [],
|
||||
) -> Record {
|
||||
Record::new(level, message, timestamp_ms=timestamp_ms, target=target, fields=fields)
|
||||
}
|
||||
+650
@@ -0,0 +1,650 @@
|
||||
pub trait Sink {
|
||||
write(Self, Record) -> Unit
|
||||
}
|
||||
|
||||
pub struct ConsoleSink {
|
||||
_dummy : Unit
|
||||
}
|
||||
|
||||
pub fn console_sink() -> ConsoleSink {
|
||||
{ _dummy: () }
|
||||
}
|
||||
|
||||
pub impl Sink for ConsoleSink with write(self, rec) {
|
||||
ignore(self)
|
||||
println(format_text(rec))
|
||||
}
|
||||
|
||||
pub struct ContextSink[S] {
|
||||
sink : S
|
||||
context_fields : Array[Field]
|
||||
}
|
||||
|
||||
pub impl[S : Sink] Sink for ContextSink[S] with write(self, rec) {
|
||||
let merged = if self.context_fields.length() == 0 {
|
||||
rec.fields
|
||||
} else if rec.fields.length() == 0 {
|
||||
self.context_fields
|
||||
} else {
|
||||
self.context_fields + rec.fields
|
||||
}
|
||||
self.sink.write({ ..rec, fields: merged })
|
||||
}
|
||||
|
||||
pub struct JsonConsoleSink {
|
||||
_dummy : Unit
|
||||
}
|
||||
|
||||
pub fn json_console_sink() -> JsonConsoleSink {
|
||||
{ _dummy: () }
|
||||
}
|
||||
|
||||
pub impl Sink for JsonConsoleSink with write(self, rec) {
|
||||
ignore(self)
|
||||
println(format_json(rec))
|
||||
}
|
||||
|
||||
pub struct FileSink {
|
||||
path : String
|
||||
append : Ref[Bool]
|
||||
default_append : Bool
|
||||
handle : Ref[FileHandle?]
|
||||
formatter : RecordFormatter
|
||||
auto_flush : Ref[Bool]
|
||||
default_auto_flush : Bool
|
||||
rotation : Ref[FileRotation?]
|
||||
default_rotation : FileRotation?
|
||||
open_failures : Ref[Int]
|
||||
write_failures : Ref[Int]
|
||||
flush_failures : Ref[Int]
|
||||
rotation_failures : Ref[Int]
|
||||
}
|
||||
|
||||
pub struct FileRotation {
|
||||
max_bytes : Int
|
||||
max_backups : Int
|
||||
}
|
||||
|
||||
pub struct FileSinkState {
|
||||
path : String
|
||||
available : Bool
|
||||
append : Bool
|
||||
auto_flush : Bool
|
||||
rotation : FileRotation?
|
||||
open_failures : Int
|
||||
write_failures : Int
|
||||
flush_failures : Int
|
||||
rotation_failures : Int
|
||||
}
|
||||
|
||||
pub struct FileSinkPolicy {
|
||||
append : Bool
|
||||
auto_flush : Bool
|
||||
rotation : FileRotation?
|
||||
}
|
||||
|
||||
pub fn FileSinkPolicy::new(
|
||||
append~ : Bool = true,
|
||||
auto_flush~ : Bool = true,
|
||||
rotation~ : FileRotation? = None,
|
||||
) -> FileSinkPolicy {
|
||||
{ append, auto_flush, rotation }
|
||||
}
|
||||
|
||||
pub fn FileSinkState::new(
|
||||
path : String,
|
||||
available~ : Bool = false,
|
||||
append~ : Bool = true,
|
||||
auto_flush~ : Bool = true,
|
||||
rotation~ : FileRotation? = None,
|
||||
open_failures~ : Int = 0,
|
||||
write_failures~ : Int = 0,
|
||||
flush_failures~ : Int = 0,
|
||||
rotation_failures~ : Int = 0,
|
||||
) -> FileSinkState {
|
||||
{
|
||||
path,
|
||||
available,
|
||||
append,
|
||||
auto_flush,
|
||||
rotation,
|
||||
open_failures,
|
||||
write_failures,
|
||||
flush_failures,
|
||||
rotation_failures,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn file_rotation(max_bytes : Int, max_backups~ : Int = 1) -> FileRotation {
|
||||
{
|
||||
max_bytes: if max_bytes <= 0 { 1 } else { max_bytes },
|
||||
max_backups: if max_backups <= 0 { 1 } else { max_backups },
|
||||
}
|
||||
}
|
||||
|
||||
pub fn native_files_supported() -> Bool {
|
||||
native_files_supported_internal()
|
||||
}
|
||||
|
||||
pub fn file_sink(
|
||||
path : String,
|
||||
append~ : Bool = true,
|
||||
auto_flush~ : Bool = true,
|
||||
rotation~ : FileRotation? = None,
|
||||
formatter~ : RecordFormatter = fn(rec) {
|
||||
format_text(rec)
|
||||
},
|
||||
) -> FileSink {
|
||||
let handle = open_file_handle_internal(path, append)
|
||||
{
|
||||
path,
|
||||
append: Ref::new(append),
|
||||
default_append: append,
|
||||
handle: Ref::new(handle),
|
||||
formatter,
|
||||
auto_flush: Ref::new(auto_flush),
|
||||
default_auto_flush: auto_flush,
|
||||
rotation: Ref::new(rotation),
|
||||
default_rotation: rotation,
|
||||
open_failures: Ref::new(if handle is Some(_) { 0 } else { 1 }),
|
||||
write_failures: Ref::new(0),
|
||||
flush_failures: Ref::new(0),
|
||||
rotation_failures: Ref::new(0),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn FileSink::is_available(self : FileSink) -> Bool {
|
||||
self.handle.val is Some(_)
|
||||
}
|
||||
|
||||
pub fn FileSink::flush(self : FileSink) -> Bool {
|
||||
match self.handle.val {
|
||||
None => false
|
||||
Some(handle) => {
|
||||
let ok = flush_file_handle_internal(handle)
|
||||
if !ok {
|
||||
self.flush_failures.val += 1
|
||||
}
|
||||
ok
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn FileSink::append_mode(self : FileSink) -> Bool {
|
||||
self.append.val
|
||||
}
|
||||
|
||||
pub fn FileSink::set_append_mode(self : FileSink, append : Bool) -> Unit {
|
||||
self.append.val = append
|
||||
}
|
||||
|
||||
pub fn FileSink::path(self : FileSink) -> String {
|
||||
self.path
|
||||
}
|
||||
|
||||
pub fn FileSink::auto_flush_enabled(self : FileSink) -> Bool {
|
||||
self.auto_flush.val
|
||||
}
|
||||
|
||||
pub fn FileSink::rotation_enabled(self : FileSink) -> Bool {
|
||||
self.rotation.val is Some(_)
|
||||
}
|
||||
|
||||
pub fn FileSink::rotation_config(self : FileSink) -> FileRotation? {
|
||||
self.rotation.val
|
||||
}
|
||||
|
||||
pub fn FileSink::set_auto_flush(self : FileSink, enabled : Bool) -> Unit {
|
||||
self.auto_flush.val = enabled
|
||||
}
|
||||
|
||||
pub fn FileSink::set_policy(self : FileSink, policy : FileSinkPolicy) -> Unit {
|
||||
self.append.val = policy.append
|
||||
self.auto_flush.val = policy.auto_flush
|
||||
self.rotation.val = policy.rotation
|
||||
}
|
||||
|
||||
pub fn FileSink::set_rotation(self : FileSink, rotation : FileRotation?) -> Unit {
|
||||
self.rotation.val = rotation
|
||||
}
|
||||
|
||||
pub fn FileSink::clear_rotation(self : FileSink) -> Unit {
|
||||
self.rotation.val = None
|
||||
}
|
||||
|
||||
pub fn FileSink::close(self : FileSink) -> Bool {
|
||||
match self.handle.val {
|
||||
None => false
|
||||
Some(handle) => {
|
||||
let ok = close_file_handle_internal(handle)
|
||||
self.handle.val = None
|
||||
ok
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn FileSink::rotation_failures(self : FileSink) -> Int {
|
||||
self.rotation_failures.val
|
||||
}
|
||||
|
||||
pub fn FileSink::open_failures(self : FileSink) -> Int {
|
||||
self.open_failures.val
|
||||
}
|
||||
|
||||
pub fn FileSink::write_failures(self : FileSink) -> Int {
|
||||
self.write_failures.val
|
||||
}
|
||||
|
||||
pub fn FileSink::flush_failures(self : FileSink) -> Int {
|
||||
self.flush_failures.val
|
||||
}
|
||||
|
||||
pub fn FileSink::reset_failure_counters(self : FileSink) -> Unit {
|
||||
self.open_failures.val = 0
|
||||
self.write_failures.val = 0
|
||||
self.flush_failures.val = 0
|
||||
self.rotation_failures.val = 0
|
||||
}
|
||||
|
||||
pub fn FileSink::reset_policy(self : FileSink) -> Unit {
|
||||
self.append.val = self.default_append
|
||||
self.auto_flush.val = self.default_auto_flush
|
||||
self.rotation.val = self.default_rotation
|
||||
}
|
||||
|
||||
pub fn FileSink::policy(self : FileSink) -> FileSinkPolicy {
|
||||
FileSinkPolicy::new(
|
||||
append=self.append.val,
|
||||
auto_flush=self.auto_flush.val,
|
||||
rotation=self.rotation.val,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn FileSink::default_policy(self : FileSink) -> FileSinkPolicy {
|
||||
FileSinkPolicy::new(
|
||||
append=self.default_append,
|
||||
auto_flush=self.default_auto_flush,
|
||||
rotation=self.default_rotation,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn FileSink::policy_matches_default(self : FileSink) -> Bool {
|
||||
let current = self.policy()
|
||||
let default = self.default_policy()
|
||||
current.append == default.append &&
|
||||
current.auto_flush == default.auto_flush &&
|
||||
policy_rotation_equals_internal(current.rotation, default.rotation)
|
||||
}
|
||||
|
||||
fn policy_rotation_equals_internal(left : FileRotation?, right : FileRotation?) -> Bool {
|
||||
match (left, right) {
|
||||
(None, None) => true
|
||||
(Some(a), Some(b)) => a.max_bytes == b.max_bytes && a.max_backups == b.max_backups
|
||||
_ => false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn FileSink::state(self : FileSink) -> FileSinkState {
|
||||
{
|
||||
path: self.path,
|
||||
available: self.is_available(),
|
||||
append: self.append.val,
|
||||
auto_flush: self.auto_flush.val,
|
||||
rotation: self.rotation.val,
|
||||
open_failures: self.open_failures.val,
|
||||
write_failures: self.write_failures.val,
|
||||
flush_failures: self.flush_failures.val,
|
||||
rotation_failures: self.rotation_failures.val,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn FileSink::reopen(self : FileSink, append~ : Bool? = None) -> Bool {
|
||||
let append_mode = append.unwrap_or(self.append.val)
|
||||
self.append.val = append_mode
|
||||
match self.handle.val {
|
||||
None => ()
|
||||
Some(handle) => {
|
||||
ignore(close_file_handle_internal(handle))
|
||||
self.handle.val = None
|
||||
}
|
||||
}
|
||||
let reopened = open_file_handle_internal(self.path, append_mode)
|
||||
self.handle.val = reopened
|
||||
if reopened is Some(_) {
|
||||
true
|
||||
} else {
|
||||
self.open_failures.val += 1
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn FileSink::reopen_with_current_policy(self : FileSink) -> Bool {
|
||||
self.reopen()
|
||||
}
|
||||
|
||||
pub fn FileSink::reopen_append(self : FileSink) -> Bool {
|
||||
self.reopen(append=Some(true))
|
||||
}
|
||||
|
||||
pub fn FileSink::reopen_truncate(self : FileSink) -> Bool {
|
||||
self.reopen(append=Some(false))
|
||||
}
|
||||
|
||||
fn rotated_file_path(path : String, index : Int) -> String {
|
||||
"\{path}.\{index}"
|
||||
}
|
||||
|
||||
fn rotate_file_sink_internal(sink : FileSink, rotation : FileRotation) -> Bool {
|
||||
let closed = match sink.handle.val {
|
||||
None => true
|
||||
Some(handle) => {
|
||||
let ok = close_file_handle_internal(handle)
|
||||
sink.handle.val = None
|
||||
ok
|
||||
}
|
||||
}
|
||||
if !closed {
|
||||
return false
|
||||
}
|
||||
if rotation.max_backups > 0 {
|
||||
ignore(remove_file_internal(rotated_file_path(sink.path, rotation.max_backups)))
|
||||
for index = rotation.max_backups - 1; index >= 1; {
|
||||
let from_path = rotated_file_path(sink.path, index)
|
||||
let to_path = rotated_file_path(sink.path, index + 1)
|
||||
ignore(rename_file_internal(from_path, to_path))
|
||||
continue index - 1
|
||||
}
|
||||
ignore(rename_file_internal(sink.path, rotated_file_path(sink.path, 1)))
|
||||
} else {
|
||||
ignore(remove_file_internal(sink.path))
|
||||
}
|
||||
sink.handle.val = open_file_handle_internal(sink.path, false)
|
||||
sink.handle.val is Some(_)
|
||||
}
|
||||
|
||||
fn rotate_if_needed_internal(sink : FileSink, next_line_bytes : Int) -> Bool {
|
||||
match sink.rotation.val {
|
||||
None => true
|
||||
Some(rotation) => match sink.handle.val {
|
||||
None => false
|
||||
Some(handle) => {
|
||||
let size = file_size_internal(handle)
|
||||
if size + next_line_bytes <= rotation.max_bytes {
|
||||
true
|
||||
} else {
|
||||
let rotated = rotate_file_sink_internal(sink, rotation)
|
||||
if !rotated {
|
||||
sink.rotation_failures.val += 1
|
||||
}
|
||||
rotated
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub impl Sink for FileSink with write(self, rec) {
|
||||
match self.handle.val {
|
||||
None => {
|
||||
self.write_failures.val += 1
|
||||
}
|
||||
Some(_) => {
|
||||
let line = "\{(self.formatter)(rec)}\n"
|
||||
let can_write = rotate_if_needed_internal(self, string_byte_length_internal(line))
|
||||
if can_write {
|
||||
match self.handle.val {
|
||||
None => {
|
||||
self.write_failures.val += 1
|
||||
}
|
||||
Some(active) => {
|
||||
let wrote = write_file_handle_internal(active, line)
|
||||
if wrote {
|
||||
if self.auto_flush.val {
|
||||
let flushed = flush_file_handle_internal(active)
|
||||
if !flushed {
|
||||
self.flush_failures.val += 1
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.write_failures.val += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.write_failures.val += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FormattedConsoleSink {
|
||||
formatter : RecordFormatter
|
||||
}
|
||||
|
||||
pub fn formatted_console_sink(formatter : RecordFormatter) -> FormattedConsoleSink {
|
||||
{ formatter, }
|
||||
}
|
||||
|
||||
pub fn text_console_sink(formatter : TextFormatter) -> FormattedConsoleSink {
|
||||
formatted_console_sink(fn(rec) {
|
||||
format_text(rec, formatter=formatter)
|
||||
})
|
||||
}
|
||||
|
||||
pub impl Sink for FormattedConsoleSink with write(self, rec) {
|
||||
println((self.formatter)(rec))
|
||||
}
|
||||
|
||||
pub struct FormattedCallbackSink {
|
||||
formatter : RecordFormatter
|
||||
callback : (String) -> Unit
|
||||
}
|
||||
|
||||
pub fn formatted_callback_sink(
|
||||
formatter : RecordFormatter,
|
||||
callback : (String) -> Unit,
|
||||
) -> FormattedCallbackSink {
|
||||
{ formatter, callback }
|
||||
}
|
||||
|
||||
pub fn text_callback_sink(
|
||||
formatter : TextFormatter,
|
||||
callback : (String) -> Unit,
|
||||
) -> FormattedCallbackSink {
|
||||
formatted_callback_sink(fn(rec) {
|
||||
format_text(rec, formatter=formatter)
|
||||
}, callback)
|
||||
}
|
||||
|
||||
pub impl Sink for FormattedCallbackSink with write(self, rec) {
|
||||
(self.callback)((self.formatter)(rec))
|
||||
}
|
||||
|
||||
pub struct FanoutSink[A, B] {
|
||||
left : A
|
||||
right : B
|
||||
}
|
||||
|
||||
pub fn[A, B] fanout_sink(left : A, right : B) -> FanoutSink[A, B] {
|
||||
{ left, right }
|
||||
}
|
||||
|
||||
pub impl[A : Sink, B : Sink] Sink for FanoutSink[A, B] with write(self, rec) {
|
||||
self.left.write(rec)
|
||||
self.right.write({ ..rec })
|
||||
}
|
||||
|
||||
pub struct SplitSink[A, B] {
|
||||
left : A
|
||||
right : B
|
||||
predicate : (Record) -> Bool
|
||||
}
|
||||
|
||||
pub fn[A, B] split_sink(left : A, right : B, predicate : (Record) -> Bool) -> SplitSink[A, B] {
|
||||
{ left, right, predicate }
|
||||
}
|
||||
|
||||
pub fn[A, B] split_by_level(
|
||||
left : A,
|
||||
right : B,
|
||||
min_level~ : Level = Level::Warn,
|
||||
) -> SplitSink[A, B] {
|
||||
split_sink(left, right, fn(rec) {
|
||||
rec.level.enabled(min_level)
|
||||
})
|
||||
}
|
||||
|
||||
pub impl[A : Sink, B : Sink] Sink for SplitSink[A, B] with write(self, rec) {
|
||||
if (self.predicate)(rec) {
|
||||
self.left.write(rec)
|
||||
} else {
|
||||
self.right.write(rec)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CallbackSink {
|
||||
callback : (Record) -> Unit
|
||||
}
|
||||
|
||||
pub fn callback_sink(callback : (Record) -> Unit) -> CallbackSink {
|
||||
{ callback, }
|
||||
}
|
||||
|
||||
pub impl Sink for CallbackSink with write(self, rec) {
|
||||
(self.callback)(rec)
|
||||
}
|
||||
|
||||
pub struct BufferedSink[S] {
|
||||
sink : S
|
||||
buffer : Ref[Array[Record]]
|
||||
flush_limit : Int
|
||||
}
|
||||
|
||||
pub fn[S] buffered_sink(sink : S, flush_limit~ : Int = 1) -> BufferedSink[S] {
|
||||
let actual_limit = if flush_limit <= 0 { 1 } else { flush_limit }
|
||||
{ sink, buffer: Ref::new([]), flush_limit: actual_limit }
|
||||
}
|
||||
|
||||
pub fn[S] BufferedSink::pending_count(self : BufferedSink[S]) -> Int {
|
||||
self.buffer.val.length()
|
||||
}
|
||||
|
||||
pub fn[S : Sink] BufferedSink::flush(self : BufferedSink[S]) -> Unit {
|
||||
if self.buffer.val.length() == 0 {
|
||||
()
|
||||
} else {
|
||||
let pending = self.buffer.val
|
||||
self.buffer.val = []
|
||||
for rec in pending {
|
||||
self.sink.write(rec)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub impl[S : Sink] Sink for BufferedSink[S] with write(self, rec) {
|
||||
self.buffer.val.push(rec)
|
||||
if self.buffer.val.length() >= self.flush_limit {
|
||||
self.flush()
|
||||
}
|
||||
}
|
||||
|
||||
pub(all) enum QueueOverflowPolicy {
|
||||
DropNewest
|
||||
DropOldest
|
||||
}
|
||||
|
||||
pub struct QueuedSink[S] {
|
||||
sink : S
|
||||
queue : @queue.Queue[Record]
|
||||
max_pending : Int
|
||||
overflow : QueueOverflowPolicy
|
||||
dropped_count : Ref[Int]
|
||||
}
|
||||
|
||||
pub fn[S] queued_sink(
|
||||
sink : S,
|
||||
max_pending~ : Int = 0,
|
||||
overflow~ : QueueOverflowPolicy = QueueOverflowPolicy::DropNewest,
|
||||
) -> QueuedSink[S] {
|
||||
{
|
||||
sink,
|
||||
queue: @queue.Queue::new(),
|
||||
max_pending,
|
||||
overflow,
|
||||
dropped_count: Ref::new(0),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn[S] QueuedSink::pending_count(self : QueuedSink[S]) -> Int {
|
||||
self.queue.length()
|
||||
}
|
||||
|
||||
pub fn[S] QueuedSink::dropped_count(self : QueuedSink[S]) -> Int {
|
||||
self.dropped_count.val
|
||||
}
|
||||
|
||||
pub fn[S : Sink] QueuedSink::drain(self : QueuedSink[S], max_items~ : Int = -1) -> Int {
|
||||
if max_items == 0 {
|
||||
return 0
|
||||
}
|
||||
let limit = if max_items < 0 { self.pending_count() } else { max_items }
|
||||
for drained = 0; drained < limit; {
|
||||
match self.queue.pop() {
|
||||
None => break drained
|
||||
Some(rec) => {
|
||||
self.sink.write(rec)
|
||||
continue drained + 1
|
||||
}
|
||||
}
|
||||
} nobreak {
|
||||
limit
|
||||
}
|
||||
}
|
||||
|
||||
pub fn[S : Sink] QueuedSink::flush(self : QueuedSink[S]) -> Int {
|
||||
self.drain()
|
||||
}
|
||||
|
||||
pub impl[S] Sink for QueuedSink[S] with write(self, rec) {
|
||||
let full = self.max_pending > 0 && self.pending_count() >= self.max_pending
|
||||
if !full {
|
||||
self.queue.push(rec)
|
||||
} else {
|
||||
self.dropped_count.val += 1
|
||||
match self.overflow {
|
||||
QueueOverflowPolicy::DropNewest => ()
|
||||
QueueOverflowPolicy::DropOldest => {
|
||||
ignore(self.queue.pop())
|
||||
self.queue.push(rec)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FilterSink[S] {
|
||||
sink : S
|
||||
predicate : (Record) -> Bool
|
||||
}
|
||||
|
||||
pub fn[S] filter_sink(sink : S, predicate : (Record) -> Bool) -> FilterSink[S] {
|
||||
{ sink, predicate }
|
||||
}
|
||||
|
||||
pub impl[S : Sink] Sink for FilterSink[S] with write(self, rec) {
|
||||
if (self.predicate)(rec) {
|
||||
self.sink.write(rec)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PatchSink[S] {
|
||||
sink : S
|
||||
patch : RecordPatch
|
||||
}
|
||||
|
||||
pub fn[S] patch_sink(sink : S, patch : RecordPatch) -> PatchSink[S] {
|
||||
{ sink, patch }
|
||||
}
|
||||
|
||||
pub impl[S : Sink] Sink for PatchSink[S] with write(self, rec) {
|
||||
self.sink.write((self.patch)(rec))
|
||||
}
|
||||
Reference in New Issue
Block a user