mirror of
https://github.com/Nanaloveyuki/BitLogger.git
synced 2026-07-24 00:42:18 +00:00
1917 lines
78 KiB
MoonBit
1917 lines
78 KiB
MoonBit
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 parser fills omitted top-level keys from defaults" {
|
|
let config = parse_logger_config_text("{}")
|
|
inspect(config.min_level.label(), content="INFO")
|
|
inspect(config.target, content="")
|
|
inspect(config.timestamp, content="false")
|
|
inspect(config.queue is None, content="true")
|
|
inspect(match config.sink.kind {
|
|
SinkKind::Console => "Console"
|
|
SinkKind::JsonConsole => "JsonConsole"
|
|
SinkKind::TextConsole => "TextConsole"
|
|
SinkKind::File => "File"
|
|
}, content="Console")
|
|
inspect(config.sink.path, content="")
|
|
inspect(config.sink.append, content="true")
|
|
inspect(config.sink.auto_flush, content="true")
|
|
inspect(config.sink.rotation is None, content="true")
|
|
inspect(config.sink.text_formatter.show_timestamp, content="true")
|
|
inspect(config.sink.text_formatter.separator, content=" ")
|
|
inspect(config.sink.text_formatter.field_separator, content=" ")
|
|
inspect(config.sink.text_formatter.template, content="")
|
|
}
|
|
|
|
test "logger config parser fills nested queue and formatter defaults" {
|
|
let config = parse_logger_config_text(
|
|
"{\"sink\":{\"kind\":\"text_console\",\"text_formatter\":{\"separator\":\" | \"}},\"queue\":{\"max_pending\":4}}",
|
|
)
|
|
inspect(match config.sink.kind {
|
|
SinkKind::Console => "Console"
|
|
SinkKind::JsonConsole => "JsonConsole"
|
|
SinkKind::TextConsole => "TextConsole"
|
|
SinkKind::File => "File"
|
|
}, content="TextConsole")
|
|
inspect(config.sink.text_formatter.show_timestamp, content="true")
|
|
inspect(config.sink.text_formatter.show_level, content="true")
|
|
inspect(config.sink.text_formatter.show_target, content="true")
|
|
inspect(config.sink.text_formatter.show_fields, content="true")
|
|
inspect(config.sink.text_formatter.separator, content=" | ")
|
|
inspect(config.sink.text_formatter.field_separator, content=" ")
|
|
inspect(config.sink.text_formatter.template, content="")
|
|
inspect(color_mode_label(config.sink.text_formatter.color_mode), content="never")
|
|
inspect(color_support_label(config.sink.text_formatter.color_support), content="truecolor")
|
|
inspect(style_markup_mode_label(config.sink.text_formatter.style_markup), content="full")
|
|
inspect(style_markup_mode_label(config.sink.text_formatter.target_style_markup), content="disabled")
|
|
inspect(style_markup_mode_label(config.sink.text_formatter.fields_style_markup), content="disabled")
|
|
inspect(config.sink.text_formatter.style_tags.length(), content="0")
|
|
match config.queue {
|
|
Some(queue) => {
|
|
inspect(queue.max_pending, content="4")
|
|
inspect(match queue.overflow {
|
|
QueueOverflowPolicy::DropNewest => "DropNewest"
|
|
QueueOverflowPolicy::DropOldest => "DropOldest"
|
|
}, content="DropNewest")
|
|
}
|
|
None => inspect(false, content="true")
|
|
}
|
|
}
|
|
|
|
test "logger config parser rejects malformed sync config input" {
|
|
let invalid_json_error = (fn() -> String raise ConfigError {
|
|
ignore(parse_logger_config_text("{"))
|
|
"no error"
|
|
})() catch {
|
|
ConfigError::InvalidConfig(message) => message
|
|
}
|
|
inspect(invalid_json_error.contains("Invalid JSON:"), content="true")
|
|
|
|
let wrong_type_error = (fn() -> String raise ConfigError {
|
|
ignore(parse_logger_config_text("{\"timestamp\":\"true\"}"))
|
|
"no error"
|
|
})() catch {
|
|
ConfigError::InvalidConfig(message) => message
|
|
}
|
|
inspect(wrong_type_error, content="Expected bool at key timestamp")
|
|
|
|
let invalid_enum_error = (fn() -> String raise ConfigError {
|
|
ignore(parse_logger_config_text("{\"sink\":{\"kind\":\"console\",\"text_formatter\":{\"color_mode\":\"loud\"}}}"))
|
|
"no error"
|
|
})() catch {
|
|
ConfigError::InvalidConfig(message) => message
|
|
}
|
|
inspect(invalid_enum_error, content="Unsupported color mode: loud")
|
|
}
|
|
|
|
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 json helpers export stable structured shapes" {
|
|
let queue_json = queue_config_to_json(
|
|
QueueConfig::new(8, overflow=QueueOverflowPolicy::DropOldest),
|
|
)
|
|
let queue_obj = queue_json.as_object().unwrap()
|
|
inspect(@json_parser.stringify(queue_json), content="{\"max_pending\":8,\"overflow\":\"DropOldest\"}")
|
|
inspect(queue_obj.get("max_pending").unwrap().as_number().unwrap().to_int(), content="8")
|
|
inspect(queue_obj.get("overflow").unwrap().as_string().unwrap(), content="DropOldest")
|
|
|
|
let formatter_json = text_formatter_config_to_json(
|
|
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),
|
|
},
|
|
),
|
|
)
|
|
let formatter_obj = formatter_json.as_object().unwrap()
|
|
let style_tags_obj = formatter_obj.get("style_tags").unwrap().as_object().unwrap()
|
|
let accent_obj = style_tags_obj.get("accent").unwrap().as_object().unwrap()
|
|
inspect(formatter_obj.get("show_timestamp").unwrap().as_bool().unwrap(), content="false")
|
|
inspect(formatter_obj.get("show_target").unwrap().as_bool().unwrap(), content="false")
|
|
inspect(formatter_obj.get("separator").unwrap().as_string().unwrap(), content=" | ")
|
|
inspect(formatter_obj.get("field_separator").unwrap().as_string().unwrap(), content=",")
|
|
inspect(formatter_obj.get("template").unwrap().as_string().unwrap(), content="[{level}] {message} :: {fields}")
|
|
inspect(formatter_obj.get("color_mode").unwrap().as_string().unwrap(), content="always")
|
|
inspect(formatter_obj.get("color_support").unwrap().as_string().unwrap(), content="basic")
|
|
inspect(formatter_obj.get("style_markup").unwrap().as_string().unwrap(), content="builtin")
|
|
inspect(formatter_obj.get("target_style_markup").unwrap().as_string().unwrap(), content="builtin")
|
|
inspect(formatter_obj.get("fields_style_markup").unwrap().as_string().unwrap(), content="disabled")
|
|
inspect(accent_obj.get("bold").unwrap().as_bool().unwrap(), content="true")
|
|
inspect(accent_obj.get("fg").unwrap().as_string().unwrap(), content="#4cc9f0")
|
|
|
|
let sink_json = sink_config_to_json(
|
|
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),
|
|
),
|
|
)
|
|
let sink_obj = sink_json.as_object().unwrap()
|
|
let sink_formatter_obj = sink_obj.get("text_formatter").unwrap().as_object().unwrap()
|
|
let rotation_obj = sink_obj.get("rotation").unwrap().as_object().unwrap()
|
|
inspect(sink_obj.get("kind").unwrap().as_string().unwrap(), content="file")
|
|
inspect(sink_obj.get("path").unwrap().as_string().unwrap(), content="demo.log")
|
|
inspect(sink_obj.get("append").unwrap().as_bool().unwrap(), content="false")
|
|
inspect(sink_obj.get("auto_flush").unwrap().as_bool().unwrap(), content="false")
|
|
inspect(sink_formatter_obj.get("show_timestamp").unwrap().as_bool().unwrap(), content="false")
|
|
inspect(sink_formatter_obj.get("color_mode").unwrap().as_string().unwrap(), content="auto")
|
|
inspect(rotation_obj.get("max_bytes").unwrap().as_number().unwrap().to_int(), content="128")
|
|
inspect(rotation_obj.get("max_backups").unwrap().as_number().unwrap().to_int(), content="2")
|
|
|
|
let logger_json = logger_config_to_json(
|
|
LoggerConfig::new(
|
|
min_level=Level::Warn,
|
|
target="api",
|
|
timestamp=true,
|
|
sink=SinkConfig::new(kind=SinkKind::TextConsole),
|
|
queue=Some(QueueConfig::new(8, overflow=QueueOverflowPolicy::DropNewest)),
|
|
),
|
|
)
|
|
let logger_obj = logger_json.as_object().unwrap()
|
|
let logger_sink_obj = logger_obj.get("sink").unwrap().as_object().unwrap()
|
|
let logger_queue_obj = logger_obj.get("queue").unwrap().as_object().unwrap()
|
|
inspect(logger_obj.get("min_level").unwrap().as_string().unwrap(), content="WARN")
|
|
inspect(logger_obj.get("target").unwrap().as_string().unwrap(), content="api")
|
|
inspect(logger_obj.get("timestamp").unwrap().as_bool().unwrap(), content="true")
|
|
inspect(logger_sink_obj.get("kind").unwrap().as_string().unwrap(), content="text_console")
|
|
inspect(logger_queue_obj.get("max_pending").unwrap().as_number().unwrap().to_int(), content="8")
|
|
inspect(logger_queue_obj.get("overflow").unwrap().as_string().unwrap(), content="DropNewest")
|
|
}
|
|
|
|
test "logger config json export materializes parsed omitted defaults" {
|
|
let parsed = parse_logger_config_text("{}")
|
|
let logger_json = logger_config_to_json(parsed)
|
|
let logger_obj = logger_json.as_object().unwrap()
|
|
let sink_obj = logger_obj.get("sink").unwrap().as_object().unwrap()
|
|
let formatter_obj = sink_obj.get("text_formatter").unwrap().as_object().unwrap()
|
|
|
|
inspect(
|
|
@json_parser.stringify(logger_json),
|
|
content="{\"min_level\":\"INFO\",\"target\":\"\",\"timestamp\":false,\"sink\":{\"kind\":\"console\",\"path\":\"\",\"append\":true,\"auto_flush\":true,\"text_formatter\":{\"show_timestamp\":true,\"show_level\":true,\"show_target\":true,\"show_fields\":true,\"separator\":\" \",\"field_separator\":\" \",\"template\":\"\",\"color_mode\":\"never\",\"color_support\":\"truecolor\",\"style_markup\":\"full\",\"target_style_markup\":\"disabled\",\"fields_style_markup\":\"disabled\"}}}",
|
|
)
|
|
inspect(logger_obj.get("min_level").unwrap().as_string().unwrap(), content="INFO")
|
|
inspect(logger_obj.get("target").unwrap().as_string().unwrap(), content="")
|
|
inspect(logger_obj.get("timestamp").unwrap().as_bool().unwrap(), content="false")
|
|
inspect(logger_obj.get("queue") is None, content="true")
|
|
inspect(sink_obj.get("kind").unwrap().as_string().unwrap(), content="console")
|
|
inspect(sink_obj.get("path").unwrap().as_string().unwrap(), content="")
|
|
inspect(sink_obj.get("append").unwrap().as_bool().unwrap(), content="true")
|
|
inspect(sink_obj.get("auto_flush").unwrap().as_bool().unwrap(), content="true")
|
|
inspect(formatter_obj.get("show_timestamp").unwrap().as_bool().unwrap(), content="true")
|
|
inspect(formatter_obj.get("show_level").unwrap().as_bool().unwrap(), content="true")
|
|
inspect(formatter_obj.get("show_target").unwrap().as_bool().unwrap(), content="true")
|
|
inspect(formatter_obj.get("show_fields").unwrap().as_bool().unwrap(), content="true")
|
|
inspect(formatter_obj.get("separator").unwrap().as_string().unwrap(), content=" ")
|
|
inspect(formatter_obj.get("field_separator").unwrap().as_string().unwrap(), content=" ")
|
|
inspect(formatter_obj.get("template").unwrap().as_string().unwrap(), content="")
|
|
inspect(formatter_obj.get("color_mode").unwrap().as_string().unwrap(), content="never")
|
|
inspect(formatter_obj.get("color_support").unwrap().as_string().unwrap(), content="truecolor")
|
|
inspect(formatter_obj.get("style_markup").unwrap().as_string().unwrap(), content="full")
|
|
inspect(formatter_obj.get("target_style_markup").unwrap().as_string().unwrap(), content="disabled")
|
|
inspect(formatter_obj.get("fields_style_markup").unwrap().as_string().unwrap(), content="disabled")
|
|
inspect(formatter_obj.get("style_tags") is None, content="true")
|
|
}
|
|
|
|
test "default config helpers expose baseline config shapes" {
|
|
let formatter = default_text_formatter_config()
|
|
inspect(formatter.show_timestamp, content="true")
|
|
inspect(color_mode_label(formatter.color_mode), content="never")
|
|
inspect(color_support_label(formatter.color_support), content="truecolor")
|
|
inspect(style_markup_mode_label(formatter.style_markup), content="full")
|
|
|
|
let sink = default_sink_config()
|
|
inspect(match sink.kind {
|
|
SinkKind::Console => "Console"
|
|
_ => "other"
|
|
}, content="Console")
|
|
inspect(sink.path, content="")
|
|
inspect(sink.rotation is None, content="true")
|
|
|
|
let logger = default_logger_config()
|
|
inspect(logger.min_level.label(), content="INFO")
|
|
inspect(logger.target, content="")
|
|
inspect(logger.timestamp, content="false")
|
|
inspect(logger.queue is None, content="true")
|
|
inspect(match logger.sink.kind {
|
|
SinkKind::Console => "Console"
|
|
_ => "other"
|
|
}, content="Console")
|
|
}
|
|
|
|
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 "runtime sink queue helpers forward direct queue state" {
|
|
let sink = RuntimeSink::QueuedTextConsole(
|
|
queued_sink(
|
|
text_console_sink(text_formatter(show_timestamp=false, show_target=false)),
|
|
max_pending=2,
|
|
overflow=QueueOverflowPolicy::DropOldest,
|
|
),
|
|
)
|
|
let logger = Logger::new(sink, min_level=Level::Info, target="runtime.queue")
|
|
logger.info("one")
|
|
logger.info("two")
|
|
logger.info("three")
|
|
inspect(sink.pending_count(), content="2")
|
|
inspect(sink.dropped_count(), content="1")
|
|
inspect(sink.drain(max_items=1), content="1")
|
|
inspect(sink.pending_count(), content="1")
|
|
inspect(sink.flush(), content="1")
|
|
inspect(sink.pending_count(), content="0")
|
|
}
|
|
|
|
test "runtime sink plain variants use documented fallback counts" {
|
|
let console_sink = RuntimeSink::Console(console_sink())
|
|
inspect(console_sink.pending_count(), content="0")
|
|
inspect(console_sink.dropped_count(), content="0")
|
|
inspect(console_sink.flush(), content="0")
|
|
inspect(console_sink.drain(), content="0")
|
|
|
|
let file_sink = RuntimeSink::File(file_sink("runtime-direct.log", auto_flush=false))
|
|
inspect(file_sink.pending_count(), content="0")
|
|
inspect(file_sink.dropped_count(), content="0")
|
|
if file_sink.file_available() {
|
|
inspect(file_sink.flush(), content="1")
|
|
inspect(file_sink.drain(), content="1")
|
|
inspect(file_sink.close(), content="true")
|
|
} else {
|
|
inspect(file_sink.flush(), content="0")
|
|
inspect(file_sink.drain(), content="0")
|
|
inspect(file_sink.close(), content="false")
|
|
}
|
|
}
|
|
|
|
test "runtime sink non-file variants expose documented file fallbacks" {
|
|
let sink = RuntimeSink::Console(console_sink())
|
|
inspect(sink.file_available(), content="false")
|
|
inspect(sink.file_path(), content="")
|
|
inspect(sink.file_append_mode(), content="false")
|
|
inspect(sink.file_auto_flush(), content="false")
|
|
inspect(sink.file_rotation_enabled(), content="false")
|
|
inspect(sink.file_rotation_config() is None, content="true")
|
|
inspect(sink.file_open_failures(), content="0")
|
|
inspect(sink.file_write_failures(), content="0")
|
|
inspect(sink.file_flush_failures(), content="0")
|
|
inspect(sink.file_rotation_failures(), content="0")
|
|
inspect(sink.file_reopen(), content="false")
|
|
inspect(sink.file_reopen_with_current_policy(), content="false")
|
|
inspect(sink.file_reopen_append(), content="false")
|
|
inspect(sink.file_reopen_truncate(), content="false")
|
|
inspect(sink.file_set_append_mode(true), content="false")
|
|
inspect(sink.file_set_auto_flush(true), content="false")
|
|
inspect(sink.file_set_rotation(Some(file_rotation(32, max_backups=2))), content="false")
|
|
inspect(sink.file_clear_rotation(), content="false")
|
|
inspect(sink.file_set_policy(FileSinkPolicy::new()), content="false")
|
|
inspect(sink.file_reset_failure_counters(), content="false")
|
|
inspect(sink.file_reset_policy(), content="false")
|
|
inspect(sink.file_flush(), content="false")
|
|
inspect(sink.file_close(), content="false")
|
|
let policy = sink.file_policy()
|
|
let defaults = sink.file_default_policy()
|
|
let state = sink.file_state()
|
|
inspect(policy.append, content="false")
|
|
inspect(policy.auto_flush, content="false")
|
|
inspect(policy.rotation is None, content="true")
|
|
inspect(defaults.append, content="false")
|
|
inspect(defaults.auto_flush, content="false")
|
|
inspect(defaults.rotation is None, content="true")
|
|
inspect(sink.file_policy_matches_default(), content="false")
|
|
inspect(state.path, content="")
|
|
inspect(state.available, content="false")
|
|
inspect(state.append, content="false")
|
|
inspect(state.auto_flush, content="false")
|
|
inspect(state.rotation is None, content="true")
|
|
inspect(state.open_failures, content="0")
|
|
inspect(state.write_failures, content="0")
|
|
inspect(state.flush_failures, content="0")
|
|
inspect(state.rotation_failures, content="0")
|
|
inspect(sink.file_runtime_state() is None, content="true")
|
|
}
|
|
|
|
test "runtime sink plain file helpers expose direct file state and policy" {
|
|
let sink = RuntimeSink::File(
|
|
file_sink(
|
|
"runtime-file-helpers.log",
|
|
auto_flush=false,
|
|
rotation=Some(file_rotation(40, max_backups=2)),
|
|
),
|
|
)
|
|
inspect(sink.file_available() == native_files_supported(), content="true")
|
|
inspect(sink.file_path(), content="runtime-file-helpers.log")
|
|
inspect(sink.file_append_mode(), content="true")
|
|
inspect(sink.file_auto_flush(), content="false")
|
|
inspect(sink.file_rotation_enabled(), content="true")
|
|
let defaults = sink.file_default_policy()
|
|
let policy = sink.file_policy()
|
|
inspect(defaults.append, content="true")
|
|
inspect(defaults.auto_flush, content="false")
|
|
inspect(policy.append, content="true")
|
|
inspect(policy.auto_flush, content="false")
|
|
inspect(sink.file_policy_matches_default(), content="true")
|
|
match sink.file_rotation_config() {
|
|
Some(rotation) => {
|
|
inspect(rotation.max_bytes, content="40")
|
|
inspect(rotation.max_backups, content="2")
|
|
}
|
|
None => inspect(false, content="true")
|
|
}
|
|
let state = sink.file_state()
|
|
inspect(state.path, content="runtime-file-helpers.log")
|
|
inspect(state.available == sink.file_available(), content="true")
|
|
inspect(state.append, content="true")
|
|
inspect(state.auto_flush, content="false")
|
|
match sink.file_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="runtime-file-helpers.log")
|
|
}
|
|
None => inspect(false, content="true")
|
|
}
|
|
inspect(sink.file_set_append_mode(false), content="true")
|
|
inspect(sink.file_set_auto_flush(true), content="true")
|
|
inspect(sink.file_clear_rotation(), content="true")
|
|
inspect(sink.file_policy_matches_default(), content="false")
|
|
inspect(sink.file_append_mode(), content="false")
|
|
inspect(sink.file_auto_flush(), content="true")
|
|
inspect(sink.file_rotation_config() is None, content="true")
|
|
inspect(sink.file_reset_policy(), content="true")
|
|
inspect(sink.file_policy_matches_default(), content="true")
|
|
inspect(sink.file_append_mode(), content="true")
|
|
inspect(sink.file_auto_flush(), content="false")
|
|
inspect(sink.file_rotation_enabled(), content="true")
|
|
if sink.file_available() {
|
|
inspect(sink.file_flush(), content="true")
|
|
inspect(sink.file_close(), content="true")
|
|
} else {
|
|
inspect(sink.file_flush(), content="false")
|
|
inspect(sink.file_close(), content="false")
|
|
}
|
|
}
|
|
|
|
test "runtime sink queued file helpers preserve queue-aware file runtime state" {
|
|
let sink = RuntimeSink::QueuedFile(
|
|
queued_sink(
|
|
file_sink("runtime-queued-file.log", auto_flush=false),
|
|
max_pending=2,
|
|
overflow=QueueOverflowPolicy::DropNewest,
|
|
),
|
|
)
|
|
let logger = Logger::new(sink, min_level=Level::Info, target="runtime.file.queue")
|
|
logger.info("one")
|
|
logger.info("two")
|
|
logger.info("three")
|
|
inspect(sink.file_available() == native_files_supported(), content="true")
|
|
inspect(sink.file_path(), content="runtime-queued-file.log")
|
|
inspect(sink.pending_count(), content="2")
|
|
inspect(sink.dropped_count(), content="1")
|
|
match sink.file_runtime_state() {
|
|
Some(snapshot) => {
|
|
inspect(snapshot.queued, content="true")
|
|
inspect(snapshot.pending_count, content="2")
|
|
inspect(snapshot.dropped_count, content="1")
|
|
inspect(snapshot.file.path, content="runtime-queued-file.log")
|
|
}
|
|
None => inspect(false, content="true")
|
|
}
|
|
if sink.file_available() {
|
|
inspect(sink.file_flush(), content="true")
|
|
inspect(sink.pending_count(), content="0")
|
|
match sink.file_runtime_state() {
|
|
Some(snapshot) => inspect(snapshot.pending_count, content="0")
|
|
None => inspect(false, content="true")
|
|
}
|
|
inspect(sink.file_close(), content="true")
|
|
} else {
|
|
inspect(sink.file_flush(), content="false")
|
|
inspect(sink.pending_count(), content="0")
|
|
match sink.file_runtime_state() {
|
|
Some(snapshot) => inspect(snapshot.pending_count, content="0")
|
|
None => inspect(false, content="true")
|
|
}
|
|
inspect(sink.file_close(), content="false")
|
|
}
|
|
}
|
|
|
|
test "runtime sink plain file reopen helpers and failure resets work directly" {
|
|
let sink = RuntimeSink::File(file_sink("runtime-reopen-direct.log"))
|
|
if sink.file_available() {
|
|
inspect(sink.close(), content="true")
|
|
inspect(sink.file_reopen_append(), content="true")
|
|
inspect(sink.file_available(), content="true")
|
|
inspect(sink.file_append_mode(), content="true")
|
|
inspect(sink.file_open_failures(), content="0")
|
|
Logger::new(sink, min_level=Level::Info, target="runtime.reopen.direct").info("reopened")
|
|
inspect(sink.file_write_failures(), content="0")
|
|
inspect(sink.file_reopen_truncate(), content="true")
|
|
inspect(sink.file_append_mode(), content="false")
|
|
inspect(sink.file_reopen(), content="true")
|
|
inspect(sink.file_append_mode(), content="false")
|
|
inspect(sink.file_reopen_with_current_policy(), content="true")
|
|
inspect(sink.file_append_mode(), content="false")
|
|
inspect(sink.file_reopen_append(), content="true")
|
|
inspect(sink.file_append_mode(), content="true")
|
|
inspect(sink.file_reset_failure_counters(), content="true")
|
|
inspect(sink.file_open_failures(), content="0")
|
|
inspect(sink.file_write_failures(), content="0")
|
|
inspect(sink.file_flush_failures(), content="0")
|
|
inspect(sink.file_rotation_failures(), content="0")
|
|
inspect(sink.close(), content="true")
|
|
} else {
|
|
inspect(sink.file_append_mode(), content="true")
|
|
inspect(sink.file_open_failures(), content="1")
|
|
Logger::new(sink, min_level=Level::Info, target="runtime.reopen.direct").info("dropped")
|
|
inspect(sink.file_write_failures(), content="1")
|
|
inspect(sink.file_reopen_append(), content="false")
|
|
inspect(sink.file_open_failures(), content="2")
|
|
inspect(sink.file_reopen_truncate(), content="false")
|
|
inspect(sink.file_append_mode(), content="false")
|
|
inspect(sink.file_reopen_with_current_policy(), content="false")
|
|
inspect(sink.file_open_failures(), content="4")
|
|
inspect(sink.file_reopen_append(), content="false")
|
|
inspect(sink.file_append_mode(), content="true")
|
|
inspect(sink.file_open_failures(), content="5")
|
|
inspect(sink.file_reset_failure_counters(), content="true")
|
|
inspect(sink.file_open_failures(), content="0")
|
|
inspect(sink.file_write_failures(), content="0")
|
|
inspect(sink.file_flush_failures(), content="0")
|
|
inspect(sink.file_rotation_failures(), content="0")
|
|
}
|
|
}
|
|
|
|
test "runtime sink queued file reopen helpers and failure resets work directly" {
|
|
let sink = RuntimeSink::QueuedFile(
|
|
queued_sink(file_sink("runtime-reopen-queued.log"), max_pending=2),
|
|
)
|
|
if sink.file_available() {
|
|
inspect(sink.file_close(), content="true")
|
|
inspect(sink.file_reopen_append(), content="true")
|
|
inspect(sink.file_available(), content="true")
|
|
inspect(sink.file_append_mode(), content="true")
|
|
inspect(sink.file_open_failures(), content="0")
|
|
let logger = Logger::new(sink, min_level=Level::Info, target="runtime.reopen.queued")
|
|
logger.info("one")
|
|
logger.info("two")
|
|
inspect(sink.pending_count(), content="2")
|
|
inspect(sink.file_reopen_truncate(), content="true")
|
|
inspect(sink.file_append_mode(), content="false")
|
|
inspect(sink.file_reopen(), content="true")
|
|
inspect(sink.file_append_mode(), content="false")
|
|
inspect(sink.file_reopen_with_current_policy(), content="true")
|
|
inspect(sink.file_append_mode(), content="false")
|
|
inspect(sink.file_reopen_append(), content="true")
|
|
inspect(sink.file_append_mode(), content="true")
|
|
inspect(sink.file_reset_failure_counters(), content="true")
|
|
inspect(sink.file_open_failures(), content="0")
|
|
inspect(sink.file_write_failures(), content="0")
|
|
inspect(sink.file_flush_failures(), content="0")
|
|
inspect(sink.file_rotation_failures(), content="0")
|
|
inspect(sink.file_close(), content="true")
|
|
} else {
|
|
inspect(sink.file_append_mode(), content="true")
|
|
inspect(sink.file_open_failures(), content="1")
|
|
let logger = Logger::new(sink, min_level=Level::Info, target="runtime.reopen.queued")
|
|
logger.info("one")
|
|
inspect(sink.pending_count(), content="1")
|
|
inspect(sink.file_write_failures(), content="0")
|
|
inspect(sink.file_flush(), content="false")
|
|
inspect(sink.pending_count(), content="0")
|
|
inspect(sink.file_write_failures(), content="1")
|
|
inspect(sink.file_reopen_append(), content="false")
|
|
inspect(sink.file_open_failures(), content="2")
|
|
inspect(sink.file_reopen_truncate(), content="false")
|
|
inspect(sink.file_append_mode(), content="false")
|
|
inspect(sink.file_reopen_with_current_policy(), content="false")
|
|
inspect(sink.file_open_failures(), content="4")
|
|
inspect(sink.file_reopen_append(), content="false")
|
|
inspect(sink.file_append_mode(), content="true")
|
|
inspect(sink.file_open_failures(), content="5")
|
|
inspect(sink.file_reset_failure_counters(), content="true")
|
|
inspect(sink.file_open_failures(), content="0")
|
|
inspect(sink.file_write_failures(), content="0")
|
|
inspect(sink.file_flush_failures(), content="0")
|
|
inspect(sink.file_rotation_failures(), content="0")
|
|
}
|
|
}
|
|
|
|
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 rotation_json = file_rotation_config_to_json(file_rotation(64, max_backups=2))
|
|
let rotation_obj = rotation_json.as_object().unwrap()
|
|
inspect(@json_parser.stringify(rotation_json), content="{\"max_bytes\":64,\"max_backups\":2}")
|
|
inspect(rotation_obj.get("max_bytes").unwrap().as_number().unwrap().to_int(), content="64")
|
|
inspect(rotation_obj.get("max_backups").unwrap().as_number().unwrap().to_int(), content="2")
|
|
|
|
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,
|
|
),
|
|
)
|
|
let plain_obj = plain.as_object().unwrap()
|
|
let plain_rotation_obj = plain_obj.get("rotation").unwrap().as_object().unwrap()
|
|
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(plain_obj.get("path").unwrap().as_string().unwrap(), content="demo.log")
|
|
inspect(plain_obj.get("available").unwrap().as_bool().unwrap(), content="true")
|
|
inspect(plain_obj.get("append").unwrap().as_bool().unwrap(), content="false")
|
|
inspect(plain_obj.get("auto_flush").unwrap().as_bool().unwrap(), content="true")
|
|
inspect(plain_obj.get("open_failures").unwrap().as_number().unwrap().to_int(), content="1")
|
|
inspect(plain_obj.get("write_failures").unwrap().as_number().unwrap().to_int(), content="2")
|
|
inspect(plain_obj.get("flush_failures").unwrap().as_number().unwrap().to_int(), content="3")
|
|
inspect(plain_obj.get("rotation_failures").unwrap().as_number().unwrap().to_int(), content="4")
|
|
inspect(plain_rotation_obj.get("max_bytes").unwrap().as_number().unwrap().to_int(), content="64")
|
|
inspect(plain_rotation_obj.get("max_backups").unwrap().as_number().unwrap().to_int(), content="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 runtime_json = runtime_file_state_to_json(
|
|
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,
|
|
),
|
|
)
|
|
let runtime_obj = runtime_json.as_object().unwrap()
|
|
let runtime_file_obj = runtime_obj.get("file").unwrap().as_object().unwrap()
|
|
inspect(runtime_obj.get("queued").unwrap().as_bool().unwrap(), content="true")
|
|
inspect(runtime_obj.get("pending_count").unwrap().as_number().unwrap().to_int(), content="7")
|
|
inspect(runtime_obj.get("dropped_count").unwrap().as_number().unwrap().to_int(), content="5")
|
|
inspect(runtime_file_obj.get("path").unwrap().as_string().unwrap(), content="queue.log")
|
|
inspect(runtime_file_obj.get("available").unwrap().as_bool().unwrap(), content="true")
|
|
inspect(runtime_file_obj.get("rotation").unwrap() is @json_parser.JsonValue::Null, content="true")
|
|
|
|
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" {
|
|
let policy_json = file_sink_policy_to_json(
|
|
FileSinkPolicy::new(
|
|
append=false,
|
|
auto_flush=true,
|
|
rotation=Some(file_rotation(96, max_backups=3)),
|
|
),
|
|
)
|
|
let policy_obj = policy_json.as_object().unwrap()
|
|
let policy_rotation_obj = policy_obj.get("rotation").unwrap().as_object().unwrap()
|
|
inspect(
|
|
@json_parser.stringify(
|
|
policy_json,
|
|
),
|
|
content="{\"append\":false,\"auto_flush\":true,\"rotation\":{\"max_bytes\":96,\"max_backups\":3}}",
|
|
)
|
|
inspect(policy_obj.get("append").unwrap().as_bool().unwrap(), content="false")
|
|
inspect(policy_obj.get("auto_flush").unwrap().as_bool().unwrap(), content="true")
|
|
inspect(policy_rotation_obj.get("max_bytes").unwrap().as_number().unwrap().to_int(), content="96")
|
|
inspect(policy_rotation_obj.get("max_backups").unwrap().as_number().unwrap().to_int(), content="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 file logger mirrors backend file capability" {
|
|
let logger = build_logger(
|
|
LoggerConfig::new(
|
|
sink=SinkConfig::new(kind=SinkKind::File, path="config-capability.log"),
|
|
),
|
|
)
|
|
inspect(logger.file_available() == native_files_supported(), content="true")
|
|
if logger.file_available() {
|
|
inspect(logger.file_state().available, content="true")
|
|
ignore(logger.close())
|
|
} else {
|
|
inspect(logger.file_state().available, content="false")
|
|
inspect(logger.file_flush(), content="false")
|
|
inspect(logger.file_close(), content="false")
|
|
}
|
|
}
|
|
|
|
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")
|
|
}
|
|
}
|
|
|
|
test "library logger keeps a smaller stable facade" {
|
|
let captured_target : Ref[String] = Ref("")
|
|
let captured_message : Ref[String] = Ref("")
|
|
let captured_fields : Ref[Array[Field]] = Ref([])
|
|
let logger = LibraryLogger::new(
|
|
callback_sink(fn(rec) {
|
|
captured_target.val = rec.target
|
|
captured_message.val = rec.message
|
|
captured_fields.val = rec.fields
|
|
}),
|
|
min_level=Level::Info,
|
|
target="svc",
|
|
).with_context_fields([field("service", "bitlogger")]).child("worker")
|
|
logger.info("ready", fields=[field("mode", "test")])
|
|
inspect(captured_target.val, content="svc.worker")
|
|
inspect(captured_message.val, content="ready")
|
|
inspect(captured_fields.val.length(), content="2")
|
|
inspect(captured_fields.val[0].key, content="service")
|
|
inspect(captured_fields.val[1].key, content="mode")
|
|
}
|
|
|
|
test "library logger context binding unwraps to context sink shape" {
|
|
let logger = LibraryLogger::new(console_sink(), min_level=Level::Warn, target="lib.ctx")
|
|
.with_context_fields([field("service", "bitlogger"), field("scope", "sdk")])
|
|
let full = logger.to_logger()
|
|
|
|
inspect(full.target, content="lib.ctx")
|
|
inspect(full.timestamp, content="false")
|
|
inspect(full.is_enabled(Level::Error), content="true")
|
|
inspect(full.is_enabled(Level::Info), content="false")
|
|
inspect(full.sink.context_fields.length(), content="2")
|
|
inspect(full.sink.context_fields[0].key, content="service")
|
|
inspect(full.sink.context_fields[0].value, content="bitlogger")
|
|
inspect(full.sink.context_fields[1].key, content="scope")
|
|
inspect(full.sink.context_fields[1].value, content="sdk")
|
|
}
|
|
|
|
test "library logger bind matches context facade contract" {
|
|
let captured_target : Ref[String] = Ref("")
|
|
let captured_timestamp : Ref[UInt64] = Ref(0UL)
|
|
let captured_fields : Ref[Array[Field]] = Ref([])
|
|
let logger = Logger::new(
|
|
callback_sink(fn(rec) {
|
|
captured_target.val = rec.target
|
|
captured_timestamp.val = rec.timestamp_ms
|
|
captured_fields.val = rec.fields
|
|
}),
|
|
min_level=Level::Warn,
|
|
target="lib.bind",
|
|
)
|
|
.with_timestamp()
|
|
.to_library_logger()
|
|
.bind([field("service", "bitlogger")])
|
|
let full = logger.to_logger()
|
|
|
|
inspect(logger.is_enabled(Level::Error), content="true")
|
|
inspect(logger.is_enabled(Level::Info), content="false")
|
|
inspect(full.target, content="lib.bind")
|
|
inspect(full.timestamp, content="true")
|
|
inspect(full.sink.context_fields.length(), content="1")
|
|
inspect(full.sink.context_fields[0].key, content="service")
|
|
inspect(full.sink.context_fields[0].value, content="bitlogger")
|
|
|
|
logger.error("bound", fields=[field("mode", "test")])
|
|
inspect(captured_target.val, content="lib.bind")
|
|
inspect(captured_timestamp.val > 0UL, content="true")
|
|
inspect(captured_fields.val.length(), content="2")
|
|
inspect(captured_fields.val[0].key, content="service")
|
|
inspect(captured_fields.val[0].value, content="bitlogger")
|
|
inspect(captured_fields.val[1].key, content="mode")
|
|
inspect(captured_fields.val[1].value, content="test")
|
|
}
|
|
|
|
test "library logger with_target preserves facade state while replacing target" {
|
|
let captured_target : Ref[String] = Ref("")
|
|
let captured_timestamp : Ref[UInt64] = Ref(0UL)
|
|
let logger = Logger::new(
|
|
callback_sink(fn(rec) {
|
|
captured_target.val = rec.target
|
|
captured_timestamp.val = rec.timestamp_ms
|
|
}),
|
|
min_level=Level::Warn,
|
|
target="lib.base",
|
|
)
|
|
.with_timestamp()
|
|
.to_library_logger()
|
|
.with_target("lib.retarget")
|
|
let full = logger.to_logger()
|
|
|
|
inspect(logger.is_enabled(Level::Error), content="true")
|
|
inspect(logger.is_enabled(Level::Info), content="false")
|
|
inspect(full.target, content="lib.retarget")
|
|
inspect(full.timestamp, content="true")
|
|
|
|
logger.error("retargeted")
|
|
inspect(captured_target.val, content="lib.retarget")
|
|
inspect(captured_timestamp.val > 0UL, content="true")
|
|
}
|
|
|
|
test "library logger child composes target through facade" {
|
|
let captured_target : Ref[String] = Ref("")
|
|
let captured_timestamp : Ref[UInt64] = Ref(0UL)
|
|
let logger = Logger::new(
|
|
callback_sink(fn(rec) {
|
|
captured_target.val = rec.target
|
|
captured_timestamp.val = rec.timestamp_ms
|
|
}),
|
|
min_level=Level::Warn,
|
|
target="sdk",
|
|
)
|
|
.with_timestamp()
|
|
.to_library_logger()
|
|
.child("worker")
|
|
let full = logger.to_logger()
|
|
|
|
inspect(full.target, content="sdk.worker")
|
|
inspect(full.timestamp, content="true")
|
|
inspect(logger.is_enabled(Level::Error), content="true")
|
|
inspect(logger.is_enabled(Level::Info), content="false")
|
|
|
|
logger.error("child")
|
|
inspect(captured_target.val, content="sdk.worker")
|
|
inspect(captured_timestamp.val > 0UL, content="true")
|
|
|
|
let root_child = LibraryLogger::new(console_sink()).child("worker")
|
|
inspect(root_child.to_logger().target, content="worker")
|
|
|
|
let keep_parent = LibraryLogger::new(console_sink(), target="sdk").child("")
|
|
inspect(keep_parent.to_logger().target, content="sdk")
|
|
}
|
|
|
|
test "library logger log supports per-call target override" {
|
|
let captured_target : Ref[String] = Ref("")
|
|
let captured_message : Ref[String] = Ref("")
|
|
let captured_fields : Ref[Array[Field]] = Ref([])
|
|
let logger = LibraryLogger::new(
|
|
callback_sink(fn(rec) {
|
|
captured_target.val = rec.target
|
|
captured_message.val = rec.message
|
|
captured_fields.val = rec.fields
|
|
}),
|
|
min_level=Level::Warn,
|
|
target="lib.default",
|
|
)
|
|
.with_context_fields([field("service", "bitlogger")])
|
|
|
|
logger.log(
|
|
Level::Error,
|
|
"override",
|
|
fields=[field("mode", "test")],
|
|
target="lib.override",
|
|
)
|
|
inspect(captured_target.val, content="lib.override")
|
|
inspect(captured_message.val, content="override")
|
|
inspect(captured_fields.val.length(), content="2")
|
|
inspect(captured_fields.val[0].key, content="service")
|
|
inspect(captured_fields.val[0].value, content="bitlogger")
|
|
inspect(captured_fields.val[1].key, content="mode")
|
|
inspect(captured_fields.val[1].value, content="test")
|
|
|
|
logger.log(Level::Error, "default")
|
|
inspect(captured_target.val, content="lib.default")
|
|
inspect(captured_message.val, content="default")
|
|
inspect(captured_fields.val.length(), content="1")
|
|
inspect(captured_fields.val[0].key, content="service")
|
|
}
|
|
|
|
test "library logger severity helpers use stored target without override" {
|
|
let captured_levels : Ref[Array[String]] = Ref([])
|
|
let captured_targets : Ref[Array[String]] = Ref([])
|
|
let captured_fields : Ref[Array[Array[Field]]] = Ref([])
|
|
let logger = LibraryLogger::new(
|
|
callback_sink(fn(rec) {
|
|
captured_levels.val.push(rec.level.label())
|
|
captured_targets.val.push(rec.target)
|
|
captured_fields.val.push(rec.fields)
|
|
}),
|
|
min_level=Level::Info,
|
|
target="lib.helpers",
|
|
)
|
|
.bind([field("service", "bitlogger")])
|
|
|
|
logger.info("info", fields=[field("mode", "info")])
|
|
logger.warn("warn", fields=[field("mode", "warn")])
|
|
logger.error("error", fields=[field("mode", "error")])
|
|
|
|
inspect(captured_levels.val.length(), content="3")
|
|
inspect(captured_levels.val[0], content="INFO")
|
|
inspect(captured_levels.val[1], content="WARN")
|
|
inspect(captured_levels.val[2], content="ERROR")
|
|
inspect(captured_targets.val.length(), content="3")
|
|
inspect(captured_targets.val[0], content="lib.helpers")
|
|
inspect(captured_targets.val[1], content="lib.helpers")
|
|
inspect(captured_targets.val[2], content="lib.helpers")
|
|
inspect(captured_fields.val[0].length(), content="2")
|
|
inspect(captured_fields.val[1].length(), content="2")
|
|
inspect(captured_fields.val[2].length(), content="2")
|
|
inspect(captured_fields.val[0][0].key, content="service")
|
|
inspect(captured_fields.val[0][1].value, content="info")
|
|
inspect(captured_fields.val[1][0].key, content="service")
|
|
inspect(captured_fields.val[1][1].value, content="warn")
|
|
inspect(captured_fields.val[2][0].key, content="service")
|
|
inspect(captured_fields.val[2][1].value, content="error")
|
|
}
|
|
|
|
test "library logger can unwrap to full logger when needed" {
|
|
let base = LibraryLogger::new(console_sink(), min_level=Level::Warn, target="lib")
|
|
let full = base.to_logger().with_timestamp()
|
|
inspect(base.is_enabled(Level::Error), content="true")
|
|
inspect(base.is_enabled(Level::Info), content="false")
|
|
inspect(full.timestamp, content="true")
|
|
inspect(full.target, content="lib")
|
|
}
|
|
|
|
test "library logger can be built from configured runtime path" {
|
|
let logger = build_library_logger(
|
|
LoggerConfig::new(
|
|
min_level=Level::Warn,
|
|
target="lib.config",
|
|
sink=SinkConfig::new(kind=SinkKind::Console),
|
|
),
|
|
)
|
|
let full = logger.to_logger()
|
|
inspect(logger.is_enabled(Level::Error), content="true")
|
|
inspect(logger.is_enabled(Level::Info), content="false")
|
|
inspect(full.target, content="lib.config")
|
|
}
|
|
|
|
test "library logger can be parsed and built from config text" {
|
|
let logger = parse_and_build_library_logger(
|
|
"{\"min_level\":\"warn\",\"target\":\"lib.json\",\"sink\":{\"kind\":\"console\"}}",
|
|
)
|
|
let full = logger.to_logger()
|
|
inspect(logger.is_enabled(Level::Error), content="true")
|
|
inspect(logger.is_enabled(Level::Info), content="false")
|
|
inspect(full.target, content="lib.json")
|
|
}
|
|
|
|
test "parsed library logger unwrap preserves configured runtime path" {
|
|
let logger = parse_and_build_library_logger(
|
|
"{\"min_level\":\"warn\",\"target\":\"lib.json.runtime\",\"timestamp\":true,\"queue\":{\"max_pending\":3,\"overflow\":\"DropNewest\"},\"sink\":{\"kind\":\"console\"}}",
|
|
)
|
|
let full = logger.to_logger()
|
|
|
|
inspect(logger.is_enabled(Level::Error), content="true")
|
|
inspect(logger.is_enabled(Level::Info), content="false")
|
|
inspect(full.target, content="lib.json.runtime")
|
|
inspect(full.timestamp, content="true")
|
|
|
|
full.error("one")
|
|
full.error("two")
|
|
inspect(match full.sink {
|
|
RuntimeSink::QueuedConsole(_) => "QueuedConsole"
|
|
RuntimeSink::QueuedJsonConsole(_) => "QueuedJsonConsole"
|
|
RuntimeSink::QueuedTextConsole(_) => "QueuedTextConsole"
|
|
RuntimeSink::QueuedFile(_) => "QueuedFile"
|
|
_ => "Other"
|
|
}, content="QueuedConsole")
|
|
inspect(full.sink.pending_count(), content="2")
|
|
inspect(full.sink.dropped_count(), content="0")
|
|
inspect(full.sink.drain(max_items=1), content="1")
|
|
inspect(full.sink.pending_count(), content="1")
|
|
inspect(full.sink.flush(), content="1")
|
|
inspect(full.sink.pending_count(), content="0")
|
|
}
|
|
|
|
test "configured logger can project to library logger" {
|
|
let configured = build_logger(
|
|
LoggerConfig::new(min_level=Level::Warn, target="lib.projected"),
|
|
)
|
|
let logger = configured.to_library_logger()
|
|
inspect(logger.is_enabled(Level::Error), content="true")
|
|
inspect(logger.is_enabled(Level::Info), content="false")
|
|
inspect(logger.to_logger().target, content="lib.projected")
|
|
}
|
|
|
|
test "logger projection preserves sync composition state through library facade" {
|
|
let captured_target : Ref[String] = Ref("")
|
|
let captured_timestamp : Ref[UInt64] = Ref(0UL)
|
|
let captured_fields : Ref[Array[Field]] = Ref([])
|
|
let base = Logger::new(
|
|
callback_sink(fn(rec) {
|
|
captured_target.val = rec.target
|
|
captured_timestamp.val = rec.timestamp_ms
|
|
captured_fields.val = rec.fields
|
|
}),
|
|
min_level=Level::Warn,
|
|
target="lib.projected.state",
|
|
)
|
|
.with_timestamp()
|
|
.with_context_fields([field("service", "bitlogger")])
|
|
let projected = base.to_library_logger()
|
|
let full = projected.to_logger()
|
|
|
|
inspect(projected.is_enabled(Level::Error), content="true")
|
|
inspect(projected.is_enabled(Level::Info), content="false")
|
|
inspect(full.target, content="lib.projected.state")
|
|
inspect(full.timestamp, content="true")
|
|
full.error("boom", fields=[field("mode", "test")])
|
|
inspect(captured_target.val, content="lib.projected.state")
|
|
inspect(captured_timestamp.val > 0UL, content="true")
|
|
inspect(captured_fields.val.length(), content="2")
|
|
inspect(captured_fields.val[0].key, content="service")
|
|
inspect(captured_fields.val[0].value, content="bitlogger")
|
|
inspect(captured_fields.val[1].key, content="mode")
|
|
}
|
|
|
|
test "configured logger projection preserves runtime helpers through unwrap" {
|
|
let projected = build_logger(
|
|
LoggerConfig::new(
|
|
min_level=Level::Warn,
|
|
target="lib.projected.runtime",
|
|
sink=SinkConfig::new(kind=SinkKind::Console),
|
|
queue=Some(QueueConfig::new(3, overflow=QueueOverflowPolicy::DropNewest)),
|
|
),
|
|
).to_library_logger()
|
|
let full = projected.to_logger()
|
|
|
|
inspect(projected.is_enabled(Level::Error), content="true")
|
|
inspect(projected.is_enabled(Level::Info), content="false")
|
|
inspect(full.target, content="lib.projected.runtime")
|
|
full.error("one")
|
|
full.error("two")
|
|
inspect(match full.sink {
|
|
RuntimeSink::QueuedConsole(_) => "QueuedConsole"
|
|
RuntimeSink::QueuedJsonConsole(_) => "QueuedJsonConsole"
|
|
RuntimeSink::QueuedTextConsole(_) => "QueuedTextConsole"
|
|
RuntimeSink::QueuedFile(_) => "QueuedFile"
|
|
_ => "Other"
|
|
}, content="QueuedConsole")
|
|
inspect(full.sink.pending_count(), content="2")
|
|
inspect(full.sink.dropped_count(), content="0")
|
|
inspect(full.sink.drain(max_items=1), content="1")
|
|
inspect(full.sink.pending_count(), content="1")
|
|
inspect(full.sink.flush(), content="1")
|
|
inspect(full.sink.pending_count(), content="0")
|
|
}
|
|
|
|
test "default library logger mirrors shared sync defaults" {
|
|
set_default_min_level(Level::Warn)
|
|
set_default_target("lib.default")
|
|
let logger = default_library_logger()
|
|
inspect(logger.is_enabled(Level::Error), content="true")
|
|
inspect(logger.is_enabled(Level::Info), content="false")
|
|
inspect(logger.to_logger().target, content="lib.default")
|
|
}
|
|
|
|
test "default library logger snapshots shared defaults at creation time" {
|
|
set_default_min_level(Level::Warn)
|
|
set_default_target("lib.before")
|
|
let before = default_library_logger()
|
|
|
|
set_default_min_level(Level::Debug)
|
|
set_default_target("lib.after")
|
|
let after = default_library_logger()
|
|
|
|
inspect(before.is_enabled(Level::Error), content="true")
|
|
inspect(before.is_enabled(Level::Info), content="false")
|
|
inspect(before.to_logger().target, content="lib.before")
|
|
|
|
inspect(after.is_enabled(Level::Error), content="true")
|
|
inspect(after.is_enabled(Level::Info), content="true")
|
|
inspect(after.to_logger().target, content="lib.after")
|
|
}
|
|
|
|
test "application logger aliases configured runtime entry" {
|
|
let logger = build_application_logger(
|
|
LoggerConfig::new(min_level=Level::Warn, target="app.runtime"),
|
|
)
|
|
inspect(logger.is_enabled(Level::Error), content="true")
|
|
inspect(logger.is_enabled(Level::Info), content="false")
|
|
inspect(logger.target, content="app.runtime")
|
|
}
|
|
|
|
test "configured logger keeps broader logger composition surface" {
|
|
let logger : ConfiguredLogger = build_logger(
|
|
LoggerConfig::new(
|
|
min_level=Level::Warn,
|
|
target="configured.compose",
|
|
queue=Some(QueueConfig::new(2, overflow=QueueOverflowPolicy::DropNewest)),
|
|
sink=SinkConfig::new(kind=SinkKind::Console),
|
|
),
|
|
)
|
|
let derived = logger.with_timestamp().child("worker")
|
|
|
|
inspect(derived.target, content="configured.compose.worker")
|
|
inspect(derived.timestamp, content="true")
|
|
inspect(derived.is_enabled(Level::Error), content="true")
|
|
inspect(derived.is_enabled(Level::Info), content="false")
|
|
inspect(match derived.sink {
|
|
RuntimeSink::QueuedConsole(_) => "QueuedConsole"
|
|
RuntimeSink::QueuedJsonConsole(_) => "QueuedJsonConsole"
|
|
RuntimeSink::QueuedTextConsole(_) => "QueuedTextConsole"
|
|
RuntimeSink::QueuedFile(_) => "QueuedFile"
|
|
_ => "Other"
|
|
}, content="QueuedConsole")
|
|
|
|
derived.error("one")
|
|
derived.error("two")
|
|
inspect(derived.pending_count(), content="2")
|
|
inspect(derived.dropped_count(), content="0")
|
|
inspect(derived.flush(), content="2")
|
|
}
|
|
|
|
test "application logger keeps configured runtime helper surface" {
|
|
let logger : ApplicationLogger = build_application_logger(
|
|
LoggerConfig::new(
|
|
min_level=Level::Warn,
|
|
target="app.alias.helpers",
|
|
sink=SinkConfig::new(kind=SinkKind::File, path="app-alias.log"),
|
|
queue=Some(QueueConfig::new(2, overflow=QueueOverflowPolicy::DropOldest)),
|
|
),
|
|
)
|
|
|
|
logger.error("one")
|
|
logger.error("two")
|
|
logger.error("three")
|
|
inspect(logger.pending_count(), content="2")
|
|
inspect(logger.dropped_count(), content="1")
|
|
inspect(logger.file_policy().append, content="true")
|
|
inspect(logger.file_runtime_state() is None, content="false")
|
|
inspect(logger.flush(), content="2")
|
|
inspect(logger.pending_count(), content="0")
|
|
|
|
if logger.file_available() {
|
|
inspect(logger.file_flush(), content="true")
|
|
inspect(logger.file_close(), content="true")
|
|
} else {
|
|
inspect(logger.file_flush(), content="false")
|
|
inspect(logger.file_close(), content="false")
|
|
}
|
|
}
|
|
|
|
test "application logger keeps broader logger composition surface" {
|
|
let logger : ApplicationLogger = build_application_logger(
|
|
LoggerConfig::new(
|
|
min_level=Level::Warn,
|
|
target="app.compose",
|
|
queue=Some(QueueConfig::new(2, overflow=QueueOverflowPolicy::DropNewest)),
|
|
sink=SinkConfig::new(kind=SinkKind::Console),
|
|
),
|
|
)
|
|
let derived = logger.with_timestamp().child("worker")
|
|
|
|
inspect(derived.target, content="app.compose.worker")
|
|
inspect(derived.timestamp, content="true")
|
|
inspect(derived.is_enabled(Level::Error), content="true")
|
|
inspect(derived.is_enabled(Level::Info), content="false")
|
|
inspect(match derived.sink {
|
|
RuntimeSink::QueuedConsole(_) => "QueuedConsole"
|
|
RuntimeSink::QueuedJsonConsole(_) => "QueuedJsonConsole"
|
|
RuntimeSink::QueuedTextConsole(_) => "QueuedTextConsole"
|
|
RuntimeSink::QueuedFile(_) => "QueuedFile"
|
|
_ => "Other"
|
|
}, content="QueuedConsole")
|
|
|
|
derived.error("one")
|
|
derived.error("two")
|
|
inspect(derived.pending_count(), content="2")
|
|
inspect(derived.dropped_count(), content="0")
|
|
inspect(derived.flush(), content="2")
|
|
inspect(derived.pending_count(), content="0")
|
|
}
|
|
|
|
test "application logger can be built from config text" {
|
|
let logger = parse_and_build_application_logger(
|
|
"{\"min_level\":\"warn\",\"target\":\"app.json\",\"sink\":{\"kind\":\"console\"}}",
|
|
)
|
|
inspect(logger.is_enabled(Level::Error), content="true")
|
|
inspect(logger.is_enabled(Level::Info), content="false")
|
|
inspect(logger.target, content="app.json")
|
|
}
|
|
|
|
test "parsed application logger keeps direct runtime helper surface" {
|
|
let logger : ApplicationLogger = parse_and_build_application_logger(
|
|
"{\"min_level\":\"warn\",\"target\":\"app.json.helpers\",\"queue\":{\"max_pending\":2,\"overflow\":\"DropOldest\"},\"sink\":{\"kind\":\"file\",\"path\":\"app-json-helpers.log\"}}",
|
|
)
|
|
|
|
logger.error("one")
|
|
logger.error("two")
|
|
logger.error("three")
|
|
inspect(logger.pending_count(), content="2")
|
|
inspect(logger.dropped_count(), content="1")
|
|
inspect(logger.file_policy().append, content="true")
|
|
inspect(logger.file_runtime_state() is None, content="false")
|
|
inspect(logger.flush(), content="2")
|
|
inspect(logger.pending_count(), content="0")
|
|
|
|
if logger.file_available() {
|
|
inspect(logger.file_flush(), content="true")
|
|
inspect(logger.file_close(), content="true")
|
|
} else {
|
|
inspect(logger.file_flush(), content="false")
|
|
inspect(logger.file_close(), content="false")
|
|
}
|
|
}
|
|
|
|
test "parsed configured logger keeps composition and runtime helper surface" {
|
|
let logger : ConfiguredLogger = parse_and_build_logger(
|
|
"{\"min_level\":\"warn\",\"target\":\"parsed.compose\",\"timestamp\":true,\"queue\":{\"max_pending\":2,\"overflow\":\"DropNewest\"},\"sink\":{\"kind\":\"console\"}}",
|
|
)
|
|
let derived = logger.child("worker")
|
|
|
|
inspect(derived.target, content="parsed.compose.worker")
|
|
inspect(derived.timestamp, content="true")
|
|
inspect(derived.is_enabled(Level::Error), content="true")
|
|
inspect(derived.is_enabled(Level::Info), content="false")
|
|
inspect(match derived.sink {
|
|
RuntimeSink::QueuedConsole(_) => "QueuedConsole"
|
|
RuntimeSink::QueuedJsonConsole(_) => "QueuedJsonConsole"
|
|
RuntimeSink::QueuedTextConsole(_) => "QueuedTextConsole"
|
|
RuntimeSink::QueuedFile(_) => "QueuedFile"
|
|
_ => "Other"
|
|
}, content="QueuedConsole")
|
|
|
|
derived.error("one")
|
|
derived.error("two")
|
|
inspect(derived.pending_count(), content="2")
|
|
inspect(derived.dropped_count(), content="0")
|
|
inspect(derived.flush(), content="2")
|
|
inspect(derived.pending_count(), content="0")
|
|
}
|
|
|
|
test "sync parse-build facades forward config errors" {
|
|
let raw = "{\"sink\":{\"kind\":\"file\",\"path\":\"\"}}"
|
|
|
|
let configured_error = (fn() -> String raise ConfigError {
|
|
ignore(parse_and_build_logger(raw))
|
|
"no error"
|
|
})() catch {
|
|
ConfigError::InvalidConfig(message) => message
|
|
}
|
|
inspect(configured_error, content="File sink requires non-empty path")
|
|
|
|
let application_error = (fn() -> String raise ConfigError {
|
|
ignore(parse_and_build_application_logger(raw))
|
|
"no error"
|
|
})() catch {
|
|
ConfigError::InvalidConfig(message) => message
|
|
}
|
|
inspect(application_error, content="File sink requires non-empty path")
|
|
|
|
let library_error = (fn() -> String raise ConfigError {
|
|
ignore(parse_and_build_library_logger(raw))
|
|
"no error"
|
|
})() catch {
|
|
ConfigError::InvalidConfig(message) => message
|
|
}
|
|
inspect(library_error, content="File sink requires non-empty path")
|
|
}
|