🚚 Move bitlogger&bitlogger-async to src& src-async

This commit is contained in:
Nanaloveyuki
2026-05-15 10:13:36 +08:00
parent 02c40f26f9
commit 1c75c98e3c
25 changed files with 16 additions and 18 deletions
+2
View File
@@ -0,0 +1,2 @@
///|
/// BitLogger public API surface.
+772
View File
@@ -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
+56
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+117
View File
@@ -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
}
+51
View File
@@ -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
}
+75
View File
@@ -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
}
}
+905
View File
@@ -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()
}
}
+39
View File
@@ -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)
}
+31
View File
@@ -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
View File
@@ -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)
}
+16
View File
@@ -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" ],
},
)
+69
View File
@@ -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
}
}
+42
View File
@@ -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
View File
@@ -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))
}