diff --git a/src/BitLogger_test.mbt b/src/BitLogger_test.mbt index f971621..17d581a 100644 --- a/src/BitLogger_test.mbt +++ b/src/BitLogger_test.mbt @@ -28,6 +28,40 @@ test "fields helper builds field arrays ergonomically" { inspect(items[1].value, content="test") } +///| +test "default logger can be reconfigured" { + set_default_min_level(Level::Debug) + set_default_target("global") + let logger = default_logger() + inspect(logger.min_level.label(), content="DEBUG") + inspect(logger.target, content="global") +} + +///| +test "default logger snapshots shared defaults at creation time" { + set_default_min_level(Level::Warn) + set_default_target("before") + let before = default_logger() + + set_default_min_level(Level::Debug) + set_default_target("after") + let after = default_logger() + + inspect(before.is_enabled(Level::Error), content="true") + inspect(before.is_enabled(Level::Info), content="false") + inspect(before.target, content="before") + + inspect(after.is_enabled(Level::Error), content="true") + inspect(after.is_enabled(Level::Info), content="true") + inspect(after.target, content="after") +} + +///| +test "logger can enable timestamps" { + let logger = Logger::new(console_sink(), min_level=Level::Info, target="time").with_timestamp() + inspect(logger.timestamp, content="true") +} + ///| test "fanout sink can write to plain and json outputs" { let logger = Logger::new( @@ -47,3024 +81,6 @@ test "child logger composes target path" { 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") - inspect(rotation.native_wide_max_bytes is None, content="true") - } - None => inspect(false, content="true") - } -} - -///| -test "logger config parser prefers max_bytes_i64 when present" { - let config = parse_logger_config_text( - "{\"sink\":{\"kind\":\"file\",\"path\":\"bitlogger.log\",\"rotation\":{\"max_bytes\":128,\"max_bytes_i64\":\"4294967296\",\"max_backups\":3}}}", - ) - inspect(config.sink.path, content="bitlogger.log") - match config.sink.rotation { - Some(rotation) => { - inspect(rotation.max_bytes, content="2147483647") - inspect(rotation.max_backups, content="3") - inspect(rotation.native_wide_max_bytes.unwrap(), content="4294967296") - } - 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") - inspect(rotation.native_wide_max_bytes is None, content="true") - } - None => inspect(false, content="true") - } -} - -///| -test "logger config stringify roundtrips file rotation i64 metadata" { - let text = stringify_logger_config( - LoggerConfig::new( - sink=SinkConfig::new( - kind=SinkKind::File, - path="bitlogger.log", - rotation=Some(file_rotation_i64(4294967296L, 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="2147483647") - inspect(rotation.max_backups, content="2") - inspect(rotation.native_wide_max_bytes.unwrap(), content="4294967296") - } - 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}}", - ) - inspect( - @json_parser.stringify( - file_rotation_config_to_json( - file_rotation_i64(4294967296L, max_backups=2), - ), - ), - content="{\"max_bytes\":2147483647,\"max_backups\":2,\"max_bytes_i64\":\"4294967296\"}", - ) -} - -///| -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", - ) - inspect(rotation_obj.get("max_bytes_i64") is None, content="true") - - let wide_rotation_json = file_rotation_config_to_json( - file_rotation_i64(4294967296L, max_backups=3), - ) - let wide_rotation_obj = wide_rotation_json.as_object().unwrap() - inspect( - wide_rotation_obj.get("max_bytes").unwrap().as_number().unwrap().to_int(), - content="2147483647", - ) - inspect( - wide_rotation_obj.get("max_backups").unwrap().as_number().unwrap().to_int(), - content="3", - ) - inspect( - wide_rotation_obj.get("max_bytes_i64").unwrap().as_string().unwrap(), - content="4294967296", - ) - - 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"), - 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, "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, "custom builtin"), - formatter=formatter.to_formatter(), - ) - inspect(rendered, content="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, "raw tag"), - formatter=formatter.to_formatter(), - ) - inspect(rendered, content="raw 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, - "message stays raw", - target="svc", - fields=[field("status", "ok")], - ), - formatter=formatter.to_formatter(), - ) - inspect( - rendered, - content="[\u{001b}[34m\u{001b}[31;1msvc\u{001b}[0m\u{001b}[0m] 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 queue helpers stay aligned with runtime sink helpers" { - let logger = build_logger( - LoggerConfig::new( - min_level=Level::Info, - target="config.delegate", - sink=SinkConfig::new( - kind=SinkKind::TextConsole, - text_formatter=TextFormatterConfig::new( - show_timestamp=false, - show_target=false, - ), - ), - queue=Some(QueueConfig::new(2, overflow=QueueOverflowPolicy::DropOldest)), - ), - ) - let sink = logger.sink - - logger.info("one") - logger.info("two") - logger.info("three") - - inspect(logger.pending_count() == sink.pending_count(), content="true") - inspect(logger.dropped_count() == sink.dropped_count(), content="true") - inspect(logger.drain(max_items=1) == sink.drain(max_items=1), content="true") - inspect(logger.pending_count() == sink.pending_count(), content="true") - inspect(logger.flush() == sink.flush(), content="true") - inspect(logger.pending_count() == sink.pending_count(), content="true") - inspect(logger.dropped_count() == sink.dropped_count(), content="true") -} - -///| -test "configured logger progress helpers stay aligned with runtime sink helpers" { - let logger = build_logger( - LoggerConfig::new( - min_level=Level::Info, - target="config.progress.delegate", - sink=SinkConfig::new( - kind=SinkKind::TextConsole, - text_formatter=TextFormatterConfig::new( - show_timestamp=false, - show_target=false, - ), - ), - queue=Some(QueueConfig::new(3, overflow=QueueOverflowPolicy::DropOldest)), - ), - ) - let sink = logger.sink - - logger.info("one") - logger.info("two") - logger.info("three") - - let logger_drain = logger.drain_progress(max_items=1) - inspect(logger_drain.queue_advanced_count, content="1") - inspect(logger_drain.file_flush_step_count, content="0") - inspect(logger_drain.queue_backed, content="true") - inspect(logger_drain.file_backed, content="false") - - let sink_drain = sink.drain_progress(max_items=1) - inspect(sink_drain.queue_advanced_count, content="1") - inspect(sink_drain.file_flush_step_count, content="0") - inspect(sink_drain.queue_backed, content="true") - inspect(sink_drain.file_backed, content="false") - - let logger_flush = logger.flush_progress() - inspect(logger_flush.queue_advanced_count, content="1") - inspect(logger_flush.file_flush_step_count, content="0") - inspect(logger_flush.queue_backed, content="true") - inspect(logger_flush.file_backed, content="false") - - let sink_flush = sink.flush_progress() - inspect(sink_flush.queue_advanced_count, content="0") - inspect(sink_flush.file_flush_step_count, content="0") - inspect(sink_flush.queue_backed, content="true") - inspect(sink_flush.file_backed, content="false") -} - -///| -test "configured logger close stays aligned with runtime sink close" { - let console_config = LoggerConfig::new( - min_level=Level::Info, - target="config.close.console", - sink=SinkConfig::new(kind=SinkKind::Console), - queue=Some(QueueConfig::new(2, overflow=QueueOverflowPolicy::DropNewest)), - ) - let console_logger = build_logger(console_config) - let console_sink = build_logger(console_config).sink - console_logger.info("one") - console_logger.info("two") - inspect(console_logger.pending_count(), content="2") - inspect(console_logger.close() == console_sink.close(), content="true") - inspect(console_logger.pending_count(), content="0") - - let file_config = LoggerConfig::new( - target="config.close.file", - sink=SinkConfig::new(kind=SinkKind::File, path="config-close.log"), - ) - let file_logger = build_logger(file_config) - let file_sink = build_logger(file_config).sink - inspect(file_logger.close() == file_sink.close(), content="true") -} - -///| -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 "queued console close drains pending queue before reporting success" { - let sink = RuntimeSink::QueuedConsole( - queued_sink(console_sink(), max_pending=4, overflow=QueueOverflowPolicy::DropNewest), - ) - let logger = Logger::new(sink, min_level=Level::Info, target="runtime.close.console") - logger.info("one") - logger.info("two") - inspect(sink.pending_count(), content="2") - inspect(sink.close(), content="true") - inspect(sink.pending_count(), content="0") - inspect(sink.flush(), content="0") -} - -///| -test "queued json console close drains pending queue before reporting success" { - let sink = RuntimeSink::QueuedJsonConsole( - queued_sink(json_console_sink(), max_pending=4, overflow=QueueOverflowPolicy::DropNewest), - ) - let logger = Logger::new(sink, min_level=Level::Info, target="runtime.close.json") - logger.info("one") - logger.info("two") - inspect(sink.pending_count(), content="2") - inspect(sink.close(), content="true") - inspect(sink.pending_count(), content="0") - inspect(sink.drain(), content="0") -} - -///| -test "queued text console close drains pending queue before reporting success" { - let sink = RuntimeSink::QueuedTextConsole( - queued_sink( - text_console_sink(text_formatter(show_timestamp=false, show_target=false)), - max_pending=4, - overflow=QueueOverflowPolicy::DropNewest, - ), - ) - let logger = Logger::new(sink, min_level=Level::Info, target="runtime.close.text") - logger.info("one") - logger.info("two") - inspect(sink.pending_count(), content="2") - inspect(sink.close(), content="true") - inspect(sink.pending_count(), content="0") - inspect(sink.flush(), content="0") -} - -///| -test "configured queued console close drains pending queue" { - let logger = build_logger( - LoggerConfig::new( - min_level=Level::Info, - target="config.close.queued.console", - sink=SinkConfig::new(kind=SinkKind::Console), - queue=Some(QueueConfig::new(4, overflow=QueueOverflowPolicy::DropNewest)), - ), - ) - logger.info("one") - logger.info("two") - inspect(logger.pending_count(), content="2") - inspect(logger.close(), content="true") - inspect(logger.pending_count(), content="0") -} - -///| -test "runtime sink queued file drain counts queue consumption separately from file write success" { - let sink = RuntimeSink::QueuedFile( - queued_sink( - file_sink("missing-dir/runtime-drain-unavailable.log", auto_flush=false), - max_pending=4, - ), - ) - let logger = Logger::new( - sink, - min_level=Level::Info, - target="runtime.queue.file.unavailable", - ) - logger.info("one") - logger.info("two") - inspect(sink.file_available(), content="false") - inspect(sink.pending_count(), content="2") - inspect(sink.file_write_failures(), content="0") - inspect(sink.drain(max_items=1), content="1") - inspect(sink.pending_count(), content="1") - inspect(sink.file_write_failures(), content="1") - inspect(sink.flush(), content="1") - inspect(sink.pending_count(), content="0") - inspect(sink.file_write_failures(), content="2") -} - -///| -test "runtime sink queued file progress helpers stay distinct from file flush outcome" { - let sink = RuntimeSink::QueuedFile( - queued_sink( - file_sink("missing-dir/runtime-progress-unavailable.log", auto_flush=false), - max_pending=4, - ), - ) - let logger = Logger::new( - sink, - min_level=Level::Info, - target="runtime.queue.file.progress.unavailable", - ) - logger.info("one") - logger.info("two") - - let drained = sink.drain_progress(max_items=1) - inspect(drained.queue_advanced_count, content="1") - inspect(drained.file_flush_step_count, content="0") - inspect(drained.queue_backed, content="true") - inspect(drained.file_backed, content="true") - inspect(sink.pending_count(), content="1") - inspect(sink.file_write_failures(), content="1") - - let flushed = sink.flush_progress() - inspect(flushed.queue_advanced_count, content="1") - inspect(flushed.file_flush_step_count, content="0") - inspect(flushed.queue_backed, content="true") - inspect(flushed.file_backed, content="true") - inspect(sink.pending_count(), content="0") - inspect(sink.file_write_failures(), content="2") - inspect(sink.file_flush(), content="false") -} - -///| -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 progress helpers expose queue and plain file steps separately" { - let console_sink = RuntimeSink::Console(console_sink()) - let console_progress = console_sink.flush_progress() - inspect(console_progress.queue_advanced_count, content="0") - inspect(console_progress.file_flush_step_count, content="0") - inspect(console_progress.queue_backed, content="false") - inspect(console_progress.file_backed, content="false") - - let queued_sink = RuntimeSink::QueuedTextConsole( - queued_sink( - text_console_sink(text_formatter(show_timestamp=false, show_target=false)), - max_pending=4, - overflow=QueueOverflowPolicy::DropNewest, - ), - ) - let queued_logger = Logger::new( - queued_sink, - min_level=Level::Info, - target="runtime.progress.queue.text", - ) - queued_logger.info("one") - let queued_progress = queued_sink.flush_progress() - inspect(queued_progress.queue_advanced_count, content="1") - inspect(queued_progress.file_flush_step_count, content="0") - inspect(queued_progress.queue_backed, content="true") - inspect(queued_progress.file_backed, content="false") - - let file_sink = RuntimeSink::File( - file_sink("runtime-progress-direct.log", auto_flush=false), - ) - let file_progress = file_sink.flush_progress() - inspect(file_progress.queue_advanced_count, content="0") - inspect(file_progress.queue_backed, content="false") - inspect(file_progress.file_backed, content="true") - if file_sink.file_available() { - inspect(file_progress.file_flush_step_count, content="1") - inspect(file_sink.close(), content="true") - } else { - inspect(file_progress.file_flush_step_count, 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_or_none() is None, content="true") - 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") - inspect(sink.file_policy_or_none() is None, content="true") - inspect(sink.file_default_policy_or_none() is None, content="true") - let policy = sink.file_policy() - let defaults = sink.file_default_policy() - inspect(sink.file_state_or_none() is None, content="true") - 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") - match sink.file_path_or_none() { - Some(path) => inspect(path, content="runtime-file-helpers.log") - None => inspect(false, 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") - match sink.file_default_policy_or_none() { - Some(policy) => { - inspect(policy.append, content="true") - inspect(policy.auto_flush, content="false") - } - None => inspect(false, content="true") - } - match sink.file_policy_or_none() { - Some(policy) => { - inspect(policy.append, content="true") - inspect(policy.auto_flush, content="false") - } - None => inspect(false, 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") - } - match sink.file_state_or_none() { - Some(snapshot) => { - inspect(snapshot.path, content="runtime-file-helpers.log") - inspect(snapshot.append, content="true") - inspect(snapshot.auto_flush, content="false") - } - 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") - match sink.file_path_or_none() { - Some(path) => inspect(path, content="runtime-queued-file.log") - None => inspect(false, content="true") - } - inspect(sink.file_path(), content="runtime-queued-file.log") - inspect(sink.file_policy_or_none() is None, content="false") - inspect(sink.file_default_policy_or_none() is None, content="false") - inspect(sink.file_state_or_none() is None, content="false") - 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="false") - inspect(sink.file_append_mode(), content="true") - inspect(sink.file_set_append_mode(false), content="false") - inspect(sink.file_append_mode(), content="true") - inspect( - sink.file_set_policy( - FileSinkPolicy::new(append=false, auto_flush=false, rotation=None), - ), - content="false", - ) - inspect(sink.pending_count(), content="2") - inspect(sink.file_flush(), content="true") - inspect(sink.pending_count(), 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.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_set_append_mode(false), content="false") - inspect(sink.file_append_mode(), content="true") - 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 "runtime sink queued file close drains queue before teardown" { - let sink = RuntimeSink::QueuedFile( - queued_sink(file_sink("runtime-close-queued.log", auto_flush=false), max_pending=4), - ) - let logger = Logger::new( - sink, - min_level=Level::Info, - target="runtime.close.queued", - ) - logger.info("one") - logger.info("two") - inspect(sink.pending_count(), content="2") - if sink.file_available() { - inspect(sink.close(), content="true") - inspect(sink.pending_count(), content="0") - inspect(sink.file_available(), content="false") - inspect(sink.file_close(), content="false") - } else { - inspect(sink.close(), content="false") - inspect(sink.pending_count(), content="0") - inspect(sink.file_available(), content="false") - inspect(sink.file_write_failures(), content="2") - inspect(sink.file_close(), content="false") - } -} - -///| -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 "configured logger file observation helpers stay aligned with runtime sink helpers" { - let file_logger = build_logger( - LoggerConfig::new( - target="config.file.delegate", - sink=SinkConfig::new(kind=SinkKind::File, path="config-file-delegate.log"), - ), - ) - let file_sink = file_logger.sink - - inspect( - file_logger.file_available() == file_sink.file_available(), - content="true", - ) - inspect(file_logger.file_path() == file_sink.file_path(), content="true") - inspect( - file_logger.file_open_failures() == file_sink.file_open_failures(), - content="true", - ) - inspect( - file_logger.file_write_failures() == file_sink.file_write_failures(), - content="true", - ) - inspect( - file_logger.file_flush_failures() == file_sink.file_flush_failures(), - content="true", - ) - inspect( - file_logger.file_rotation_failures() == file_sink.file_rotation_failures(), - content="true", - ) - - let console_logger = build_logger( - LoggerConfig::new( - target="config.file.delegate.console", - sink=SinkConfig::new(kind=SinkKind::Console), - ), - ) - let console_sink = console_logger.sink - - inspect( - console_logger.file_available() == console_sink.file_available(), - content="true", - ) - inspect( - console_logger.file_path() == console_sink.file_path(), - content="true", - ) - inspect( - console_logger.file_open_failures() == console_sink.file_open_failures(), - content="true", - ) - inspect( - console_logger.file_write_failures() == console_sink.file_write_failures(), - content="true", - ) - inspect( - console_logger.file_flush_failures() == console_sink.file_flush_failures(), - content="true", - ) - inspect( - console_logger.file_rotation_failures() == - console_sink.file_rotation_failures(), - content="true", - ) - - ignore(file_logger.close()) -} - -///| -test "configured logger file policy helpers stay aligned with runtime sink helpers" { - let file_logger = build_logger( - LoggerConfig::new( - target="config.file.policy.delegate", - sink=SinkConfig::new( - kind=SinkKind::File, - path="config-file-policy-delegate.log", - append=false, - auto_flush=true, - rotation=Some(file_rotation(64, max_backups=2)), - ), - ), - ) - let file_sink = file_logger.sink - let file_policy = file_logger.file_policy() - let file_sink_policy = file_sink.file_policy() - let file_defaults = file_logger.file_default_policy() - let file_sink_defaults = file_sink.file_default_policy() - let file_policy_has_rotation = match file_policy.rotation { - Some(_) => true - None => false - } - let file_sink_policy_has_rotation = match file_sink_policy.rotation { - Some(_) => true - None => false - } - let file_defaults_has_rotation = match file_defaults.rotation { - Some(_) => true - None => false - } - let file_sink_defaults_has_rotation = match file_sink_defaults.rotation { - Some(_) => true - None => false - } - - inspect( - file_logger.file_append_mode() == file_sink.file_append_mode(), - content="true", - ) - inspect( - file_logger.file_auto_flush() == file_sink.file_auto_flush(), - content="true", - ) - inspect( - file_logger.file_rotation_enabled() == file_sink.file_rotation_enabled(), - content="true", - ) - inspect( - file_logger.file_policy_matches_default() == - file_sink.file_policy_matches_default(), - content="true", - ) - inspect(file_policy.append == file_sink_policy.append, content="true") - inspect(file_policy.auto_flush == file_sink_policy.auto_flush, content="true") - inspect( - file_policy_has_rotation == file_sink_policy_has_rotation, - content="true", - ) - inspect(file_defaults.append == file_sink_defaults.append, content="true") - inspect( - file_defaults.auto_flush == file_sink_defaults.auto_flush, - content="true", - ) - inspect( - file_defaults_has_rotation == file_sink_defaults_has_rotation, - content="true", - ) - - let console_logger = build_logger( - LoggerConfig::new( - target="config.file.policy.console", - sink=SinkConfig::new(kind=SinkKind::Console), - ), - ) - let console_sink = console_logger.sink - let console_policy = console_logger.file_policy() - let console_sink_policy = console_sink.file_policy() - let console_defaults = console_logger.file_default_policy() - let console_sink_defaults = console_sink.file_default_policy() - let console_policy_has_rotation = match console_policy.rotation { - Some(_) => true - None => false - } - let console_sink_policy_has_rotation = match console_sink_policy.rotation { - Some(_) => true - None => false - } - let console_defaults_has_rotation = match console_defaults.rotation { - Some(_) => true - None => false - } - let console_sink_defaults_has_rotation = match - console_sink_defaults.rotation { - Some(_) => true - None => false - } - - inspect( - console_logger.file_append_mode() == console_sink.file_append_mode(), - content="true", - ) - inspect( - console_logger.file_auto_flush() == console_sink.file_auto_flush(), - content="true", - ) - inspect( - console_logger.file_rotation_enabled() == - console_sink.file_rotation_enabled(), - content="true", - ) - inspect( - console_logger.file_policy_matches_default() == - console_sink.file_policy_matches_default(), - content="true", - ) - inspect(console_policy.append == console_sink_policy.append, content="true") - inspect( - console_policy.auto_flush == console_sink_policy.auto_flush, - content="true", - ) - inspect( - console_policy_has_rotation == console_sink_policy_has_rotation, - content="true", - ) - inspect( - console_defaults.append == console_sink_defaults.append, - content="true", - ) - inspect( - console_defaults.auto_flush == console_sink_defaults.auto_flush, - content="true", - ) - inspect( - console_defaults_has_rotation == console_sink_defaults_has_rotation, - content="true", - ) - - ignore(file_logger.close()) -} - -///| -test "configured logger file setters stay aligned with runtime sink setters" { - let logger = build_logger( - LoggerConfig::new( - target="config.file.setters.delegate", - sink=SinkConfig::new( - kind=SinkKind::File, - path="config-file-setters-delegate.log", - ), - ), - ) - let sink = logger.sink - - inspect( - logger.file_set_append_mode(false) == sink.file_set_append_mode(false), - content="true", - ) - inspect(logger.file_append_mode() == sink.file_append_mode(), content="true") - - inspect( - logger.file_set_auto_flush(false) == sink.file_set_auto_flush(false), - content="true", - ) - inspect(logger.file_auto_flush() == sink.file_auto_flush(), content="true") - - let rotation = Some(file_rotation(48, max_backups=3)) - inspect( - logger.file_set_rotation(rotation) == sink.file_set_rotation(rotation), - content="true", - ) - inspect( - logger.file_rotation_enabled() == sink.file_rotation_enabled(), - content="true", - ) - - let logger_policy = logger.file_policy() - let sink_policy = sink.file_policy() - inspect(logger_policy.append == sink_policy.append, content="true") - inspect(logger_policy.auto_flush == sink_policy.auto_flush, content="true") - inspect( - match logger_policy.rotation { - Some(value) => - match sink_policy.rotation { - Some(other) => - value.max_bytes == other.max_bytes && - value.max_backups == other.max_backups - None => false - } - None => sink_policy.rotation is None - }, - content="true", - ) - - inspect( - logger.file_clear_rotation() == sink.file_clear_rotation(), - content="true", - ) - inspect( - logger.file_rotation_enabled() == sink.file_rotation_enabled(), - content="true", - ) - inspect( - logger.file_policy_matches_default() == sink.file_policy_matches_default(), - content="true", - ) - - let console_logger = build_logger( - LoggerConfig::new( - target="config.file.setters.console", - sink=SinkConfig::new(kind=SinkKind::Console), - ), - ) - let console_sink = console_logger.sink - inspect( - console_logger.file_set_append_mode(true) == - console_sink.file_set_append_mode(true), - content="true", - ) - inspect( - console_logger.file_set_auto_flush(true) == - console_sink.file_set_auto_flush(true), - content="true", - ) - inspect( - console_logger.file_set_rotation(rotation) == - console_sink.file_set_rotation(rotation), - content="true", - ) - inspect( - console_logger.file_clear_rotation() == console_sink.file_clear_rotation(), - content="true", - ) - - 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", - ) - inspect(rotation_obj.get("max_bytes_i64") is None, content="true") - - let wide_rotation_json = file_rotation_config_to_json( - file_rotation_i64(4294967296L, max_backups=2), - ) - let wide_rotation_obj = wide_rotation_json.as_object().unwrap() - inspect( - wide_rotation_obj.get("max_bytes_i64").unwrap().as_string().unwrap(), - content="4294967296", - ) - - 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", - ) - - let wide = file_sink_state_to_json( - FileSinkState::new( - "wide.log", - available=true, - append=true, - auto_flush=false, - rotation=Some(file_rotation_i64(4294967296L, max_backups=3)), - open_failures=0, - write_failures=0, - flush_failures=0, - rotation_failures=0, - ), - ) - let wide_obj = wide.as_object().unwrap() - let wide_state_rotation_obj = wide_obj - .get("rotation") - .unwrap() - .as_object() - .unwrap() - inspect( - wide_state_rotation_obj - .get("max_bytes") - .unwrap() - .as_number() - .unwrap() - .to_int(), - content="2147483647", - ) - inspect( - wide_state_rotation_obj - .get("max_backups") - .unwrap() - .as_number() - .unwrap() - .to_int(), - content="3", - ) - inspect( - wide_state_rotation_obj.get("max_bytes_i64").unwrap().as_string().unwrap(), - content="4294967296", - ) - 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}", - ) - - let wide_runtime_json = runtime_file_state_to_json( - RuntimeFileState::new( - FileSinkState::new( - "wide-queue.log", - available=true, - append=true, - auto_flush=true, - rotation=Some(file_rotation_i64(4294967296L, max_backups=2)), - open_failures=0, - write_failures=0, - flush_failures=0, - rotation_failures=0, - ), - queued=false, - pending_count=0, - dropped_count=0, - ), - ) - let wide_runtime_obj = wide_runtime_json.as_object().unwrap() - let wide_runtime_file_obj = wide_runtime_obj - .get("file") - .unwrap() - .as_object() - .unwrap() - let wide_runtime_rotation_obj = wide_runtime_file_obj - .get("rotation") - .unwrap() - .as_object() - .unwrap() - inspect( - wide_runtime_rotation_obj - .get("max_bytes") - .unwrap() - .as_number() - .unwrap() - .to_int(), - content="2147483647", - ) - inspect( - wide_runtime_rotation_obj - .get("max_backups") - .unwrap() - .as_number() - .unwrap() - .to_int(), - content="2", - ) - inspect( - wide_runtime_rotation_obj.get("max_bytes_i64").unwrap().as_string().unwrap(), - content="4294967296", - ) -} - -///| -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", - ) - - let wide_policy_json = file_sink_policy_to_json( - FileSinkPolicy::new( - append=true, - auto_flush=false, - rotation=Some(file_rotation_i64(4294967296L, max_backups=4)), - ), - ) - let wide_policy_obj = wide_policy_json.as_object().unwrap() - let wide_policy_rotation_obj = wide_policy_obj - .get("rotation") - .unwrap() - .as_object() - .unwrap() - inspect( - wide_policy_rotation_obj - .get("max_bytes") - .unwrap() - .as_number() - .unwrap() - .to_int(), - content="2147483647", - ) - inspect( - wide_policy_rotation_obj - .get("max_backups") - .unwrap() - .as_number() - .unwrap() - .to_int(), - content="4", - ) - inspect( - wide_policy_rotation_obj.get("max_bytes_i64").unwrap().as_string().unwrap(), - content="4294967296", - ) - 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_path_or_none() is None, content="true") - inspect(logger.file_policy_or_none() is None, content="true") - inspect(logger.file_default_policy_or_none() is None, content="true") - inspect(logger.file_state_or_none() is None, content="true") - 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") - match logger.file_path_or_none() { - Some(path) => inspect(path, content="config-capability.log") - None => inspect(false, content="true") - } - inspect(logger.file_policy_or_none() is None, content="false") - inspect(logger.file_default_policy_or_none() is None, content="false") - inspect(logger.file_state_or_none() is None, content="false") - 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 file reopen helpers stay aligned with runtime sink helpers" { - let logger = build_logger( - LoggerConfig::new( - sink=SinkConfig::new( - kind=SinkKind::File, - path="config-reopen-delegate.log", - ), - ), - ) - let sink = logger.sink - - inspect( - logger.file_set_append_mode(false) == sink.file_set_append_mode(false), - content="true", - ) - inspect(logger.file_reopen() == sink.file_reopen(), content="true") - inspect(logger.file_append_mode() == sink.file_append_mode(), content="true") - - inspect( - logger.file_reopen_append() == sink.file_reopen_append(), - content="true", - ) - inspect(logger.file_append_mode() == sink.file_append_mode(), content="true") - - inspect( - logger.file_reopen_truncate() == sink.file_reopen_truncate(), - content="true", - ) - inspect(logger.file_append_mode() == sink.file_append_mode(), content="true") - - inspect( - logger.file_set_append_mode(true) == sink.file_set_append_mode(true), - content="true", - ) - inspect( - logger.file_reopen_with_current_policy() == - sink.file_reopen_with_current_policy(), - content="true", - ) - inspect(logger.file_append_mode() == sink.file_append_mode(), content="true") - - inspect( - logger.file_reopen(append=Some(false)) == - sink.file_reopen(append=Some(false)), - content="true", - ) - inspect(logger.file_append_mode() == sink.file_append_mode(), content="true") - - let console_logger = build_logger( - LoggerConfig::new(sink=SinkConfig::new(kind=SinkKind::Console)), - ) - let console_sink = console_logger.sink - inspect( - console_logger.file_reopen() == console_sink.file_reopen(), - content="true", - ) - inspect( - console_logger.file_reopen_with_current_policy() == - console_sink.file_reopen_with_current_policy(), - content="true", - ) - inspect( - console_logger.file_reopen_append() == console_sink.file_reopen_append(), - content="true", - ) - inspect( - console_logger.file_reopen_truncate() == console_sink.file_reopen_truncate(), - 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 file reset helpers stay aligned with runtime sink helpers" { - let logger = build_logger( - LoggerConfig::new( - sink=SinkConfig::new( - kind=SinkKind::File, - path="config-reset-delegate.log", - append=false, - auto_flush=false, - rotation=Some(file_rotation(40, max_backups=2)), - ), - ), - ) - let sink = logger.sink - - ignore(logger.file_set_append_mode(true)) - ignore(sink.file_set_append_mode(true)) - ignore(logger.file_set_auto_flush(true)) - ignore(sink.file_set_auto_flush(true)) - ignore(logger.file_set_rotation(None)) - ignore(sink.file_set_rotation(None)) - - inspect( - logger.file_reset_policy() == sink.file_reset_policy(), - content="true", - ) - inspect( - logger.file_policy_matches_default() == sink.file_policy_matches_default(), - content="true", - ) - inspect(logger.file_append_mode() == sink.file_append_mode(), content="true") - inspect(logger.file_auto_flush() == sink.file_auto_flush(), content="true") - - inspect( - logger.file_reset_failure_counters() == sink.file_reset_failure_counters(), - content="true", - ) - inspect( - logger.file_open_failures() == sink.file_open_failures(), - content="true", - ) - inspect( - logger.file_write_failures() == sink.file_write_failures(), - content="true", - ) - inspect( - logger.file_flush_failures() == sink.file_flush_failures(), - content="true", - ) - inspect( - logger.file_rotation_failures() == sink.file_rotation_failures(), - content="true", - ) - - let console_logger = build_logger( - LoggerConfig::new(sink=SinkConfig::new(kind=SinkKind::Console)), - ) - let console_sink = console_logger.sink - inspect( - console_logger.file_reset_policy() == console_sink.file_reset_policy(), - content="true", - ) - inspect( - console_logger.file_reset_failure_counters() == - console_sink.file_reset_failure_counters(), - content="true", - ) - - ignore(logger.close()) -} - -///| -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 logger file flush and close stay aligned with runtime sink helpers" { - let file_config = LoggerConfig::new( - sink=SinkConfig::new( - kind=SinkKind::File, - path="config-file-helper-delegate.log", - ), - ) - let file_logger = build_logger(file_config) - let file_sink = build_logger(file_config).sink - - inspect(file_logger.file_flush() == file_sink.file_flush(), content="true") - inspect(file_logger.file_close() == file_sink.file_close(), content="true") - - let console_config = LoggerConfig::new( - sink=SinkConfig::new(kind=SinkKind::Console), - ) - let console_logger = build_logger(console_config) - let console_sink = build_logger(console_config).sink - - inspect( - console_logger.file_flush() == console_sink.file_flush(), - content="true", - ) - inspect( - console_logger.file_close() == console_sink.file_close(), - content="true", - ) -} - -///| -test "configured logger file state helpers stay aligned with runtime sink helpers" { - let file_logger = build_logger( - LoggerConfig::new( - target="config.file.state.delegate", - sink=SinkConfig::new( - kind=SinkKind::File, - path="config-file-state-delegate.log", - ), - ), - ) - let file_sink = file_logger.sink - let file_state = file_logger.file_state() - let file_sink_state = file_sink.file_state() - - inspect(file_state.path == file_sink_state.path, content="true") - inspect(file_state.available == file_sink_state.available, content="true") - inspect(file_state.append == file_sink_state.append, content="true") - inspect(file_state.auto_flush == file_sink_state.auto_flush, content="true") - inspect( - file_state.open_failures == file_sink_state.open_failures, - content="true", - ) - inspect( - file_state.write_failures == file_sink_state.write_failures, - content="true", - ) - inspect( - file_state.flush_failures == file_sink_state.flush_failures, - content="true", - ) - inspect( - file_state.rotation_failures == file_sink_state.rotation_failures, - content="true", - ) - inspect( - match file_state.rotation { - Some(rotation) => - match file_sink_state.rotation { - Some(other) => - rotation.max_bytes == other.max_bytes && - rotation.max_backups == other.max_backups - None => false - } - None => file_sink_state.rotation is None - }, - content="true", - ) - inspect( - (file_logger.file_runtime_state() is None) == - (file_sink.file_runtime_state() is None), - content="true", - ) - - let queued_logger = build_logger( - LoggerConfig::new( - target="config.file.runtime.delegate", - sink=SinkConfig::new( - kind=SinkKind::File, - path="config-file-runtime-delegate.log", - ), - queue=Some(QueueConfig::new(4, overflow=QueueOverflowPolicy::DropNewest)), - ), - ) - let queued_sink = queued_logger.sink - queued_logger.info("one") - queued_logger.info("two") - let queued_state = queued_logger.file_state() - let queued_sink_state = queued_sink.file_state() - - inspect(queued_state.path == queued_sink_state.path, content="true") - inspect(queued_state.available == queued_sink_state.available, content="true") - inspect(queued_state.append == queued_sink_state.append, content="true") - inspect( - queued_state.auto_flush == queued_sink_state.auto_flush, - content="true", - ) - inspect( - queued_state.open_failures == queued_sink_state.open_failures, - content="true", - ) - inspect( - queued_state.write_failures == queued_sink_state.write_failures, - content="true", - ) - inspect( - queued_state.flush_failures == queued_sink_state.flush_failures, - content="true", - ) - inspect( - queued_state.rotation_failures == queued_sink_state.rotation_failures, - content="true", - ) - inspect( - match queued_state.rotation { - Some(rotation) => - match queued_sink_state.rotation { - Some(other) => - rotation.max_bytes == other.max_bytes && - rotation.max_backups == other.max_backups - None => false - } - None => queued_sink_state.rotation is None - }, - content="true", - ) - - match (queued_logger.file_runtime_state(), queued_sink.file_runtime_state()) { - (Some(snapshot), Some(sink_snapshot)) => { - inspect(snapshot.file.path == sink_snapshot.file.path, content="true") - inspect( - snapshot.file.available == sink_snapshot.file.available, - content="true", - ) - inspect(snapshot.file.append == sink_snapshot.file.append, content="true") - inspect( - snapshot.file.auto_flush == sink_snapshot.file.auto_flush, - content="true", - ) - inspect( - snapshot.file.open_failures == sink_snapshot.file.open_failures, - content="true", - ) - inspect( - snapshot.file.write_failures == sink_snapshot.file.write_failures, - content="true", - ) - inspect( - snapshot.file.flush_failures == sink_snapshot.file.flush_failures, - content="true", - ) - inspect( - snapshot.file.rotation_failures == sink_snapshot.file.rotation_failures, - content="true", - ) - inspect(snapshot.queued == sink_snapshot.queued, content="true") - inspect( - snapshot.pending_count == sink_snapshot.pending_count, - content="true", - ) - inspect( - snapshot.dropped_count == sink_snapshot.dropped_count, - content="true", - ) - inspect( - match snapshot.file.rotation { - Some(rotation) => - match sink_snapshot.file.rotation { - Some(other) => - rotation.max_bytes == other.max_bytes && - rotation.max_backups == other.max_backups - None => false - } - None => sink_snapshot.file.rotation is None - }, - content="true", - ) - } - _ => inspect(false, content="true") - } - - let console_logger = build_logger( - LoggerConfig::new( - target="config.file.state.console", - sink=SinkConfig::new(kind=SinkKind::Console), - ), - ) - let console_sink = console_logger.sink - let console_state = console_logger.file_state() - let console_sink_state = console_sink.file_state() - - inspect(console_state.path == console_sink_state.path, content="true") - inspect( - console_state.available == console_sink_state.available, - content="true", - ) - inspect(console_state.append == console_sink_state.append, content="true") - inspect( - console_state.auto_flush == console_sink_state.auto_flush, - content="true", - ) - inspect( - console_state.open_failures == console_sink_state.open_failures, - content="true", - ) - inspect( - console_state.write_failures == console_sink_state.write_failures, - content="true", - ) - inspect( - console_state.flush_failures == console_sink_state.flush_failures, - content="true", - ) - inspect( - console_state.rotation_failures == console_sink_state.rotation_failures, - content="true", - ) - inspect( - (console_state.rotation is None) == (console_sink_state.rotation is None), - content="true", - ) - inspect( - (console_logger.file_runtime_state() is None) == - (console_sink.file_runtime_state() is None), - content="true", - ) - - ignore(file_logger.close()) - ignore(queued_logger.close()) -} - -///| -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 "configured queued file logger blocks policy mutation while queue is pending" { - let logger = build_logger( - LoggerConfig::new( - sink=SinkConfig::new(kind=SinkKind::File, path="config-queued-policy.log"), - queue=Some(QueueConfig::new(4, overflow=QueueOverflowPolicy::DropNewest)), - ), - ) - logger.info("one") - logger.info("two") - inspect(logger.pending_count(), content="2") - inspect(logger.file_set_append_mode(false), content="false") - inspect(logger.file_append_mode(), content="true") - inspect( - logger.file_set_policy( - FileSinkPolicy::new(append=false, auto_flush=false, rotation=None), - ), - content="false", - ) - inspect(logger.file_reopen_truncate(), content="false") - inspect(logger.pending_count(), content="2") - if logger.file_available() { - inspect(logger.file_flush(), content="true") - inspect(logger.pending_count(), content="0") - inspect(logger.file_set_append_mode(false), content="true") - inspect(logger.file_append_mode(), content="false") - inspect(logger.file_reopen_with_current_policy(), content="true") - inspect(logger.file_close(), content="true") - } else { - inspect(logger.file_flush(), content="false") - inspect(logger.pending_count(), content="0") - inspect(logger.file_reopen_with_current_policy(), content="false") - inspect(logger.file_close(), content="false") - } -} - -///| -test "configured queued file logger close drains queue before teardown" { - let logger = build_logger( - LoggerConfig::new( - sink=SinkConfig::new(kind=SinkKind::File, path="config-close-queued.log"), - queue=Some(QueueConfig::new(4, overflow=QueueOverflowPolicy::DropNewest)), - ), - ) - logger.info("one") - logger.info("two") - inspect(logger.pending_count(), content="2") - if logger.file_available() { - inspect(logger.close(), content="true") - inspect(logger.pending_count(), content="0") - inspect(logger.file_available(), content="false") - inspect(logger.file_close(), content="false") - } else { - inspect(logger.close(), content="false") - inspect(logger.pending_count(), content="0") - inspect(logger.file_available(), content="false") - inspect(logger.file_write_failures(), content="2") - inspect(logger.file_close(), content="false") - } -} - ///| test "logger log supports per-call target override" { let captured_target : Ref[String] = Ref("") @@ -3152,2197 +168,53 @@ test "logger severity helpers use stored target without override" { } ///| -test "library logger keeps a smaller stable facade" { +test "callback sink sees child target and context logger shape" { let captured_target : Ref[String] = Ref("") let captured_message : Ref[String] = Ref("") - let captured_fields : Ref[Array[Field]] = Ref([]) - let logger = LibraryLogger::new( + let captured_field_count : Ref[Int] = Ref(0) + let captured_timestamp : Ref[UInt64] = Ref(0UL) + let logger = Logger::new( callback_sink(fn(rec) { captured_target.val = rec.target captured_message.val = rec.message - captured_fields.val = rec.fields + captured_field_count.val = rec.fields.length() + captured_timestamp.val = rec.timestamp_ms }), min_level=Level::Info, - target="svc", + target="app", ) + .child("worker") .with_context_fields([field("service", "bitlogger")]) - .child("worker") + .with_timestamp() logger.info("ready", fields=[field("mode", "test")]) - inspect(captured_target.val, content="svc.worker") + inspect(captured_target.val, content="app.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_field_count.val, content="2") 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" { +test "bind aliases context fields ergonomically" { let captured_target : Ref[String] = Ref("") let captured_message : Ref[String] = Ref("") let captured_fields : Ref[Array[Field]] = Ref([]) - let logger = LibraryLogger::new( + let logger = Logger::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 builder log supports per-call target override through facade" { - let captured_target : Ref[String] = Ref("") - let captured_message : Ref[String] = Ref("") - let captured_fields : Ref[Array[Field]] = Ref([]) - let logger = build_library_logger( - LoggerConfig::new( - min_level=Level::Warn, - target="lib.config.default", - sink=SinkConfig::new(kind=SinkKind::Console), - ), - ) - .to_logger() - .with_patch(fn(rec) { - captured_target.val = rec.target - captured_message.val = rec.message - captured_fields.val = rec.fields - rec - }) - .to_library_logger() - .bind([field("service", "bitlogger")]) - - logger.log( - Level::Error, - "override", - fields=[field("mode", "test")], - target="lib.config.override", - ) - inspect(captured_target.val, content="lib.config.override") - inspect(captured_message.val, content="override") - inspect(captured_fields.val.length(), content="2") + target="bind", + ).bind(fields([("service", "bitlogger"), ("scope", "audit")])) + logger.info("ready", fields=[field("mode", "test")]) + inspect(captured_target.val, content="bind") + inspect(captured_message.val, content="ready") + inspect(captured_fields.val.length(), content="3") 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.config.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 builder 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 = build_library_logger( - LoggerConfig::new( - min_level=Level::Info, - target="lib.config.helpers", - sink=SinkConfig::new(kind=SinkKind::Console), - ), - ) - .to_logger() - .with_patch(fn(rec) { - captured_levels.val.push(rec.level.label()) - captured_targets.val.push(rec.target) - captured_fields.val.push(rec.fields) - rec - }) - .to_library_logger() - .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.config.helpers") - inspect(captured_targets.val[1], content="lib.config.helpers") - inspect(captured_targets.val[2], content="lib.config.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 builder unwrap matches direct configured builder behavior" { - let config = LoggerConfig::new( - min_level=Level::Warn, - target="lib.config.same-build", - timestamp=true, - queue=Some(QueueConfig::new(2, overflow=QueueOverflowPolicy::DropNewest)), - sink=SinkConfig::new(kind=SinkKind::Console), - ) - let logger = build_library_logger(config) - let full = logger.to_logger() - let configured = build_logger(config) - - inspect(full.target, content=configured.target) - inspect(full.timestamp == configured.timestamp, content="true") - inspect( - logger.is_enabled(Level::Error) == configured.is_enabled(Level::Error), - content="true", - ) - inspect( - logger.is_enabled(Level::Info) == configured.is_enabled(Level::Info), - content="true", - ) - let full_sink_kind = match full.sink { - RuntimeSink::QueuedConsole(_) => "QueuedConsole" - RuntimeSink::QueuedJsonConsole(_) => "QueuedJsonConsole" - RuntimeSink::QueuedTextConsole(_) => "QueuedTextConsole" - RuntimeSink::QueuedFile(_) => "QueuedFile" - RuntimeSink::Console(_) => "Console" - RuntimeSink::JsonConsole(_) => "JsonConsole" - RuntimeSink::TextConsole(_) => "TextConsole" - RuntimeSink::File(_) => "File" - } - let configured_sink_kind = match configured.sink { - RuntimeSink::QueuedConsole(_) => "QueuedConsole" - RuntimeSink::QueuedJsonConsole(_) => "QueuedJsonConsole" - RuntimeSink::QueuedTextConsole(_) => "QueuedTextConsole" - RuntimeSink::QueuedFile(_) => "QueuedFile" - RuntimeSink::Console(_) => "Console" - RuntimeSink::JsonConsole(_) => "JsonConsole" - RuntimeSink::TextConsole(_) => "TextConsole" - RuntimeSink::File(_) => "File" - } - inspect(full_sink_kind == configured_sink_kind, content="true") - - full.error("one") - full.error("two") - full.error("three") - configured.error("one") - configured.error("two") - configured.error("three") - - inspect(full.pending_count() == configured.pending_count(), content="true") - inspect(full.dropped_count() == configured.dropped_count(), content="true") - inspect(full.flush() == configured.flush(), content="true") - inspect(full.pending_count() == configured.pending_count(), content="true") -} - -///| -test "library logger builder unwrap preserves file runtime helpers" { - let config = LoggerConfig::new( - min_level=Level::Warn, - target="lib.file.same-build", - sink=SinkConfig::new(kind=SinkKind::File, path="lib-file-same-build.log"), - queue=Some(QueueConfig::new(2, overflow=QueueOverflowPolicy::DropOldest)), - ) - let logger = build_library_logger(config) - let full = logger.to_logger() - let configured = build_logger(config) - - full.error("one") - full.error("two") - full.error("three") - configured.error("one") - configured.error("two") - configured.error("three") - - inspect(full.pending_count() == configured.pending_count(), content="true") - inspect(full.dropped_count() == configured.dropped_count(), content="true") - inspect(full.file_available() == configured.file_available(), content="true") - inspect( - full.file_append_mode() == configured.file_append_mode(), - content="true", - ) - inspect( - full.file_auto_flush() == configured.file_auto_flush(), - content="true", - ) - - let full_state = full.file_state() - let configured_state = configured.file_state() - inspect(full_state.path == configured_state.path, content="true") - inspect(full_state.available == configured_state.available, content="true") - inspect(full_state.append == configured_state.append, content="true") - inspect(full_state.auto_flush == configured_state.auto_flush, content="true") - inspect( - full_state.open_failures == configured_state.open_failures, - content="true", - ) - inspect( - full_state.write_failures == configured_state.write_failures, - content="true", - ) - inspect( - full_state.flush_failures == configured_state.flush_failures, - content="true", - ) - inspect( - full_state.rotation_failures == configured_state.rotation_failures, - content="true", - ) - - match (full.file_runtime_state(), configured.file_runtime_state()) { - (Some(snapshot), Some(other)) => { - inspect(snapshot.file.path == other.file.path, content="true") - inspect(snapshot.file.available == other.file.available, content="true") - inspect(snapshot.queued == other.queued, content="true") - inspect(snapshot.pending_count == other.pending_count, content="true") - inspect(snapshot.dropped_count == other.dropped_count, content="true") - } - _ => inspect(false, content="true") - } - - inspect(full.file_flush() == configured.file_flush(), content="true") - inspect(full.pending_count() == configured.pending_count(), content="true") - inspect(full.file_close() == configured.file_close(), content="true") -} - -///| -test "library logger builder unwrap preserves file controls" { - let config = LoggerConfig::new( - min_level=Level::Warn, - target="lib.file.controls.same-build", - sink=SinkConfig::new( - kind=SinkKind::File, - path="lib-file-controls-same-build.log", - ), - ) - let logger = build_library_logger(config) - let full = logger.to_logger() - let configured = build_logger(config) - - inspect( - full.file_set_append_mode(false) == configured.file_set_append_mode(false), - content="true", - ) - inspect( - full.file_append_mode() == configured.file_append_mode(), - content="true", - ) - inspect( - full.file_set_auto_flush(false) == configured.file_set_auto_flush(false), - content="true", - ) - inspect( - full.file_auto_flush() == configured.file_auto_flush(), - content="true", - ) - - let rotation = Some(file_rotation(48, max_backups=3)) - inspect( - full.file_set_rotation(rotation) == configured.file_set_rotation(rotation), - content="true", - ) - inspect( - full.file_rotation_enabled() == configured.file_rotation_enabled(), - content="true", - ) - - let full_policy = full.file_policy() - let configured_policy = configured.file_policy() - inspect(full_policy.append == configured_policy.append, content="true") - inspect( - full_policy.auto_flush == configured_policy.auto_flush, - content="true", - ) - inspect( - match full_policy.rotation { - Some(value) => - match configured_policy.rotation { - Some(other) => - value.max_bytes == other.max_bytes && - value.max_backups == other.max_backups - None => false - } - None => configured_policy.rotation is None - }, - content="true", - ) - - inspect( - full.file_reopen_append() == configured.file_reopen_append(), - content="true", - ) - inspect( - full.file_append_mode() == configured.file_append_mode(), - content="true", - ) - inspect( - full.file_reopen_truncate() == configured.file_reopen_truncate(), - content="true", - ) - inspect( - full.file_append_mode() == configured.file_append_mode(), - content="true", - ) - inspect( - full.file_reopen_with_current_policy() == - configured.file_reopen_with_current_policy(), - content="true", - ) - - inspect( - full.file_clear_rotation() == configured.file_clear_rotation(), - content="true", - ) - inspect( - full.file_rotation_enabled() == configured.file_rotation_enabled(), - content="true", - ) - inspect( - full.file_policy_matches_default() == - configured.file_policy_matches_default(), - content="true", - ) - inspect( - full.file_reset_policy() == configured.file_reset_policy(), - content="true", - ) - inspect( - full.file_policy_matches_default() == - configured.file_policy_matches_default(), - content="true", - ) - inspect( - full.file_reset_failure_counters() == - configured.file_reset_failure_counters(), - content="true", - ) - - inspect(full.file_close() == configured.file_close(), content="true") -} - -///| -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 log supports per-call target override through facade" { - let captured_target : Ref[String] = Ref("") - let captured_message : Ref[String] = Ref("") - let captured_fields : Ref[Array[Field]] = Ref([]) - let logger = parse_and_build_library_logger( - "{\"min_level\":\"warn\",\"target\":\"lib.json.default\",\"sink\":{\"kind\":\"console\"}}", - ) - .to_logger() - .with_patch(fn(rec) { - captured_target.val = rec.target - captured_message.val = rec.message - captured_fields.val = rec.fields - rec - }) - .to_library_logger() - .bind([field("service", "bitlogger")]) - - logger.log( - Level::Error, - "override", - fields=[field("mode", "test")], - target="lib.json.override", - ) - inspect(captured_target.val, content="lib.json.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.json.default") - inspect(captured_message.val, content="default") - inspect(captured_fields.val.length(), content="1") - inspect(captured_fields.val[0].key, content="service") -} - -///| -test "parsed 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 = parse_and_build_library_logger( - "{\"min_level\":\"info\",\"target\":\"lib.json.helpers\",\"sink\":{\"kind\":\"console\"}}", - ) - .to_logger() - .with_patch(fn(rec) { - captured_levels.val.push(rec.level.label()) - captured_targets.val.push(rec.target) - captured_fields.val.push(rec.fields) - rec - }) - .to_library_logger() - .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.json.helpers") - inspect(captured_targets.val[1], content="lib.json.helpers") - inspect(captured_targets.val[2], content="lib.json.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 parse-build unwrap matches parsed direct builder behavior" { - let raw = "{\"min_level\":\"warn\",\"target\":\"lib.json.same-build\",\"timestamp\":true,\"queue\":{\"max_pending\":2,\"overflow\":\"DropNewest\"},\"sink\":{\"kind\":\"console\"}}" - let logger = parse_and_build_library_logger(raw) - let full = logger.to_logger() - let configured = parse_and_build_logger(raw) - - inspect(full.target, content=configured.target) - inspect(full.timestamp == configured.timestamp, content="true") - inspect( - logger.is_enabled(Level::Error) == configured.is_enabled(Level::Error), - content="true", - ) - inspect( - logger.is_enabled(Level::Info) == configured.is_enabled(Level::Info), - content="true", - ) - let full_sink_kind = match full.sink { - RuntimeSink::QueuedConsole(_) => "QueuedConsole" - RuntimeSink::QueuedJsonConsole(_) => "QueuedJsonConsole" - RuntimeSink::QueuedTextConsole(_) => "QueuedTextConsole" - RuntimeSink::QueuedFile(_) => "QueuedFile" - RuntimeSink::Console(_) => "Console" - RuntimeSink::JsonConsole(_) => "JsonConsole" - RuntimeSink::TextConsole(_) => "TextConsole" - RuntimeSink::File(_) => "File" - } - let configured_sink_kind = match configured.sink { - RuntimeSink::QueuedConsole(_) => "QueuedConsole" - RuntimeSink::QueuedJsonConsole(_) => "QueuedJsonConsole" - RuntimeSink::QueuedTextConsole(_) => "QueuedTextConsole" - RuntimeSink::QueuedFile(_) => "QueuedFile" - RuntimeSink::Console(_) => "Console" - RuntimeSink::JsonConsole(_) => "JsonConsole" - RuntimeSink::TextConsole(_) => "TextConsole" - RuntimeSink::File(_) => "File" - } - inspect(full_sink_kind == configured_sink_kind, content="true") - - full.error("one") - full.error("two") - full.error("three") - configured.error("one") - configured.error("two") - configured.error("three") - - inspect(full.pending_count() == configured.pending_count(), content="true") - inspect(full.dropped_count() == configured.dropped_count(), content="true") - inspect(full.flush() == configured.flush(), content="true") - inspect(full.pending_count() == configured.pending_count(), content="true") -} - -///| -test "library logger parse-build unwrap preserves file runtime helpers" { - let raw = "{\"min_level\":\"warn\",\"target\":\"lib.json.file.same-build\",\"queue\":{\"max_pending\":2,\"overflow\":\"DropOldest\"},\"sink\":{\"kind\":\"file\",\"path\":\"lib-json-file-same-build.log\"}}" - let logger = parse_and_build_library_logger(raw) - let full = logger.to_logger() - let configured = parse_and_build_logger(raw) - - full.error("one") - full.error("two") - full.error("three") - configured.error("one") - configured.error("two") - configured.error("three") - - inspect(full.pending_count() == configured.pending_count(), content="true") - inspect(full.dropped_count() == configured.dropped_count(), content="true") - inspect(full.file_available() == configured.file_available(), content="true") - inspect( - full.file_append_mode() == configured.file_append_mode(), - content="true", - ) - inspect( - full.file_auto_flush() == configured.file_auto_flush(), - content="true", - ) - - let full_state = full.file_state() - let configured_state = configured.file_state() - inspect(full_state.path == configured_state.path, content="true") - inspect(full_state.available == configured_state.available, content="true") - inspect(full_state.append == configured_state.append, content="true") - inspect(full_state.auto_flush == configured_state.auto_flush, content="true") - inspect( - full_state.open_failures == configured_state.open_failures, - content="true", - ) - inspect( - full_state.write_failures == configured_state.write_failures, - content="true", - ) - inspect( - full_state.flush_failures == configured_state.flush_failures, - content="true", - ) - inspect( - full_state.rotation_failures == configured_state.rotation_failures, - content="true", - ) - - match (full.file_runtime_state(), configured.file_runtime_state()) { - (Some(snapshot), Some(other)) => { - inspect(snapshot.file.path == other.file.path, content="true") - inspect(snapshot.file.available == other.file.available, content="true") - inspect(snapshot.queued == other.queued, content="true") - inspect(snapshot.pending_count == other.pending_count, content="true") - inspect(snapshot.dropped_count == other.dropped_count, content="true") - } - _ => inspect(false, content="true") - } - - inspect(full.file_flush() == configured.file_flush(), content="true") - inspect(full.pending_count() == configured.pending_count(), content="true") - inspect(full.file_close() == configured.file_close(), content="true") -} - -///| -test "library logger parse-build unwrap preserves file controls" { - let raw = "{\"min_level\":\"warn\",\"target\":\"lib.json.file.controls.same-build\",\"sink\":{\"kind\":\"file\",\"path\":\"lib-json-file-controls-same-build.log\"}}" - let logger = parse_and_build_library_logger(raw) - let full = logger.to_logger() - let configured = parse_and_build_logger(raw) - - inspect( - full.file_set_append_mode(false) == configured.file_set_append_mode(false), - content="true", - ) - inspect( - full.file_append_mode() == configured.file_append_mode(), - content="true", - ) - inspect( - full.file_set_auto_flush(false) == configured.file_set_auto_flush(false), - content="true", - ) - inspect( - full.file_auto_flush() == configured.file_auto_flush(), - content="true", - ) - - let rotation = Some(file_rotation(40, max_backups=2)) - inspect( - full.file_set_rotation(rotation) == configured.file_set_rotation(rotation), - content="true", - ) - inspect( - full.file_rotation_enabled() == configured.file_rotation_enabled(), - content="true", - ) - inspect( - full.file_reopen(append=Some(false)) == - configured.file_reopen(append=Some(false)), - content="true", - ) - inspect( - full.file_append_mode() == configured.file_append_mode(), - content="true", - ) - inspect( - full.file_reopen_append() == configured.file_reopen_append(), - content="true", - ) - inspect( - full.file_append_mode() == configured.file_append_mode(), - content="true", - ) - inspect( - full.file_clear_rotation() == configured.file_clear_rotation(), - content="true", - ) - inspect( - full.file_rotation_enabled() == configured.file_rotation_enabled(), - content="true", - ) - inspect( - full.file_reset_policy() == configured.file_reset_policy(), - content="true", - ) - inspect( - full.file_policy_matches_default() == - configured.file_policy_matches_default(), - content="true", - ) - inspect( - full.file_reset_failure_counters() == - configured.file_reset_failure_counters(), - content="true", - ) - - inspect(full.file_close() == configured.file_close(), content="true") -} - -///| -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 "configured logger projection preserves file runtime helpers through unwrap" { - let configured = build_logger( - LoggerConfig::new( - min_level=Level::Warn, - target="lib.projected.file", - sink=SinkConfig::new(kind=SinkKind::File, path="lib-projected-file.log"), - queue=Some(QueueConfig::new(2, overflow=QueueOverflowPolicy::DropOldest)), - ), - ) - let projected = configured.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.file") - - full.error("one") - full.error("two") - full.error("three") - inspect(full.pending_count() == configured.pending_count(), content="true") - inspect(full.dropped_count() == configured.dropped_count(), content="true") - inspect(full.file_available() == configured.file_available(), content="true") - inspect( - full.file_append_mode() == configured.file_append_mode(), - content="true", - ) - inspect( - full.file_auto_flush() == configured.file_auto_flush(), - content="true", - ) - - let full_state = full.file_state() - let configured_state = configured.file_state() - inspect(full_state.path == configured_state.path, content="true") - inspect(full_state.available == configured_state.available, content="true") - inspect(full_state.append == configured_state.append, content="true") - inspect(full_state.auto_flush == configured_state.auto_flush, content="true") - inspect( - full_state.open_failures == configured_state.open_failures, - content="true", - ) - inspect( - full_state.write_failures == configured_state.write_failures, - content="true", - ) - inspect( - full_state.flush_failures == configured_state.flush_failures, - content="true", - ) - inspect( - full_state.rotation_failures == configured_state.rotation_failures, - content="true", - ) - - match (full.file_runtime_state(), configured.file_runtime_state()) { - (Some(snapshot), Some(other)) => { - inspect(snapshot.file.path == other.file.path, content="true") - inspect(snapshot.file.available == other.file.available, content="true") - inspect(snapshot.queued == other.queued, content="true") - inspect(snapshot.pending_count == other.pending_count, content="true") - inspect(snapshot.dropped_count == other.dropped_count, content="true") - } - _ => inspect(false, content="true") - } - - inspect( - full.file_set_append_mode(false) == configured.file_set_append_mode(false), - content="true", - ) - inspect( - full.file_append_mode() == configured.file_append_mode(), - content="true", - ) - inspect( - full.file_set_auto_flush(false) == configured.file_set_auto_flush(false), - content="true", - ) - inspect( - full.file_auto_flush() == configured.file_auto_flush(), - content="true", - ) - inspect( - full.file_set_rotation(Some(file_rotation(36, max_backups=2))) == - configured.file_set_rotation(Some(file_rotation(36, max_backups=2))), - content="true", - ) - inspect( - full.file_rotation_enabled() == configured.file_rotation_enabled(), - content="true", - ) - inspect( - full.file_reopen_append() == configured.file_reopen_append(), - content="true", - ) - inspect( - full.file_append_mode() == configured.file_append_mode(), - content="true", - ) - inspect( - full.file_clear_rotation() == configured.file_clear_rotation(), - content="true", - ) - inspect( - full.file_reset_policy() == configured.file_reset_policy(), - content="true", - ) - inspect( - full.file_policy_matches_default() == - configured.file_policy_matches_default(), - content="true", - ) - inspect( - full.file_reset_failure_counters() == - configured.file_reset_failure_counters(), - content="true", - ) - inspect(full.file_flush() == configured.file_flush(), content="true") - inspect(full.pending_count() == configured.pending_count(), content="true") - - let was_available = configured.file_available() - inspect(configured.file_close() == was_available, content="true") - inspect(full.file_available(), content="false") - inspect(configured.file_available(), content="false") - inspect(full.file_close(), content="false") -} - -///| -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 "application logger builder matches direct configured builder behavior" { - let config = LoggerConfig::new( - min_level=Level::Warn, - target="app.runtime.same-build", - timestamp=true, - queue=Some(QueueConfig::new(2, overflow=QueueOverflowPolicy::DropNewest)), - sink=SinkConfig::new(kind=SinkKind::Console), - ) - let application = build_application_logger(config) - let configured = build_logger(config) - - inspect(application.target, content=configured.target) - inspect(application.timestamp, content="true") - inspect(configured.timestamp, content="true") - inspect( - application.is_enabled(Level::Error) == configured.is_enabled(Level::Error), - content="true", - ) - inspect( - application.is_enabled(Level::Info) == configured.is_enabled(Level::Info), - content="true", - ) - let application_sink_kind = match application.sink { - RuntimeSink::QueuedConsole(_) => "QueuedConsole" - RuntimeSink::QueuedJsonConsole(_) => "QueuedJsonConsole" - RuntimeSink::QueuedTextConsole(_) => "QueuedTextConsole" - RuntimeSink::QueuedFile(_) => "QueuedFile" - RuntimeSink::Console(_) => "Console" - RuntimeSink::JsonConsole(_) => "JsonConsole" - RuntimeSink::TextConsole(_) => "TextConsole" - RuntimeSink::File(_) => "File" - } - let configured_sink_kind = match configured.sink { - RuntimeSink::QueuedConsole(_) => "QueuedConsole" - RuntimeSink::QueuedJsonConsole(_) => "QueuedJsonConsole" - RuntimeSink::QueuedTextConsole(_) => "QueuedTextConsole" - RuntimeSink::QueuedFile(_) => "QueuedFile" - RuntimeSink::Console(_) => "Console" - RuntimeSink::JsonConsole(_) => "JsonConsole" - RuntimeSink::TextConsole(_) => "TextConsole" - RuntimeSink::File(_) => "File" - } - inspect(application_sink_kind == configured_sink_kind, content="true") - - application.error("one") - application.error("two") - application.error("three") - configured.error("one") - configured.error("two") - configured.error("three") - - inspect( - application.pending_count() == configured.pending_count(), - content="true", - ) - inspect( - application.dropped_count() == configured.dropped_count(), - content="true", - ) - inspect(application.flush() == configured.flush(), content="true") - inspect( - application.pending_count() == configured.pending_count(), - content="true", - ) -} - -///| -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 "configured logger build 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 = build_logger( - LoggerConfig::new( - min_level=Level::Warn, - target="configured.default", - sink=SinkConfig::new(kind=SinkKind::Console), - ), - ) - .with_patch(fn(rec) { - captured_target.val = rec.target - captured_message.val = rec.message - captured_fields.val = rec.fields - rec - }) - .with_context_fields([field("service", "bitlogger")]) - - logger.log( - Level::Error, - "override", - fields=[field("mode", "test")], - target="configured.override", - ) - inspect(captured_target.val, content="configured.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="configured.default") - inspect(captured_message.val, content="default") - inspect(captured_fields.val.length(), content="1") - inspect(captured_fields.val[0].key, content="service") -} - -///| -test "configured logger build 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 = build_logger( - LoggerConfig::new( - min_level=Level::Info, - target="configured.helpers", - sink=SinkConfig::new(kind=SinkKind::Console), - ), - ) - .with_patch(fn(rec) { - captured_levels.val.push(rec.level.label()) - captured_targets.val.push(rec.target) - captured_fields.val.push(rec.fields) - rec - }) - .with_context_fields([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="configured.helpers") - inspect(captured_targets.val[1], content="configured.helpers") - inspect(captured_targets.val[2], content="configured.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 "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 build 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 = build_application_logger( - LoggerConfig::new( - min_level=Level::Warn, - target="app.config.default", - sink=SinkConfig::new(kind=SinkKind::Console), - ), - ) - .with_patch(fn(rec) { - captured_target.val = rec.target - captured_message.val = rec.message - captured_fields.val = rec.fields - rec - }) - .with_context_fields([field("service", "bitlogger")]) - - logger.log( - Level::Error, - "override", - fields=[field("mode", "test")], - target="app.config.override", - ) - inspect(captured_target.val, content="app.config.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="app.config.default") - inspect(captured_message.val, content="default") - inspect(captured_fields.val.length(), content="1") - inspect(captured_fields.val[0].key, content="service") -} - -///| -test "application logger build 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 = build_application_logger( - LoggerConfig::new( - min_level=Level::Info, - target="app.config.helpers", - sink=SinkConfig::new(kind=SinkKind::Console), - ), - ) - .with_patch(fn(rec) { - captured_levels.val.push(rec.level.label()) - captured_targets.val.push(rec.target) - captured_fields.val.push(rec.fields) - rec - }) - .with_context_fields([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="app.config.helpers") - inspect(captured_targets.val[1], content="app.config.helpers") - inspect(captured_targets.val[2], content="app.config.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 "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 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 = parse_and_build_application_logger( - "{\"min_level\":\"warn\",\"target\":\"app.json.default\",\"sink\":{\"kind\":\"console\"}}", - ) - .with_patch(fn(rec) { - captured_target.val = rec.target - captured_message.val = rec.message - captured_fields.val = rec.fields - rec - }) - .with_context_fields([field("service", "bitlogger")]) - - logger.log( - Level::Error, - "override", - fields=[field("mode", "test")], - target="app.json.override", - ) - inspect(captured_target.val, content="app.json.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="app.json.default") - inspect(captured_message.val, content="default") - inspect(captured_fields.val.length(), content="1") - inspect(captured_fields.val[0].key, content="service") -} - -///| -test "parsed application 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 = parse_and_build_application_logger( - "{\"min_level\":\"info\",\"target\":\"app.json.helpers\",\"sink\":{\"kind\":\"console\"}}", - ) - .with_patch(fn(rec) { - captured_levels.val.push(rec.level.label()) - captured_targets.val.push(rec.target) - captured_fields.val.push(rec.fields) - rec - }) - .with_context_fields([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="app.json.helpers") - inspect(captured_targets.val[1], content="app.json.helpers") - inspect(captured_targets.val[2], content="app.json.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 "application logger parse-build matches parsed direct builder behavior" { - let raw = "{\"min_level\":\"warn\",\"target\":\"app.json.same-build\",\"timestamp\":true,\"queue\":{\"max_pending\":2,\"overflow\":\"DropNewest\"},\"sink\":{\"kind\":\"console\"}}" - let application = parse_and_build_application_logger(raw) - let configured = build_logger(parse_logger_config_text(raw)) - - inspect(application.target, content=configured.target) - inspect(application.timestamp == configured.timestamp, content="true") - inspect( - application.is_enabled(Level::Error) == configured.is_enabled(Level::Error), - content="true", - ) - inspect( - application.is_enabled(Level::Info) == configured.is_enabled(Level::Info), - content="true", - ) - let application_sink_kind = match application.sink { - RuntimeSink::QueuedConsole(_) => "QueuedConsole" - RuntimeSink::QueuedJsonConsole(_) => "QueuedJsonConsole" - RuntimeSink::QueuedTextConsole(_) => "QueuedTextConsole" - RuntimeSink::QueuedFile(_) => "QueuedFile" - RuntimeSink::Console(_) => "Console" - RuntimeSink::JsonConsole(_) => "JsonConsole" - RuntimeSink::TextConsole(_) => "TextConsole" - RuntimeSink::File(_) => "File" - } - let configured_sink_kind = match configured.sink { - RuntimeSink::QueuedConsole(_) => "QueuedConsole" - RuntimeSink::QueuedJsonConsole(_) => "QueuedJsonConsole" - RuntimeSink::QueuedTextConsole(_) => "QueuedTextConsole" - RuntimeSink::QueuedFile(_) => "QueuedFile" - RuntimeSink::Console(_) => "Console" - RuntimeSink::JsonConsole(_) => "JsonConsole" - RuntimeSink::TextConsole(_) => "TextConsole" - RuntimeSink::File(_) => "File" - } - inspect(application_sink_kind == configured_sink_kind, content="true") - - application.error("one") - application.error("two") - application.error("three") - configured.error("one") - configured.error("two") - configured.error("three") - - inspect( - application.pending_count() == configured.pending_count(), - content="true", - ) - inspect( - application.dropped_count() == configured.dropped_count(), - content="true", - ) - inspect(application.flush() == configured.flush(), content="true") - inspect( - application.pending_count() == configured.pending_count(), - content="true", - ) -} - -///| -test "application logger file helpers match direct configured builder behavior" { - let config = LoggerConfig::new( - min_level=Level::Warn, - target="app.file.same-build", - sink=SinkConfig::new(kind=SinkKind::File, path="app-file-same-build.log"), - queue=Some(QueueConfig::new(2, overflow=QueueOverflowPolicy::DropOldest)), - ) - let application = build_application_logger(config) - let configured = build_logger(config) - - application.error("one") - application.error("two") - application.error("three") - configured.error("one") - configured.error("two") - configured.error("three") - - inspect( - application.pending_count() == configured.pending_count(), - content="true", - ) - inspect( - application.dropped_count() == configured.dropped_count(), - content="true", - ) - inspect( - application.file_available() == configured.file_available(), - content="true", - ) - inspect( - application.file_append_mode() == configured.file_append_mode(), - content="true", - ) - inspect( - application.file_auto_flush() == configured.file_auto_flush(), - content="true", - ) - inspect( - application.file_policy_matches_default() == - configured.file_policy_matches_default(), - content="true", - ) - - let application_policy = application.file_policy() - let configured_policy = configured.file_policy() - inspect(application_policy.append == configured_policy.append, content="true") - inspect( - application_policy.auto_flush == configured_policy.auto_flush, - content="true", - ) - inspect( - match application_policy.rotation { - Some(rotation) => - match configured_policy.rotation { - Some(other) => - rotation.max_bytes == other.max_bytes && - rotation.max_backups == other.max_backups - None => false - } - None => configured_policy.rotation is None - }, - content="true", - ) - - let application_state = application.file_state() - let configured_state = configured.file_state() - inspect(application_state.path == configured_state.path, content="true") - inspect( - application_state.available == configured_state.available, - content="true", - ) - inspect(application_state.append == configured_state.append, content="true") - inspect( - application_state.auto_flush == configured_state.auto_flush, - content="true", - ) - inspect( - application_state.open_failures == configured_state.open_failures, - content="true", - ) - inspect( - application_state.write_failures == configured_state.write_failures, - content="true", - ) - inspect( - application_state.flush_failures == configured_state.flush_failures, - content="true", - ) - inspect( - application_state.rotation_failures == configured_state.rotation_failures, - content="true", - ) - - match (application.file_runtime_state(), configured.file_runtime_state()) { - (Some(snapshot), Some(other)) => { - inspect(snapshot.file.path == other.file.path, content="true") - inspect(snapshot.file.available == other.file.available, content="true") - inspect(snapshot.queued == other.queued, content="true") - inspect(snapshot.pending_count == other.pending_count, content="true") - inspect(snapshot.dropped_count == other.dropped_count, content="true") - } - _ => inspect(false, content="true") - } - - inspect(application.file_flush() == configured.file_flush(), content="true") - inspect( - application.pending_count() == configured.pending_count(), - content="true", - ) - inspect(application.file_close() == configured.file_close(), content="true") -} - -///| -test "application logger file controls match direct configured builder behavior" { - let config = LoggerConfig::new( - min_level=Level::Warn, - target="app.file.controls.same-build", - sink=SinkConfig::new( - kind=SinkKind::File, - path="app-file-controls-same-build.log", - ), - ) - let application = build_application_logger(config) - let configured = build_logger(config) - - inspect( - application.file_set_append_mode(false) == - configured.file_set_append_mode(false), - content="true", - ) - inspect( - application.file_append_mode() == configured.file_append_mode(), - content="true", - ) - - inspect( - application.file_set_auto_flush(false) == - configured.file_set_auto_flush(false), - content="true", - ) - inspect( - application.file_auto_flush() == configured.file_auto_flush(), - content="true", - ) - - let rotation = Some(file_rotation(48, max_backups=3)) - inspect( - application.file_set_rotation(rotation) == - configured.file_set_rotation(rotation), - content="true", - ) - inspect( - application.file_rotation_enabled() == configured.file_rotation_enabled(), - content="true", - ) - - let application_policy = application.file_policy() - let configured_policy = configured.file_policy() - inspect(application_policy.append == configured_policy.append, content="true") - inspect( - application_policy.auto_flush == configured_policy.auto_flush, - content="true", - ) - inspect( - match application_policy.rotation { - Some(value) => - match configured_policy.rotation { - Some(other) => - value.max_bytes == other.max_bytes && - value.max_backups == other.max_backups - None => false - } - None => configured_policy.rotation is None - }, - content="true", - ) - - inspect( - application.file_reopen_append() == configured.file_reopen_append(), - content="true", - ) - inspect( - application.file_append_mode() == configured.file_append_mode(), - content="true", - ) - inspect( - application.file_reopen_truncate() == configured.file_reopen_truncate(), - content="true", - ) - inspect( - application.file_append_mode() == configured.file_append_mode(), - content="true", - ) - inspect( - application.file_reopen_with_current_policy() == - configured.file_reopen_with_current_policy(), - content="true", - ) - - inspect( - application.file_clear_rotation() == configured.file_clear_rotation(), - content="true", - ) - inspect( - application.file_rotation_enabled() == configured.file_rotation_enabled(), - content="true", - ) - inspect( - application.file_policy_matches_default() == - configured.file_policy_matches_default(), - content="true", - ) - inspect( - application.file_reset_policy() == configured.file_reset_policy(), - content="true", - ) - inspect( - application.file_policy_matches_default() == - configured.file_policy_matches_default(), - content="true", - ) - inspect( - application.file_reset_failure_counters() == - configured.file_reset_failure_counters(), - content="true", - ) - - inspect(application.file_close() == configured.file_close(), content="true") -} - -///| -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 application logger file helpers match parsed configured builder behavior" { - let raw = "{\"min_level\":\"warn\",\"target\":\"app.json.file.same-build\",\"queue\":{\"max_pending\":2,\"overflow\":\"DropOldest\"},\"sink\":{\"kind\":\"file\",\"path\":\"app-json-file-same-build.log\"}}" - let application = parse_and_build_application_logger(raw) - let configured = parse_and_build_logger(raw) - - application.error("one") - application.error("two") - application.error("three") - configured.error("one") - configured.error("two") - configured.error("three") - - inspect( - application.pending_count() == configured.pending_count(), - content="true", - ) - inspect( - application.dropped_count() == configured.dropped_count(), - content="true", - ) - inspect( - application.file_available() == configured.file_available(), - content="true", - ) - inspect( - application.file_append_mode() == configured.file_append_mode(), - content="true", - ) - inspect( - application.file_auto_flush() == configured.file_auto_flush(), - content="true", - ) - inspect( - application.file_policy_matches_default() == - configured.file_policy_matches_default(), - content="true", - ) - - let application_state = application.file_state() - let configured_state = configured.file_state() - inspect(application_state.path == configured_state.path, content="true") - inspect( - application_state.available == configured_state.available, - content="true", - ) - inspect(application_state.append == configured_state.append, content="true") - inspect( - application_state.auto_flush == configured_state.auto_flush, - content="true", - ) - inspect( - application_state.open_failures == configured_state.open_failures, - content="true", - ) - inspect( - application_state.write_failures == configured_state.write_failures, - content="true", - ) - inspect( - application_state.flush_failures == configured_state.flush_failures, - content="true", - ) - inspect( - application_state.rotation_failures == configured_state.rotation_failures, - content="true", - ) - - match (application.file_runtime_state(), configured.file_runtime_state()) { - (Some(snapshot), Some(other)) => { - inspect(snapshot.file.path == other.file.path, content="true") - inspect(snapshot.file.available == other.file.available, content="true") - inspect(snapshot.queued == other.queued, content="true") - inspect(snapshot.pending_count == other.pending_count, content="true") - inspect(snapshot.dropped_count == other.dropped_count, content="true") - } - _ => inspect(false, content="true") - } - - inspect(application.file_flush() == configured.file_flush(), content="true") - inspect( - application.pending_count() == configured.pending_count(), - content="true", - ) - inspect(application.file_close() == configured.file_close(), content="true") -} - -///| -test "parsed application logger file controls match parsed configured builder behavior" { - let raw = "{\"min_level\":\"warn\",\"target\":\"app.json.file.controls.same-build\",\"sink\":{\"kind\":\"file\",\"path\":\"app-json-file-controls-same-build.log\"}}" - let application = parse_and_build_application_logger(raw) - let configured = parse_and_build_logger(raw) - - inspect( - application.file_set_append_mode(false) == - configured.file_set_append_mode(false), - content="true", - ) - inspect( - application.file_append_mode() == configured.file_append_mode(), - content="true", - ) - inspect( - application.file_set_auto_flush(false) == - configured.file_set_auto_flush(false), - content="true", - ) - inspect( - application.file_auto_flush() == configured.file_auto_flush(), - content="true", - ) - - let rotation = Some(file_rotation(40, max_backups=2)) - inspect( - application.file_set_rotation(rotation) == - configured.file_set_rotation(rotation), - content="true", - ) - inspect( - application.file_rotation_enabled() == configured.file_rotation_enabled(), - content="true", - ) - inspect( - application.file_reopen(append=Some(false)) == - configured.file_reopen(append=Some(false)), - content="true", - ) - inspect( - application.file_append_mode() == configured.file_append_mode(), - content="true", - ) - inspect( - application.file_reopen_append() == configured.file_reopen_append(), - content="true", - ) - inspect( - application.file_append_mode() == configured.file_append_mode(), - content="true", - ) - inspect( - application.file_clear_rotation() == configured.file_clear_rotation(), - content="true", - ) - inspect( - application.file_rotation_enabled() == configured.file_rotation_enabled(), - content="true", - ) - inspect( - application.file_reset_policy() == configured.file_reset_policy(), - content="true", - ) - inspect( - application.file_policy_matches_default() == - configured.file_policy_matches_default(), - content="true", - ) - inspect( - application.file_reset_failure_counters() == - configured.file_reset_failure_counters(), - content="true", - ) - - inspect(application.file_close() == configured.file_close(), content="true") -} - -///| -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 "parsed configured 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 = parse_and_build_logger( - "{\"min_level\":\"warn\",\"target\":\"parsed.default\",\"sink\":{\"kind\":\"console\"}}", - ) - .with_patch(fn(rec) { - captured_target.val = rec.target - captured_message.val = rec.message - captured_fields.val = rec.fields - rec - }) - .with_context_fields([field("service", "bitlogger")]) - - logger.log( - Level::Error, - "override", - fields=[field("mode", "test")], - target="parsed.override", - ) - inspect(captured_target.val, content="parsed.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="parsed.default") - inspect(captured_message.val, content="default") - inspect(captured_fields.val.length(), content="1") - inspect(captured_fields.val[0].key, content="service") -} - -///| -test "parsed configured 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 = parse_and_build_logger( - "{\"min_level\":\"info\",\"target\":\"parsed.helpers\",\"sink\":{\"kind\":\"console\"}}", - ) - .with_patch(fn(rec) { - captured_levels.val.push(rec.level.label()) - captured_targets.val.push(rec.target) - captured_fields.val.push(rec.fields) - rec - }) - .with_context_fields([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="parsed.helpers") - inspect(captured_targets.val[1], content="parsed.helpers") - inspect(captured_targets.val[2], content="parsed.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 "sync parse-build facades forward config errors" { - let raw = "{\"sink\":{\"kind\":\"file\",\"path\":\"\"}}" - let malformed = "{" - - 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") - - let configured_invalid_json = (fn() -> String raise ConfigError { - ignore(parse_and_build_logger(malformed)) - "no error" - })() catch { - ConfigError::InvalidConfig(message) => message - } - inspect(configured_invalid_json.contains("Invalid JSON:"), content="true") - - let application_invalid_json = (fn() -> String raise ConfigError { - ignore(parse_and_build_application_logger(malformed)) - "no error" - })() catch { - ConfigError::InvalidConfig(message) => message - } - inspect(application_invalid_json.contains("Invalid JSON:"), content="true") - - let library_invalid_json = (fn() -> String raise ConfigError { - ignore(parse_and_build_library_logger(malformed)) - "no error" - })() catch { - ConfigError::InvalidConfig(message) => message - } - inspect(library_invalid_json.contains("Invalid JSON:"), content="true") + inspect(captured_fields.val[1].key, content="scope") + inspect(captured_fields.val[1].value, content="audit") + inspect(captured_fields.val[2].key, content="mode") + inspect(captured_fields.val[2].value, content="test") } diff --git a/src/BitLogger_wbtest.mbt b/src/BitLogger_wbtest.mbt index f6af4a2..9bd8f8d 100644 --- a/src/BitLogger_wbtest.mbt +++ b/src/BitLogger_wbtest.mbt @@ -1,708 +1,16 @@ -///| -test "default logger can be reconfigured" { - set_default_min_level(Level::Debug) - set_default_target("global") - let logger = default_logger() - inspect(logger.min_level.label(), content="DEBUG") - inspect(logger.target, content="global") -} - -///| -test "default logger snapshots shared defaults at creation time" { - set_default_min_level(Level::Warn) - set_default_target("before") - let before = default_logger() - - set_default_min_level(Level::Debug) - set_default_target("after") - let after = default_logger() - - inspect(before.is_enabled(Level::Error), content="true") - inspect(before.is_enabled(Level::Info), content="false") - inspect(before.target, content="before") - - inspect(after.is_enabled(Level::Error), content="true") - inspect(after.is_enabled(Level::Info), content="true") - inspect(after.target, content="after") -} - -///| -test "logger can enable timestamps" { - let logger = Logger::new(console_sink(), min_level=Level::Info, target="time").with_timestamp() - inspect(logger.timestamp, content="true") -} - -///| -test "text formatter can customize visible parts" { - let rec = record(Level::Info, "hello", timestamp_ms=123UL, target="svc.api", fields=[ - field("user", "alice"), - field("request_id", "42"), - ]) - let compact = text_formatter( - show_timestamp=false, - show_target=false, - field_separator=",", - ) - inspect( - format_text(rec, formatter=compact), - content="[INFO] hello user=alice,request_id=42", - ) -} - -///| -test "text formatter can emit message only" { - let rec = record( - Level::Warn, - "just message", - timestamp_ms=999UL, - target="svc", - fields=[field("ignored", "yes")], - ) - let message_only = text_formatter( - show_timestamp=false, - show_level=false, - show_target=false, - show_fields=false, - ) - inspect(format_text(rec, formatter=message_only), content="just message") -} - -///| -test "text formatter supports template rendering" { - let rec = record( - Level::Info, - "template hello", - timestamp_ms=123UL, - target="svc.api", - fields=[field("user", "alice"), field("request_id", "42")], - ) - let formatter = text_formatter( - separator=" | ", - field_separator=",", - template="[{level}] {target} {message} :: {fields} @{timestamp}", - ) - inspect( - format_text(rec, formatter~), - content="[INFO] svc.api template hello :: user=alice,request_id=42 @123", - ) -} - -///| -test "text formatter can render ansi level colors" { - let rec = record(Level::Error, "boom", target="svc") - let formatter = text_formatter(color_mode=ColorMode::Always) - inspect( - format_text(rec, formatter~), - content="[\u{001b}[31;1mERROR\u{001b}[0m] [\u{001b}[34msvc\u{001b}[0m] boom", - ) -} - -///| -test "text formatter auto color respects NO_COLOR" { - let rec = record(Level::Warn, "boom", target="svc") - let previous = @env.get_env_var("NO_COLOR") - @env.set_env_var("NO_COLOR", "1") - inspect( - format_text(rec, formatter=text_formatter(color_mode=ColorMode::Auto)), - content="[WARN] [svc] boom", - ) - match previous { - Some(value) => @env.set_env_var("NO_COLOR", value) - None => @env.unset_env_var("NO_COLOR") - } -} - -///| -test "text formatter renders named inline color tags in ansi mode" { - let rec = record(Level::Info, "boom", target="svc") - inspect( - format_text(rec, formatter=text_formatter(color_mode=ColorMode::Always)), - content="[\u{001b}[32mINFO\u{001b}[0m] [\u{001b}[34msvc\u{001b}[0m] \u{001b}[31mboom\u{001b}[0m", - ) -} - -///| -test "text formatter strips inline tags in plain mode" { - let rec = record(Level::Info, "boom bold", target="svc") - inspect( - format_text(rec, formatter=text_formatter(color_mode=ColorMode::Never)), - content="[INFO] [svc] boom bold", - ) -} - -///| -test "text formatter supports nested inline tags" { - let rec = record(Level::Info, "fatal") - inspect( - format_text( - rec, - formatter=text_formatter( - show_level=false, - show_target=false, - color_mode=ColorMode::Always, - ), - ), - content="\u{001b}[31;1mfatal\u{001b}[0m", - ) -} - -///| -test "text formatter supports named closing tags" { - let rec = record(Level::Info, "boom") - inspect( - format_text( - rec, - formatter=text_formatter( - show_level=false, - show_target=false, - color_mode=ColorMode::Always, - ), - ), - content="\u{001b}[31mboom\u{001b}[0m", - ) -} - -///| -test "text formatter supports mixed short and named closing tags" { - let rec = record(Level::Info, "fatal") - inspect( - format_text( - rec, - formatter=text_formatter( - show_level=false, - show_target=false, - color_mode=ColorMode::Always, - ), - ), - content="\u{001b}[31;1mfatal\u{001b}[0m", - ) -} - -///| -test "text formatter keeps unmatched named closing tags as plain text" { - let rec = record(Level::Info, "boom") - inspect( - format_text( - rec, - formatter=text_formatter(show_level=false, show_target=false), - ), - content="boom", - ) -} - -///| -test "text formatter supports hex inline colors" { - let rec = record(Level::Info, "<#ff0000>hot bg") - inspect( - format_text( - rec, - formatter=text_formatter( - show_level=false, - show_target=false, - color_mode=ColorMode::Always, - ), - ), - content="\u{001b}[38;2;255;0;0mhot\u{001b}[0m \u{001b}[48;2;1;2;3mbg\u{001b}[0m", - ) -} - -///| -test "text formatter can downgrade hex colors to basic ansi" { - let rec = record(Level::Info, "<#ff0000>hot bg") - inspect( - format_text( - rec, - formatter=text_formatter( - show_level=false, - show_target=false, - color_mode=ColorMode::Always, - color_support=ColorSupport::Basic, - ), - ), - content="\u{001b}[31mhot\u{001b}[0m \u{001b}[100mbg\u{001b}[0m", - ) -} - -///| -test "text formatter keeps unknown inline tags as plain text" { - let rec = record(Level::Info, "boom") - inspect( - format_text( - rec, - formatter=text_formatter(show_level=false, show_target=false), - ), - content="boom", - ) -} - -///| -test "text formatter can disable style markup parsing" { - let rec = record(Level::Info, "boom bold", target="svc") - inspect( - format_text( - rec, - formatter=text_formatter(color_mode=ColorMode::Always).without_style_markup(), - ), - content="[\u{001b}[32mINFO\u{001b}[0m] [\u{001b}[34msvc\u{001b}[0m] boom bold", - ) -} - -///| -test "text formatter builtin style markup mode ignores custom tags" { - let formatter = text_formatter( - show_level=false, - show_target=false, - color_mode=ColorMode::Always, - style_markup=StyleMarkupMode::Builtin, - ).with_style_tags( - style_tag_registry().set_tag("brand", fg=Some("#4cc9f0"), bold=true), - ) - let rec = record(Level::Info, "custom builtin") - inspect( - format_text(rec, formatter~), - content="custom \u{001b}[31mbuiltin\u{001b}[0m", - ) -} - -///| -test "style markup mode label is stable" { - inspect( - style_markup_mode_label(StyleMarkupMode::Disabled), - content="disabled", - ) - inspect(style_markup_mode_label(StyleMarkupMode::Builtin), content="builtin") - inspect(style_markup_mode_label(StyleMarkupMode::Full), content="full") -} - -///| -test "text formatter supports custom style tags" { - let formatter = text_formatter( - show_level=false, - show_target=false, - color_mode=ColorMode::Always, - ).with_style_tags( - style_tag_registry().set_tag("accent", fg=Some("#4cc9f0"), bold=true), - ) - let rec = record(Level::Info, "api") - inspect( - format_text(rec, formatter~), - content="\u{001b}[38;2;76;201;240;1mapi\u{001b}[0m", - ) -} - -///| -test "text formatter can override builtin style tags" { - let formatter = text_formatter( - show_level=false, - show_target=false, - color_mode=ColorMode::Always, - ).with_style_tags( - default_style_tag_registry().set_tag( - "red", - fg=Some("#ff5a5f"), - underline=true, - ), - ) - let rec = record(Level::Info, "alert") - inspect( - format_text(rec, formatter~), - content="\u{001b}[38;2;255;90;95;4malert\u{001b}[0m", - ) -} - -///| -test "formatter style tags take priority over global style tags" { - let previous = global_style_tag_registry() - set_global_style_tag_registry( - style_tag_registry().set_tag("accent", fg=Some("#123456")), - ) - let formatter = text_formatter( - show_level=false, - show_target=false, - color_mode=ColorMode::Always, - ).with_style_tags(style_tag_registry().set_tag("accent", fg=Some("#abcdef"))) - let rec = record(Level::Info, "tag") - inspect( - format_text(rec, formatter~), - content="\u{001b}[38;2;171;205;239mtag\u{001b}[0m", - ) - set_global_style_tag_registry(previous) -} - -///| -test "global style tags apply when formatter has no local registry" { - let previous = global_style_tag_registry() - set_global_style_tag_registry( - style_tag_registry().set_tag("accent", fg=Some("#102030"), dim=true), - ) - let rec = record(Level::Info, "tag") - inspect( - format_text( - rec, - formatter=text_formatter( - show_level=false, - show_target=false, - color_mode=ColorMode::Always, - ), - ), - content="\u{001b}[38;2;16;32;48;2mtag\u{001b}[0m", - ) - set_global_style_tag_registry(previous) -} - -///| -test "reset global style tag registry restores builtin defaults" { - set_global_style_tag_registry( - style_tag_registry().set_tag("accent", fg=Some("#123456"), bold=true), - ) - reset_global_style_tag_registry() - let rec = record(Level::Info, "ok") - inspect( - format_text( - rec, - formatter=text_formatter( - show_level=false, - show_target=false, - color_mode=ColorMode::Always, - ), - ), - content="\u{001b}[32;1mok\u{001b}[0m", - ) -} - -///| -test "style tag alias can reuse builtin tags" { - let formatter = text_formatter( - show_level=false, - show_target=false, - color_mode=ColorMode::Always, - ).with_style_tags(style_tag_registry().define_alias("danger", "red")) - let rec = record(Level::Info, "boom") - inspect(format_text(rec, formatter~), content="\u{001b}[31mboom\u{001b}[0m") -} - -///| -test "builtin semantic style tags are available" { - let formatter = text_formatter( - show_level=false, - show_target=false, - color_mode=ColorMode::Always, - style_markup=StyleMarkupMode::Builtin, - ) - let rec = record( - Level::Info, - "ok careful boom quiet", - ) - inspect( - format_text(rec, formatter~), - content="\u{001b}[32;1mok\u{001b}[0m \u{001b}[33;1mcareful\u{001b}[0m \u{001b}[31;1mboom\u{001b}[0m \u{001b}[90;2mquiet\u{001b}[0m", - ) -} - -///| -test "builtin semantic style tags can still be overridden" { - let formatter = text_formatter( - show_level=false, - show_target=false, - color_mode=ColorMode::Always, - ).with_style_tags( - default_style_tag_registry().set_tag( - "success", - fg=Some("#00ffaa"), - underline=true, - ), - ) - let rec = record(Level::Info, "ok") - inspect( - format_text(rec, formatter~), - content="\u{001b}[38;2;0;255;170;4mok\u{001b}[0m", - ) -} - -///| -test "text formatter can enable markup for target separately" { - let formatter = text_formatter( - show_level=false, - color_mode=ColorMode::Always, - target_style_markup=StyleMarkupMode::Builtin, - ) - let rec = record(Level::Info, "hello", target="svc") - inspect( - format_text(rec, formatter~), - content="[\u{001b}[34m\u{001b}[31;1msvc\u{001b}[0m\u{001b}[0m] hello", - ) -} - -///| -test "text formatter can enable markup for field values separately" { - let formatter = text_formatter( - show_level=false, - show_target=false, - color_mode=ColorMode::Always, - fields_style_markup=StyleMarkupMode::Builtin, - ) - let rec = record(Level::Info, "hello", fields=[ - field("status", "ok"), - ]) - inspect( - format_text(rec, formatter~), - content="hello \u{001b}[35mstatus=\u{001b}[32;1mok\u{001b}[0m\u{001b}[0m", - ) -} - -///| -test "text formatter leaves field keys raw when field markup is enabled" { - let formatter = text_formatter( - show_level=false, - show_target=false, - color_mode=ColorMode::Always, - fields_style_markup=StyleMarkupMode::Builtin, - ) - let rec = record(Level::Info, "hello", fields=[ - field("status", "ok"), - ]) - inspect( - format_text(rec, formatter~), - content="hello \u{001b}[35mstatus=ok\u{001b}[0m", - ) -} - -///| -test "text formatter template respects disabled fields" { - let rec = record(Level::Warn, "just message", target="svc") - let formatter = text_formatter( - show_target=false, - show_fields=false, - template="[{level}] {target}{message}{fields}", - ) - inspect(format_text(rec, formatter~), content="[WARN] just message") -} - -///| -test "formatted callback sink receives rendered text" { - let rendered : Ref[String] = Ref("") - let sink = text_callback_sink( - text_formatter(show_timestamp=false, separator=" | "), - fn(text) { rendered.val = text }, - ) - let logger = Logger::new(sink, min_level=Level::Info, target="svc") - logger.info("hello", fields=[field("user", "alice")]) - inspect(rendered.val, content="[INFO] | [svc] | hello | user=alice") -} - -///| -test "native file support flag is queryable" { - inspect( - native_files_supported() == true || native_files_supported() == false, - content="true", - ) -} - -///| -test "file sink availability reflects backend support" { - let sink = file_sink("bitlogger-test.log") - let state = sink.state() - inspect(sink.path(), content="bitlogger-test.log") - inspect(sink.is_available() == native_files_supported(), content="true") - inspect(state.path, content="bitlogger-test.log") - inspect(state.available == sink.is_available(), content="true") - inspect(state.append == sink.append_mode(), content="true") - inspect(state.auto_flush == sink.auto_flush_enabled(), content="true") - inspect( - (state.rotation is None) == (sink.rotation_config() is None), - content="true", - ) - inspect(sink.append_mode(), content="true") - inspect(sink.auto_flush_enabled(), content="true") - inspect(sink.rotation_enabled(), content="false") - inspect(sink.rotation_config() is None, content="true") - inspect( - sink.open_failures(), - content=if sink.is_available() { "0" } else { "1" }, - ) - inspect(sink.write_failures(), content="0") - inspect(sink.flush_failures(), content="0") - if sink.is_available() { - inspect(sink.flush(), content="true") - inspect(sink.close(), content="true") - } else { - inspect(sink.flush(), content="false") - inspect(sink.close(), content="false") - } -} - -///| -test "file sink unavailable backend keeps stable fallback state" { - let sink = file_sink( - "bitlogger-unavailable.log", - rotation=Some(file_rotation(64, max_backups=2)), - ) - if sink.is_available() { - inspect(sink.state().available, content="true") - ignore(sink.close()) - } else { - let state = sink.state() - inspect(native_files_supported(), content="false") - inspect(state.available, content="false") - inspect(sink.flush(), content="false") - inspect(sink.close(), content="false") - inspect(sink.write_failures(), content="0") - sink.write(record(Level::Info, "dropped")) - inspect(sink.write_failures(), content="1") - inspect(sink.rotation_failures(), content="0") - match state.rotation { - Some(rotation) => { - inspect(rotation.max_bytes, content="64") - inspect(rotation.max_backups, content="2") - } - None => inspect(false, content="true") - } - } -} - -///| -test "file sink rotation config normalizes invalid inputs" { - let rotation = file_rotation(0, max_backups=0) - inspect(rotation.max_bytes, content="1") - inspect(rotation.max_backups, content="1") - let sink = file_sink("bitlogger-rotation-config.log", rotation=Some(rotation)) - inspect(sink.rotation_enabled(), content="true") - match sink.rotation_config() { - Some(config) => { - inspect(config.max_bytes, content="1") - inspect(config.max_backups, content="1") - } - None => inspect(false, content="true") - } -} - -///| -test "file rotation i64 preserves wide threshold metadata" { - let rotation = file_rotation_i64(4294967296L, max_backups=2) - inspect(rotation.max_bytes, content="2147483647") - inspect(rotation.max_backups, content="2") - match rotation.native_wide_max_bytes { - Some(value) => inspect(value, content="4294967296") - None => inspect(false, content="true") - } -} - -///| -test "file sink setters update auto flush and rotation state" { - let sink = file_sink("bitlogger-setters.log") - let default_policy = sink.default_policy() - inspect(sink.append_mode(), content="true") - inspect(sink.auto_flush_enabled(), content="true") - inspect(sink.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(sink.policy_matches_default(), content="true") - sink.set_append_mode(false) - inspect(sink.append_mode(), content="false") - sink.set_auto_flush(false) - inspect(sink.auto_flush_enabled(), content="false") - sink.set_rotation(Some(file_rotation(32, max_backups=2))) - inspect(sink.rotation_enabled(), content="true") - match sink.rotation_config() { - Some(config) => { - inspect(config.max_bytes, content="32") - inspect(config.max_backups, content="2") - } - None => inspect(false, content="true") - } - sink.clear_rotation() - inspect(sink.rotation_enabled(), content="false") - inspect(sink.rotation_config() is None, content="true") - inspect( - sink.reopen(), - content=if sink.is_available() { "true" } else { "false" }, - ) - inspect(sink.append_mode(), content="false") - let state = sink.state() - let policy = sink.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(sink.policy_matches_default(), content="false") - sink.reset_policy() - inspect(sink.append_mode(), content="true") - inspect(sink.auto_flush_enabled(), content="true") - inspect(sink.rotation_config() is None, content="true") - inspect(sink.policy_matches_default(), content="true") -} - -///| -test "file sink reset policy restores configured defaults" { - let sink = file_sink( - "bitlogger-reset-policy.log", - append=false, - auto_flush=false, - rotation=Some(file_rotation(24, max_backups=3)), - ) - sink.set_append_mode(true) - sink.set_auto_flush(true) - sink.clear_rotation() - let default_policy = sink.default_policy() - inspect(default_policy.append, content="false") - inspect(default_policy.auto_flush, content="false") - inspect(sink.policy_matches_default(), content="false") - sink.reset_policy() - inspect(sink.append_mode(), content="false") - inspect(sink.auto_flush_enabled(), content="false") - inspect(sink.policy_matches_default(), content="true") - match sink.rotation_config() { - Some(rotation) => { - inspect(rotation.max_bytes, content="24") - inspect(rotation.max_backups, content="3") - } - None => inspect(false, content="true") - } -} - -///| -test "file sink set policy applies bundled runtime policy" { - let sink = file_sink("bitlogger-set-policy.log") - let default_policy = sink.default_policy() - sink.set_policy( - FileSinkPolicy::new( - append=false, - auto_flush=false, - rotation=Some(file_rotation(18, max_backups=2)), - ), - ) - let policy = sink.policy() - inspect(policy.append, content="false") - inspect(policy.auto_flush, content="false") - match policy.rotation { - Some(rotation) => { - inspect(rotation.max_bytes, content="18") - inspect(rotation.max_backups, content="2") - } - 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(sink.policy_matches_default(), content="false") -} - ///| test "file sink tracks rotation failures on unavailable backend" { let path = "bitlogger-rotate.log" - ignore(remove_file_internal(path)) - ignore(remove_file_internal(path + ".1")) + ignore(@utils.remove_file_internal(path)) + ignore(@utils.remove_file_internal(path + ".1")) let sink = file_sink(path, rotation=Some(file_rotation(1, max_backups=1))) sink.write(record(Level::Info, "a")) if sink.is_available() { inspect(sink.rotation_failures(), content="0") inspect(sink.write_failures(), content="0") ignore(sink.close()) - ignore(remove_file_internal(path)) - ignore(remove_file_internal(path + ".1")) + ignore(@utils.remove_file_internal(path)) + ignore(@utils.remove_file_internal(path + ".1")) } else { inspect(sink.rotation_failures(), content="0") inspect(sink.write_failures(), content="1") @@ -737,7 +45,7 @@ test "file sink reopen and failure counters reflect backend state" { inspect(sink.flush_failures(), content="0") inspect(sink.rotation_failures(), content="0") inspect(sink.close(), content="true") - ignore(remove_file_internal("bitlogger-reopen.log")) + ignore(@utils.remove_file_internal("bitlogger-reopen.log")) } else { inspect(sink.append_mode(), content="true") inspect(sink.open_failures(), content="1") @@ -764,9 +72,9 @@ test "file sink reopen and failure counters reflect backend state" { ///| test "file sink can rotate on native backend" { let path = "bitlogger-rotate-native.log" - ignore(remove_file_internal(path)) - ignore(remove_file_internal(path + ".1")) - ignore(remove_file_internal(path + ".2")) + ignore(@utils.remove_file_internal(path)) + ignore(@utils.remove_file_internal(path + ".1")) + ignore(@utils.remove_file_internal(path + ".2")) let sink = file_sink( path, auto_flush=true, @@ -776,423 +84,78 @@ test "file sink can rotate on native backend" { if sink.is_available() { sink.write(record(Level::Info, "1234567890")) sink.write(record(Level::Info, "abcdefghij")) - let current = open_file_handle_internal(path, true) - let rotated = open_file_handle_internal(path + ".1", true) + let current = @utils.open_file_handle_internal(path, true) + let rotated = @utils.open_file_handle_internal(path + ".1", true) inspect(current is Some(_), content="true") inspect(rotated is Some(_), content="true") match current { None => () - Some(handle) => ignore(close_file_handle_internal(handle)) + Some(handle) => ignore(@utils.close_file_handle_internal(handle)) } match rotated { None => () - Some(handle) => ignore(close_file_handle_internal(handle)) + Some(handle) => ignore(@utils.close_file_handle_internal(handle)) } inspect(sink.rotation_failures(), content="0") inspect(sink.close(), content="true") - ignore(remove_file_internal(path)) - ignore(remove_file_internal(path + ".1")) - ignore(remove_file_internal(path + ".2")) + ignore(@utils.remove_file_internal(path)) + ignore(@utils.remove_file_internal(path + ".1")) + ignore(@utils.remove_file_internal(path + ".2")) } else { inspect(sink.rotation_failures(), content="0") } } ///| -test "json formatter keeps structured shape" { - let rec = record(Level::Error, "failed", timestamp_ms=55UL, target="svc", fields=[ - field("code", "500"), - ]) - inspect( - format_json(rec), - content="{\"level\":\"ERROR\",\"message\":\"failed\",\"fields\":{\"code\":\"500\"},\"timestamp_ms\":\"55\",\"target\":\"svc\"}", - ) -} +#cfg(platform="windows") +test "file sink rotation failure counts partial backup-chain failure on native backend" { + let path = "bitlogger-rotate-partial-failure.log" + ignore(@utils.remove_file_internal(path)) + ignore(@utils.remove_file_internal(path + ".1")) + ignore(@utils.remove_file_internal(path + ".2")) -///| -test "callback sink receives record" { - let captured_target : Ref[String] = Ref("") - let captured_message : Ref[String] = Ref("") - let logger = Logger::new( - callback_sink(fn(rec) { - captured_target.val = rec.target - captured_message.val = rec.message - }), - min_level=Level::Info, - target="callback", + let seed = file_sink( + path, + auto_flush=true, + rotation=Some(file_rotation(20, max_backups=2)), + formatter=fn(rec) { rec.message }, ) - logger.info("hello") - inspect(captured_target.val, content="callback") - inspect(captured_message.val, content="hello") -} + if seed.is_available() { + seed.write(record(Level::Info, "1234567890")) + seed.write(record(Level::Info, "abcdefghij")) + inspect(seed.rotation_failures(), content="0") + inspect(seed.close(), content="true") -///| -test "split sink routes records by predicate" { - let left_messages : Ref[Array[String]] = Ref([]) - let right_messages : Ref[Array[String]] = Ref([]) - let logger = Logger::new( - split_sink( - callback_sink(fn(rec) { left_messages.val.push(rec.message) }), - callback_sink(fn(rec) { right_messages.val.push(rec.message) }), - fn(rec) { rec.target == "audit" }, - ), - min_level=Level::Info, - target="main", - ) - logger.info("drop to right") - logger.log(Level::Info, "keep on left", target="audit") - inspect(left_messages.val.length(), content="1") - inspect(left_messages.val[0], content="keep on left") - inspect(right_messages.val.length(), content="1") - inspect(right_messages.val[0], content="drop to right") -} + let locked = @utils.open_file_handle_internal(path + ".2", true) + inspect(locked is Some(_), content="true") -///| -test "split_by_level routes warn and error separately" { - let high_messages : Ref[Array[String]] = Ref([]) - let low_messages : Ref[Array[String]] = Ref([]) - let logger = Logger::new( - split_by_level( - callback_sink(fn(rec) { - high_messages.val.push(rec.level.label() + ":" + rec.message) - }), - callback_sink(fn(rec) { - low_messages.val.push(rec.level.label() + ":" + rec.message) - }), - min_level=Level::Warn, - ), - min_level=Level::Trace, - target="split", - ) - logger.info("info") - logger.warn("warn") - logger.error("error") - inspect(high_messages.val.length(), content="2") - inspect(high_messages.val[0], content="WARN:warn") - inspect(high_messages.val[1], content="ERROR:error") - inspect(low_messages.val.length(), content="1") - inspect(low_messages.val[0], content="INFO:info") -} - -///| -test "callback sink sees child target and context logger shape" { - let captured_target : Ref[String] = Ref("") - let captured_message : Ref[String] = Ref("") - let captured_field_count : Ref[Int] = Ref(0) - let captured_timestamp : Ref[UInt64] = Ref(0UL) - let logger = Logger::new( - callback_sink(fn(rec) { - captured_target.val = rec.target - captured_message.val = rec.message - captured_field_count.val = rec.fields.length() - captured_timestamp.val = rec.timestamp_ms - }), - min_level=Level::Info, - target="app", + let sink = file_sink( + path, + auto_flush=true, + rotation=Some(file_rotation(20, max_backups=2)), + formatter=fn(rec) { rec.message }, ) - .child("worker") - .with_context_fields([field("service", "bitlogger")]) - .with_timestamp() - logger.info("ready", fields=[field("mode", "test")]) - inspect(captured_target.val, content="app.worker") - inspect(captured_message.val, content="ready") - inspect(captured_field_count.val, content="2") - inspect(captured_timestamp.val > 0UL, content="true") -} + inspect(sink.is_available(), content="true") + sink.write(record(Level::Info, "klmnopqrst")) -///| -test "bind aliases context fields ergonomically" { - let captured_target : Ref[String] = Ref("") - let captured_message : Ref[String] = Ref("") - let captured_fields : Ref[Array[Field]] = Ref([]) - let logger = Logger::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="bind", - ).bind(fields([("service", "bitlogger"), ("scope", "audit")])) - logger.info("ready", fields=[field("mode", "test")]) - inspect(captured_target.val, content="bind") - inspect(captured_message.val, content="ready") - inspect(captured_fields.val.length(), content="3") - inspect(captured_fields.val[0].key, content="service") - inspect(captured_fields.val[0].value, content="bitlogger") - inspect(captured_fields.val[1].key, content="scope") - inspect(captured_fields.val[1].value, content="audit") - inspect(captured_fields.val[2].key, content="mode") - inspect(captured_fields.val[2].value, content="test") -} + inspect(sink.rotation_failures(), content="1") + inspect(sink.state().rotation_failures, content="1") + inspect(sink.write_failures(), content="1") + inspect(sink.is_available(), content="false") -///| -test "buffered sink flushes manually" { - let flushed_messages : Ref[Array[String]] = Ref([]) - let sink = buffered_sink( - callback_sink(fn(rec) { flushed_messages.val.push(rec.message) }), - flush_limit=10, - ) - let logger = Logger::new(sink, min_level=Level::Info, target="buffered") - logger.info("one") - logger.info("two") - inspect(sink.pending_count(), content="2") - inspect(flushed_messages.val.length(), content="0") - sink.flush() - inspect(sink.pending_count(), content="0") - inspect(flushed_messages.val.length(), content="2") - inspect(flushed_messages.val[0], content="one") - inspect(flushed_messages.val[1], content="two") -} + match locked { + None => () + Some(handle) => ignore(@utils.close_file_handle_internal(handle)) + } -///| -test "buffered sink flushes automatically at limit" { - let flushed_messages : Ref[Array[String]] = Ref([]) - let sink = buffered_sink( - callback_sink(fn(rec) { flushed_messages.val.push(rec.message) }), - flush_limit=2, - ) - let logger = Logger::new(sink, min_level=Level::Info, target="buffered") - logger.info("one") - inspect(sink.pending_count(), content="1") - logger.info("two") - inspect(sink.pending_count(), content="0") - inspect(flushed_messages.val.length(), content="2") - inspect(flushed_messages.val[0], content="one") - inspect(flushed_messages.val[1], content="two") -} + inspect(sink.reopen_append(), content="true") + inspect(sink.is_available(), content="true") + inspect(sink.close(), content="true") -///| -test "filter sink only forwards matching records" { - let flushed_messages : Ref[Array[String]] = Ref([]) - let sink = filter_sink( - callback_sink(fn(rec) { flushed_messages.val.push(rec.message) }), - fn(rec) { rec.target == "kept" }, - ) - let kept = Logger::new(sink, min_level=Level::Info, target="kept") - let dropped = Logger::new(sink, min_level=Level::Info, target="dropped") - kept.info("one") - dropped.info("two") - kept.info("three") - inspect(flushed_messages.val.length(), content="2") - inspect(flushed_messages.val[0], content="one") - inspect(flushed_messages.val[1], content="three") -} - -///| -test "logger with_filter composes naturally" { - let flushed_messages : Ref[Array[String]] = Ref([]) - let logger = Logger::new( - callback_sink(fn(rec) { flushed_messages.val.push(rec.message) }), - min_level=Level::Info, - target="app", - ).with_filter(fn(rec) { rec.target == "app.worker" }) - logger.info("drop at app") - logger.child("worker").info("keep at worker") - inspect(flushed_messages.val.length(), content="1") - inspect(flushed_messages.val[0], content="keep at worker") -} - -///| -test "filter helpers support target level and message composition" { - let flushed_messages : Ref[Array[String]] = Ref([]) - let logger = Logger::new( - callback_sink(fn(rec) { flushed_messages.val.push(rec.message) }), - min_level=Level::Trace, - target="service", - ).with_filter( - all_of([ - target_has_prefix("service"), - level_at_least(Level::Info), - message_contains("visible"), - ]), - ) - logger.debug("visible debug") - logger.info("hidden info") - logger.child("api").info("visible info") - inspect(flushed_messages.val.length(), content="1") - inspect(flushed_messages.val[0], content="visible info") -} - -///| -test "field helpers can match and negate records" { - let flushed_messages : Ref[Array[String]] = Ref([]) - let logger = Logger::new( - callback_sink(fn(rec) { flushed_messages.val.push(rec.message) }), - min_level=Level::Info, - target="fields", - ).with_filter( - all_of([ - has_field("request_id"), - field_equals("kind", "audit"), - not_(target_is("fields.drop")), - ]), - ) - logger.info("missing field") - logger.info("wrong kind", fields=[ - field("request_id", "1"), - field("kind", "trace"), - ]) - logger - .child("drop") - .info("blocked target", fields=[ - field("request_id", "2"), - field("kind", "audit"), - ]) - logger.info("kept", fields=[field("request_id", "3"), field("kind", "audit")]) - inspect(flushed_messages.val.length(), content="1") - inspect(flushed_messages.val[0], content="kept") -} - -///| -test "any_of helper accepts multiple predicates" { - let flushed_messages : Ref[Array[String]] = Ref([]) - let logger = Logger::new( - callback_sink(fn(rec) { flushed_messages.val.push(rec.message) }), - min_level=Level::Info, - target="multi", - ).with_filter( - any_of([target_is("multi.keep"), field_equals("force", "true")]), - ) - logger.info("drop") - logger.child("keep").info("keep by target") - logger.info("keep by field", fields=[field("force", "true")]) - inspect(flushed_messages.val.length(), content="2") - inspect(flushed_messages.val[0], content="keep by target") - inspect(flushed_messages.val[1], content="keep by field") -} - -///| -test "patch sink can rewrite message target and fields" { - let captured_target : Ref[String] = Ref("") - let captured_message : Ref[String] = Ref("") - let captured_fields : Ref[Array[Field]] = Ref([]) - let logger = Logger::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="auth", - ).with_patch( - compose_patches([ - set_target("audit.auth"), - prefix_message("[safe] "), - redact_field("token"), - append_fields([field("service", "bitlogger")]), - ]), - ) - logger.info("login", fields=[field("token", "secret"), field("user", "alice")]) - inspect(captured_target.val, content="audit.auth") - inspect(captured_message.val, content="[safe] login") - inspect(captured_fields.val.length(), content="3") - inspect(captured_fields.val[0].key, content="token") - inspect(captured_fields.val[0].value, content="***") - inspect(captured_fields.val[1].key, content="user") - inspect(captured_fields.val[1].value, content="alice") - inspect(captured_fields.val[2].key, content="service") - inspect(captured_fields.val[2].value, content="bitlogger") -} - -///| -test "patch helpers can redact multiple fields" { - let captured_fields : Ref[Array[Field]] = Ref([]) - let logger = Logger::new( - callback_sink(fn(rec) { captured_fields.val = rec.fields }), - min_level=Level::Info, - target="audit", - ).with_patch(redact_fields(["token", "password"], placeholder="[redacted]")) - logger.info("credentials", fields=[ - field("token", "abc"), - field("password", "123"), - field("user", "alice"), - ]) - inspect(captured_fields.val.length(), content="3") - inspect(captured_fields.val[0].value, content="[redacted]") - inspect(captured_fields.val[1].value, content="[redacted]") - inspect(captured_fields.val[2].value, content="alice") -} - -///| -test "queued sink drains in order" { - let flushed_messages : Ref[Array[String]] = Ref([]) - let sink = queued_sink( - callback_sink(fn(rec) { flushed_messages.val.push(rec.message) }), - ) - let logger = Logger::new(sink, min_level=Level::Info, target="queue") - logger.info("one") - logger.info("two") - logger.info("three") - inspect(sink.pending_count(), content="3") - inspect(sink.dropped_count(), content="0") - inspect(sink.drain(max_items=2), content="2") - inspect(sink.pending_count(), content="1") - inspect(flushed_messages.val.length(), content="2") - inspect(flushed_messages.val[0], content="one") - inspect(flushed_messages.val[1], content="two") - inspect(sink.flush(), content="1") - inspect(sink.pending_count(), content="0") - inspect(flushed_messages.val[2], content="three") -} - -///| -test "queued sink can drop newest when full" { - let flushed_messages : Ref[Array[String]] = Ref([]) - let sink = queued_sink( - callback_sink(fn(rec) { flushed_messages.val.push(rec.message) }), - max_pending=2, - overflow=QueueOverflowPolicy::DropNewest, - ) - let logger = Logger::new(sink, min_level=Level::Info, target="queue") - logger.info("one") - logger.info("two") - logger.info("three") - inspect(sink.pending_count(), content="2") - inspect(sink.dropped_count(), content="1") - inspect(sink.flush(), content="2") - inspect(flushed_messages.val.length(), content="2") - inspect(flushed_messages.val[0], content="one") - inspect(flushed_messages.val[1], content="two") -} - -///| -test "queued sink can drop oldest when full" { - let flushed_messages : Ref[Array[String]] = Ref([]) - let sink = queued_sink( - callback_sink(fn(rec) { flushed_messages.val.push(rec.message) }), - max_pending=2, - overflow=QueueOverflowPolicy::DropOldest, - ) - let logger = Logger::new(sink, min_level=Level::Info, target="queue") - logger.info("one") - logger.info("two") - logger.info("three") - inspect(sink.pending_count(), content="2") - inspect(sink.dropped_count(), content="1") - inspect(sink.flush(), content="2") - inspect(flushed_messages.val.length(), content="2") - inspect(flushed_messages.val[0], content="two") - inspect(flushed_messages.val[1], content="three") -} - -///| -test "logger with_queue preserves chaining ergonomics" { - let flushed_messages : Ref[Array[String]] = Ref([]) - let logger = Logger::new( - callback_sink(fn(rec) { flushed_messages.val.push(rec.message) }), - min_level=Level::Info, - target="service", - ) - .with_patch(prefix_message("[queued] ")) - .with_queue(max_pending=2, overflow=QueueOverflowPolicy::DropOldest) - logger.info("one") - logger.child("api").info("two") - logger.info("three") - inspect(logger.sink.pending_count(), content="2") - inspect(logger.sink.dropped_count(), content="1") - inspect(logger.sink.flush(), content="2") - inspect(flushed_messages.val.length(), content="2") - inspect(flushed_messages.val[0], content="[queued] two") - inspect(flushed_messages.val[1], content="[queued] three") + ignore(@utils.remove_file_internal(path)) + ignore(@utils.remove_file_internal(path + ".1")) + ignore(@utils.remove_file_internal(path + ".2")) + } else { + inspect(native_files_supported(), content="false") + } } diff --git a/src/application_logger_facade_test.mbt b/src/application_logger_facade_test.mbt new file mode 100644 index 0000000..2586b51 --- /dev/null +++ b/src/application_logger_facade_test.mbt @@ -0,0 +1,458 @@ +///| +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 "application logger builder matches direct configured builder behavior" { + let config = LoggerConfig::new( + min_level=Level::Warn, + target="app.runtime.same-build", + timestamp=true, + queue=Some(QueueConfig::new(2, overflow=QueueOverflowPolicy::DropNewest)), + sink=SinkConfig::new(kind=SinkKind::Console), + ) + let application = build_application_logger(config) + let configured = build_logger(config) + + inspect(application.target, content=configured.target) + inspect(application.timestamp, content="true") + inspect(configured.timestamp, content="true") + inspect( + application.is_enabled(Level::Error) == configured.is_enabled(Level::Error), + content="true", + ) + inspect( + application.is_enabled(Level::Info) == configured.is_enabled(Level::Info), + content="true", + ) + let application_sink_kind = match application.sink { + RuntimeSink::QueuedConsole(_) => "QueuedConsole" + RuntimeSink::QueuedJsonConsole(_) => "QueuedJsonConsole" + RuntimeSink::QueuedTextConsole(_) => "QueuedTextConsole" + RuntimeSink::QueuedFile(_) => "QueuedFile" + RuntimeSink::Console(_) => "Console" + RuntimeSink::JsonConsole(_) => "JsonConsole" + RuntimeSink::TextConsole(_) => "TextConsole" + RuntimeSink::File(_) => "File" + } + let configured_sink_kind = match configured.sink { + RuntimeSink::QueuedConsole(_) => "QueuedConsole" + RuntimeSink::QueuedJsonConsole(_) => "QueuedJsonConsole" + RuntimeSink::QueuedTextConsole(_) => "QueuedTextConsole" + RuntimeSink::QueuedFile(_) => "QueuedFile" + RuntimeSink::Console(_) => "Console" + RuntimeSink::JsonConsole(_) => "JsonConsole" + RuntimeSink::TextConsole(_) => "TextConsole" + RuntimeSink::File(_) => "File" + } + inspect(application_sink_kind == configured_sink_kind, content="true") + + application.error("one") + application.error("two") + application.error("three") + configured.error("one") + configured.error("two") + configured.error("three") + + inspect( + application.pending_count() == configured.pending_count(), + content="true", + ) + inspect( + application.dropped_count() == configured.dropped_count(), + content="true", + ) + inspect(application.flush() == configured.flush(), content="true") + inspect( + application.pending_count() == configured.pending_count(), + content="true", + ) +} + +///| +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 build 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 = build_application_logger( + LoggerConfig::new( + min_level=Level::Warn, + target="app.config.default", + sink=SinkConfig::new(kind=SinkKind::Console), + ), + ) + .with_patch(fn(rec) { + captured_target.val = rec.target + captured_message.val = rec.message + captured_fields.val = rec.fields + rec + }) + .with_context_fields([field("service", "bitlogger")]) + + logger.log( + Level::Error, + "override", + fields=[field("mode", "test")], + target="app.config.override", + ) + inspect(captured_target.val, content="app.config.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="app.config.default") + inspect(captured_message.val, content="default") + inspect(captured_fields.val.length(), content="1") + inspect(captured_fields.val[0].key, content="service") +} + +///| +test "application logger build 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 = build_application_logger( + LoggerConfig::new( + min_level=Level::Info, + target="app.config.helpers", + sink=SinkConfig::new(kind=SinkKind::Console), + ), + ) + .with_patch(fn(rec) { + captured_levels.val.push(rec.level.label()) + captured_targets.val.push(rec.target) + captured_fields.val.push(rec.fields) + rec + }) + .with_context_fields([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="app.config.helpers") + inspect(captured_targets.val[1], content="app.config.helpers") + inspect(captured_targets.val[2], content="app.config.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 "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 file helpers match direct configured builder behavior" { + let config = LoggerConfig::new( + min_level=Level::Warn, + target="app.file.same-build", + sink=SinkConfig::new(kind=SinkKind::File, path="app-file-same-build.log"), + queue=Some(QueueConfig::new(2, overflow=QueueOverflowPolicy::DropOldest)), + ) + let application = build_application_logger(config) + let configured = build_logger(config) + + application.error("one") + application.error("two") + application.error("three") + configured.error("one") + configured.error("two") + configured.error("three") + + inspect( + application.pending_count() == configured.pending_count(), + content="true", + ) + inspect( + application.dropped_count() == configured.dropped_count(), + content="true", + ) + inspect( + application.file_available() == configured.file_available(), + content="true", + ) + inspect( + application.file_append_mode() == configured.file_append_mode(), + content="true", + ) + inspect( + application.file_auto_flush() == configured.file_auto_flush(), + content="true", + ) + inspect( + application.file_policy_matches_default() == + configured.file_policy_matches_default(), + content="true", + ) + + let application_policy = application.file_policy() + let configured_policy = configured.file_policy() + inspect(application_policy.append == configured_policy.append, content="true") + inspect( + application_policy.auto_flush == configured_policy.auto_flush, + content="true", + ) + inspect( + match application_policy.rotation { + Some(rotation) => + match configured_policy.rotation { + Some(other) => + rotation.max_bytes == other.max_bytes && + rotation.max_backups == other.max_backups + None => false + } + None => configured_policy.rotation is None + }, + content="true", + ) + + let application_state = application.file_state() + let configured_state = configured.file_state() + inspect(application_state.path == configured_state.path, content="true") + inspect( + application_state.available == configured_state.available, + content="true", + ) + inspect(application_state.append == configured_state.append, content="true") + inspect( + application_state.auto_flush == configured_state.auto_flush, + content="true", + ) + inspect( + application_state.open_failures == configured_state.open_failures, + content="true", + ) + inspect( + application_state.write_failures == configured_state.write_failures, + content="true", + ) + inspect( + application_state.flush_failures == configured_state.flush_failures, + content="true", + ) + inspect( + application_state.rotation_failures == configured_state.rotation_failures, + content="true", + ) + + match (application.file_runtime_state(), configured.file_runtime_state()) { + (Some(snapshot), Some(other)) => { + inspect(snapshot.file.path == other.file.path, content="true") + inspect(snapshot.file.available == other.file.available, content="true") + inspect(snapshot.queued == other.queued, content="true") + inspect(snapshot.pending_count == other.pending_count, content="true") + inspect(snapshot.dropped_count == other.dropped_count, content="true") + } + _ => inspect(false, content="true") + } + + inspect(application.file_flush() == configured.file_flush(), content="true") + inspect( + application.pending_count() == configured.pending_count(), + content="true", + ) + inspect(application.file_close() == configured.file_close(), content="true") +} + +///| +test "application logger file controls match direct configured builder behavior" { + let config = LoggerConfig::new( + min_level=Level::Warn, + target="app.file.controls.same-build", + sink=SinkConfig::new( + kind=SinkKind::File, + path="app-file-controls-same-build.log", + ), + ) + let application = build_application_logger(config) + let configured = build_logger(config) + + inspect( + application.file_set_append_mode(false) == + configured.file_set_append_mode(false), + content="true", + ) + inspect( + application.file_append_mode() == configured.file_append_mode(), + content="true", + ) + + inspect( + application.file_set_auto_flush(false) == + configured.file_set_auto_flush(false), + content="true", + ) + inspect( + application.file_auto_flush() == configured.file_auto_flush(), + content="true", + ) + + let rotation = Some(file_rotation(48, max_backups=3)) + inspect( + application.file_set_rotation(rotation) == + configured.file_set_rotation(rotation), + content="true", + ) + inspect( + application.file_rotation_enabled() == configured.file_rotation_enabled(), + content="true", + ) + + let application_policy = application.file_policy() + let configured_policy = configured.file_policy() + inspect(application_policy.append == configured_policy.append, content="true") + inspect( + application_policy.auto_flush == configured_policy.auto_flush, + content="true", + ) + inspect( + match application_policy.rotation { + Some(value) => + match configured_policy.rotation { + Some(other) => + value.max_bytes == other.max_bytes && + value.max_backups == other.max_backups + None => false + } + None => configured_policy.rotation is None + }, + content="true", + ) + + inspect( + application.file_reopen_append() == configured.file_reopen_append(), + content="true", + ) + inspect( + application.file_append_mode() == configured.file_append_mode(), + content="true", + ) + inspect( + application.file_reopen_truncate() == configured.file_reopen_truncate(), + content="true", + ) + inspect( + application.file_append_mode() == configured.file_append_mode(), + content="true", + ) + inspect( + application.file_reopen_with_current_policy() == + configured.file_reopen_with_current_policy(), + content="true", + ) + + inspect( + application.file_clear_rotation() == configured.file_clear_rotation(), + content="true", + ) + inspect( + application.file_rotation_enabled() == configured.file_rotation_enabled(), + content="true", + ) + inspect( + application.file_policy_matches_default() == + configured.file_policy_matches_default(), + content="true", + ) + inspect( + application.file_reset_policy() == configured.file_reset_policy(), + content="true", + ) + inspect( + application.file_policy_matches_default() == + configured.file_policy_matches_default(), + content="true", + ) + inspect( + application.file_reset_failure_counters() == + configured.file_reset_failure_counters(), + content="true", + ) + + inspect(application.file_close() == configured.file_close(), content="true") +} + +///| diff --git a/src/configured_file_controls_parity_test.mbt b/src/configured_file_controls_parity_test.mbt new file mode 100644 index 0000000..f309d1a --- /dev/null +++ b/src/configured_file_controls_parity_test.mbt @@ -0,0 +1,242 @@ +///| +test "configured logger file setters stay aligned with runtime sink setters" { + let logger = build_logger( + LoggerConfig::new( + target="config.file.setters.delegate", + sink=SinkConfig::new( + kind=SinkKind::File, + path="config-file-setters-delegate.log", + ), + ), + ) + let sink = logger.sink + + inspect( + logger.file_set_append_mode(false) == sink.file_set_append_mode(false), + content="true", + ) + inspect(logger.file_append_mode() == sink.file_append_mode(), content="true") + + inspect( + logger.file_set_auto_flush(false) == sink.file_set_auto_flush(false), + content="true", + ) + inspect(logger.file_auto_flush() == sink.file_auto_flush(), content="true") + + let rotation = Some(file_rotation(48, max_backups=3)) + inspect( + logger.file_set_rotation(rotation) == sink.file_set_rotation(rotation), + content="true", + ) + inspect( + logger.file_rotation_enabled() == sink.file_rotation_enabled(), + content="true", + ) + + let logger_policy = logger.file_policy() + let sink_policy = sink.file_policy() + inspect(logger_policy.append == sink_policy.append, content="true") + inspect(logger_policy.auto_flush == sink_policy.auto_flush, content="true") + inspect( + match logger_policy.rotation { + Some(value) => + match sink_policy.rotation { + Some(other) => + value.max_bytes == other.max_bytes && + value.max_backups == other.max_backups + None => false + } + None => sink_policy.rotation is None + }, + content="true", + ) + + inspect( + logger.file_clear_rotation() == sink.file_clear_rotation(), + content="true", + ) + inspect( + logger.file_rotation_enabled() == sink.file_rotation_enabled(), + content="true", + ) + inspect( + logger.file_policy_matches_default() == sink.file_policy_matches_default(), + content="true", + ) + + let console_logger = build_logger( + LoggerConfig::new( + target="config.file.setters.console", + sink=SinkConfig::new(kind=SinkKind::Console), + ), + ) + let console_sink = console_logger.sink + inspect( + console_logger.file_set_append_mode(true) == + console_sink.file_set_append_mode(true), + content="true", + ) + inspect( + console_logger.file_set_auto_flush(true) == + console_sink.file_set_auto_flush(true), + content="true", + ) + inspect( + console_logger.file_set_rotation(rotation) == + console_sink.file_set_rotation(rotation), + content="true", + ) + inspect( + console_logger.file_clear_rotation() == console_sink.file_clear_rotation(), + content="true", + ) + + ignore(logger.close()) +} + +///| +test "configured logger file reopen helpers stay aligned with runtime sink helpers" { + let logger = build_logger( + LoggerConfig::new( + sink=SinkConfig::new( + kind=SinkKind::File, + path="config-reopen-delegate.log", + ), + ), + ) + let sink = logger.sink + + inspect( + logger.file_set_append_mode(false) == sink.file_set_append_mode(false), + content="true", + ) + inspect(logger.file_reopen() == sink.file_reopen(), content="true") + inspect(logger.file_append_mode() == sink.file_append_mode(), content="true") + + inspect( + logger.file_reopen_append() == sink.file_reopen_append(), + content="true", + ) + inspect(logger.file_append_mode() == sink.file_append_mode(), content="true") + + inspect( + logger.file_reopen_truncate() == sink.file_reopen_truncate(), + content="true", + ) + inspect(logger.file_append_mode() == sink.file_append_mode(), content="true") + + inspect( + logger.file_set_append_mode(true) == sink.file_set_append_mode(true), + content="true", + ) + inspect( + logger.file_reopen_with_current_policy() == + sink.file_reopen_with_current_policy(), + content="true", + ) + inspect(logger.file_append_mode() == sink.file_append_mode(), content="true") + + inspect( + logger.file_reopen(append=Some(false)) == + sink.file_reopen(append=Some(false)), + content="true", + ) + inspect(logger.file_append_mode() == sink.file_append_mode(), content="true") + + let console_logger = build_logger( + LoggerConfig::new(sink=SinkConfig::new(kind=SinkKind::Console)), + ) + let console_sink = console_logger.sink + inspect( + console_logger.file_reopen() == console_sink.file_reopen(), + content="true", + ) + inspect( + console_logger.file_reopen_with_current_policy() == + console_sink.file_reopen_with_current_policy(), + content="true", + ) + inspect( + console_logger.file_reopen_append() == console_sink.file_reopen_append(), + content="true", + ) + inspect( + console_logger.file_reopen_truncate() == console_sink.file_reopen_truncate(), + content="true", + ) + + ignore(logger.close()) +} + +///| +test "configured logger file reset helpers stay aligned with runtime sink helpers" { + let logger = build_logger( + LoggerConfig::new( + sink=SinkConfig::new( + kind=SinkKind::File, + path="config-reset-delegate.log", + append=false, + auto_flush=false, + rotation=Some(file_rotation(40, max_backups=2)), + ), + ), + ) + let sink = logger.sink + + ignore(logger.file_set_append_mode(true)) + ignore(sink.file_set_append_mode(true)) + ignore(logger.file_set_auto_flush(true)) + ignore(sink.file_set_auto_flush(true)) + ignore(logger.file_set_rotation(None)) + ignore(sink.file_set_rotation(None)) + + inspect( + logger.file_reset_policy() == sink.file_reset_policy(), + content="true", + ) + inspect( + logger.file_policy_matches_default() == sink.file_policy_matches_default(), + content="true", + ) + inspect(logger.file_append_mode() == sink.file_append_mode(), content="true") + inspect(logger.file_auto_flush() == sink.file_auto_flush(), content="true") + + inspect( + logger.file_reset_failure_counters() == sink.file_reset_failure_counters(), + content="true", + ) + inspect( + logger.file_open_failures() == sink.file_open_failures(), + content="true", + ) + inspect( + logger.file_write_failures() == sink.file_write_failures(), + content="true", + ) + inspect( + logger.file_flush_failures() == sink.file_flush_failures(), + content="true", + ) + inspect( + logger.file_rotation_failures() == sink.file_rotation_failures(), + content="true", + ) + + let console_logger = build_logger( + LoggerConfig::new(sink=SinkConfig::new(kind=SinkKind::Console)), + ) + let console_sink = console_logger.sink + inspect( + console_logger.file_reset_policy() == console_sink.file_reset_policy(), + content="true", + ) + inspect( + console_logger.file_reset_failure_counters() == + console_sink.file_reset_failure_counters(), + content="true", + ) + + ignore(logger.close()) +} + +///| diff --git a/src/configured_file_controls_test.mbt b/src/configured_file_controls_test.mbt new file mode 100644 index 0000000..3d711e5 --- /dev/null +++ b/src/configured_file_controls_test.mbt @@ -0,0 +1,315 @@ +///| +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 "configured queued file logger blocks policy mutation while queue is pending" { + let logger = build_logger( + LoggerConfig::new( + sink=SinkConfig::new(kind=SinkKind::File, path="config-queued-policy.log"), + queue=Some(QueueConfig::new(4, overflow=QueueOverflowPolicy::DropNewest)), + ), + ) + logger.info("one") + logger.info("two") + inspect(logger.pending_count(), content="2") + inspect(logger.file_set_append_mode(false), content="false") + inspect(logger.file_append_mode(), content="true") + inspect( + logger.file_set_policy( + FileSinkPolicy::new(append=false, auto_flush=false, rotation=None), + ), + content="false", + ) + inspect(logger.file_reopen_truncate(), content="false") + inspect(logger.pending_count(), content="2") + if logger.file_available() { + inspect(logger.file_flush(), content="true") + inspect(logger.pending_count(), content="0") + inspect(logger.file_set_append_mode(false), content="true") + inspect(logger.file_append_mode(), content="false") + inspect(logger.file_reopen_with_current_policy(), content="true") + inspect(logger.file_close(), content="true") + } else { + inspect(logger.file_flush(), content="false") + inspect(logger.pending_count(), content="0") + inspect(logger.file_reopen_with_current_policy(), content="false") + inspect(logger.file_close(), content="false") + } +} + +///| +test "configured queued file logger close drains queue before teardown" { + let logger = build_logger( + LoggerConfig::new( + sink=SinkConfig::new(kind=SinkKind::File, path="config-close-queued.log"), + queue=Some(QueueConfig::new(4, overflow=QueueOverflowPolicy::DropNewest)), + ), + ) + logger.info("one") + logger.info("two") + inspect(logger.pending_count(), content="2") + if logger.file_available() { + inspect(logger.close(), content="true") + inspect(logger.pending_count(), content="0") + inspect(logger.file_available(), content="false") + inspect(logger.file_close(), content="false") + } else { + inspect(logger.close(), content="false") + inspect(logger.pending_count(), content="0") + inspect(logger.file_available(), content="false") + inspect(logger.file_write_failures(), content="2") + inspect(logger.file_close(), content="false") + } +} diff --git a/src/configured_file_observability_parity_test.mbt b/src/configured_file_observability_parity_test.mbt new file mode 100644 index 0000000..c032441 --- /dev/null +++ b/src/configured_file_observability_parity_test.mbt @@ -0,0 +1,452 @@ +///| +test "configured logger file observation helpers stay aligned with runtime sink helpers" { + let file_logger = build_logger( + LoggerConfig::new( + target="config.file.delegate", + sink=SinkConfig::new(kind=SinkKind::File, path="config-file-delegate.log"), + ), + ) + let file_sink = file_logger.sink + + inspect( + file_logger.file_available() == file_sink.file_available(), + content="true", + ) + inspect(file_logger.file_path() == file_sink.file_path(), content="true") + inspect( + file_logger.file_open_failures() == file_sink.file_open_failures(), + content="true", + ) + inspect( + file_logger.file_write_failures() == file_sink.file_write_failures(), + content="true", + ) + inspect( + file_logger.file_flush_failures() == file_sink.file_flush_failures(), + content="true", + ) + inspect( + file_logger.file_rotation_failures() == file_sink.file_rotation_failures(), + content="true", + ) + + let console_logger = build_logger( + LoggerConfig::new( + target="config.file.delegate.console", + sink=SinkConfig::new(kind=SinkKind::Console), + ), + ) + let console_sink = console_logger.sink + + inspect( + console_logger.file_available() == console_sink.file_available(), + content="true", + ) + inspect( + console_logger.file_path() == console_sink.file_path(), + content="true", + ) + inspect( + console_logger.file_open_failures() == console_sink.file_open_failures(), + content="true", + ) + inspect( + console_logger.file_write_failures() == console_sink.file_write_failures(), + content="true", + ) + inspect( + console_logger.file_flush_failures() == console_sink.file_flush_failures(), + content="true", + ) + inspect( + console_logger.file_rotation_failures() == + console_sink.file_rotation_failures(), + content="true", + ) + + ignore(file_logger.close()) +} + +///| +test "configured logger file policy helpers stay aligned with runtime sink helpers" { + let file_logger = build_logger( + LoggerConfig::new( + target="config.file.policy.delegate", + sink=SinkConfig::new( + kind=SinkKind::File, + path="config-file-policy-delegate.log", + append=false, + auto_flush=true, + rotation=Some(file_rotation(64, max_backups=2)), + ), + ), + ) + let file_sink = file_logger.sink + let file_policy = file_logger.file_policy() + let file_sink_policy = file_sink.file_policy() + let file_defaults = file_logger.file_default_policy() + let file_sink_defaults = file_sink.file_default_policy() + let file_policy_has_rotation = match file_policy.rotation { + Some(_) => true + None => false + } + let file_sink_policy_has_rotation = match file_sink_policy.rotation { + Some(_) => true + None => false + } + let file_defaults_has_rotation = match file_defaults.rotation { + Some(_) => true + None => false + } + let file_sink_defaults_has_rotation = match file_sink_defaults.rotation { + Some(_) => true + None => false + } + + inspect( + file_logger.file_append_mode() == file_sink.file_append_mode(), + content="true", + ) + inspect( + file_logger.file_auto_flush() == file_sink.file_auto_flush(), + content="true", + ) + inspect( + file_logger.file_rotation_enabled() == file_sink.file_rotation_enabled(), + content="true", + ) + inspect( + file_logger.file_policy_matches_default() == + file_sink.file_policy_matches_default(), + content="true", + ) + inspect(file_policy.append == file_sink_policy.append, content="true") + inspect(file_policy.auto_flush == file_sink_policy.auto_flush, content="true") + inspect( + file_policy_has_rotation == file_sink_policy_has_rotation, + content="true", + ) + inspect(file_defaults.append == file_sink_defaults.append, content="true") + inspect( + file_defaults.auto_flush == file_sink_defaults.auto_flush, + content="true", + ) + inspect( + file_defaults_has_rotation == file_sink_defaults_has_rotation, + content="true", + ) + + let console_logger = build_logger( + LoggerConfig::new( + target="config.file.policy.console", + sink=SinkConfig::new(kind=SinkKind::Console), + ), + ) + let console_sink = console_logger.sink + let console_policy = console_logger.file_policy() + let console_sink_policy = console_sink.file_policy() + let console_defaults = console_logger.file_default_policy() + let console_sink_defaults = console_sink.file_default_policy() + let console_policy_has_rotation = match console_policy.rotation { + Some(_) => true + None => false + } + let console_sink_policy_has_rotation = match console_sink_policy.rotation { + Some(_) => true + None => false + } + let console_defaults_has_rotation = match console_defaults.rotation { + Some(_) => true + None => false + } + let console_sink_defaults_has_rotation = match + console_sink_defaults.rotation { + Some(_) => true + None => false + } + + inspect( + console_logger.file_append_mode() == console_sink.file_append_mode(), + content="true", + ) + inspect( + console_logger.file_auto_flush() == console_sink.file_auto_flush(), + content="true", + ) + inspect( + console_logger.file_rotation_enabled() == + console_sink.file_rotation_enabled(), + content="true", + ) + inspect( + console_logger.file_policy_matches_default() == + console_sink.file_policy_matches_default(), + content="true", + ) + inspect(console_policy.append == console_sink_policy.append, content="true") + inspect( + console_policy.auto_flush == console_sink_policy.auto_flush, + content="true", + ) + inspect( + console_policy_has_rotation == console_sink_policy_has_rotation, + content="true", + ) + inspect( + console_defaults.append == console_sink_defaults.append, + content="true", + ) + inspect( + console_defaults.auto_flush == console_sink_defaults.auto_flush, + content="true", + ) + inspect( + console_defaults_has_rotation == console_sink_defaults_has_rotation, + content="true", + ) + + ignore(file_logger.close()) +} + +///| +test "configured logger file flush and close stay aligned with runtime sink helpers" { + let file_config = LoggerConfig::new( + sink=SinkConfig::new( + kind=SinkKind::File, + path="config-file-helper-delegate.log", + ), + ) + let file_logger = build_logger(file_config) + let file_sink = build_logger(file_config).sink + + inspect(file_logger.file_flush() == file_sink.file_flush(), content="true") + inspect(file_logger.file_close() == file_sink.file_close(), content="true") + + let console_config = LoggerConfig::new( + sink=SinkConfig::new(kind=SinkKind::Console), + ) + let console_logger = build_logger(console_config) + let console_sink = build_logger(console_config).sink + + inspect( + console_logger.file_flush() == console_sink.file_flush(), + content="true", + ) + inspect( + console_logger.file_close() == console_sink.file_close(), + content="true", + ) +} + +///| +test "configured logger file state helpers stay aligned with runtime sink helpers" { + let file_logger = build_logger( + LoggerConfig::new( + target="config.file.state.delegate", + sink=SinkConfig::new( + kind=SinkKind::File, + path="config-file-state-delegate.log", + ), + ), + ) + let file_sink = file_logger.sink + let file_state = file_logger.file_state() + let file_sink_state = file_sink.file_state() + + inspect(file_state.path == file_sink_state.path, content="true") + inspect(file_state.available == file_sink_state.available, content="true") + inspect(file_state.append == file_sink_state.append, content="true") + inspect(file_state.auto_flush == file_sink_state.auto_flush, content="true") + inspect( + file_state.open_failures == file_sink_state.open_failures, + content="true", + ) + inspect( + file_state.write_failures == file_sink_state.write_failures, + content="true", + ) + inspect( + file_state.flush_failures == file_sink_state.flush_failures, + content="true", + ) + inspect( + file_state.rotation_failures == file_sink_state.rotation_failures, + content="true", + ) + inspect( + match file_state.rotation { + Some(rotation) => + match file_sink_state.rotation { + Some(other) => + rotation.max_bytes == other.max_bytes && + rotation.max_backups == other.max_backups + None => false + } + None => file_sink_state.rotation is None + }, + content="true", + ) + inspect( + (file_logger.file_runtime_state() is None) == + (file_sink.file_runtime_state() is None), + content="true", + ) + + let queued_logger = build_logger( + LoggerConfig::new( + target="config.file.runtime.delegate", + sink=SinkConfig::new( + kind=SinkKind::File, + path="config-file-runtime-delegate.log", + ), + queue=Some(QueueConfig::new(4, overflow=QueueOverflowPolicy::DropNewest)), + ), + ) + let queued_sink = queued_logger.sink + queued_logger.info("one") + queued_logger.info("two") + let queued_state = queued_logger.file_state() + let queued_sink_state = queued_sink.file_state() + + inspect(queued_state.path == queued_sink_state.path, content="true") + inspect(queued_state.available == queued_sink_state.available, content="true") + inspect(queued_state.append == queued_sink_state.append, content="true") + inspect( + queued_state.auto_flush == queued_sink_state.auto_flush, + content="true", + ) + inspect( + queued_state.open_failures == queued_sink_state.open_failures, + content="true", + ) + inspect( + queued_state.write_failures == queued_sink_state.write_failures, + content="true", + ) + inspect( + queued_state.flush_failures == queued_sink_state.flush_failures, + content="true", + ) + inspect( + queued_state.rotation_failures == queued_sink_state.rotation_failures, + content="true", + ) + inspect( + match queued_state.rotation { + Some(rotation) => + match queued_sink_state.rotation { + Some(other) => + rotation.max_bytes == other.max_bytes && + rotation.max_backups == other.max_backups + None => false + } + None => queued_sink_state.rotation is None + }, + content="true", + ) + + match (queued_logger.file_runtime_state(), queued_sink.file_runtime_state()) { + (Some(snapshot), Some(sink_snapshot)) => { + inspect(snapshot.file.path == sink_snapshot.file.path, content="true") + inspect( + snapshot.file.available == sink_snapshot.file.available, + content="true", + ) + inspect(snapshot.file.append == sink_snapshot.file.append, content="true") + inspect( + snapshot.file.auto_flush == sink_snapshot.file.auto_flush, + content="true", + ) + inspect( + snapshot.file.open_failures == sink_snapshot.file.open_failures, + content="true", + ) + inspect( + snapshot.file.write_failures == sink_snapshot.file.write_failures, + content="true", + ) + inspect( + snapshot.file.flush_failures == sink_snapshot.file.flush_failures, + content="true", + ) + inspect( + snapshot.file.rotation_failures == sink_snapshot.file.rotation_failures, + content="true", + ) + inspect(snapshot.queued == sink_snapshot.queued, content="true") + inspect( + snapshot.pending_count == sink_snapshot.pending_count, + content="true", + ) + inspect( + snapshot.dropped_count == sink_snapshot.dropped_count, + content="true", + ) + inspect( + match snapshot.file.rotation { + Some(rotation) => + match sink_snapshot.file.rotation { + Some(other) => + rotation.max_bytes == other.max_bytes && + rotation.max_backups == other.max_backups + None => false + } + None => sink_snapshot.file.rotation is None + }, + content="true", + ) + } + _ => inspect(false, content="true") + } + + let console_logger = build_logger( + LoggerConfig::new( + target="config.file.state.console", + sink=SinkConfig::new(kind=SinkKind::Console), + ), + ) + let console_sink = console_logger.sink + let console_state = console_logger.file_state() + let console_sink_state = console_sink.file_state() + + inspect(console_state.path == console_sink_state.path, content="true") + inspect( + console_state.available == console_sink_state.available, + content="true", + ) + inspect(console_state.append == console_sink_state.append, content="true") + inspect( + console_state.auto_flush == console_sink_state.auto_flush, + content="true", + ) + inspect( + console_state.open_failures == console_sink_state.open_failures, + content="true", + ) + inspect( + console_state.write_failures == console_sink_state.write_failures, + content="true", + ) + inspect( + console_state.flush_failures == console_sink_state.flush_failures, + content="true", + ) + inspect( + console_state.rotation_failures == console_sink_state.rotation_failures, + content="true", + ) + inspect( + (console_state.rotation is None) == (console_sink_state.rotation is None), + content="true", + ) + inspect( + (console_logger.file_runtime_state() is None) == + (console_sink.file_runtime_state() is None), + content="true", + ) + + ignore(file_logger.close()) + ignore(queued_logger.close()) +} + +///| diff --git a/src/configured_file_observability_test.mbt b/src/configured_file_observability_test.mbt new file mode 100644 index 0000000..2d6e5b2 --- /dev/null +++ b/src/configured_file_observability_test.mbt @@ -0,0 +1,103 @@ +///| +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 "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_path_or_none() is None, content="true") + inspect(logger.file_policy_or_none() is None, content="true") + inspect(logger.file_default_policy_or_none() is None, content="true") + inspect(logger.file_state_or_none() is None, content="true") + 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") + match logger.file_path_or_none() { + Some(path) => inspect(path, content="config-capability.log") + None => inspect(false, content="true") + } + inspect(logger.file_policy_or_none() is None, content="false") + inspect(logger.file_default_policy_or_none() is None, content="false") + inspect(logger.file_state_or_none() is None, content="false") + 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") + } +} + +///| diff --git a/src/configured_file_test.mbt b/src/configured_file_test.mbt new file mode 100644 index 0000000..c6ef4af --- /dev/null +++ b/src/configured_file_test.mbt @@ -0,0 +1,10 @@ +///| +// Configured file tests were split by responsibility. +// Add new configured file checks to the focused files in this package instead +// of growing this catch-all file again. +// +// Current layout: +// - configured_file_observability_test.mbt +// - configured_file_controls_test.mbt +// - configured_file_observability_parity_test.mbt +// - configured_file_controls_parity_test.mbt diff --git a/src/configured_logger_facade_test.mbt b/src/configured_logger_facade_test.mbt new file mode 100644 index 0000000..0e05a21 --- /dev/null +++ b/src/configured_logger_facade_test.mbt @@ -0,0 +1,324 @@ +///| +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 "configured logger projection preserves file runtime helpers through unwrap" { + let configured = build_logger( + LoggerConfig::new( + min_level=Level::Warn, + target="lib.projected.file", + sink=SinkConfig::new(kind=SinkKind::File, path="lib-projected-file.log"), + queue=Some(QueueConfig::new(2, overflow=QueueOverflowPolicy::DropOldest)), + ), + ) + let projected = configured.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.file") + + full.error("one") + full.error("two") + full.error("three") + inspect(full.pending_count() == configured.pending_count(), content="true") + inspect(full.dropped_count() == configured.dropped_count(), content="true") + inspect(full.file_available() == configured.file_available(), content="true") + inspect( + full.file_append_mode() == configured.file_append_mode(), + content="true", + ) + inspect( + full.file_auto_flush() == configured.file_auto_flush(), + content="true", + ) + + let full_state = full.file_state() + let configured_state = configured.file_state() + inspect(full_state.path == configured_state.path, content="true") + inspect(full_state.available == configured_state.available, content="true") + inspect(full_state.append == configured_state.append, content="true") + inspect(full_state.auto_flush == configured_state.auto_flush, content="true") + inspect( + full_state.open_failures == configured_state.open_failures, + content="true", + ) + inspect( + full_state.write_failures == configured_state.write_failures, + content="true", + ) + inspect( + full_state.flush_failures == configured_state.flush_failures, + content="true", + ) + inspect( + full_state.rotation_failures == configured_state.rotation_failures, + content="true", + ) + + match (full.file_runtime_state(), configured.file_runtime_state()) { + (Some(snapshot), Some(other)) => { + inspect(snapshot.file.path == other.file.path, content="true") + inspect(snapshot.file.available == other.file.available, content="true") + inspect(snapshot.queued == other.queued, content="true") + inspect(snapshot.pending_count == other.pending_count, content="true") + inspect(snapshot.dropped_count == other.dropped_count, content="true") + } + _ => inspect(false, content="true") + } + + inspect( + full.file_set_append_mode(false) == configured.file_set_append_mode(false), + content="true", + ) + inspect( + full.file_append_mode() == configured.file_append_mode(), + content="true", + ) + inspect( + full.file_set_auto_flush(false) == configured.file_set_auto_flush(false), + content="true", + ) + inspect( + full.file_auto_flush() == configured.file_auto_flush(), + content="true", + ) + inspect( + full.file_set_rotation(Some(file_rotation(36, max_backups=2))) == + configured.file_set_rotation(Some(file_rotation(36, max_backups=2))), + content="true", + ) + inspect( + full.file_rotation_enabled() == configured.file_rotation_enabled(), + content="true", + ) + inspect( + full.file_reopen_append() == configured.file_reopen_append(), + content="true", + ) + inspect( + full.file_append_mode() == configured.file_append_mode(), + content="true", + ) + inspect( + full.file_clear_rotation() == configured.file_clear_rotation(), + content="true", + ) + inspect( + full.file_reset_policy() == configured.file_reset_policy(), + content="true", + ) + inspect( + full.file_policy_matches_default() == + configured.file_policy_matches_default(), + content="true", + ) + inspect( + full.file_reset_failure_counters() == + configured.file_reset_failure_counters(), + content="true", + ) + inspect(full.file_flush() == configured.file_flush(), content="true") + inspect(full.pending_count() == configured.pending_count(), content="true") + + let was_available = configured.file_available() + inspect(configured.file_close() == was_available, content="true") + inspect(full.file_available(), content="false") + inspect(configured.file_available(), content="false") + inspect(full.file_close(), content="false") +} + +///| +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 "configured logger build 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 = build_logger( + LoggerConfig::new( + min_level=Level::Warn, + target="configured.default", + sink=SinkConfig::new(kind=SinkKind::Console), + ), + ) + .with_patch(fn(rec) { + captured_target.val = rec.target + captured_message.val = rec.message + captured_fields.val = rec.fields + rec + }) + .with_context_fields([field("service", "bitlogger")]) + + logger.log( + Level::Error, + "override", + fields=[field("mode", "test")], + target="configured.override", + ) + inspect(captured_target.val, content="configured.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="configured.default") + inspect(captured_message.val, content="default") + inspect(captured_fields.val.length(), content="1") + inspect(captured_fields.val[0].key, content="service") +} + +///| +test "configured logger build 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 = build_logger( + LoggerConfig::new( + min_level=Level::Info, + target="configured.helpers", + sink=SinkConfig::new(kind=SinkKind::Console), + ), + ) + .with_patch(fn(rec) { + captured_levels.val.push(rec.level.label()) + captured_targets.val.push(rec.target) + captured_fields.val.push(rec.fields) + rec + }) + .with_context_fields([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="configured.helpers") + inspect(captured_targets.val[1], content="configured.helpers") + inspect(captured_targets.val[2], content="configured.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") +} + +///| diff --git a/src/file_sink_test.mbt b/src/file_sink_test.mbt new file mode 100644 index 0000000..28bda01 --- /dev/null +++ b/src/file_sink_test.mbt @@ -0,0 +1,199 @@ +///| +test "native file support flag is queryable" { + inspect( + native_files_supported() == true || native_files_supported() == false, + content="true", + ) +} + +///| +test "file sink availability reflects backend support" { + let sink = file_sink("bitlogger-test.log") + let state = sink.state() + inspect(sink.path(), content="bitlogger-test.log") + inspect(sink.is_available() == native_files_supported(), content="true") + inspect(state.path, content="bitlogger-test.log") + inspect(state.available == sink.is_available(), content="true") + inspect(state.append == sink.append_mode(), content="true") + inspect(state.auto_flush == sink.auto_flush_enabled(), content="true") + inspect( + (state.rotation is None) == (sink.rotation_config() is None), + content="true", + ) + inspect(sink.append_mode(), content="true") + inspect(sink.auto_flush_enabled(), content="true") + inspect(sink.rotation_enabled(), content="false") + inspect(sink.rotation_config() is None, content="true") + inspect( + sink.open_failures(), + content=if sink.is_available() { "0" } else { "1" }, + ) + inspect(sink.write_failures(), content="0") + inspect(sink.flush_failures(), content="0") + if sink.is_available() { + inspect(sink.flush(), content="true") + inspect(sink.close(), content="true") + } else { + inspect(sink.flush(), content="false") + inspect(sink.close(), content="false") + } +} + +///| +test "file sink unavailable backend keeps stable fallback state" { + let sink = file_sink( + "bitlogger-unavailable.log", + rotation=Some(file_rotation(64, max_backups=2)), + ) + if sink.is_available() { + inspect(sink.state().available, content="true") + ignore(sink.close()) + } else { + let state = sink.state() + inspect(native_files_supported(), content="false") + inspect(state.available, content="false") + inspect(sink.flush(), content="false") + inspect(sink.close(), content="false") + inspect(sink.write_failures(), content="0") + sink.write(Record::new(Level::Info, "dropped")) + inspect(sink.write_failures(), content="1") + inspect(sink.rotation_failures(), content="0") + match state.rotation { + Some(rotation) => { + inspect(rotation.max_bytes, content="64") + inspect(rotation.max_backups, content="2") + } + None => inspect(false, content="true") + } + } +} + +///| +test "file sink rotation config normalizes invalid inputs" { + let rotation = file_rotation(0, max_backups=0) + inspect(rotation.max_bytes, content="1") + inspect(rotation.max_backups, content="1") + let sink = file_sink("bitlogger-rotation-config.log", rotation=Some(rotation)) + inspect(sink.rotation_enabled(), content="true") + match sink.rotation_config() { + Some(config) => { + inspect(config.max_bytes, content="1") + inspect(config.max_backups, content="1") + } + None => inspect(false, content="true") + } +} + +///| +test "file rotation i64 preserves wide threshold metadata" { + let rotation = file_rotation_i64(4294967296L, max_backups=2) + inspect(rotation.max_bytes, content="2147483647") + inspect(rotation.max_backups, content="2") + match rotation.native_wide_max_bytes { + Some(value) => inspect(value, content="4294967296") + None => inspect(false, content="true") + } +} + +///| +test "file sink setters update auto flush and rotation state" { + let sink = file_sink("bitlogger-setters.log") + let default_policy = sink.default_policy() + inspect(sink.append_mode(), content="true") + inspect(sink.auto_flush_enabled(), content="true") + inspect(sink.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(sink.policy_matches_default(), content="true") + sink.set_append_mode(false) + inspect(sink.append_mode(), content="false") + sink.set_auto_flush(false) + inspect(sink.auto_flush_enabled(), content="false") + sink.set_rotation(Some(file_rotation(32, max_backups=2))) + inspect(sink.rotation_enabled(), content="true") + match sink.rotation_config() { + Some(config) => { + inspect(config.max_bytes, content="32") + inspect(config.max_backups, content="2") + } + None => inspect(false, content="true") + } + sink.clear_rotation() + inspect(sink.rotation_enabled(), content="false") + inspect(sink.rotation_config() is None, content="true") + inspect( + sink.reopen(), + content=if sink.is_available() { "true" } else { "false" }, + ) + inspect(sink.append_mode(), content="false") + let state = sink.state() + let policy = sink.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(sink.policy_matches_default(), content="false") + sink.reset_policy() + inspect(sink.append_mode(), content="true") + inspect(sink.auto_flush_enabled(), content="true") + inspect(sink.rotation_config() is None, content="true") + inspect(sink.policy_matches_default(), content="true") +} + +///| +test "file sink reset policy restores configured defaults" { + let sink = file_sink( + "bitlogger-reset-policy.log", + append=false, + auto_flush=false, + rotation=Some(file_rotation(24, max_backups=3)), + ) + sink.set_append_mode(true) + sink.set_auto_flush(true) + sink.clear_rotation() + let default_policy = sink.default_policy() + inspect(default_policy.append, content="false") + inspect(default_policy.auto_flush, content="false") + inspect(sink.policy_matches_default(), content="false") + sink.reset_policy() + inspect(sink.append_mode(), content="false") + inspect(sink.auto_flush_enabled(), content="false") + inspect(sink.policy_matches_default(), content="true") + match sink.rotation_config() { + Some(rotation) => { + inspect(rotation.max_bytes, content="24") + inspect(rotation.max_backups, content="3") + } + None => inspect(false, content="true") + } +} + +///| +test "file sink set policy applies bundled runtime policy" { + let sink = file_sink("bitlogger-set-policy.log") + let default_policy = sink.default_policy() + sink.set_policy( + FileSinkPolicy::new( + append=false, + auto_flush=false, + rotation=Some(file_rotation(18, max_backups=2)), + ), + ) + let policy = sink.policy() + inspect(policy.append, content="false") + inspect(policy.auto_flush, content="false") + match policy.rotation { + Some(rotation) => { + inspect(rotation.max_bytes, content="18") + inspect(rotation.max_backups, content="2") + } + 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(sink.policy_matches_default(), content="false") +} diff --git a/src/library_logger_builder_test.mbt b/src/library_logger_builder_test.mbt new file mode 100644 index 0000000..9754117 --- /dev/null +++ b/src/library_logger_builder_test.mbt @@ -0,0 +1,347 @@ +///| +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 builder log supports per-call target override through facade" { + let captured_target : Ref[String] = Ref("") + let captured_message : Ref[String] = Ref("") + let captured_fields : Ref[Array[Field]] = Ref([]) + let logger = build_library_logger( + LoggerConfig::new( + min_level=Level::Warn, + target="lib.config.default", + sink=SinkConfig::new(kind=SinkKind::Console), + ), + ) + .to_logger() + .with_patch(fn(rec) { + captured_target.val = rec.target + captured_message.val = rec.message + captured_fields.val = rec.fields + rec + }) + .to_library_logger() + .bind([field("service", "bitlogger")]) + + logger.log( + Level::Error, + "override", + fields=[field("mode", "test")], + target="lib.config.override", + ) + inspect(captured_target.val, content="lib.config.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.config.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 builder 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 = build_library_logger( + LoggerConfig::new( + min_level=Level::Info, + target="lib.config.helpers", + sink=SinkConfig::new(kind=SinkKind::Console), + ), + ) + .to_logger() + .with_patch(fn(rec) { + captured_levels.val.push(rec.level.label()) + captured_targets.val.push(rec.target) + captured_fields.val.push(rec.fields) + rec + }) + .to_library_logger() + .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.config.helpers") + inspect(captured_targets.val[1], content="lib.config.helpers") + inspect(captured_targets.val[2], content="lib.config.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 builder unwrap matches direct configured builder behavior" { + let config = LoggerConfig::new( + min_level=Level::Warn, + target="lib.config.same-build", + timestamp=true, + queue=Some(QueueConfig::new(2, overflow=QueueOverflowPolicy::DropNewest)), + sink=SinkConfig::new(kind=SinkKind::Console), + ) + let logger = build_library_logger(config) + let full = logger.to_logger() + let configured = build_logger(config) + + inspect(full.target, content=configured.target) + inspect(full.timestamp == configured.timestamp, content="true") + inspect( + logger.is_enabled(Level::Error) == configured.is_enabled(Level::Error), + content="true", + ) + inspect( + logger.is_enabled(Level::Info) == configured.is_enabled(Level::Info), + content="true", + ) + let full_sink_kind = match full.sink { + RuntimeSink::QueuedConsole(_) => "QueuedConsole" + RuntimeSink::QueuedJsonConsole(_) => "QueuedJsonConsole" + RuntimeSink::QueuedTextConsole(_) => "QueuedTextConsole" + RuntimeSink::QueuedFile(_) => "QueuedFile" + RuntimeSink::Console(_) => "Console" + RuntimeSink::JsonConsole(_) => "JsonConsole" + RuntimeSink::TextConsole(_) => "TextConsole" + RuntimeSink::File(_) => "File" + } + let configured_sink_kind = match configured.sink { + RuntimeSink::QueuedConsole(_) => "QueuedConsole" + RuntimeSink::QueuedJsonConsole(_) => "QueuedJsonConsole" + RuntimeSink::QueuedTextConsole(_) => "QueuedTextConsole" + RuntimeSink::QueuedFile(_) => "QueuedFile" + RuntimeSink::Console(_) => "Console" + RuntimeSink::JsonConsole(_) => "JsonConsole" + RuntimeSink::TextConsole(_) => "TextConsole" + RuntimeSink::File(_) => "File" + } + inspect(full_sink_kind == configured_sink_kind, content="true") + + full.error("one") + full.error("two") + full.error("three") + configured.error("one") + configured.error("two") + configured.error("three") + + inspect(full.pending_count() == configured.pending_count(), content="true") + inspect(full.dropped_count() == configured.dropped_count(), content="true") + inspect(full.flush() == configured.flush(), content="true") + inspect(full.pending_count() == configured.pending_count(), content="true") +} + +///| +test "library logger builder unwrap preserves file runtime helpers" { + let config = LoggerConfig::new( + min_level=Level::Warn, + target="lib.file.same-build", + sink=SinkConfig::new(kind=SinkKind::File, path="lib-file-same-build.log"), + queue=Some(QueueConfig::new(2, overflow=QueueOverflowPolicy::DropOldest)), + ) + let logger = build_library_logger(config) + let full = logger.to_logger() + let configured = build_logger(config) + + full.error("one") + full.error("two") + full.error("three") + configured.error("one") + configured.error("two") + configured.error("three") + + inspect(full.pending_count() == configured.pending_count(), content="true") + inspect(full.dropped_count() == configured.dropped_count(), content="true") + inspect(full.file_available() == configured.file_available(), content="true") + inspect( + full.file_append_mode() == configured.file_append_mode(), + content="true", + ) + inspect( + full.file_auto_flush() == configured.file_auto_flush(), + content="true", + ) + + let full_state = full.file_state() + let configured_state = configured.file_state() + inspect(full_state.path == configured_state.path, content="true") + inspect(full_state.available == configured_state.available, content="true") + inspect(full_state.append == configured_state.append, content="true") + inspect(full_state.auto_flush == configured_state.auto_flush, content="true") + inspect( + full_state.open_failures == configured_state.open_failures, + content="true", + ) + inspect( + full_state.write_failures == configured_state.write_failures, + content="true", + ) + inspect( + full_state.flush_failures == configured_state.flush_failures, + content="true", + ) + inspect( + full_state.rotation_failures == configured_state.rotation_failures, + content="true", + ) + + match (full.file_runtime_state(), configured.file_runtime_state()) { + (Some(snapshot), Some(other)) => { + inspect(snapshot.file.path == other.file.path, content="true") + inspect(snapshot.file.available == other.file.available, content="true") + inspect(snapshot.queued == other.queued, content="true") + inspect(snapshot.pending_count == other.pending_count, content="true") + inspect(snapshot.dropped_count == other.dropped_count, content="true") + } + _ => inspect(false, content="true") + } + + inspect(full.file_flush() == configured.file_flush(), content="true") + inspect(full.pending_count() == configured.pending_count(), content="true") + inspect(full.file_close() == configured.file_close(), content="true") +} + +///| +test "library logger builder unwrap preserves file controls" { + let config = LoggerConfig::new( + min_level=Level::Warn, + target="lib.file.controls.same-build", + sink=SinkConfig::new( + kind=SinkKind::File, + path="lib-file-controls-same-build.log", + ), + ) + let logger = build_library_logger(config) + let full = logger.to_logger() + let configured = build_logger(config) + + inspect( + full.file_set_append_mode(false) == configured.file_set_append_mode(false), + content="true", + ) + inspect( + full.file_append_mode() == configured.file_append_mode(), + content="true", + ) + inspect( + full.file_set_auto_flush(false) == configured.file_set_auto_flush(false), + content="true", + ) + inspect( + full.file_auto_flush() == configured.file_auto_flush(), + content="true", + ) + + let rotation = Some(file_rotation(48, max_backups=3)) + inspect( + full.file_set_rotation(rotation) == configured.file_set_rotation(rotation), + content="true", + ) + inspect( + full.file_rotation_enabled() == configured.file_rotation_enabled(), + content="true", + ) + + let full_policy = full.file_policy() + let configured_policy = configured.file_policy() + inspect(full_policy.append == configured_policy.append, content="true") + inspect( + full_policy.auto_flush == configured_policy.auto_flush, + content="true", + ) + inspect( + match full_policy.rotation { + Some(value) => + match configured_policy.rotation { + Some(other) => + value.max_bytes == other.max_bytes && + value.max_backups == other.max_backups + None => false + } + None => configured_policy.rotation is None + }, + content="true", + ) + + inspect( + full.file_reopen_append() == configured.file_reopen_append(), + content="true", + ) + inspect( + full.file_append_mode() == configured.file_append_mode(), + content="true", + ) + inspect( + full.file_reopen_truncate() == configured.file_reopen_truncate(), + content="true", + ) + inspect( + full.file_append_mode() == configured.file_append_mode(), + content="true", + ) + inspect( + full.file_reopen_with_current_policy() == + configured.file_reopen_with_current_policy(), + content="true", + ) + + inspect( + full.file_clear_rotation() == configured.file_clear_rotation(), + content="true", + ) + inspect( + full.file_rotation_enabled() == configured.file_rotation_enabled(), + content="true", + ) + inspect( + full.file_policy_matches_default() == + configured.file_policy_matches_default(), + content="true", + ) + inspect( + full.file_reset_policy() == configured.file_reset_policy(), + content="true", + ) + inspect( + full.file_policy_matches_default() == + configured.file_policy_matches_default(), + content="true", + ) + inspect( + full.file_reset_failure_counters() == + configured.file_reset_failure_counters(), + content="true", + ) + + inspect(full.file_close() == configured.file_close(), content="true") +} + +///| diff --git a/src/library_logger_facade_test.mbt b/src/library_logger_facade_test.mbt new file mode 100644 index 0000000..4b26606 --- /dev/null +++ b/src/library_logger_facade_test.mbt @@ -0,0 +1,259 @@ +///| +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 "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") +} + +///| diff --git a/src/logger_config_formatter_test.mbt b/src/logger_config_formatter_test.mbt new file mode 100644 index 0000000..6727d08 --- /dev/null +++ b/src/logger_config_formatter_test.mbt @@ -0,0 +1,91 @@ +///| +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"), + 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, "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, "custom builtin"), + formatter=formatter.to_formatter(), + ) + inspect(rendered, content="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, "raw tag"), + formatter=formatter.to_formatter(), + ) + inspect(rendered, content="raw 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, + "message stays raw", + target="svc", + fields=[field("status", "ok")], + ), + formatter=formatter.to_formatter(), + ) + inspect( + rendered, + content="[\u{001b}[34m\u{001b}[31;1msvc\u{001b}[0m\u{001b}[0m] message stays raw \u{001b}[35mstatus=\u{001b}[32;1mok\u{001b}[0m\u{001b}[0m", + ) +} diff --git a/src/logger_config_json_helpers_test.mbt b/src/logger_config_json_helpers_test.mbt new file mode 100644 index 0000000..b131f50 --- /dev/null +++ b/src/logger_config_json_helpers_test.mbt @@ -0,0 +1,358 @@ +///| +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}}", + ) + inspect( + @json_parser.stringify( + file_rotation_config_to_json( + file_rotation_i64(4294967296L, max_backups=2), + ), + ), + content="{\"max_bytes\":2147483647,\"max_backups\":2,\"max_bytes_i64\":\"4294967296\"}", + ) +} + +///| +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", + ) + inspect(rotation_obj.get("max_bytes_i64") is None, content="true") + + let wide_rotation_json = file_rotation_config_to_json( + file_rotation_i64(4294967296L, max_backups=3), + ) + let wide_rotation_obj = wide_rotation_json.as_object().unwrap() + inspect( + wide_rotation_obj.get("max_bytes").unwrap().as_number().unwrap().to_int(), + content="2147483647", + ) + inspect( + wide_rotation_obj.get("max_backups").unwrap().as_number().unwrap().to_int(), + content="3", + ) + inspect( + wide_rotation_obj.get("max_bytes_i64").unwrap().as_string().unwrap(), + content="4294967296", + ) + + 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", + ) +} + +///| diff --git a/src/logger_config_parser_test.mbt b/src/logger_config_parser_test.mbt new file mode 100644 index 0000000..e54faef --- /dev/null +++ b/src/logger_config_parser_test.mbt @@ -0,0 +1,198 @@ +///| +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 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") + inspect(rotation.native_wide_max_bytes is None, content="true") + } + None => inspect(false, content="true") + } +} + +///| +test "logger config parser prefers max_bytes_i64 when present" { + let config = parse_logger_config_text( + "{\"sink\":{\"kind\":\"file\",\"path\":\"bitlogger.log\",\"rotation\":{\"max_bytes\":128,\"max_bytes_i64\":\"4294967296\",\"max_backups\":3}}}", + ) + inspect(config.sink.path, content="bitlogger.log") + match config.sink.rotation { + Some(rotation) => { + inspect(rotation.max_bytes, content="2147483647") + inspect(rotation.max_backups, content="3") + inspect(rotation.native_wide_max_bytes.unwrap(), content="4294967296") + } + 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") +} + +///| diff --git a/src/logger_config_rotation_test.mbt b/src/logger_config_rotation_test.mbt new file mode 100644 index 0000000..97f8f32 --- /dev/null +++ b/src/logger_config_rotation_test.mbt @@ -0,0 +1,86 @@ +///| +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 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") + inspect(rotation.native_wide_max_bytes is None, content="true") + } + None => inspect(false, content="true") + } +} + +///| +test "logger config stringify roundtrips file rotation i64 metadata" { + let text = stringify_logger_config( + LoggerConfig::new( + sink=SinkConfig::new( + kind=SinkKind::File, + path="bitlogger.log", + rotation=Some(file_rotation_i64(4294967296L, 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="2147483647") + inspect(rotation.max_backups, content="2") + inspect(rotation.native_wide_max_bytes.unwrap(), content="4294967296") + } + None => inspect(false, content="true") + } +} + +///| diff --git a/src/logger_config_stringify_test.mbt b/src/logger_config_stringify_test.mbt new file mode 100644 index 0000000..35dc2f4 --- /dev/null +++ b/src/logger_config_stringify_test.mbt @@ -0,0 +1,94 @@ +///| +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", + ) +} + +///| diff --git a/src/logger_config_test.mbt b/src/logger_config_test.mbt new file mode 100644 index 0000000..c3f3674 --- /dev/null +++ b/src/logger_config_test.mbt @@ -0,0 +1,11 @@ +///| +// Logger config tests were split by parser, rotation, stringify, JSON helpers, +// and formatter rendering responsibilities. Add new config checks to the +// focused files in this package instead of growing this catch-all file again. +// +// Current layout: +// - logger_config_parser_test.mbt +// - logger_config_rotation_test.mbt +// - logger_config_stringify_test.mbt +// - logger_config_json_helpers_test.mbt +// - logger_config_formatter_test.mbt diff --git a/src/parsed_application_logger_test.mbt b/src/parsed_application_logger_test.mbt new file mode 100644 index 0000000..4c15278 --- /dev/null +++ b/src/parsed_application_logger_test.mbt @@ -0,0 +1,172 @@ +///| +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 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 = parse_and_build_application_logger( + "{\"min_level\":\"warn\",\"target\":\"app.json.default\",\"sink\":{\"kind\":\"console\"}}", + ) + .with_patch(fn(rec) { + captured_target.val = rec.target + captured_message.val = rec.message + captured_fields.val = rec.fields + rec + }) + .with_context_fields([field("service", "bitlogger")]) + + logger.log( + Level::Error, + "override", + fields=[field("mode", "test")], + target="app.json.override", + ) + inspect(captured_target.val, content="app.json.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="app.json.default") + inspect(captured_message.val, content="default") + inspect(captured_fields.val.length(), content="1") + inspect(captured_fields.val[0].key, content="service") +} + +///| +test "parsed application 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 = parse_and_build_application_logger( + "{\"min_level\":\"info\",\"target\":\"app.json.helpers\",\"sink\":{\"kind\":\"console\"}}", + ) + .with_patch(fn(rec) { + captured_levels.val.push(rec.level.label()) + captured_targets.val.push(rec.target) + captured_fields.val.push(rec.fields) + rec + }) + .with_context_fields([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="app.json.helpers") + inspect(captured_targets.val[1], content="app.json.helpers") + inspect(captured_targets.val[2], content="app.json.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 "application logger parse-build matches parsed direct builder behavior" { + let raw = "{\"min_level\":\"warn\",\"target\":\"app.json.same-build\",\"timestamp\":true,\"queue\":{\"max_pending\":2,\"overflow\":\"DropNewest\"},\"sink\":{\"kind\":\"console\"}}" + let application = parse_and_build_application_logger(raw) + let configured = build_logger(parse_logger_config_text(raw)) + + inspect(application.target, content=configured.target) + inspect(application.timestamp == configured.timestamp, content="true") + inspect( + application.is_enabled(Level::Error) == configured.is_enabled(Level::Error), + content="true", + ) + inspect( + application.is_enabled(Level::Info) == configured.is_enabled(Level::Info), + content="true", + ) + let application_sink_kind = match application.sink { + RuntimeSink::QueuedConsole(_) => "QueuedConsole" + RuntimeSink::QueuedJsonConsole(_) => "QueuedJsonConsole" + RuntimeSink::QueuedTextConsole(_) => "QueuedTextConsole" + RuntimeSink::QueuedFile(_) => "QueuedFile" + RuntimeSink::Console(_) => "Console" + RuntimeSink::JsonConsole(_) => "JsonConsole" + RuntimeSink::TextConsole(_) => "TextConsole" + RuntimeSink::File(_) => "File" + } + let configured_sink_kind = match configured.sink { + RuntimeSink::QueuedConsole(_) => "QueuedConsole" + RuntimeSink::QueuedJsonConsole(_) => "QueuedJsonConsole" + RuntimeSink::QueuedTextConsole(_) => "QueuedTextConsole" + RuntimeSink::QueuedFile(_) => "QueuedFile" + RuntimeSink::Console(_) => "Console" + RuntimeSink::JsonConsole(_) => "JsonConsole" + RuntimeSink::TextConsole(_) => "TextConsole" + RuntimeSink::File(_) => "File" + } + inspect(application_sink_kind == configured_sink_kind, content="true") + + application.error("one") + application.error("two") + application.error("three") + configured.error("one") + configured.error("two") + configured.error("three") + + inspect( + application.pending_count() == configured.pending_count(), + content="true", + ) + inspect( + application.dropped_count() == configured.dropped_count(), + content="true", + ) + inspect(application.flush() == configured.flush(), content="true") + inspect( + application.pending_count() == configured.pending_count(), + content="true", + ) +} + +///| +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") + } +} + +///| diff --git a/src/parsed_library_logger_test.mbt b/src/parsed_library_logger_test.mbt new file mode 100644 index 0000000..ebcd977 --- /dev/null +++ b/src/parsed_library_logger_test.mbt @@ -0,0 +1,317 @@ +///| +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 log supports per-call target override through facade" { + let captured_target : Ref[String] = Ref("") + let captured_message : Ref[String] = Ref("") + let captured_fields : Ref[Array[Field]] = Ref([]) + let logger = parse_and_build_library_logger( + "{\"min_level\":\"warn\",\"target\":\"lib.json.default\",\"sink\":{\"kind\":\"console\"}}", + ) + .to_logger() + .with_patch(fn(rec) { + captured_target.val = rec.target + captured_message.val = rec.message + captured_fields.val = rec.fields + rec + }) + .to_library_logger() + .bind([field("service", "bitlogger")]) + + logger.log( + Level::Error, + "override", + fields=[field("mode", "test")], + target="lib.json.override", + ) + inspect(captured_target.val, content="lib.json.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.json.default") + inspect(captured_message.val, content="default") + inspect(captured_fields.val.length(), content="1") + inspect(captured_fields.val[0].key, content="service") +} + +///| +test "parsed 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 = parse_and_build_library_logger( + "{\"min_level\":\"info\",\"target\":\"lib.json.helpers\",\"sink\":{\"kind\":\"console\"}}", + ) + .to_logger() + .with_patch(fn(rec) { + captured_levels.val.push(rec.level.label()) + captured_targets.val.push(rec.target) + captured_fields.val.push(rec.fields) + rec + }) + .to_library_logger() + .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.json.helpers") + inspect(captured_targets.val[1], content="lib.json.helpers") + inspect(captured_targets.val[2], content="lib.json.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 parse-build unwrap matches parsed direct builder behavior" { + let raw = "{\"min_level\":\"warn\",\"target\":\"lib.json.same-build\",\"timestamp\":true,\"queue\":{\"max_pending\":2,\"overflow\":\"DropNewest\"},\"sink\":{\"kind\":\"console\"}}" + let logger = parse_and_build_library_logger(raw) + let full = logger.to_logger() + let configured = parse_and_build_logger(raw) + + inspect(full.target, content=configured.target) + inspect(full.timestamp == configured.timestamp, content="true") + inspect( + logger.is_enabled(Level::Error) == configured.is_enabled(Level::Error), + content="true", + ) + inspect( + logger.is_enabled(Level::Info) == configured.is_enabled(Level::Info), + content="true", + ) + let full_sink_kind = match full.sink { + RuntimeSink::QueuedConsole(_) => "QueuedConsole" + RuntimeSink::QueuedJsonConsole(_) => "QueuedJsonConsole" + RuntimeSink::QueuedTextConsole(_) => "QueuedTextConsole" + RuntimeSink::QueuedFile(_) => "QueuedFile" + RuntimeSink::Console(_) => "Console" + RuntimeSink::JsonConsole(_) => "JsonConsole" + RuntimeSink::TextConsole(_) => "TextConsole" + RuntimeSink::File(_) => "File" + } + let configured_sink_kind = match configured.sink { + RuntimeSink::QueuedConsole(_) => "QueuedConsole" + RuntimeSink::QueuedJsonConsole(_) => "QueuedJsonConsole" + RuntimeSink::QueuedTextConsole(_) => "QueuedTextConsole" + RuntimeSink::QueuedFile(_) => "QueuedFile" + RuntimeSink::Console(_) => "Console" + RuntimeSink::JsonConsole(_) => "JsonConsole" + RuntimeSink::TextConsole(_) => "TextConsole" + RuntimeSink::File(_) => "File" + } + inspect(full_sink_kind == configured_sink_kind, content="true") + + full.error("one") + full.error("two") + full.error("three") + configured.error("one") + configured.error("two") + configured.error("three") + + inspect(full.pending_count() == configured.pending_count(), content="true") + inspect(full.dropped_count() == configured.dropped_count(), content="true") + inspect(full.flush() == configured.flush(), content="true") + inspect(full.pending_count() == configured.pending_count(), content="true") +} + +///| +test "library logger parse-build unwrap preserves file runtime helpers" { + let raw = "{\"min_level\":\"warn\",\"target\":\"lib.json.file.same-build\",\"queue\":{\"max_pending\":2,\"overflow\":\"DropOldest\"},\"sink\":{\"kind\":\"file\",\"path\":\"lib-json-file-same-build.log\"}}" + let logger = parse_and_build_library_logger(raw) + let full = logger.to_logger() + let configured = parse_and_build_logger(raw) + + full.error("one") + full.error("two") + full.error("three") + configured.error("one") + configured.error("two") + configured.error("three") + + inspect(full.pending_count() == configured.pending_count(), content="true") + inspect(full.dropped_count() == configured.dropped_count(), content="true") + inspect(full.file_available() == configured.file_available(), content="true") + inspect( + full.file_append_mode() == configured.file_append_mode(), + content="true", + ) + inspect( + full.file_auto_flush() == configured.file_auto_flush(), + content="true", + ) + + let full_state = full.file_state() + let configured_state = configured.file_state() + inspect(full_state.path == configured_state.path, content="true") + inspect(full_state.available == configured_state.available, content="true") + inspect(full_state.append == configured_state.append, content="true") + inspect(full_state.auto_flush == configured_state.auto_flush, content="true") + inspect( + full_state.open_failures == configured_state.open_failures, + content="true", + ) + inspect( + full_state.write_failures == configured_state.write_failures, + content="true", + ) + inspect( + full_state.flush_failures == configured_state.flush_failures, + content="true", + ) + inspect( + full_state.rotation_failures == configured_state.rotation_failures, + content="true", + ) + + match (full.file_runtime_state(), configured.file_runtime_state()) { + (Some(snapshot), Some(other)) => { + inspect(snapshot.file.path == other.file.path, content="true") + inspect(snapshot.file.available == other.file.available, content="true") + inspect(snapshot.queued == other.queued, content="true") + inspect(snapshot.pending_count == other.pending_count, content="true") + inspect(snapshot.dropped_count == other.dropped_count, content="true") + } + _ => inspect(false, content="true") + } + + inspect(full.file_flush() == configured.file_flush(), content="true") + inspect(full.pending_count() == configured.pending_count(), content="true") + inspect(full.file_close() == configured.file_close(), content="true") +} + +///| +test "library logger parse-build unwrap preserves file controls" { + let raw = "{\"min_level\":\"warn\",\"target\":\"lib.json.file.controls.same-build\",\"sink\":{\"kind\":\"file\",\"path\":\"lib-json-file-controls-same-build.log\"}}" + let logger = parse_and_build_library_logger(raw) + let full = logger.to_logger() + let configured = parse_and_build_logger(raw) + + inspect( + full.file_set_append_mode(false) == configured.file_set_append_mode(false), + content="true", + ) + inspect( + full.file_append_mode() == configured.file_append_mode(), + content="true", + ) + inspect( + full.file_set_auto_flush(false) == configured.file_set_auto_flush(false), + content="true", + ) + inspect( + full.file_auto_flush() == configured.file_auto_flush(), + content="true", + ) + + let rotation = Some(file_rotation(40, max_backups=2)) + inspect( + full.file_set_rotation(rotation) == configured.file_set_rotation(rotation), + content="true", + ) + inspect( + full.file_rotation_enabled() == configured.file_rotation_enabled(), + content="true", + ) + inspect( + full.file_reopen(append=Some(false)) == + configured.file_reopen(append=Some(false)), + content="true", + ) + inspect( + full.file_append_mode() == configured.file_append_mode(), + content="true", + ) + inspect( + full.file_reopen_append() == configured.file_reopen_append(), + content="true", + ) + inspect( + full.file_append_mode() == configured.file_append_mode(), + content="true", + ) + inspect( + full.file_clear_rotation() == configured.file_clear_rotation(), + content="true", + ) + inspect( + full.file_rotation_enabled() == configured.file_rotation_enabled(), + content="true", + ) + inspect( + full.file_reset_policy() == configured.file_reset_policy(), + content="true", + ) + inspect( + full.file_policy_matches_default() == + configured.file_policy_matches_default(), + content="true", + ) + inspect( + full.file_reset_failure_counters() == + configured.file_reset_failure_counters(), + content="true", + ) + + inspect(full.file_close() == configured.file_close(), content="true") +} + +///| +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") +} + +///| diff --git a/src/runtime_file_json_test.mbt b/src/runtime_file_json_test.mbt new file mode 100644 index 0000000..ffe538e --- /dev/null +++ b/src/runtime_file_json_test.mbt @@ -0,0 +1,349 @@ +///| +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", + ) + inspect(rotation_obj.get("max_bytes_i64") is None, content="true") + + let wide_rotation_json = file_rotation_config_to_json( + file_rotation_i64(4294967296L, max_backups=2), + ) + let wide_rotation_obj = wide_rotation_json.as_object().unwrap() + inspect( + wide_rotation_obj.get("max_bytes_i64").unwrap().as_string().unwrap(), + content="4294967296", + ) + + 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", + ) + + let wide = file_sink_state_to_json( + FileSinkState::new( + "wide.log", + available=true, + append=true, + auto_flush=false, + rotation=Some(file_rotation_i64(4294967296L, max_backups=3)), + open_failures=0, + write_failures=0, + flush_failures=0, + rotation_failures=0, + ), + ) + let wide_obj = wide.as_object().unwrap() + let wide_state_rotation_obj = wide_obj + .get("rotation") + .unwrap() + .as_object() + .unwrap() + inspect( + wide_state_rotation_obj + .get("max_bytes") + .unwrap() + .as_number() + .unwrap() + .to_int(), + content="2147483647", + ) + inspect( + wide_state_rotation_obj + .get("max_backups") + .unwrap() + .as_number() + .unwrap() + .to_int(), + content="3", + ) + inspect( + wide_state_rotation_obj.get("max_bytes_i64").unwrap().as_string().unwrap(), + content="4294967296", + ) + 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}", + ) + + let wide_runtime_json = runtime_file_state_to_json( + RuntimeFileState::new( + FileSinkState::new( + "wide-queue.log", + available=true, + append=true, + auto_flush=true, + rotation=Some(file_rotation_i64(4294967296L, max_backups=2)), + open_failures=0, + write_failures=0, + flush_failures=0, + rotation_failures=0, + ), + queued=false, + pending_count=0, + dropped_count=0, + ), + ) + let wide_runtime_obj = wide_runtime_json.as_object().unwrap() + let wide_runtime_file_obj = wide_runtime_obj + .get("file") + .unwrap() + .as_object() + .unwrap() + let wide_runtime_rotation_obj = wide_runtime_file_obj + .get("rotation") + .unwrap() + .as_object() + .unwrap() + inspect( + wide_runtime_rotation_obj + .get("max_bytes") + .unwrap() + .as_number() + .unwrap() + .to_int(), + content="2147483647", + ) + inspect( + wide_runtime_rotation_obj + .get("max_backups") + .unwrap() + .as_number() + .unwrap() + .to_int(), + content="2", + ) + inspect( + wide_runtime_rotation_obj.get("max_bytes_i64").unwrap().as_string().unwrap(), + content="4294967296", + ) +} + +///| +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", + ) + + let wide_policy_json = file_sink_policy_to_json( + FileSinkPolicy::new( + append=true, + auto_flush=false, + rotation=Some(file_rotation_i64(4294967296L, max_backups=4)), + ), + ) + let wide_policy_obj = wide_policy_json.as_object().unwrap() + let wide_policy_rotation_obj = wide_policy_obj + .get("rotation") + .unwrap() + .as_object() + .unwrap() + inspect( + wide_policy_rotation_obj + .get("max_bytes") + .unwrap() + .as_number() + .unwrap() + .to_int(), + content="2147483647", + ) + inspect( + wide_policy_rotation_obj + .get("max_backups") + .unwrap() + .as_number() + .unwrap() + .to_int(), + content="4", + ) + inspect( + wide_policy_rotation_obj.get("max_bytes_i64").unwrap().as_string().unwrap(), + content="4294967296", + ) + inspect( + stringify_file_sink_policy( + FileSinkPolicy::new(append=true, auto_flush=false, rotation=None), + ), + content="{\"append\":true,\"auto_flush\":false,\"rotation\":null}", + ) +} diff --git a/src/runtime_sink_close_test.mbt b/src/runtime_sink_close_test.mbt new file mode 100644 index 0000000..73b0c00 --- /dev/null +++ b/src/runtime_sink_close_test.mbt @@ -0,0 +1,137 @@ +///| +test "configured logger close stays aligned with runtime sink close" { + let console_config = LoggerConfig::new( + min_level=Level::Info, + target="config.close.console", + sink=SinkConfig::new(kind=SinkKind::Console), + queue=Some(QueueConfig::new(2, overflow=QueueOverflowPolicy::DropNewest)), + ) + let console_logger = build_logger(console_config) + let console_sink = build_logger(console_config).sink + console_logger.info("one") + console_logger.info("two") + inspect(console_logger.pending_count(), content="2") + inspect(console_logger.close() == console_sink.close(), content="true") + inspect(console_logger.pending_count(), content="0") + + let file_config = LoggerConfig::new( + target="config.close.file", + sink=SinkConfig::new(kind=SinkKind::File, path="config-close.log"), + ) + let file_logger = build_logger(file_config) + let file_sink = build_logger(file_config).sink + inspect(file_logger.close() == file_sink.close(), content="true") +} + +///| +test "queued console close drains pending queue before reporting success" { + let sink = RuntimeSink::QueuedConsole( + queued_sink( + console_sink(), + max_pending=4, + overflow=QueueOverflowPolicy::DropNewest, + ), + ) + let logger = Logger::new( + sink, + min_level=Level::Info, + target="runtime.close.console", + ) + logger.info("one") + logger.info("two") + inspect(sink.pending_count(), content="2") + inspect(sink.close(), content="true") + inspect(sink.pending_count(), content="0") + inspect(sink.flush(), content="0") +} + +///| +test "queued json console close drains pending queue before reporting success" { + let sink = RuntimeSink::QueuedJsonConsole( + queued_sink( + json_console_sink(), + max_pending=4, + overflow=QueueOverflowPolicy::DropNewest, + ), + ) + let logger = Logger::new( + sink, + min_level=Level::Info, + target="runtime.close.json", + ) + logger.info("one") + logger.info("two") + inspect(sink.pending_count(), content="2") + inspect(sink.close(), content="true") + inspect(sink.pending_count(), content="0") + inspect(sink.drain(), content="0") +} + +///| +test "queued text console close drains pending queue before reporting success" { + let sink = RuntimeSink::QueuedTextConsole( + queued_sink( + text_console_sink(text_formatter(show_timestamp=false, show_target=false)), + max_pending=4, + overflow=QueueOverflowPolicy::DropNewest, + ), + ) + let logger = Logger::new( + sink, + min_level=Level::Info, + target="runtime.close.text", + ) + logger.info("one") + logger.info("two") + inspect(sink.pending_count(), content="2") + inspect(sink.close(), content="true") + inspect(sink.pending_count(), content="0") + inspect(sink.flush(), content="0") +} + +///| +test "configured queued console close drains pending queue" { + let logger = build_logger( + LoggerConfig::new( + min_level=Level::Info, + target="config.close.queued.console", + sink=SinkConfig::new(kind=SinkKind::Console), + queue=Some(QueueConfig::new(4, overflow=QueueOverflowPolicy::DropNewest)), + ), + ) + logger.info("one") + logger.info("two") + inspect(logger.pending_count(), content="2") + inspect(logger.close(), content="true") + inspect(logger.pending_count(), content="0") +} + +///| +test "runtime sink queued file close drains queue before teardown" { + let sink = RuntimeSink::QueuedFile( + queued_sink( + file_sink("runtime-close-queued.log", auto_flush=false), + max_pending=4, + ), + ) + let logger = Logger::new( + sink, + min_level=Level::Info, + target="runtime.close.queued", + ) + logger.info("one") + logger.info("two") + inspect(sink.pending_count(), content="2") + if sink.file_available() { + inspect(sink.close(), content="true") + inspect(sink.pending_count(), content="0") + inspect(sink.file_available(), content="false") + inspect(sink.file_close(), content="false") + } else { + inspect(sink.close(), content="false") + inspect(sink.pending_count(), content="0") + inspect(sink.file_available(), content="false") + inspect(sink.file_write_failures(), content="2") + inspect(sink.file_close(), content="false") + } +} diff --git a/src/runtime_sink_file_controls_test.mbt b/src/runtime_sink_file_controls_test.mbt new file mode 100644 index 0000000..4af05a0 --- /dev/null +++ b/src/runtime_sink_file_controls_test.mbt @@ -0,0 +1,131 @@ +///| +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="false") + inspect(sink.file_append_mode(), content="true") + inspect(sink.file_set_append_mode(false), content="false") + inspect(sink.file_append_mode(), content="true") + inspect( + sink.file_set_policy( + FileSinkPolicy::new(append=false, auto_flush=false, rotation=None), + ), + content="false", + ) + inspect(sink.pending_count(), content="2") + inspect(sink.file_flush(), content="true") + inspect(sink.pending_count(), 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.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_set_append_mode(false), content="false") + inspect(sink.file_append_mode(), content="true") + 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") + } +} + +///| diff --git a/src/runtime_sink_file_surface_test.mbt b/src/runtime_sink_file_surface_test.mbt new file mode 100644 index 0000000..25b82cd --- /dev/null +++ b/src/runtime_sink_file_surface_test.mbt @@ -0,0 +1,225 @@ +///| +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_or_none() is None, content="true") + 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") + inspect(sink.file_policy_or_none() is None, content="true") + inspect(sink.file_default_policy_or_none() is None, content="true") + let policy = sink.file_policy() + let defaults = sink.file_default_policy() + inspect(sink.file_state_or_none() is None, content="true") + 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") + match sink.file_path_or_none() { + Some(path) => inspect(path, content="runtime-file-helpers.log") + None => inspect(false, 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") + match sink.file_default_policy_or_none() { + Some(policy) => { + inspect(policy.append, content="true") + inspect(policy.auto_flush, content="false") + } + None => inspect(false, content="true") + } + match sink.file_policy_or_none() { + Some(policy) => { + inspect(policy.append, content="true") + inspect(policy.auto_flush, content="false") + } + None => inspect(false, 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") + } + match sink.file_state_or_none() { + Some(snapshot) => { + inspect(snapshot.path, content="runtime-file-helpers.log") + inspect(snapshot.append, content="true") + inspect(snapshot.auto_flush, content="false") + } + 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") + match sink.file_path_or_none() { + Some(path) => inspect(path, content="runtime-queued-file.log") + None => inspect(false, content="true") + } + inspect(sink.file_path(), content="runtime-queued-file.log") + inspect(sink.file_policy_or_none() is None, content="false") + inspect(sink.file_default_policy_or_none() is None, content="false") + inspect(sink.file_state_or_none() is None, content="false") + 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") + } +} + +///| diff --git a/src/runtime_sink_progress_test.mbt b/src/runtime_sink_progress_test.mbt new file mode 100644 index 0000000..f81a5a3 --- /dev/null +++ b/src/runtime_sink_progress_test.mbt @@ -0,0 +1,129 @@ +///| +test "configured logger progress helpers stay aligned with runtime sink helpers" { + let logger = build_logger( + LoggerConfig::new( + min_level=Level::Info, + target="config.progress.delegate", + sink=SinkConfig::new( + kind=SinkKind::TextConsole, + text_formatter=TextFormatterConfig::new( + show_timestamp=false, + show_target=false, + ), + ), + queue=Some(QueueConfig::new(3, overflow=QueueOverflowPolicy::DropOldest)), + ), + ) + let sink = logger.sink + + logger.info("one") + logger.info("two") + logger.info("three") + + let logger_drain = logger.drain_progress(max_items=1) + inspect(logger_drain.queue_advanced_count, content="1") + inspect(logger_drain.file_flush_step_count, content="0") + inspect(logger_drain.queue_backed, content="true") + inspect(logger_drain.file_backed, content="false") + + let sink_drain = sink.drain_progress(max_items=1) + inspect(sink_drain.queue_advanced_count, content="1") + inspect(sink_drain.file_flush_step_count, content="0") + inspect(sink_drain.queue_backed, content="true") + inspect(sink_drain.file_backed, content="false") + + let logger_flush = logger.flush_progress() + inspect(logger_flush.queue_advanced_count, content="1") + inspect(logger_flush.file_flush_step_count, content="0") + inspect(logger_flush.queue_backed, content="true") + inspect(logger_flush.file_backed, content="false") + + let sink_flush = sink.flush_progress() + inspect(sink_flush.queue_advanced_count, content="0") + inspect(sink_flush.file_flush_step_count, content="0") + inspect(sink_flush.queue_backed, content="true") + inspect(sink_flush.file_backed, content="false") +} + +///| +test "runtime sink queued file progress helpers stay distinct from file flush outcome" { + let sink = RuntimeSink::QueuedFile( + queued_sink( + file_sink( + "missing-dir/runtime-progress-unavailable.log", + auto_flush=false, + ), + max_pending=4, + ), + ) + let logger = Logger::new( + sink, + min_level=Level::Info, + target="runtime.queue.file.progress.unavailable", + ) + logger.info("one") + logger.info("two") + + let drained = sink.drain_progress(max_items=1) + inspect(drained.queue_advanced_count, content="1") + inspect(drained.file_flush_step_count, content="0") + inspect(drained.queue_backed, content="true") + inspect(drained.file_backed, content="true") + inspect(sink.pending_count(), content="1") + inspect(sink.file_write_failures(), content="1") + + let flushed = sink.flush_progress() + inspect(flushed.queue_advanced_count, content="1") + inspect(flushed.file_flush_step_count, content="0") + inspect(flushed.queue_backed, content="true") + inspect(flushed.file_backed, content="true") + inspect(sink.pending_count(), content="0") + inspect(sink.file_write_failures(), content="2") + inspect(sink.file_flush(), content="false") +} + +///| +test "runtime sink progress helpers expose queue and plain file steps separately" { + let console_sink = RuntimeSink::Console(console_sink()) + let console_progress = console_sink.flush_progress() + inspect(console_progress.queue_advanced_count, content="0") + inspect(console_progress.file_flush_step_count, content="0") + inspect(console_progress.queue_backed, content="false") + inspect(console_progress.file_backed, content="false") + + let queued_sink = RuntimeSink::QueuedTextConsole( + queued_sink( + text_console_sink(text_formatter(show_timestamp=false, show_target=false)), + max_pending=4, + overflow=QueueOverflowPolicy::DropNewest, + ), + ) + let queued_logger = Logger::new( + queued_sink, + min_level=Level::Info, + target="runtime.progress.queue.text", + ) + queued_logger.info("one") + let queued_progress = queued_sink.flush_progress() + inspect(queued_progress.queue_advanced_count, content="1") + inspect(queued_progress.file_flush_step_count, content="0") + inspect(queued_progress.queue_backed, content="true") + inspect(queued_progress.file_backed, content="false") + + let file_sink = RuntimeSink::File( + file_sink("runtime-progress-direct.log", auto_flush=false), + ) + let file_progress = file_sink.flush_progress() + inspect(file_progress.queue_advanced_count, content="0") + inspect(file_progress.queue_backed, content="false") + inspect(file_progress.file_backed, content="true") + if file_sink.file_available() { + inspect(file_progress.file_flush_step_count, content="1") + inspect(file_sink.close(), content="true") + } else { + inspect(file_progress.file_flush_step_count, content="0") + inspect(file_sink.close(), content="false") + } +} + +///| diff --git a/src/runtime_sink_queue_test.mbt b/src/runtime_sink_queue_test.mbt new file mode 100644 index 0000000..a01a1b4 --- /dev/null +++ b/src/runtime_sink_queue_test.mbt @@ -0,0 +1,139 @@ +///| +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 queue helpers stay aligned with runtime sink helpers" { + let logger = build_logger( + LoggerConfig::new( + min_level=Level::Info, + target="config.delegate", + sink=SinkConfig::new( + kind=SinkKind::TextConsole, + text_formatter=TextFormatterConfig::new( + show_timestamp=false, + show_target=false, + ), + ), + queue=Some(QueueConfig::new(2, overflow=QueueOverflowPolicy::DropOldest)), + ), + ) + let sink = logger.sink + + logger.info("one") + logger.info("two") + logger.info("three") + + inspect(logger.pending_count() == sink.pending_count(), content="true") + inspect(logger.dropped_count() == sink.dropped_count(), content="true") + inspect(logger.drain(max_items=1) == sink.drain(max_items=1), content="true") + inspect(logger.pending_count() == sink.pending_count(), content="true") + inspect(logger.flush() == sink.flush(), content="true") + inspect(logger.pending_count() == sink.pending_count(), content="true") + inspect(logger.dropped_count() == sink.dropped_count(), content="true") +} + +///| +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 queued file drain counts queue consumption separately from file write success" { + let sink = RuntimeSink::QueuedFile( + queued_sink( + file_sink("missing-dir/runtime-drain-unavailable.log", auto_flush=false), + max_pending=4, + ), + ) + let logger = Logger::new( + sink, + min_level=Level::Info, + target="runtime.queue.file.unavailable", + ) + logger.info("one") + logger.info("two") + inspect(sink.file_available(), content="false") + inspect(sink.pending_count(), content="2") + inspect(sink.file_write_failures(), content="0") + inspect(sink.drain(max_items=1), content="1") + inspect(sink.pending_count(), content="1") + inspect(sink.file_write_failures(), content="1") + inspect(sink.flush(), content="1") + inspect(sink.pending_count(), content="0") + inspect(sink.file_write_failures(), content="2") +} diff --git a/src/runtime_sink_test.mbt b/src/runtime_sink_test.mbt new file mode 100644 index 0000000..3ec3c66 --- /dev/null +++ b/src/runtime_sink_test.mbt @@ -0,0 +1,12 @@ +///| +// This file is an index shell only. +// Runtime sink tests were split by queue, progress, close, file surface, and +// file controls responsibilities. Add new runtime sink checks to the focused +// files in this package instead of growing this catch-all file again. +// +// Current layout: +// - runtime_sink_queue_test.mbt +// - runtime_sink_progress_test.mbt +// - runtime_sink_close_test.mbt +// - runtime_sink_file_surface_test.mbt +// - runtime_sink_file_controls_test.mbt diff --git a/src/sink_graph_surface_test.mbt b/src/sink_graph_surface_test.mbt new file mode 100644 index 0000000..f3719f6 --- /dev/null +++ b/src/sink_graph_surface_test.mbt @@ -0,0 +1,335 @@ +///| +test "callback sink receives record" { + let captured_target : Ref[String] = Ref("") + let captured_message : Ref[String] = Ref("") + let logger = Logger::new( + callback_sink(fn(rec) { + captured_target.val = rec.target + captured_message.val = rec.message + }), + min_level=Level::Info, + target="callback", + ) + logger.info("hello") + inspect(captured_target.val, content="callback") + inspect(captured_message.val, content="hello") +} + +///| +test "split sink routes records by predicate" { + let left_messages : Ref[Array[String]] = Ref([]) + let right_messages : Ref[Array[String]] = Ref([]) + let logger = Logger::new( + split_sink( + callback_sink(fn(rec) { left_messages.val.push(rec.message) }), + callback_sink(fn(rec) { right_messages.val.push(rec.message) }), + fn(rec) { rec.target == "audit" }, + ), + min_level=Level::Info, + target="main", + ) + logger.info("drop to right") + logger.log(Level::Info, "keep on left", target="audit") + inspect(left_messages.val.length(), content="1") + inspect(left_messages.val[0], content="keep on left") + inspect(right_messages.val.length(), content="1") + inspect(right_messages.val[0], content="drop to right") +} + +///| +test "split_by_level routes warn and error separately" { + let high_messages : Ref[Array[String]] = Ref([]) + let low_messages : Ref[Array[String]] = Ref([]) + let logger = Logger::new( + split_by_level( + callback_sink(fn(rec) { + high_messages.val.push(rec.level.label() + ":" + rec.message) + }), + callback_sink(fn(rec) { + low_messages.val.push(rec.level.label() + ":" + rec.message) + }), + min_level=Level::Warn, + ), + min_level=Level::Trace, + target="split", + ) + logger.info("info") + logger.warn("warn") + logger.error("error") + inspect(high_messages.val.length(), content="2") + inspect(high_messages.val[0], content="WARN:warn") + inspect(high_messages.val[1], content="ERROR:error") + inspect(low_messages.val.length(), content="1") + inspect(low_messages.val[0], content="INFO:info") +} + +///| +test "buffered sink flushes manually" { + let flushed_messages : Ref[Array[String]] = Ref([]) + let sink = buffered_sink( + callback_sink(fn(rec) { flushed_messages.val.push(rec.message) }), + flush_limit=10, + ) + let logger = Logger::new(sink, min_level=Level::Info, target="buffered") + logger.info("one") + logger.info("two") + inspect(sink.pending_count(), content="2") + inspect(flushed_messages.val.length(), content="0") + sink.flush() + inspect(sink.pending_count(), content="0") + inspect(flushed_messages.val.length(), content="2") + inspect(flushed_messages.val[0], content="one") + inspect(flushed_messages.val[1], content="two") +} + +///| +test "buffered sink flushes automatically at limit" { + let flushed_messages : Ref[Array[String]] = Ref([]) + let sink = buffered_sink( + callback_sink(fn(rec) { flushed_messages.val.push(rec.message) }), + flush_limit=2, + ) + let logger = Logger::new(sink, min_level=Level::Info, target="buffered") + logger.info("one") + inspect(sink.pending_count(), content="1") + logger.info("two") + inspect(sink.pending_count(), content="0") + inspect(flushed_messages.val.length(), content="2") + inspect(flushed_messages.val[0], content="one") + inspect(flushed_messages.val[1], content="two") +} + +///| +test "filter sink only forwards matching records" { + let flushed_messages : Ref[Array[String]] = Ref([]) + let sink = filter_sink( + callback_sink(fn(rec) { flushed_messages.val.push(rec.message) }), + fn(rec) { rec.target == "kept" }, + ) + let kept = Logger::new(sink, min_level=Level::Info, target="kept") + let dropped = Logger::new(sink, min_level=Level::Info, target="dropped") + kept.info("one") + dropped.info("two") + kept.info("three") + inspect(flushed_messages.val.length(), content="2") + inspect(flushed_messages.val[0], content="one") + inspect(flushed_messages.val[1], content="three") +} + +///| +test "logger with_filter composes naturally" { + let flushed_messages : Ref[Array[String]] = Ref([]) + let logger = Logger::new( + callback_sink(fn(rec) { flushed_messages.val.push(rec.message) }), + min_level=Level::Info, + target="app", + ).with_filter(fn(rec) { rec.target == "app.worker" }) + logger.info("drop at app") + logger.child("worker").info("keep at worker") + inspect(flushed_messages.val.length(), content="1") + inspect(flushed_messages.val[0], content="keep at worker") +} + +///| +test "filter helpers support target level and message composition" { + let flushed_messages : Ref[Array[String]] = Ref([]) + let logger = Logger::new( + callback_sink(fn(rec) { flushed_messages.val.push(rec.message) }), + min_level=Level::Trace, + target="service", + ).with_filter( + all_of([ + target_has_prefix("service"), + level_at_least(Level::Info), + message_contains("visible"), + ]), + ) + logger.debug("visible debug") + logger.info("hidden info") + logger.child("api").info("visible info") + inspect(flushed_messages.val.length(), content="1") + inspect(flushed_messages.val[0], content="visible info") +} + +///| +test "field helpers can match and negate records" { + let flushed_messages : Ref[Array[String]] = Ref([]) + let logger = Logger::new( + callback_sink(fn(rec) { flushed_messages.val.push(rec.message) }), + min_level=Level::Info, + target="fields", + ).with_filter( + all_of([ + has_field("request_id"), + field_equals("kind", "audit"), + not_(target_is("fields.drop")), + ]), + ) + logger.info("missing field") + logger.info("wrong kind", fields=[ + field("request_id", "1"), + field("kind", "trace"), + ]) + logger + .child("drop") + .info("blocked target", fields=[ + field("request_id", "2"), + field("kind", "audit"), + ]) + logger.info("kept", fields=[field("request_id", "3"), field("kind", "audit")]) + inspect(flushed_messages.val.length(), content="1") + inspect(flushed_messages.val[0], content="kept") +} + +///| +test "any_of helper accepts multiple predicates" { + let flushed_messages : Ref[Array[String]] = Ref([]) + let logger = Logger::new( + callback_sink(fn(rec) { flushed_messages.val.push(rec.message) }), + min_level=Level::Info, + target="multi", + ).with_filter( + any_of([target_is("multi.keep"), field_equals("force", "true")]), + ) + logger.info("drop") + logger.child("keep").info("keep by target") + logger.info("keep by field", fields=[field("force", "true")]) + inspect(flushed_messages.val.length(), content="2") + inspect(flushed_messages.val[0], content="keep by target") + inspect(flushed_messages.val[1], content="keep by field") +} + +///| +test "patch sink can rewrite message target and fields" { + let captured_target : Ref[String] = Ref("") + let captured_message : Ref[String] = Ref("") + let captured_fields : Ref[Array[Field]] = Ref([]) + let logger = Logger::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="auth", + ).with_patch( + compose_patches([ + set_target("audit.auth"), + prefix_message("[safe] "), + redact_field("token"), + append_fields([field("service", "bitlogger")]), + ]), + ) + logger.info("login", fields=[field("token", "secret"), field("user", "alice")]) + inspect(captured_target.val, content="audit.auth") + inspect(captured_message.val, content="[safe] login") + inspect(captured_fields.val.length(), content="3") + inspect(captured_fields.val[0].key, content="token") + inspect(captured_fields.val[0].value, content="***") + inspect(captured_fields.val[1].key, content="user") + inspect(captured_fields.val[1].value, content="alice") + inspect(captured_fields.val[2].key, content="service") + inspect(captured_fields.val[2].value, content="bitlogger") +} + +///| +test "patch helpers can redact multiple fields" { + let captured_fields : Ref[Array[Field]] = Ref([]) + let logger = Logger::new( + callback_sink(fn(rec) { captured_fields.val = rec.fields }), + min_level=Level::Info, + target="audit", + ).with_patch(redact_fields(["token", "password"], placeholder="[redacted]")) + logger.info("credentials", fields=[ + field("token", "abc"), + field("password", "123"), + field("user", "alice"), + ]) + inspect(captured_fields.val.length(), content="3") + inspect(captured_fields.val[0].value, content="[redacted]") + inspect(captured_fields.val[1].value, content="[redacted]") + inspect(captured_fields.val[2].value, content="alice") +} + +///| +test "queued sink drains in order" { + let flushed_messages : Ref[Array[String]] = Ref([]) + let sink = queued_sink( + callback_sink(fn(rec) { flushed_messages.val.push(rec.message) }), + ) + let logger = Logger::new(sink, min_level=Level::Info, target="queue") + logger.info("one") + logger.info("two") + logger.info("three") + inspect(sink.pending_count(), content="3") + inspect(sink.dropped_count(), content="0") + inspect(sink.drain(max_items=2), content="2") + inspect(sink.pending_count(), content="1") + inspect(flushed_messages.val.length(), content="2") + inspect(flushed_messages.val[0], content="one") + inspect(flushed_messages.val[1], content="two") + inspect(sink.flush(), content="1") + inspect(sink.pending_count(), content="0") + inspect(flushed_messages.val[2], content="three") +} + +///| +test "queued sink can drop newest when full" { + let flushed_messages : Ref[Array[String]] = Ref([]) + let sink = queued_sink( + callback_sink(fn(rec) { flushed_messages.val.push(rec.message) }), + max_pending=2, + overflow=QueueOverflowPolicy::DropNewest, + ) + let logger = Logger::new(sink, min_level=Level::Info, target="queue") + logger.info("one") + logger.info("two") + logger.info("three") + inspect(sink.pending_count(), content="2") + inspect(sink.dropped_count(), content="1") + inspect(sink.flush(), content="2") + inspect(flushed_messages.val.length(), content="2") + inspect(flushed_messages.val[0], content="one") + inspect(flushed_messages.val[1], content="two") +} + +///| +test "queued sink can drop oldest when full" { + let flushed_messages : Ref[Array[String]] = Ref([]) + let sink = queued_sink( + callback_sink(fn(rec) { flushed_messages.val.push(rec.message) }), + max_pending=2, + overflow=QueueOverflowPolicy::DropOldest, + ) + let logger = Logger::new(sink, min_level=Level::Info, target="queue") + logger.info("one") + logger.info("two") + logger.info("three") + inspect(sink.pending_count(), content="2") + inspect(sink.dropped_count(), content="1") + inspect(sink.flush(), content="2") + inspect(flushed_messages.val.length(), content="2") + inspect(flushed_messages.val[0], content="two") + inspect(flushed_messages.val[1], content="three") +} + +///| +test "logger with_queue preserves chaining ergonomics" { + let flushed_messages : Ref[Array[String]] = Ref([]) + let logger = Logger::new( + callback_sink(fn(rec) { flushed_messages.val.push(rec.message) }), + min_level=Level::Info, + target="service", + ) + .with_patch(prefix_message("[queued] ")) + .with_queue(max_pending=2, overflow=QueueOverflowPolicy::DropOldest) + logger.info("one") + logger.child("api").info("two") + logger.info("three") + inspect(logger.sink.pending_count(), content="2") + inspect(logger.sink.dropped_count(), content="1") + inspect(logger.sink.flush(), content="2") + inspect(flushed_messages.val.length(), content="2") + inspect(flushed_messages.val[0], content="[queued] two") + inspect(flushed_messages.val[1], content="[queued] three") +} diff --git a/src/sync_facades_test.mbt b/src/sync_facades_test.mbt new file mode 100644 index 0000000..5483405 --- /dev/null +++ b/src/sync_facades_test.mbt @@ -0,0 +1,17 @@ +///| +// Sync facade tests were split by facade responsibility. +// Add new facade checks to the focused files in this package instead of +// growing this historical catch-all file again. +// +// Current layout: +// - library_logger_facade_test.mbt +// - library_logger_builder_test.mbt +// - parsed_library_logger_test.mbt +// - configured_logger_facade_test.mbt +// - application_logger_facade_test.mbt +// - parsed_application_logger_test.mbt +// - sync_parse_build_facade_test.mbt +// - BitLogger_test.mbt +// - text_formatter_test.mbt +// - file_sink_test.mbt +// - sink_graph_surface_test.mbt diff --git a/src/sync_parse_build_facade_test.mbt b/src/sync_parse_build_facade_test.mbt new file mode 100644 index 0000000..1ebfbeb --- /dev/null +++ b/src/sync_parse_build_facade_test.mbt @@ -0,0 +1,159 @@ +///| +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 "parsed configured 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 = parse_and_build_logger( + "{\"min_level\":\"warn\",\"target\":\"parsed.default\",\"sink\":{\"kind\":\"console\"}}", + ) + .with_patch(fn(rec) { + captured_target.val = rec.target + captured_message.val = rec.message + captured_fields.val = rec.fields + rec + }) + .with_context_fields([field("service", "bitlogger")]) + + logger.log( + Level::Error, + "override", + fields=[field("mode", "test")], + target="parsed.override", + ) + inspect(captured_target.val, content="parsed.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="parsed.default") + inspect(captured_message.val, content="default") + inspect(captured_fields.val.length(), content="1") + inspect(captured_fields.val[0].key, content="service") +} + +///| +test "parsed configured 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 = parse_and_build_logger( + "{\"min_level\":\"info\",\"target\":\"parsed.helpers\",\"sink\":{\"kind\":\"console\"}}", + ) + .with_patch(fn(rec) { + captured_levels.val.push(rec.level.label()) + captured_targets.val.push(rec.target) + captured_fields.val.push(rec.fields) + rec + }) + .with_context_fields([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="parsed.helpers") + inspect(captured_targets.val[1], content="parsed.helpers") + inspect(captured_targets.val[2], content="parsed.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 "sync parse-build facades forward config errors" { + let raw = "{\"sink\":{\"kind\":\"file\",\"path\":\"\"}}" + let malformed = "{" + + 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") + + let configured_invalid_json = (fn() -> String raise ConfigError { + ignore(parse_and_build_logger(malformed)) + "no error" + })() catch { + ConfigError::InvalidConfig(message) => message + } + inspect(configured_invalid_json.contains("Invalid JSON:"), content="true") + + let application_invalid_json = (fn() -> String raise ConfigError { + ignore(parse_and_build_application_logger(malformed)) + "no error" + })() catch { + ConfigError::InvalidConfig(message) => message + } + inspect(application_invalid_json.contains("Invalid JSON:"), content="true") + + let library_invalid_json = (fn() -> String raise ConfigError { + ignore(parse_and_build_library_logger(malformed)) + "no error" + })() catch { + ConfigError::InvalidConfig(message) => message + } + inspect(library_invalid_json.contains("Invalid JSON:"), content="true") +} diff --git a/src/text_formatter_test.mbt b/src/text_formatter_test.mbt new file mode 100644 index 0000000..36e21c7 --- /dev/null +++ b/src/text_formatter_test.mbt @@ -0,0 +1,468 @@ +///| +test "text formatter can customize visible parts" { + let rec = Record::new(Level::Info, "hello", timestamp_ms=123UL, target="svc.api", fields=[ + field("user", "alice"), + field("request_id", "42"), + ]) + let compact = text_formatter( + show_timestamp=false, + show_target=false, + field_separator=",", + ) + inspect( + format_text(rec, formatter=compact), + content="[INFO] hello user=alice,request_id=42", + ) +} + +///| +test "text formatter can emit message only" { + let rec = Record::new( + Level::Warn, + "just message", + timestamp_ms=999UL, + target="svc", + fields=[field("ignored", "yes")], + ) + let message_only = text_formatter( + show_timestamp=false, + show_level=false, + show_target=false, + show_fields=false, + ) + inspect(format_text(rec, formatter=message_only), content="just message") +} + +///| +test "text formatter supports template rendering" { + let rec = Record::new( + Level::Info, + "template hello", + timestamp_ms=123UL, + target="svc.api", + fields=[field("user", "alice"), field("request_id", "42")], + ) + let formatter = text_formatter( + separator=" | ", + field_separator=",", + template="[{level}] {target} {message} :: {fields} @{timestamp}", + ) + inspect( + format_text(rec, formatter~), + content="[INFO] svc.api template hello :: user=alice,request_id=42 @123", + ) +} + +///| +test "text formatter can render ansi level colors" { + let rec = Record::new(Level::Error, "boom", target="svc") + let formatter = text_formatter(color_mode=ColorMode::Always) + inspect( + format_text(rec, formatter~), + content="[\u{001b}[31;1mERROR\u{001b}[0m] [\u{001b}[34msvc\u{001b}[0m] boom", + ) +} + +///| +test "text formatter auto color respects NO_COLOR" { + let rec = Record::new(Level::Warn, "boom", target="svc") + let previous = @env.get_env_var("NO_COLOR") + @env.set_env_var("NO_COLOR", "1") + inspect( + format_text(rec, formatter=text_formatter(color_mode=ColorMode::Auto)), + content="[WARN] [svc] boom", + ) + match previous { + Some(value) => @env.set_env_var("NO_COLOR", value) + None => @env.unset_env_var("NO_COLOR") + } +} + +///| +test "text formatter renders named inline color tags in ansi mode" { + let rec = Record::new(Level::Info, "boom", target="svc") + inspect( + format_text(rec, formatter=text_formatter(color_mode=ColorMode::Always)), + content="[\u{001b}[32mINFO\u{001b}[0m] [\u{001b}[34msvc\u{001b}[0m] \u{001b}[31mboom\u{001b}[0m", + ) +} + +///| +test "text formatter strips inline tags in plain mode" { + let rec = Record::new(Level::Info, "boom bold", target="svc") + inspect( + format_text(rec, formatter=text_formatter(color_mode=ColorMode::Never)), + content="[INFO] [svc] boom bold", + ) +} + +///| +test "text formatter supports nested inline tags" { + let rec = Record::new(Level::Info, "fatal") + inspect( + format_text( + rec, + formatter=text_formatter( + show_level=false, + show_target=false, + color_mode=ColorMode::Always, + ), + ), + content="\u{001b}[31;1mfatal\u{001b}[0m", + ) +} + +///| +test "text formatter supports named closing tags" { + let rec = Record::new(Level::Info, "boom") + inspect( + format_text( + rec, + formatter=text_formatter( + show_level=false, + show_target=false, + color_mode=ColorMode::Always, + ), + ), + content="\u{001b}[31mboom\u{001b}[0m", + ) +} + +///| +test "text formatter supports mixed short and named closing tags" { + let rec = Record::new(Level::Info, "fatal") + inspect( + format_text( + rec, + formatter=text_formatter( + show_level=false, + show_target=false, + color_mode=ColorMode::Always, + ), + ), + content="\u{001b}[31;1mfatal\u{001b}[0m", + ) +} + +///| +test "text formatter keeps unmatched named closing tags as plain text" { + let rec = Record::new(Level::Info, "boom") + inspect( + format_text( + rec, + formatter=text_formatter(show_level=false, show_target=false), + ), + content="boom", + ) +} + +///| +test "text formatter supports hex inline colors" { + let rec = Record::new(Level::Info, "<#ff0000>hot bg") + inspect( + format_text( + rec, + formatter=text_formatter( + show_level=false, + show_target=false, + color_mode=ColorMode::Always, + ), + ), + content="\u{001b}[38;2;255;0;0mhot\u{001b}[0m \u{001b}[48;2;1;2;3mbg\u{001b}[0m", + ) +} + +///| +test "text formatter can downgrade hex colors to basic ansi" { + let rec = Record::new(Level::Info, "<#ff0000>hot bg") + inspect( + format_text( + rec, + formatter=text_formatter( + show_level=false, + show_target=false, + color_mode=ColorMode::Always, + color_support=ColorSupport::Basic, + ), + ), + content="\u{001b}[31mhot\u{001b}[0m \u{001b}[100mbg\u{001b}[0m", + ) +} + +///| +test "text formatter keeps unknown inline tags as plain text" { + let rec = Record::new(Level::Info, "boom") + inspect( + format_text( + rec, + formatter=text_formatter(show_level=false, show_target=false), + ), + content="boom", + ) +} + +///| +test "text formatter can disable style markup parsing" { + let rec = Record::new(Level::Info, "boom bold", target="svc") + inspect( + format_text( + rec, + formatter=text_formatter(color_mode=ColorMode::Always).without_style_markup(), + ), + content="[\u{001b}[32mINFO\u{001b}[0m] [\u{001b}[34msvc\u{001b}[0m] boom bold", + ) +} + +///| +test "text formatter builtin style markup mode ignores custom tags" { + let formatter = text_formatter( + show_level=false, + show_target=false, + color_mode=ColorMode::Always, + style_markup=StyleMarkupMode::Builtin, + ).with_style_tags( + style_tag_registry().set_tag("brand", fg=Some("#4cc9f0"), bold=true), + ) + let rec = Record::new(Level::Info, "custom builtin") + inspect( + format_text(rec, formatter~), + content="custom \u{001b}[31mbuiltin\u{001b}[0m", + ) +} + +///| +test "style markup mode label is stable" { + inspect( + style_markup_mode_label(StyleMarkupMode::Disabled), + content="disabled", + ) + inspect(style_markup_mode_label(StyleMarkupMode::Builtin), content="builtin") + inspect(style_markup_mode_label(StyleMarkupMode::Full), content="full") +} + +///| +test "text formatter supports custom style tags" { + let formatter = text_formatter( + show_level=false, + show_target=false, + color_mode=ColorMode::Always, + ).with_style_tags( + style_tag_registry().set_tag("accent", fg=Some("#4cc9f0"), bold=true), + ) + let rec = Record::new(Level::Info, "api") + inspect( + format_text(rec, formatter~), + content="\u{001b}[38;2;76;201;240;1mapi\u{001b}[0m", + ) +} + +///| +test "text formatter can override builtin style tags" { + let formatter = text_formatter( + show_level=false, + show_target=false, + color_mode=ColorMode::Always, + ).with_style_tags( + default_style_tag_registry().set_tag( + "red", + fg=Some("#ff5a5f"), + underline=true, + ), + ) + let rec = Record::new(Level::Info, "alert") + inspect( + format_text(rec, formatter~), + content="\u{001b}[38;2;255;90;95;4malert\u{001b}[0m", + ) +} + +///| +test "formatter style tags take priority over global style tags" { + let previous = global_style_tag_registry() + set_global_style_tag_registry( + style_tag_registry().set_tag("accent", fg=Some("#123456")), + ) + let formatter = text_formatter( + show_level=false, + show_target=false, + color_mode=ColorMode::Always, + ).with_style_tags(style_tag_registry().set_tag("accent", fg=Some("#abcdef"))) + let rec = Record::new(Level::Info, "tag") + inspect( + format_text(rec, formatter~), + content="\u{001b}[38;2;171;205;239mtag\u{001b}[0m", + ) + set_global_style_tag_registry(previous) +} + +///| +test "global style tags apply when formatter has no local registry" { + let previous = global_style_tag_registry() + set_global_style_tag_registry( + style_tag_registry().set_tag("accent", fg=Some("#102030"), dim=true), + ) + let rec = Record::new(Level::Info, "tag") + inspect( + format_text( + rec, + formatter=text_formatter( + show_level=false, + show_target=false, + color_mode=ColorMode::Always, + ), + ), + content="\u{001b}[38;2;16;32;48;2mtag\u{001b}[0m", + ) + set_global_style_tag_registry(previous) +} + +///| +test "reset global style tag registry restores builtin defaults" { + set_global_style_tag_registry( + style_tag_registry().set_tag("accent", fg=Some("#123456"), bold=true), + ) + reset_global_style_tag_registry() + let rec = Record::new(Level::Info, "ok") + inspect( + format_text( + rec, + formatter=text_formatter( + show_level=false, + show_target=false, + color_mode=ColorMode::Always, + ), + ), + content="\u{001b}[32;1mok\u{001b}[0m", + ) +} + +///| +test "style tag alias can reuse builtin tags" { + let formatter = text_formatter( + show_level=false, + show_target=false, + color_mode=ColorMode::Always, + ).with_style_tags(style_tag_registry().define_alias("danger", "red")) + let rec = Record::new(Level::Info, "boom") + inspect(format_text(rec, formatter~), content="\u{001b}[31mboom\u{001b}[0m") +} + +///| +test "builtin semantic style tags are available" { + let formatter = text_formatter( + show_level=false, + show_target=false, + color_mode=ColorMode::Always, + style_markup=StyleMarkupMode::Builtin, + ) + let rec = Record::new( + Level::Info, + "ok careful boom quiet", + ) + inspect( + format_text(rec, formatter~), + content="\u{001b}[32;1mok\u{001b}[0m \u{001b}[33;1mcareful\u{001b}[0m \u{001b}[31;1mboom\u{001b}[0m \u{001b}[90;2mquiet\u{001b}[0m", + ) +} + +///| +test "builtin semantic style tags can still be overridden" { + let formatter = text_formatter( + show_level=false, + show_target=false, + color_mode=ColorMode::Always, + ).with_style_tags( + default_style_tag_registry().set_tag( + "success", + fg=Some("#00ffaa"), + underline=true, + ), + ) + let rec = Record::new(Level::Info, "ok") + inspect( + format_text(rec, formatter~), + content="\u{001b}[38;2;0;255;170;4mok\u{001b}[0m", + ) +} + +///| +test "text formatter can enable markup for target separately" { + let formatter = text_formatter( + show_level=false, + color_mode=ColorMode::Always, + target_style_markup=StyleMarkupMode::Builtin, + ) + let rec = Record::new(Level::Info, "hello", target="svc") + inspect( + format_text(rec, formatter~), + content="[\u{001b}[34m\u{001b}[31;1msvc\u{001b}[0m\u{001b}[0m] hello", + ) +} + +///| +test "text formatter can enable markup for field values separately" { + let formatter = text_formatter( + show_level=false, + show_target=false, + color_mode=ColorMode::Always, + fields_style_markup=StyleMarkupMode::Builtin, + ) + let rec = Record::new(Level::Info, "hello", fields=[ + field("status", "ok"), + ]) + inspect( + format_text(rec, formatter~), + content="hello \u{001b}[35mstatus=\u{001b}[32;1mok\u{001b}[0m\u{001b}[0m", + ) +} + +///| +test "text formatter leaves field keys raw when field markup is enabled" { + let formatter = text_formatter( + show_level=false, + show_target=false, + color_mode=ColorMode::Always, + fields_style_markup=StyleMarkupMode::Builtin, + ) + let rec = Record::new(Level::Info, "hello", fields=[ + field("status", "ok"), + ]) + inspect( + format_text(rec, formatter~), + content="hello \u{001b}[35mstatus=ok\u{001b}[0m", + ) +} + +///| +test "text formatter template respects disabled fields" { + let rec = Record::new(Level::Warn, "just message", target="svc") + let formatter = text_formatter( + show_target=false, + show_fields=false, + template="[{level}] {target}{message}{fields}", + ) + inspect(format_text(rec, formatter~), content="[WARN] just message") +} + +///| +test "formatted callback sink receives rendered text" { + let rendered : Ref[String] = Ref("") + let sink = text_callback_sink( + text_formatter(show_timestamp=false, separator=" | "), + fn(text) { rendered.val = text }, + ) + let logger = Logger::new(sink, min_level=Level::Info, target="svc") + logger.info("hello", fields=[field("user", "alice")]) + inspect(rendered.val, content="[INFO] | [svc] | hello | user=alice") +} + +///| +test "json formatter keeps structured shape" { + let rec = Record::new(Level::Error, "failed", timestamp_ms=55UL, target="svc", fields=[ + field("code", "500"), + ]) + inspect( + format_json(rec), + content="{\"level\":\"ERROR\",\"message\":\"failed\",\"fields\":{\"code\":\"500\"},\"timestamp_ms\":\"55\",\"target\":\"svc\"}", + ) +}