From 0cbe25a5512530fb36c205ef7c9b884ca52ea2a0 Mon Sep 17 00:00:00 2001 From: Nanaloveyuki Date: Sat, 27 Jun 2026 10:45:36 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=8A=20Update=201.0.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/changes/1.0.0.md | 26 + docs/changes/index.md | 1 + examples/async_basic/main.mbt | 22 +- examples/async_basic/moon.pkg | 2 +- examples/basic/main.mbt | 106 +- examples/config_build/main.mbt | 1 + examples/console_basic/main.mbt | 1 + examples/file_rotation/main.mbt | 5 +- examples/presets/main.mbt | 9 +- examples/style_tags/main.mbt | 5 +- examples/text_formatter/main.mbt | 6 +- src-async/BitLoggerAsync_test.mbt | 2309 +++++++++++++++-------- src-async/application_async_logger.mbt | 9 +- src-async/async_logger_native.mbt | 5 + src-async/async_logger_shared.mbt | 222 ++- src-async/async_logger_stub.mbt | 5 + src-async/library_async_logger.mbt | 68 +- src-async/moon.pkg | 18 +- src-async/utils/async_models.mbt | 198 +- src-async/utils/moon.pkg | 2 +- src/BitLogger_test.mbt | 2374 ++++++++++++++++++------ src/BitLogger_wbtest.mbt | 423 +++-- src/README.mbt.md | 10 +- src/application_logger.mbt | 3 + src/config.mbt | 56 +- src/core/level.mbt | 4 + src/core/record.mbt | 24 +- src/file_backend_native.mbt | 10 + src/file_backend_stub.mbt | 10 + src/filters.mbt | 10 + src/formatter.mbt | 99 +- src/global.mbt | 14 +- src/global_emitters.mbt | 34 +- src/level.mbt | 1 + src/library_logger.mbt | 63 +- src/logger.mbt | 59 +- src/logger_emitters.mbt | 50 +- src/moon.pkg | 10 +- src/patchers.mbt | 19 +- src/presets.mbt | 86 +- src/presets_test.mbt | 142 +- src/record.mbt | 13 +- src/runtime_file_controls.mbt | 171 +- src/runtime_logger.mbt | 155 +- src/sinks.mbt | 107 +- src/sinks_file.mbt | 112 +- src/utils/config.mbt | 290 ++- src/utils/file_backend_native.mbt | 31 +- src/utils/file_backend_stub.mbt | 15 +- src/utils/filters.mbt | 32 +- src/utils/formatter.mbt | 472 +++-- src/utils/moon.pkg | 6 +- src/utils/patchers.mbt | 55 +- src/utils/runtime_file_state.mbt | 71 +- src/utils/sink_models.mbt | 43 +- 55 files changed, 5734 insertions(+), 2360 deletions(-) create mode 100644 docs/changes/1.0.0.md diff --git a/docs/changes/1.0.0.md b/docs/changes/1.0.0.md new file mode 100644 index 0000000..ce1cc3c --- /dev/null +++ b/docs/changes/1.0.0.md @@ -0,0 +1,26 @@ +## BitLogger Update Changes + +version 1.0.0 + +### Migration + +- migration: run the MoonBit 0.10 formatter migration across the repository and adopt the current `trait` / `impl` formatting and `fn`-prefixed impl method style +- migration: replace the legacy root manifest with `moon.mod`, aligning the package root with the current MoonBit manifest format +- migration: update the async dependency to `moonbitlang/async@0.20.0` + +### Docs + +- docs: refresh the README example and project command notes after the formatter migration so examples match the current MoonBit surface +- docs: document that `async_basic` depends on `moonbitlang/async`, whose upstream README currently only claims native/LLVM support on Linux/MacOS +- docs: narrow the repository prompt validation guidance so Windows-local `src-async --target native` failures are not treated as BitLogger regressions unless reproduced on a supported native platform + +### Verification + +- verify: keep the main package passing `moon check`, `moon test`, `moon check --target native`, `moon check --target wasm-gc`, and `moon check --target js` +- verify: keep `src-async` passing `moon check` / `moon test` on `wasm-gc` and `js`, and confirm the native async path on a supported environment baseline +- verify: keep the example path healthy with `moon run examples/basic` and `moon check examples/async_basic --target native` + +### Notes + +- this release marks the MoonBit 0.10 generation as the new BitLogger baseline +- the repository now treats Linux/MacOS as the supported native baseline for `moonbitlang/async` until upstream Windows native support is stated clearly diff --git a/docs/changes/index.md b/docs/changes/index.md index 3798b1f..d2ccabf 100644 --- a/docs/changes/index.md +++ b/docs/changes/index.md @@ -2,6 +2,7 @@ Versioned BitLogger change summaries. +- [1.0.0](./1.0.0.md) - [0.5.3](./0.5.3.md) - [0.5.2](./0.5.2.md) - [0.5.1](./0.5.1.md) diff --git a/examples/async_basic/main.mbt b/examples/async_basic/main.mbt index 72ae05c..26d1369 100644 --- a/examples/async_basic/main.mbt +++ b/examples/async_basic/main.mbt @@ -1,6 +1,14 @@ +///| async fn main { - println("examples/async_basic is shipped as a native-only example because async fn main entry support is still limited on non-native targets, even though src-async itself keeps compatibility behavior.") - println(@lib_async.stringify_async_runtime_state(@lib_async.async_runtime_state(), pretty=true)) + println( + "examples/async_basic is shipped as a native-only example because async fn main entry support is still limited on non-native targets, even though src-async itself keeps compatibility behavior.", + ) + println( + @lib_async.stringify_async_runtime_state( + @lib_async.async_runtime_state(), + pretty=true, + ), + ) let raw = "{\"logger\":{\"min_level\":\"info\",\"target\":\"async.demo\",\"timestamp\":true,\"sink\":{\"kind\":\"text_console\",\"text_formatter\":{\"show_timestamp\":false,\"separator\":\" | \"}}},\"async_config\":{\"max_pending\":2,\"overflow\":\"DropOldest\",\"max_batch\":4,\"linger_ms\":5,\"flush\":\"Batch\"}}" let config = @lib_async.parse_async_logger_build_config_text(raw) catch { @@ -14,10 +22,12 @@ async fn main { let logger = @lib_async.build_async_logger(config) .with_context_fields([@lib.field("service", "bitlogger")]) .with_filter(@lib.target_has_prefix("async")) - .with_patch(@lib.compose_patches([ - @lib.prefix_message("[async] "), - @lib.redact_fields(["token"]), - ])) + .with_patch( + @lib.compose_patches([ + @lib.prefix_message("[async] "), + @lib.redact_fields(["token"]), + ]), + ) println(@lib_async.stringify_async_logger_state(logger.state(), pretty=true)) diff --git a/examples/async_basic/moon.pkg b/examples/async_basic/moon.pkg index 0347e10..a0318ec 100644 --- a/examples/async_basic/moon.pkg +++ b/examples/async_basic/moon.pkg @@ -1,7 +1,7 @@ import { "Nanaloveyuki/BitLogger/src" @lib, "Nanaloveyuki/BitLogger/src-async" @lib_async, - "moonbitlang/async" @async, + "moonbitlang/async", } supported_targets = "+native" diff --git a/examples/basic/main.mbt b/examples/basic/main.mbt index 0167d65..3bb9164 100644 --- a/examples/basic/main.mbt +++ b/examples/basic/main.mbt @@ -1,10 +1,14 @@ +///| fn main { let preset_logger = @lib.build_logger( @lib.with_queue( @lib.text_console( min_level=@lib.Level::Info, target="preset", - text_formatter=@lib.TextFormatterConfig::new(show_timestamp=false, separator=" | "), + text_formatter=@lib.TextFormatterConfig::new( + show_timestamp=false, + separator=" | ", + ), ), max_pending=2, overflow=@lib.QueueOverflowPolicy::DropOldest, @@ -20,8 +24,11 @@ fn main { @lib.info("hello from BitLogger", fields=[@lib.field("mode", "demo")]) // Direct Logger::new(...) composition stays useful for custom sink graphs. - let logger = @lib.Logger::new(@lib.console_sink(), min_level=@lib.Level::Trace, target="custom") - .with_context_fields([@lib.field("service", "bitlogger")]) + let logger = @lib.Logger::new( + @lib.console_sink(), + min_level=@lib.Level::Trace, + target="custom", + ).with_context_fields([@lib.field("service", "bitlogger")]) logger.debug("custom logger ready", fields=[@lib.field("sink", "console")]) let bound_logger = @lib.Logger::new( @@ -31,7 +38,11 @@ fn main { ).bind(@lib.fields([("service", "bitlogger"), ("scope", "audit")])) bound_logger.info("bound logger ready", fields=[@lib.field("mode", "demo")]) - let json_logger = @lib.Logger::new(@lib.json_console_sink(), min_level=@lib.Level::Info, target="json") + let json_logger = @lib.Logger::new( + @lib.json_console_sink(), + min_level=@lib.Level::Info, + target="json", + ) json_logger.info("json output", fields=[@lib.field("kind", "example")]) let fanout_logger = @lib.Logger::new( @@ -55,12 +66,18 @@ fn main { split_logger.info("normal output") split_logger.warn("warning output") - let timed_logger = @lib.Logger::new(@lib.console_sink(), min_level=@lib.Level::Info, target="timed") - .with_timestamp() + let timed_logger = @lib.Logger::new( + @lib.console_sink(), + min_level=@lib.Level::Info, + target="timed", + ).with_timestamp() timed_logger.info("timestamp enabled", fields=[@lib.field("kind", "time")]) - let child_logger = @lib.Logger::new(@lib.console_sink(), min_level=@lib.Level::Info, target="app") - .child("worker") + let child_logger = @lib.Logger::new( + @lib.console_sink(), + min_level=@lib.Level::Info, + target="app", + ).child("worker") child_logger.info("child target ready") let callback_logger = @lib.Logger::new( @@ -90,8 +107,8 @@ fn main { color_mode=@lib.ColorMode::Always, ).with_style_tags( @lib.default_style_tag_registry() - .set_tag("accent", fg=Some("#4cc9f0"), bold=true) - .define_alias("danger", "red"), + .set_tag("accent", fg=Some("#4cc9f0"), bold=true) + .define_alias("danger", "red"), ) let styled_logger = @lib.Logger::new( @lib.text_console_sink(styled_formatter), @@ -109,25 +126,36 @@ fn main { min_level=@lib.Level::Info, target="file", ) - file_logger.info("native file sink ready", fields=[@lib.field("kind", "file")]) + file_logger.info("native file sink ready", fields=[ + @lib.field("kind", "file"), + ]) ignore(file_logger.sink.flush()) ignore(file_logger.sink.close()) } let buffered = @lib.buffered_sink(@lib.console_sink(), flush_limit=2) - let buffered_logger = @lib.Logger::new(buffered, min_level=@lib.Level::Info, target="buffered") + let buffered_logger = @lib.Logger::new( + buffered, + min_level=@lib.Level::Info, + target="buffered", + ) buffered_logger.info("buffered one") buffered_logger.info("buffered two") buffered.flush() - let filtered = @lib.filter_sink( - @lib.console_sink(), - fn(rec) { - rec.target == "kept" - }, + let filtered = @lib.filter_sink(@lib.console_sink(), fn(rec) { + rec.target == "kept" + }) + let kept_logger = @lib.Logger::new( + filtered, + min_level=@lib.Level::Info, + target="kept", + ) + let dropped_logger = @lib.Logger::new( + filtered, + min_level=@lib.Level::Info, + target="dropped", ) - let kept_logger = @lib.Logger::new(filtered, min_level=@lib.Level::Info, target="kept") - let dropped_logger = @lib.Logger::new(filtered, min_level=@lib.Level::Info, target="dropped") kept_logger.info("filter kept this") dropped_logger.info("filter dropped this") @@ -135,10 +163,12 @@ fn main { @lib.console_sink(), min_level=@lib.Level::Info, target="service", - ).with_filter(@lib.all_of([ - @lib.target_has_prefix("service"), - @lib.message_contains("kept"), - ])) + ).with_filter( + @lib.all_of([ + @lib.target_has_prefix("service"), + @lib.message_contains("kept"), + ]), + ) filtered_logger.info("logger filter dropped this") filtered_logger.child("api").info("logger filter kept this") @@ -146,12 +176,17 @@ fn main { @lib.console_sink(), min_level=@lib.Level::Info, target="auth", - ).with_patch(@lib.compose_patches([ - @lib.prefix_message("[safe] "), - @lib.redact_fields(["token"]), - @lib.append_fields([@lib.field("service", "bitlogger")]), - ])) - patched_logger.info("login", fields=[@lib.field("user", "alice"), @lib.field("token", "secret")]) + ).with_patch( + @lib.compose_patches([ + @lib.prefix_message("[safe] "), + @lib.redact_fields(["token"]), + @lib.append_fields([@lib.field("service", "bitlogger")]), + ]), + ) + patched_logger.info("login", fields=[ + @lib.field("user", "alice"), + @lib.field("token", "secret"), + ]) let config_logger = @lib.parse_and_build_logger( "{\"min_level\":\"debug\",\"target\":\"config.demo\",\"timestamp\":true,\"sink\":{\"kind\":\"text_console\",\"text_formatter\":{\"show_timestamp\":false,\"field_separator\":\",\",\"template\":\"[{level}] {target} {message} :: {fields}\"}},\"queue\":{\"max_pending\":2,\"overflow\":\"DropOldest\"}}", @@ -165,7 +200,10 @@ fn main { config_logger.info("configured from json") ignore(config_logger.flush()) - let queue_config = @lib.QueueConfig::new(4, overflow=@lib.QueueOverflowPolicy::DropOldest) + let queue_config = @lib.QueueConfig::new( + 4, + overflow=@lib.QueueOverflowPolicy::DropOldest, + ) println(@lib.stringify_queue_config(queue_config)) let formatter_config = @lib.TextFormatterConfig::new( @@ -195,13 +233,17 @@ fn main { let file_runtime_logger = @lib.build_logger( @lib.LoggerConfig::new( - sink=@lib.SinkConfig::new(kind=@lib.SinkKind::File, path="bitlogger-runtime-demo.log"), + sink=@lib.SinkConfig::new( + kind=@lib.SinkKind::File, + path="bitlogger-runtime-demo.log", + ), queue=Some(@lib.QueueConfig::new(8)), ), ) file_runtime_logger.info("runtime file logger ready") match file_runtime_logger.file_runtime_state() { - Some(snapshot) => println(@lib.stringify_runtime_file_state(snapshot, pretty=true)) + Some(snapshot) => + println(@lib.stringify_runtime_file_state(snapshot, pretty=true)) None => () } ignore(file_runtime_logger.file_close()) diff --git a/examples/config_build/main.mbt b/examples/config_build/main.mbt index 1549dd5..117b856 100644 --- a/examples/config_build/main.mbt +++ b/examples/config_build/main.mbt @@ -1,3 +1,4 @@ +///| fn main { let config = @lib.parse_logger_config_text( "{\"min_level\":\"debug\",\"target\":\"demo.config\",\"sink\":{\"kind\":\"text_console\",\"text_formatter\":{\"show_timestamp\":false,\"separator\":\" | \",\"template\":\"[{level}] {target} {message}\"}},\"queue\":{\"max_pending\":2,\"overflow\":\"DropOldest\"}}", diff --git a/examples/console_basic/main.mbt b/examples/console_basic/main.mbt index 9bd1718..ff8849a 100644 --- a/examples/console_basic/main.mbt +++ b/examples/console_basic/main.mbt @@ -1,3 +1,4 @@ +///| fn main { let logger = @lib.build_logger( @lib.console(min_level=@lib.Level::Info, target="demo.console"), diff --git a/examples/file_rotation/main.mbt b/examples/file_rotation/main.mbt index 2c03538..c05413a 100644 --- a/examples/file_rotation/main.mbt +++ b/examples/file_rotation/main.mbt @@ -1,6 +1,9 @@ +///| fn main { if !@lib.native_files_supported() { - println("native file sink is not available on this backend; examples/presets stays portable, while file_rotation remains native-focused") + println( + "native file sink is not available on this backend; examples/presets stays portable, while file_rotation remains native-focused", + ) return } diff --git a/examples/presets/main.mbt b/examples/presets/main.mbt index a603ce6..f1c922c 100644 --- a/examples/presets/main.mbt +++ b/examples/presets/main.mbt @@ -1,3 +1,4 @@ +///| fn main { let config = @lib.with_queue( @lib.text_console( @@ -36,10 +37,14 @@ fn main { } let file_logger = @lib.build_logger(file_config) - file_logger.info("file preset ready", fields=[@lib.field("kind", "native-only")]) + file_logger.info("file preset ready", fields=[ + @lib.field("kind", "native-only"), + ]) ignore(file_logger.flush()) ignore(file_logger.file_close()) } else { - println("file presets are skipped on this backend because native file support is unavailable") + println( + "file presets are skipped on this backend because native file support is unavailable", + ) } } diff --git a/examples/style_tags/main.mbt b/examples/style_tags/main.mbt index 9996b83..4545751 100644 --- a/examples/style_tags/main.mbt +++ b/examples/style_tags/main.mbt @@ -1,3 +1,4 @@ +///| fn main { let logger = @lib.build_logger( @lib.text_console( @@ -6,9 +7,7 @@ fn main { text_formatter=@lib.TextFormatterConfig::new( show_timestamp=false, color_mode=@lib.ColorMode::Always, - style_tags={ - "accent": @lib.text_style(fg=Some("#4cc9f0"), bold=true), - }, + style_tags={ "accent": @lib.text_style(fg=Some("#4cc9f0"), bold=true) }, ), ), ) diff --git a/examples/text_formatter/main.mbt b/examples/text_formatter/main.mbt index cc0b07a..64603db 100644 --- a/examples/text_formatter/main.mbt +++ b/examples/text_formatter/main.mbt @@ -1,3 +1,4 @@ +///| fn main { let logger = @lib.build_logger( @lib.text_console( @@ -11,5 +12,8 @@ fn main { ), ), ) - logger.info("formatted output", fields=[@lib.field("user", "alice"), @lib.field("request_id", "42")]) + logger.info("formatted output", fields=[ + @lib.field("user", "alice"), + @lib.field("request_id", "42"), + ]) } diff --git a/src-async/BitLoggerAsync_test.mbt b/src-async/BitLoggerAsync_test.mbt index 0a08f7b..2ab0e64 100644 --- a/src-async/BitLoggerAsync_test.mbt +++ b/src-async/BitLoggerAsync_test.mbt @@ -1,15 +1,19 @@ +///| suberror TestFlushError { TestFlushError(String) } +///| async test "shutdown drains pending records" { - inspect(async_runtime_mode_label(async_runtime_mode()) == "native_worker" || async_runtime_mode_label(async_runtime_mode()) == "compatibility", content="true") + inspect( + async_runtime_mode_label(async_runtime_mode()) == "native_worker" || + async_runtime_mode_label(async_runtime_mode()) == "compatibility", + content="true", + ) let written : Ref[Array[String]] = Ref([]) let flushes : Ref[Int] = Ref(0) let logger = async_logger( - @bitlogger.callback_sink(fn(rec) { - written.val.push(rec.message) - }), + @bitlogger.callback_sink(fn(rec) { written.val.push(rec.message) }), config=AsyncLoggerConfig::new( max_pending=4, overflow=AsyncOverflowPolicy::Blocking, @@ -36,22 +40,24 @@ async test "shutdown drains pending records" { inspect(logger.is_running(), content="false") inspect(logger.has_failed(), content="false") inspect(logger.pending_count(), content="0") - inspect(match logger.flush_policy() { - AsyncFlushPolicy::Never => "Never" - AsyncFlushPolicy::Batch => "Batch" - AsyncFlushPolicy::Shutdown => "Shutdown" - }, content="Batch") + inspect( + match logger.flush_policy() { + AsyncFlushPolicy::Never => "Never" + AsyncFlushPolicy::Batch => "Batch" + AsyncFlushPolicy::Shutdown => "Shutdown" + }, + content="Batch", + ) inspect(written.val.length(), content="2") inspect(written.val[0], content="one") inspect(written.val[1], content="two") inspect(flushes.val, content="1") } +///| async test "close clear counts abandoned records as dropped" { let logger = async_logger( - @bitlogger.callback_sink(fn(_) { - - }), + @bitlogger.callback_sink(fn(_) { }), config=AsyncLoggerConfig::new( max_pending=4, overflow=AsyncOverflowPolicy::Blocking, @@ -70,11 +76,10 @@ async test "close clear counts abandoned records as dropped" { inspect(logger.dropped_count(), content="2") } +///| async test "shutdown clear closes without worker startup" { let logger = async_logger( - @bitlogger.callback_sink(fn(_) { - - }), + @bitlogger.callback_sink(fn(_) { }), config=AsyncLoggerConfig::new( max_pending=2, overflow=AsyncOverflowPolicy::Blocking, @@ -91,11 +96,10 @@ async test "shutdown clear closes without worker startup" { inspect(logger.dropped_count(), content="1") } +///| async test "closed blocking logger does not add pending count on later log attempts" { let logger = async_logger( - @bitlogger.callback_sink(fn(_) { - - }), + @bitlogger.callback_sink(fn(_) { }), config=AsyncLoggerConfig::new( max_pending=2, overflow=AsyncOverflowPolicy::Blocking, @@ -114,12 +118,11 @@ async test "closed blocking logger does not add pending count on later log attem inspect(logger.dropped_count(), content="1") } +///| async test "closed logger runtime determines whether later log reaches patch path" { let patched : Ref[Int] = Ref(0) let logger = async_logger( - @bitlogger.callback_sink(fn(_) { - - }), + @bitlogger.callback_sink(fn(_) { }), config=AsyncLoggerConfig::new( max_pending=2, overflow=AsyncOverflowPolicy::Blocking, @@ -146,6 +149,7 @@ async test "closed logger runtime determines whether later log reaches patch pat inspect(logger.dropped_count(), content="1") } +///| async test "async logger with_filter composes and leaves base logger unchanged" { let written_targets : Ref[Array[String]] = Ref([]) let written_messages : Ref[Array[String]] = Ref([]) @@ -158,11 +162,13 @@ async test "async logger with_filter composes and leaves base logger unchanged" min_level=@bitlogger.Level::Trace, target="service", ) - let filtered = base.with_filter(@bitlogger.all_of([ - @bitlogger.target_has_prefix("service.api"), - @bitlogger.level_at_least(@bitlogger.Level::Info), - @bitlogger.message_contains("visible"), - ])) + let filtered = base.with_filter( + @bitlogger.all_of([ + @bitlogger.target_has_prefix("service.api"), + @bitlogger.level_at_least(@bitlogger.Level::Info), + @bitlogger.message_contains("visible"), + ]), + ) @async.with_task_group(group => { group.spawn_bg(() => filtered.run()) @@ -178,9 +184,7 @@ async test "async logger with_filter composes and leaves base logger unchanged" let base_written : Ref[Array[String]] = Ref([]) let untouched = async_logger( - @bitlogger.callback_sink(fn(rec) { - base_written.val.push(rec.message) - }), + @bitlogger.callback_sink(fn(rec) { base_written.val.push(rec.message) }), config=AsyncLoggerConfig::new(max_pending=4), min_level=@bitlogger.Level::Info, target="base", @@ -197,31 +201,37 @@ async test "async logger with_filter composes and leaves base logger unchanged" inspect(base_written.val[0], content="base still writes") } +///| async test "async logger with_patch composes before filter and does not mutate base logger" { let captured_target : Ref[String] = Ref("") let captured_message : Ref[String] = Ref("") let captured_fields : Ref[Array[@bitlogger.Field]] = Ref([]) let logger = async_logger( - @bitlogger.callback_sink(fn(rec) { - captured_target.val = rec.target - captured_message.val = rec.message - captured_fields.val = rec.fields - }), - config=AsyncLoggerConfig::new(max_pending=4), - min_level=@bitlogger.Level::Info, - target="auth", - ) - .with_patch(@bitlogger.compose_patches([ - @bitlogger.set_target("audit.auth"), - @bitlogger.prefix_message("[safe] "), - @bitlogger.redact_field("token"), - @bitlogger.append_fields([@bitlogger.field("service", "bitlogger")]), - ])) + @bitlogger.callback_sink(fn(rec) { + captured_target.val = rec.target + captured_message.val = rec.message + captured_fields.val = rec.fields + }), + config=AsyncLoggerConfig::new(max_pending=4), + min_level=@bitlogger.Level::Info, + target="auth", + ) + .with_patch( + @bitlogger.compose_patches([ + @bitlogger.set_target("audit.auth"), + @bitlogger.prefix_message("[safe] "), + @bitlogger.redact_field("token"), + @bitlogger.append_fields([@bitlogger.field("service", "bitlogger")]), + ]), + ) .with_filter(@bitlogger.target_is("audit.auth")) @async.with_task_group(group => { group.spawn_bg(() => logger.run()) - logger.info("login", fields=[@bitlogger.field("token", "secret"), @bitlogger.field("user", "alice")]) + logger.info("login", fields=[ + @bitlogger.field("token", "secret"), + @bitlogger.field("user", "alice"), + ]) logger.shutdown() }) @@ -262,21 +272,18 @@ async test "async logger with_patch composes before filter and does not mutate b inspect(base_fields.val[0].value, content="secret") } +///| async test "async logger with_min_level only affects derived logger threshold" { let base_written : Ref[Array[String]] = Ref([]) let derived_written : Ref[Array[String]] = Ref([]) let base = async_logger( - @bitlogger.callback_sink(fn(rec) { - base_written.val.push(rec.message) - }), + @bitlogger.callback_sink(fn(rec) { base_written.val.push(rec.message) }), config=AsyncLoggerConfig::new(max_pending=4), min_level=@bitlogger.Level::Info, target="level.base", ) let derived = async_logger( - @bitlogger.callback_sink(fn(rec) { - derived_written.val.push(rec.message) - }), + @bitlogger.callback_sink(fn(rec) { derived_written.val.push(rec.message) }), config=AsyncLoggerConfig::new(max_pending=4), min_level=@bitlogger.Level::Info, target="level.derived", @@ -302,12 +309,11 @@ async test "async logger with_min_level only affects derived logger threshold" { inspect(derived_written.val[0], content="derived error kept") } +///| async test "async logger with_target replaces default target without mutating base logger" { let written_targets : Ref[Array[String]] = Ref([]) let logger = async_logger( - @bitlogger.callback_sink(fn(rec) { - written_targets.val.push(rec.target) - }), + @bitlogger.callback_sink(fn(rec) { written_targets.val.push(rec.target) }), config=AsyncLoggerConfig::new(max_pending=4), min_level=@bitlogger.Level::Warn, target="async.base", @@ -330,9 +336,7 @@ async test "async logger with_target replaces default target without mutating ba let base_targets : Ref[Array[String]] = Ref([]) let base = async_logger( - @bitlogger.callback_sink(fn(rec) { - base_targets.val.push(rec.target) - }), + @bitlogger.callback_sink(fn(rec) { base_targets.val.push(rec.target) }), config=AsyncLoggerConfig::new(max_pending=4), min_level=@bitlogger.Level::Warn, target="async.base", @@ -349,12 +353,11 @@ async test "async logger with_target replaces default target without mutating ba inspect(base_targets.val[0], content="async.base") } +///| async test "async logger with_context_fields prepends shared fields without mutating base logger" { let written_fields : Ref[Array[@bitlogger.Field]] = Ref([]) let logger = async_logger( - @bitlogger.callback_sink(fn(rec) { - written_fields.val = rec.fields - }), + @bitlogger.callback_sink(fn(rec) { written_fields.val = rec.fields }), config=AsyncLoggerConfig::new(max_pending=4), min_level=@bitlogger.Level::Warn, target="async.ctx", @@ -382,6 +385,7 @@ async test "async logger with_context_fields prepends shared fields without muta inspect(written_fields.val[2].value, content="test") } +///| async test "async logger child composes target and preserves other state" { let written_target : Ref[String] = Ref("") let written_timestamp : Ref[UInt64] = Ref(0UL) @@ -412,9 +416,7 @@ async test "async logger child composes target and preserves other state" { inspect(written_timestamp.val > 0UL, content="true") let root_child = async_logger( - @bitlogger.callback_sink(fn(_) { - - }), + @bitlogger.callback_sink(fn(_) { }), config=AsyncLoggerConfig::new(max_pending=4), min_level=@bitlogger.Level::Info, target="", @@ -422,9 +424,7 @@ async test "async logger child composes target and preserves other state" { inspect(root_child.target, content="worker") let keep_parent = async_logger( - @bitlogger.callback_sink(fn(_) { - - }), + @bitlogger.callback_sink(fn(_) { }), config=AsyncLoggerConfig::new(max_pending=4), min_level=@bitlogger.Level::Info, target="sdk", @@ -432,13 +432,12 @@ async test "async logger child composes target and preserves other state" { inspect(keep_parent.target, content="sdk") } +///| async test "async logger with_timestamp only affects derived logger timestamp flag" { let base_timestamp : Ref[UInt64] = Ref(0UL) let derived_timestamp : Ref[UInt64] = Ref(0UL) let base = async_logger( - @bitlogger.callback_sink(fn(rec) { - base_timestamp.val = rec.timestamp_ms - }), + @bitlogger.callback_sink(fn(rec) { base_timestamp.val = rec.timestamp_ms }), config=AsyncLoggerConfig::new(max_pending=4), min_level=@bitlogger.Level::Warn, target="async.ts.base", @@ -468,6 +467,7 @@ async test "async logger with_timestamp only affects derived logger timestamp fl inspect(derived_timestamp.val > 0UL, content="true") } +///| async test "async logger log supports per-call target override" { let written_target : Ref[String] = Ref("") let written_message : Ref[String] = Ref("") @@ -528,6 +528,7 @@ async test "async logger log supports per-call target override" { inspect(second_fields.val[0].key, content="service") } +///| async test "async logger severity helpers use stored target without override" { let written_levels : Ref[Array[String]] = Ref([]) let written_targets : Ref[Array[String]] = Ref([]) @@ -570,6 +571,7 @@ async test "async logger severity helpers use stored target without override" { inspect(written_fields.val[2][1].value, content="error") } +///| test "async logger config stringify roundtrips stable fields" { let text = stringify_async_logger_config( AsyncLoggerConfig::new( @@ -584,36 +586,50 @@ test "async logger config stringify roundtrips stable fields" { inspect(config.max_pending, content="8") inspect(config.max_batch, content="3") inspect(config.linger_ms, content="25") - inspect(match config.overflow { - AsyncOverflowPolicy::Blocking => "Blocking" - AsyncOverflowPolicy::DropOldest => "DropOldest" - AsyncOverflowPolicy::DropNewest => "DropNewest" - }, content="DropOldest") - inspect(match config.flush { - AsyncFlushPolicy::Never => "Never" - AsyncFlushPolicy::Batch => "Batch" - AsyncFlushPolicy::Shutdown => "Shutdown" - }, content="Batch") + inspect( + match config.overflow { + AsyncOverflowPolicy::Blocking => "Blocking" + AsyncOverflowPolicy::DropOldest => "DropOldest" + AsyncOverflowPolicy::DropNewest => "DropNewest" + }, + content="DropOldest", + ) + inspect( + match config.flush { + AsyncFlushPolicy::Never => "Never" + AsyncFlushPolicy::Batch => "Batch" + AsyncFlushPolicy::Shutdown => "Shutdown" + }, + content="Batch", + ) } +///| test "async logger config parser accepts compatibility aliases" { let config = parse_async_logger_config_text( "{\"overflow\":\"DropLatest\",\"flush\":\"None\",\"max_batch\":0,\"linger_ms\":-2}", ) inspect(config.max_batch, content="1") inspect(config.linger_ms, content="0") - inspect(match config.overflow { - AsyncOverflowPolicy::Blocking => "Blocking" - AsyncOverflowPolicy::DropOldest => "DropOldest" - AsyncOverflowPolicy::DropNewest => "DropNewest" - }, content="DropNewest") - inspect(match config.flush { - AsyncFlushPolicy::Never => "Never" - AsyncFlushPolicy::Batch => "Batch" - AsyncFlushPolicy::Shutdown => "Shutdown" - }, content="Never") + inspect( + match config.overflow { + AsyncOverflowPolicy::Blocking => "Blocking" + AsyncOverflowPolicy::DropOldest => "DropOldest" + AsyncOverflowPolicy::DropNewest => "DropNewest" + }, + content="DropNewest", + ) + inspect( + match config.flush { + AsyncFlushPolicy::Never => "Never" + AsyncFlushPolicy::Batch => "Batch" + AsyncFlushPolicy::Shutdown => "Shutdown" + }, + content="Never", + ) } +///| async test "async logger config constructor normalizes batch and linger but preserves pending input" { let config = AsyncLoggerConfig::new( max_pending=-3, @@ -625,22 +641,26 @@ async test "async logger config constructor normalizes batch and linger but pres inspect(config.max_pending, content="-3") inspect(config.max_batch, content="1") inspect(config.linger_ms, content="0") - inspect(match config.overflow { - AsyncOverflowPolicy::Blocking => "Blocking" - AsyncOverflowPolicy::DropOldest => "DropOldest" - AsyncOverflowPolicy::DropNewest => "DropNewest" - }, content="DropNewest") - inspect(match config.flush { - AsyncFlushPolicy::Never => "Never" - AsyncFlushPolicy::Batch => "Batch" - AsyncFlushPolicy::Shutdown => "Shutdown" - }, content="Batch") + inspect( + match config.overflow { + AsyncOverflowPolicy::Blocking => "Blocking" + AsyncOverflowPolicy::DropOldest => "DropOldest" + AsyncOverflowPolicy::DropNewest => "DropNewest" + }, + content="DropNewest", + ) + inspect( + match config.flush { + AsyncFlushPolicy::Never => "Never" + AsyncFlushPolicy::Batch => "Batch" + AsyncFlushPolicy::Shutdown => "Shutdown" + }, + content="Batch", + ) let logger = async_logger( - @bitlogger.callback_sink(fn(_) { - - }), - config=config, + @bitlogger.callback_sink(fn(_) { }), + config~, min_level=@bitlogger.Level::Info, target="async.config.normalized", ) @@ -649,6 +669,7 @@ async test "async logger config constructor normalizes batch and linger but pres inspect(logger.dropped_count(), content="1") } +///| test "async build config stringify roundtrips nested logger and async fields" { let text = stringify_async_logger_build_config( AsyncLoggerBuildConfig::new( @@ -674,45 +695,62 @@ test "async build config stringify roundtrips nested logger and async fields" { inspect(config.async_config.max_pending, content="2") inspect(config.async_config.max_batch, content="5") inspect(config.async_config.linger_ms, content="40") - inspect(match config.async_config.overflow { - AsyncOverflowPolicy::Blocking => "Blocking" - AsyncOverflowPolicy::DropOldest => "DropOldest" - AsyncOverflowPolicy::DropNewest => "DropNewest" - }, content="DropNewest") - inspect(match config.async_config.flush { - AsyncFlushPolicy::Never => "Never" - AsyncFlushPolicy::Batch => "Batch" - AsyncFlushPolicy::Shutdown => "Shutdown" - }, content="Shutdown") + inspect( + match config.async_config.overflow { + AsyncOverflowPolicy::Blocking => "Blocking" + AsyncOverflowPolicy::DropOldest => "DropOldest" + AsyncOverflowPolicy::DropNewest => "DropNewest" + }, + content="DropNewest", + ) + inspect( + match config.async_config.flush { + AsyncFlushPolicy::Never => "Never" + AsyncFlushPolicy::Batch => "Batch" + AsyncFlushPolicy::Shutdown => "Shutdown" + }, + content="Shutdown", + ) } +///| test "async build config parser fills omitted sections from defaults" { let config = parse_async_logger_build_config_text("{}") inspect(config.logger.min_level.label(), content="INFO") inspect(config.logger.target, content="") inspect(config.logger.timestamp, content="false") - inspect(match config.logger.sink.kind { - @bitlogger.SinkKind::Console => "Console" - @bitlogger.SinkKind::JsonConsole => "JsonConsole" - @bitlogger.SinkKind::TextConsole => "TextConsole" - @bitlogger.SinkKind::File => "File" - }, content="Console") + inspect( + match config.logger.sink.kind { + @bitlogger.SinkKind::Console => "Console" + @bitlogger.SinkKind::JsonConsole => "JsonConsole" + @bitlogger.SinkKind::TextConsole => "TextConsole" + @bitlogger.SinkKind::File => "File" + }, + content="Console", + ) inspect(config.logger.queue is None, content="true") inspect(config.async_config.max_pending, content="0") inspect(config.async_config.max_batch, content="1") inspect(config.async_config.linger_ms, content="0") - inspect(match config.async_config.overflow { - AsyncOverflowPolicy::Blocking => "Blocking" - AsyncOverflowPolicy::DropOldest => "DropOldest" - AsyncOverflowPolicy::DropNewest => "DropNewest" - }, content="Blocking") - inspect(match config.async_config.flush { - AsyncFlushPolicy::Never => "Never" - AsyncFlushPolicy::Batch => "Batch" - AsyncFlushPolicy::Shutdown => "Shutdown" - }, content="Never") + inspect( + match config.async_config.overflow { + AsyncOverflowPolicy::Blocking => "Blocking" + AsyncOverflowPolicy::DropOldest => "DropOldest" + AsyncOverflowPolicy::DropNewest => "DropNewest" + }, + content="Blocking", + ) + inspect( + match config.async_config.flush { + AsyncFlushPolicy::Never => "Never" + AsyncFlushPolicy::Batch => "Batch" + AsyncFlushPolicy::Shutdown => "Shutdown" + }, + content="Never", + ) } +///| test "async json helpers export stable structured shapes" { let config_json = async_logger_config_to_json( AsyncLoggerConfig::new( @@ -728,11 +766,26 @@ test "async json helpers export stable structured shapes" { @json_parser.stringify(config_json), content="{\"max_pending\":8,\"max_batch\":3,\"linger_ms\":25,\"overflow\":\"DropOldest\",\"flush\":\"Batch\"}", ) - inspect(config_obj.get("max_pending").unwrap().as_number().unwrap().to_int(), content="8") - inspect(config_obj.get("max_batch").unwrap().as_number().unwrap().to_int(), content="3") - inspect(config_obj.get("linger_ms").unwrap().as_number().unwrap().to_int(), content="25") - inspect(config_obj.get("overflow").unwrap().as_string().unwrap(), content="DropOldest") - inspect(config_obj.get("flush").unwrap().as_string().unwrap(), content="Batch") + inspect( + config_obj.get("max_pending").unwrap().as_number().unwrap().to_int(), + content="8", + ) + inspect( + config_obj.get("max_batch").unwrap().as_number().unwrap().to_int(), + content="3", + ) + inspect( + config_obj.get("linger_ms").unwrap().as_number().unwrap().to_int(), + content="25", + ) + inspect( + config_obj.get("overflow").unwrap().as_string().unwrap(), + content="DropOldest", + ) + inspect( + config_obj.get("flush").unwrap().as_string().unwrap(), + content="Batch", + ) let build_json = async_logger_build_config_to_json( AsyncLoggerBuildConfig::new( @@ -754,72 +807,190 @@ test "async json helpers export stable structured shapes" { let build_obj = build_json.as_object().unwrap() let logger_obj = build_obj.get("logger").unwrap().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() + let formatter_obj = sink_obj + .get("text_formatter") + .unwrap() + .as_object() + .unwrap() let async_obj = build_obj.get("async_config").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="async.roundtrip") - inspect(logger_obj.get("timestamp").unwrap().as_bool().unwrap(), content="true") + inspect( + logger_obj.get("min_level").unwrap().as_string().unwrap(), + content="WARN", + ) + inspect( + logger_obj.get("target").unwrap().as_string().unwrap(), + content="async.roundtrip", + ) + inspect( + logger_obj.get("timestamp").unwrap().as_bool().unwrap(), + content="true", + ) inspect(logger_obj.get("queue") is None, content="true") - inspect(sink_obj.get("kind").unwrap().as_string().unwrap(), content="text_console") + inspect( + sink_obj.get("kind").unwrap().as_string().unwrap(), + content="text_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("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(async_obj.get("max_pending").unwrap().as_number().unwrap().to_int(), content="2") - inspect(async_obj.get("max_batch").unwrap().as_number().unwrap().to_int(), content="5") - inspect(async_obj.get("linger_ms").unwrap().as_number().unwrap().to_int(), content="40") - inspect(async_obj.get("overflow").unwrap().as_string().unwrap(), content="DropNewest") - inspect(async_obj.get("flush").unwrap().as_string().unwrap(), content="Shutdown") + 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("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( + async_obj.get("max_pending").unwrap().as_number().unwrap().to_int(), + content="2", + ) + inspect( + async_obj.get("max_batch").unwrap().as_number().unwrap().to_int(), + content="5", + ) + inspect( + async_obj.get("linger_ms").unwrap().as_number().unwrap().to_int(), + content="40", + ) + inspect( + async_obj.get("overflow").unwrap().as_string().unwrap(), + content="DropNewest", + ) + inspect( + async_obj.get("flush").unwrap().as_string().unwrap(), + content="Shutdown", + ) } +///| test "async build config json export materializes parsed omitted defaults" { let parsed = parse_async_logger_build_config_text("{}") let build_json = async_logger_build_config_to_json(parsed) let build_obj = build_json.as_object().unwrap() let logger_obj = build_obj.get("logger").unwrap().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() + let formatter_obj = sink_obj + .get("text_formatter") + .unwrap() + .as_object() + .unwrap() let async_obj = build_obj.get("async_config").unwrap().as_object().unwrap() inspect( @json_parser.stringify(build_json), content="{\"logger\":{\"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\"}}},\"async_config\":{\"max_pending\":0,\"max_batch\":1,\"linger_ms\":0,\"overflow\":\"Blocking\",\"flush\":\"Never\"}}", ) - inspect(logger_obj.get("min_level").unwrap().as_string().unwrap(), content="INFO") + 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("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( + 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") - inspect(async_obj.get("max_pending").unwrap().as_number().unwrap().to_int(), content="0") - inspect(async_obj.get("max_batch").unwrap().as_number().unwrap().to_int(), content="1") - inspect(async_obj.get("linger_ms").unwrap().as_number().unwrap().to_int(), content="0") - inspect(async_obj.get("overflow").unwrap().as_string().unwrap(), content="Blocking") + inspect( + async_obj.get("max_pending").unwrap().as_number().unwrap().to_int(), + content="0", + ) + inspect( + async_obj.get("max_batch").unwrap().as_number().unwrap().to_int(), + content="1", + ) + inspect( + async_obj.get("linger_ms").unwrap().as_number().unwrap().to_int(), + content="0", + ) + inspect( + async_obj.get("overflow").unwrap().as_string().unwrap(), + content="Blocking", + ) inspect(async_obj.get("flush").unwrap().as_string().unwrap(), content="Never") } +///| test "async config parsers reject malformed input" { let invalid_json_error = (fn() -> String raise { ignore(parse_async_logger_config_text("{")) @@ -835,7 +1006,10 @@ test "async config parsers reject malformed input" { })() catch { err => err.to_string() } - inspect(wrong_type_error.contains("Expected number at async_config.max_pending"), content="true") + inspect( + wrong_type_error.contains("Expected number at async_config.max_pending"), + content="true", + ) let invalid_enum_error = (fn() -> String raise { ignore(parse_async_logger_config_text("{\"overflow\":\"Burst\"}")) @@ -843,7 +1017,10 @@ test "async config parsers reject malformed input" { })() catch { err => err.to_string() } - inspect(invalid_enum_error.contains("Unsupported async overflow policy: Burst"), content="true") + inspect( + invalid_enum_error.contains("Unsupported async overflow policy: Burst"), + content="true", + ) let invalid_build_root_error = (fn() -> String raise { ignore(parse_async_logger_build_config_text("[]")) @@ -851,17 +1028,30 @@ test "async config parsers reject malformed input" { })() catch { err => err.to_string() } - inspect(invalid_build_root_error.contains("Expected object at async logger build config root"), content="true") + inspect( + invalid_build_root_error.contains( + "Expected object at async logger build config root", + ), + content="true", + ) let nested_sync_error = (fn() -> String raise { - ignore(parse_async_logger_build_config_text("{\"logger\":{\"timestamp\":\"true\"}}")) + ignore( + parse_async_logger_build_config_text( + "{\"logger\":{\"timestamp\":\"true\"}}", + ), + ) "no error" })() catch { err => err.to_string() } - inspect(nested_sync_error.contains("ConfigError.InvalidConfig"), content="true") + inspect( + nested_sync_error.contains("ConfigError.InvalidConfig"), + content="true", + ) } +///| test "async runtime capability helpers stay consistent" { let mode = async_runtime_mode() let state = async_runtime_state() @@ -870,11 +1060,18 @@ test "async runtime capability helpers stay consistent" { AsyncRuntimeMode::Compatibility => false } inspect( - async_runtime_mode_label(mode) == "native_worker" || async_runtime_mode_label(mode) == "compatibility", + async_runtime_mode_label(mode) == "native_worker" || + async_runtime_mode_label(mode) == "compatibility", + content="true", + ) + inspect( + async_runtime_supports_background_worker() == worker_supported, + content="true", + ) + inspect( + async_runtime_mode_label(state.mode) == async_runtime_mode_label(mode), content="true", ) - inspect(async_runtime_supports_background_worker() == worker_supported, content="true") - inspect(async_runtime_mode_label(state.mode) == async_runtime_mode_label(mode), content="true") inspect(state.background_worker == worker_supported, content="true") inspect( @json_parser.stringify(async_runtime_state_to_json(state)), @@ -894,19 +1091,22 @@ test "async runtime capability helpers stay consistent" { ) } +///| test "async runtime mode and worker flag encode target contract" { let mode_label = async_runtime_mode_label(async_runtime_mode()) let worker = async_runtime_supports_background_worker() - inspect(mode_label == "native_worker" || mode_label == "compatibility", content="true") + inspect( + mode_label == "native_worker" || mode_label == "compatibility", + content="true", + ) inspect((mode_label == "native_worker") == worker, content="true") inspect((mode_label == "compatibility") == !worker, content="true") } +///| test "async logger state snapshot reflects current counters and runtime" { let logger = async_logger( - @bitlogger.callback_sink(fn(_) { - - }), + @bitlogger.callback_sink(fn(_) { }), config=AsyncLoggerConfig::new( max_pending=3, overflow=AsyncOverflowPolicy::DropNewest, @@ -916,19 +1116,30 @@ test "async logger state snapshot reflects current counters and runtime" { target="async.state", ) let state = logger.state() - inspect(async_runtime_mode_label(state.runtime.mode) == async_runtime_mode_label(async_runtime_mode()), content="true") - inspect(state.runtime.background_worker == async_runtime_supports_background_worker(), content="true") + inspect( + async_runtime_mode_label(state.runtime.mode) == + async_runtime_mode_label(async_runtime_mode()), + content="true", + ) + inspect( + state.runtime.background_worker == + async_runtime_supports_background_worker(), + content="true", + ) inspect(state.pending_count, content="0") inspect(state.dropped_count, content="0") inspect(state.is_closed, content="false") inspect(state.is_running, content="false") inspect(state.has_failed, content="false") inspect(state.last_error, content="") - inspect(match state.flush_policy { - AsyncFlushPolicy::Never => "Never" - AsyncFlushPolicy::Batch => "Batch" - AsyncFlushPolicy::Shutdown => "Shutdown" - }, content="Shutdown") + inspect( + match state.flush_policy { + AsyncFlushPolicy::Never => "Never" + AsyncFlushPolicy::Batch => "Batch" + AsyncFlushPolicy::Shutdown => "Shutdown" + }, + content="Shutdown", + ) inspect( @json_parser.stringify(async_logger_state_to_json(state)), content=if async_runtime_supports_background_worker() { @@ -947,12 +1158,11 @@ test "async logger state snapshot reflects current counters and runtime" { ) } +///| async test "run drains queued records in compatibility backends too" { let written : Ref[Array[String]] = Ref([]) let logger = async_logger( - @bitlogger.callback_sink(fn(rec) { - written.val.push(rec.message) - }), + @bitlogger.callback_sink(fn(rec) { written.val.push(rec.message) }), config=AsyncLoggerConfig::new( max_pending=4, overflow=AsyncOverflowPolicy::DropNewest, @@ -979,12 +1189,11 @@ async test "run drains queued records in compatibility backends too" { inspect(written.val[1], content="two") } +///| async test "async logger records worker failures and wait_idle stops early" { let writes : Ref[Int] = Ref(0) let logger = async_logger( - @bitlogger.callback_sink(fn(_) { - writes.val += 1 - }), + @bitlogger.callback_sink(fn(_) { writes.val += 1 }), config=AsyncLoggerConfig::new( max_pending=4, overflow=AsyncOverflowPolicy::Blocking, @@ -992,9 +1201,7 @@ async test "async logger records worker failures and wait_idle stops early" { ), min_level=@bitlogger.Level::Info, target="async.failure", - flush=fn(_) -> Int raise { - raise TestFlushError("flush exploded") - }, + flush=fn(_) -> Int raise { raise TestFlushError("flush exploded") }, ) @async.with_task_group(group => { @@ -1014,13 +1221,12 @@ async test "async logger records worker failures and wait_idle stops early" { inspect(logger.pending_count(), content="0") } +///| async test "later started run resets async failure state before draining remaining backlog" { let writes : Ref[Int] = Ref(0) let flushes : Ref[Int] = Ref(0) let logger = async_logger( - @bitlogger.callback_sink(fn(_) { - writes.val += 1 - }), + @bitlogger.callback_sink(fn(_) { writes.val += 1 }), config=AsyncLoggerConfig::new( max_pending=4, overflow=AsyncOverflowPolicy::Blocking, @@ -1065,12 +1271,11 @@ async test "later started run resets async failure state before draining remaini inspect(logger.pending_count(), content="0") } +///| async test "shutdown after worker failure uses runtime-specific pending cleanup" { let writes : Ref[Int] = Ref(0) let logger = async_logger( - @bitlogger.callback_sink(fn(_) { - writes.val += 1 - }), + @bitlogger.callback_sink(fn(_) { writes.val += 1 }), config=AsyncLoggerConfig::new( max_pending=4, overflow=AsyncOverflowPolicy::Blocking, @@ -1108,20 +1313,23 @@ async test "shutdown after worker failure uses runtime-specific pending cleanup" ) } +///| async test "library async logger keeps a smaller async facade" { let written_targets : Ref[Array[String]] = Ref([]) let written_messages : Ref[Array[String]] = Ref([]) let written_field_counts : Ref[Array[Int]] = Ref([]) let logger = LibraryAsyncLogger::new( - @bitlogger.callback_sink(fn(rec) { - written_targets.val.push(rec.target) - written_messages.val.push(rec.message) - written_field_counts.val.push(rec.fields.length()) - }), - config=AsyncLoggerConfig::new(max_pending=4), - min_level=@bitlogger.Level::Info, - target="async.lib", - ).with_context_fields([@bitlogger.field("service", "bitlogger")]).child("worker") + @bitlogger.callback_sink(fn(rec) { + written_targets.val.push(rec.target) + written_messages.val.push(rec.message) + written_field_counts.val.push(rec.fields.length()) + }), + config=AsyncLoggerConfig::new(max_pending=4), + min_level=@bitlogger.Level::Info, + target="async.lib", + ) + .with_context_fields([@bitlogger.field("service", "bitlogger")]) + .child("worker") @async.with_task_group(group => { group.spawn_bg(() => logger.run()) @@ -1135,13 +1343,12 @@ async test "library async logger keeps a smaller async facade" { inspect(written_field_counts.val[0], content="2") } +///| async test "library async new preserves config and flush failure contract" { let writes : Ref[Int] = Ref(0) let flushes : Ref[Int] = Ref(0) let logger = LibraryAsyncLogger::new( - @bitlogger.callback_sink(fn(_) { - writes.val += 1 - }), + @bitlogger.callback_sink(fn(_) { writes.val += 1 }), config=AsyncLoggerConfig::new( max_pending=4, overflow=AsyncOverflowPolicy::Blocking, @@ -1161,11 +1368,14 @@ async test "library async new preserves config and flush failure contract" { inspect(logger.is_enabled(@bitlogger.Level::Error), content="true") inspect(logger.is_enabled(@bitlogger.Level::Info), content="false") inspect(full.target, content="async.lib.new") - inspect(match full.flush_policy() { - AsyncFlushPolicy::Never => "Never" - AsyncFlushPolicy::Batch => "Batch" - AsyncFlushPolicy::Shutdown => "Shutdown" - }, content="Batch") + inspect( + match full.flush_policy() { + AsyncFlushPolicy::Never => "Never" + AsyncFlushPolicy::Batch => "Batch" + AsyncFlushPolicy::Shutdown => "Shutdown" + }, + content="Batch", + ) inspect(full.pending_count(), content="0") inspect(full.has_failed(), content="false") inspect(full.last_error(), content="") @@ -1194,16 +1404,15 @@ async test "library async new preserves config and flush failure contract" { inspect(full.is_closed(), content="true") } +///| async test "library async context binding replaces stored field set" { let written_fields : Ref[Array[@bitlogger.Field]] = Ref([]) let logger = LibraryAsyncLogger::new( - @bitlogger.callback_sink(fn(rec) { - written_fields.val = rec.fields - }), - config=AsyncLoggerConfig::new(max_pending=4), - min_level=@bitlogger.Level::Warn, - target="async.lib.ctx", - ) + @bitlogger.callback_sink(fn(rec) { written_fields.val = rec.fields }), + config=AsyncLoggerConfig::new(max_pending=4), + min_level=@bitlogger.Level::Warn, + target="async.lib.ctx", + ) .with_context_fields([@bitlogger.field("old", "gone")]) .with_context_fields([ @bitlogger.field("service", "bitlogger"), @@ -1236,20 +1445,21 @@ async test "library async context binding replaces stored field set" { inspect(written_fields.val[2].value, content="test") } +///| async test "library async bind matches context facade contract" { let written_target : Ref[String] = Ref("") let written_timestamp : Ref[UInt64] = Ref(0UL) let written_fields : Ref[Array[@bitlogger.Field]] = Ref([]) let logger = async_logger( - @bitlogger.callback_sink(fn(rec) { - written_target.val = rec.target - written_timestamp.val = rec.timestamp_ms - written_fields.val = rec.fields - }), - config=AsyncLoggerConfig::new(max_pending=4), - min_level=@bitlogger.Level::Warn, - target="async.lib.bind", - ) + @bitlogger.callback_sink(fn(rec) { + written_target.val = rec.target + written_timestamp.val = rec.timestamp_ms + written_fields.val = rec.fields + }), + config=AsyncLoggerConfig::new(max_pending=4), + min_level=@bitlogger.Level::Warn, + target="async.lib.bind", + ) .with_timestamp() .to_library_async_logger() .bind([@bitlogger.field("service", "bitlogger")]) @@ -1278,18 +1488,19 @@ async test "library async bind matches context facade contract" { inspect(written_fields.val[1].value, content="test") } +///| async test "library async logger with_target preserves facade state while replacing target" { let written_target : Ref[String] = Ref("") let written_timestamp : Ref[UInt64] = Ref(0UL) let logger = async_logger( - @bitlogger.callback_sink(fn(rec) { - written_target.val = rec.target - written_timestamp.val = rec.timestamp_ms - }), - config=AsyncLoggerConfig::new(max_pending=4), - min_level=@bitlogger.Level::Warn, - target="async.lib.base", - ) + @bitlogger.callback_sink(fn(rec) { + written_target.val = rec.target + written_timestamp.val = rec.timestamp_ms + }), + config=AsyncLoggerConfig::new(max_pending=4), + min_level=@bitlogger.Level::Warn, + target="async.lib.base", + ) .with_timestamp() .to_library_async_logger() .with_target("async.lib.retarget") @@ -1316,18 +1527,19 @@ async test "library async logger with_target preserves facade state while replac inspect(written_timestamp.val > 0UL, content="true") } +///| async test "library async logger child composes target through facade" { let written_target : Ref[String] = Ref("") let written_timestamp : Ref[UInt64] = Ref(0UL) let logger = async_logger( - @bitlogger.callback_sink(fn(rec) { - written_target.val = rec.target - written_timestamp.val = rec.timestamp_ms - }), - config=AsyncLoggerConfig::new(max_pending=4), - min_level=@bitlogger.Level::Warn, - target="sdk", - ) + @bitlogger.callback_sink(fn(rec) { + written_target.val = rec.target + written_timestamp.val = rec.timestamp_ms + }), + config=AsyncLoggerConfig::new(max_pending=4), + min_level=@bitlogger.Level::Warn, + target="sdk", + ) .with_timestamp() .to_library_async_logger() .child("worker") @@ -1347,22 +1559,19 @@ async test "library async logger child composes target through facade" { inspect(written_target.val, content="sdk.worker") inspect(written_timestamp.val > 0UL, content="true") - let root_child = LibraryAsyncLogger::new( - @bitlogger.callback_sink(fn(_) { - - }), - ).child("worker") + let root_child = LibraryAsyncLogger::new(@bitlogger.callback_sink(fn(_) { })).child( + "worker", + ) inspect(root_child.to_async_logger().target, content="worker") let keep_parent = LibraryAsyncLogger::new( - @bitlogger.callback_sink(fn(_) { - - }), + @bitlogger.callback_sink(fn(_) { }), target="sdk", ).child("") inspect(keep_parent.to_async_logger().target, content="sdk") } +///| async test "library async logger log supports per-call target override" { let written_target : Ref[String] = Ref("") let written_message : Ref[String] = Ref("") @@ -1376,8 +1585,7 @@ async test "library async logger log supports per-call target override" { config=AsyncLoggerConfig::new(max_pending=4), min_level=@bitlogger.Level::Warn, target="async.lib.default", - ) - .with_context_fields([@bitlogger.field("service", "bitlogger")]) + ).with_context_fields([@bitlogger.field("service", "bitlogger")]) @async.with_task_group(group => { group.spawn_bg(() => logger.run()) @@ -1410,8 +1618,7 @@ async test "library async logger log supports per-call target override" { config=AsyncLoggerConfig::new(max_pending=4), min_level=@bitlogger.Level::Warn, target="async.lib.default", - ) - .with_context_fields([@bitlogger.field("service", "bitlogger")]) + ).with_context_fields([@bitlogger.field("service", "bitlogger")]) @async.with_task_group(group => { group.spawn_bg(() => second.run()) @@ -1425,6 +1632,7 @@ async test "library async logger log supports per-call target override" { inspect(second_fields.val[0].key, content="service") } +///| async test "library async severity helpers use stored target without override" { let written_levels : Ref[Array[String]] = Ref([]) let written_targets : Ref[Array[String]] = Ref([]) @@ -1438,8 +1646,7 @@ async test "library async severity helpers use stored target without override" { config=AsyncLoggerConfig::new(max_pending=4), min_level=@bitlogger.Level::Info, target="async.lib.helpers", - ) - .bind([@bitlogger.field("service", "bitlogger")]) + ).bind([@bitlogger.field("service", "bitlogger")]) @async.with_task_group(group => { group.spawn_bg(() => logger.run()) @@ -1468,12 +1675,11 @@ async test "library async severity helpers use stored target without override" { inspect(written_fields.val[2][1].value, content="error") } +///| async test "library async shutdown preserves wrapped failure cleanup semantics" { let writes : Ref[Int] = Ref(0) let logger = LibraryAsyncLogger::new( - @bitlogger.callback_sink(fn(_) { - writes.val += 1 - }), + @bitlogger.callback_sink(fn(_) { writes.val += 1 }), config=AsyncLoggerConfig::new( max_pending=4, overflow=AsyncOverflowPolicy::Blocking, @@ -1512,11 +1718,10 @@ async test "library async shutdown preserves wrapped failure cleanup semantics" ) } +///| async test "library async shutdown clear abandons pending records through facade" { let logger = LibraryAsyncLogger::new( - @bitlogger.callback_sink(fn(_) { - - }), + @bitlogger.callback_sink(fn(_) { }), config=AsyncLoggerConfig::new( max_pending=2, overflow=AsyncOverflowPolicy::Blocking, @@ -1536,6 +1741,7 @@ async test "library async shutdown clear abandons pending records through facade inspect(full.dropped_count(), content="1") } +///| async test "library async logger can be built from config" { let logger = parse_and_build_library_async_logger( "{\"logger\":{\"min_level\":\"warn\",\"target\":\"async.lib.config\",\"sink\":{\"kind\":\"console\"}},\"async_config\":{\"max_pending\":2,\"overflow\":\"DropNewest\",\"max_batch\":1,\"linger_ms\":0,\"flush\":\"Never\"}}", @@ -1546,13 +1752,19 @@ async test "library async logger can be built from config" { inspect(full.target, content="async.lib.config") } +///| async test "library async builder unwrap matches direct async builder behavior" { let config = AsyncLoggerBuildConfig::new( logger=@bitlogger.LoggerConfig::new( min_level=@bitlogger.Level::Warn, target="async.lib.same-build", timestamp=true, - queue=Some(@bitlogger.QueueConfig::new(3, overflow=@bitlogger.QueueOverflowPolicy::DropNewest)), + queue=Some( + @bitlogger.QueueConfig::new( + 3, + overflow=@bitlogger.QueueOverflowPolicy::DropNewest, + ), + ), sink=@bitlogger.SinkConfig::new(kind=@bitlogger.SinkKind::Console), ), async_config=AsyncLoggerConfig::new( @@ -1569,10 +1781,19 @@ async test "library async builder unwrap matches direct async builder behavior" inspect(full.target, content=direct.target) inspect(full.timestamp == direct.timestamp, content="true") - inspect(logger.is_enabled(@bitlogger.Level::Error) == direct.is_enabled(@bitlogger.Level::Error), content="true") - inspect(logger.is_enabled(@bitlogger.Level::Info) == direct.is_enabled(@bitlogger.Level::Info), content="true") inspect( - stringify_async_logger_state(full_state) == stringify_async_logger_state(direct_state), + logger.is_enabled(@bitlogger.Level::Error) == + direct.is_enabled(@bitlogger.Level::Error), + content="true", + ) + inspect( + logger.is_enabled(@bitlogger.Level::Info) == + direct.is_enabled(@bitlogger.Level::Info), + content="true", + ) + inspect( + stringify_async_logger_state(full_state) == + stringify_async_logger_state(direct_state), content="true", ) let full_sink_kind = match full.sink { @@ -1610,8 +1831,14 @@ async test "library async builder unwrap matches direct async builder behavior" inspect(full.dropped_count() == direct.dropped_count(), content="true") inspect(full.is_closed() == direct.is_closed(), content="true") inspect(full.is_running() == direct.is_running(), content="true") - inspect(full.sink.pending_count() == direct.sink.pending_count(), content="true") - inspect(full.sink.dropped_count() == direct.sink.dropped_count(), content="true") + inspect( + full.sink.pending_count() == direct.sink.pending_count(), + content="true", + ) + inspect( + full.sink.dropped_count() == direct.sink.dropped_count(), + content="true", + ) @async.with_task_group(group => { group.spawn_bg(() => logger.run()) @@ -1628,23 +1855,24 @@ async test "library async builder unwrap matches direct async builder behavior" inspect(full.last_error() == direct.last_error(), content="true") } +///| async test "library async builder log supports per-call target override through facade" { let written_target : Ref[String] = Ref("") let logger = build_library_async_logger( - AsyncLoggerBuildConfig::new( - logger=@bitlogger.LoggerConfig::new( - min_level=@bitlogger.Level::Warn, - target="async.lib.builder.default", - sink=@bitlogger.SinkConfig::new(kind=@bitlogger.SinkKind::Console), + AsyncLoggerBuildConfig::new( + logger=@bitlogger.LoggerConfig::new( + min_level=@bitlogger.Level::Warn, + target="async.lib.builder.default", + sink=@bitlogger.SinkConfig::new(kind=@bitlogger.SinkKind::Console), + ), + async_config=AsyncLoggerConfig::new( + max_pending=2, + overflow=AsyncOverflowPolicy::Blocking, + flush=AsyncFlushPolicy::Never, + ), ), - async_config=AsyncLoggerConfig::new( - max_pending=2, - overflow=AsyncOverflowPolicy::Blocking, - flush=AsyncFlushPolicy::Never, - ), - ), - ) + ) .to_async_logger() .with_patch(fn(rec) { written_target.val = rec.target @@ -1667,19 +1895,19 @@ async test "library async builder log supports per-call target override through let second_target : Ref[String] = Ref("") let second = build_library_async_logger( - AsyncLoggerBuildConfig::new( - logger=@bitlogger.LoggerConfig::new( - min_level=@bitlogger.Level::Warn, - target="async.lib.builder.default", - sink=@bitlogger.SinkConfig::new(kind=@bitlogger.SinkKind::Console), + AsyncLoggerBuildConfig::new( + logger=@bitlogger.LoggerConfig::new( + min_level=@bitlogger.Level::Warn, + target="async.lib.builder.default", + sink=@bitlogger.SinkConfig::new(kind=@bitlogger.SinkKind::Console), + ), + async_config=AsyncLoggerConfig::new( + max_pending=2, + overflow=AsyncOverflowPolicy::Blocking, + flush=AsyncFlushPolicy::Never, + ), ), - async_config=AsyncLoggerConfig::new( - max_pending=2, - overflow=AsyncOverflowPolicy::Blocking, - flush=AsyncFlushPolicy::Never, - ), - ), - ) + ) .to_async_logger() .with_patch(fn(rec) { second_target.val = rec.target @@ -1696,23 +1924,24 @@ async test "library async builder log supports per-call target override through inspect(second_target.val, content="async.lib.builder.default") } +///| async test "library async builder severity helpers use stored target without override" { let written_targets : Ref[Array[String]] = Ref([]) let logger = build_library_async_logger( - AsyncLoggerBuildConfig::new( - logger=@bitlogger.LoggerConfig::new( - min_level=@bitlogger.Level::Trace, - target="async.lib.builder.helpers", - sink=@bitlogger.SinkConfig::new(kind=@bitlogger.SinkKind::Console), + AsyncLoggerBuildConfig::new( + logger=@bitlogger.LoggerConfig::new( + min_level=@bitlogger.Level::Trace, + target="async.lib.builder.helpers", + sink=@bitlogger.SinkConfig::new(kind=@bitlogger.SinkKind::Console), + ), + async_config=AsyncLoggerConfig::new( + max_pending=4, + overflow=AsyncOverflowPolicy::Blocking, + flush=AsyncFlushPolicy::Never, + ), ), - async_config=AsyncLoggerConfig::new( - max_pending=4, - overflow=AsyncOverflowPolicy::Blocking, - flush=AsyncFlushPolicy::Never, - ), - ), - ) + ) .to_async_logger() .with_patch(fn(rec) { written_targets.val.push(rec.target) @@ -1734,6 +1963,7 @@ async test "library async builder severity helpers use stored target without ove inspect(written_targets.val[2], content="async.lib.builder.helpers") } +///| async test "library async builder shutdown clear abandons pending records through facade" { let logger = build_library_async_logger( AsyncLoggerBuildConfig::new( @@ -1760,6 +1990,7 @@ async test "library async builder shutdown clear abandons pending records throug inspect(full.dropped_count(), content="1") } +///| async test "parsed library async logger unwrap keeps async helper surface" { let logger = parse_and_build_library_async_logger( "{\"logger\":{\"min_level\":\"warn\",\"target\":\"async.lib.config.helpers\",\"sink\":{\"kind\":\"console\"}},\"async_config\":{\"max_pending\":2,\"overflow\":\"DropOldest\",\"max_batch\":1,\"linger_ms\":0,\"flush\":\"Shutdown\"}}", @@ -1775,11 +2006,14 @@ async test "parsed library async logger unwrap keeps async helper surface" { inspect(state.is_closed, content="false") inspect(state.is_running, content="false") inspect(state.has_failed, content="false") - inspect(match state.flush_policy { - AsyncFlushPolicy::Never => "Never" - AsyncFlushPolicy::Batch => "Batch" - AsyncFlushPolicy::Shutdown => "Shutdown" - }, content="Shutdown") + inspect( + match state.flush_policy { + AsyncFlushPolicy::Never => "Never" + AsyncFlushPolicy::Batch => "Batch" + AsyncFlushPolicy::Shutdown => "Shutdown" + }, + content="Shutdown", + ) logger.info("skip") logger.error("one") @@ -1803,6 +2037,7 @@ async test "parsed library async logger unwrap keeps async helper surface" { inspect(full.last_error(), content="") } +///| async test "parsed library async logger shutdown clear abandons pending records through facade" { let logger = parse_and_build_library_async_logger( "{\"logger\":{\"min_level\":\"info\",\"target\":\"async.lib.json.clear\",\"sink\":{\"kind\":\"console\"}},\"async_config\":{\"max_pending\":2,\"overflow\":\"Blocking\",\"max_batch\":1,\"linger_ms\":0,\"flush\":\"Never\"}}", @@ -1819,12 +2054,13 @@ async test "parsed library async logger shutdown clear abandons pending records inspect(full.dropped_count(), content="1") } +///| async test "parsed library async logger log supports per-call target override through facade" { let written_target : Ref[String] = Ref("") let logger = parse_and_build_library_async_logger( - "{\"logger\":{\"min_level\":\"warn\",\"target\":\"async.lib.json.default\",\"sink\":{\"kind\":\"console\"}},\"async_config\":{\"max_pending\":2,\"overflow\":\"Blocking\",\"max_batch\":1,\"linger_ms\":0,\"flush\":\"Never\"}}", - ) + "{\"logger\":{\"min_level\":\"warn\",\"target\":\"async.lib.json.default\",\"sink\":{\"kind\":\"console\"}},\"async_config\":{\"max_pending\":2,\"overflow\":\"Blocking\",\"max_batch\":1,\"linger_ms\":0,\"flush\":\"Never\"}}", + ) .to_async_logger() .with_patch(fn(rec) { written_target.val = rec.target @@ -1847,8 +2083,8 @@ async test "parsed library async logger log supports per-call target override th let second_target : Ref[String] = Ref("") let second = parse_and_build_library_async_logger( - "{\"logger\":{\"min_level\":\"warn\",\"target\":\"async.lib.json.default\",\"sink\":{\"kind\":\"console\"}},\"async_config\":{\"max_pending\":2,\"overflow\":\"Blocking\",\"max_batch\":1,\"linger_ms\":0,\"flush\":\"Never\"}}", - ) + "{\"logger\":{\"min_level\":\"warn\",\"target\":\"async.lib.json.default\",\"sink\":{\"kind\":\"console\"}},\"async_config\":{\"max_pending\":2,\"overflow\":\"Blocking\",\"max_batch\":1,\"linger_ms\":0,\"flush\":\"Never\"}}", + ) .to_async_logger() .with_patch(fn(rec) { second_target.val = rec.target @@ -1865,12 +2101,13 @@ async test "parsed library async logger log supports per-call target override th inspect(second_target.val, content="async.lib.json.default") } +///| async test "parsed library async severity helpers use stored target without override" { let written_targets : Ref[Array[String]] = Ref([]) let logger = parse_and_build_library_async_logger( - "{\"logger\":{\"min_level\":\"trace\",\"target\":\"async.lib.json.helpers.target\",\"sink\":{\"kind\":\"console\"}},\"async_config\":{\"max_pending\":4,\"overflow\":\"Blocking\",\"max_batch\":1,\"linger_ms\":0,\"flush\":\"Never\"}}", - ) + "{\"logger\":{\"min_level\":\"trace\",\"target\":\"async.lib.json.helpers.target\",\"sink\":{\"kind\":\"console\"}},\"async_config\":{\"max_pending\":4,\"overflow\":\"Blocking\",\"max_batch\":1,\"linger_ms\":0,\"flush\":\"Never\"}}", + ) .to_async_logger() .with_patch(fn(rec) { written_targets.val.push(rec.target) @@ -1892,6 +2129,7 @@ async test "parsed library async severity helpers use stored target without over inspect(written_targets.val[2], content="async.lib.json.helpers.target") } +///| async test "library async parse-build unwrap matches parsed direct async builder behavior" { let raw = "{\"logger\":{\"min_level\":\"warn\",\"target\":\"async.lib.json.same-build\",\"timestamp\":true,\"queue\":{\"max_pending\":3,\"overflow\":\"DropNewest\"},\"sink\":{\"kind\":\"console\"}},\"async_config\":{\"max_pending\":2,\"overflow\":\"DropOldest\",\"max_batch\":1,\"linger_ms\":0,\"flush\":\"Shutdown\"}}" let logger = parse_and_build_library_async_logger(raw) @@ -1902,10 +2140,19 @@ async test "library async parse-build unwrap matches parsed direct async builder inspect(full.target, content=direct.target) inspect(full.timestamp == direct.timestamp, content="true") - inspect(logger.is_enabled(@bitlogger.Level::Error) == direct.is_enabled(@bitlogger.Level::Error), content="true") - inspect(logger.is_enabled(@bitlogger.Level::Info) == direct.is_enabled(@bitlogger.Level::Info), content="true") inspect( - stringify_async_logger_state(full_state) == stringify_async_logger_state(direct_state), + logger.is_enabled(@bitlogger.Level::Error) == + direct.is_enabled(@bitlogger.Level::Error), + content="true", + ) + inspect( + logger.is_enabled(@bitlogger.Level::Info) == + direct.is_enabled(@bitlogger.Level::Info), + content="true", + ) + inspect( + stringify_async_logger_state(full_state) == + stringify_async_logger_state(direct_state), content="true", ) let full_sink_kind = match full.sink { @@ -1943,8 +2190,14 @@ async test "library async parse-build unwrap matches parsed direct async builder inspect(full.dropped_count() == direct.dropped_count(), content="true") inspect(full.is_closed() == direct.is_closed(), content="true") inspect(full.is_running() == direct.is_running(), content="true") - inspect(full.sink.pending_count() == direct.sink.pending_count(), content="true") - inspect(full.sink.dropped_count() == direct.sink.dropped_count(), content="true") + inspect( + full.sink.pending_count() == direct.sink.pending_count(), + content="true", + ) + inspect( + full.sink.dropped_count() == direct.sink.dropped_count(), + content="true", + ) @async.with_task_group(group => { group.spawn_bg(() => logger.run()) @@ -1961,6 +2214,7 @@ async test "library async parse-build unwrap matches parsed direct async builder inspect(full.last_error() == direct.last_error(), content="true") } +///| async test "library async parse-build unwrap preserves file-backed runtime helpers" { let raw = "{\"logger\":{\"min_level\":\"warn\",\"target\":\"async.lib.json.file.same-build\",\"queue\":{\"max_pending\":3,\"overflow\":\"DropOldest\"},\"sink\":{\"kind\":\"file\",\"path\":\"async-lib-json-file-same-build.log\"}},\"async_config\":{\"max_pending\":2,\"overflow\":\"DropOldest\",\"max_batch\":1,\"linger_ms\":0,\"flush\":\"Shutdown\"}}" let logger = parse_and_build_library_async_logger(raw) @@ -1978,11 +2232,26 @@ async test "library async parse-build unwrap preserves file-backed runtime helpe inspect(full.pending_count() == direct.pending_count(), content="true") inspect(full.dropped_count() == direct.dropped_count(), content="true") - inspect(full.sink.pending_count() == direct.sink.pending_count(), content="true") - inspect(full.sink.dropped_count() == direct.sink.dropped_count(), content="true") - inspect(full.sink.file_available() == direct.sink.file_available(), content="true") - inspect(full.sink.file_append_mode() == direct.sink.file_append_mode(), content="true") - inspect(full.sink.file_auto_flush() == direct.sink.file_auto_flush(), content="true") + inspect( + full.sink.pending_count() == direct.sink.pending_count(), + content="true", + ) + inspect( + full.sink.dropped_count() == direct.sink.dropped_count(), + content="true", + ) + inspect( + full.sink.file_available() == direct.sink.file_available(), + content="true", + ) + inspect( + full.sink.file_append_mode() == direct.sink.file_append_mode(), + content="true", + ) + inspect( + full.sink.file_auto_flush() == direct.sink.file_auto_flush(), + content="true", + ) let full_state = full.sink.file_state() let direct_state = direct.sink.file_state() @@ -1990,10 +2259,22 @@ async test "library async parse-build unwrap preserves file-backed runtime helpe inspect(full_state.available == direct_state.available, content="true") inspect(full_state.append == direct_state.append, content="true") inspect(full_state.auto_flush == direct_state.auto_flush, content="true") - inspect(full_state.open_failures == direct_state.open_failures, content="true") - inspect(full_state.write_failures == direct_state.write_failures, content="true") - inspect(full_state.flush_failures == direct_state.flush_failures, content="true") - inspect(full_state.rotation_failures == direct_state.rotation_failures, content="true") + inspect( + full_state.open_failures == direct_state.open_failures, + content="true", + ) + inspect( + full_state.write_failures == direct_state.write_failures, + content="true", + ) + inspect( + full_state.flush_failures == direct_state.flush_failures, + content="true", + ) + inspect( + full_state.rotation_failures == direct_state.rotation_failures, + content="true", + ) match (full.sink.file_runtime_state(), direct.sink.file_runtime_state()) { (Some(snapshot), Some(other)) => { @@ -2006,23 +2287,61 @@ async test "library async parse-build unwrap preserves file-backed runtime helpe _ => inspect(false, content="true") } - inspect(full.sink.file_set_append_mode(false) == direct.sink.file_set_append_mode(false), content="true") - inspect(full.sink.file_append_mode() == direct.sink.file_append_mode(), content="true") - inspect(full.sink.file_set_auto_flush(false) == direct.sink.file_set_auto_flush(false), content="true") - inspect(full.sink.file_auto_flush() == direct.sink.file_auto_flush(), content="true") inspect( - full.sink.file_set_rotation(Some(@bitlogger.file_rotation(36, max_backups=2))) == - direct.sink.file_set_rotation(Some(@bitlogger.file_rotation(36, max_backups=2))), + full.sink.file_set_append_mode(false) == + direct.sink.file_set_append_mode(false), content="true", ) - inspect(full.sink.file_rotation_enabled() == direct.sink.file_rotation_enabled(), content="true") - inspect(full.sink.file_reopen_append() == direct.sink.file_reopen_append(), content="true") - inspect(full.sink.file_append_mode() == direct.sink.file_append_mode(), content="true") - inspect(full.sink.file_clear_rotation() == direct.sink.file_clear_rotation(), content="true") - inspect(full.sink.file_reset_policy() == direct.sink.file_reset_policy(), content="true") - inspect(full.sink.file_policy_matches_default() == direct.sink.file_policy_matches_default(), content="true") inspect( - full.sink.file_reset_failure_counters() == direct.sink.file_reset_failure_counters(), + full.sink.file_append_mode() == direct.sink.file_append_mode(), + content="true", + ) + inspect( + full.sink.file_set_auto_flush(false) == + direct.sink.file_set_auto_flush(false), + content="true", + ) + inspect( + full.sink.file_auto_flush() == direct.sink.file_auto_flush(), + content="true", + ) + inspect( + full.sink.file_set_rotation( + Some(@bitlogger.file_rotation(36, max_backups=2)), + ) == + direct.sink.file_set_rotation( + Some(@bitlogger.file_rotation(36, max_backups=2)), + ), + content="true", + ) + inspect( + full.sink.file_rotation_enabled() == direct.sink.file_rotation_enabled(), + content="true", + ) + inspect( + full.sink.file_reopen_append() == direct.sink.file_reopen_append(), + content="true", + ) + inspect( + full.sink.file_append_mode() == direct.sink.file_append_mode(), + content="true", + ) + inspect( + full.sink.file_clear_rotation() == direct.sink.file_clear_rotation(), + content="true", + ) + inspect( + full.sink.file_reset_policy() == direct.sink.file_reset_policy(), + content="true", + ) + inspect( + full.sink.file_policy_matches_default() == + direct.sink.file_policy_matches_default(), + content="true", + ) + inspect( + full.sink.file_reset_failure_counters() == + direct.sink.file_reset_failure_counters(), content="true", ) inspect(full.sink.file_flush() == direct.sink.file_flush(), content="true") @@ -2030,6 +2349,7 @@ async test "library async parse-build unwrap preserves file-backed runtime helpe inspect(full.sink.file_close() == direct.sink.file_close(), content="true") } +///| test "library async logger parse-and-build preserves parsed sync queue layer" { let logger = parse_and_build_library_async_logger( "{\"logger\":{\"min_level\":\"warn\",\"target\":\"async.lib.config.queued\",\"queue\":{\"max_pending\":3,\"overflow\":\"DropNewest\"},\"sink\":{\"kind\":\"console\"}},\"async_config\":{\"max_pending\":2,\"overflow\":\"DropNewest\",\"max_batch\":1,\"linger_ms\":0,\"flush\":\"Never\"}}", @@ -2039,22 +2359,29 @@ test "library async logger parse-and-build preserves parsed sync queue layer" { inspect(logger.is_enabled(@bitlogger.Level::Error), content="true") inspect(logger.is_enabled(@bitlogger.Level::Info), content="false") inspect(full.target, content="async.lib.config.queued") - inspect(match full.sink { - @bitlogger.RuntimeSink::QueuedConsole(_) => "QueuedConsole" - @bitlogger.RuntimeSink::QueuedJsonConsole(_) => "QueuedJsonConsole" - @bitlogger.RuntimeSink::QueuedTextConsole(_) => "QueuedTextConsole" - @bitlogger.RuntimeSink::QueuedFile(_) => "QueuedFile" - _ => "Other" - }, content="QueuedConsole") + inspect( + match full.sink { + @bitlogger.RuntimeSink::QueuedConsole(_) => "QueuedConsole" + @bitlogger.RuntimeSink::QueuedJsonConsole(_) => "QueuedJsonConsole" + @bitlogger.RuntimeSink::QueuedTextConsole(_) => "QueuedTextConsole" + @bitlogger.RuntimeSink::QueuedFile(_) => "QueuedFile" + _ => "Other" + }, + content="QueuedConsole", + ) } +///| test "library async text logger can be built from typed config" { let logger = build_library_async_text_logger( AsyncLoggerBuildConfig::new( logger=@bitlogger.text_console( min_level=@bitlogger.Level::Warn, target="async.lib.text", - text_formatter=@bitlogger.TextFormatterConfig::new(show_timestamp=false, separator=" | "), + text_formatter=@bitlogger.TextFormatterConfig::new( + show_timestamp=false, + separator=" | ", + ), ), async_config=AsyncLoggerConfig::new(max_pending=2), ), @@ -2065,13 +2392,19 @@ test "library async text logger can be built from typed config" { inspect(full.target, content="async.lib.text") } +///| async test "library async text builder unwrap matches direct text builder behavior" { let config = AsyncLoggerBuildConfig::new( logger=@bitlogger.LoggerConfig::new( min_level=@bitlogger.Level::Warn, target="async.lib.text.same-build", timestamp=true, - queue=Some(@bitlogger.QueueConfig::new(3, overflow=@bitlogger.QueueOverflowPolicy::DropNewest)), + queue=Some( + @bitlogger.QueueConfig::new( + 3, + overflow=@bitlogger.QueueOverflowPolicy::DropNewest, + ), + ), sink=@bitlogger.SinkConfig::new( kind=@bitlogger.SinkKind::TextConsole, text_formatter=@bitlogger.TextFormatterConfig::new( @@ -2095,14 +2428,30 @@ async test "library async text builder unwrap matches direct text builder behavi inspect(full.target, content=direct.target) inspect(full.timestamp == direct.timestamp, content="true") - inspect(logger.is_enabled(@bitlogger.Level::Error) == direct.is_enabled(@bitlogger.Level::Error), content="true") - inspect(logger.is_enabled(@bitlogger.Level::Info) == direct.is_enabled(@bitlogger.Level::Info), content="true") inspect( - stringify_async_logger_state(full_state) == stringify_async_logger_state(direct_state), + logger.is_enabled(@bitlogger.Level::Error) == + direct.is_enabled(@bitlogger.Level::Error), + content="true", + ) + inspect( + logger.is_enabled(@bitlogger.Level::Info) == + direct.is_enabled(@bitlogger.Level::Info), + content="true", + ) + inspect( + stringify_async_logger_state(full_state) == + stringify_async_logger_state(direct_state), + content="true", + ) + let record = @bitlogger.Record::new( + @bitlogger.Level::Error, + "boom", + target="async.lib.text.same-build", + ) + inspect( + (full.sink.formatter)(record) == (direct.sink.formatter)(record), content="true", ) - let record = @bitlogger.Record::new(@bitlogger.Level::Error, "boom", target="async.lib.text.same-build") - inspect((full.sink.formatter)(record) == (direct.sink.formatter)(record), content="true") logger.info("skip") logger.error("one") @@ -2133,23 +2482,27 @@ async test "library async text builder unwrap matches direct text builder behavi inspect(full.last_error() == direct.last_error(), content="true") } +///| async test "library async text builder log supports per-call target override through facade" { let written_target : Ref[String] = Ref("") let logger = build_library_async_text_logger( - AsyncLoggerBuildConfig::new( - logger=@bitlogger.text_console( - min_level=@bitlogger.Level::Warn, - target="async.lib.text.builder.default", - text_formatter=@bitlogger.TextFormatterConfig::new(show_timestamp=false, separator=" | "), + AsyncLoggerBuildConfig::new( + logger=@bitlogger.text_console( + min_level=@bitlogger.Level::Warn, + target="async.lib.text.builder.default", + text_formatter=@bitlogger.TextFormatterConfig::new( + show_timestamp=false, + separator=" | ", + ), + ), + async_config=AsyncLoggerConfig::new( + max_pending=2, + overflow=AsyncOverflowPolicy::Blocking, + flush=AsyncFlushPolicy::Never, + ), ), - async_config=AsyncLoggerConfig::new( - max_pending=2, - overflow=AsyncOverflowPolicy::Blocking, - flush=AsyncFlushPolicy::Never, - ), - ), - ) + ) .to_async_logger() .with_patch(fn(rec) { written_target.val = rec.target @@ -2172,19 +2525,22 @@ async test "library async text builder log supports per-call target override thr let second_target : Ref[String] = Ref("") let second = build_library_async_text_logger( - AsyncLoggerBuildConfig::new( - logger=@bitlogger.text_console( - min_level=@bitlogger.Level::Warn, - target="async.lib.text.builder.default", - text_formatter=@bitlogger.TextFormatterConfig::new(show_timestamp=false, separator=" | "), + AsyncLoggerBuildConfig::new( + logger=@bitlogger.text_console( + min_level=@bitlogger.Level::Warn, + target="async.lib.text.builder.default", + text_formatter=@bitlogger.TextFormatterConfig::new( + show_timestamp=false, + separator=" | ", + ), + ), + async_config=AsyncLoggerConfig::new( + max_pending=2, + overflow=AsyncOverflowPolicy::Blocking, + flush=AsyncFlushPolicy::Never, + ), ), - async_config=AsyncLoggerConfig::new( - max_pending=2, - overflow=AsyncOverflowPolicy::Blocking, - flush=AsyncFlushPolicy::Never, - ), - ), - ) + ) .to_async_logger() .with_patch(fn(rec) { second_target.val = rec.target @@ -2201,23 +2557,27 @@ async test "library async text builder log supports per-call target override thr inspect(second_target.val, content="async.lib.text.builder.default") } +///| async test "library async text builder severity helpers use stored target without override" { let written_targets : Ref[Array[String]] = Ref([]) let logger = build_library_async_text_logger( - AsyncLoggerBuildConfig::new( - logger=@bitlogger.text_console( - min_level=@bitlogger.Level::Info, - target="async.lib.text.builder.helpers", - text_formatter=@bitlogger.TextFormatterConfig::new(show_timestamp=false, separator=" | "), + AsyncLoggerBuildConfig::new( + logger=@bitlogger.text_console( + min_level=@bitlogger.Level::Info, + target="async.lib.text.builder.helpers", + text_formatter=@bitlogger.TextFormatterConfig::new( + show_timestamp=false, + separator=" | ", + ), + ), + async_config=AsyncLoggerConfig::new( + max_pending=4, + overflow=AsyncOverflowPolicy::Blocking, + flush=AsyncFlushPolicy::Never, + ), ), - async_config=AsyncLoggerConfig::new( - max_pending=4, - overflow=AsyncOverflowPolicy::Blocking, - flush=AsyncFlushPolicy::Never, - ), - ), - ) + ) .to_async_logger() .with_patch(fn(rec) { written_targets.val.push(rec.target) @@ -2239,13 +2599,17 @@ async test "library async text builder severity helpers use stored target withou inspect(written_targets.val[2], content="async.lib.text.builder.helpers") } +///| async test "library async text logger unwrap keeps async helper surface" { let logger = build_library_async_text_logger( AsyncLoggerBuildConfig::new( logger=@bitlogger.text_console( min_level=@bitlogger.Level::Warn, target="async.lib.text.helpers", - text_formatter=@bitlogger.TextFormatterConfig::new(show_timestamp=false, separator=" | "), + text_formatter=@bitlogger.TextFormatterConfig::new( + show_timestamp=false, + separator=" | ", + ), ), async_config=AsyncLoggerConfig::new( max_pending=2, @@ -2265,11 +2629,14 @@ async test "library async text logger unwrap keeps async helper surface" { inspect(state.is_closed, content="false") inspect(state.is_running, content="false") inspect(state.has_failed, content="false") - inspect(match state.flush_policy { - AsyncFlushPolicy::Never => "Never" - AsyncFlushPolicy::Batch => "Batch" - AsyncFlushPolicy::Shutdown => "Shutdown" - }, content="Shutdown") + inspect( + match state.flush_policy { + AsyncFlushPolicy::Never => "Never" + AsyncFlushPolicy::Batch => "Batch" + AsyncFlushPolicy::Shutdown => "Shutdown" + }, + content="Shutdown", + ) logger.info("skip") logger.error("one") @@ -2293,13 +2660,17 @@ async test "library async text logger unwrap keeps async helper surface" { inspect(full.last_error(), content="") } +///| async test "library async text logger shutdown clear abandons pending records through facade" { let logger = build_library_async_text_logger( AsyncLoggerBuildConfig::new( logger=@bitlogger.text_console( min_level=@bitlogger.Level::Info, target="async.lib.text.clear", - text_formatter=@bitlogger.TextFormatterConfig::new(show_timestamp=false, separator=" | "), + text_formatter=@bitlogger.TextFormatterConfig::new( + show_timestamp=false, + separator=" | ", + ), ), async_config=AsyncLoggerConfig::new( max_pending=2, @@ -2319,16 +2690,25 @@ async test "library async text logger shutdown clear abandons pending records th inspect(full.dropped_count(), content="1") } +///| test "library async text logger builder ignores sync queue layer" { let logger = build_library_async_text_logger( AsyncLoggerBuildConfig::new( logger=@bitlogger.LoggerConfig::new( min_level=@bitlogger.Level::Warn, target="async.lib.text.queued", - queue=Some(@bitlogger.QueueConfig::new(3, overflow=@bitlogger.QueueOverflowPolicy::DropNewest)), + queue=Some( + @bitlogger.QueueConfig::new( + 3, + overflow=@bitlogger.QueueOverflowPolicy::DropNewest, + ), + ), sink=@bitlogger.SinkConfig::new( kind=@bitlogger.SinkKind::TextConsole, - text_formatter=@bitlogger.TextFormatterConfig::new(show_timestamp=false, separator=" | "), + text_formatter=@bitlogger.TextFormatterConfig::new( + show_timestamp=false, + separator=" | ", + ), ), ), async_config=AsyncLoggerConfig::new(max_pending=2), @@ -2343,6 +2723,7 @@ test "library async text logger builder ignores sync queue layer" { inspect(full.dropped_count(), content="0") } +///| test "library async text logger ignores sink kind and still uses text formatter" { let logger = build_library_async_text_logger( AsyncLoggerBuildConfig::new( @@ -2368,15 +2749,23 @@ test "library async text logger ignores sink kind and still uses text formatter" inspect(logger.is_enabled(@bitlogger.Level::Info), content="false") inspect(full.target, content="async.lib.text.kind") let rendered = (full.sink.formatter)( - @bitlogger.Record::new(@bitlogger.Level::Error, "boom", target="async.lib.text.kind"), + @bitlogger.Record::new( + @bitlogger.Level::Error, + "boom", + target="async.lib.text.kind", + ), ) inspect(rendered, content="TEXT:async.lib.text.kind:boom") } +///| async test "async logger can project to library async logger" { let logger = build_async_logger( AsyncLoggerBuildConfig::new( - logger=@bitlogger.LoggerConfig::new(target="async.projected", min_level=@bitlogger.Level::Warn), + logger=@bitlogger.LoggerConfig::new( + target="async.projected", + min_level=@bitlogger.Level::Warn, + ), async_config=AsyncLoggerConfig::new(max_pending=2), ), ).to_library_async_logger() @@ -2385,13 +2774,19 @@ async test "async logger can project to library async logger" { inspect(logger.to_async_logger().target, content="async.projected") } +///| async test "build async logger keeps direct helper surface and runtime sink path" { let logger = build_async_logger( AsyncLoggerBuildConfig::new( logger=@bitlogger.LoggerConfig::new( min_level=@bitlogger.Level::Warn, target="async.direct.helpers", - queue=Some(@bitlogger.QueueConfig::new(3, overflow=@bitlogger.QueueOverflowPolicy::DropNewest)), + queue=Some( + @bitlogger.QueueConfig::new( + 3, + overflow=@bitlogger.QueueOverflowPolicy::DropNewest, + ), + ), sink=@bitlogger.SinkConfig::new(kind=@bitlogger.SinkKind::Console), ), async_config=AsyncLoggerConfig::new( @@ -2411,18 +2806,24 @@ async test "build async logger keeps direct helper surface and runtime sink path inspect(state.is_closed, content="false") inspect(state.is_running, content="false") inspect(state.has_failed, content="false") - inspect(match state.flush_policy { - AsyncFlushPolicy::Never => "Never" - AsyncFlushPolicy::Batch => "Batch" - AsyncFlushPolicy::Shutdown => "Shutdown" - }, content="Shutdown") - inspect(match logger.sink { - @bitlogger.RuntimeSink::QueuedConsole(_) => "QueuedConsole" - @bitlogger.RuntimeSink::QueuedJsonConsole(_) => "QueuedJsonConsole" - @bitlogger.RuntimeSink::QueuedTextConsole(_) => "QueuedTextConsole" - @bitlogger.RuntimeSink::QueuedFile(_) => "QueuedFile" - _ => "Other" - }, content="QueuedConsole") + inspect( + match state.flush_policy { + AsyncFlushPolicy::Never => "Never" + AsyncFlushPolicy::Batch => "Batch" + AsyncFlushPolicy::Shutdown => "Shutdown" + }, + content="Shutdown", + ) + inspect( + match logger.sink { + @bitlogger.RuntimeSink::QueuedConsole(_) => "QueuedConsole" + @bitlogger.RuntimeSink::QueuedJsonConsole(_) => "QueuedJsonConsole" + @bitlogger.RuntimeSink::QueuedTextConsole(_) => "QueuedTextConsole" + @bitlogger.RuntimeSink::QueuedFile(_) => "QueuedFile" + _ => "Other" + }, + content="QueuedConsole", + ) logger.info("skip") logger.error("one") @@ -2448,6 +2849,7 @@ async test "build async logger keeps direct helper surface and runtime sink path inspect(logger.last_error(), content="") } +///| async test "build async logger log supports per-call target override" { let written_target : Ref[String] = Ref("") @@ -2464,11 +2866,10 @@ async test "build async logger log supports per-call target override" { flush=AsyncFlushPolicy::Never, ), ), - ) - .with_patch(fn(rec) { - written_target.val = rec.target - rec - }) + ).with_patch(fn(rec) { + written_target.val = rec.target + rec + }) @async.with_task_group(group => { group.spawn_bg(() => logger.run()) @@ -2497,11 +2898,10 @@ async test "build async logger log supports per-call target override" { flush=AsyncFlushPolicy::Never, ), ), - ) - .with_patch(fn(rec) { - second_target.val = rec.target - rec - }) + ).with_patch(fn(rec) { + second_target.val = rec.target + rec + }) @async.with_task_group(group => { group.spawn_bg(() => second.run()) @@ -2512,6 +2912,7 @@ async test "build async logger log supports per-call target override" { inspect(second_target.val, content="async.builder.default") } +///| async test "build async logger severity helpers use stored target without override" { let written_targets : Ref[Array[String]] = Ref([]) @@ -2528,11 +2929,10 @@ async test "build async logger severity helpers use stored target without overri flush=AsyncFlushPolicy::Never, ), ), - ) - .with_patch(fn(rec) { - written_targets.val.push(rec.target) - rec - }) + ).with_patch(fn(rec) { + written_targets.val.push(rec.target) + rec + }) @async.with_task_group(group => { group.spawn_bg(() => logger.run()) @@ -2548,11 +2948,10 @@ async test "build async logger severity helpers use stored target without overri inspect(written_targets.val[2], content="async.builder.helpers") } +///| async test "async logger projection preserves async queue and failure state" { let logger = async_logger( - @bitlogger.callback_sink(fn(_) { - - }), + @bitlogger.callback_sink(fn(_) { }), config=AsyncLoggerConfig::new( max_pending=4, overflow=AsyncOverflowPolicy::Blocking, @@ -2596,6 +2995,7 @@ async test "async logger projection preserves async queue and failure state" { inspect(full.last_error().contains("TestFlushError"), content="true") } +///| async test "async logger projection preserves file-backed runtime helpers through unwrap" { let configured = build_async_logger( AsyncLoggerBuildConfig::new( @@ -2606,7 +3006,12 @@ async test "async logger projection preserves file-backed runtime helpers throug kind=@bitlogger.SinkKind::File, path="async-projected-file.log", ), - queue=Some(@bitlogger.QueueConfig::new(2, overflow=@bitlogger.QueueOverflowPolicy::DropOldest)), + queue=Some( + @bitlogger.QueueConfig::new( + 2, + overflow=@bitlogger.QueueOverflowPolicy::DropOldest, + ), + ), ), async_config=AsyncLoggerConfig::new( max_pending=2, @@ -2627,11 +3032,26 @@ async test "async logger projection preserves file-backed runtime helpers throug full.error("three") inspect(full.pending_count() == configured.pending_count(), content="true") inspect(full.dropped_count() == configured.dropped_count(), content="true") - inspect(full.sink.pending_count() == configured.sink.pending_count(), content="true") - inspect(full.sink.dropped_count() == configured.sink.dropped_count(), content="true") - inspect(full.sink.file_available() == configured.sink.file_available(), content="true") - inspect(full.sink.file_append_mode() == configured.sink.file_append_mode(), content="true") - inspect(full.sink.file_auto_flush() == configured.sink.file_auto_flush(), content="true") + inspect( + full.sink.pending_count() == configured.sink.pending_count(), + content="true", + ) + inspect( + full.sink.dropped_count() == configured.sink.dropped_count(), + content="true", + ) + inspect( + full.sink.file_available() == configured.sink.file_available(), + content="true", + ) + inspect( + full.sink.file_append_mode() == configured.sink.file_append_mode(), + content="true", + ) + inspect( + full.sink.file_auto_flush() == configured.sink.file_auto_flush(), + content="true", + ) let full_state = full.sink.file_state() let configured_state = configured.sink.file_state() @@ -2639,10 +3059,22 @@ async test "async logger projection preserves file-backed runtime helpers throug 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") + 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.sink.file_runtime_state(), configured.sink.file_runtime_state()) { (Some(snapshot), Some(other)) => { @@ -2657,47 +3089,100 @@ async test "async logger projection preserves file-backed runtime helpers throug inspect(full.sink.file_set_append_mode(false), content="true") inspect(configured.sink.file_append_mode(), content="false") - inspect(full.sink.file_append_mode() == configured.sink.file_append_mode(), content="true") + inspect( + full.sink.file_append_mode() == configured.sink.file_append_mode(), + content="true", + ) inspect(full.sink.file_set_auto_flush(false), content="true") inspect(configured.sink.file_auto_flush(), content="false") - inspect(full.sink.file_auto_flush() == configured.sink.file_auto_flush(), content="true") + inspect( + full.sink.file_auto_flush() == configured.sink.file_auto_flush(), + content="true", + ) - inspect(full.sink.file_set_rotation(Some(@bitlogger.file_rotation(36, max_backups=2))), content="true") + inspect( + full.sink.file_set_rotation( + Some(@bitlogger.file_rotation(36, max_backups=2)), + ), + content="true", + ) inspect(configured.sink.file_rotation_enabled(), content="true") - inspect(full.sink.file_rotation_enabled() == configured.sink.file_rotation_enabled(), content="true") + inspect( + full.sink.file_rotation_enabled() == configured.sink.file_rotation_enabled(), + content="true", + ) let reopen_append_ok = full.sink.file_reopen_append() - inspect(reopen_append_ok == configured.sink.file_reopen_append(), content="true") - inspect(full.sink.file_append_mode() == configured.sink.file_append_mode(), content="true") + inspect( + reopen_append_ok == configured.sink.file_reopen_append(), + content="true", + ) + inspect( + full.sink.file_append_mode() == configured.sink.file_append_mode(), + content="true", + ) inspect(full.sink.file_clear_rotation(), content="true") inspect(configured.sink.file_rotation_enabled(), content="false") - inspect(full.sink.file_rotation_enabled() == configured.sink.file_rotation_enabled(), content="true") + inspect( + full.sink.file_rotation_enabled() == configured.sink.file_rotation_enabled(), + content="true", + ) inspect(full.sink.file_reset_policy(), content="true") - inspect(full.sink.file_policy_matches_default() == configured.sink.file_policy_matches_default(), content="true") + inspect( + full.sink.file_policy_matches_default() == + configured.sink.file_policy_matches_default(), + content="true", + ) inspect(full.sink.file_reset_failure_counters(), content="true") let reset_full_state = full.sink.file_state() let reset_configured_state = configured.sink.file_state() - inspect(reset_full_state.open_failures == reset_configured_state.open_failures, content="true") - inspect(reset_full_state.write_failures == reset_configured_state.write_failures, content="true") - inspect(reset_full_state.flush_failures == reset_configured_state.flush_failures, content="true") - inspect(reset_full_state.rotation_failures == reset_configured_state.rotation_failures, content="true") + inspect( + reset_full_state.open_failures == reset_configured_state.open_failures, + content="true", + ) + inspect( + reset_full_state.write_failures == reset_configured_state.write_failures, + content="true", + ) + inspect( + reset_full_state.flush_failures == reset_configured_state.flush_failures, + content="true", + ) + inspect( + reset_full_state.rotation_failures == + reset_configured_state.rotation_failures, + content="true", + ) - inspect(full.sink.file_flush(), content=if configured.sink.file_available() { "true" } else { "false" }) + inspect( + full.sink.file_flush(), + content=if configured.sink.file_available() { "true" } else { "false" }, + ) inspect(full.pending_count() == configured.pending_count(), content="true") let close_expected = configured.sink.file_available() - inspect(full.sink.file_close(), content=if close_expected { "true" } else { "false" }) - inspect(full.sink.file_available() == configured.sink.file_available(), content="true") + inspect( + full.sink.file_close(), + content=if close_expected { "true" } else { "false" }, + ) + inspect( + full.sink.file_available() == configured.sink.file_available(), + content="true", + ) } +///| test "application async logger aliases runtime async entry" { let logger = build_application_async_logger( AsyncLoggerBuildConfig::new( - logger=@bitlogger.LoggerConfig::new(target="async.app", min_level=@bitlogger.Level::Warn), + logger=@bitlogger.LoggerConfig::new( + target="async.app", + min_level=@bitlogger.Level::Warn, + ), async_config=AsyncLoggerConfig::new(max_pending=2), ), ) @@ -2706,13 +3191,19 @@ test "application async logger aliases runtime async entry" { inspect(logger.target, content="async.app") } +///| async test "application async logger builder matches direct async builder behavior" { let config = AsyncLoggerBuildConfig::new( logger=@bitlogger.LoggerConfig::new( min_level=@bitlogger.Level::Warn, target="async.app.same-build", timestamp=true, - queue=Some(@bitlogger.QueueConfig::new(3, overflow=@bitlogger.QueueOverflowPolicy::DropNewest)), + queue=Some( + @bitlogger.QueueConfig::new( + 3, + overflow=@bitlogger.QueueOverflowPolicy::DropNewest, + ), + ), sink=@bitlogger.SinkConfig::new(kind=@bitlogger.SinkKind::Console), ), async_config=AsyncLoggerConfig::new( @@ -2729,15 +3220,18 @@ async test "application async logger builder matches direct async builder behavi inspect(application.target, content=direct.target) inspect(application.timestamp == direct.timestamp, content="true") inspect( - application.is_enabled(@bitlogger.Level::Error) == direct.is_enabled(@bitlogger.Level::Error), + application.is_enabled(@bitlogger.Level::Error) == + direct.is_enabled(@bitlogger.Level::Error), content="true", ) inspect( - application.is_enabled(@bitlogger.Level::Info) == direct.is_enabled(@bitlogger.Level::Info), + application.is_enabled(@bitlogger.Level::Info) == + direct.is_enabled(@bitlogger.Level::Info), content="true", ) inspect( - stringify_async_logger_state(application_state) == stringify_async_logger_state(direct_state), + stringify_async_logger_state(application_state) == + stringify_async_logger_state(direct_state), content="true", ) let application_sink_kind = match application.sink { @@ -2775,8 +3269,14 @@ async test "application async logger builder matches direct async builder behavi inspect(application.dropped_count() == direct.dropped_count(), content="true") inspect(application.is_closed() == direct.is_closed(), content="true") inspect(application.is_running() == direct.is_running(), content="true") - inspect(application.sink.pending_count() == direct.sink.pending_count(), content="true") - inspect(application.sink.dropped_count() == direct.sink.dropped_count(), content="true") + inspect( + application.sink.pending_count() == direct.sink.pending_count(), + content="true", + ) + inspect( + application.sink.dropped_count() == direct.sink.dropped_count(), + content="true", + ) @async.with_task_group(group => { group.spawn_bg(() => application.run()) @@ -2793,13 +3293,22 @@ async test "application async logger builder matches direct async builder behavi inspect(application.last_error() == direct.last_error(), content="true") } +///| async test "application async builder preserves file-backed runtime helpers" { let config = AsyncLoggerBuildConfig::new( logger=@bitlogger.LoggerConfig::new( min_level=@bitlogger.Level::Warn, target="async.app.file.same-build", - sink=@bitlogger.SinkConfig::new(kind=@bitlogger.SinkKind::File, path="async-app-file-same-build.log"), - queue=Some(@bitlogger.QueueConfig::new(3, overflow=@bitlogger.QueueOverflowPolicy::DropOldest)), + sink=@bitlogger.SinkConfig::new( + kind=@bitlogger.SinkKind::File, + path="async-app-file-same-build.log", + ), + queue=Some( + @bitlogger.QueueConfig::new( + 3, + overflow=@bitlogger.QueueOverflowPolicy::DropOldest, + ), + ), ), async_config=AsyncLoggerConfig::new( max_pending=2, @@ -2821,24 +3330,55 @@ async test "application async builder preserves file-backed runtime helpers" { inspect(application.pending_count() == direct.pending_count(), content="true") inspect(application.dropped_count() == direct.dropped_count(), content="true") - inspect(application.sink.pending_count() == direct.sink.pending_count(), content="true") - inspect(application.sink.dropped_count() == direct.sink.dropped_count(), content="true") - inspect(application.sink.file_available() == direct.sink.file_available(), content="true") - inspect(application.sink.file_append_mode() == direct.sink.file_append_mode(), content="true") - inspect(application.sink.file_auto_flush() == direct.sink.file_auto_flush(), content="true") + inspect( + application.sink.pending_count() == direct.sink.pending_count(), + content="true", + ) + inspect( + application.sink.dropped_count() == direct.sink.dropped_count(), + content="true", + ) + inspect( + application.sink.file_available() == direct.sink.file_available(), + content="true", + ) + inspect( + application.sink.file_append_mode() == direct.sink.file_append_mode(), + content="true", + ) + inspect( + application.sink.file_auto_flush() == direct.sink.file_auto_flush(), + content="true", + ) let application_state = application.sink.file_state() let direct_state = direct.sink.file_state() inspect(application_state.path == direct_state.path, content="true") inspect(application_state.available == direct_state.available, content="true") inspect(application_state.append == direct_state.append, content="true") - inspect(application_state.auto_flush == direct_state.auto_flush, content="true") - inspect(application_state.open_failures == direct_state.open_failures, content="true") - inspect(application_state.write_failures == direct_state.write_failures, content="true") - inspect(application_state.flush_failures == direct_state.flush_failures, content="true") - inspect(application_state.rotation_failures == direct_state.rotation_failures, content="true") + inspect( + application_state.auto_flush == direct_state.auto_flush, + content="true", + ) + inspect( + application_state.open_failures == direct_state.open_failures, + content="true", + ) + inspect( + application_state.write_failures == direct_state.write_failures, + content="true", + ) + inspect( + application_state.flush_failures == direct_state.flush_failures, + content="true", + ) + inspect( + application_state.rotation_failures == direct_state.rotation_failures, + content="true", + ) - match (application.sink.file_runtime_state(), direct.sink.file_runtime_state()) { + match + (application.sink.file_runtime_state(), direct.sink.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") @@ -2849,30 +3389,76 @@ async test "application async builder preserves file-backed runtime helpers" { _ => inspect(false, content="true") } - inspect(application.sink.file_set_append_mode(false) == direct.sink.file_set_append_mode(false), content="true") - inspect(application.sink.file_append_mode() == direct.sink.file_append_mode(), content="true") - inspect(application.sink.file_set_auto_flush(false) == direct.sink.file_set_auto_flush(false), content="true") - inspect(application.sink.file_auto_flush() == direct.sink.file_auto_flush(), content="true") inspect( - application.sink.file_set_rotation(Some(@bitlogger.file_rotation(36, max_backups=2))) == - direct.sink.file_set_rotation(Some(@bitlogger.file_rotation(36, max_backups=2))), + application.sink.file_set_append_mode(false) == + direct.sink.file_set_append_mode(false), content="true", ) - inspect(application.sink.file_rotation_enabled() == direct.sink.file_rotation_enabled(), content="true") - inspect(application.sink.file_reopen_append() == direct.sink.file_reopen_append(), content="true") - inspect(application.sink.file_append_mode() == direct.sink.file_append_mode(), content="true") - inspect(application.sink.file_clear_rotation() == direct.sink.file_clear_rotation(), content="true") - inspect(application.sink.file_reset_policy() == direct.sink.file_reset_policy(), content="true") - inspect(application.sink.file_policy_matches_default() == direct.sink.file_policy_matches_default(), content="true") inspect( - application.sink.file_reset_failure_counters() == direct.sink.file_reset_failure_counters(), + application.sink.file_append_mode() == direct.sink.file_append_mode(), + content="true", + ) + inspect( + application.sink.file_set_auto_flush(false) == + direct.sink.file_set_auto_flush(false), + content="true", + ) + inspect( + application.sink.file_auto_flush() == direct.sink.file_auto_flush(), + content="true", + ) + inspect( + application.sink.file_set_rotation( + Some(@bitlogger.file_rotation(36, max_backups=2)), + ) == + direct.sink.file_set_rotation( + Some(@bitlogger.file_rotation(36, max_backups=2)), + ), + content="true", + ) + inspect( + application.sink.file_rotation_enabled() == + direct.sink.file_rotation_enabled(), + content="true", + ) + inspect( + application.sink.file_reopen_append() == direct.sink.file_reopen_append(), + content="true", + ) + inspect( + application.sink.file_append_mode() == direct.sink.file_append_mode(), + content="true", + ) + inspect( + application.sink.file_clear_rotation() == direct.sink.file_clear_rotation(), + content="true", + ) + inspect( + application.sink.file_reset_policy() == direct.sink.file_reset_policy(), + content="true", + ) + inspect( + application.sink.file_policy_matches_default() == + direct.sink.file_policy_matches_default(), + content="true", + ) + inspect( + application.sink.file_reset_failure_counters() == + direct.sink.file_reset_failure_counters(), + content="true", + ) + inspect( + application.sink.file_flush() == direct.sink.file_flush(), content="true", ) - inspect(application.sink.file_flush() == direct.sink.file_flush(), content="true") inspect(application.pending_count() == direct.pending_count(), content="true") - inspect(application.sink.file_close() == direct.sink.file_close(), content="true") + inspect( + application.sink.file_close() == direct.sink.file_close(), + content="true", + ) } +///| async test "application async logger keeps async helper surface" { let logger : ApplicationAsyncLogger = build_application_async_logger( AsyncLoggerBuildConfig::new( @@ -2895,11 +3481,14 @@ async test "application async logger keeps async helper surface" { inspect(state.is_closed, content="false") inspect(state.is_running, content="false") inspect(state.has_failed, content="false") - inspect(match state.flush_policy { - AsyncFlushPolicy::Never => "Never" - AsyncFlushPolicy::Batch => "Batch" - AsyncFlushPolicy::Shutdown => "Shutdown" - }, content="Shutdown") + inspect( + match state.flush_policy { + AsyncFlushPolicy::Never => "Never" + AsyncFlushPolicy::Batch => "Batch" + AsyncFlushPolicy::Shutdown => "Shutdown" + }, + content="Shutdown", + ) logger.info("skip") logger.error("one") @@ -2923,6 +3512,7 @@ async test "application async logger keeps async helper surface" { inspect(logger.last_error(), content="") } +///| async test "application async logger shutdown clear abandons pending records" { let logger : ApplicationAsyncLogger = build_application_async_logger( AsyncLoggerBuildConfig::new( @@ -2948,21 +3538,22 @@ async test "application async logger shutdown clear abandons pending records" { inspect(logger.dropped_count(), content="1") } +///| async test "application async logger keeps broader async composition surface" { let logger : ApplicationAsyncLogger = build_application_async_logger( - AsyncLoggerBuildConfig::new( - logger=@bitlogger.LoggerConfig::new( - min_level=@bitlogger.Level::Warn, - target="async.app.compose", - sink=@bitlogger.SinkConfig::new(kind=@bitlogger.SinkKind::Console), + AsyncLoggerBuildConfig::new( + logger=@bitlogger.LoggerConfig::new( + min_level=@bitlogger.Level::Warn, + target="async.app.compose", + sink=@bitlogger.SinkConfig::new(kind=@bitlogger.SinkKind::Console), + ), + async_config=AsyncLoggerConfig::new( + max_pending=2, + overflow=AsyncOverflowPolicy::DropNewest, + flush=AsyncFlushPolicy::Shutdown, + ), ), - async_config=AsyncLoggerConfig::new( - max_pending=2, - overflow=AsyncOverflowPolicy::DropNewest, - flush=AsyncFlushPolicy::Shutdown, - ), - ), - ) + ) .with_timestamp() .with_context_fields([@bitlogger.field("service", "bitlogger")]) .child("worker") @@ -2989,20 +3580,21 @@ async test "application async logger keeps broader async composition surface" { inspect(logger.last_error(), content="") } +///| async test "application async logger log supports per-call target override" { let written_target : Ref[String] = Ref("") let written_message : Ref[String] = Ref("") let written_fields : Ref[Array[@bitlogger.Field]] = Ref([]) let logger : ApplicationAsyncLogger = build_application_async_logger( - AsyncLoggerBuildConfig::new( - logger=@bitlogger.LoggerConfig::new( - min_level=@bitlogger.Level::Warn, - target="async.app.default", - sink=@bitlogger.SinkConfig::new(kind=@bitlogger.SinkKind::Console), + AsyncLoggerBuildConfig::new( + logger=@bitlogger.LoggerConfig::new( + min_level=@bitlogger.Level::Warn, + target="async.app.default", + sink=@bitlogger.SinkConfig::new(kind=@bitlogger.SinkKind::Console), + ), + async_config=AsyncLoggerConfig::new(max_pending=4), ), - async_config=AsyncLoggerConfig::new(max_pending=4), - ), - ) + ) .with_context_fields([@bitlogger.field("service", "bitlogger")]) .with_patch(fn(rec) { written_target.val = rec.target @@ -3034,15 +3626,15 @@ async test "application async logger log supports per-call target override" { let second_message : Ref[String] = Ref("") let second_fields : Ref[Array[@bitlogger.Field]] = Ref([]) let second : ApplicationAsyncLogger = build_application_async_logger( - AsyncLoggerBuildConfig::new( - logger=@bitlogger.LoggerConfig::new( - min_level=@bitlogger.Level::Warn, - target="async.app.default", - sink=@bitlogger.SinkConfig::new(kind=@bitlogger.SinkKind::Console), + AsyncLoggerBuildConfig::new( + logger=@bitlogger.LoggerConfig::new( + min_level=@bitlogger.Level::Warn, + target="async.app.default", + sink=@bitlogger.SinkConfig::new(kind=@bitlogger.SinkKind::Console), + ), + async_config=AsyncLoggerConfig::new(max_pending=4), ), - async_config=AsyncLoggerConfig::new(max_pending=4), - ), - ) + ) .with_context_fields([@bitlogger.field("service", "bitlogger")]) .with_patch(fn(rec) { second_target.val = rec.target @@ -3063,20 +3655,21 @@ async test "application async logger log supports per-call target override" { inspect(second_fields.val[0].key, content="service") } +///| async test "application async logger severity helpers use stored target without override" { let written_levels : Ref[Array[String]] = Ref([]) let written_targets : Ref[Array[String]] = Ref([]) let written_fields : Ref[Array[Array[@bitlogger.Field]]] = Ref([]) let logger : ApplicationAsyncLogger = build_application_async_logger( - AsyncLoggerBuildConfig::new( - logger=@bitlogger.LoggerConfig::new( - min_level=@bitlogger.Level::Info, - target="async.app.helpers", - sink=@bitlogger.SinkConfig::new(kind=@bitlogger.SinkKind::Console), + AsyncLoggerBuildConfig::new( + logger=@bitlogger.LoggerConfig::new( + min_level=@bitlogger.Level::Info, + target="async.app.helpers", + sink=@bitlogger.SinkConfig::new(kind=@bitlogger.SinkKind::Console), + ), + async_config=AsyncLoggerConfig::new(max_pending=4), ), - async_config=AsyncLoggerConfig::new(max_pending=4), - ), - ) + ) .with_context_fields([@bitlogger.field("service", "bitlogger")]) .with_patch(fn(rec) { written_levels.val.push(rec.level.label()) @@ -3112,13 +3705,19 @@ async test "application async logger severity helpers use stored target without inspect(written_fields.val[2][1].value, content="error") } +///| test "application async logger builder preserves sync queue-backed runtime sink" { let logger = build_application_async_logger( AsyncLoggerBuildConfig::new( logger=@bitlogger.LoggerConfig::new( min_level=@bitlogger.Level::Warn, target="async.app.queued", - queue=Some(@bitlogger.QueueConfig::new(3, overflow=@bitlogger.QueueOverflowPolicy::DropNewest)), + queue=Some( + @bitlogger.QueueConfig::new( + 3, + overflow=@bitlogger.QueueOverflowPolicy::DropNewest, + ), + ), ), async_config=AsyncLoggerConfig::new(max_pending=2), ), @@ -3129,22 +3728,29 @@ test "application async logger builder preserves sync queue-backed runtime sink" inspect(logger.target, content="async.app.queued") inspect(logger.sink.pending_count(), content="0") inspect(logger.sink.dropped_count(), content="0") - inspect(match logger.sink { - @bitlogger.RuntimeSink::QueuedConsole(_) => "QueuedConsole" - @bitlogger.RuntimeSink::QueuedJsonConsole(_) => "QueuedJsonConsole" - @bitlogger.RuntimeSink::QueuedTextConsole(_) => "QueuedTextConsole" - @bitlogger.RuntimeSink::QueuedFile(_) => "QueuedFile" - _ => "Other" - }, content="QueuedConsole") + inspect( + match logger.sink { + @bitlogger.RuntimeSink::QueuedConsole(_) => "QueuedConsole" + @bitlogger.RuntimeSink::QueuedJsonConsole(_) => "QueuedJsonConsole" + @bitlogger.RuntimeSink::QueuedTextConsole(_) => "QueuedTextConsole" + @bitlogger.RuntimeSink::QueuedFile(_) => "QueuedFile" + _ => "Other" + }, + content="QueuedConsole", + ) } +///| test "application text async logger uses text facade build path" { let logger = build_application_text_async_logger( AsyncLoggerBuildConfig::new( logger=@bitlogger.text_console( min_level=@bitlogger.Level::Warn, target="async.app.text", - text_formatter=@bitlogger.TextFormatterConfig::new(show_timestamp=false, separator=" | "), + text_formatter=@bitlogger.TextFormatterConfig::new( + show_timestamp=false, + separator=" | ", + ), ), async_config=AsyncLoggerConfig::new(max_pending=2), ), @@ -3154,13 +3760,19 @@ test "application text async logger uses text facade build path" { inspect(logger.target, content="async.app.text") } +///| async test "application text builder matches direct text builder behavior" { let config = AsyncLoggerBuildConfig::new( logger=@bitlogger.LoggerConfig::new( min_level=@bitlogger.Level::Warn, target="async.app.text.same-build", timestamp=true, - queue=Some(@bitlogger.QueueConfig::new(3, overflow=@bitlogger.QueueOverflowPolicy::DropNewest)), + queue=Some( + @bitlogger.QueueConfig::new( + 3, + overflow=@bitlogger.QueueOverflowPolicy::DropNewest, + ), + ), sink=@bitlogger.SinkConfig::new( kind=@bitlogger.SinkKind::TextConsole, text_formatter=@bitlogger.TextFormatterConfig::new( @@ -3183,14 +3795,30 @@ async test "application text builder matches direct text builder behavior" { inspect(logger.target, content=direct.target) inspect(logger.timestamp == direct.timestamp, content="true") - inspect(logger.is_enabled(@bitlogger.Level::Error) == direct.is_enabled(@bitlogger.Level::Error), content="true") - inspect(logger.is_enabled(@bitlogger.Level::Info) == direct.is_enabled(@bitlogger.Level::Info), content="true") inspect( - stringify_async_logger_state(logger_state) == stringify_async_logger_state(direct_state), + logger.is_enabled(@bitlogger.Level::Error) == + direct.is_enabled(@bitlogger.Level::Error), + content="true", + ) + inspect( + logger.is_enabled(@bitlogger.Level::Info) == + direct.is_enabled(@bitlogger.Level::Info), + content="true", + ) + inspect( + stringify_async_logger_state(logger_state) == + stringify_async_logger_state(direct_state), + content="true", + ) + let record = @bitlogger.Record::new( + @bitlogger.Level::Error, + "boom", + target="async.app.text.same-build", + ) + inspect( + (logger.sink.formatter)(record) == (direct.sink.formatter)(record), content="true", ) - let record = @bitlogger.Record::new(@bitlogger.Level::Error, "boom", target="async.app.text.same-build") - inspect((logger.sink.formatter)(record) == (direct.sink.formatter)(record), content="true") logger.info("skip") logger.error("one") @@ -3221,13 +3849,17 @@ async test "application text builder matches direct text builder behavior" { inspect(logger.last_error() == direct.last_error(), content="true") } +///| async test "application text async logger keeps async helper surface" { let logger : ApplicationTextAsyncLogger = build_application_text_async_logger( AsyncLoggerBuildConfig::new( logger=@bitlogger.text_console( min_level=@bitlogger.Level::Warn, target="async.app.text.alias", - text_formatter=@bitlogger.TextFormatterConfig::new(show_timestamp=false, separator=" | "), + text_formatter=@bitlogger.TextFormatterConfig::new( + show_timestamp=false, + separator=" | ", + ), ), async_config=AsyncLoggerConfig::new( max_pending=2, @@ -3243,11 +3875,14 @@ async test "application text async logger keeps async helper surface" { inspect(state.is_closed, content="false") inspect(state.is_running, content="false") inspect(state.has_failed, content="false") - inspect(match state.flush_policy { - AsyncFlushPolicy::Never => "Never" - AsyncFlushPolicy::Batch => "Batch" - AsyncFlushPolicy::Shutdown => "Shutdown" - }, content="Shutdown") + inspect( + match state.flush_policy { + AsyncFlushPolicy::Never => "Never" + AsyncFlushPolicy::Batch => "Batch" + AsyncFlushPolicy::Shutdown => "Shutdown" + }, + content="Shutdown", + ) logger.info("skip") logger.error("one") @@ -3271,13 +3906,17 @@ async test "application text async logger keeps async helper surface" { inspect(logger.last_error(), content="") } +///| async test "application text async logger shutdown clear abandons pending records" { let logger : ApplicationTextAsyncLogger = build_application_text_async_logger( AsyncLoggerBuildConfig::new( logger=@bitlogger.text_console( min_level=@bitlogger.Level::Info, target="async.app.text.clear", - text_formatter=@bitlogger.TextFormatterConfig::new(show_timestamp=false, separator=" | "), + text_formatter=@bitlogger.TextFormatterConfig::new( + show_timestamp=false, + separator=" | ", + ), ), async_config=AsyncLoggerConfig::new( max_pending=2, @@ -3296,21 +3935,25 @@ async test "application text async logger shutdown clear abandons pending record inspect(logger.dropped_count(), content="1") } +///| async test "application text async logger keeps broader async composition surface" { let logger : ApplicationTextAsyncLogger = build_application_text_async_logger( - AsyncLoggerBuildConfig::new( - logger=@bitlogger.text_console( - min_level=@bitlogger.Level::Warn, - target="async.app.text.compose", - text_formatter=@bitlogger.TextFormatterConfig::new(show_timestamp=false, separator=" | "), + AsyncLoggerBuildConfig::new( + logger=@bitlogger.text_console( + min_level=@bitlogger.Level::Warn, + target="async.app.text.compose", + text_formatter=@bitlogger.TextFormatterConfig::new( + show_timestamp=false, + separator=" | ", + ), + ), + async_config=AsyncLoggerConfig::new( + max_pending=2, + overflow=AsyncOverflowPolicy::DropNewest, + flush=AsyncFlushPolicy::Shutdown, + ), ), - async_config=AsyncLoggerConfig::new( - max_pending=2, - overflow=AsyncOverflowPolicy::DropNewest, - flush=AsyncFlushPolicy::Shutdown, - ), - ), - ) + ) .with_timestamp() .with_context_fields([@bitlogger.field("service", "bitlogger")]) .child("worker") @@ -3337,27 +3980,31 @@ async test "application text async logger keeps broader async composition surfac inspect(logger.last_error(), content="") } +///| async test "application text async logger log supports per-call target override" { let written_target : Ref[String] = Ref("") let written_message : Ref[String] = Ref("") let written_fields : Ref[Array[@bitlogger.Field]] = Ref([]) let logger : ApplicationTextAsyncLogger = build_application_text_async_logger( - AsyncLoggerBuildConfig::new( - logger=@bitlogger.text_console( - min_level=@bitlogger.Level::Warn, - target="async.app.text.default", - text_formatter=@bitlogger.TextFormatterConfig::new(show_timestamp=false, separator=" | "), + AsyncLoggerBuildConfig::new( + logger=@bitlogger.text_console( + min_level=@bitlogger.Level::Warn, + target="async.app.text.default", + text_formatter=@bitlogger.TextFormatterConfig::new( + show_timestamp=false, + separator=" | ", + ), + ), + async_config=AsyncLoggerConfig::new(max_pending=4), ), - async_config=AsyncLoggerConfig::new(max_pending=4), - ), - ) + ) .with_context_fields([@bitlogger.field("service", "bitlogger")]) .with_patch(fn(rec) { - written_target.val = rec.target - written_message.val = rec.message - written_fields.val = rec.fields - rec - }) + written_target.val = rec.target + written_message.val = rec.message + written_fields.val = rec.fields + rec + }) @async.with_task_group(group => { group.spawn_bg(() => logger.run()) @@ -3382,22 +4029,25 @@ async test "application text async logger log supports per-call target override" let second_message : Ref[String] = Ref("") let second_fields : Ref[Array[@bitlogger.Field]] = Ref([]) let second : ApplicationTextAsyncLogger = build_application_text_async_logger( - AsyncLoggerBuildConfig::new( - logger=@bitlogger.text_console( - min_level=@bitlogger.Level::Warn, - target="async.app.text.default", - text_formatter=@bitlogger.TextFormatterConfig::new(show_timestamp=false, separator=" | "), + AsyncLoggerBuildConfig::new( + logger=@bitlogger.text_console( + min_level=@bitlogger.Level::Warn, + target="async.app.text.default", + text_formatter=@bitlogger.TextFormatterConfig::new( + show_timestamp=false, + separator=" | ", + ), + ), + async_config=AsyncLoggerConfig::new(max_pending=4), ), - async_config=AsyncLoggerConfig::new(max_pending=4), - ), - ) + ) .with_context_fields([@bitlogger.field("service", "bitlogger")]) .with_patch(fn(rec) { - second_target.val = rec.target - second_message.val = rec.message - second_fields.val = rec.fields - rec - }) + second_target.val = rec.target + second_message.val = rec.message + second_fields.val = rec.fields + rec + }) @async.with_task_group(group => { group.spawn_bg(() => second.run()) @@ -3411,27 +4061,31 @@ async test "application text async logger log supports per-call target override" inspect(second_fields.val[0].key, content="service") } +///| async test "application text async logger severity helpers use stored target without override" { let written_levels : Ref[Array[String]] = Ref([]) let written_targets : Ref[Array[String]] = Ref([]) let written_fields : Ref[Array[Array[@bitlogger.Field]]] = Ref([]) let logger : ApplicationTextAsyncLogger = build_application_text_async_logger( - AsyncLoggerBuildConfig::new( - logger=@bitlogger.text_console( - min_level=@bitlogger.Level::Info, - target="async.app.text.helpers", - text_formatter=@bitlogger.TextFormatterConfig::new(show_timestamp=false, separator=" | "), + AsyncLoggerBuildConfig::new( + logger=@bitlogger.text_console( + min_level=@bitlogger.Level::Info, + target="async.app.text.helpers", + text_formatter=@bitlogger.TextFormatterConfig::new( + show_timestamp=false, + separator=" | ", + ), + ), + async_config=AsyncLoggerConfig::new(max_pending=4), ), - async_config=AsyncLoggerConfig::new(max_pending=4), - ), - ) + ) .with_context_fields([@bitlogger.field("service", "bitlogger")]) .with_patch(fn(rec) { - written_levels.val.push(rec.level.label()) - written_targets.val.push(rec.target) - written_fields.val.push(rec.fields) - rec - }) + written_levels.val.push(rec.level.label()) + written_targets.val.push(rec.target) + written_fields.val.push(rec.fields) + rec + }) @async.with_task_group(group => { group.spawn_bg(() => logger.run()) @@ -3460,16 +4114,25 @@ async test "application text async logger severity helpers use stored target wit inspect(written_fields.val[2][1].value, content="error") } +///| test "application text async logger builder ignores sync queue layer" { let logger = build_application_text_async_logger( AsyncLoggerBuildConfig::new( logger=@bitlogger.LoggerConfig::new( min_level=@bitlogger.Level::Warn, target="async.app.text.queued", - queue=Some(@bitlogger.QueueConfig::new(3, overflow=@bitlogger.QueueOverflowPolicy::DropNewest)), + queue=Some( + @bitlogger.QueueConfig::new( + 3, + overflow=@bitlogger.QueueOverflowPolicy::DropNewest, + ), + ), sink=@bitlogger.SinkConfig::new( kind=@bitlogger.SinkKind::TextConsole, - text_formatter=@bitlogger.TextFormatterConfig::new(show_timestamp=false, separator=" | "), + text_formatter=@bitlogger.TextFormatterConfig::new( + show_timestamp=false, + separator=" | ", + ), ), ), async_config=AsyncLoggerConfig::new(max_pending=2), @@ -3483,6 +4146,7 @@ test "application text async logger builder ignores sync queue layer" { inspect(logger.dropped_count(), content="0") } +///| test "application text async logger ignores sink kind and still uses text formatter" { let logger : ApplicationTextAsyncLogger = build_application_text_async_logger( AsyncLoggerBuildConfig::new( @@ -3507,11 +4171,16 @@ test "application text async logger ignores sink kind and still uses text format inspect(logger.is_enabled(@bitlogger.Level::Info), content="false") inspect(logger.target, content="async.app.text.kind.alias") let rendered = (logger.sink.formatter)( - @bitlogger.Record::new(@bitlogger.Level::Error, "boom", target="async.app.text.kind.alias"), + @bitlogger.Record::new( + @bitlogger.Level::Error, + "boom", + target="async.app.text.kind.alias", + ), ) inspect(rendered, content="TEXT:async.app.text.kind.alias:boom") } +///| test "build async text logger keeps text-console config fields" { let logger = build_async_text_logger( AsyncLoggerBuildConfig::new( @@ -3521,27 +4190,41 @@ test "build async text logger keeps text-console config fields" { timestamp=true, sink=@bitlogger.SinkConfig::new( kind=@bitlogger.SinkKind::TextConsole, - text_formatter=@bitlogger.TextFormatterConfig::new(show_timestamp=false, separator=" | "), + text_formatter=@bitlogger.TextFormatterConfig::new( + show_timestamp=false, + separator=" | ", + ), ), ), - async_config=AsyncLoggerConfig::new(max_pending=3, overflow=AsyncOverflowPolicy::DropOldest), + async_config=AsyncLoggerConfig::new( + max_pending=3, + overflow=AsyncOverflowPolicy::DropOldest, + ), ), ) inspect(logger.is_enabled(@bitlogger.Level::Error), content="true") inspect(logger.is_enabled(@bitlogger.Level::Info), content="false") inspect(logger.target, content="async.text.direct") inspect(logger.timestamp, content="true") - inspect(match logger.flush_policy() { - AsyncFlushPolicy::Never => "Never" - AsyncFlushPolicy::Batch => "Batch" - AsyncFlushPolicy::Shutdown => "Shutdown" - }, content="Never") + inspect( + match logger.flush_policy() { + AsyncFlushPolicy::Never => "Never" + AsyncFlushPolicy::Batch => "Batch" + AsyncFlushPolicy::Shutdown => "Shutdown" + }, + content="Never", + ) let rendered = (logger.sink.formatter)( - @bitlogger.Record::new(@bitlogger.Level::Error, "boom", target="async.text.direct"), + @bitlogger.Record::new( + @bitlogger.Level::Error, + "boom", + target="async.text.direct", + ), ) inspect(rendered, content="[ERROR] | [async.text.direct] | boom") } +///| test "build async text logger ignores sink kind and still uses text formatter" { let logger = build_async_text_logger( AsyncLoggerBuildConfig::new( @@ -3566,11 +4249,16 @@ test "build async text logger ignores sink kind and still uses text formatter" { inspect(logger.is_enabled(@bitlogger.Level::Info), content="false") inspect(logger.target, content="async.text.kind") let rendered = (logger.sink.formatter)( - @bitlogger.Record::new(@bitlogger.Level::Error, "boom", target="async.text.kind"), + @bitlogger.Record::new( + @bitlogger.Level::Error, + "boom", + target="async.text.kind", + ), ) inspect(rendered, content="TEXT:async.text.kind:boom") } +///| async test "build async text logger keeps direct helper surface" { let logger = build_async_text_logger( AsyncLoggerBuildConfig::new( @@ -3580,7 +4268,10 @@ async test "build async text logger keeps direct helper surface" { timestamp=true, sink=@bitlogger.SinkConfig::new( kind=@bitlogger.SinkKind::TextConsole, - text_formatter=@bitlogger.TextFormatterConfig::new(show_timestamp=false, separator=" | "), + text_formatter=@bitlogger.TextFormatterConfig::new( + show_timestamp=false, + separator=" | ", + ), ), ), async_config=AsyncLoggerConfig::new( @@ -3601,11 +4292,14 @@ async test "build async text logger keeps direct helper surface" { inspect(state.is_closed, content="false") inspect(state.is_running, content="false") inspect(state.has_failed, content="false") - inspect(match state.flush_policy { - AsyncFlushPolicy::Never => "Never" - AsyncFlushPolicy::Batch => "Batch" - AsyncFlushPolicy::Shutdown => "Shutdown" - }, content="Shutdown") + inspect( + match state.flush_policy { + AsyncFlushPolicy::Never => "Never" + AsyncFlushPolicy::Batch => "Batch" + AsyncFlushPolicy::Shutdown => "Shutdown" + }, + content="Shutdown", + ) logger.info("skip") logger.error("one") @@ -3629,6 +4323,7 @@ async test "build async text logger keeps direct helper surface" { inspect(logger.last_error(), content="") } +///| async test "build async text logger log supports per-call target override" { let written_target : Ref[String] = Ref("") @@ -3639,7 +4334,10 @@ async test "build async text logger log supports per-call target override" { target="async.text.builder.default", sink=@bitlogger.SinkConfig::new( kind=@bitlogger.SinkKind::TextConsole, - text_formatter=@bitlogger.TextFormatterConfig::new(show_timestamp=false, separator=" | "), + text_formatter=@bitlogger.TextFormatterConfig::new( + show_timestamp=false, + separator=" | ", + ), ), ), async_config=AsyncLoggerConfig::new( @@ -3648,11 +4346,10 @@ async test "build async text logger log supports per-call target override" { flush=AsyncFlushPolicy::Never, ), ), - ) - .with_patch(fn(rec) { - written_target.val = rec.target - rec - }) + ).with_patch(fn(rec) { + written_target.val = rec.target + rec + }) @async.with_task_group(group => { group.spawn_bg(() => logger.run()) @@ -3675,7 +4372,10 @@ async test "build async text logger log supports per-call target override" { target="async.text.builder.default", sink=@bitlogger.SinkConfig::new( kind=@bitlogger.SinkKind::TextConsole, - text_formatter=@bitlogger.TextFormatterConfig::new(show_timestamp=false, separator=" | "), + text_formatter=@bitlogger.TextFormatterConfig::new( + show_timestamp=false, + separator=" | ", + ), ), ), async_config=AsyncLoggerConfig::new( @@ -3684,11 +4384,10 @@ async test "build async text logger log supports per-call target override" { flush=AsyncFlushPolicy::Never, ), ), - ) - .with_patch(fn(rec) { - second_target.val = rec.target - rec - }) + ).with_patch(fn(rec) { + second_target.val = rec.target + rec + }) @async.with_task_group(group => { group.spawn_bg(() => second.run()) @@ -3699,6 +4398,7 @@ async test "build async text logger log supports per-call target override" { inspect(second_target.val, content="async.text.builder.default") } +///| async test "build async text logger severity helpers use stored target without override" { let written_targets : Ref[Array[String]] = Ref([]) @@ -3709,7 +4409,10 @@ async test "build async text logger severity helpers use stored target without o target="async.text.builder.helpers", sink=@bitlogger.SinkConfig::new( kind=@bitlogger.SinkKind::TextConsole, - text_formatter=@bitlogger.TextFormatterConfig::new(show_timestamp=false, separator=" | "), + text_formatter=@bitlogger.TextFormatterConfig::new( + show_timestamp=false, + separator=" | ", + ), ), ), async_config=AsyncLoggerConfig::new( @@ -3718,11 +4421,10 @@ async test "build async text logger severity helpers use stored target without o flush=AsyncFlushPolicy::Never, ), ), - ) - .with_patch(fn(rec) { - written_targets.val.push(rec.target) - rec - }) + ).with_patch(fn(rec) { + written_targets.val.push(rec.target) + rec + }) @async.with_task_group(group => { group.spawn_bg(() => logger.run()) @@ -3738,6 +4440,7 @@ async test "build async text logger severity helpers use stored target without o inspect(written_targets.val[2], content="async.text.builder.helpers") } +///| async test "build async text logger batch flush policy keeps default no-op flush callback" { let config = AsyncLoggerBuildConfig::new( logger=@bitlogger.LoggerConfig::new( @@ -3762,7 +4465,9 @@ async test "build async text logger batch flush policy keeps default no-op flush ) let configured = build_async_text_logger(config) let manual = async_logger( - @bitlogger.text_console_sink(config.logger.sink.text_formatter.to_formatter()), + @bitlogger.text_console_sink( + config.logger.sink.text_formatter.to_formatter(), + ), config=config.async_config, min_level=config.logger.min_level, target=config.logger.target, @@ -3783,16 +4488,22 @@ async test "build async text logger batch flush policy keeps default no-op flush configured.wait_idle() manual.wait_idle() - inspect(match configured.flush_policy() { - AsyncFlushPolicy::Never => "Never" - AsyncFlushPolicy::Batch => "Batch" - AsyncFlushPolicy::Shutdown => "Shutdown" - }, content="Batch") - inspect(match manual.flush_policy() { - AsyncFlushPolicy::Never => "Never" - AsyncFlushPolicy::Batch => "Batch" - AsyncFlushPolicy::Shutdown => "Shutdown" - }, content="Batch") + inspect( + match configured.flush_policy() { + AsyncFlushPolicy::Never => "Never" + AsyncFlushPolicy::Batch => "Batch" + AsyncFlushPolicy::Shutdown => "Shutdown" + }, + content="Batch", + ) + inspect( + match manual.flush_policy() { + AsyncFlushPolicy::Never => "Never" + AsyncFlushPolicy::Batch => "Batch" + AsyncFlushPolicy::Shutdown => "Shutdown" + }, + content="Batch", + ) inspect(configured.has_failed(), content="false") inspect(configured.last_error(), content="") inspect(configured.pending_count(), content="0") @@ -3814,6 +4525,7 @@ async test "build async text logger batch flush policy keeps default no-op flush inspect(manual.last_error().contains("TestFlushError"), content="true") } +///| async test "build async text logger shutdown flush policy keeps default no-op flush callback" { let config = AsyncLoggerBuildConfig::new( logger=@bitlogger.LoggerConfig::new( @@ -3838,7 +4550,9 @@ async test "build async text logger shutdown flush policy keeps default no-op fl ) let configured = build_async_text_logger(config) let manual = async_logger( - @bitlogger.text_console_sink(config.logger.sink.text_formatter.to_formatter()), + @bitlogger.text_console_sink( + config.logger.sink.text_formatter.to_formatter(), + ), config=config.async_config, min_level=config.logger.min_level, target=config.logger.target, @@ -3860,16 +4574,22 @@ async test "build async text logger shutdown flush policy keeps default no-op fl manual.shutdown() }) - inspect(match configured.flush_policy() { - AsyncFlushPolicy::Never => "Never" - AsyncFlushPolicy::Batch => "Batch" - AsyncFlushPolicy::Shutdown => "Shutdown" - }, content="Shutdown") - inspect(match manual.flush_policy() { - AsyncFlushPolicy::Never => "Never" - AsyncFlushPolicy::Batch => "Batch" - AsyncFlushPolicy::Shutdown => "Shutdown" - }, content="Shutdown") + inspect( + match configured.flush_policy() { + AsyncFlushPolicy::Never => "Never" + AsyncFlushPolicy::Batch => "Batch" + AsyncFlushPolicy::Shutdown => "Shutdown" + }, + content="Shutdown", + ) + inspect( + match manual.flush_policy() { + AsyncFlushPolicy::Never => "Never" + AsyncFlushPolicy::Batch => "Batch" + AsyncFlushPolicy::Shutdown => "Shutdown" + }, + content="Shutdown", + ) inspect(configured.is_closed(), content="true") inspect(configured.is_running(), content="false") inspect(configured.has_failed(), content="false") @@ -3882,16 +4602,25 @@ async test "build async text logger shutdown flush policy keeps default no-op fl inspect(manual.pending_count(), content="0") } +///| test "build async text logger ignores sync queue layer" { let logger = build_async_text_logger( AsyncLoggerBuildConfig::new( logger=@bitlogger.LoggerConfig::new( min_level=@bitlogger.Level::Warn, target="async.text.queued", - queue=Some(@bitlogger.QueueConfig::new(3, overflow=@bitlogger.QueueOverflowPolicy::DropNewest)), + queue=Some( + @bitlogger.QueueConfig::new( + 3, + overflow=@bitlogger.QueueOverflowPolicy::DropNewest, + ), + ), sink=@bitlogger.SinkConfig::new( kind=@bitlogger.SinkKind::TextConsole, - text_formatter=@bitlogger.TextFormatterConfig::new(show_timestamp=false, separator=" | "), + text_formatter=@bitlogger.TextFormatterConfig::new( + show_timestamp=false, + separator=" | ", + ), ), ), async_config=AsyncLoggerConfig::new(max_pending=2), @@ -3905,6 +4634,7 @@ test "build async text logger ignores sync queue layer" { inspect(logger.dropped_count(), content="0") } +///| test "application async logger can be built from config text" { let logger = parse_and_build_application_async_logger( "{\"logger\":{\"min_level\":\"warn\",\"target\":\"async.app.json\",\"sink\":{\"kind\":\"console\"}},\"async_config\":{\"max_pending\":2,\"overflow\":\"DropNewest\",\"max_batch\":1,\"linger_ms\":0,\"flush\":\"Never\"}}", @@ -3914,6 +4644,7 @@ test "application async logger can be built from config text" { inspect(logger.target, content="async.app.json") } +///| async test "application async parse-build matches parsed direct async builder behavior" { let raw = "{\"logger\":{\"min_level\":\"warn\",\"target\":\"async.app.json.same-build\",\"timestamp\":true,\"queue\":{\"max_pending\":3,\"overflow\":\"DropNewest\"},\"sink\":{\"kind\":\"console\"}},\"async_config\":{\"max_pending\":2,\"overflow\":\"DropOldest\",\"max_batch\":1,\"linger_ms\":0,\"flush\":\"Shutdown\"}}" let application = parse_and_build_application_async_logger(raw) @@ -3924,15 +4655,18 @@ async test "application async parse-build matches parsed direct async builder be inspect(application.target, content=direct.target) inspect(application.timestamp == direct.timestamp, content="true") inspect( - application.is_enabled(@bitlogger.Level::Error) == direct.is_enabled(@bitlogger.Level::Error), + application.is_enabled(@bitlogger.Level::Error) == + direct.is_enabled(@bitlogger.Level::Error), content="true", ) inspect( - application.is_enabled(@bitlogger.Level::Info) == direct.is_enabled(@bitlogger.Level::Info), + application.is_enabled(@bitlogger.Level::Info) == + direct.is_enabled(@bitlogger.Level::Info), content="true", ) inspect( - stringify_async_logger_state(application_state) == stringify_async_logger_state(direct_state), + stringify_async_logger_state(application_state) == + stringify_async_logger_state(direct_state), content="true", ) let application_sink_kind = match application.sink { @@ -3970,8 +4704,14 @@ async test "application async parse-build matches parsed direct async builder be inspect(application.dropped_count() == direct.dropped_count(), content="true") inspect(application.is_closed() == direct.is_closed(), content="true") inspect(application.is_running() == direct.is_running(), content="true") - inspect(application.sink.pending_count() == direct.sink.pending_count(), content="true") - inspect(application.sink.dropped_count() == direct.sink.dropped_count(), content="true") + inspect( + application.sink.pending_count() == direct.sink.pending_count(), + content="true", + ) + inspect( + application.sink.dropped_count() == direct.sink.dropped_count(), + content="true", + ) @async.with_task_group(group => { group.spawn_bg(() => application.run()) @@ -3988,6 +4728,7 @@ async test "application async parse-build matches parsed direct async builder be inspect(application.last_error() == direct.last_error(), content="true") } +///| async test "application async parse-build preserves file-backed runtime helpers" { let raw = "{\"logger\":{\"min_level\":\"warn\",\"target\":\"async.app.json.file.same-build\",\"queue\":{\"max_pending\":3,\"overflow\":\"DropOldest\"},\"sink\":{\"kind\":\"file\",\"path\":\"async-app-json-file-same-build.log\"}},\"async_config\":{\"max_pending\":2,\"overflow\":\"DropOldest\",\"max_batch\":1,\"linger_ms\":0,\"flush\":\"Shutdown\"}}" let application = parse_and_build_application_async_logger(raw) @@ -4004,24 +4745,55 @@ async test "application async parse-build preserves file-backed runtime helpers" inspect(application.pending_count() == direct.pending_count(), content="true") inspect(application.dropped_count() == direct.dropped_count(), content="true") - inspect(application.sink.pending_count() == direct.sink.pending_count(), content="true") - inspect(application.sink.dropped_count() == direct.sink.dropped_count(), content="true") - inspect(application.sink.file_available() == direct.sink.file_available(), content="true") - inspect(application.sink.file_append_mode() == direct.sink.file_append_mode(), content="true") - inspect(application.sink.file_auto_flush() == direct.sink.file_auto_flush(), content="true") + inspect( + application.sink.pending_count() == direct.sink.pending_count(), + content="true", + ) + inspect( + application.sink.dropped_count() == direct.sink.dropped_count(), + content="true", + ) + inspect( + application.sink.file_available() == direct.sink.file_available(), + content="true", + ) + inspect( + application.sink.file_append_mode() == direct.sink.file_append_mode(), + content="true", + ) + inspect( + application.sink.file_auto_flush() == direct.sink.file_auto_flush(), + content="true", + ) let application_state = application.sink.file_state() let direct_state = direct.sink.file_state() inspect(application_state.path == direct_state.path, content="true") inspect(application_state.available == direct_state.available, content="true") inspect(application_state.append == direct_state.append, content="true") - inspect(application_state.auto_flush == direct_state.auto_flush, content="true") - inspect(application_state.open_failures == direct_state.open_failures, content="true") - inspect(application_state.write_failures == direct_state.write_failures, content="true") - inspect(application_state.flush_failures == direct_state.flush_failures, content="true") - inspect(application_state.rotation_failures == direct_state.rotation_failures, content="true") + inspect( + application_state.auto_flush == direct_state.auto_flush, + content="true", + ) + inspect( + application_state.open_failures == direct_state.open_failures, + content="true", + ) + inspect( + application_state.write_failures == direct_state.write_failures, + content="true", + ) + inspect( + application_state.flush_failures == direct_state.flush_failures, + content="true", + ) + inspect( + application_state.rotation_failures == direct_state.rotation_failures, + content="true", + ) - match (application.sink.file_runtime_state(), direct.sink.file_runtime_state()) { + match + (application.sink.file_runtime_state(), direct.sink.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") @@ -4032,30 +4804,76 @@ async test "application async parse-build preserves file-backed runtime helpers" _ => inspect(false, content="true") } - inspect(application.sink.file_set_append_mode(false) == direct.sink.file_set_append_mode(false), content="true") - inspect(application.sink.file_append_mode() == direct.sink.file_append_mode(), content="true") - inspect(application.sink.file_set_auto_flush(false) == direct.sink.file_set_auto_flush(false), content="true") - inspect(application.sink.file_auto_flush() == direct.sink.file_auto_flush(), content="true") inspect( - application.sink.file_set_rotation(Some(@bitlogger.file_rotation(36, max_backups=2))) == - direct.sink.file_set_rotation(Some(@bitlogger.file_rotation(36, max_backups=2))), + application.sink.file_set_append_mode(false) == + direct.sink.file_set_append_mode(false), content="true", ) - inspect(application.sink.file_rotation_enabled() == direct.sink.file_rotation_enabled(), content="true") - inspect(application.sink.file_reopen_append() == direct.sink.file_reopen_append(), content="true") - inspect(application.sink.file_append_mode() == direct.sink.file_append_mode(), content="true") - inspect(application.sink.file_clear_rotation() == direct.sink.file_clear_rotation(), content="true") - inspect(application.sink.file_reset_policy() == direct.sink.file_reset_policy(), content="true") - inspect(application.sink.file_policy_matches_default() == direct.sink.file_policy_matches_default(), content="true") inspect( - application.sink.file_reset_failure_counters() == direct.sink.file_reset_failure_counters(), + application.sink.file_append_mode() == direct.sink.file_append_mode(), + content="true", + ) + inspect( + application.sink.file_set_auto_flush(false) == + direct.sink.file_set_auto_flush(false), + content="true", + ) + inspect( + application.sink.file_auto_flush() == direct.sink.file_auto_flush(), + content="true", + ) + inspect( + application.sink.file_set_rotation( + Some(@bitlogger.file_rotation(36, max_backups=2)), + ) == + direct.sink.file_set_rotation( + Some(@bitlogger.file_rotation(36, max_backups=2)), + ), + content="true", + ) + inspect( + application.sink.file_rotation_enabled() == + direct.sink.file_rotation_enabled(), + content="true", + ) + inspect( + application.sink.file_reopen_append() == direct.sink.file_reopen_append(), + content="true", + ) + inspect( + application.sink.file_append_mode() == direct.sink.file_append_mode(), + content="true", + ) + inspect( + application.sink.file_clear_rotation() == direct.sink.file_clear_rotation(), + content="true", + ) + inspect( + application.sink.file_reset_policy() == direct.sink.file_reset_policy(), + content="true", + ) + inspect( + application.sink.file_policy_matches_default() == + direct.sink.file_policy_matches_default(), + content="true", + ) + inspect( + application.sink.file_reset_failure_counters() == + direct.sink.file_reset_failure_counters(), + content="true", + ) + inspect( + application.sink.file_flush() == direct.sink.file_flush(), content="true", ) - inspect(application.sink.file_flush() == direct.sink.file_flush(), content="true") inspect(application.pending_count() == direct.pending_count(), content="true") - inspect(application.sink.file_close() == direct.sink.file_close(), content="true") + inspect( + application.sink.file_close() == direct.sink.file_close(), + content="true", + ) } +///| async test "parsed application async logger keeps async helper surface" { let logger : ApplicationAsyncLogger = parse_and_build_application_async_logger( "{\"logger\":{\"min_level\":\"warn\",\"target\":\"async.app.json.helpers\",\"sink\":{\"kind\":\"console\"}},\"async_config\":{\"max_pending\":2,\"overflow\":\"DropOldest\",\"max_batch\":1,\"linger_ms\":0,\"flush\":\"Shutdown\"}}", @@ -4067,11 +4885,14 @@ async test "parsed application async logger keeps async helper surface" { inspect(state.is_closed, content="false") inspect(state.is_running, content="false") inspect(state.has_failed, content="false") - inspect(match state.flush_policy { - AsyncFlushPolicy::Never => "Never" - AsyncFlushPolicy::Batch => "Batch" - AsyncFlushPolicy::Shutdown => "Shutdown" - }, content="Shutdown") + inspect( + match state.flush_policy { + AsyncFlushPolicy::Never => "Never" + AsyncFlushPolicy::Batch => "Batch" + AsyncFlushPolicy::Shutdown => "Shutdown" + }, + content="Shutdown", + ) logger.info("skip") logger.error("one") @@ -4095,6 +4916,7 @@ async test "parsed application async logger keeps async helper surface" { inspect(logger.last_error(), content="") } +///| async test "parsed application async logger shutdown clear abandons pending records" { let logger : ApplicationAsyncLogger = parse_and_build_application_async_logger( "{\"logger\":{\"min_level\":\"info\",\"target\":\"async.app.json.clear\",\"sink\":{\"kind\":\"console\"}},\"async_config\":{\"max_pending\":2,\"overflow\":\"Blocking\",\"max_batch\":1,\"linger_ms\":0,\"flush\":\"Never\"}}", @@ -4110,10 +4932,11 @@ async test "parsed application async logger shutdown clear abandons pending reco inspect(logger.dropped_count(), content="1") } +///| async test "parsed application async logger keeps broader async composition surface" { let logger : ApplicationAsyncLogger = parse_and_build_application_async_logger( - "{\"logger\":{\"min_level\":\"warn\",\"target\":\"async.app.json.compose\",\"sink\":{\"kind\":\"console\"}},\"async_config\":{\"max_pending\":2,\"overflow\":\"DropNewest\",\"max_batch\":1,\"linger_ms\":0,\"flush\":\"Shutdown\"}}", - ) + "{\"logger\":{\"min_level\":\"warn\",\"target\":\"async.app.json.compose\",\"sink\":{\"kind\":\"console\"}},\"async_config\":{\"max_pending\":2,\"overflow\":\"DropNewest\",\"max_batch\":1,\"linger_ms\":0,\"flush\":\"Shutdown\"}}", + ) .with_timestamp() .with_context_fields([@bitlogger.field("service", "bitlogger")]) .child("worker") @@ -4140,16 +4963,16 @@ async test "parsed application async logger keeps broader async composition surf inspect(logger.last_error(), content="") } +///| async test "parsed application async logger log supports per-call target override" { let written_target : Ref[String] = Ref("") let logger : ApplicationAsyncLogger = parse_and_build_application_async_logger( "{\"logger\":{\"min_level\":\"warn\",\"target\":\"async.app.json.default\",\"sink\":{\"kind\":\"console\"}},\"async_config\":{\"max_pending\":2,\"overflow\":\"Blocking\",\"max_batch\":1,\"linger_ms\":0,\"flush\":\"Never\"}}", - ) - .with_patch(fn(rec) { - written_target.val = rec.target - rec - }) + ).with_patch(fn(rec) { + written_target.val = rec.target + rec + }) @async.with_task_group(group => { group.spawn_bg(() => logger.run()) @@ -4167,11 +4990,10 @@ async test "parsed application async logger log supports per-call target overrid let second : ApplicationAsyncLogger = parse_and_build_application_async_logger( "{\"logger\":{\"min_level\":\"warn\",\"target\":\"async.app.json.default\",\"sink\":{\"kind\":\"console\"}},\"async_config\":{\"max_pending\":2,\"overflow\":\"Blocking\",\"max_batch\":1,\"linger_ms\":0,\"flush\":\"Never\"}}", - ) - .with_patch(fn(rec) { - second_target.val = rec.target - rec - }) + ).with_patch(fn(rec) { + second_target.val = rec.target + rec + }) @async.with_task_group(group => { group.spawn_bg(() => second.run()) @@ -4182,16 +5004,16 @@ async test "parsed application async logger log supports per-call target overrid inspect(second_target.val, content="async.app.json.default") } +///| async test "parsed application async logger severity helpers use stored target without override" { let written_targets : Ref[Array[String]] = Ref([]) let logger : ApplicationAsyncLogger = parse_and_build_application_async_logger( "{\"logger\":{\"min_level\":\"trace\",\"target\":\"async.app.json.helpers.target\",\"sink\":{\"kind\":\"console\"}},\"async_config\":{\"max_pending\":4,\"overflow\":\"Blocking\",\"max_batch\":1,\"linger_ms\":0,\"flush\":\"Never\"}}", - ) - .with_patch(fn(rec) { - written_targets.val.push(rec.target) - rec - }) + ).with_patch(fn(rec) { + written_targets.val.push(rec.target) + rec + }) @async.with_task_group(group => { group.spawn_bg(() => logger.run()) @@ -4207,6 +5029,7 @@ async test "parsed application async logger severity helpers use stored target w inspect(written_targets.val[2], content="async.app.json.helpers.target") } +///| test "application async logger parse-and-build preserves parsed sync queue layer" { let logger = parse_and_build_application_async_logger( "{\"logger\":{\"min_level\":\"warn\",\"target\":\"async.app.json.queued\",\"queue\":{\"max_pending\":3,\"overflow\":\"DropNewest\"},\"sink\":{\"kind\":\"console\"}},\"async_config\":{\"max_pending\":2,\"overflow\":\"DropNewest\",\"max_batch\":1,\"linger_ms\":0,\"flush\":\"Never\"}}", @@ -4215,15 +5038,19 @@ test "application async logger parse-and-build preserves parsed sync queue layer inspect(logger.is_enabled(@bitlogger.Level::Error), content="true") inspect(logger.is_enabled(@bitlogger.Level::Info), content="false") inspect(logger.target, content="async.app.json.queued") - inspect(match logger.sink { - @bitlogger.RuntimeSink::QueuedConsole(_) => "QueuedConsole" - @bitlogger.RuntimeSink::QueuedJsonConsole(_) => "QueuedJsonConsole" - @bitlogger.RuntimeSink::QueuedTextConsole(_) => "QueuedTextConsole" - @bitlogger.RuntimeSink::QueuedFile(_) => "QueuedFile" - _ => "Other" - }, content="QueuedConsole") + inspect( + match logger.sink { + @bitlogger.RuntimeSink::QueuedConsole(_) => "QueuedConsole" + @bitlogger.RuntimeSink::QueuedJsonConsole(_) => "QueuedJsonConsole" + @bitlogger.RuntimeSink::QueuedTextConsole(_) => "QueuedTextConsole" + @bitlogger.RuntimeSink::QueuedFile(_) => "QueuedFile" + _ => "Other" + }, + content="QueuedConsole", + ) } +///| test "async parse-build facades forward parse failures" { let raw = "{\"logger\":{\"sink\":{\"kind\":\"file\",\"path\":\"\"}},\"async_config\":{\"max_pending\":2}}" diff --git a/src-async/application_async_logger.mbt b/src-async/application_async_logger.mbt index da78aeb..f255c38 100644 --- a/src-async/application_async_logger.mbt +++ b/src-async/application_async_logger.mbt @@ -1,19 +1,26 @@ +///| pub type ApplicationAsyncLogger = AsyncLogger[@bitlogger.RuntimeSink] -pub type ApplicationTextAsyncLogger = AsyncLogger[@bitlogger.FormattedConsoleSink] +///| +pub type ApplicationTextAsyncLogger = AsyncLogger[ + @bitlogger.FormattedConsoleSink, +] +///| pub fn build_application_async_logger( config : AsyncLoggerBuildConfig, ) -> ApplicationAsyncLogger { build_async_logger(config) } +///| pub fn build_application_text_async_logger( config : AsyncLoggerBuildConfig, ) -> ApplicationTextAsyncLogger { build_async_text_logger(config) } +///| pub fn parse_and_build_application_async_logger( input : String, ) -> ApplicationAsyncLogger raise { diff --git a/src-async/async_logger_native.mbt b/src-async/async_logger_native.mbt index d9d8f5a..fcd13e3 100644 --- a/src-async/async_logger_native.mbt +++ b/src-async/async_logger_native.mbt @@ -1,20 +1,25 @@ +///| pub fn async_runtime_mode() -> AsyncRuntimeMode { @utils.native_worker_async_runtime_mode() } +///| pub fn async_runtime_supports_background_worker() -> Bool { ignore(all_async_runtime_modes()) true } +///| fn async_runtime_guard_closed_on_log() -> Bool { false } +///| fn async_runtime_shutdown_clears_pending_after_wait_idle() -> Bool { true } +///| fn async_runtime_shutdown_waits_for_worker() -> Bool { true } diff --git a/src-async/async_logger_shared.mbt b/src-async/async_logger_shared.mbt index d76be25..8a7d855 100644 --- a/src-async/async_logger_shared.mbt +++ b/src-async/async_logger_shared.mbt @@ -1,84 +1,125 @@ +///| pub(all) suberror AsyncLoggerClosed { AsyncLoggerClosed } +///| pub type AsyncOverflowPolicy = @utils.AsyncOverflowPolicy +///| pub type AsyncFlushPolicy = @utils.AsyncFlushPolicy +///| pub type AsyncRuntimeMode = @utils.AsyncRuntimeMode +///| fn all_async_runtime_modes() -> Array[AsyncRuntimeMode] { - [@utils.native_worker_async_runtime_mode(), @utils.compatibility_async_runtime_mode()] + [ + @utils.native_worker_async_runtime_mode(), + @utils.compatibility_async_runtime_mode(), + ] } +///| pub type AsyncRuntimeState = @utils.AsyncRuntimeState +///| pub type AsyncLoggerState = @utils.AsyncLoggerState +///| pub fn async_runtime_mode_label(mode : AsyncRuntimeMode) -> String { @utils.async_runtime_mode_label(mode) } +///| pub fn async_runtime_state() -> AsyncRuntimeState { - AsyncRuntimeState::new(async_runtime_mode(), async_runtime_supports_background_worker()) + AsyncRuntimeState::new( + async_runtime_mode(), + async_runtime_supports_background_worker(), + ) } -pub fn async_runtime_state_to_json(state : AsyncRuntimeState) -> @json_parser.JsonValue { +///| +pub fn async_runtime_state_to_json( + state : AsyncRuntimeState, +) -> @json_parser.JsonValue { @utils.async_runtime_state_to_json(state) } +///| pub fn stringify_async_runtime_state( state : AsyncRuntimeState, - pretty~ : Bool = false, + pretty? : Bool = false, ) -> String { - @utils.stringify_async_runtime_state(state, pretty=pretty) + @utils.stringify_async_runtime_state(state, pretty~) } -pub fn async_logger_state_to_json(state : AsyncLoggerState) -> @json_parser.JsonValue { +///| +pub fn async_logger_state_to_json( + state : AsyncLoggerState, +) -> @json_parser.JsonValue { @utils.async_logger_state_to_json(state) } +///| pub fn stringify_async_logger_state( state : AsyncLoggerState, - pretty~ : Bool = false, + pretty? : Bool = false, ) -> String { - @utils.stringify_async_logger_state(state, pretty=pretty) + @utils.stringify_async_logger_state(state, pretty~) } +///| pub type AsyncLoggerConfig = @utils.AsyncLoggerConfig -pub fn parse_async_logger_config_text(input : String) -> AsyncLoggerConfig raise { +///| +pub fn parse_async_logger_config_text( + input : String, +) -> AsyncLoggerConfig raise { @utils.parse_async_logger_config_text(input) } -pub fn async_logger_config_to_json(config : AsyncLoggerConfig) -> @json_parser.JsonValue { +///| +pub fn async_logger_config_to_json( + config : AsyncLoggerConfig, +) -> @json_parser.JsonValue { @utils.async_logger_config_to_json(config) } -pub fn stringify_async_logger_config(config : AsyncLoggerConfig, pretty~ : Bool = false) -> String { - @utils.stringify_async_logger_config(config, pretty=pretty) +///| +pub fn stringify_async_logger_config( + config : AsyncLoggerConfig, + pretty? : Bool = false, +) -> String { + @utils.stringify_async_logger_config(config, pretty~) } +///| pub type AsyncLoggerBuildConfig = @utils.AsyncLoggerBuildConfig -pub fn parse_async_logger_build_config_text(input : String) -> AsyncLoggerBuildConfig raise { +///| +pub fn parse_async_logger_build_config_text( + input : String, +) -> AsyncLoggerBuildConfig raise { @utils.parse_async_logger_build_config_text(input) } +///| pub fn async_logger_build_config_to_json( config : AsyncLoggerBuildConfig, ) -> @json_parser.JsonValue { @utils.async_logger_build_config_to_json(config) } +///| pub fn stringify_async_logger_build_config( config : AsyncLoggerBuildConfig, - pretty~ : Bool = false, + pretty? : Bool = false, ) -> String { - @utils.stringify_async_logger_build_config(config, pretty=pretty) + @utils.stringify_async_logger_build_config(config, pretty~) } +///| pub struct AsyncLogger[S] { min_level : @bitlogger.Level target : String @@ -101,12 +142,13 @@ pub struct AsyncLogger[S] { last_error : Ref[String] } +///| pub fn[S] async_logger( sink : S, - config~ : AsyncLoggerConfig = AsyncLoggerConfig::new(), - min_level~ : @bitlogger.Level = @bitlogger.Level::Info, - target~ : String = "", - flush~ : (S) -> Int raise = fn(_) { 0 }, + config? : AsyncLoggerConfig = AsyncLoggerConfig::new(), + min_level? : @bitlogger.Level = @bitlogger.Level::Info, + target? : String = "", + flush? : (S) -> Int raise = fn(_) { 0 }, ) -> AsyncLogger[S] { { min_level, @@ -131,6 +173,7 @@ pub fn[S] async_logger( } } +///| fn queue_kind_of(config : AsyncLoggerConfig) -> @aqueue.Kind { let limit = if config.max_pending < 0 { 0 } else { config.max_pending } match config.overflow { @@ -140,14 +183,23 @@ fn queue_kind_of(config : AsyncLoggerConfig) -> @aqueue.Kind { } } -pub fn[S] AsyncLogger::with_timestamp(self : AsyncLogger[S], enabled~ : Bool = true) -> AsyncLogger[S] { +///| +pub fn[S] AsyncLogger::with_timestamp( + self : AsyncLogger[S], + enabled? : Bool = true, +) -> AsyncLogger[S] { { ..self, timestamp: enabled } } -pub fn[S] AsyncLogger::with_target(self : AsyncLogger[S], target : String) -> AsyncLogger[S] { - { ..self, target } +///| +pub fn[S] AsyncLogger::with_target( + self : AsyncLogger[S], + target : String, +) -> AsyncLogger[S] { + { ..self, target, } } +///| pub fn[S] AsyncLogger::with_context_fields( self : AsyncLogger[S], fields : Array[@bitlogger.Field], @@ -155,39 +207,33 @@ pub fn[S] AsyncLogger::with_context_fields( { ..self, context_fields: fields } } +///| pub fn[S] AsyncLogger::with_filter( self : AsyncLogger[S], predicate : (@bitlogger.Record) -> Bool, ) -> AsyncLogger[S] { let current = self.filter - { - ..self, - filter: fn(rec) { - current(rec) && predicate(rec) - }, - } + { ..self, filter: fn(rec) { current(rec) && predicate(rec) } } } +///| pub fn[S] AsyncLogger::with_patch( self : AsyncLogger[S], patch : @bitlogger.RecordPatch, ) -> AsyncLogger[S] { let current = self.patch - { - ..self, - patch: fn(rec) { - patch(current(rec)) - }, - } + { ..self, patch: fn(rec) { patch(current(rec)) } } } +///| pub fn[S] AsyncLogger::with_min_level( self : AsyncLogger[S], min_level : @bitlogger.Level, ) -> AsyncLogger[S] { - { ..self, min_level } + { ..self, min_level, } } +///| fn combine_targets(parent : String, child : String) -> String { if parent == "" { child @@ -198,14 +244,23 @@ fn combine_targets(parent : String, child : String) -> String { } } -pub fn[S] AsyncLogger::child(self : AsyncLogger[S], target : String) -> AsyncLogger[S] { +///| +pub fn[S] AsyncLogger::child( + self : AsyncLogger[S], + target : String, +) -> AsyncLogger[S] { { ..self, target: combine_targets(self.target, target) } } -pub fn[S] AsyncLogger::is_enabled(self : AsyncLogger[S], level : @bitlogger.Level) -> Bool { +///| +pub fn[S] AsyncLogger::is_enabled( + self : AsyncLogger[S], + level : @bitlogger.Level, +) -> Bool { level.enabled(self.min_level) } +///| fn merge_fields( left : Array[@bitlogger.Field], right : Array[@bitlogger.Field], @@ -219,32 +274,27 @@ fn merge_fields( } } +///| pub async fn[S] AsyncLogger::log( self : AsyncLogger[S], level : @bitlogger.Level, message : String, - fields~ : Array[@bitlogger.Field] = [], + fields? : Array[@bitlogger.Field] = [], target? : String = "", ) -> Unit { - guard !(async_runtime_guard_closed_on_log() && self.is_closed()) else { - () - } - guard self.is_enabled(level) else { - () - } + guard !(async_runtime_guard_closed_on_log() && self.is_closed()) else { () } + guard self.is_enabled(level) else { () } let actual_target = if target == "" { self.target } else { target } let timestamp_ms = if self.timestamp { @env.now() } else { 0UL } let rec = @bitlogger.Record::new( level, message, - timestamp_ms=timestamp_ms, + timestamp_ms~, target=actual_target, fields=merge_fields(self.context_fields, fields), ) let rec = (self.patch)(rec) - guard (self.filter)(rec) else { - () - } + guard (self.filter)(rec) else { () } let accepted = self.queue.try_put(rec) catch { err if err is AsyncLoggerClosed => false err => raise err @@ -254,7 +304,7 @@ pub async fn[S] AsyncLogger::log( } else { match self.overflow { AsyncOverflowPolicy::Blocking => { - let accepted = (async fn() -> Bool raise { + let accepted = (async fn() -> Bool { self.queue.put(rec) true })() catch { @@ -265,81 +315,93 @@ pub async fn[S] AsyncLogger::log( self.pending_count.val += 1 } } - AsyncOverflowPolicy::DropOldest | AsyncOverflowPolicy::DropNewest => { + AsyncOverflowPolicy::DropOldest | AsyncOverflowPolicy::DropNewest => self.dropped_count.val += 1 - } } } } +///| pub async fn[S] AsyncLogger::trace( self : AsyncLogger[S], message : String, - fields~ : Array[@bitlogger.Field] = [], + fields? : Array[@bitlogger.Field] = [], ) -> Unit { - self.log(@bitlogger.Level::Trace, message, fields=fields) + self.log(@bitlogger.Level::Trace, message, fields~) } +///| pub async fn[S] AsyncLogger::debug( self : AsyncLogger[S], message : String, - fields~ : Array[@bitlogger.Field] = [], + fields? : Array[@bitlogger.Field] = [], ) -> Unit { - self.log(@bitlogger.Level::Debug, message, fields=fields) + self.log(@bitlogger.Level::Debug, message, fields~) } +///| pub async fn[S] AsyncLogger::info( self : AsyncLogger[S], message : String, - fields~ : Array[@bitlogger.Field] = [], + fields? : Array[@bitlogger.Field] = [], ) -> Unit { - self.log(@bitlogger.Level::Info, message, fields=fields) + self.log(@bitlogger.Level::Info, message, fields~) } +///| pub async fn[S] AsyncLogger::warn( self : AsyncLogger[S], message : String, - fields~ : Array[@bitlogger.Field] = [], + fields? : Array[@bitlogger.Field] = [], ) -> Unit { - self.log(@bitlogger.Level::Warn, message, fields=fields) + self.log(@bitlogger.Level::Warn, message, fields~) } +///| pub async fn[S] AsyncLogger::error( self : AsyncLogger[S], message : String, - fields~ : Array[@bitlogger.Field] = [], + fields? : Array[@bitlogger.Field] = [], ) -> Unit { - self.log(@bitlogger.Level::Error, message, fields=fields) + self.log(@bitlogger.Level::Error, message, fields~) } +///| pub fn[S] AsyncLogger::pending_count(self : AsyncLogger[S]) -> Int { self.pending_count.val } +///| pub fn[S] AsyncLogger::dropped_count(self : AsyncLogger[S]) -> Int { self.dropped_count.val } +///| pub fn[S] AsyncLogger::is_closed(self : AsyncLogger[S]) -> Bool { self.is_closed.val } +///| pub fn[S] AsyncLogger::is_running(self : AsyncLogger[S]) -> Bool { self.is_running.val } +///| pub fn[S] AsyncLogger::has_failed(self : AsyncLogger[S]) -> Bool { self.has_failed.val } +///| pub fn[S] AsyncLogger::last_error(self : AsyncLogger[S]) -> String { self.last_error.val } +///| pub fn[S] AsyncLogger::flush_policy(self : AsyncLogger[S]) -> AsyncFlushPolicy { self.flush_policy } +///| pub fn[S] AsyncLogger::state(self : AsyncLogger[S]) -> AsyncLoggerState { AsyncLoggerState::new( async_runtime_state(), @@ -353,7 +415,11 @@ pub fn[S] AsyncLogger::state(self : AsyncLogger[S]) -> AsyncLoggerState { ) } -pub fn[S] AsyncLogger::close(self : AsyncLogger[S], clear? : Bool = false) -> Unit { +///| +pub fn[S] AsyncLogger::close( + self : AsyncLogger[S], + clear? : Bool = false, +) -> Unit { self.is_closed.val = true if clear { let abandoned = self.pending_count() @@ -362,9 +428,10 @@ pub fn[S] AsyncLogger::close(self : AsyncLogger[S], clear? : Bool = false) -> Un self.pending_count.val = 0 } } - self.queue.close(error=AsyncLoggerClosed, clear=clear) + self.queue.close(error=AsyncLoggerClosed, clear~) } +///| pub async fn[S] AsyncLogger::wait_idle(self : AsyncLogger[S]) -> Unit { while self.pending_count() > 0 { if self.has_failed() { @@ -374,12 +441,17 @@ pub async fn[S] AsyncLogger::wait_idle(self : AsyncLogger[S]) -> Unit { } } -pub async fn[S] AsyncLogger::shutdown(self : AsyncLogger[S], clear? : Bool = false) -> Unit { +///| +pub async fn[S] AsyncLogger::shutdown( + self : AsyncLogger[S], + clear? : Bool = false, +) -> Unit { if clear { self.close(clear=true) } else { self.wait_idle() - if async_runtime_shutdown_clears_pending_after_wait_idle() && self.pending_count() > 0 { + if async_runtime_shutdown_clears_pending_after_wait_idle() && + self.pending_count() > 0 { self.close(clear=true) } else { self.close() @@ -392,6 +464,7 @@ pub async fn[S] AsyncLogger::shutdown(self : AsyncLogger[S], clear? : Bool = fal } } +///| async fn[S : @bitlogger.Sink] run_worker(logger : AsyncLogger[S]) -> Unit { while true { let rec = logger.queue.get() catch { @@ -419,7 +492,9 @@ async fn[S : @bitlogger.Sink] run_worker(logger : AsyncLogger[S]) -> Unit { if logger.linger_ms <= 0 { break } - let waited = @async.with_timeout_opt(logger.linger_ms, () => logger.queue.get()) catch { + let waited = @async.with_timeout_opt(logger.linger_ms, () => { + logger.queue.get() + }) catch { err if err is AsyncLoggerClosed => None err => raise err } @@ -447,7 +522,10 @@ async fn[S : @bitlogger.Sink] run_worker(logger : AsyncLogger[S]) -> Unit { } } -pub async fn[S : @bitlogger.Sink] AsyncLogger::run(self : AsyncLogger[S]) -> Unit { +///| +pub async fn[S : @bitlogger.Sink] AsyncLogger::run( + self : AsyncLogger[S], +) -> Unit { self.is_running.val = true self.has_failed.val = false self.last_error.val = "" @@ -462,6 +540,7 @@ pub async fn[S : @bitlogger.Sink] AsyncLogger::run(self : AsyncLogger[S]) -> Uni self.is_running.val = false } +///| pub fn build_async_logger( config : AsyncLoggerBuildConfig, ) -> AsyncLogger[@bitlogger.RuntimeSink] { @@ -475,9 +554,14 @@ pub fn build_async_logger( ).with_timestamp(enabled=logger.timestamp) } -pub fn build_async_text_logger(config : AsyncLoggerBuildConfig) -> AsyncLogger[@bitlogger.FormattedConsoleSink] { +///| +pub fn build_async_text_logger( + config : AsyncLoggerBuildConfig, +) -> AsyncLogger[@bitlogger.FormattedConsoleSink] { async_logger( - @bitlogger.text_console_sink(config.logger.sink.text_formatter.to_formatter()), + @bitlogger.text_console_sink( + config.logger.sink.text_formatter.to_formatter(), + ), config=config.async_config, min_level=config.logger.min_level, target=config.logger.target, diff --git a/src-async/async_logger_stub.mbt b/src-async/async_logger_stub.mbt index fdff56b..97127c3 100644 --- a/src-async/async_logger_stub.mbt +++ b/src-async/async_logger_stub.mbt @@ -1,20 +1,25 @@ +///| pub fn async_runtime_mode() -> AsyncRuntimeMode { @utils.compatibility_async_runtime_mode() } +///| pub fn async_runtime_supports_background_worker() -> Bool { ignore(all_async_runtime_modes()) false } +///| fn async_runtime_guard_closed_on_log() -> Bool { true } +///| fn async_runtime_shutdown_clears_pending_after_wait_idle() -> Bool { false } +///| fn async_runtime_shutdown_waits_for_worker() -> Bool { false } diff --git a/src-async/library_async_logger.mbt b/src-async/library_async_logger.mbt index f89c68b..1f87cae 100644 --- a/src-async/library_async_logger.mbt +++ b/src-async/library_async_logger.mbt @@ -1,61 +1,67 @@ +///| pub struct LibraryAsyncLogger[S] { inner : AsyncLogger[S] } +///| fn[S] library_async_logger(logger : AsyncLogger[S]) -> LibraryAsyncLogger[S] { { inner: logger } } -pub fn[S] AsyncLogger::to_library_async_logger(self : AsyncLogger[S]) -> LibraryAsyncLogger[S] { +///| +pub fn[S] AsyncLogger::to_library_async_logger( + self : AsyncLogger[S], +) -> LibraryAsyncLogger[S] { library_async_logger(self) } +///| pub fn[S] LibraryAsyncLogger::new( sink : S, - config~ : AsyncLoggerConfig = AsyncLoggerConfig::new(), - min_level~ : @bitlogger.Level = @bitlogger.Level::Info, - target~ : String = "", - flush~ : (S) -> Int raise = fn(_) { 0 }, + config? : AsyncLoggerConfig = AsyncLoggerConfig::new(), + min_level? : @bitlogger.Level = @bitlogger.Level::Info, + target? : String = "", + flush? : (S) -> Int raise = fn(_) { 0 }, ) -> LibraryAsyncLogger[S] { - library_async_logger( - async_logger( - sink, - config=config, - min_level=min_level, - target=target, - flush=flush, - ), - ) + library_async_logger(async_logger(sink, config~, min_level~, target~, flush~)) } -pub fn[S] LibraryAsyncLogger::to_async_logger(self : LibraryAsyncLogger[S]) -> AsyncLogger[S] { +///| +pub fn[S] LibraryAsyncLogger::to_async_logger( + self : LibraryAsyncLogger[S], +) -> AsyncLogger[S] { self.inner } +///| fn[S] configured_library_async_logger( logger : AsyncLogger[S], ) -> LibraryAsyncLogger[S] { library_async_logger(logger) } +///| pub fn build_library_async_logger( config : AsyncLoggerBuildConfig, ) -> LibraryAsyncLogger[@bitlogger.RuntimeSink] { configured_library_async_logger(build_async_logger(config)) } +///| pub fn build_library_async_text_logger( config : AsyncLoggerBuildConfig, ) -> LibraryAsyncLogger[@bitlogger.FormattedConsoleSink] { configured_library_async_logger(build_async_text_logger(config)) } +///| pub fn parse_and_build_library_async_logger( input : String, ) -> LibraryAsyncLogger[@bitlogger.RuntimeSink] raise { build_library_async_logger(parse_async_logger_build_config_text(input)) } +///| pub fn[S] LibraryAsyncLogger::with_target( self : LibraryAsyncLogger[S], target : String, @@ -63,6 +69,7 @@ pub fn[S] LibraryAsyncLogger::with_target( library_async_logger(self.inner.with_target(target)) } +///| pub fn[S] LibraryAsyncLogger::child( self : LibraryAsyncLogger[S], target : String, @@ -70,6 +77,7 @@ pub fn[S] LibraryAsyncLogger::child( library_async_logger(self.inner.child(target)) } +///| pub fn[S] LibraryAsyncLogger::with_context_fields( self : LibraryAsyncLogger[S], fields : Array[@bitlogger.Field], @@ -77,6 +85,7 @@ pub fn[S] LibraryAsyncLogger::with_context_fields( library_async_logger(self.inner.with_context_fields(fields)) } +///| pub fn[S] LibraryAsyncLogger::bind( self : LibraryAsyncLogger[S], fields : Array[@bitlogger.Field], @@ -84,6 +93,7 @@ pub fn[S] LibraryAsyncLogger::bind( self.with_context_fields(fields) } +///| pub fn[S] LibraryAsyncLogger::is_enabled( self : LibraryAsyncLogger[S], level : @bitlogger.Level, @@ -91,47 +101,55 @@ pub fn[S] LibraryAsyncLogger::is_enabled( self.inner.is_enabled(level) } +///| pub async fn[S] LibraryAsyncLogger::log( self : LibraryAsyncLogger[S], level : @bitlogger.Level, message : String, - fields~ : Array[@bitlogger.Field] = [], + fields? : Array[@bitlogger.Field] = [], target? : String = "", ) -> Unit { - self.inner.log(level, message, fields=fields, target=target) + self.inner.log(level, message, fields~, target~) } +///| pub async fn[S] LibraryAsyncLogger::info( self : LibraryAsyncLogger[S], message : String, - fields~ : Array[@bitlogger.Field] = [], + fields? : Array[@bitlogger.Field] = [], ) -> Unit { - self.inner.info(message, fields=fields) + self.inner.info(message, fields~) } +///| pub async fn[S] LibraryAsyncLogger::warn( self : LibraryAsyncLogger[S], message : String, - fields~ : Array[@bitlogger.Field] = [], + fields? : Array[@bitlogger.Field] = [], ) -> Unit { - self.inner.warn(message, fields=fields) + self.inner.warn(message, fields~) } +///| pub async fn[S] LibraryAsyncLogger::error( self : LibraryAsyncLogger[S], message : String, - fields~ : Array[@bitlogger.Field] = [], + fields? : Array[@bitlogger.Field] = [], ) -> Unit { - self.inner.error(message, fields=fields) + self.inner.error(message, fields~) } -pub async fn[S : @bitlogger.Sink] LibraryAsyncLogger::run(self : LibraryAsyncLogger[S]) -> Unit { +///| +pub async fn[S : @bitlogger.Sink] LibraryAsyncLogger::run( + self : LibraryAsyncLogger[S], +) -> Unit { self.inner.run() } +///| pub async fn[S] LibraryAsyncLogger::shutdown( self : LibraryAsyncLogger[S], clear? : Bool = false, ) -> Unit { - self.inner.shutdown(clear=clear) + self.inner.shutdown(clear~) } diff --git a/src-async/moon.pkg b/src-async/moon.pkg index 9d8693c..4e551c3 100644 --- a/src-async/moon.pkg +++ b/src-async/moon.pkg @@ -1,17 +1,23 @@ import { "Nanaloveyuki/BitLogger/src" @bitlogger, - "Nanaloveyuki/BitLogger/src-async/utils" @utils, - "maria/json_parser" @json_parser, - "moonbitlang/async" @async, - "moonbitlang/async/aqueue" @aqueue, - "moonbitlang/core/env" @env, + "Nanaloveyuki/BitLogger/src-async/utils", + "maria/json_parser", + "moonbitlang/async", + "moonbitlang/async/aqueue", + "moonbitlang/core/env", "moonbitlang/core/ref", } options( targets: { "async_logger_shared.mbt": [ "native", "llvm", "js", "wasm", "wasm-gc" ], - "application_async_logger.mbt": [ "native", "llvm", "js", "wasm", "wasm-gc" ], + "application_async_logger.mbt": [ + "native", + "llvm", + "js", + "wasm", + "wasm-gc", + ], "library_async_logger.mbt": [ "native", "llvm", "js", "wasm", "wasm-gc" ], "async_logger_native.mbt": [ "native", "llvm" ], "async_logger_stub.mbt": [ "js", "wasm", "wasm-gc" ], diff --git a/src-async/utils/async_models.mbt b/src-async/utils/async_models.mbt index b95a716..dad47f7 100644 --- a/src-async/utils/async_models.mbt +++ b/src-async/utils/async_models.mbt @@ -1,20 +1,24 @@ +///| pub(all) enum AsyncOverflowPolicy { Blocking DropOldest DropNewest } +///| pub(all) enum AsyncFlushPolicy { Never Batch Shutdown } +///| pub enum AsyncRuntimeMode { NativeWorker Compatibility } +///| pub fn async_runtime_mode_label(mode : AsyncRuntimeMode) -> String { match mode { AsyncRuntimeMode::NativeWorker => "native_worker" @@ -22,19 +26,23 @@ pub fn async_runtime_mode_label(mode : AsyncRuntimeMode) -> String { } } +///| pub fn native_worker_async_runtime_mode() -> AsyncRuntimeMode { AsyncRuntimeMode::NativeWorker } +///| pub fn compatibility_async_runtime_mode() -> AsyncRuntimeMode { AsyncRuntimeMode::Compatibility } +///| pub struct AsyncRuntimeState { mode : AsyncRuntimeMode background_worker : Bool } +///| pub fn AsyncRuntimeState::new( mode : AsyncRuntimeMode, background_worker : Bool, @@ -42,6 +50,7 @@ pub fn AsyncRuntimeState::new( { mode, background_worker } } +///| pub struct AsyncLoggerState { runtime : AsyncRuntimeState pending_count : Int @@ -53,6 +62,7 @@ pub struct AsyncLoggerState { flush_policy : AsyncFlushPolicy } +///| pub fn AsyncLoggerState::new( runtime : AsyncRuntimeState, pending_count : Int, @@ -75,16 +85,20 @@ pub fn AsyncLoggerState::new( } } -pub fn async_runtime_state_to_json(state : AsyncRuntimeState) -> @json_parser.JsonValue { +///| +pub fn async_runtime_state_to_json( + state : AsyncRuntimeState, +) -> @json_parser.JsonValue { @json_parser.JsonValue::Object({ "mode": @json_parser.JsonValue::String(async_runtime_mode_label(state.mode)), "background_worker": @json_parser.JsonValue::Bool(state.background_worker), }) } +///| pub fn stringify_async_runtime_state( state : AsyncRuntimeState, - pretty~ : Bool = false, + pretty? : Bool = false, ) -> String { let value = async_runtime_state_to_json(state) if pretty { @@ -94,6 +108,7 @@ pub fn stringify_async_runtime_state( } } +///| fn async_flush_policy_label(policy : AsyncFlushPolicy) -> String { match policy { AsyncFlushPolicy::Never => "Never" @@ -102,26 +117,39 @@ fn async_flush_policy_label(policy : AsyncFlushPolicy) -> String { } } -fn async_logger_state_to_json_value(state : AsyncLoggerState) -> @json_parser.JsonValue { +///| +fn async_logger_state_to_json_value( + state : AsyncLoggerState, +) -> @json_parser.JsonValue { @json_parser.JsonValue::Object({ "runtime": async_runtime_state_to_json(state.runtime), - "pending_count": @json_parser.JsonValue::Number(state.pending_count.to_double()), - "dropped_count": @json_parser.JsonValue::Number(state.dropped_count.to_double()), + "pending_count": @json_parser.JsonValue::Number( + state.pending_count.to_double(), + ), + "dropped_count": @json_parser.JsonValue::Number( + state.dropped_count.to_double(), + ), "is_closed": @json_parser.JsonValue::Bool(state.is_closed), "is_running": @json_parser.JsonValue::Bool(state.is_running), "has_failed": @json_parser.JsonValue::Bool(state.has_failed), "last_error": @json_parser.JsonValue::String(state.last_error), - "flush_policy": @json_parser.JsonValue::String(async_flush_policy_label(state.flush_policy)), + "flush_policy": @json_parser.JsonValue::String( + async_flush_policy_label(state.flush_policy), + ), }) } -pub fn async_logger_state_to_json(state : AsyncLoggerState) -> @json_parser.JsonValue { +///| +pub fn async_logger_state_to_json( + state : AsyncLoggerState, +) -> @json_parser.JsonValue { async_logger_state_to_json_value(state) } +///| pub fn stringify_async_logger_state( state : AsyncLoggerState, - pretty~ : Bool = false, + pretty? : Bool = false, ) -> String { let value = async_logger_state_to_json_value(state) if pretty { @@ -131,6 +159,7 @@ pub fn stringify_async_logger_state( } } +///| pub struct AsyncLoggerConfig { max_pending : Int overflow : AsyncOverflowPolicy @@ -139,22 +168,32 @@ pub struct AsyncLoggerConfig { flush : AsyncFlushPolicy } +///| pub fn AsyncLoggerConfig::new( - max_pending~ : Int = 0, - overflow~ : AsyncOverflowPolicy = AsyncOverflowPolicy::Blocking, - max_batch~ : Int = 1, - linger_ms~ : Int = 0, - flush~ : AsyncFlushPolicy = AsyncFlushPolicy::Never, + max_pending? : Int = 0, + overflow? : AsyncOverflowPolicy = AsyncOverflowPolicy::Blocking, + max_batch? : Int = 1, + linger_ms? : Int = 0, + flush? : AsyncFlushPolicy = AsyncFlushPolicy::Never, ) -> AsyncLoggerConfig { { max_pending, overflow, - max_batch: if max_batch <= 1 { 1 } else { max_batch }, - linger_ms: if linger_ms < 0 { 0 } else { linger_ms }, + max_batch: if max_batch <= 1 { + 1 + } else { + max_batch + }, + linger_ms: if linger_ms < 0 { + 0 + } else { + linger_ms + }, flush, } } +///| fn parse_async_overflow(name : String) -> AsyncOverflowPolicy raise { match name.to_upper() { "BLOCKING" => AsyncOverflowPolicy::Blocking @@ -165,6 +204,7 @@ fn parse_async_overflow(name : String) -> AsyncOverflowPolicy raise { } } +///| fn parse_async_flush(name : String) -> AsyncFlushPolicy raise { match name.to_upper() { "NEVER" => AsyncFlushPolicy::Never @@ -175,75 +215,100 @@ fn parse_async_flush(name : String) -> AsyncFlushPolicy raise { } } -pub fn parse_async_logger_config_text(input : String) -> AsyncLoggerConfig raise { +///| +pub fn parse_async_logger_config_text( + input : String, +) -> AsyncLoggerConfig raise { let root = @json_parser.parse(input) let obj = match root.as_object() { Some(obj) => obj None => raise Failure::Failure("Expected object for async logger config") } let max_pending = match obj.get("max_pending") { - Some(value) => match value.as_number() { - Some(number) => number.to_int() - None => raise Failure::Failure("Expected number at async_config.max_pending") - } + Some(value) => + match value.as_number() { + Some(number) => number.to_int() + None => + raise Failure::Failure("Expected number at async_config.max_pending") + } None => 0 } let overflow = match obj.get("overflow") { - Some(value) => match value.as_string() { - Some(text) => parse_async_overflow(text) - None => raise Failure::Failure("Expected string at async_config.overflow") - } + Some(value) => + match value.as_string() { + Some(text) => parse_async_overflow(text) + None => + raise Failure::Failure("Expected string at async_config.overflow") + } None => AsyncOverflowPolicy::Blocking } let max_batch = match obj.get("max_batch") { - Some(value) => match value.as_number() { - Some(number) => number.to_int() - None => raise Failure::Failure("Expected number at async_config.max_batch") - } + Some(value) => + match value.as_number() { + Some(number) => number.to_int() + None => + raise Failure::Failure("Expected number at async_config.max_batch") + } None => 1 } let linger_ms = match obj.get("linger_ms") { - Some(value) => match value.as_number() { - Some(number) => number.to_int() - None => raise Failure::Failure("Expected number at async_config.linger_ms") - } + Some(value) => + match value.as_number() { + Some(number) => number.to_int() + None => + raise Failure::Failure("Expected number at async_config.linger_ms") + } None => 0 } let flush = match obj.get("flush") { - Some(value) => match value.as_string() { - Some(text) => parse_async_flush(text) - None => raise Failure::Failure("Expected string at async_config.flush") - } + Some(value) => + match value.as_string() { + Some(text) => parse_async_flush(text) + None => raise Failure::Failure("Expected string at async_config.flush") + } None => AsyncFlushPolicy::Never } AsyncLoggerConfig::new( - max_pending=max_pending, - overflow=overflow, - max_batch=max_batch, - linger_ms=linger_ms, - flush=flush, + max_pending~, + overflow~, + max_batch~, + linger_ms~, + flush~, ) } -pub fn async_logger_config_to_json(config : AsyncLoggerConfig) -> @json_parser.JsonValue { +///| +pub fn async_logger_config_to_json( + config : AsyncLoggerConfig, +) -> @json_parser.JsonValue { @json_parser.JsonValue::Object({ - "max_pending": @json_parser.JsonValue::Number(config.max_pending.to_double()), + "max_pending": @json_parser.JsonValue::Number( + config.max_pending.to_double(), + ), "max_batch": @json_parser.JsonValue::Number(config.max_batch.to_double()), "linger_ms": @json_parser.JsonValue::Number(config.linger_ms.to_double()), - "overflow": @json_parser.JsonValue::String(match config.overflow { - AsyncOverflowPolicy::Blocking => "Blocking" - AsyncOverflowPolicy::DropOldest => "DropOldest" - AsyncOverflowPolicy::DropNewest => "DropNewest" - }), - "flush": @json_parser.JsonValue::String(match config.flush { - AsyncFlushPolicy::Never => "Never" - AsyncFlushPolicy::Batch => "Batch" - AsyncFlushPolicy::Shutdown => "Shutdown" - }), + "overflow": @json_parser.JsonValue::String( + match config.overflow { + AsyncOverflowPolicy::Blocking => "Blocking" + AsyncOverflowPolicy::DropOldest => "DropOldest" + AsyncOverflowPolicy::DropNewest => "DropNewest" + }, + ), + "flush": @json_parser.JsonValue::String( + match config.flush { + AsyncFlushPolicy::Never => "Never" + AsyncFlushPolicy::Batch => "Batch" + AsyncFlushPolicy::Shutdown => "Shutdown" + }, + ), }) } -pub fn stringify_async_logger_config(config : AsyncLoggerConfig, pretty~ : Bool = false) -> String { +///| +pub fn stringify_async_logger_config( + config : AsyncLoggerConfig, + pretty? : Bool = false, +) -> String { let value = async_logger_config_to_json(config) if pretty { @json_parser.stringify_pretty(value, 2) @@ -252,35 +317,45 @@ pub fn stringify_async_logger_config(config : AsyncLoggerConfig, pretty~ : Bool } } +///| pub struct AsyncLoggerBuildConfig { logger : @bitlogger.LoggerConfig async_config : AsyncLoggerConfig } +///| pub fn AsyncLoggerBuildConfig::new( - logger~ : @bitlogger.LoggerConfig = @bitlogger.default_logger_config(), - async_config~ : AsyncLoggerConfig = AsyncLoggerConfig::new(), + logger? : @bitlogger.LoggerConfig = @bitlogger.default_logger_config(), + async_config? : AsyncLoggerConfig = AsyncLoggerConfig::new(), ) -> AsyncLoggerBuildConfig { { logger, async_config } } -pub fn parse_async_logger_build_config_text(input : String) -> AsyncLoggerBuildConfig raise { +///| +pub fn parse_async_logger_build_config_text( + input : String, +) -> AsyncLoggerBuildConfig raise { let root = @json_parser.parse(input) let obj = match root.as_object() { Some(obj) => obj - None => raise Failure::Failure("Expected object at async logger build config root") + None => + raise Failure::Failure( + "Expected object at async logger build config root", + ) } let logger = match obj.get("logger") { - Some(value) => @bitlogger.parse_logger_config_text(@json_parser.stringify(value)) + Some(value) => + @bitlogger.parse_logger_config_text(@json_parser.stringify(value)) None => @bitlogger.default_logger_config() } let async_config = match obj.get("async_config") { Some(value) => parse_async_logger_config_text(@json_parser.stringify(value)) None => AsyncLoggerConfig::new() } - AsyncLoggerBuildConfig::new(logger=logger, async_config=async_config) + AsyncLoggerBuildConfig::new(logger~, async_config~) } +///| pub fn async_logger_build_config_to_json( config : AsyncLoggerBuildConfig, ) -> @json_parser.JsonValue { @@ -290,9 +365,10 @@ pub fn async_logger_build_config_to_json( }) } +///| pub fn stringify_async_logger_build_config( config : AsyncLoggerBuildConfig, - pretty~ : Bool = false, + pretty? : Bool = false, ) -> String { let value = async_logger_build_config_to_json(config) if pretty { diff --git a/src-async/utils/moon.pkg b/src-async/utils/moon.pkg index 8c32779..81bcffe 100644 --- a/src-async/utils/moon.pkg +++ b/src-async/utils/moon.pkg @@ -1,4 +1,4 @@ import { "Nanaloveyuki/BitLogger/src" @bitlogger, - "maria/json_parser" @json_parser, + "maria/json_parser", } diff --git a/src/BitLogger_test.mbt b/src/BitLogger_test.mbt index 7417853..9aaa119 100644 --- a/src/BitLogger_test.mbt +++ b/src/BitLogger_test.mbt @@ -1,12 +1,16 @@ +///| test "level filter works" { let logger = Logger::new(console_sink(), min_level=Level::Warn, target="test") inspect(logger.is_enabled(Level::Error), content="true") inspect(logger.is_enabled(Level::Info), content="false") } +///| test "context sink merges fields" { - let logger = Logger::new(console_sink(), min_level=Level::Info, target="ctx") - .with_context_fields([field("service", "bitlogger")]) + let logger = Logger::new(console_sink(), min_level=Level::Info, target="ctx").with_context_fields([ + field("service", "bitlogger"), + ], + ) let merged = [field("service", "bitlogger"), field("mode", "test")] inspect(merged.length(), content="2") inspect(merged[0].key, content="service") @@ -14,6 +18,7 @@ test "context sink merges fields" { logger.info("hello", fields=[field("mode", "test")]) } +///| test "fields helper builds field arrays ergonomically" { let items = fields([("service", "bitlogger"), ("mode", "test")]) inspect(items.length(), content="2") @@ -23,6 +28,7 @@ test "fields helper builds field arrays ergonomically" { inspect(items[1].value, content="test") } +///| test "fanout sink can write to plain and json outputs" { let logger = Logger::new( fanout_sink(console_sink(), json_console_sink()), @@ -33,12 +39,15 @@ test "fanout sink can write to plain and json outputs" { logger.info("hello", fields=[field("kind", "dual")]) } +///| test "child logger composes target path" { - let logger = Logger::new(console_sink(), min_level=Level::Info, target="app") - .child("worker") + let logger = Logger::new(console_sink(), min_level=Level::Info, target="app").child( + "worker", + ) inspect(logger.target, content="app.worker") } +///| test "logger config parser reads core options" { let config = parse_logger_config_text( "{\"min_level\":\"debug\",\"target\":\"service\",\"timestamp\":true}", @@ -48,38 +57,61 @@ test "logger config parser reads core options" { 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( + 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") + 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") + 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( + 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) => { @@ -98,14 +130,18 @@ test "logger config parser reads formatter style tags" { } } +///| 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( + match config.sink.kind { + SinkKind::File => "File" + _ => "other" + }, + content="File", + ) inspect(config.sink.path, content="bitlogger.log") match config.sink.rotation { Some(rotation) => { @@ -116,18 +152,22 @@ test "logger config parser reads file rotation options" { } } +///| 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( + 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") @@ -138,16 +178,20 @@ test "logger config parser fills omitted top-level keys from defaults" { 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( + 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") @@ -155,24 +199,43 @@ test "logger config parser fills nested queue and formatter defaults" { 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( + 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") + 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("{")) @@ -191,7 +254,11 @@ test "logger config parser rejects malformed sync config input" { 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\"}}}")) + ignore( + parse_logger_config_text( + "{\"sink\":{\"kind\":\"console\",\"text_formatter\":{\"color_mode\":\"loud\"}}}", + ), + ) "no error" })() catch { ConfigError::InvalidConfig(message) => message @@ -199,6 +266,7 @@ test "logger config parser rejects malformed sync config input" { inspect(invalid_enum_error, content="Unsupported color mode: loud") } +///| test "logger config stringify roundtrips stable fields" { let text = stringify_logger_config( LoggerConfig::new( @@ -225,9 +293,13 @@ test "logger config stringify roundtrips stable fields" { 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}") + 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( @@ -248,18 +320,46 @@ test "logger config stringify roundtrips formatter style tags" { ), ) 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( + 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") + 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( @@ -281,6 +381,7 @@ test "logger config stringify roundtrips file rotation fields" { } } +///| test "config subtype json helpers stringify stable shapes" { inspect( stringify_queue_config( @@ -303,9 +404,7 @@ test "config subtype json helpers stringify stable shapes" { style_markup=StyleMarkupMode::Builtin, target_style_markup=StyleMarkupMode::Builtin, fields_style_markup=StyleMarkupMode::Disabled, - style_tags={ - "accent": text_style(fg=Some("#4cc9f0"), bold=true), - }, + 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\"}}}", @@ -318,21 +417,34 @@ test "config subtype json helpers stringify stable shapes" { append=false, auto_flush=false, rotation=Some(file_rotation(128, max_backups=2)), - text_formatter=TextFormatterConfig::new(show_timestamp=false, color_mode=ColorMode::Auto), + text_formatter=TextFormatterConfig::new( + show_timestamp=false, + color_mode=ColorMode::Auto, + ), ), ), content="{\"kind\":\"file\",\"path\":\"demo.log\",\"append\":false,\"auto_flush\":false,\"text_formatter\":{\"show_timestamp\":false,\"show_level\":true,\"show_target\":true,\"show_fields\":true,\"separator\":\" \",\"field_separator\":\" \",\"template\":\"\",\"color_mode\":\"auto\",\"color_support\":\"truecolor\",\"style_markup\":\"full\",\"target_style_markup\":\"disabled\",\"fields_style_markup\":\"disabled\"},\"rotation\":{\"max_bytes\":128,\"max_backups\":2}}", ) } +///| test "config json helpers export stable structured shapes" { let queue_json = queue_config_to_json( QueueConfig::new(8, overflow=QueueOverflowPolicy::DropOldest), ) let queue_obj = queue_json.as_object().unwrap() - inspect(@json_parser.stringify(queue_json), content="{\"max_pending\":8,\"overflow\":\"DropOldest\"}") - inspect(queue_obj.get("max_pending").unwrap().as_number().unwrap().to_int(), content="8") - inspect(queue_obj.get("overflow").unwrap().as_string().unwrap(), content="DropOldest") + 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( @@ -348,24 +460,56 @@ test "config json helpers export stable structured shapes" { style_markup=StyleMarkupMode::Builtin, target_style_markup=StyleMarkupMode::Builtin, fields_style_markup=StyleMarkupMode::Disabled, - style_tags={ - "accent": text_style(fg=Some("#4cc9f0"), bold=true), - }, + 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 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( + 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") @@ -376,20 +520,45 @@ test "config json helpers export stable structured shapes" { append=false, auto_flush=false, rotation=Some(file_rotation(128, max_backups=2)), - text_formatter=TextFormatterConfig::new(show_timestamp=false, color_mode=ColorMode::Auto), + 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 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("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( + sink_obj.get("auto_flush").unwrap().as_bool().unwrap(), + content="false", + ) + inspect( + sink_formatter_obj.get("show_timestamp").unwrap().as_bool().unwrap(), + content="false", + ) + inspect( + sink_formatter_obj.get("color_mode").unwrap().as_string().unwrap(), + content="auto", + ) + inspect( + rotation_obj.get("max_bytes").unwrap().as_number().unwrap().to_int(), + content="128", + ) + inspect( + rotation_obj.get("max_backups").unwrap().as_number().unwrap().to_int(), + content="2", + ) let logger_json = logger_config_to_json( LoggerConfig::new( @@ -403,48 +572,114 @@ test "config json helpers export stable structured shapes" { 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("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") + 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() + 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("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("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( + 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") @@ -453,10 +688,13 @@ test "default config helpers expose baseline config shapes" { 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( + match sink.kind { + SinkKind::Console => "Console" + _ => "other" + }, + content="Console", + ) inspect(sink.path, content="") inspect(sink.rotation is None, content="true") @@ -465,12 +703,16 @@ test "default config helpers expose baseline config shapes" { 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") + 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, @@ -482,34 +724,38 @@ test "config basic color support downgrades hex colors" { 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") + 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), - }, + 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") + 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), - }, + style_tags={ "brand": text_style(fg=Some("#4cc9f0"), bold=true) }, ) let rendered = format_text( Record::new(Level::Info, "custom builtin"), @@ -518,15 +764,14 @@ test "config builtin style markup ignores custom tags" { 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), - }, + style_tags={ "accent": text_style(fg=Some("#4cc9f0"), bold=true) }, ) let rendered = format_text( Record::new(Level::Info, "raw tag"), @@ -535,6 +780,7 @@ test "config disabled style markup keeps raw tags" { inspect(rendered, content="raw tag") } +///| test "config target and fields markup modes render separately" { let formatter = TextFormatterConfig::new( show_level=false, @@ -558,6 +804,7 @@ test "config target and fields markup modes render separately" { ) } +///| test "build logger from config supports queued text console" { let logger = build_logger( LoggerConfig::new( @@ -566,7 +813,10 @@ test "build logger from config supports queued text console" { timestamp=true, sink=SinkConfig::new( kind=SinkKind::TextConsole, - text_formatter=TextFormatterConfig::new(show_timestamp=false, separator=" | "), + text_formatter=TextFormatterConfig::new( + show_timestamp=false, + separator=" | ", + ), ), queue=Some(QueueConfig::new(2, overflow=QueueOverflowPolicy::DropOldest)), ), @@ -578,6 +828,7 @@ test "build logger from config supports queued text console" { inspect(logger.flush(), content="2") } +///| test "configured logger drain supports partial queue draining" { let logger = build_logger( LoggerConfig::new( @@ -596,6 +847,7 @@ test "configured logger drain supports partial queue draining" { inspect(logger.flush(), content="1") } +///| test "configured logger reports dropped_count for bounded queue" { let logger = build_logger( LoggerConfig::new( @@ -614,6 +866,7 @@ test "configured logger reports dropped_count for bounded queue" { inspect(logger.flush(), content="2") } +///| test "configured logger queue helpers stay aligned with runtime sink helpers" { let logger = build_logger( LoggerConfig::new( @@ -621,7 +874,10 @@ test "configured logger queue helpers stay aligned with runtime sink helpers" { target="config.delegate", sink=SinkConfig::new( kind=SinkKind::TextConsole, - text_formatter=TextFormatterConfig::new(show_timestamp=false, show_target=false), + text_formatter=TextFormatterConfig::new( + show_timestamp=false, + show_target=false, + ), ), queue=Some(QueueConfig::new(2, overflow=QueueOverflowPolicy::DropOldest)), ), @@ -641,6 +897,7 @@ test "configured logger queue helpers stay aligned with runtime sink helpers" { inspect(logger.dropped_count() == sink.dropped_count(), content="true") } +///| test "configured logger close stays aligned with runtime sink close" { let console_config = LoggerConfig::new( min_level=Level::Info, @@ -661,6 +918,7 @@ test "configured logger close stays aligned with runtime sink close" { inspect(file_logger.close() == file_sink.close(), content="true") } +///| test "runtime sink queue helpers forward direct queue state" { let sink = RuntimeSink::QueuedTextConsole( queued_sink( @@ -681,6 +939,7 @@ test "runtime sink queue helpers forward direct queue state" { inspect(sink.pending_count(), content="0") } +///| test "runtime sink plain variants use documented fallback counts" { let console_sink = RuntimeSink::Console(console_sink()) inspect(console_sink.pending_count(), content="0") @@ -688,7 +947,9 @@ test "runtime sink plain variants use documented fallback counts" { 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)) + 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() { @@ -702,6 +963,7 @@ test "runtime sink plain variants use documented fallback counts" { } } +///| test "runtime sink non-file variants expose documented file fallbacks" { let sink = RuntimeSink::Console(console_sink()) inspect(sink.file_available(), content="false") @@ -720,7 +982,10 @@ test "runtime sink non-file variants expose documented file fallbacks" { 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_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") @@ -749,6 +1014,7 @@ test "runtime sink non-file variants expose documented file fallbacks" { 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( @@ -811,6 +1077,7 @@ test "runtime sink plain file helpers expose direct file state and policy" { } } +///| test "runtime sink queued file helpers preserve queue-aware file runtime state" { let sink = RuntimeSink::QueuedFile( queued_sink( @@ -819,7 +1086,11 @@ test "runtime sink queued file helpers preserve queue-aware file runtime state" overflow=QueueOverflowPolicy::DropNewest, ), ) - let logger = Logger::new(sink, min_level=Level::Info, target="runtime.file.queue") + let logger = Logger::new( + sink, + min_level=Level::Info, + target="runtime.file.queue", + ) logger.info("one") logger.info("two") logger.info("three") @@ -855,6 +1126,7 @@ test "runtime sink queued file helpers preserve queue-aware file runtime state" } } +///| 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() { @@ -863,7 +1135,9 @@ test "runtime sink plain file reopen helpers and failure resets work directly" { 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") + 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") @@ -882,7 +1156,9 @@ test "runtime sink plain file reopen helpers and failure resets work directly" { } 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") + 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") @@ -901,6 +1177,7 @@ test "runtime sink plain file reopen helpers and failure resets work directly" { } } +///| 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), @@ -911,7 +1188,11 @@ test "runtime sink queued file reopen helpers and failure resets work directly" 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") + 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") @@ -932,7 +1213,11 @@ test "runtime sink queued file reopen helpers and failure resets work directly" } 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") + 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") @@ -956,6 +1241,7 @@ test "runtime sink queued file reopen helpers and failure resets work directly" } } +///| test "configured logger exposes file sink observability helpers" { let logger = build_logger( LoggerConfig::new( @@ -994,13 +1280,17 @@ test "configured logger exposes file sink observability helpers" { } None => inspect(false, content="true") } - inspect(logger.file_open_failures(), content=if logger.file_available() { "0" } else { "1" }) + 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( @@ -1010,12 +1300,27 @@ test "configured logger file observation helpers stay aligned with runtime sink ) let file_sink = file_logger.sink - inspect(file_logger.file_available() == file_sink.file_available(), content="true") + 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") + 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( @@ -1025,16 +1330,36 @@ test "configured logger file observation helpers stay aligned with runtime sink ) 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") + 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( @@ -1070,16 +1395,38 @@ test "configured logger file policy helpers stay aligned with runtime sink helpe 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_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_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") + 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( @@ -1104,59 +1451,120 @@ test "configured logger file policy helpers stay aligned with runtime sink helpe Some(_) => true None => false } - let console_sink_defaults_has_rotation = match console_sink_defaults.rotation { + 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_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") + 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"), + 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_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_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") + 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( + 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") + 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( @@ -1165,20 +1573,47 @@ test "configured logger file setters stay aligned with runtime sink setters" { ), ) 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") + 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_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( + @json_parser.stringify(rotation_json), + content="{\"max_bytes\":64,\"max_backups\":2}", + ) + inspect( + rotation_obj.get("max_bytes").unwrap().as_number().unwrap().to_int(), + content="64", + ) + inspect( + rotation_obj.get("max_backups").unwrap().as_number().unwrap().to_int(), + content="2", + ) let plain = file_sink_state_to_json( FileSinkState::new( @@ -1194,21 +1629,52 @@ test "file state json helpers stringify stable snapshots" { ), ) let plain_obj = plain.as_object().unwrap() - let plain_rotation_obj = plain_obj.get("rotation").unwrap().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("path").unwrap().as_string().unwrap(), + content="demo.log", + ) + inspect( + plain_obj.get("available").unwrap().as_bool().unwrap(), + content="true", + ) inspect(plain_obj.get("append").unwrap().as_bool().unwrap(), content="false") - inspect(plain_obj.get("auto_flush").unwrap().as_bool().unwrap(), content="true") - inspect(plain_obj.get("open_failures").unwrap().as_number().unwrap().to_int(), content="1") - inspect(plain_obj.get("write_failures").unwrap().as_number().unwrap().to_int(), content="2") - inspect(plain_obj.get("flush_failures").unwrap().as_number().unwrap().to_int(), content="3") - inspect(plain_obj.get("rotation_failures").unwrap().as_number().unwrap().to_int(), content="4") - inspect(plain_rotation_obj.get("max_bytes").unwrap().as_number().unwrap().to_int(), content="64") - inspect(plain_rotation_obj.get("max_backups").unwrap().as_number().unwrap().to_int(), content="2") + inspect( + plain_obj.get("auto_flush").unwrap().as_bool().unwrap(), + content="true", + ) + inspect( + plain_obj.get("open_failures").unwrap().as_number().unwrap().to_int(), + content="1", + ) + inspect( + plain_obj.get("write_failures").unwrap().as_number().unwrap().to_int(), + content="2", + ) + inspect( + plain_obj.get("flush_failures").unwrap().as_number().unwrap().to_int(), + content="3", + ) + inspect( + plain_obj.get("rotation_failures").unwrap().as_number().unwrap().to_int(), + content="4", + ) + inspect( + plain_rotation_obj.get("max_bytes").unwrap().as_number().unwrap().to_int(), + content="64", + ) + inspect( + plain_rotation_obj.get("max_backups").unwrap().as_number().unwrap().to_int(), + content="2", + ) inspect( stringify_file_sink_state( FileSinkState::new( @@ -1227,6 +1693,7 @@ test "file state json helpers stringify stable snapshots" { ) } +///| test "runtime file state json helpers stringify queue snapshots" { let runtime_json = runtime_file_state_to_json( RuntimeFileState::new( @@ -1249,11 +1716,26 @@ test "runtime file state json helpers stringify queue snapshots" { 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") + 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( @@ -1279,6 +1761,7 @@ test "runtime file state json helpers stringify queue snapshots" { ) } +///| test "file sink policy json helpers stringify stable policies" { let policy_json = file_sink_policy_to_json( FileSinkPolicy::new( @@ -1288,17 +1771,33 @@ test "file sink policy json helpers stringify stable policies" { ), ) let policy_obj = policy_json.as_object().unwrap() - let policy_rotation_obj = policy_obj.get("rotation").unwrap().as_object().unwrap() + let policy_rotation_obj = policy_obj + .get("rotation") + .unwrap() + .as_object() + .unwrap() inspect( - @json_parser.stringify( - policy_json, - ), + @json_parser.stringify(policy_json), content="{\"append\":false,\"auto_flush\":true,\"rotation\":{\"max_bytes\":96,\"max_backups\":3}}", ) inspect(policy_obj.get("append").unwrap().as_bool().unwrap(), content="false") - inspect(policy_obj.get("auto_flush").unwrap().as_bool().unwrap(), content="true") - inspect(policy_rotation_obj.get("max_bytes").unwrap().as_number().unwrap().to_int(), content="96") - inspect(policy_rotation_obj.get("max_backups").unwrap().as_number().unwrap().to_int(), content="3") + inspect( + policy_obj.get("auto_flush").unwrap().as_bool().unwrap(), + content="true", + ) + inspect( + policy_rotation_obj.get("max_bytes").unwrap().as_number().unwrap().to_int(), + content="96", + ) + inspect( + policy_rotation_obj + .get("max_backups") + .unwrap() + .as_number() + .unwrap() + .to_int(), + content="3", + ) inspect( stringify_file_sink_policy( FileSinkPolicy::new(append=true, auto_flush=false, rotation=None), @@ -1307,6 +1806,7 @@ test "file sink policy json helpers stringify stable policies" { ) } +///| test "configured logger reports disabled rotation when file sink has none" { let logger = build_logger( LoggerConfig::new( @@ -1322,6 +1822,7 @@ test "configured logger reports disabled rotation when file sink has none" { 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)), @@ -1329,6 +1830,7 @@ test "configured non-file logger has no file runtime state" { inspect(logger.file_runtime_state() is None, content="true") } +///| test "configured file logger mirrors backend file capability" { let logger = build_logger( LoggerConfig::new( @@ -1346,6 +1848,7 @@ test "configured file logger mirrors backend file capability" { } } +///| test "configured logger file setters update file sink policy state" { let logger = build_logger( LoggerConfig::new( @@ -1365,7 +1868,10 @@ test "configured logger file setters update file sink policy state" { 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_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) => { @@ -1377,7 +1883,10 @@ test "configured logger file setters update file sink policy state" { 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_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() @@ -1396,43 +1905,81 @@ test "configured logger file setters update file sink policy state" { 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"), + 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_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_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_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_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_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") + 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( @@ -1466,6 +2013,7 @@ test "configured logger reset policy restores configured file defaults" { ignore(logger.close()) } +///| test "configured logger set policy applies bundled runtime file policy" { let logger = build_logger( LoggerConfig::new( @@ -1500,8 +2048,11 @@ test "configured logger set policy applies bundled runtime file policy" { ignore(logger.close()) } +///| test "configured non-file logger cannot reset file policy" { - let logger = build_logger(LoggerConfig::new(sink=SinkConfig::new(kind=SinkKind::Console))) + 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") @@ -1509,6 +2060,7 @@ test "configured non-file logger cannot reset file policy" { inspect(logger.file_policy_matches_default(), content="false") } +///| test "configured logger can reopen built file sink" { let logger = build_logger( LoggerConfig::new( @@ -1559,11 +2111,15 @@ test "configured logger can reopen built file sink" { } } +///| test "configured non-file logger cannot reset file failure counters" { - let logger = build_logger(LoggerConfig::new(sink=SinkConfig::new(kind=SinkKind::Console))) + 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( @@ -1585,28 +2141,56 @@ test "configured logger file reset helpers stay aligned with runtime sink helper 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_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(), + 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( @@ -1625,9 +2209,13 @@ test "configured logger exposes file flush and close helpers" { } } +///| 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"), + 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 @@ -1635,19 +2223,31 @@ test "configured logger file flush and close stay aligned with runtime sink help 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_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") + 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"), + sink=SinkConfig::new( + kind=SinkKind::File, + path="config-file-state-delegate.log", + ), ), ) let file_sink = file_logger.sink @@ -1658,26 +2258,48 @@ test "configured logger file state helpers stay aligned with runtime sink helper 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), + 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"), + sink=SinkConfig::new( + kind=SinkKind::File, + path="config-file-runtime-delegate.log", + ), queue=Some(QueueConfig::new(4, overflow=QueueOverflowPolicy::DropNewest)), ), ) @@ -1690,39 +2312,90 @@ test "configured logger file state helpers stay aligned with runtime sink helper 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") + 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.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.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( + 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") } @@ -1738,16 +2411,38 @@ test "configured logger file state helpers stay aligned with runtime sink helper 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), + 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", ) @@ -1755,6 +2450,7 @@ test "configured logger file state helpers stay aligned with runtime sink helper ignore(queued_logger.close()) } +///| test "configured queued file logger flushes queue through file helper" { let logger = build_logger( LoggerConfig::new( @@ -1793,6 +2489,7 @@ test "configured queued file logger flushes queue through file helper" { } } +///| test "logger log supports per-call target override" { let captured_target : Ref[String] = Ref("") let captured_message : Ref[String] = Ref("") @@ -1828,6 +2525,7 @@ test "logger log supports per-call target override" { inspect(captured_fields.val[0].key, content="service") } +///| test "logger severity helpers use stored target without override" { let captured_levels : Ref[Array[String]] = Ref([]) let captured_targets : Ref[Array[String]] = Ref([]) @@ -1877,19 +2575,22 @@ test "logger severity helpers use stored target without override" { inspect(captured_fields.val[4][1].value, content="error") } +///| 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") + 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") @@ -1898,9 +2599,13 @@ test "library logger keeps a smaller stable facade" { 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 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") @@ -1914,19 +2619,20 @@ test "library logger context binding unwraps to context sink shape" { 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", - ) + 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")]) @@ -1950,17 +2656,18 @@ test "library logger bind matches context facade contract" { 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", - ) + 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") @@ -1976,17 +2683,18 @@ test "library logger with_target preserves facade state while replacing target" 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", - ) + 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") @@ -2008,6 +2716,7 @@ test "library logger child composes target through facade" { 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("") @@ -2020,8 +2729,7 @@ test "library logger log supports per-call target override" { }), min_level=Level::Warn, target="lib.default", - ) - .with_context_fields([field("service", "bitlogger")]) + ).with_context_fields([field("service", "bitlogger")]) logger.log( Level::Error, @@ -2044,6 +2752,7 @@ test "library logger log supports per-call target override" { 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([]) @@ -2056,8 +2765,7 @@ test "library logger severity helpers use stored target without override" { }), min_level=Level::Info, target="lib.helpers", - ) - .bind([field("service", "bitlogger")]) + ).bind([field("service", "bitlogger")]) logger.info("info", fields=[field("mode", "info")]) logger.warn("warn", fields=[field("mode", "warn")]) @@ -2082,8 +2790,13 @@ test "library logger severity helpers use stored target without override" { 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 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") @@ -2091,6 +2804,7 @@ test "library logger can unwrap to full logger when needed" { inspect(full.target, content="lib") } +///| test "library logger can be built from configured runtime path" { let logger = build_library_logger( LoggerConfig::new( @@ -2105,17 +2819,18 @@ test "library logger can be built from configured runtime path" { 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), - ), - ) + 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 @@ -2147,17 +2862,18 @@ test "library logger builder log supports per-call target override through facad 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), - ), - ) + 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()) @@ -2191,6 +2907,7 @@ test "library logger builder severity helpers use stored target without override 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, @@ -2205,8 +2922,14 @@ test "library logger builder unwrap matches direct configured builder behavior" 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") + 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" @@ -2242,6 +2965,7 @@ test "library logger builder unwrap matches direct configured builder behavior" 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, @@ -2263,8 +2987,14 @@ test "library logger builder unwrap preserves file runtime helpers" { 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") + 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() @@ -2272,10 +3002,22 @@ test "library logger builder unwrap preserves file runtime helpers" { 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") + 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)) => { @@ -2293,53 +3035,122 @@ test "library logger builder unwrap preserves file runtime helpers" { 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"), + 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") + 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") + 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_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_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_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\"}}", @@ -2350,13 +3161,14 @@ test "library logger can be parsed and built from config text" { 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\"}}", - ) + "{\"min_level\":\"warn\",\"target\":\"lib.json.default\",\"sink\":{\"kind\":\"console\"}}", + ) .to_logger() .with_patch(fn(rec) { captured_target.val = rec.target @@ -2388,13 +3200,14 @@ test "parsed library logger log supports per-call target override through facade 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\"}}", - ) + "{\"min_level\":\"info\",\"target\":\"lib.json.helpers\",\"sink\":{\"kind\":\"console\"}}", + ) .to_logger() .with_patch(fn(rec) { captured_levels.val.push(rec.level.label()) @@ -2428,6 +3241,7 @@ test "parsed library logger severity helpers use stored target without override" 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) @@ -2436,8 +3250,14 @@ test "library logger parse-build unwrap matches parsed direct builder behavior" 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") + 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" @@ -2473,6 +3293,7 @@ test "library logger parse-build unwrap matches parsed direct builder behavior" 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) @@ -2489,8 +3310,14 @@ test "library logger parse-build unwrap preserves file runtime helpers" { 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") + 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() @@ -2498,10 +3325,22 @@ test "library logger parse-build unwrap preserves file runtime helpers" { 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") + 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)) => { @@ -2519,33 +3358,83 @@ test "library logger parse-build unwrap preserves file runtime helpers" { 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") + 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_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\"}}", @@ -2559,13 +3448,16 @@ test "parsed library logger unwrap preserves configured runtime path" { 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( + 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") @@ -2574,6 +3466,7 @@ test "parsed library logger unwrap preserves configured runtime path" { 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"), @@ -2584,19 +3477,20 @@ test "configured logger can project to library logger" { 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", - ) + 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() @@ -2615,6 +3509,7 @@ test "logger projection preserves sync composition state through library facade" inspect(captured_fields.val[1].key, content="mode") } +///| test "configured logger projection preserves runtime helpers through unwrap" { let projected = build_logger( LoggerConfig::new( @@ -2631,13 +3526,16 @@ test "configured logger projection preserves runtime helpers through unwrap" { 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( + 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") @@ -2646,6 +3544,7 @@ test "configured logger projection preserves runtime helpers through unwrap" { inspect(full.sink.pending_count(), content="0") } +///| test "configured logger projection preserves file runtime helpers through unwrap" { let configured = build_logger( LoggerConfig::new( @@ -2668,8 +3567,14 @@ test "configured logger projection preserves file runtime helpers through unwrap 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") + 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() @@ -2677,10 +3582,22 @@ test "configured logger projection preserves file runtime helpers through unwrap 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") + 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)) => { @@ -2693,22 +3610,57 @@ test "configured logger projection preserves file runtime helpers through unwrap _ => 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))), + 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_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") @@ -2719,6 +3671,7 @@ test "configured logger projection preserves file runtime helpers through unwrap 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") @@ -2728,6 +3681,7 @@ test "default library logger mirrors shared sync defaults" { 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") @@ -2746,6 +3700,7 @@ test "default library logger snapshots shared defaults at creation time" { 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"), @@ -2755,6 +3710,7 @@ test "application logger aliases configured runtime entry" { inspect(logger.target, content="app.runtime") } +///| test "application logger builder matches direct configured builder behavior" { let config = LoggerConfig::new( min_level=Level::Warn, @@ -2769,8 +3725,14 @@ test "application logger builder matches direct configured builder behavior" { 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") + 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" @@ -2800,12 +3762,22 @@ test "application logger builder matches direct configured builder behavior" { 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.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") + inspect( + application.pending_count() == configured.pending_count(), + content="true", + ) } +///| test "configured logger keeps broader logger composition surface" { let logger : ConfiguredLogger = build_logger( LoggerConfig::new( @@ -2821,13 +3793,16 @@ test "configured logger keeps broader logger composition surface" { 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") + 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") @@ -2836,17 +3811,18 @@ test "configured logger keeps broader logger composition surface" { 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), - ), - ) + 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 @@ -2876,17 +3852,18 @@ test "configured logger build supports per-call target override" { 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), - ), - ) + 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) @@ -2918,6 +3895,7 @@ test "configured logger build severity helpers use stored target without overrid 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( @@ -2947,17 +3925,18 @@ test "application logger keeps configured runtime helper surface" { } } +///| 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), - ), - ) + 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 @@ -2987,17 +3966,18 @@ test "application logger build supports per-call target override" { 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), - ), - ) + 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) @@ -3029,6 +4009,7 @@ test "application logger build severity helpers use stored target without overri 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( @@ -3044,13 +4025,16 @@ test "application logger keeps broader logger composition surface" { 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") + 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") @@ -3060,6 +4044,7 @@ test "application logger keeps broader logger composition surface" { 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\"}}", @@ -3069,13 +4054,14 @@ test "application logger can be built from config text" { 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\"}}", - ) + "{\"min_level\":\"warn\",\"target\":\"app.json.default\",\"sink\":{\"kind\":\"console\"}}", + ) .with_patch(fn(rec) { captured_target.val = rec.target captured_message.val = rec.message @@ -3105,13 +4091,14 @@ test "parsed application logger log supports per-call target override" { 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\"}}", - ) + "{\"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) @@ -3143,6 +4130,7 @@ test "parsed application logger severity helpers use stored target without overr 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) @@ -3150,8 +4138,14 @@ test "application logger parse-build matches parsed direct builder behavior" { 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") + 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" @@ -3181,12 +4175,22 @@ test "application logger parse-build matches parsed direct builder behavior" { 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.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") + 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, @@ -3204,35 +4208,81 @@ test "application logger file helpers match direct configured builder behavior" 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") + 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") + 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.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") + 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)) => { @@ -3246,60 +4296,132 @@ test "application logger file helpers match direct configured builder behavior" } inspect(application.file_flush() == configured.file_flush(), content="true") - inspect(application.pending_count() == configured.pending_count(), 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"), + 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_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") + 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") + 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(), + 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_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_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\"}}", @@ -3324,6 +4446,7 @@ test "parsed application logger keeps direct runtime helper surface" { } } +///| 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) @@ -3336,23 +4459,60 @@ test "parsed application logger file helpers match parsed configured builder beh 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") + 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.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") + 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)) => { @@ -3366,36 +4526,92 @@ test "parsed application logger file helpers match parsed configured builder beh } inspect(application.file_flush() == configured.file_flush(), content="true") - inspect(application.pending_count() == configured.pending_count(), 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") + 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_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\"}}", @@ -3406,13 +4622,16 @@ test "parsed configured logger keeps composition and runtime helper surface" { 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") + 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") @@ -3422,13 +4641,14 @@ test "parsed configured logger keeps composition and runtime helper surface" { 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\"}}", - ) + "{\"min_level\":\"warn\",\"target\":\"parsed.default\",\"sink\":{\"kind\":\"console\"}}", + ) .with_patch(fn(rec) { captured_target.val = rec.target captured_message.val = rec.message @@ -3458,13 +4678,14 @@ test "parsed configured logger log supports per-call target override" { 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\"}}", - ) + "{\"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) @@ -3496,6 +4717,7 @@ test "parsed configured logger severity helpers use stored target without overri 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 = "{" diff --git a/src/BitLogger_wbtest.mbt b/src/BitLogger_wbtest.mbt index 275d4ea..5e8004b 100644 --- a/src/BitLogger_wbtest.mbt +++ b/src/BitLogger_wbtest.mbt @@ -1,3 +1,4 @@ +///| test "default logger can be reconfigured" { set_default_min_level(Level::Debug) set_default_target("global") @@ -6,6 +7,7 @@ test "default logger can be reconfigured" { inspect(logger.target, content="global") } +///| test "default logger snapshots shared defaults at creation time" { set_default_min_level(Level::Warn) set_default_target("before") @@ -24,24 +26,30 @@ test "default logger snapshots shared defaults at creation time" { inspect(after.target, content="after") } +///| test "logger can enable timestamps" { - let logger = Logger::new(console_sink(), min_level=Level::Info, target="time") - .with_timestamp() + 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 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", ) - 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, @@ -59,6 +67,7 @@ test "text formatter can emit message only" { inspect(format_text(rec, formatter=message_only), content="just message") } +///| test "text formatter supports template rendering" { let rec = record( Level::Info, @@ -73,20 +82,22 @@ test "text formatter supports template rendering" { template="[{level}] {target} {message} :: {fields} @{timestamp}", ) inspect( - format_text(rec, formatter=formatter), + 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=formatter), + 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") @@ -101,6 +112,7 @@ test "text formatter auto color respects NO_COLOR" { } } +///| test "text formatter renders named inline color tags in ansi mode" { let rec = record(Level::Info, "boom", target="svc") inspect( @@ -109,6 +121,7 @@ test "text formatter renders named inline color tags in ansi mode" { ) } +///| test "text formatter strips inline tags in plain mode" { let rec = record(Level::Info, "boom bold", target="svc") inspect( @@ -117,46 +130,83 @@ test "text formatter strips inline tags in plain mode" { ) } +///| 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)), + 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)), + 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)), + 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)), + 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)), + 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( @@ -173,14 +223,19 @@ test "text formatter can downgrade hex colors to basic ansi" { ) } +///| 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)), + 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( @@ -192,6 +247,7 @@ test "text formatter can disable style markup parsing" { ) } +///| test "text formatter builtin style markup mode ignores custom tags" { let formatter = text_formatter( show_level=false, @@ -203,17 +259,22 @@ test "text formatter builtin style markup mode ignores custom tags" { ) let rec = record(Level::Info, "custom builtin") inspect( - format_text(rec, formatter=formatter), + 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::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, @@ -224,83 +285,103 @@ test "text formatter supports custom style tags" { ) let rec = record(Level::Info, "api") inspect( - format_text(rec, formatter=formatter), + 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), + default_style_tag_registry().set_tag( + "red", + fg=Some("#ff5a5f"), + underline=true, + ), ) let rec = record(Level::Info, "alert") inspect( - format_text(rec, formatter=formatter), + 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"))) + 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")), - ) + ).with_style_tags(style_tag_registry().set_tag("accent", fg=Some("#abcdef"))) let rec = record(Level::Info, "tag") inspect( - format_text(rec, formatter=formatter), + 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)) + 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)), + 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)) + 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), + 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"), - ) + ).with_style_tags(style_tag_registry().define_alias("danger", "red")) let rec = record(Level::Info, "boom") - inspect( - format_text(rec, formatter=formatter), - content="\u{001b}[31mboom\u{001b}[0m", - ) + 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, @@ -308,28 +389,37 @@ test "builtin semantic style tags are available" { color_mode=ColorMode::Always, style_markup=StyleMarkupMode::Builtin, ) - let rec = record(Level::Info, "ok careful boom quiet") + let rec = record( + Level::Info, + "ok careful boom quiet", + ) inspect( - format_text(rec, formatter=formatter), + 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), + default_style_tag_registry().set_tag( + "success", + fg=Some("#00ffaa"), + underline=true, + ), ) let rec = record(Level::Info, "ok") inspect( - format_text(rec, formatter=formatter), + 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, @@ -338,11 +428,12 @@ test "text formatter can enable markup for target separately" { ) let rec = record(Level::Info, "hello", target="svc") inspect( - format_text(rec, formatter=formatter), + 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, @@ -350,13 +441,16 @@ test "text formatter can enable markup for field values separately" { color_mode=ColorMode::Always, fields_style_markup=StyleMarkupMode::Builtin, ) - let rec = record(Level::Info, "hello", fields=[field("status", "ok")]) + let rec = record(Level::Info, "hello", fields=[ + field("status", "ok"), + ]) inspect( - format_text(rec, formatter=formatter), + 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, @@ -364,13 +458,16 @@ test "text formatter leaves field keys raw when field markup is enabled" { color_mode=ColorMode::Always, fields_style_markup=StyleMarkupMode::Builtin, ) - let rec = record(Level::Info, "hello", fields=[field("status", "ok")]) + let rec = record(Level::Info, "hello", fields=[ + field("status", "ok"), + ]) inspect( - format_text(rec, formatter=formatter), + 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( @@ -378,26 +475,30 @@ test "text formatter template respects disabled fields" { show_fields=false, template="[{level}] {target}{message}{fields}", ) - inspect(format_text(rec, formatter=formatter), content="[WARN] just message") + 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 - }, + 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") + 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() @@ -407,12 +508,18 @@ test "file sink availability reflects backend support" { 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( + (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.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() { @@ -424,8 +531,12 @@ test "file sink availability reflects backend support" { } } +///| test "file sink unavailable backend keeps stable fallback state" { - let sink = file_sink("bitlogger-unavailable.log", rotation=Some(file_rotation(64, max_backups=2))) + 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()) @@ -449,6 +560,7 @@ test "file sink unavailable backend keeps stable fallback state" { } } +///| test "file sink rotation config normalizes invalid inputs" { let rotation = file_rotation(0, max_backups=0) inspect(rotation.max_bytes, content="1") @@ -464,6 +576,7 @@ test "file sink rotation config normalizes invalid inputs" { } } +///| test "file sink setters update auto flush and rotation state" { let sink = file_sink("bitlogger-setters.log") let default_policy = sink.default_policy() @@ -490,7 +603,10 @@ test "file sink setters update auto flush and rotation state" { 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.reopen(), + content=if sink.is_available() { "true" } else { "false" }, + ) inspect(sink.append_mode(), content="false") let state = sink.state() let policy = sink.policy() @@ -508,6 +624,7 @@ test "file sink setters update auto flush and rotation state" { inspect(sink.policy_matches_default(), content="true") } +///| test "file sink reset policy restores configured defaults" { let sink = file_sink( "bitlogger-reset-policy.log", @@ -535,6 +652,7 @@ test "file sink reset policy restores configured defaults" { } } +///| test "file sink set policy applies bundled runtime policy" { let sink = file_sink("bitlogger-set-policy.log") let default_policy = sink.default_policy() @@ -561,6 +679,7 @@ test "file sink set policy applies bundled runtime policy" { 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)) @@ -579,6 +698,7 @@ test "file sink tracks rotation failures on unavailable backend" { } } +///| test "file sink reopen and failure counters reflect backend state" { let sink = file_sink("bitlogger-reopen.log") if sink.is_available() { @@ -630,6 +750,7 @@ 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)) @@ -666,20 +787,18 @@ test "file sink can rotate on native backend" { } } +///| test "json formatter keeps structured shape" { - let rec = record( - Level::Error, - "failed", - timestamp_ms=55UL, - target="svc", - fields=[field("code", "500")], - ) + 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\"}", ) } +///| test "callback sink receives record" { let captured_target : Ref[String] = Ref("") let captured_message : Ref[String] = Ref("") @@ -696,20 +815,15 @@ test "callback sink receives record" { 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" - }, + 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", @@ -722,6 +836,7 @@ test "split sink routes records by predicate" { 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([]) @@ -748,21 +863,22 @@ test "split_by_level routes warn and error separately" { 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", - ) + 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", + ) .child("worker") .with_context_fields([field("service", "bitlogger")]) .with_timestamp() @@ -773,6 +889,7 @@ test "callback sink sees child target and context logger shape" { inspect(captured_timestamp.val > 0UL, content="true") } +///| test "bind aliases context fields ergonomically" { let captured_target : Ref[String] = Ref("") let captured_message : Ref[String] = Ref("") @@ -798,12 +915,11 @@ test "bind aliases context fields ergonomically" { inspect(captured_fields.val[2].value, content="test") } +///| 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) - }), + callback_sink(fn(rec) { flushed_messages.val.push(rec.message) }), flush_limit=10, ) let logger = Logger::new(sink, min_level=Level::Info, target="buffered") @@ -818,12 +934,11 @@ test "buffered sink flushes manually" { 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) - }), + callback_sink(fn(rec) { flushed_messages.val.push(rec.message) }), flush_limit=2, ) let logger = Logger::new(sink, min_level=Level::Info, target="buffered") @@ -836,15 +951,12 @@ test "buffered sink flushes automatically at limit" { 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" - }, + 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") @@ -856,37 +968,34 @@ test "filter sink only forwards matching records" { 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) - }), + callback_sink(fn(rec) { flushed_messages.val.push(rec.message) }), min_level=Level::Info, target="app", - ) - .with_filter(fn(rec) { - rec.target == "app.worker" - }) + ).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) - }), + 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"), - ])) + ).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") @@ -894,39 +1003,46 @@ test "filter helpers support target level and message composition" { 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) - }), + 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")), - ])) + ).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("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) - }), + 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"), - ])) + ).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")]) @@ -935,6 +1051,7 @@ test "any_of helper accepts multiple predicates" { 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("") @@ -947,12 +1064,14 @@ test "patch sink can rewrite message target and 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")]), - ])) + ).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") @@ -965,31 +1084,30 @@ test "patch sink can rewrite message target and fields" { 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 - }), + 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")], - ) + 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) - }), + callback_sink(fn(rec) { flushed_messages.val.push(rec.message) }), ) let logger = Logger::new(sink, min_level=Level::Info, target="queue") logger.info("one") @@ -1007,12 +1125,11 @@ test "queued sink drains in order" { 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) - }), + callback_sink(fn(rec) { flushed_messages.val.push(rec.message) }), max_pending=2, overflow=QueueOverflowPolicy::DropNewest, ) @@ -1028,12 +1145,11 @@ test "queued sink can drop newest when full" { 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) - }), + callback_sink(fn(rec) { flushed_messages.val.push(rec.message) }), max_pending=2, overflow=QueueOverflowPolicy::DropOldest, ) @@ -1049,15 +1165,14 @@ test "queued sink can drop oldest when full" { 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", - ) + 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") diff --git a/src/README.mbt.md b/src/README.mbt.md index dcc9b7a..f5df72b 100644 --- a/src/README.mbt.md +++ b/src/README.mbt.md @@ -19,13 +19,17 @@ BitLogger 是一个使用 MoonBit 编写的结构化日志库. ## Example / 示例 -```moonbit +```moonbit nocheck +///| test { let logger = build_logger( text_console( min_level=Level::Debug, target="demo", - text_formatter=TextFormatterConfig::new(show_timestamp=false, separator=" | "), + text_formatter=TextFormatterConfig::new( + show_timestamp=false, + separator=" | ", + ), ), ) logger.info("starting", fields=[field("port", "8080")]) @@ -76,7 +80,9 @@ Project command note / 项目命令说明: - `console_basic` / `text_formatter` / `style_tags` / `config_build` / `presets` are the portable-first set - `console_basic` / `text_formatter` / `style_tags` / `config_build` / `presets` 是 portable-first 示例集 - `file_rotation` is native-sensitive, and `async_basic` stays native-only because of the current `async fn main` entry limitation + - `async_basic` currently relies on `moonbitlang/async`, whose upstream README only claims native/LLVM support on Linux/MacOS; do not treat Windows native as a supported baseline for this example until upstream support is stated clearly - `file_rotation` 是 native-sensitive,`async_basic` 的 native-only 限制来自当前 `async fn main` 入口能力 + - `async_basic` 当前依赖 `moonbitlang/async`,其上游 README 目前只明确承诺 Linux/MacOS 的 native/LLVM 支持;在上游明确支持之前,不要把 Windows native 当作这个示例的支持基线 - package-level API docs / 单接口 API 文档: - `../docs/api/` - common entry points / 常用入口: diff --git a/src/application_logger.mbt b/src/application_logger.mbt index c0360f1..ee56cae 100644 --- a/src/application_logger.mbt +++ b/src/application_logger.mbt @@ -1,9 +1,12 @@ +///| pub type ApplicationLogger = ConfiguredLogger +///| pub fn build_application_logger(config : LoggerConfig) -> ApplicationLogger { build_logger(config) } +///| pub fn parse_and_build_application_logger( input : String, ) -> ApplicationLogger raise ConfigError { diff --git a/src/config.mbt b/src/config.mbt index b7f1421..7311868 100644 --- a/src/config.mbt +++ b/src/config.mbt @@ -1,66 +1,100 @@ +///| pub type ConfigError = @utils.ConfigError +///| pub type SinkKind = @utils.SinkKind +///| pub type TextFormatterConfig = @utils.TextFormatterConfig +///| pub type QueueConfig = @utils.QueueConfig +///| pub type SinkConfig = @utils.SinkConfig +///| pub type LoggerConfig = @utils.LoggerConfig +///| pub fn default_text_formatter_config() -> TextFormatterConfig { @utils.default_text_formatter_config() } +///| pub fn default_sink_config() -> SinkConfig { @utils.default_sink_config() } +///| pub fn default_logger_config() -> LoggerConfig { @utils.default_logger_config() } -pub fn parse_logger_config_text(input : String) -> LoggerConfig raise ConfigError { +///| +pub fn parse_logger_config_text( + input : String, +) -> LoggerConfig raise ConfigError { @utils.parse_logger_config_text(input) } +///| pub fn queue_config_to_json(queue : QueueConfig) -> @json_parser.JsonValue { @utils.queue_config_to_json(queue) } -pub fn stringify_queue_config(queue : QueueConfig, pretty~ : Bool = false) -> String { - @utils.stringify_queue_config(queue, pretty=pretty) +///| +pub fn stringify_queue_config( + queue : QueueConfig, + pretty? : Bool = false, +) -> String { + @utils.stringify_queue_config(queue, pretty~) } -pub fn text_formatter_config_to_json(config : TextFormatterConfig) -> @json_parser.JsonValue { +///| +pub fn text_formatter_config_to_json( + config : TextFormatterConfig, +) -> @json_parser.JsonValue { @utils.text_formatter_config_to_json(config) } +///| pub fn stringify_text_formatter_config( config : TextFormatterConfig, - pretty~ : Bool = false, + pretty? : Bool = false, ) -> String { - @utils.stringify_text_formatter_config(config, pretty=pretty) + @utils.stringify_text_formatter_config(config, pretty~) } -pub fn file_rotation_config_to_json(config : FileRotation) -> @json_parser.JsonValue { +///| +pub fn file_rotation_config_to_json( + config : FileRotation, +) -> @json_parser.JsonValue { @utils.file_rotation_config_to_json(config) } +///| pub fn sink_config_to_json(config : SinkConfig) -> @json_parser.JsonValue { @utils.sink_config_to_json(config) } -pub fn stringify_sink_config(config : SinkConfig, pretty~ : Bool = false) -> String { - @utils.stringify_sink_config(config, pretty=pretty) +///| +pub fn stringify_sink_config( + config : SinkConfig, + pretty? : Bool = false, +) -> String { + @utils.stringify_sink_config(config, pretty~) } +///| pub fn logger_config_to_json(config : LoggerConfig) -> @json_parser.JsonValue { @utils.logger_config_to_json(config) } -pub fn stringify_logger_config(config : LoggerConfig, pretty~ : Bool = false) -> String { - @utils.stringify_logger_config(config, pretty=pretty) +///| +pub fn stringify_logger_config( + config : LoggerConfig, + pretty? : Bool = false, +) -> String { + @utils.stringify_logger_config(config, pretty~) } diff --git a/src/core/level.mbt b/src/core/level.mbt index e81ea30..25e56a0 100644 --- a/src/core/level.mbt +++ b/src/core/level.mbt @@ -1,3 +1,4 @@ +///| pub(all) enum Level { Trace Debug @@ -6,6 +7,7 @@ pub(all) enum Level { Error } +///| pub fn Level::priority(self : Level) -> Int { match self { Level::Trace => 10 @@ -16,6 +18,7 @@ pub fn Level::priority(self : Level) -> Int { } } +///| pub fn Level::label(self : Level) -> String { match self { Level::Trace => "TRACE" @@ -26,6 +29,7 @@ pub fn Level::label(self : Level) -> String { } } +///| pub fn Level::enabled(self : Level, min_level : Level) -> Bool { self.priority() >= min_level.priority() } diff --git a/src/core/record.mbt b/src/core/record.mbt index 5d7ebc1..facffb0 100644 --- a/src/core/record.mbt +++ b/src/core/record.mbt @@ -1,18 +1,20 @@ +///| pub struct Field { key : String value : String } +///| pub fn field(key : String, value : String) -> Field { { key, value } } +///| pub fn fields(entries : Array[(String, String)]) -> Array[Field] { - entries.map(fn(entry) { - field(entry.0, entry.1) - }) + entries.map(fn(entry) { field(entry.0, entry.1) }) } +///| pub struct Record { level : Level timestamp_ms : UInt64 @@ -21,30 +23,34 @@ pub struct Record { fields : Array[Field] } +///| pub fn Record::new( level : Level, message : String, - timestamp_ms~ : UInt64 = 0UL, - target~ : String = "", - fields~ : Array[Field] = [], + timestamp_ms? : UInt64 = 0UL, + target? : String = "", + fields? : Array[Field] = [], ) -> Record { { level, timestamp_ms, target, message, fields } } +///| pub fn Field::with_value(self : Field, value : String) -> Field { field(self.key, value) } +///| pub fn Record::with_target(self : Record, target : String) -> Record { Record::new( self.level, self.message, timestamp_ms=self.timestamp_ms, - target=target, + target~, fields=self.fields, ) } +///| pub fn Record::with_message(self : Record, message : String) -> Record { Record::new( self.level, @@ -55,16 +61,18 @@ pub fn Record::with_message(self : Record, message : String) -> Record { ) } +///| pub fn Record::with_fields(self : Record, fields : Array[Field]) -> Record { Record::new( self.level, self.message, timestamp_ms=self.timestamp_ms, target=self.target, - fields=fields, + fields~, ) } +///| pub fn Record::copy(self : Record) -> Record { Record::new( self.level, diff --git a/src/file_backend_native.mbt b/src/file_backend_native.mbt index b979044..49bfd39 100644 --- a/src/file_backend_native.mbt +++ b/src/file_backend_native.mbt @@ -1,37 +1,47 @@ +///| type FileHandle = @utils.FileHandle +///| fn open_file_handle_internal(path : String, append : Bool) -> FileHandle? { @utils.open_file_handle_internal(path, append) } +///| fn write_file_handle_internal(handle : FileHandle, content : String) -> Bool { @utils.write_file_handle_internal(handle, content) } +///| fn flush_file_handle_internal(handle : FileHandle) -> Bool { @utils.flush_file_handle_internal(handle) } +///| fn close_file_handle_internal(handle : FileHandle) -> Bool { @utils.close_file_handle_internal(handle) } +///| fn file_size_internal(handle : FileHandle) -> Int { @utils.file_size_internal(handle) } +///| fn rename_file_internal(from_path : String, to_path : String) -> Bool { @utils.rename_file_internal(from_path, to_path) } +///| fn remove_file_internal(path : String) -> Bool { @utils.remove_file_internal(path) } +///| fn string_byte_length_internal(content : String) -> Int { @utils.string_byte_length_internal(content) } +///| fn native_files_supported_internal() -> Bool { @utils.native_files_supported_internal() } diff --git a/src/file_backend_stub.mbt b/src/file_backend_stub.mbt index b979044..49bfd39 100644 --- a/src/file_backend_stub.mbt +++ b/src/file_backend_stub.mbt @@ -1,37 +1,47 @@ +///| type FileHandle = @utils.FileHandle +///| fn open_file_handle_internal(path : String, append : Bool) -> FileHandle? { @utils.open_file_handle_internal(path, append) } +///| fn write_file_handle_internal(handle : FileHandle, content : String) -> Bool { @utils.write_file_handle_internal(handle, content) } +///| fn flush_file_handle_internal(handle : FileHandle) -> Bool { @utils.flush_file_handle_internal(handle) } +///| fn close_file_handle_internal(handle : FileHandle) -> Bool { @utils.close_file_handle_internal(handle) } +///| fn file_size_internal(handle : FileHandle) -> Int { @utils.file_size_internal(handle) } +///| fn rename_file_internal(from_path : String, to_path : String) -> Bool { @utils.rename_file_internal(from_path, to_path) } +///| fn remove_file_internal(path : String) -> Bool { @utils.remove_file_internal(path) } +///| fn string_byte_length_internal(content : String) -> Int { @utils.string_byte_length_internal(content) } +///| fn native_files_supported_internal() -> Bool { @utils.native_files_supported_internal() } diff --git a/src/filters.mbt b/src/filters.mbt index bccb229..07928ee 100644 --- a/src/filters.mbt +++ b/src/filters.mbt @@ -1,37 +1,47 @@ +///| pub type RecordPredicate = @utils.RecordPredicate +///| pub fn level_at_least(min_level : Level) -> RecordPredicate { @utils.level_at_least(min_level) } +///| pub fn target_is(target : String) -> RecordPredicate { @utils.target_is(target) } +///| pub fn target_has_prefix(prefix : String) -> RecordPredicate { @utils.target_has_prefix(prefix) } +///| pub fn message_contains(fragment : String) -> RecordPredicate { @utils.message_contains(fragment) } +///| pub fn has_field(key : String) -> RecordPredicate { @utils.has_field(key) } +///| pub fn field_equals(key : String, value : String) -> RecordPredicate { @utils.field_equals(key, value) } +///| pub fn not_(predicate : RecordPredicate) -> RecordPredicate { @utils.not_(predicate) } +///| pub fn all_of(predicates : Array[RecordPredicate]) -> RecordPredicate { @utils.all_of(predicates) } +///| pub fn any_of(predicates : Array[RecordPredicate]) -> RecordPredicate { @utils.any_of(predicates) } diff --git a/src/formatter.mbt b/src/formatter.mbt index 09f871b..9925ce3 100644 --- a/src/formatter.mbt +++ b/src/formatter.mbt @@ -1,103 +1,118 @@ +///| pub type RecordFormatter = @utils.RecordFormatter +///| pub type ColorMode = @utils.ColorMode +///| pub type ColorSupport = @utils.ColorSupport +///| pub type StyleMarkupMode = @utils.StyleMarkupMode +///| pub type TextStyle = @utils.TextStyle +///| pub fn text_style( - fg~ : String? = None, - bg~ : String? = None, - bold~ : Bool = false, - dim~ : Bool = false, - italic~ : Bool = false, - underline~ : Bool = false, + fg? : String? = None, + bg? : String? = None, + bold? : Bool = false, + dim? : Bool = false, + italic? : Bool = false, + underline? : Bool = false, ) -> TextStyle { - @utils.text_style( - fg=fg, - bg=bg, - bold=bold, - dim=dim, - italic=italic, - underline=underline, - ) + @utils.text_style(fg~, bg~, bold~, dim~, italic~, underline~) } +///| pub type StyleTagRegistry = @utils.StyleTagRegistry +///| pub fn style_tag_registry() -> StyleTagRegistry { @utils.style_tag_registry() } +///| pub fn default_style_tag_registry() -> StyleTagRegistry { @utils.default_style_tag_registry() } +///| pub fn global_style_tag_registry() -> StyleTagRegistry { @utils.global_style_tag_registry() } +///| pub fn set_global_style_tag_registry(registry : StyleTagRegistry) -> Unit { @utils.set_global_style_tag_registry(registry) } +///| pub fn reset_global_style_tag_registry() -> Unit { @utils.reset_global_style_tag_registry() } +///| pub type TextFormatter = @utils.TextFormatter +///| pub fn text_formatter( - show_timestamp~ : Bool = true, - show_level~ : Bool = true, - show_target~ : Bool = true, - show_fields~ : Bool = true, - separator~ : String = " ", - field_separator~ : String = " ", - template~ : String = "", - color_mode~ : ColorMode = ColorMode::Never, - color_support~ : ColorSupport = ColorSupport::TrueColor, - style_markup~ : StyleMarkupMode = StyleMarkupMode::Full, - target_style_markup~ : StyleMarkupMode = StyleMarkupMode::Disabled, - fields_style_markup~ : StyleMarkupMode = StyleMarkupMode::Disabled, - style_tags~ : StyleTagRegistry? = None, + show_timestamp? : Bool = true, + show_level? : Bool = true, + show_target? : Bool = true, + show_fields? : Bool = true, + separator? : String = " ", + field_separator? : String = " ", + template? : String = "", + color_mode? : ColorMode = ColorMode::Never, + color_support? : ColorSupport = ColorSupport::TrueColor, + style_markup? : StyleMarkupMode = StyleMarkupMode::Full, + target_style_markup? : StyleMarkupMode = StyleMarkupMode::Disabled, + fields_style_markup? : StyleMarkupMode = StyleMarkupMode::Disabled, + style_tags? : StyleTagRegistry? = None, ) -> TextFormatter { @utils.text_formatter( - show_timestamp=show_timestamp, - show_level=show_level, - show_target=show_target, - show_fields=show_fields, - separator=separator, - field_separator=field_separator, - template=template, - color_mode=color_mode, - color_support=color_support, - style_markup=style_markup, - target_style_markup=target_style_markup, - fields_style_markup=fields_style_markup, - style_tags=style_tags, + show_timestamp~, + show_level~, + show_target~, + show_fields~, + separator~, + field_separator~, + template~, + color_mode~, + color_support~, + style_markup~, + target_style_markup~, + fields_style_markup~, + style_tags~, ) } +///| pub fn color_support_label(support : ColorSupport) -> String { @utils.color_support_label(support) } +///| pub fn style_markup_mode_label(mode : StyleMarkupMode) -> String { @utils.style_markup_mode_label(mode) } +///| pub fn color_mode_label(mode : ColorMode) -> String { @utils.color_mode_label(mode) } -pub fn format_text(rec : Record, formatter~ : TextFormatter = text_formatter()) -> String { - @utils.format_text(rec, formatter=formatter) +///| +pub fn format_text( + rec : Record, + formatter? : TextFormatter = text_formatter(), +) -> String { + @utils.format_text(rec, formatter~) } +///| pub fn format_json(rec : Record) -> String { @utils.format_json(rec) } diff --git a/src/global.mbt b/src/global.mbt index 064f279..25d7017 100644 --- a/src/global.mbt +++ b/src/global.mbt @@ -1,15 +1,27 @@ +///| let default_console_sink : ConsoleSink = console_sink() + +///| let default_min_level_ref : Ref[Level] = Ref(Level::Info) + +///| let default_target_ref : Ref[String] = Ref("") +///| pub fn set_default_min_level(level : Level) -> Unit { default_min_level_ref.val = level } +///| pub fn set_default_target(target : String) -> Unit { default_target_ref.val = target } +///| pub fn default_logger() -> Logger[ConsoleSink] { - Logger::new(default_console_sink, min_level=default_min_level_ref.val, target=default_target_ref.val) + Logger::new( + default_console_sink, + min_level=default_min_level_ref.val, + target=default_target_ref.val, + ) } diff --git a/src/global_emitters.mbt b/src/global_emitters.mbt index 7690632..942bd12 100644 --- a/src/global_emitters.mbt +++ b/src/global_emitters.mbt @@ -1,23 +1,33 @@ -pub fn log(level : Level, message : String, fields~ : Array[Field] = []) -> Unit { - default_logger().log(level, message, fields=fields) +///| +pub fn log( + level : Level, + message : String, + fields? : Array[Field] = [], +) -> Unit { + default_logger().log(level, message, fields~) } -pub fn trace(message : String, fields~ : Array[Field] = []) -> Unit { - default_logger().trace(message, fields=fields) +///| +pub fn trace(message : String, fields? : Array[Field] = []) -> Unit { + default_logger().trace(message, fields~) } -pub fn debug(message : String, fields~ : Array[Field] = []) -> Unit { - default_logger().debug(message, fields=fields) +///| +pub fn debug(message : String, fields? : Array[Field] = []) -> Unit { + default_logger().debug(message, fields~) } -pub fn info(message : String, fields~ : Array[Field] = []) -> Unit { - default_logger().info(message, fields=fields) +///| +pub fn info(message : String, fields? : Array[Field] = []) -> Unit { + default_logger().info(message, fields~) } -pub fn warn(message : String, fields~ : Array[Field] = []) -> Unit { - default_logger().warn(message, fields=fields) +///| +pub fn warn(message : String, fields? : Array[Field] = []) -> Unit { + default_logger().warn(message, fields~) } -pub fn error(message : String, fields~ : Array[Field] = []) -> Unit { - default_logger().error(message, fields=fields) +///| +pub fn error(message : String, fields? : Array[Field] = []) -> Unit { + default_logger().error(message, fields~) } diff --git a/src/level.mbt b/src/level.mbt index 0f76378..861e7ff 100644 --- a/src/level.mbt +++ b/src/level.mbt @@ -1 +1,2 @@ +///| pub type Level = @core.Level diff --git a/src/library_logger.mbt b/src/library_logger.mbt index 08f3569..a77df7b 100644 --- a/src/library_logger.mbt +++ b/src/library_logger.mbt @@ -1,49 +1,70 @@ +///| pub struct LibraryLogger[S] { inner : Logger[S] } +///| fn[S] library_logger(logger : Logger[S]) -> LibraryLogger[S] { { inner: logger } } +///| pub fn[S] Logger::to_library_logger(self : Logger[S]) -> LibraryLogger[S] { library_logger(self) } +///| pub fn[S] LibraryLogger::new( sink : S, - min_level~ : Level = Level::Info, - target~ : String = "", + min_level? : Level = Level::Info, + target? : String = "", ) -> LibraryLogger[S] { - library_logger(Logger::new(sink, min_level=min_level, target=target)) + library_logger(Logger::new(sink, min_level~, target~)) } +///| pub fn[S] LibraryLogger::to_logger(self : LibraryLogger[S]) -> Logger[S] { self.inner } -fn configured_library_logger(logger : ConfiguredLogger) -> LibraryLogger[RuntimeSink] { +///| +fn configured_library_logger( + logger : ConfiguredLogger, +) -> LibraryLogger[RuntimeSink] { library_logger(logger) } -pub fn build_library_logger(config : LoggerConfig) -> LibraryLogger[RuntimeSink] { +///| +pub fn build_library_logger( + config : LoggerConfig, +) -> LibraryLogger[RuntimeSink] { configured_library_logger(build_logger(config)) } +///| pub fn parse_and_build_library_logger( input : String, ) -> LibraryLogger[RuntimeSink] raise ConfigError { configured_library_logger(parse_and_build_logger(input)) } -pub fn[S] LibraryLogger::with_target(self : LibraryLogger[S], target : String) -> LibraryLogger[S] { +///| +pub fn[S] LibraryLogger::with_target( + self : LibraryLogger[S], + target : String, +) -> LibraryLogger[S] { library_logger(self.inner.with_target(target)) } -pub fn[S] LibraryLogger::child(self : LibraryLogger[S], target : String) -> LibraryLogger[S] { +///| +pub fn[S] LibraryLogger::child( + self : LibraryLogger[S], + target : String, +) -> LibraryLogger[S] { library_logger(self.inner.child(target)) } +///| pub fn[S] LibraryLogger::with_context_fields( self : LibraryLogger[S], fields : Array[Field], @@ -51,6 +72,7 @@ pub fn[S] LibraryLogger::with_context_fields( library_logger(self.inner.with_context_fields(fields)) } +///| pub fn[S] LibraryLogger::bind( self : LibraryLogger[S], fields : Array[Field], @@ -58,44 +80,53 @@ pub fn[S] LibraryLogger::bind( self.with_context_fields(fields) } -pub fn[S] LibraryLogger::is_enabled(self : LibraryLogger[S], level : Level) -> Bool { +///| +pub fn[S] LibraryLogger::is_enabled( + self : LibraryLogger[S], + level : Level, +) -> Bool { self.inner.is_enabled(level) } +///| pub fn[S : Sink] LibraryLogger::log( self : LibraryLogger[S], level : Level, message : String, - fields~ : Array[Field] = [], + fields? : Array[Field] = [], target? : String = "", ) -> Unit { - self.inner.log(level, message, fields=fields, target=target) + self.inner.log(level, message, fields~, target~) } +///| pub fn[S : Sink] LibraryLogger::info( self : LibraryLogger[S], message : String, - fields~ : Array[Field] = [], + fields? : Array[Field] = [], ) -> Unit { - self.inner.info(message, fields=fields) + self.inner.info(message, fields~) } +///| pub fn[S : Sink] LibraryLogger::warn( self : LibraryLogger[S], message : String, - fields~ : Array[Field] = [], + fields? : Array[Field] = [], ) -> Unit { - self.inner.warn(message, fields=fields) + self.inner.warn(message, fields~) } +///| pub fn[S : Sink] LibraryLogger::error( self : LibraryLogger[S], message : String, - fields~ : Array[Field] = [], + fields? : Array[Field] = [], ) -> Unit { - self.inner.error(message, fields=fields) + self.inner.error(message, fields~) } +///| pub fn default_library_logger() -> LibraryLogger[ConsoleSink] { library_logger(default_logger()) } diff --git a/src/logger.mbt b/src/logger.mbt index 857ae6e..3b638ec 100644 --- a/src/logger.mbt +++ b/src/logger.mbt @@ -1,3 +1,4 @@ +///| pub struct Logger[S] { min_level : Level sink : S @@ -5,14 +6,21 @@ pub struct Logger[S] { timestamp : Bool } -pub fn[S] Logger::new(sink : S, min_level~ : Level = Level::Info, target~ : String = "") -> Logger[S] { +///| +pub fn[S] Logger::new( + sink : S, + min_level? : Level = Level::Info, + target? : String = "", +) -> Logger[S] { { min_level, sink, target, timestamp: false } } +///| pub fn[S] Logger::with_target(self : Logger[S], target : String) -> Logger[S] { - { ..self, target } + { ..self, target, } } +///| fn combine_targets(parent : String, child : String) -> String { if parent == "" { child @@ -23,11 +31,16 @@ fn combine_targets(parent : String, child : String) -> String { } } +///| pub fn[S] Logger::child(self : Logger[S], target : String) -> Logger[S] { { ..self, target: combine_targets(self.target, target) } } -pub fn[S] Logger::with_context_fields(self : Logger[S], fields : Array[Field]) -> Logger[ContextSink[S]] { +///| +pub fn[S] Logger::with_context_fields( + self : Logger[S], + fields : Array[Field], +) -> Logger[ContextSink[S]] { { min_level: self.min_level, sink: ContextSink::{ sink: self.sink, context_fields: fields }, @@ -36,11 +49,19 @@ pub fn[S] Logger::with_context_fields(self : Logger[S], fields : Array[Field]) - } } -pub fn[S] Logger::bind(self : Logger[S], fields : Array[Field]) -> Logger[ContextSink[S]] { +///| +pub fn[S] Logger::bind( + self : Logger[S], + fields : Array[Field], +) -> Logger[ContextSink[S]] { self.with_context_fields(fields) } -pub fn[S] Logger::with_filter(self : Logger[S], predicate : (Record) -> Bool) -> Logger[FilterSink[S]] { +///| +pub fn[S] Logger::with_filter( + self : Logger[S], + predicate : (Record) -> Bool, +) -> Logger[FilterSink[S]] { { min_level: self.min_level, sink: filter_sink(self.sink, predicate), @@ -49,7 +70,11 @@ pub fn[S] Logger::with_filter(self : Logger[S], predicate : (Record) -> Bool) -> } } -pub fn[S] Logger::with_patch(self : Logger[S], patch : RecordPatch) -> Logger[PatchSink[S]] { +///| +pub fn[S] Logger::with_patch( + self : Logger[S], + patch : RecordPatch, +) -> Logger[PatchSink[S]] { { min_level: self.min_level, sink: patch_sink(self.sink, patch), @@ -58,27 +83,37 @@ pub fn[S] Logger::with_patch(self : Logger[S], patch : RecordPatch) -> Logger[Pa } } +///| pub fn[S] Logger::with_queue( self : Logger[S], - max_pending~ : Int = 0, - overflow~ : QueueOverflowPolicy = QueueOverflowPolicy::DropNewest, + max_pending? : Int = 0, + overflow? : QueueOverflowPolicy = QueueOverflowPolicy::DropNewest, ) -> Logger[QueuedSink[S]] { { min_level: self.min_level, - sink: queued_sink(self.sink, max_pending=max_pending, overflow=overflow), + sink: queued_sink(self.sink, max_pending~, overflow~), target: self.target, timestamp: self.timestamp, } } -pub fn[S] Logger::with_min_level(self : Logger[S], min_level : Level) -> Logger[S] { - { ..self, min_level } +///| +pub fn[S] Logger::with_min_level( + self : Logger[S], + min_level : Level, +) -> Logger[S] { + { ..self, min_level, } } -pub fn[S] Logger::with_timestamp(self : Logger[S], enabled~ : Bool = true) -> Logger[S] { +///| +pub fn[S] Logger::with_timestamp( + self : Logger[S], + enabled? : Bool = true, +) -> Logger[S] { { ..self, timestamp: enabled } } +///| pub fn[S] Logger::is_enabled(self : Logger[S], level : Level) -> Bool { level.enabled(self.min_level) } diff --git a/src/logger_emitters.mbt b/src/logger_emitters.mbt index ed82478..2c19f84 100644 --- a/src/logger_emitters.mbt +++ b/src/logger_emitters.mbt @@ -1,8 +1,9 @@ +///| pub fn[S : Sink] Logger::log( self : Logger[S], level : Level, message : String, - fields~ : Array[Field] = [], + fields? : Array[Field] = [], target? : String = "", ) -> Unit { if !self.is_enabled(level) { @@ -11,27 +12,52 @@ pub fn[S : Sink] Logger::log( let actual_target = if target == "" { self.target } else { target } let timestamp_ms = if self.timestamp { @env.now() } else { 0UL } self.sink.write( - record(level, message, timestamp_ms=timestamp_ms, target=actual_target, fields=fields), + record(level, message, timestamp_ms~, target=actual_target, fields~), ) } } -pub fn[S : Sink] Logger::trace(self : Logger[S], message : String, fields~ : Array[Field] = []) -> Unit { - self.log(Level::Trace, message, fields=fields) +///| +pub fn[S : Sink] Logger::trace( + self : Logger[S], + message : String, + fields? : Array[Field] = [], +) -> Unit { + self.log(Level::Trace, message, fields~) } -pub fn[S : Sink] Logger::debug(self : Logger[S], message : String, fields~ : Array[Field] = []) -> Unit { - self.log(Level::Debug, message, fields=fields) +///| +pub fn[S : Sink] Logger::debug( + self : Logger[S], + message : String, + fields? : Array[Field] = [], +) -> Unit { + self.log(Level::Debug, message, fields~) } -pub fn[S : Sink] Logger::info(self : Logger[S], message : String, fields~ : Array[Field] = []) -> Unit { - self.log(Level::Info, message, fields=fields) +///| +pub fn[S : Sink] Logger::info( + self : Logger[S], + message : String, + fields? : Array[Field] = [], +) -> Unit { + self.log(Level::Info, message, fields~) } -pub fn[S : Sink] Logger::warn(self : Logger[S], message : String, fields~ : Array[Field] = []) -> Unit { - self.log(Level::Warn, message, fields=fields) +///| +pub fn[S : Sink] Logger::warn( + self : Logger[S], + message : String, + fields? : Array[Field] = [], +) -> Unit { + self.log(Level::Warn, message, fields~) } -pub fn[S : Sink] Logger::error(self : Logger[S], message : String, fields~ : Array[Field] = []) -> Unit { - self.log(Level::Error, message, fields=fields) +///| +pub fn[S : Sink] Logger::error( + self : Logger[S], + message : String, + fields? : Array[Field] = [], +) -> Unit { + self.log(Level::Error, message, fields~) } diff --git a/src/moon.pkg b/src/moon.pkg index feabc2f..2e1ef26 100644 --- a/src/moon.pkg +++ b/src/moon.pkg @@ -1,12 +1,12 @@ import { - "Nanaloveyuki/BitLogger/src/core" @core, - "Nanaloveyuki/BitLogger/src/utils" @utils, - "maria/json_parser" @json_parser, + "Nanaloveyuki/BitLogger/src/core", + "Nanaloveyuki/BitLogger/src/utils", + "maria/json_parser", "moonbitlang/core/array", "moonbitlang/core/builtin", - "moonbitlang/core/env" @env, + "moonbitlang/core/env", "moonbitlang/core/json", - "moonbitlang/core/queue" @queue, + "moonbitlang/core/queue", "moonbitlang/core/ref", } diff --git a/src/patchers.mbt b/src/patchers.mbt index e51a042..2cafa78 100644 --- a/src/patchers.mbt +++ b/src/patchers.mbt @@ -1,29 +1,40 @@ +///| pub type RecordPatch = @utils.RecordPatch +///| pub fn identity_patch() -> RecordPatch { @utils.identity_patch() } +///| pub fn set_target(target : String) -> RecordPatch { @utils.set_target(target) } +///| pub fn prefix_message(prefix : String) -> RecordPatch { @utils.prefix_message(prefix) } +///| pub fn append_fields(extra_fields : Array[Field]) -> RecordPatch { @utils.append_fields(extra_fields) } -pub fn redact_field(key : String, placeholder~ : String = "***") -> RecordPatch { - @utils.redact_field(key, placeholder=placeholder) +///| +pub fn redact_field(key : String, placeholder? : String = "***") -> RecordPatch { + @utils.redact_field(key, placeholder~) } -pub fn redact_fields(keys : Array[String], placeholder~ : String = "***") -> RecordPatch { - @utils.redact_fields(keys, placeholder=placeholder) +///| +pub fn redact_fields( + keys : Array[String], + placeholder? : String = "***", +) -> RecordPatch { + @utils.redact_fields(keys, placeholder~) } +///| pub fn compose_patches(patches : Array[RecordPatch]) -> RecordPatch { @utils.compose_patches(patches) } diff --git a/src/presets.mbt b/src/presets.mbt index 026285e..d8cd28c 100644 --- a/src/presets.mbt +++ b/src/presets.mbt @@ -1,89 +1,95 @@ +///| pub fn console( - min_level~ : Level = Level::Info, - target~ : String = "", - timestamp~ : Bool = false, + min_level? : Level = Level::Info, + target? : String = "", + timestamp? : Bool = false, ) -> LoggerConfig { LoggerConfig::new( - min_level=min_level, - target=target, - timestamp=timestamp, + min_level~, + target~, + timestamp~, sink=SinkConfig::new(kind=SinkKind::Console), ) } +///| pub fn json_console( - min_level~ : Level = Level::Info, - target~ : String = "", - timestamp~ : Bool = false, + min_level? : Level = Level::Info, + target? : String = "", + timestamp? : Bool = false, ) -> LoggerConfig { LoggerConfig::new( - min_level=min_level, - target=target, - timestamp=timestamp, + min_level~, + target~, + timestamp~, sink=SinkConfig::new(kind=SinkKind::JsonConsole), ) } +///| pub fn text_console( - min_level~ : Level = Level::Info, - target~ : String = "", - timestamp~ : Bool = false, - text_formatter~ : TextFormatterConfig = default_text_formatter_config(), + min_level? : Level = Level::Info, + target? : String = "", + timestamp? : Bool = false, + text_formatter? : TextFormatterConfig = default_text_formatter_config(), ) -> LoggerConfig { LoggerConfig::new( - min_level=min_level, - target=target, - timestamp=timestamp, - sink=SinkConfig::new(kind=SinkKind::TextConsole, text_formatter=text_formatter), + min_level~, + target~, + timestamp~, + sink=SinkConfig::new(kind=SinkKind::TextConsole, text_formatter~), ) } +///| pub fn file( path : String, - min_level~ : Level = Level::Info, - target~ : String = "", - timestamp~ : Bool = false, - append~ : Bool = true, - auto_flush~ : Bool = true, - rotation~ : FileRotation? = None, - text_formatter~ : TextFormatterConfig = default_text_formatter_config(), + min_level? : Level = Level::Info, + target? : String = "", + timestamp? : Bool = false, + append? : Bool = true, + auto_flush? : Bool = true, + rotation? : FileRotation? = None, + text_formatter? : TextFormatterConfig = default_text_formatter_config(), ) -> LoggerConfig raise ConfigError { if path == "" { raise ConfigError::InvalidConfig("File sink requires non-empty path") } LoggerConfig::new( - min_level=min_level, - target=target, - timestamp=timestamp, + min_level~, + target~, + timestamp~, sink=SinkConfig::new( kind=SinkKind::File, - path=path, - append=append, - auto_flush=auto_flush, - rotation=rotation, - text_formatter=text_formatter, + path~, + append~, + auto_flush~, + rotation~, + text_formatter~, ), ) } +///| pub fn with_queue( config : LoggerConfig, - max_pending~ : Int = 0, - overflow~ : QueueOverflowPolicy = QueueOverflowPolicy::DropNewest, + max_pending? : Int = 0, + overflow? : QueueOverflowPolicy = QueueOverflowPolicy::DropNewest, ) -> LoggerConfig { LoggerConfig::new( min_level=config.min_level, target=config.target, timestamp=config.timestamp, sink=config.sink, - queue=Some(QueueConfig::new(max_pending, overflow=overflow)), + queue=Some(QueueConfig::new(max_pending, overflow~)), ) } +///| pub fn with_file_rotation( config : LoggerConfig, max_bytes : Int, - max_backups~ : Int = 1, + max_backups? : Int = 1, ) -> LoggerConfig { match config.sink.kind { SinkKind::File => @@ -96,7 +102,7 @@ pub fn with_file_rotation( path=config.sink.path, append=config.sink.append, auto_flush=config.sink.auto_flush, - rotation=Some(file_rotation(max_bytes, max_backups=max_backups)), + rotation=Some(file_rotation(max_bytes, max_backups~)), text_formatter=config.sink.text_formatter, ), queue=config.queue, diff --git a/src/presets_test.mbt b/src/presets_test.mbt index 189de4a..e00c4ae 100644 --- a/src/presets_test.mbt +++ b/src/presets_test.mbt @@ -1,39 +1,56 @@ +///| test "logger config presets use expected defaults" { let console_config = console() inspect(console_config.min_level.label(), content="INFO") inspect(console_config.target, content="") inspect(console_config.timestamp, content="false") - inspect(match console_config.sink.kind { - SinkKind::Console => "Console" - _ => "other" - }, content="Console") + inspect( + match console_config.sink.kind { + SinkKind::Console => "Console" + _ => "other" + }, + content="Console", + ) inspect(console_config.queue is None, content="true") let json_config = json_console() - inspect(match json_config.sink.kind { - SinkKind::JsonConsole => "JsonConsole" - _ => "other" - }, content="JsonConsole") + inspect( + match json_config.sink.kind { + SinkKind::JsonConsole => "JsonConsole" + _ => "other" + }, + content="JsonConsole", + ) inspect(json_config.queue is None, content="true") let text_config = text_console() - inspect(match text_config.sink.kind { - SinkKind::TextConsole => "TextConsole" - _ => "other" - }, content="TextConsole") + inspect( + match text_config.sink.kind { + SinkKind::TextConsole => "TextConsole" + _ => "other" + }, + content="TextConsole", + ) inspect(text_config.sink.text_formatter.show_timestamp, content="true") - inspect(color_mode_label(text_config.sink.text_formatter.color_mode), content="never") + inspect( + color_mode_label(text_config.sink.text_formatter.color_mode), + content="never", + ) } +///| test "file preset uses file sink defaults" { let config = file("bitlogger.log") inspect(config.min_level.label(), content="INFO") inspect(config.target, content="") inspect(config.timestamp, content="false") - inspect(match config.sink.kind { - SinkKind::File => "File" - _ => "other" - }, content="File") + inspect( + match config.sink.kind { + SinkKind::File => "File" + _ => "other" + }, + content="File", + ) inspect(config.sink.path, content="bitlogger.log") inspect(config.sink.append, content="true") inspect(config.sink.auto_flush, content="true") @@ -41,6 +58,7 @@ test "file preset uses file sink defaults" { inspect(config.queue is None, content="true") } +///| test "file preset rejects empty path like parser file sink config" { let preset_error = (fn() -> String raise ConfigError { ignore(file("")) @@ -51,7 +69,9 @@ test "file preset rejects empty path like parser file sink config" { inspect(preset_error, content="File sink requires non-empty path") let parser_error = (fn() -> String raise ConfigError { - ignore(parse_logger_config_text("{\"sink\":{\"kind\":\"file\",\"path\":\"\"}}")) + ignore( + parse_logger_config_text("{\"sink\":{\"kind\":\"file\",\"path\":\"\"}}"), + ) "no error" })() catch { ConfigError::InvalidConfig(message) => message @@ -59,8 +79,12 @@ test "file preset rejects empty path like parser file sink config" { inspect(parser_error, content="File sink requires non-empty path") } +///| test "preset helpers compose queue and file rotation without losing config" { - let formatter = TextFormatterConfig::new(show_timestamp=false, separator=" | ") + let formatter = TextFormatterConfig::new( + show_timestamp=false, + separator=" | ", + ) let config = file( "service.log", min_level=Level::Warn, @@ -86,10 +110,13 @@ test "preset helpers compose queue and file rotation without losing config" { match config.queue { Some(queue) => { inspect(queue.max_pending, content="32") - inspect(match queue.overflow { - QueueOverflowPolicy::DropNewest => "DropNewest" - QueueOverflowPolicy::DropOldest => "DropOldest" - }, content="DropOldest") + inspect( + match queue.overflow { + QueueOverflowPolicy::DropNewest => "DropNewest" + QueueOverflowPolicy::DropOldest => "DropOldest" + }, + content="DropOldest", + ) } None => inspect(false, content="true") } @@ -102,6 +129,7 @@ test "preset helpers compose queue and file rotation without losing config" { } } +///| test "queue helper preserves file rotation when applied after rotation" { let config = with_queue( with_file_rotation(file("service.log", append=false), 256, max_backups=2), @@ -113,10 +141,13 @@ test "queue helper preserves file rotation when applied after rotation" { match config.queue { Some(queue) => { inspect(queue.max_pending, content="8") - inspect(match queue.overflow { - QueueOverflowPolicy::DropNewest => "DropNewest" - QueueOverflowPolicy::DropOldest => "DropOldest" - }, content="DropNewest") + inspect( + match queue.overflow { + QueueOverflowPolicy::DropNewest => "DropNewest" + QueueOverflowPolicy::DropOldest => "DropOldest" + }, + content="DropNewest", + ) } None => inspect(false, content="true") } @@ -129,29 +160,44 @@ test "queue helper preserves file rotation when applied after rotation" { } } +///| test "file rotation helper leaves non-file presets unchanged" { - let console_config = with_file_rotation(console(target="console"), 64, max_backups=2) - inspect(match console_config.sink.kind { - SinkKind::Console => "Console" - _ => "other" - }, content="Console") + let console_config = with_file_rotation( + console(target="console"), + 64, + max_backups=2, + ) + inspect( + match console_config.sink.kind { + SinkKind::Console => "Console" + _ => "other" + }, + content="Console", + ) inspect(console_config.target, content="console") inspect(console_config.sink.rotation is None, content="true") let text_config = with_file_rotation( - text_console(target="text", text_formatter=TextFormatterConfig::new(separator=" :: ")), + text_console( + target="text", + text_formatter=TextFormatterConfig::new(separator=" :: "), + ), 96, max_backups=4, ) - inspect(match text_config.sink.kind { - SinkKind::TextConsole => "TextConsole" - _ => "other" - }, content="TextConsole") + inspect( + match text_config.sink.kind { + SinkKind::TextConsole => "TextConsole" + _ => "other" + }, + content="TextConsole", + ) inspect(text_config.target, content="text") inspect(text_config.sink.text_formatter.separator, content=" :: ") inspect(text_config.sink.rotation is None, content="true") } +///| test "file rotation helper keeps queued non-file presets unchanged" { let config = with_queue( json_console(min_level=Level::Error, target="json"), @@ -161,18 +207,24 @@ test "file rotation helper keeps queued non-file presets unchanged" { let rotated = with_file_rotation(config, 512, max_backups=5) inspect(rotated.min_level.label(), content="ERROR") inspect(rotated.target, content="json") - inspect(match rotated.sink.kind { - SinkKind::JsonConsole => "JsonConsole" - _ => "other" - }, content="JsonConsole") + inspect( + match rotated.sink.kind { + SinkKind::JsonConsole => "JsonConsole" + _ => "other" + }, + content="JsonConsole", + ) inspect(rotated.sink.rotation is None, content="true") match rotated.queue { Some(queue) => { inspect(queue.max_pending, content="4") - inspect(match queue.overflow { - QueueOverflowPolicy::DropNewest => "DropNewest" - QueueOverflowPolicy::DropOldest => "DropOldest" - }, content="DropOldest") + inspect( + match queue.overflow { + QueueOverflowPolicy::DropNewest => "DropNewest" + QueueOverflowPolicy::DropOldest => "DropOldest" + }, + content="DropOldest", + ) } None => inspect(false, content="true") } diff --git a/src/record.mbt b/src/record.mbt index 89e3ad8..b90c092 100644 --- a/src/record.mbt +++ b/src/record.mbt @@ -1,21 +1,26 @@ +///| pub type Field = @core.Field +///| pub fn field(key : String, value : String) -> Field { @core.field(key, value) } +///| pub fn fields(entries : Array[(String, String)]) -> Array[Field] { @core.fields(entries) } +///| pub type Record = @core.Record +///| fn record( level : Level, message : String, - timestamp_ms~ : UInt64 = 0UL, - target~ : String = "", - fields~ : Array[Field] = [], + timestamp_ms? : UInt64 = 0UL, + target? : String = "", + fields? : Array[Field] = [], ) -> Record { - @core.Record::new(level, message, timestamp_ms=timestamp_ms, target=target, fields=fields) + @core.Record::new(level, message, timestamp_ms~, target~, fields~) } diff --git a/src/runtime_file_controls.mbt b/src/runtime_file_controls.mbt index 05fff5b..45f6a33 100644 --- a/src/runtime_file_controls.mbt +++ b/src/runtime_file_controls.mbt @@ -1,3 +1,4 @@ +///| pub fn RuntimeSink::file_available(self : RuntimeSink) -> Bool { match self { File(sink) => sink.is_available() @@ -6,14 +7,19 @@ pub fn RuntimeSink::file_available(self : RuntimeSink) -> Bool { } } -pub fn RuntimeSink::file_reopen(self : RuntimeSink, append~ : Bool? = None) -> Bool { +///| +pub fn RuntimeSink::file_reopen( + self : RuntimeSink, + append? : Bool? = None, +) -> Bool { match self { - File(sink) => sink.reopen(append=append) - QueuedFile(sink) => sink.sink.reopen(append=append) + File(sink) => sink.reopen(append~) + QueuedFile(sink) => sink.sink.reopen(append~) _ => false } } +///| pub fn RuntimeSink::file_reopen_with_current_policy(self : RuntimeSink) -> Bool { match self { File(sink) => sink.reopen_with_current_policy() @@ -22,6 +28,7 @@ pub fn RuntimeSink::file_reopen_with_current_policy(self : RuntimeSink) -> Bool } } +///| pub fn RuntimeSink::file_reopen_append(self : RuntimeSink) -> Bool { match self { File(sink) => sink.reopen_append() @@ -30,6 +37,7 @@ pub fn RuntimeSink::file_reopen_append(self : RuntimeSink) -> Bool { } } +///| pub fn RuntimeSink::file_reopen_truncate(self : RuntimeSink) -> Bool { match self { File(sink) => sink.reopen_truncate() @@ -38,6 +46,7 @@ pub fn RuntimeSink::file_reopen_truncate(self : RuntimeSink) -> Bool { } } +///| pub fn RuntimeSink::file_append_mode(self : RuntimeSink) -> Bool { match self { File(sink) => sink.append_mode() @@ -46,7 +55,11 @@ pub fn RuntimeSink::file_append_mode(self : RuntimeSink) -> Bool { } } -pub fn RuntimeSink::file_set_append_mode(self : RuntimeSink, append : Bool) -> Bool { +///| +pub fn RuntimeSink::file_set_append_mode( + self : RuntimeSink, + append : Bool, +) -> Bool { match self { File(sink) => { sink.set_append_mode(append) @@ -60,6 +73,7 @@ pub fn RuntimeSink::file_set_append_mode(self : RuntimeSink, append : Bool) -> B } } +///| pub fn RuntimeSink::file_path(self : RuntimeSink) -> String { match self { File(sink) => sink.path() @@ -68,6 +82,7 @@ pub fn RuntimeSink::file_path(self : RuntimeSink) -> String { } } +///| pub fn RuntimeSink::file_auto_flush(self : RuntimeSink) -> Bool { match self { File(sink) => sink.auto_flush_enabled() @@ -76,6 +91,7 @@ pub fn RuntimeSink::file_auto_flush(self : RuntimeSink) -> Bool { } } +///| pub fn RuntimeSink::file_rotation_enabled(self : RuntimeSink) -> Bool { match self { File(sink) => sink.rotation_enabled() @@ -84,6 +100,7 @@ pub fn RuntimeSink::file_rotation_enabled(self : RuntimeSink) -> Bool { } } +///| pub fn RuntimeSink::file_rotation_config(self : RuntimeSink) -> FileRotation? { match self { File(sink) => sink.rotation_config() @@ -92,7 +109,11 @@ pub fn RuntimeSink::file_rotation_config(self : RuntimeSink) -> FileRotation? { } } -pub fn RuntimeSink::file_set_auto_flush(self : RuntimeSink, enabled : Bool) -> Bool { +///| +pub fn RuntimeSink::file_set_auto_flush( + self : RuntimeSink, + enabled : Bool, +) -> Bool { match self { File(sink) => { sink.set_auto_flush(enabled) @@ -106,7 +127,11 @@ pub fn RuntimeSink::file_set_auto_flush(self : RuntimeSink, enabled : Bool) -> B } } -pub fn RuntimeSink::file_set_policy(self : RuntimeSink, policy : FileSinkPolicy) -> Bool { +///| +pub fn RuntimeSink::file_set_policy( + self : RuntimeSink, + policy : FileSinkPolicy, +) -> Bool { match self { File(sink) => { sink.set_policy(policy) @@ -120,7 +145,11 @@ pub fn RuntimeSink::file_set_policy(self : RuntimeSink, policy : FileSinkPolicy) } } -pub fn RuntimeSink::file_set_rotation(self : RuntimeSink, rotation : FileRotation?) -> Bool { +///| +pub fn RuntimeSink::file_set_rotation( + self : RuntimeSink, + rotation : FileRotation?, +) -> Bool { match self { File(sink) => { sink.set_rotation(rotation) @@ -134,6 +163,7 @@ pub fn RuntimeSink::file_set_rotation(self : RuntimeSink, rotation : FileRotatio } } +///| pub fn RuntimeSink::file_clear_rotation(self : RuntimeSink) -> Bool { match self { File(sink) => { @@ -148,6 +178,7 @@ pub fn RuntimeSink::file_clear_rotation(self : RuntimeSink) -> Bool { } } +///| pub fn RuntimeSink::file_flush(self : RuntimeSink) -> Bool { match self { File(sink) => sink.flush() @@ -159,6 +190,7 @@ pub fn RuntimeSink::file_flush(self : RuntimeSink) -> Bool { } } +///| pub fn RuntimeSink::file_close(self : RuntimeSink) -> Bool { match self { File(sink) => sink.close() @@ -170,6 +202,7 @@ pub fn RuntimeSink::file_close(self : RuntimeSink) -> Bool { } } +///| pub fn RuntimeSink::file_open_failures(self : RuntimeSink) -> Int { match self { File(sink) => sink.open_failures() @@ -178,6 +211,7 @@ pub fn RuntimeSink::file_open_failures(self : RuntimeSink) -> Int { } } +///| pub fn RuntimeSink::file_write_failures(self : RuntimeSink) -> Int { match self { File(sink) => sink.write_failures() @@ -186,6 +220,7 @@ pub fn RuntimeSink::file_write_failures(self : RuntimeSink) -> Int { } } +///| pub fn RuntimeSink::file_flush_failures(self : RuntimeSink) -> Int { match self { File(sink) => sink.flush_failures() @@ -194,6 +229,7 @@ pub fn RuntimeSink::file_flush_failures(self : RuntimeSink) -> Int { } } +///| pub fn RuntimeSink::file_rotation_failures(self : RuntimeSink) -> Int { match self { File(sink) => sink.rotation_failures() @@ -202,6 +238,7 @@ pub fn RuntimeSink::file_rotation_failures(self : RuntimeSink) -> Int { } } +///| pub fn RuntimeSink::file_reset_failure_counters(self : RuntimeSink) -> Bool { match self { File(sink) => { @@ -216,6 +253,7 @@ pub fn RuntimeSink::file_reset_failure_counters(self : RuntimeSink) -> Bool { } } +///| pub fn RuntimeSink::file_reset_policy(self : RuntimeSink) -> Bool { match self { File(sink) => { @@ -230,6 +268,7 @@ pub fn RuntimeSink::file_reset_policy(self : RuntimeSink) -> Bool { } } +///| pub fn RuntimeSink::file_policy(self : RuntimeSink) -> FileSinkPolicy { match self { File(sink) => sink.policy() @@ -238,6 +277,7 @@ pub fn RuntimeSink::file_policy(self : RuntimeSink) -> FileSinkPolicy { } } +///| pub fn RuntimeSink::file_default_policy(self : RuntimeSink) -> FileSinkPolicy { match self { File(sink) => sink.default_policy() @@ -246,6 +286,7 @@ pub fn RuntimeSink::file_default_policy(self : RuntimeSink) -> FileSinkPolicy { } } +///| pub fn RuntimeSink::file_policy_matches_default(self : RuntimeSink) -> Bool { match self { File(sink) => sink.policy_matches_default() @@ -254,91 +295,125 @@ pub fn RuntimeSink::file_policy_matches_default(self : RuntimeSink) -> Bool { } } +///| pub fn RuntimeSink::file_state(self : RuntimeSink) -> FileSinkState { match self { File(sink) => sink.state() QueuedFile(sink) => sink.sink.state() - _ => FileSinkState::new( - "", - available=false, - append=false, - auto_flush=false, - rotation=None, - open_failures=0, - write_failures=0, - flush_failures=0, - rotation_failures=0, - ) + _ => + FileSinkState::new( + "", + available=false, + append=false, + auto_flush=false, + rotation=None, + open_failures=0, + write_failures=0, + flush_failures=0, + rotation_failures=0, + ) } } +///| pub fn RuntimeSink::file_runtime_state(self : RuntimeSink) -> RuntimeFileState? { match self { File(sink) => Some(RuntimeFileState::new(sink.state())) - QueuedFile(sink) => Some( - RuntimeFileState::new( - sink.sink.state(), - queued=true, - pending_count=sink.pending_count(), - dropped_count=sink.dropped_count(), - ), - ) + QueuedFile(sink) => + Some( + RuntimeFileState::new( + sink.sink.state(), + queued=true, + pending_count=sink.pending_count(), + dropped_count=sink.dropped_count(), + ), + ) _ => None } } +///| pub fn ConfiguredLogger::file_available(self : ConfiguredLogger) -> Bool { self.sink.file_available() } -pub fn ConfiguredLogger::file_reopen(self : ConfiguredLogger, append~ : Bool? = None) -> Bool { - self.sink.file_reopen(append=append) +///| +pub fn ConfiguredLogger::file_reopen( + self : ConfiguredLogger, + append? : Bool? = None, +) -> Bool { + self.sink.file_reopen(append~) } -pub fn ConfiguredLogger::file_reopen_with_current_policy(self : ConfiguredLogger) -> Bool { +///| +pub fn ConfiguredLogger::file_reopen_with_current_policy( + self : ConfiguredLogger, +) -> Bool { self.sink.file_reopen_with_current_policy() } +///| pub fn ConfiguredLogger::file_reopen_append(self : ConfiguredLogger) -> Bool { self.sink.file_reopen_append() } +///| pub fn ConfiguredLogger::file_reopen_truncate(self : ConfiguredLogger) -> Bool { self.sink.file_reopen_truncate() } +///| pub fn ConfiguredLogger::file_append_mode(self : ConfiguredLogger) -> Bool { self.sink.file_append_mode() } -pub fn ConfiguredLogger::file_set_append_mode(self : ConfiguredLogger, append : Bool) -> Bool { +///| +pub fn ConfiguredLogger::file_set_append_mode( + self : ConfiguredLogger, + append : Bool, +) -> Bool { self.sink.file_set_append_mode(append) } +///| pub fn ConfiguredLogger::file_path(self : ConfiguredLogger) -> String { self.sink.file_path() } +///| pub fn ConfiguredLogger::file_auto_flush(self : ConfiguredLogger) -> Bool { self.sink.file_auto_flush() } +///| pub fn ConfiguredLogger::file_rotation_enabled(self : ConfiguredLogger) -> Bool { self.sink.file_rotation_enabled() } -pub fn ConfiguredLogger::file_rotation_config(self : ConfiguredLogger) -> FileRotation? { +///| +pub fn ConfiguredLogger::file_rotation_config( + self : ConfiguredLogger, +) -> FileRotation? { self.sink.file_rotation_config() } -pub fn ConfiguredLogger::file_set_auto_flush(self : ConfiguredLogger, enabled : Bool) -> Bool { +///| +pub fn ConfiguredLogger::file_set_auto_flush( + self : ConfiguredLogger, + enabled : Bool, +) -> Bool { self.sink.file_set_auto_flush(enabled) } -pub fn ConfiguredLogger::file_set_policy(self : ConfiguredLogger, policy : FileSinkPolicy) -> Bool { +///| +pub fn ConfiguredLogger::file_set_policy( + self : ConfiguredLogger, + policy : FileSinkPolicy, +) -> Bool { self.sink.file_set_policy(policy) } +///| pub fn ConfiguredLogger::file_set_rotation( self : ConfiguredLogger, rotation : FileRotation?, @@ -346,58 +421,80 @@ pub fn ConfiguredLogger::file_set_rotation( self.sink.file_set_rotation(rotation) } +///| pub fn ConfiguredLogger::file_clear_rotation(self : ConfiguredLogger) -> Bool { self.sink.file_clear_rotation() } +///| pub fn ConfiguredLogger::file_flush(self : ConfiguredLogger) -> Bool { self.sink.file_flush() } +///| pub fn ConfiguredLogger::file_close(self : ConfiguredLogger) -> Bool { self.sink.file_close() } +///| pub fn ConfiguredLogger::file_open_failures(self : ConfiguredLogger) -> Int { self.sink.file_open_failures() } +///| pub fn ConfiguredLogger::file_write_failures(self : ConfiguredLogger) -> Int { self.sink.file_write_failures() } +///| pub fn ConfiguredLogger::file_flush_failures(self : ConfiguredLogger) -> Int { self.sink.file_flush_failures() } +///| pub fn ConfiguredLogger::file_rotation_failures(self : ConfiguredLogger) -> Int { self.sink.file_rotation_failures() } -pub fn ConfiguredLogger::file_reset_failure_counters(self : ConfiguredLogger) -> Bool { +///| +pub fn ConfiguredLogger::file_reset_failure_counters( + self : ConfiguredLogger, +) -> Bool { self.sink.file_reset_failure_counters() } +///| pub fn ConfiguredLogger::file_reset_policy(self : ConfiguredLogger) -> Bool { self.sink.file_reset_policy() } +///| pub fn ConfiguredLogger::file_policy(self : ConfiguredLogger) -> FileSinkPolicy { self.sink.file_policy() } -pub fn ConfiguredLogger::file_default_policy(self : ConfiguredLogger) -> FileSinkPolicy { +///| +pub fn ConfiguredLogger::file_default_policy( + self : ConfiguredLogger, +) -> FileSinkPolicy { self.sink.file_default_policy() } -pub fn ConfiguredLogger::file_policy_matches_default(self : ConfiguredLogger) -> Bool { +///| +pub fn ConfiguredLogger::file_policy_matches_default( + self : ConfiguredLogger, +) -> Bool { self.sink.file_policy_matches_default() } +///| pub fn ConfiguredLogger::file_state(self : ConfiguredLogger) -> FileSinkState { self.sink.file_state() } -pub fn ConfiguredLogger::file_runtime_state(self : ConfiguredLogger) -> RuntimeFileState? { +///| +pub fn ConfiguredLogger::file_runtime_state( + self : ConfiguredLogger, +) -> RuntimeFileState? { self.sink.file_runtime_state() } diff --git a/src/runtime_logger.mbt b/src/runtime_logger.mbt index c118df5..a7f988b 100644 --- a/src/runtime_logger.mbt +++ b/src/runtime_logger.mbt @@ -1,3 +1,4 @@ +///| pub(all) enum RuntimeSink { Console(ConsoleSink) JsonConsole(JsonConsoleSink) @@ -9,33 +10,54 @@ pub(all) enum RuntimeSink { QueuedFile(QueuedSink[FileSink]) } +///| pub type RuntimeFileState = @utils.RuntimeFileState -pub fn file_sink_policy_to_json(policy : FileSinkPolicy) -> @json_parser.JsonValue { +///| +pub fn file_sink_policy_to_json( + policy : FileSinkPolicy, +) -> @json_parser.JsonValue { @utils.file_sink_policy_to_json(policy) } -pub fn stringify_file_sink_policy(policy : FileSinkPolicy, pretty~ : Bool = false) -> String { - @utils.stringify_file_sink_policy(policy, pretty=pretty) +///| +pub fn stringify_file_sink_policy( + policy : FileSinkPolicy, + pretty? : Bool = false, +) -> String { + @utils.stringify_file_sink_policy(policy, pretty~) } +///| pub fn file_sink_state_to_json(state : FileSinkState) -> @json_parser.JsonValue { @utils.file_sink_state_to_json(state) } -pub fn stringify_file_sink_state(state : FileSinkState, pretty~ : Bool = false) -> String { - @utils.stringify_file_sink_state(state, pretty=pretty) +///| +pub fn stringify_file_sink_state( + state : FileSinkState, + pretty? : Bool = false, +) -> String { + @utils.stringify_file_sink_state(state, pretty~) } -pub fn runtime_file_state_to_json(state : RuntimeFileState) -> @json_parser.JsonValue { +///| +pub fn runtime_file_state_to_json( + state : RuntimeFileState, +) -> @json_parser.JsonValue { @utils.runtime_file_state_to_json(state) } -pub fn stringify_runtime_file_state(state : RuntimeFileState, pretty~ : Bool = false) -> String { - @utils.stringify_runtime_file_state(state, pretty=pretty) +///| +pub fn stringify_runtime_file_state( + state : RuntimeFileState, + pretty? : Bool = false, +) -> String { + @utils.stringify_runtime_file_state(state, pretty~) } -pub impl Sink for RuntimeSink with write(self, rec) { +///| +pub impl Sink for RuntimeSink with fn write(self, rec) { match self { Console(sink) => sink.write(rec) JsonConsole(sink) => sink.write(rec) @@ -48,6 +70,7 @@ pub impl Sink for RuntimeSink with write(self, rec) { } } +///| pub fn RuntimeSink::flush(self : RuntimeSink) -> Int { match self { Console(_) => 0 @@ -61,19 +84,21 @@ pub fn RuntimeSink::flush(self : RuntimeSink) -> Int { } } -pub fn RuntimeSink::drain(self : RuntimeSink, max_items~ : Int = -1) -> Int { +///| +pub fn RuntimeSink::drain(self : RuntimeSink, max_items? : Int = -1) -> Int { match self { Console(_) => 0 JsonConsole(_) => 0 TextConsole(_) => 0 File(sink) => if sink.flush() { 1 } else { 0 } - QueuedConsole(sink) => sink.drain(max_items=max_items) - QueuedJsonConsole(sink) => sink.drain(max_items=max_items) - QueuedTextConsole(sink) => sink.drain(max_items=max_items) - QueuedFile(sink) => sink.drain(max_items=max_items) + QueuedConsole(sink) => sink.drain(max_items~) + QueuedJsonConsole(sink) => sink.drain(max_items~) + QueuedTextConsole(sink) => sink.drain(max_items~) + QueuedFile(sink) => sink.drain(max_items~) } } +///| pub fn RuntimeSink::close(self : RuntimeSink) -> Bool { match self { Console(_) => true @@ -87,6 +112,7 @@ pub fn RuntimeSink::close(self : RuntimeSink) -> Bool { } } +///| pub fn RuntimeSink::pending_count(self : RuntimeSink) -> Int { match self { Console(_) => 0 @@ -100,6 +126,7 @@ pub fn RuntimeSink::pending_count(self : RuntimeSink) -> Int { } } +///| pub fn RuntimeSink::dropped_count(self : RuntimeSink) -> Int { match self { Console(_) => 0 @@ -113,63 +140,96 @@ pub fn RuntimeSink::dropped_count(self : RuntimeSink) -> Int { } } +///| pub type ConfiguredLogger = Logger[RuntimeSink] +///| pub fn ConfiguredLogger::flush(self : ConfiguredLogger) -> Int { self.sink.flush() } -pub fn ConfiguredLogger::drain(self : ConfiguredLogger, max_items~ : Int = -1) -> Int { - self.sink.drain(max_items=max_items) +///| +pub fn ConfiguredLogger::drain( + self : ConfiguredLogger, + max_items? : Int = -1, +) -> Int { + self.sink.drain(max_items~) } +///| pub fn ConfiguredLogger::close(self : ConfiguredLogger) -> Bool { self.sink.close() } +///| pub fn ConfiguredLogger::pending_count(self : ConfiguredLogger) -> Int { self.sink.pending_count() } +///| pub fn ConfiguredLogger::dropped_count(self : ConfiguredLogger) -> Int { self.sink.dropped_count() } +///| fn build_runtime_sink(config : SinkConfig) -> RuntimeSink { match config.kind { SinkKind::Console => RuntimeSink::Console(console_sink()) SinkKind::JsonConsole => RuntimeSink::JsonConsole(json_console_sink()) - SinkKind::TextConsole => RuntimeSink::TextConsole( - text_console_sink(config.text_formatter.to_formatter()), - ) - SinkKind::File => RuntimeSink::File( - file_sink( - config.path, - append=config.append, - auto_flush=config.auto_flush, - rotation=config.rotation, - formatter=fn(rec) { - format_text(rec, formatter=config.text_formatter.to_formatter()) - }, - ), - ) + SinkKind::TextConsole => + RuntimeSink::TextConsole( + text_console_sink(config.text_formatter.to_formatter()), + ) + SinkKind::File => + RuntimeSink::File( + file_sink( + config.path, + append=config.append, + auto_flush=config.auto_flush, + rotation=config.rotation, + formatter=fn(rec) { + format_text(rec, formatter=config.text_formatter.to_formatter()) + }, + ), + ) } } +///| fn apply_queue_config(sink : RuntimeSink, queue : QueueConfig) -> RuntimeSink { match sink { - Console(inner) => RuntimeSink::QueuedConsole( - queued_sink(inner, max_pending=queue.max_pending, overflow=queue.overflow), - ) - JsonConsole(inner) => RuntimeSink::QueuedJsonConsole( - queued_sink(inner, max_pending=queue.max_pending, overflow=queue.overflow), - ) - TextConsole(inner) => RuntimeSink::QueuedTextConsole( - queued_sink(inner, max_pending=queue.max_pending, overflow=queue.overflow), - ) - File(inner) => RuntimeSink::QueuedFile( - queued_sink(inner, max_pending=queue.max_pending, overflow=queue.overflow), - ) + Console(inner) => + RuntimeSink::QueuedConsole( + queued_sink( + inner, + max_pending=queue.max_pending, + overflow=queue.overflow, + ), + ) + JsonConsole(inner) => + RuntimeSink::QueuedJsonConsole( + queued_sink( + inner, + max_pending=queue.max_pending, + overflow=queue.overflow, + ), + ) + TextConsole(inner) => + RuntimeSink::QueuedTextConsole( + queued_sink( + inner, + max_pending=queue.max_pending, + overflow=queue.overflow, + ), + ) + File(inner) => + RuntimeSink::QueuedFile( + queued_sink( + inner, + max_pending=queue.max_pending, + overflow=queue.overflow, + ), + ) QueuedConsole(_) => sink QueuedJsonConsole(_) => sink QueuedTextConsole(_) => sink @@ -177,16 +237,21 @@ fn apply_queue_config(sink : RuntimeSink, queue : QueueConfig) -> RuntimeSink { } } +///| pub fn build_logger(config : LoggerConfig) -> ConfiguredLogger { let sink = build_runtime_sink(config.sink) let actual_sink = match config.queue { None => sink Some(queue) => apply_queue_config(sink, queue) } - Logger::new(actual_sink, min_level=config.min_level, target=config.target) - .with_timestamp(enabled=config.timestamp) + Logger::new(actual_sink, min_level=config.min_level, target=config.target).with_timestamp( + enabled=config.timestamp, + ) } -pub fn parse_and_build_logger(input : String) -> ConfiguredLogger raise ConfigError { +///| +pub fn parse_and_build_logger( + input : String, +) -> ConfiguredLogger raise ConfigError { build_logger(parse_logger_config_text(input)) } diff --git a/src/sinks.mbt b/src/sinks.mbt index ebfdd7d..dca7bc6 100644 --- a/src/sinks.mbt +++ b/src/sinks.mbt @@ -1,26 +1,32 @@ +///| pub trait Sink { - write(Self, Record) -> Unit + fn write(Self, Record) -> Unit } +///| pub struct ConsoleSink { _dummy : Unit } +///| pub fn console_sink() -> ConsoleSink { { _dummy: () } } -pub impl Sink for ConsoleSink with write(self, rec) { +///| +pub impl Sink for ConsoleSink with fn write(self, rec) { ignore(self) println(format_text(rec)) } +///| pub struct ContextSink[S] { sink : S context_fields : Array[Field] } -pub impl[S : Sink] Sink for ContextSink[S] with write(self, rec) { +///| +pub impl[S : Sink] Sink for ContextSink[S] with fn write(self, rec) { let merged = if self.context_fields.length() == 0 { rec.fields } else if rec.fields.length() == 0 { @@ -31,42 +37,51 @@ pub impl[S : Sink] Sink for ContextSink[S] with write(self, rec) { self.sink.write(rec.with_fields(merged)) } +///| pub struct JsonConsoleSink { _dummy : Unit } +///| pub fn json_console_sink() -> JsonConsoleSink { { _dummy: () } } -pub impl Sink for JsonConsoleSink with write(self, rec) { +///| +pub impl Sink for JsonConsoleSink with fn write(self, rec) { ignore(self) println(format_json(rec)) } +///| pub struct FormattedConsoleSink { formatter : RecordFormatter } -pub fn formatted_console_sink(formatter : RecordFormatter) -> FormattedConsoleSink { +///| +pub fn formatted_console_sink( + formatter : RecordFormatter, +) -> FormattedConsoleSink { { formatter, } } +///| pub fn text_console_sink(formatter : TextFormatter) -> FormattedConsoleSink { - formatted_console_sink(fn(rec) { - format_text(rec, formatter=formatter) - }) + formatted_console_sink(fn(rec) { format_text(rec, formatter~) }) } -pub impl Sink for FormattedConsoleSink with write(self, rec) { +///| +pub impl Sink for FormattedConsoleSink with fn write(self, rec) { println((self.formatter)(rec)) } +///| pub struct FormattedCallbackSink { formatter : RecordFormatter callback : (String) -> Unit } +///| pub fn formatted_callback_sink( formatter : RecordFormatter, callback : (String) -> Unit, @@ -74,54 +89,63 @@ pub fn formatted_callback_sink( { formatter, callback } } +///| pub fn text_callback_sink( formatter : TextFormatter, callback : (String) -> Unit, ) -> FormattedCallbackSink { - formatted_callback_sink(fn(rec) { - format_text(rec, formatter=formatter) - }, callback) + formatted_callback_sink(fn(rec) { format_text(rec, formatter~) }, callback) } -pub impl Sink for FormattedCallbackSink with write(self, rec) { +///| +pub impl Sink for FormattedCallbackSink with fn write(self, rec) { (self.callback)((self.formatter)(rec)) } +///| pub struct FanoutSink[A, B] { left : A right : B } +///| pub fn[A, B] fanout_sink(left : A, right : B) -> FanoutSink[A, B] { { left, right } } -pub impl[A : Sink, B : Sink] Sink for FanoutSink[A, B] with write(self, rec) { +///| +pub impl[A : Sink, B : Sink] Sink for FanoutSink[A, B] with fn write(self, rec) { self.left.write(rec) self.right.write(rec.copy()) } +///| pub struct SplitSink[A, B] { left : A right : B predicate : (Record) -> Bool } -pub fn[A, B] split_sink(left : A, right : B, predicate : (Record) -> Bool) -> SplitSink[A, B] { +///| +pub fn[A, B] split_sink( + left : A, + right : B, + predicate : (Record) -> Bool, +) -> SplitSink[A, B] { { left, right, predicate } } +///| pub fn[A, B] split_by_level( left : A, right : B, - min_level~ : Level = Level::Warn, + min_level? : Level = Level::Warn, ) -> SplitSink[A, B] { - split_sink(left, right, fn(rec) { - rec.level.enabled(min_level) - }) + split_sink(left, right, fn(rec) { rec.level.enabled(min_level) }) } -pub impl[A : Sink, B : Sink] Sink for SplitSink[A, B] with write(self, rec) { +///| +pub impl[A : Sink, B : Sink] Sink for SplitSink[A, B] with fn write(self, rec) { if (self.predicate)(rec) { self.left.write(rec) } else { @@ -129,33 +153,40 @@ pub impl[A : Sink, B : Sink] Sink for SplitSink[A, B] with write(self, rec) { } } +///| pub struct CallbackSink { callback : (Record) -> Unit } +///| pub fn callback_sink(callback : (Record) -> Unit) -> CallbackSink { { callback, } } -pub impl Sink for CallbackSink with write(self, rec) { +///| +pub impl Sink for CallbackSink with fn write(self, rec) { (self.callback)(rec) } +///| pub struct BufferedSink[S] { sink : S buffer : Ref[Array[Record]] flush_limit : Int } -pub fn[S] buffered_sink(sink : S, flush_limit~ : Int = 1) -> BufferedSink[S] { +///| +pub fn[S] buffered_sink(sink : S, flush_limit? : Int = 1) -> BufferedSink[S] { let actual_limit = if flush_limit <= 0 { 1 } else { flush_limit } { sink, buffer: Ref([]), flush_limit: actual_limit } } +///| pub fn[S] BufferedSink::pending_count(self : BufferedSink[S]) -> Int { self.buffer.val.length() } +///| pub fn[S : Sink] BufferedSink::flush(self : BufferedSink[S]) -> Unit { if self.buffer.val.length() == 0 { () @@ -168,15 +199,18 @@ pub fn[S : Sink] BufferedSink::flush(self : BufferedSink[S]) -> Unit { } } -pub impl[S : Sink] Sink for BufferedSink[S] with write(self, rec) { +///| +pub impl[S : Sink] Sink for BufferedSink[S] with fn write(self, rec) { self.buffer.val.push(rec) if self.buffer.val.length() >= self.flush_limit { self.flush() } } +///| pub type QueueOverflowPolicy = @utils.QueueOverflowPolicy +///| pub struct QueuedSink[S] { sink : S queue : @queue.Queue[Record] @@ -185,10 +219,11 @@ pub struct QueuedSink[S] { dropped_count : Ref[Int] } +///| pub fn[S] queued_sink( sink : S, - max_pending~ : Int = 0, - overflow~ : QueueOverflowPolicy = QueueOverflowPolicy::DropNewest, + max_pending? : Int = 0, + overflow? : QueueOverflowPolicy = QueueOverflowPolicy::DropNewest, ) -> QueuedSink[S] { { sink, @@ -199,15 +234,21 @@ pub fn[S] queued_sink( } } +///| pub fn[S] QueuedSink::pending_count(self : QueuedSink[S]) -> Int { self.queue.length() } +///| pub fn[S] QueuedSink::dropped_count(self : QueuedSink[S]) -> Int { self.dropped_count.val } -pub fn[S : Sink] QueuedSink::drain(self : QueuedSink[S], max_items~ : Int = -1) -> Int { +///| +pub fn[S : Sink] QueuedSink::drain( + self : QueuedSink[S], + max_items? : Int = -1, +) -> Int { if max_items == 0 { return 0 } @@ -225,11 +266,13 @@ pub fn[S : Sink] QueuedSink::drain(self : QueuedSink[S], max_items~ : Int = -1) } } +///| pub fn[S : Sink] QueuedSink::flush(self : QueuedSink[S]) -> Int { self.drain() } -pub impl[S] Sink for QueuedSink[S] with write(self, rec) { +///| +pub impl[S] Sink for QueuedSink[S] with fn write(self, rec) { let full = self.max_pending > 0 && self.pending_count() >= self.max_pending if !full { self.queue.push(rec) @@ -245,30 +288,36 @@ pub impl[S] Sink for QueuedSink[S] with write(self, rec) { } } +///| pub struct FilterSink[S] { sink : S predicate : (Record) -> Bool } +///| pub fn[S] filter_sink(sink : S, predicate : (Record) -> Bool) -> FilterSink[S] { { sink, predicate } } -pub impl[S : Sink] Sink for FilterSink[S] with write(self, rec) { +///| +pub impl[S : Sink] Sink for FilterSink[S] with fn write(self, rec) { if (self.predicate)(rec) { self.sink.write(rec) } } +///| pub struct PatchSink[S] { sink : S patch : RecordPatch } +///| pub fn[S] patch_sink(sink : S, patch : RecordPatch) -> PatchSink[S] { { sink, patch } } -pub impl[S : Sink] Sink for PatchSink[S] with write(self, rec) { +///| +pub impl[S : Sink] Sink for PatchSink[S] with fn write(self, rec) { self.sink.write((self.patch)(rec)) } diff --git a/src/sinks_file.mbt b/src/sinks_file.mbt index de4cbbb..19bda74 100644 --- a/src/sinks_file.mbt +++ b/src/sinks_file.mbt @@ -1,3 +1,4 @@ +///| pub struct FileSink { path : String append : Ref[Bool] @@ -14,28 +15,32 @@ pub struct FileSink { rotation_failures : Ref[Int] } +///| pub type FileRotation = @utils.FileRotation +///| pub type FileSinkState = @utils.FileSinkState +///| pub type FileSinkPolicy = @utils.FileSinkPolicy -pub fn file_rotation(max_bytes : Int, max_backups~ : Int = 1) -> FileRotation { - @utils.file_rotation(max_bytes, max_backups=max_backups) +///| +pub fn file_rotation(max_bytes : Int, max_backups? : Int = 1) -> FileRotation { + @utils.file_rotation(max_bytes, max_backups~) } +///| pub fn native_files_supported() -> Bool { native_files_supported_internal() } +///| pub fn file_sink( path : String, - append~ : Bool = true, - auto_flush~ : Bool = true, - rotation~ : FileRotation? = None, - formatter~ : RecordFormatter = fn(rec) { - format_text(rec) - }, + append? : Bool = true, + auto_flush? : Bool = true, + rotation? : FileRotation? = None, + formatter? : RecordFormatter = fn(rec) { format_text(rec) }, ) -> FileSink { let handle = open_file_handle_internal(path, append) { @@ -55,10 +60,12 @@ pub fn file_sink( } } +///| pub fn FileSink::is_available(self : FileSink) -> Bool { self.handle.val is Some(_) } +///| pub fn FileSink::flush(self : FileSink) -> Bool { match self.handle.val { None => false @@ -72,48 +79,62 @@ pub fn FileSink::flush(self : FileSink) -> Bool { } } +///| pub fn FileSink::append_mode(self : FileSink) -> Bool { self.append.val } +///| pub fn FileSink::set_append_mode(self : FileSink, append : Bool) -> Unit { self.append.val = append } +///| pub fn FileSink::path(self : FileSink) -> String { self.path } +///| pub fn FileSink::auto_flush_enabled(self : FileSink) -> Bool { self.auto_flush.val } +///| pub fn FileSink::rotation_enabled(self : FileSink) -> Bool { self.rotation.val is Some(_) } +///| pub fn FileSink::rotation_config(self : FileSink) -> FileRotation? { self.rotation.val } +///| pub fn FileSink::set_auto_flush(self : FileSink, enabled : Bool) -> Unit { self.auto_flush.val = enabled } +///| pub fn FileSink::set_policy(self : FileSink, policy : FileSinkPolicy) -> Unit { self.append.val = policy.append self.auto_flush.val = policy.auto_flush self.rotation.val = policy.rotation } -pub fn FileSink::set_rotation(self : FileSink, rotation : FileRotation?) -> Unit { +///| +pub fn FileSink::set_rotation( + self : FileSink, + rotation : FileRotation?, +) -> Unit { self.rotation.val = rotation } +///| pub fn FileSink::clear_rotation(self : FileSink) -> Unit { self.rotation.val = None } +///| pub fn FileSink::close(self : FileSink) -> Bool { match self.handle.val { None => false @@ -125,22 +146,27 @@ pub fn FileSink::close(self : FileSink) -> Bool { } } +///| pub fn FileSink::rotation_failures(self : FileSink) -> Int { self.rotation_failures.val } +///| pub fn FileSink::open_failures(self : FileSink) -> Int { self.open_failures.val } +///| pub fn FileSink::write_failures(self : FileSink) -> Int { self.write_failures.val } +///| pub fn FileSink::flush_failures(self : FileSink) -> Int { self.flush_failures.val } +///| pub fn FileSink::reset_failure_counters(self : FileSink) -> Unit { self.open_failures.val = 0 self.write_failures.val = 0 @@ -148,12 +174,14 @@ pub fn FileSink::reset_failure_counters(self : FileSink) -> Unit { self.rotation_failures.val = 0 } +///| pub fn FileSink::reset_policy(self : FileSink) -> Unit { self.append.val = self.default_append self.auto_flush.val = self.default_auto_flush self.rotation.val = self.default_rotation } +///| pub fn FileSink::policy(self : FileSink) -> FileSinkPolicy { FileSinkPolicy::new( append=self.append.val, @@ -162,6 +190,7 @@ pub fn FileSink::policy(self : FileSink) -> FileSinkPolicy { ) } +///| pub fn FileSink::default_policy(self : FileSink) -> FileSinkPolicy { FileSinkPolicy::new( append=self.default_append, @@ -170,6 +199,7 @@ pub fn FileSink::default_policy(self : FileSink) -> FileSinkPolicy { ) } +///| pub fn FileSink::policy_matches_default(self : FileSink) -> Bool { let current = self.policy() let default = self.default_policy() @@ -178,14 +208,20 @@ pub fn FileSink::policy_matches_default(self : FileSink) -> Bool { policy_rotation_equals_internal(current.rotation, default.rotation) } -fn policy_rotation_equals_internal(left : FileRotation?, right : FileRotation?) -> Bool { +///| +fn policy_rotation_equals_internal( + left : FileRotation?, + right : FileRotation?, +) -> Bool { match (left, right) { (None, None) => true - (Some(a), Some(b)) => a.max_bytes == b.max_bytes && a.max_backups == b.max_backups + (Some(a), Some(b)) => + a.max_bytes == b.max_bytes && a.max_backups == b.max_backups _ => false } } +///| pub fn FileSink::state(self : FileSink) -> FileSinkState { FileSinkState::new( self.path, @@ -200,7 +236,8 @@ pub fn FileSink::state(self : FileSink) -> FileSinkState { ) } -pub fn FileSink::reopen(self : FileSink, append~ : Bool? = None) -> Bool { +///| +pub fn FileSink::reopen(self : FileSink, append? : Bool? = None) -> Bool { let append_mode = append.unwrap_or(self.append.val) self.append.val = append_mode match self.handle.val { @@ -220,22 +257,27 @@ pub fn FileSink::reopen(self : FileSink, append~ : Bool? = None) -> Bool { } } +///| pub fn FileSink::reopen_with_current_policy(self : FileSink) -> Bool { self.reopen() } +///| pub fn FileSink::reopen_append(self : FileSink) -> Bool { self.reopen(append=Some(true)) } +///| pub fn FileSink::reopen_truncate(self : FileSink) -> Bool { self.reopen(append=Some(false)) } +///| fn rotated_file_path(path : String, index : Int) -> String { "\{path}.\{index}" } +///| fn rotate_file_sink_internal(sink : FileSink, rotation : FileRotation) -> Bool { let closed = match sink.handle.val { None => true @@ -249,7 +291,9 @@ fn rotate_file_sink_internal(sink : FileSink, rotation : FileRotation) -> Bool { return false } if rotation.max_backups > 0 { - ignore(remove_file_internal(rotated_file_path(sink.path, rotation.max_backups))) + ignore( + remove_file_internal(rotated_file_path(sink.path, rotation.max_backups)), + ) for index = rotation.max_backups - 1; index >= 1; { let from_path = rotated_file_path(sink.path, index) let to_path = rotated_file_path(sink.path, index + 1) @@ -264,40 +308,42 @@ fn rotate_file_sink_internal(sink : FileSink, rotation : FileRotation) -> Bool { sink.handle.val is Some(_) } +///| fn rotate_if_needed_internal(sink : FileSink, next_line_bytes : Int) -> Bool { match sink.rotation.val { None => true - Some(rotation) => match sink.handle.val { - None => false - Some(handle) => { - let size = file_size_internal(handle) - if size + next_line_bytes <= rotation.max_bytes { - true - } else { - let rotated = rotate_file_sink_internal(sink, rotation) - if !rotated { - sink.rotation_failures.val += 1 + Some(rotation) => + match sink.handle.val { + None => false + Some(handle) => { + let size = file_size_internal(handle) + if size + next_line_bytes <= rotation.max_bytes { + true + } else { + let rotated = rotate_file_sink_internal(sink, rotation) + if !rotated { + sink.rotation_failures.val += 1 + } + rotated } - rotated } } - } } } -pub impl Sink for FileSink with write(self, rec) { +///| +pub impl Sink for FileSink with fn write(self, rec) { match self.handle.val { - None => { - self.write_failures.val += 1 - } + None => self.write_failures.val += 1 Some(_) => { let line = "\{(self.formatter)(rec)}\n" - let can_write = rotate_if_needed_internal(self, string_byte_length_internal(line)) + let can_write = rotate_if_needed_internal( + self, + string_byte_length_internal(line), + ) if can_write { match self.handle.val { - None => { - self.write_failures.val += 1 - } + None => self.write_failures.val += 1 Some(active) => { let wrote = write_file_handle_internal(active, line) if wrote { diff --git a/src/utils/config.mbt b/src/utils/config.mbt index 70b019f..78b972c 100644 --- a/src/utils/config.mbt +++ b/src/utils/config.mbt @@ -1,7 +1,9 @@ +///| pub(all) suberror ConfigError { InvalidConfig(String) } +///| pub(all) enum SinkKind { Console JsonConsole @@ -9,6 +11,7 @@ pub(all) enum SinkKind { File } +///| pub struct TextFormatterConfig { show_timestamp : Bool show_level : Bool @@ -25,20 +28,21 @@ pub struct TextFormatterConfig { style_tags : Map[String, TextStyle] } +///| pub fn TextFormatterConfig::new( - show_timestamp~ : Bool = true, - show_level~ : Bool = true, - show_target~ : Bool = true, - show_fields~ : Bool = true, - separator~ : String = " ", - field_separator~ : String = " ", - template~ : String = "", - color_mode~ : ColorMode = ColorMode::Never, - color_support~ : ColorSupport = ColorSupport::TrueColor, - style_markup~ : StyleMarkupMode = StyleMarkupMode::Full, - target_style_markup~ : StyleMarkupMode = StyleMarkupMode::Disabled, - fields_style_markup~ : StyleMarkupMode = StyleMarkupMode::Disabled, - style_tags~ : Map[String, TextStyle] = {}, + show_timestamp? : Bool = true, + show_level? : Bool = true, + show_target? : Bool = true, + show_fields? : Bool = true, + separator? : String = " ", + field_separator? : String = " ", + template? : String = "", + color_mode? : ColorMode = ColorMode::Never, + color_support? : ColorSupport = ColorSupport::TrueColor, + style_markup? : StyleMarkupMode = StyleMarkupMode::Full, + target_style_markup? : StyleMarkupMode = StyleMarkupMode::Disabled, + fields_style_markup? : StyleMarkupMode = StyleMarkupMode::Disabled, + style_tags? : Map[String, TextStyle] = {}, ) -> TextFormatterConfig { { show_timestamp, @@ -57,15 +61,21 @@ pub fn TextFormatterConfig::new( } } -fn style_tag_registry_from_config(style_tags : Map[String, TextStyle]) -> StyleTagRegistry { +///| +fn style_tag_registry_from_config( + style_tags : Map[String, TextStyle], +) -> StyleTagRegistry { let registry = style_tag_registry() for name, style in style_tags { - ignore(registry.set_tag(name, style=style)) + ignore(registry.set_tag(name, style~)) } registry } -pub fn TextFormatterConfig::to_formatter(self : TextFormatterConfig) -> TextFormatter { +///| +pub fn TextFormatterConfig::to_formatter( + self : TextFormatterConfig, +) -> TextFormatter { text_formatter( show_timestamp=self.show_timestamp, show_level=self.show_level, @@ -87,18 +97,21 @@ pub fn TextFormatterConfig::to_formatter(self : TextFormatterConfig) -> TextForm ) } +///| pub struct QueueConfig { max_pending : Int overflow : QueueOverflowPolicy } +///| pub fn QueueConfig::new( max_pending : Int, - overflow~ : QueueOverflowPolicy = QueueOverflowPolicy::DropNewest, + overflow? : QueueOverflowPolicy = QueueOverflowPolicy::DropNewest, ) -> QueueConfig { { max_pending, overflow } } +///| pub struct SinkConfig { kind : SinkKind path : String @@ -108,24 +121,19 @@ pub struct SinkConfig { text_formatter : TextFormatterConfig } +///| pub fn SinkConfig::new( - kind~ : SinkKind = SinkKind::Console, - path~ : String = "", - append~ : Bool = true, - auto_flush~ : Bool = true, - rotation~ : FileRotation? = None, - text_formatter~ : TextFormatterConfig = default_text_formatter_config(), + kind? : SinkKind = SinkKind::Console, + path? : String = "", + append? : Bool = true, + auto_flush? : Bool = true, + rotation? : FileRotation? = None, + text_formatter? : TextFormatterConfig = default_text_formatter_config(), ) -> SinkConfig { - { - kind, - path, - append, - auto_flush, - rotation, - text_formatter, - } + { kind, path, append, auto_flush, rotation, text_formatter } } +///| pub struct LoggerConfig { min_level : @core.Level target : String @@ -134,34 +142,33 @@ pub struct LoggerConfig { queue : QueueConfig? } +///| pub fn LoggerConfig::new( - min_level~ : @core.Level = @core.Level::Info, - target~ : String = "", - timestamp~ : Bool = false, - sink~ : SinkConfig = default_sink_config(), - queue~ : QueueConfig? = None, + min_level? : @core.Level = @core.Level::Info, + target? : String = "", + timestamp? : Bool = false, + sink? : SinkConfig = default_sink_config(), + queue? : QueueConfig? = None, ) -> LoggerConfig { - { - min_level, - target, - timestamp, - sink, - queue, - } + { min_level, target, timestamp, sink, queue } } +///| pub fn default_text_formatter_config() -> TextFormatterConfig { TextFormatterConfig::new() } +///| pub fn default_sink_config() -> SinkConfig { SinkConfig::new() } +///| pub fn default_logger_config() -> LoggerConfig { LoggerConfig::new() } +///| fn expect_object( value : @json_parser.JsonValue, context : String, @@ -172,33 +179,40 @@ fn expect_object( } } +///| fn get_string( obj : Map[String, @json_parser.JsonValue], key : String, - default~ : String = "", + default? : String = "", ) -> String raise ConfigError { match obj.get(key) { None => default - Some(value) => match value.as_string() { - Some(text) => text - None => raise ConfigError::InvalidConfig("Expected string at key " + key) - } + Some(value) => + match value.as_string() { + Some(text) => text + None => + raise ConfigError::InvalidConfig("Expected string at key " + key) + } } } +///| fn get_optional_string( obj : Map[String, @json_parser.JsonValue], key : String, ) -> String? raise ConfigError { match obj.get(key) { None => None - Some(value) => match value.as_string() { - Some(text) => Some(text) - None => raise ConfigError::InvalidConfig("Expected string at key " + key) - } + Some(value) => + match value.as_string() { + Some(text) => Some(text) + None => + raise ConfigError::InvalidConfig("Expected string at key " + key) + } } } +///| fn get_bool( obj : Map[String, @json_parser.JsonValue], key : String, @@ -206,13 +220,15 @@ fn get_bool( ) -> Bool raise ConfigError { match obj.get(key) { None => default - Some(value) => match value.as_bool() { - Some(flag) => flag - None => raise ConfigError::InvalidConfig("Expected bool at key " + key) - } + Some(value) => + match value.as_bool() { + Some(flag) => flag + None => raise ConfigError::InvalidConfig("Expected bool at key " + key) + } } } +///| fn get_int( obj : Map[String, @json_parser.JsonValue], key : String, @@ -220,13 +236,16 @@ fn get_int( ) -> Int raise ConfigError { match obj.get(key) { None => default - Some(value) => match value.as_number() { - Some(number) => number.to_int() - None => raise ConfigError::InvalidConfig("Expected number at key " + key) - } + Some(value) => + match value.as_number() { + Some(number) => number.to_int() + None => + raise ConfigError::InvalidConfig("Expected number at key " + key) + } } } +///| fn parse_level(name : String) -> @core.Level raise ConfigError { match name.to_upper() { "TRACE" => @core.Level::Trace @@ -238,15 +257,20 @@ fn parse_level(name : String) -> @core.Level raise ConfigError { } } +///| fn parse_overflow(name : String) -> QueueOverflowPolicy raise ConfigError { match name.to_upper() { "DROPNEWEST" => QueueOverflowPolicy::DropNewest "DROPPOLDEST" => QueueOverflowPolicy::DropOldest "DROPOLDEST" => QueueOverflowPolicy::DropOldest - _ => raise ConfigError::InvalidConfig("Unsupported queue overflow policy: " + name) + _ => + raise ConfigError::InvalidConfig( + "Unsupported queue overflow policy: " + name, + ) } } +///| fn parse_sink_kind(name : String) -> SinkKind raise ConfigError { match name.to_upper() { "CONSOLE" => SinkKind::Console @@ -259,6 +283,7 @@ fn parse_sink_kind(name : String) -> SinkKind raise ConfigError { } } +///| fn parse_color_mode(name : String) -> ColorMode raise ConfigError { match name.to_upper() { "NEVER" => ColorMode::Never @@ -268,6 +293,7 @@ fn parse_color_mode(name : String) -> ColorMode raise ConfigError { } } +///| fn parse_color_support(name : String) -> ColorSupport raise ConfigError { match name.to_upper() { "BASIC" => ColorSupport::Basic @@ -276,15 +302,18 @@ fn parse_color_support(name : String) -> ColorSupport raise ConfigError { } } +///| fn parse_style_markup_mode(name : String) -> StyleMarkupMode raise ConfigError { match name.to_upper() { "DISABLED" => StyleMarkupMode::Disabled "BUILTIN" => StyleMarkupMode::Builtin "FULL" => StyleMarkupMode::Full - _ => raise ConfigError::InvalidConfig("Unsupported style markup mode: " + name) + _ => + raise ConfigError::InvalidConfig("Unsupported style markup mode: " + name) } } +///| fn sink_kind_label(kind : SinkKind) -> String { match kind { SinkKind::Console => "console" @@ -294,7 +323,10 @@ fn sink_kind_label(kind : SinkKind) -> String { } } -fn parse_text_formatter_config(value : @json_parser.JsonValue) -> TextFormatterConfig raise ConfigError { +///| +fn parse_text_formatter_config( + value : @json_parser.JsonValue, +) -> TextFormatterConfig raise ConfigError { let obj = expect_object(value, "text_formatter") TextFormatterConfig::new( show_timestamp=get_bool(obj, "show_timestamp", default=true), @@ -305,10 +337,18 @@ fn parse_text_formatter_config(value : @json_parser.JsonValue) -> TextFormatterC field_separator=get_string(obj, "field_separator", default=" "), template=get_string(obj, "template", default=""), color_mode=parse_color_mode(get_string(obj, "color_mode", default="never")), - color_support=parse_color_support(get_string(obj, "color_support", default="truecolor")), - style_markup=parse_style_markup_mode(get_string(obj, "style_markup", default="full")), - target_style_markup=parse_style_markup_mode(get_string(obj, "target_style_markup", default="disabled")), - fields_style_markup=parse_style_markup_mode(get_string(obj, "fields_style_markup", default="disabled")), + color_support=parse_color_support( + get_string(obj, "color_support", default="truecolor"), + ), + style_markup=parse_style_markup_mode( + get_string(obj, "style_markup", default="full"), + ), + target_style_markup=parse_style_markup_mode( + get_string(obj, "target_style_markup", default="disabled"), + ), + fields_style_markup=parse_style_markup_mode( + get_string(obj, "fields_style_markup", default="disabled"), + ), style_tags=match obj.get("style_tags") { None => {} Some(inner) => parse_style_tags_config(inner) @@ -316,6 +356,7 @@ fn parse_text_formatter_config(value : @json_parser.JsonValue) -> TextFormatterC ) } +///| fn parse_text_style_config( value : @json_parser.JsonValue, context : String, @@ -331,16 +372,25 @@ fn parse_text_style_config( ) } -fn parse_style_tags_config(value : @json_parser.JsonValue) -> Map[String, TextStyle] raise ConfigError { +///| +fn parse_style_tags_config( + value : @json_parser.JsonValue, +) -> Map[String, TextStyle] raise ConfigError { let obj = expect_object(value, "text_formatter.style_tags") let style_tags : Map[String, TextStyle] = {} for name, item in obj { - style_tags[name] = parse_text_style_config(item, "text_formatter.style_tags." + name) + style_tags[name] = parse_text_style_config( + item, + "text_formatter.style_tags." + name, + ) } style_tags } -fn parse_queue_config(value : @json_parser.JsonValue) -> QueueConfig raise ConfigError { +///| +fn parse_queue_config( + value : @json_parser.JsonValue, +) -> QueueConfig raise ConfigError { let obj = expect_object(value, "queue") QueueConfig::new( get_int(obj, "max_pending", default=0), @@ -348,7 +398,10 @@ fn parse_queue_config(value : @json_parser.JsonValue) -> QueueConfig raise Confi ) } -fn parse_file_rotation_config(value : @json_parser.JsonValue) -> FileRotation raise ConfigError { +///| +fn parse_file_rotation_config( + value : @json_parser.JsonValue, +) -> FileRotation raise ConfigError { let obj = expect_object(value, "sink.rotation") file_rotation( get_int(obj, "max_bytes", default=1), @@ -356,7 +409,10 @@ fn parse_file_rotation_config(value : @json_parser.JsonValue) -> FileRotation ra ) } -fn parse_sink_config(value : @json_parser.JsonValue) -> SinkConfig raise ConfigError { +///| +fn parse_sink_config( + value : @json_parser.JsonValue, +) -> SinkConfig raise ConfigError { let obj = expect_object(value, "sink") let kind = parse_sink_kind(get_string(obj, "kind", default="console")) let formatter = match obj.get("text_formatter") { @@ -365,14 +421,15 @@ fn parse_sink_config(value : @json_parser.JsonValue) -> SinkConfig raise ConfigE } let path = get_string(obj, "path", default="") match kind { - SinkKind::File => if path == "" { - raise ConfigError::InvalidConfig("File sink requires non-empty path") - } + SinkKind::File => + if path == "" { + raise ConfigError::InvalidConfig("File sink requires non-empty path") + } _ => () } SinkConfig::new( - kind=kind, - path=path, + kind~, + path~, append=get_bool(obj, "append", default=true), auto_flush=get_bool(obj, "auto_flush", default=true), rotation=match obj.get("rotation") { @@ -383,7 +440,10 @@ fn parse_sink_config(value : @json_parser.JsonValue) -> SinkConfig raise ConfigE ) } -pub fn parse_logger_config_text(input : String) -> LoggerConfig raise ConfigError { +///| +pub fn parse_logger_config_text( + input : String, +) -> LoggerConfig raise ConfigError { let root = @json_parser.parse(input) catch { e => raise ConfigError::InvalidConfig("Invalid JSON: " + e.to_string()) } @@ -403,17 +463,24 @@ pub fn parse_logger_config_text(input : String) -> LoggerConfig raise ConfigErro ) } +///| pub fn queue_config_to_json(queue : QueueConfig) -> @json_parser.JsonValue { @json_parser.JsonValue::Object({ "max_pending": @json_parser.JsonValue::Number(queue.max_pending.to_double()), - "overflow": @json_parser.JsonValue::String(match queue.overflow { - QueueOverflowPolicy::DropNewest => "DropNewest" - QueueOverflowPolicy::DropOldest => "DropOldest" - }), + "overflow": @json_parser.JsonValue::String( + match queue.overflow { + QueueOverflowPolicy::DropNewest => "DropNewest" + QueueOverflowPolicy::DropOldest => "DropOldest" + }, + ), }) } -pub fn stringify_queue_config(queue : QueueConfig, pretty~ : Bool = false) -> String { +///| +pub fn stringify_queue_config( + queue : QueueConfig, + pretty? : Bool = false, +) -> String { let value = queue_config_to_json(queue) if pretty { @json_parser.stringify_pretty(value, 2) @@ -422,7 +489,10 @@ pub fn stringify_queue_config(queue : QueueConfig, pretty~ : Bool = false) -> St } } -pub fn text_formatter_config_to_json(config : TextFormatterConfig) -> @json_parser.JsonValue { +///| +pub fn text_formatter_config_to_json( + config : TextFormatterConfig, +) -> @json_parser.JsonValue { let obj : Map[String, @json_parser.JsonValue] = { "show_timestamp": @json_parser.JsonValue::Bool(config.show_timestamp), "show_level": @json_parser.JsonValue::Bool(config.show_level), @@ -431,11 +501,21 @@ pub fn text_formatter_config_to_json(config : TextFormatterConfig) -> @json_pars "separator": @json_parser.JsonValue::String(config.separator), "field_separator": @json_parser.JsonValue::String(config.field_separator), "template": @json_parser.JsonValue::String(config.template), - "color_mode": @json_parser.JsonValue::String(color_mode_label(config.color_mode)), - "color_support": @json_parser.JsonValue::String(color_support_label(config.color_support)), - "style_markup": @json_parser.JsonValue::String(style_markup_mode_label(config.style_markup)), - "target_style_markup": @json_parser.JsonValue::String(style_markup_mode_label(config.target_style_markup)), - "fields_style_markup": @json_parser.JsonValue::String(style_markup_mode_label(config.fields_style_markup)), + "color_mode": @json_parser.JsonValue::String( + color_mode_label(config.color_mode), + ), + "color_support": @json_parser.JsonValue::String( + color_support_label(config.color_support), + ), + "style_markup": @json_parser.JsonValue::String( + style_markup_mode_label(config.style_markup), + ), + "target_style_markup": @json_parser.JsonValue::String( + style_markup_mode_label(config.target_style_markup), + ), + "fields_style_markup": @json_parser.JsonValue::String( + style_markup_mode_label(config.fields_style_markup), + ), } if config.style_tags.length() != 0 { obj["style_tags"] = style_tags_config_to_json(config.style_tags) @@ -443,6 +523,7 @@ pub fn text_formatter_config_to_json(config : TextFormatterConfig) -> @json_pars @json_parser.JsonValue::Object(obj) } +///| fn text_style_config_to_json(style : TextStyle) -> @json_parser.JsonValue { let obj : Map[String, @json_parser.JsonValue] = { "bold": @json_parser.JsonValue::Bool(style.bold), @@ -461,7 +542,10 @@ fn text_style_config_to_json(style : TextStyle) -> @json_parser.JsonValue { @json_parser.JsonValue::Object(obj) } -fn style_tags_config_to_json(style_tags : Map[String, TextStyle]) -> @json_parser.JsonValue { +///| +fn style_tags_config_to_json( + style_tags : Map[String, TextStyle], +) -> @json_parser.JsonValue { let obj : Map[String, @json_parser.JsonValue] = {} for name, style in style_tags { obj[name] = text_style_config_to_json(style) @@ -469,9 +553,10 @@ fn style_tags_config_to_json(style_tags : Map[String, TextStyle]) -> @json_parse @json_parser.JsonValue::Object(obj) } +///| pub fn stringify_text_formatter_config( config : TextFormatterConfig, - pretty~ : Bool = false, + pretty? : Bool = false, ) -> String { let value = text_formatter_config_to_json(config) if pretty { @@ -481,13 +566,19 @@ pub fn stringify_text_formatter_config( } } -pub fn file_rotation_config_to_json(config : FileRotation) -> @json_parser.JsonValue { +///| +pub fn file_rotation_config_to_json( + config : FileRotation, +) -> @json_parser.JsonValue { @json_parser.JsonValue::Object({ "max_bytes": @json_parser.JsonValue::Number(config.max_bytes.to_double()), - "max_backups": @json_parser.JsonValue::Number(config.max_backups.to_double()), + "max_backups": @json_parser.JsonValue::Number( + config.max_backups.to_double(), + ), }) } +///| pub fn sink_config_to_json(config : SinkConfig) -> @json_parser.JsonValue { let obj : Map[String, @json_parser.JsonValue] = { "kind": @json_parser.JsonValue::String(sink_kind_label(config.kind)), @@ -503,7 +594,11 @@ pub fn sink_config_to_json(config : SinkConfig) -> @json_parser.JsonValue { @json_parser.JsonValue::Object(obj) } -pub fn stringify_sink_config(config : SinkConfig, pretty~ : Bool = false) -> String { +///| +pub fn stringify_sink_config( + config : SinkConfig, + pretty? : Bool = false, +) -> String { let value = sink_config_to_json(config) if pretty { @json_parser.stringify_pretty(value, 2) @@ -512,6 +607,7 @@ pub fn stringify_sink_config(config : SinkConfig, pretty~ : Bool = false) -> Str } } +///| pub fn logger_config_to_json(config : LoggerConfig) -> @json_parser.JsonValue { let obj : Map[String, @json_parser.JsonValue] = { "min_level": @json_parser.JsonValue::String(config.min_level.label()), @@ -526,7 +622,11 @@ pub fn logger_config_to_json(config : LoggerConfig) -> @json_parser.JsonValue { @json_parser.JsonValue::Object(obj) } -pub fn stringify_logger_config(config : LoggerConfig, pretty~ : Bool = false) -> String { +///| +pub fn stringify_logger_config( + config : LoggerConfig, + pretty? : Bool = false, +) -> String { let value = logger_config_to_json(config) if pretty { @json_parser.stringify_pretty(value, 2) diff --git a/src/utils/file_backend_native.mbt b/src/utils/file_backend_native.mbt index 7589a80..e302fc5 100644 --- a/src/utils/file_backend_native.mbt +++ b/src/utils/file_backend_native.mbt @@ -1,3 +1,4 @@ +///| fn string_to_c_bytes(str : String) -> Bytes { let res : Array[Byte] = [] let len = str.length() @@ -31,14 +32,18 @@ fn string_to_c_bytes(str : String) -> Bytes { Bytes::from_array(res) } +///| #external type NativeFileHandle +///| #borrow(path, mode) extern "C" fn file_open_ffi(path : Bytes, mode : Bytes) -> NativeFileHandle = "bitlogger_file_open" +///| extern "C" fn file_is_null_ffi(handle : NativeFileHandle) -> Bool = "bitlogger_pointer_is_null" +///| #borrow(buffer) extern "C" fn file_write_ffi( buffer : Bytes, @@ -47,32 +52,37 @@ extern "C" fn file_write_ffi( handle : NativeFileHandle, ) -> Int = "bitlogger_file_write" +///| extern "C" fn file_flush_ffi(handle : NativeFileHandle) -> Int = "bitlogger_file_flush" +///| extern "C" fn file_close_ffi(handle : NativeFileHandle) -> Int = "bitlogger_file_close" +///| extern "C" fn file_seek_ffi( handle : NativeFileHandle, offset : Int, origin : Int, ) -> Int = "bitlogger_file_seek" +///| extern "C" fn file_tell_ffi(handle : NativeFileHandle) -> Int = "bitlogger_file_tell" +///| #borrow(from_path, to_path) -extern "C" fn file_rename_ffi( - from_path : Bytes, - to_path : Bytes, -) -> Int = "bitlogger_file_rename" +extern "C" fn file_rename_ffi(from_path : Bytes, to_path : Bytes) -> Int = "bitlogger_file_rename" +///| #borrow(path) extern "C" fn file_remove_ffi(path : Bytes) -> Int = "bitlogger_file_remove" +///| pub struct FileHandle { path : String raw : NativeFileHandle } +///| pub fn open_file_handle_internal(path : String, append : Bool) -> FileHandle? { let mode = if append { "ab" } else { "wb" } let raw = file_open_ffi(string_to_c_bytes(path), string_to_c_bytes(mode)) @@ -83,20 +93,27 @@ pub fn open_file_handle_internal(path : String, append : Bool) -> FileHandle? { } } -pub fn write_file_handle_internal(handle : FileHandle, content : String) -> Bool { +///| +pub fn write_file_handle_internal( + handle : FileHandle, + content : String, +) -> Bool { let bytes = string_to_c_bytes(content) let written = file_write_ffi(bytes, 1, bytes.length() - 1, handle.raw) written == bytes.length() - 1 } +///| pub fn flush_file_handle_internal(handle : FileHandle) -> Bool { file_flush_ffi(handle.raw) == 0 } +///| pub fn close_file_handle_internal(handle : FileHandle) -> Bool { file_close_ffi(handle.raw) == 0 } +///| pub fn file_size_internal(handle : FileHandle) -> Int { ignore(file_seek_ffi(handle.raw, 0, 2)) let size = file_tell_ffi(handle.raw) @@ -107,18 +124,22 @@ pub fn file_size_internal(handle : FileHandle) -> Int { } } +///| pub fn rename_file_internal(from_path : String, to_path : String) -> Bool { file_rename_ffi(string_to_c_bytes(from_path), string_to_c_bytes(to_path)) == 0 } +///| pub fn remove_file_internal(path : String) -> Bool { file_remove_ffi(string_to_c_bytes(path)) == 0 } +///| pub fn string_byte_length_internal(content : String) -> Int { string_to_c_bytes(content).length() - 1 } +///| pub fn native_files_supported_internal() -> Bool { true } diff --git a/src/utils/file_backend_stub.mbt b/src/utils/file_backend_stub.mbt index 9877d6f..ed24aee 100644 --- a/src/utils/file_backend_stub.mbt +++ b/src/utils/file_backend_stub.mbt @@ -1,7 +1,9 @@ +///| pub struct FileHandle { path : String } +///| pub fn open_file_handle_internal(path : String, append : Bool) -> FileHandle? { ignore(append) ignore(path) @@ -10,42 +12,53 @@ pub fn open_file_handle_internal(path : String, append : Bool) -> FileHandle? { None } -pub fn write_file_handle_internal(handle : FileHandle, content : String) -> Bool { +///| +pub fn write_file_handle_internal( + handle : FileHandle, + content : String, +) -> Bool { ignore(handle) ignore(content) false } +///| pub fn flush_file_handle_internal(handle : FileHandle) -> Bool { ignore(handle) false } +///| pub fn close_file_handle_internal(handle : FileHandle) -> Bool { ignore(handle) false } +///| pub fn file_size_internal(handle : FileHandle) -> Int { ignore(handle) 0 } +///| pub fn rename_file_internal(from_path : String, to_path : String) -> Bool { ignore(from_path) ignore(to_path) false } +///| pub fn remove_file_internal(path : String) -> Bool { ignore(path) false } +///| pub fn string_byte_length_internal(content : String) -> Int { content.length() } +///| pub fn native_files_supported_internal() -> Bool { false } diff --git a/src/utils/filters.mbt b/src/utils/filters.mbt index 364c5a5..3f941b2 100644 --- a/src/utils/filters.mbt +++ b/src/utils/filters.mbt @@ -1,29 +1,27 @@ +///| pub type RecordPredicate = (@core.Record) -> Bool +///| pub fn level_at_least(min_level : @core.Level) -> RecordPredicate { - fn(rec) { - rec.level.priority() >= min_level.priority() - } + fn(rec) { rec.level.priority() >= min_level.priority() } } +///| pub fn target_is(target : String) -> RecordPredicate { - fn(rec) { - rec.target == target - } + fn(rec) { rec.target == target } } +///| pub fn target_has_prefix(prefix : String) -> RecordPredicate { - fn(rec) { - rec.target.has_prefix(prefix) - } + fn(rec) { rec.target.has_prefix(prefix) } } +///| pub fn message_contains(fragment : String) -> RecordPredicate { - fn(rec) { - rec.message.contains(fragment) - } + fn(rec) { rec.message.contains(fragment) } } +///| pub fn has_field(key : String) -> RecordPredicate { fn(rec) { for field in rec.fields { @@ -35,6 +33,7 @@ pub fn has_field(key : String) -> RecordPredicate { } } +///| pub fn field_equals(key : String, value : String) -> RecordPredicate { fn(rec) { for field in rec.fields { @@ -46,16 +45,16 @@ pub fn field_equals(key : String, value : String) -> RecordPredicate { } } +///| pub fn not_(predicate : RecordPredicate) -> RecordPredicate { - fn(rec) { - !(predicate(rec)) - } + fn(rec) { !predicate(rec) } } +///| pub fn all_of(predicates : Array[RecordPredicate]) -> RecordPredicate { fn(rec) { for predicate in predicates { - if !(predicate(rec)) { + if !predicate(rec) { return false } } @@ -63,6 +62,7 @@ pub fn all_of(predicates : Array[RecordPredicate]) -> RecordPredicate { } } +///| pub fn any_of(predicates : Array[RecordPredicate]) -> RecordPredicate { fn(rec) { for predicate in predicates { diff --git a/src/utils/formatter.mbt b/src/utils/formatter.mbt index 61c81f1..de265a8 100644 --- a/src/utils/formatter.mbt +++ b/src/utils/formatter.mbt @@ -1,22 +1,27 @@ +///| pub type RecordFormatter = (@core.Record) -> String +///| pub(all) enum ColorMode { Never Auto Always } +///| pub(all) enum ColorSupport { Basic TrueColor } +///| pub(all) enum StyleMarkupMode { Disabled Builtin Full } +///| pub struct TextStyle { fg : String? bg : String? @@ -26,29 +31,34 @@ pub struct TextStyle { underline : Bool } +///| pub fn text_style( - fg~ : String? = None, - bg~ : String? = None, - bold~ : Bool = false, - dim~ : Bool = false, - italic~ : Bool = false, - underline~ : Bool = false, + fg? : String? = None, + bg? : String? = None, + bold? : Bool = false, + dim? : Bool = false, + italic? : Bool = false, + underline? : Bool = false, ) -> TextStyle { { fg, bg, bold, dim, italic, underline } } +///| pub struct StyleTagRegistry { entries : Map[String, TextStyle] } +///| fn normalize_style_tag_name(name : String) -> String { name.trim().to_lower().to_owned() } +///| pub fn style_tag_registry() -> StyleTagRegistry { { entries: {} } } +///| fn merge_text_style(base : TextStyle, overlay : TextStyle) -> TextStyle { { fg: match overlay.fg { @@ -66,91 +76,113 @@ fn merge_text_style(base : TextStyle, overlay : TextStyle) -> TextStyle { } } +///| pub fn StyleTagRegistry::set_tag( self : StyleTagRegistry, name : String, - style~ : TextStyle = text_style(), - fg~ : String? = None, - bg~ : String? = None, - bold~ : Bool = false, - dim~ : Bool = false, - italic~ : Bool = false, - underline~ : Bool = false, + style? : TextStyle = text_style(), + fg? : String? = None, + bg? : String? = None, + bold? : Bool = false, + dim? : Bool = false, + italic? : Bool = false, + underline? : Bool = false, ) -> StyleTagRegistry { - let overlay = text_style(fg=fg, bg=bg, bold=bold, dim=dim, italic=italic, underline=underline) - self.entries[normalize_style_tag_name(name)] = merge_text_style(style, overlay) + let overlay = text_style(fg~, bg~, bold~, dim~, italic~, underline~) + self.entries[normalize_style_tag_name(name)] = merge_text_style( + style, overlay, + ) self } -pub fn StyleTagRegistry::get(self : StyleTagRegistry, name : String) -> TextStyle? { +///| +pub fn StyleTagRegistry::get( + self : StyleTagRegistry, + name : String, +) -> TextStyle? { self.entries.get(normalize_style_tag_name(name)) } -pub fn StyleTagRegistry::contains(self : StyleTagRegistry, name : String) -> Bool { +///| +pub fn StyleTagRegistry::contains( + self : StyleTagRegistry, + name : String, +) -> Bool { self.entries.contains(normalize_style_tag_name(name)) } +///| pub fn StyleTagRegistry::define_alias( self : StyleTagRegistry, name : String, target : String, ) -> StyleTagRegistry { match self.get(target) { - Some(style) => self.set_tag(name, style=style) - None => match global_style_tag_registry().get(target) { - Some(style) => self.set_tag(name, style=style) - None => match builtin_text_style_for_tag(normalize_style_tag_name(target)) { - Some(style) => self.set_tag(name, style=style) - None => self + Some(style) => self.set_tag(name, style~) + None => + match global_style_tag_registry().get(target) { + Some(style) => self.set_tag(name, style~) + None => + match builtin_text_style_for_tag(normalize_style_tag_name(target)) { + Some(style) => self.set_tag(name, style~) + None => self + } } - } } } +///| pub fn default_style_tag_registry() -> StyleTagRegistry { style_tag_registry() - .set_tag("black", fg=Some("black")) - .set_tag("red", fg=Some("red")) - .set_tag("green", fg=Some("green")) - .set_tag("yellow", fg=Some("yellow")) - .set_tag("blue", fg=Some("blue")) - .set_tag("magenta", fg=Some("magenta")) - .set_tag("cyan", fg=Some("cyan")) - .set_tag("white", fg=Some("white")) - .set_tag("bright_black", fg=Some("bright_black")) - .set_tag("bright_red", fg=Some("bright_red")) - .set_tag("bright_green", fg=Some("bright_green")) - .set_tag("bright_yellow", fg=Some("bright_yellow")) - .set_tag("bright_blue", fg=Some("bright_blue")) - .set_tag("bright_magenta", fg=Some("bright_magenta")) - .set_tag("bright_cyan", fg=Some("bright_cyan")) - .set_tag("bright_white", fg=Some("bright_white")) - .set_tag("accent", fg=Some("bright_cyan"), bold=true) - .set_tag("info", fg=Some("cyan")) - .set_tag("success", fg=Some("green"), bold=true) - .set_tag("warning", fg=Some("yellow"), bold=true) - .set_tag("danger", fg=Some("red"), bold=true) - .set_tag("muted", fg=Some("bright_black"), dim=true) - .set_tag("b", bold=true) - .set_tag("dim", dim=true) - .set_tag("i", italic=true) - .set_tag("u", underline=true) + .set_tag("black", fg=Some("black")) + .set_tag("red", fg=Some("red")) + .set_tag("green", fg=Some("green")) + .set_tag("yellow", fg=Some("yellow")) + .set_tag("blue", fg=Some("blue")) + .set_tag("magenta", fg=Some("magenta")) + .set_tag("cyan", fg=Some("cyan")) + .set_tag("white", fg=Some("white")) + .set_tag("bright_black", fg=Some("bright_black")) + .set_tag("bright_red", fg=Some("bright_red")) + .set_tag("bright_green", fg=Some("bright_green")) + .set_tag("bright_yellow", fg=Some("bright_yellow")) + .set_tag("bright_blue", fg=Some("bright_blue")) + .set_tag("bright_magenta", fg=Some("bright_magenta")) + .set_tag("bright_cyan", fg=Some("bright_cyan")) + .set_tag("bright_white", fg=Some("bright_white")) + .set_tag("accent", fg=Some("bright_cyan"), bold=true) + .set_tag("info", fg=Some("cyan")) + .set_tag("success", fg=Some("green"), bold=true) + .set_tag("warning", fg=Some("yellow"), bold=true) + .set_tag("danger", fg=Some("red"), bold=true) + .set_tag("muted", fg=Some("bright_black"), dim=true) + .set_tag("b", bold=true) + .set_tag("dim", dim=true) + .set_tag("i", italic=true) + .set_tag("u", underline=true) } -let global_style_tag_registry_ref : Ref[StyleTagRegistry] = Ref(style_tag_registry()) +///| +let global_style_tag_registry_ref : Ref[StyleTagRegistry] = Ref( + style_tag_registry(), +) +///| pub fn global_style_tag_registry() -> StyleTagRegistry { global_style_tag_registry_ref.val } +///| pub fn set_global_style_tag_registry(registry : StyleTagRegistry) -> Unit { global_style_tag_registry_ref.val = registry } +///| pub fn reset_global_style_tag_registry() -> Unit { global_style_tag_registry_ref.val = style_tag_registry() } +///| priv struct InlineStyle { fg_code : String? bg_code : String? @@ -162,24 +194,29 @@ priv struct InlineStyle { underline : Bool } +///| priv struct StyledSegment { text : String style : InlineStyle } +///| priv struct StyleFrame { tag : String style : InlineStyle } +///| fn code_unit(ch : Char) -> Int { ch.to_int() } +///| fn code_unit16(ch : UInt16) -> Int { ch.to_int() } +///| pub struct TextFormatter { show_timestamp : Bool show_level : Bool @@ -196,20 +233,21 @@ pub struct TextFormatter { style_tags : StyleTagRegistry? } +///| pub fn text_formatter( - show_timestamp~ : Bool = true, - show_level~ : Bool = true, - show_target~ : Bool = true, - show_fields~ : Bool = true, - separator~ : String = " ", - field_separator~ : String = " ", - template~ : String = "", - color_mode~ : ColorMode = ColorMode::Never, - color_support~ : ColorSupport = ColorSupport::TrueColor, - style_markup~ : StyleMarkupMode = StyleMarkupMode::Full, - target_style_markup~ : StyleMarkupMode = StyleMarkupMode::Disabled, - fields_style_markup~ : StyleMarkupMode = StyleMarkupMode::Disabled, - style_tags~ : StyleTagRegistry? = None, + show_timestamp? : Bool = true, + show_level? : Bool = true, + show_target? : Bool = true, + show_fields? : Bool = true, + separator? : String = " ", + field_separator? : String = " ", + template? : String = "", + color_mode? : ColorMode = ColorMode::Never, + color_support? : ColorSupport = ColorSupport::TrueColor, + style_markup? : StyleMarkupMode = StyleMarkupMode::Full, + target_style_markup? : StyleMarkupMode = StyleMarkupMode::Disabled, + fields_style_markup? : StyleMarkupMode = StyleMarkupMode::Disabled, + style_tags? : StyleTagRegistry? = None, ) -> TextFormatter { { show_timestamp, @@ -228,6 +266,7 @@ pub fn text_formatter( } } +///| pub fn color_support_label(support : ColorSupport) -> String { match support { ColorSupport::Basic => "basic" @@ -235,7 +274,11 @@ pub fn color_support_label(support : ColorSupport) -> String { } } -pub fn TextFormatter::with_color_support(self : TextFormatter, color_support : ColorSupport) -> TextFormatter { +///| +pub fn TextFormatter::with_color_support( + self : TextFormatter, + color_support : ColorSupport, +) -> TextFormatter { text_formatter( show_timestamp=self.show_timestamp, show_level=self.show_level, @@ -245,7 +288,7 @@ pub fn TextFormatter::with_color_support(self : TextFormatter, color_support : C field_separator=self.field_separator, template=self.template, color_mode=self.color_mode, - color_support=color_support, + color_support~, style_markup=self.style_markup, target_style_markup=self.target_style_markup, fields_style_markup=self.fields_style_markup, @@ -253,6 +296,7 @@ pub fn TextFormatter::with_color_support(self : TextFormatter, color_support : C ) } +///| pub fn style_markup_mode_label(mode : StyleMarkupMode) -> String { match mode { StyleMarkupMode::Disabled => "disabled" @@ -261,7 +305,11 @@ pub fn style_markup_mode_label(mode : StyleMarkupMode) -> String { } } -pub fn TextFormatter::with_style_markup(self : TextFormatter, style_markup : StyleMarkupMode) -> TextFormatter { +///| +pub fn TextFormatter::with_style_markup( + self : TextFormatter, + style_markup : StyleMarkupMode, +) -> TextFormatter { text_formatter( show_timestamp=self.show_timestamp, show_level=self.show_level, @@ -272,17 +320,21 @@ pub fn TextFormatter::with_style_markup(self : TextFormatter, style_markup : Sty template=self.template, color_mode=self.color_mode, color_support=self.color_support, - style_markup=style_markup, + style_markup~, target_style_markup=self.target_style_markup, fields_style_markup=self.fields_style_markup, style_tags=self.style_tags, ) } -pub fn TextFormatter::without_style_markup(self : TextFormatter) -> TextFormatter { +///| +pub fn TextFormatter::without_style_markup( + self : TextFormatter, +) -> TextFormatter { self.with_style_markup(StyleMarkupMode::Disabled) } +///| pub fn TextFormatter::with_target_style_markup( self : TextFormatter, style_markup : StyleMarkupMode, @@ -304,6 +356,7 @@ pub fn TextFormatter::with_target_style_markup( ) } +///| pub fn TextFormatter::with_fields_style_markup( self : TextFormatter, style_markup : StyleMarkupMode, @@ -325,7 +378,11 @@ pub fn TextFormatter::with_fields_style_markup( ) } -pub fn TextFormatter::with_style_tags(self : TextFormatter, style_tags : StyleTagRegistry) -> TextFormatter { +///| +pub fn TextFormatter::with_style_tags( + self : TextFormatter, + style_tags : StyleTagRegistry, +) -> TextFormatter { text_formatter( show_timestamp=self.show_timestamp, show_level=self.show_level, @@ -343,6 +400,7 @@ pub fn TextFormatter::with_style_tags(self : TextFormatter, style_tags : StyleTa ) } +///| pub fn color_mode_label(mode : ColorMode) -> String { match mode { ColorMode::Never => "never" @@ -351,17 +409,20 @@ pub fn color_mode_label(mode : ColorMode) -> String { } } +///| fn use_ansi_color(mode : ColorMode) -> Bool { match mode { ColorMode::Never => false ColorMode::Always => true - ColorMode::Auto => match @env.get_env_var("NO_COLOR") { - Some(_) => false - None => true - } + ColorMode::Auto => + match @env.get_env_var("NO_COLOR") { + Some(_) => false + None => true + } } } +///| fn ansi_wrap(text : String, code : String, enabled : Bool) -> String { if !enabled || text == "" { text @@ -370,7 +431,12 @@ fn ansi_wrap(text : String, code : String, enabled : Bool) -> String { } } -fn ansi_wrap_with_style(text : String, style : InlineStyle, enabled : Bool) -> String { +///| +fn ansi_wrap_with_style( + text : String, + style : InlineStyle, + enabled : Bool, +) -> String { if !enabled || text == "" { text } else { @@ -404,6 +470,7 @@ fn ansi_wrap_with_style(text : String, style : InlineStyle, enabled : Bool) -> S } } +///| fn default_inline_style() -> InlineStyle { { fg_code: None, @@ -417,6 +484,7 @@ fn default_inline_style() -> InlineStyle { } } +///| fn named_color_code(tag : String) -> String? { match tag { "black" => Some("30") @@ -439,6 +507,7 @@ fn named_color_code(tag : String) -> String? { } } +///| fn named_bg_color_code(tag : String) -> String? { match tag { "black" => Some("40") @@ -461,6 +530,7 @@ fn named_bg_color_code(tag : String) -> String? { } } +///| fn basic_name_from_hex(value : String) -> String { let r = hex_pair_to_int(value.unsafe_get(1), value.unsafe_get(2)) let g = hex_pair_to_int(value.unsafe_get(3), value.unsafe_get(4)) @@ -495,18 +565,17 @@ fn basic_name_from_hex(value : String) -> String { } else { "green" } + } else if r > 96 && g < 96 { + "magenta" + } else if g > 96 && r < 96 { + "cyan" } else { - if r > 96 && g < 96 { - "magenta" - } else if g > 96 && r < 96 { - "cyan" - } else { - "blue" - } + "blue" } } } +///| fn resolve_inline_color_code( formatter : TextFormatter, basic_name : String?, @@ -514,21 +583,25 @@ fn resolve_inline_color_code( ) -> String { match formatter.color_support { ColorSupport::TrueColor => truecolor_code - ColorSupport::Basic => match basic_name { - Some(name) => named_code_from_basic_name(name).unwrap_or(truecolor_code) - None => truecolor_code - } + ColorSupport::Basic => + match basic_name { + Some(name) => named_code_from_basic_name(name).unwrap_or(truecolor_code) + None => truecolor_code + } } } +///| fn named_code_from_basic_name(name : String) -> String? { named_color_code(name) } +///| fn named_bg_code_from_basic_name(name : String) -> String? { named_bg_color_code(name) } +///| fn resolve_inline_bg_code( formatter : TextFormatter, basic_name : String?, @@ -536,13 +609,16 @@ fn resolve_inline_bg_code( ) -> String { match formatter.color_support { ColorSupport::TrueColor => truecolor_code - ColorSupport::Basic => match basic_name { - Some(name) => named_bg_code_from_basic_name(name).unwrap_or(truecolor_code) - None => truecolor_code - } + ColorSupport::Basic => + match basic_name { + Some(name) => + named_bg_code_from_basic_name(name).unwrap_or(truecolor_code) + None => truecolor_code + } } } +///| fn is_hex_color(value : String) -> Bool { if value.length() != 7 { return false @@ -562,6 +638,7 @@ fn is_hex_color(value : String) -> Bool { true } +///| fn hex_to_int(ch : UInt16) -> Int { let code = code_unit16(ch) if code >= code_unit('0') && code <= code_unit('9') { @@ -573,10 +650,12 @@ fn hex_to_int(ch : UInt16) -> Int { } } +///| fn hex_pair_to_int(high : UInt16, low : UInt16) -> Int { hex_to_int(high) * 16 + hex_to_int(low) } +///| fn rgb_fg_code(value : String) -> String { let r = hex_pair_to_int(value.unsafe_get(1), value.unsafe_get(2)) let g = hex_pair_to_int(value.unsafe_get(3), value.unsafe_get(4)) @@ -584,6 +663,7 @@ fn rgb_fg_code(value : String) -> String { "38;2;\{r};\{g};\{b}" } +///| fn rgb_bg_code(value : String) -> String { let r = hex_pair_to_int(value.unsafe_get(4), value.unsafe_get(5)) let g = hex_pair_to_int(value.unsafe_get(6), value.unsafe_get(7)) @@ -591,6 +671,7 @@ fn rgb_bg_code(value : String) -> String { "48;2;\{r};\{g};\{b}" } +///| fn rgb_bg_code_from_hex(value : String) -> String { let r = hex_pair_to_int(value.unsafe_get(1), value.unsafe_get(2)) let g = hex_pair_to_int(value.unsafe_get(3), value.unsafe_get(4)) @@ -598,6 +679,7 @@ fn rgb_bg_code_from_hex(value : String) -> String { "48;2;\{r};\{g};\{b}" } +///| fn inline_style_from_text_style( base : InlineStyle, style : TextStyle, @@ -609,15 +691,24 @@ fn inline_style_from_text_style( let normalized = normalize_style_tag_name(value) match named_color_code(normalized) { Some(code) => Some((Some(code), Some(normalized))) - None => if is_hex_color(value) { - let basic_name = basic_name_from_hex(value) - Some(( - Some(resolve_inline_color_code(formatter, Some(basic_name), rgb_fg_code(value))), - Some(basic_name), - )) - } else { - None - } + None => + if is_hex_color(value) { + let basic_name = basic_name_from_hex(value) + Some( + ( + Some( + resolve_inline_color_code( + formatter, + Some(basic_name), + rgb_fg_code(value), + ), + ), + Some(basic_name), + ), + ) + } else { + None + } } } } @@ -634,36 +725,48 @@ fn inline_style_from_text_style( } Some((Some(code), Some(bg_name))) } - None => if is_hex_color(value) { - let basic_name = basic_name_from_hex(value) - Some(( - Some(resolve_inline_bg_code(formatter, Some(basic_name), rgb_bg_code_from_hex(value))), - Some(basic_name), - )) - } else { - None - } + None => + if is_hex_color(value) { + let basic_name = basic_name_from_hex(value) + Some( + ( + Some( + resolve_inline_bg_code( + formatter, + Some(basic_name), + rgb_bg_code_from_hex(value), + ), + ), + Some(basic_name), + ), + ) + } else { + None + } } } } match fg { - Some((next_fg_code, next_fg_name)) => match bg { - Some((next_bg_code, next_bg_name)) => Some({ - fg_code: next_fg_code, - bg_code: next_bg_code, - fg_basic_name: next_fg_name, - bg_basic_name: next_bg_name, - bold: base.bold || style.bold, - dim: base.dim || style.dim, - italic: base.italic || style.italic, - underline: base.underline || style.underline, - }) - None => None - } + Some((next_fg_code, next_fg_name)) => + match bg { + Some((next_bg_code, next_bg_name)) => + Some({ + fg_code: next_fg_code, + bg_code: next_bg_code, + fg_basic_name: next_fg_name, + bg_basic_name: next_bg_name, + bold: base.bold || style.bold, + dim: base.dim || style.dim, + italic: base.italic || style.italic, + underline: base.underline || style.underline, + }) + None => None + } None => None } } +///| fn builtin_text_style_for_tag(tag : String) -> TextStyle? { match normalize_style_tag_name(tag) { "black" => Some(text_style(fg=Some("black"))) @@ -696,41 +799,63 @@ fn builtin_text_style_for_tag(tag : String) -> TextStyle? { } } -fn resolve_registered_text_style(tag : String, formatter : TextFormatter) -> TextStyle? { +///| +fn resolve_registered_text_style( + tag : String, + formatter : TextFormatter, +) -> TextStyle? { let normalized = normalize_style_tag_name(tag) match formatter.style_markup { StyleMarkupMode::Builtin => return builtin_text_style_for_tag(normalized) _ => () } match formatter.style_tags { - Some(registry) => match registry.get(normalized) { - Some(style) => Some(style) - None => match global_style_tag_registry().get(normalized) { + Some(registry) => + match registry.get(normalized) { + Some(style) => Some(style) + None => + match global_style_tag_registry().get(normalized) { + Some(style) => Some(style) + None => builtin_text_style_for_tag(normalized) + } + } + None => + match global_style_tag_registry().get(normalized) { Some(style) => Some(style) None => builtin_text_style_for_tag(normalized) } - } - None => match global_style_tag_registry().get(normalized) { - Some(style) => Some(style) - None => builtin_text_style_for_tag(normalized) - } } } -fn apply_inline_tag(style : InlineStyle, tag : String, formatter : TextFormatter) -> InlineStyle? { +///| +fn apply_inline_tag( + style : InlineStyle, + tag : String, + formatter : TextFormatter, +) -> InlineStyle? { let normalized = normalize_style_tag_name(tag) if is_hex_color(tag) { let basic_name = basic_name_from_hex(tag) Some({ ..style, - fg_code: Some(resolve_inline_color_code(formatter, Some(basic_name), rgb_fg_code(tag))), + fg_code: Some( + resolve_inline_color_code(formatter, Some(basic_name), rgb_fg_code(tag)), + ), fg_basic_name: Some(basic_name), }) - } else if normalized.length() == 10 && normalized.has_prefix("bg:") && is_hex_color(normalized[3:].to_owned()) { + } else if normalized.length() == 10 && + normalized.has_prefix("bg:") && + is_hex_color(normalized[3:].to_owned()) { let basic_name = basic_name_from_hex(normalized[3:].to_owned()) Some({ ..style, - bg_code: Some(resolve_inline_bg_code(formatter, Some(basic_name), rgb_bg_code(normalized))), + bg_code: Some( + resolve_inline_bg_code( + formatter, + Some(basic_name), + rgb_bg_code(normalized), + ), + ), bg_basic_name: Some(basic_name), }) } else { @@ -741,6 +866,7 @@ fn apply_inline_tag(style : InlineStyle, tag : String, formatter : TextFormatter } } +///| fn push_plain_segment( segments : Array[StyledSegment], buffer : StringBuilder, @@ -753,6 +879,7 @@ fn push_plain_segment( } } +///| fn pop_named_style_frame(stack : Array[StyleFrame], tag : String) -> Bool { let normalized = normalize_style_tag_name(tag) if stack.length() <= 1 { @@ -769,6 +896,7 @@ fn pop_named_style_frame(stack : Array[StyleFrame], tag : String) -> Bool { false } +///| fn has_named_style_frame(stack : Array[StyleFrame], tag : String) -> Bool { let normalized = normalize_style_tag_name(tag) if stack.length() <= 1 { @@ -782,7 +910,11 @@ fn has_named_style_frame(stack : Array[StyleFrame], tag : String) -> Bool { false } -fn parse_inline_markup(input : String, formatter : TextFormatter) -> Array[StyledSegment] { +///| +fn parse_inline_markup( + input : String, + formatter : TextFormatter, +) -> Array[StyledSegment] { let segments : Array[StyledSegment] = [] let buffer = StringBuilder::new() let stack : Array[StyleFrame] = [{ tag: "", style: default_inline_style() }] @@ -845,7 +977,12 @@ fn parse_inline_markup(input : String, formatter : TextFormatter) -> Array[Style segments } -fn render_styled_text(text : String, formatter : TextFormatter, mode : StyleMarkupMode) -> String { +///| +fn render_styled_text( + text : String, + formatter : TextFormatter, + mode : StyleMarkupMode, +) -> String { match mode { StyleMarkupMode::Disabled => return text _ => () @@ -860,10 +997,12 @@ fn render_styled_text(text : String, formatter : TextFormatter, mode : StyleMark out.to_string() } +///| fn render_inline_markup(message : String, formatter : TextFormatter) -> String { render_styled_text(message, formatter, formatter.style_markup) } +///| fn level_ansi_code(level : @core.Level) -> String { match level { @core.Level::Trace => "90" @@ -874,6 +1013,7 @@ fn level_ansi_code(level : @core.Level) -> String { } } +///| fn fields_to_json(fields : Array[@core.Field]) -> Json { let obj : Map[String, Json] = {} for item in fields { @@ -882,58 +1022,89 @@ fn fields_to_json(fields : Array[@core.Field]) -> Json { Json::object(obj) } +///| fn timestamp_text(rec : @core.Record, formatter : TextFormatter) -> String { if formatter.show_timestamp && rec.timestamp_ms != 0UL { - ansi_wrap(rec.timestamp_ms.to_string(), "90", use_ansi_color(formatter.color_mode)) + ansi_wrap( + rec.timestamp_ms.to_string(), + "90", + use_ansi_color(formatter.color_mode), + ) } else { "" } } +///| fn level_text(rec : @core.Record, formatter : TextFormatter) -> String { if formatter.show_level { - ansi_wrap(rec.level.label(), level_ansi_code(rec.level), use_ansi_color(formatter.color_mode)) + ansi_wrap( + rec.level.label(), + level_ansi_code(rec.level), + use_ansi_color(formatter.color_mode), + ) } else { "" } } +///| fn target_text(rec : @core.Record, formatter : TextFormatter) -> String { if formatter.show_target && rec.target != "" { - let rendered = render_styled_text(rec.target, formatter, formatter.target_style_markup) + let rendered = render_styled_text( + rec.target, + formatter, + formatter.target_style_markup, + ) ansi_wrap(rendered, "34", use_ansi_color(formatter.color_mode)) } else { "" } } +///| fn format_field_text(field : @core.Field, formatter : TextFormatter) -> String { - let value = render_styled_text(field.value, formatter, formatter.fields_style_markup) + let value = render_styled_text( + field.value, + formatter, + formatter.fields_style_markup, + ) "\{field.key}=\{value}" } +///| fn fields_text(rec : @core.Record, formatter : TextFormatter) -> String { if formatter.show_fields && rec.fields.length() != 0 { - let content = rec.fields.map(fn(field) { format_field_text(field, formatter) }).join(formatter.field_separator) + let content = rec.fields + .map(fn(field) { format_field_text(field, formatter) }) + .join(formatter.field_separator) ansi_wrap(content, "35", use_ansi_color(formatter.color_mode)) } else { "" } } +///| fn render_template(rec : @core.Record, formatter : TextFormatter) -> String { formatter.template - .replace_all(old="{timestamp}", new=timestamp_text(rec, formatter)) - .replace_all(old="{timestamp_ms}", new=timestamp_text(rec, formatter)) - .replace_all(old="{level}", new=level_text(rec, formatter)) - .replace_all(old="{target}", new=target_text(rec, formatter)) - .replace_all(old="{message}", new=render_inline_markup(rec.message, formatter)) - .replace_all(old="{fields}", new=fields_text(rec, formatter)) - .trim() - .to_owned() + .replace_all(old="{timestamp}", new=timestamp_text(rec, formatter)) + .replace_all(old="{timestamp_ms}", new=timestamp_text(rec, formatter)) + .replace_all(old="{level}", new=level_text(rec, formatter)) + .replace_all(old="{target}", new=target_text(rec, formatter)) + .replace_all( + old="{message}", + new=render_inline_markup(rec.message, formatter), + ) + .replace_all(old="{fields}", new=fields_text(rec, formatter)) + .trim() + .to_owned() } -pub fn format_text(rec : @core.Record, formatter~ : TextFormatter = text_formatter()) -> String { +///| +pub fn format_text( + rec : @core.Record, + formatter? : TextFormatter = text_formatter(), +) -> String { if formatter.template != "" { return render_template(rec, formatter) } @@ -957,6 +1128,7 @@ pub fn format_text(rec : @core.Record, formatter~ : TextFormatter = text_formatt } } +///| pub fn format_json(rec : @core.Record) -> String { let obj : Map[String, Json] = { "level": Json::string(rec.level.label()), diff --git a/src/utils/moon.pkg b/src/utils/moon.pkg index 0ebc5b8..c8d344f 100644 --- a/src/utils/moon.pkg +++ b/src/utils/moon.pkg @@ -1,7 +1,7 @@ import { - "Nanaloveyuki/BitLogger/src/core" @core, - "maria/json_parser" @json_parser, - "moonbitlang/core/env" @env, + "Nanaloveyuki/BitLogger/src/core", + "maria/json_parser", + "moonbitlang/core/env", "moonbitlang/core/json", "moonbitlang/core/ref", } diff --git a/src/utils/patchers.mbt b/src/utils/patchers.mbt index 6b94d51..3d56c3b 100644 --- a/src/utils/patchers.mbt +++ b/src/utils/patchers.mbt @@ -1,21 +1,22 @@ +///| pub type RecordPatch = (@core.Record) -> @core.Record +///| pub fn identity_patch() -> RecordPatch { fn(rec) { rec } } +///| pub fn set_target(target : String) -> RecordPatch { - fn(rec) { - rec.with_target(target) - } + fn(rec) { rec.with_target(target) } } +///| pub fn prefix_message(prefix : String) -> RecordPatch { - fn(rec) { - rec.with_message("\{prefix}\{rec.message}") - } + fn(rec) { rec.with_message("\{prefix}\{rec.message}") } } +///| pub fn append_fields(extra_fields : Array[@core.Field]) -> RecordPatch { fn(rec) { if extra_fields.length() == 0 { @@ -28,30 +29,40 @@ pub fn append_fields(extra_fields : Array[@core.Field]) -> RecordPatch { } } -pub fn redact_field(key : String, placeholder~ : String = "***") -> RecordPatch { +///| +pub fn redact_field(key : String, placeholder? : String = "***") -> RecordPatch { fn(rec) { - rec.with_fields(rec.fields.map(fn(field) { - if field.key == key { - field.with_value(placeholder) - } else { - field - } - })) + rec.with_fields( + rec.fields.map(fn(field) { + if field.key == key { + field.with_value(placeholder) + } else { + field + } + }), + ) } } -pub fn redact_fields(keys : Array[String], placeholder~ : String = "***") -> RecordPatch { +///| +pub fn redact_fields( + keys : Array[String], + placeholder? : String = "***", +) -> RecordPatch { fn(rec) { - rec.with_fields(rec.fields.map(fn(field) { - if keys.contains(field.key) { - field.with_value(placeholder) - } else { - field - } - })) + rec.with_fields( + rec.fields.map(fn(field) { + if keys.contains(field.key) { + field.with_value(placeholder) + } else { + field + } + }), + ) } } +///| pub fn compose_patches(patches : Array[RecordPatch]) -> RecordPatch { fn(rec) { let mut current = rec diff --git a/src/utils/runtime_file_state.mbt b/src/utils/runtime_file_state.mbt index 9b9047e..3f1ea71 100644 --- a/src/utils/runtime_file_state.mbt +++ b/src/utils/runtime_file_state.mbt @@ -1,3 +1,4 @@ +///| pub struct RuntimeFileState { file : FileSinkState queued : Bool @@ -5,16 +6,20 @@ pub struct RuntimeFileState { dropped_count : Int } +///| pub fn RuntimeFileState::new( file : FileSinkState, - queued~ : Bool = false, - pending_count~ : Int = 0, - dropped_count~ : Int = 0, + queued? : Bool = false, + pending_count? : Int = 0, + dropped_count? : Int = 0, ) -> RuntimeFileState { { file, queued, pending_count, dropped_count } } -fn file_sink_policy_to_json_value(policy : FileSinkPolicy) -> @json_parser.JsonValue { +///| +fn file_sink_policy_to_json_value( + policy : FileSinkPolicy, +) -> @json_parser.JsonValue { let obj : Map[String, @json_parser.JsonValue] = { "append": @json_parser.JsonValue::Bool(policy.append), "auto_flush": @json_parser.JsonValue::Bool(policy.auto_flush), @@ -26,11 +31,18 @@ fn file_sink_policy_to_json_value(policy : FileSinkPolicy) -> @json_parser.JsonV @json_parser.JsonValue::Object(obj) } -pub fn file_sink_policy_to_json(policy : FileSinkPolicy) -> @json_parser.JsonValue { +///| +pub fn file_sink_policy_to_json( + policy : FileSinkPolicy, +) -> @json_parser.JsonValue { file_sink_policy_to_json_value(policy) } -pub fn stringify_file_sink_policy(policy : FileSinkPolicy, pretty~ : Bool = false) -> String { +///| +pub fn stringify_file_sink_policy( + policy : FileSinkPolicy, + pretty? : Bool = false, +) -> String { let value = file_sink_policy_to_json_value(policy) if pretty { @json_parser.stringify_pretty(value, 2) @@ -39,16 +51,27 @@ pub fn stringify_file_sink_policy(policy : FileSinkPolicy, pretty~ : Bool = fals } } -fn file_sink_state_to_json_value(state : FileSinkState) -> @json_parser.JsonValue { +///| +fn file_sink_state_to_json_value( + state : FileSinkState, +) -> @json_parser.JsonValue { let obj : Map[String, @json_parser.JsonValue] = { "path": @json_parser.JsonValue::String(state.path), "available": @json_parser.JsonValue::Bool(state.available), "append": @json_parser.JsonValue::Bool(state.append), "auto_flush": @json_parser.JsonValue::Bool(state.auto_flush), - "open_failures": @json_parser.JsonValue::Number(state.open_failures.to_double()), - "write_failures": @json_parser.JsonValue::Number(state.write_failures.to_double()), - "flush_failures": @json_parser.JsonValue::Number(state.flush_failures.to_double()), - "rotation_failures": @json_parser.JsonValue::Number(state.rotation_failures.to_double()), + "open_failures": @json_parser.JsonValue::Number( + state.open_failures.to_double(), + ), + "write_failures": @json_parser.JsonValue::Number( + state.write_failures.to_double(), + ), + "flush_failures": @json_parser.JsonValue::Number( + state.flush_failures.to_double(), + ), + "rotation_failures": @json_parser.JsonValue::Number( + state.rotation_failures.to_double(), + ), } match state.rotation { None => obj["rotation"] = @json_parser.JsonValue::Null @@ -57,11 +80,16 @@ fn file_sink_state_to_json_value(state : FileSinkState) -> @json_parser.JsonValu @json_parser.JsonValue::Object(obj) } +///| pub fn file_sink_state_to_json(state : FileSinkState) -> @json_parser.JsonValue { file_sink_state_to_json_value(state) } -pub fn stringify_file_sink_state(state : FileSinkState, pretty~ : Bool = false) -> String { +///| +pub fn stringify_file_sink_state( + state : FileSinkState, + pretty? : Bool = false, +) -> String { let value = file_sink_state_to_json_value(state) if pretty { @json_parser.stringify_pretty(value, 2) @@ -70,16 +98,27 @@ pub fn stringify_file_sink_state(state : FileSinkState, pretty~ : Bool = false) } } -pub fn runtime_file_state_to_json(state : RuntimeFileState) -> @json_parser.JsonValue { +///| +pub fn runtime_file_state_to_json( + state : RuntimeFileState, +) -> @json_parser.JsonValue { @json_parser.JsonValue::Object({ "file": file_sink_state_to_json_value(state.file), "queued": @json_parser.JsonValue::Bool(state.queued), - "pending_count": @json_parser.JsonValue::Number(state.pending_count.to_double()), - "dropped_count": @json_parser.JsonValue::Number(state.dropped_count.to_double()), + "pending_count": @json_parser.JsonValue::Number( + state.pending_count.to_double(), + ), + "dropped_count": @json_parser.JsonValue::Number( + state.dropped_count.to_double(), + ), }) } -pub fn stringify_runtime_file_state(state : RuntimeFileState, pretty~ : Bool = false) -> String { +///| +pub fn stringify_runtime_file_state( + state : RuntimeFileState, + pretty? : Bool = false, +) -> String { let value = runtime_file_state_to_json(state) if pretty { @json_parser.stringify_pretty(value, 2) diff --git a/src/utils/sink_models.mbt b/src/utils/sink_models.mbt index f0e9512..7dad8df 100644 --- a/src/utils/sink_models.mbt +++ b/src/utils/sink_models.mbt @@ -1,8 +1,10 @@ +///| pub struct FileRotation { max_bytes : Int max_backups : Int } +///| pub struct FileSinkState { path : String available : Bool @@ -15,30 +17,33 @@ pub struct FileSinkState { rotation_failures : Int } +///| pub struct FileSinkPolicy { append : Bool auto_flush : Bool rotation : FileRotation? } +///| pub fn FileSinkPolicy::new( - append~ : Bool = true, - auto_flush~ : Bool = true, - rotation~ : FileRotation? = None, + append? : Bool = true, + auto_flush? : Bool = true, + rotation? : FileRotation? = None, ) -> FileSinkPolicy { { append, auto_flush, rotation } } +///| pub fn FileSinkState::new( path : String, - available~ : Bool = false, - append~ : Bool = true, - auto_flush~ : Bool = true, - rotation~ : FileRotation? = None, - open_failures~ : Int = 0, - write_failures~ : Int = 0, - flush_failures~ : Int = 0, - rotation_failures~ : Int = 0, + available? : Bool = false, + append? : Bool = true, + auto_flush? : Bool = true, + rotation? : FileRotation? = None, + open_failures? : Int = 0, + write_failures? : Int = 0, + flush_failures? : Int = 0, + rotation_failures? : Int = 0, ) -> FileSinkState { { path, @@ -53,13 +58,23 @@ pub fn FileSinkState::new( } } -pub fn file_rotation(max_bytes : Int, max_backups~ : Int = 1) -> FileRotation { +///| +pub fn file_rotation(max_bytes : Int, max_backups? : Int = 1) -> FileRotation { { - max_bytes: if max_bytes <= 0 { 1 } else { max_bytes }, - max_backups: if max_backups <= 0 { 1 } else { max_backups }, + max_bytes: if max_bytes <= 0 { + 1 + } else { + max_bytes + }, + max_backups: if max_backups <= 0 { + 1 + } else { + max_backups + }, } } +///| pub(all) enum QueueOverflowPolicy { DropNewest DropOldest