diff --git a/bitlogger_async/BitLoggerAsync_test.mbt b/bitlogger_async/BitLoggerAsync_test.mbt new file mode 100644 index 0000000..fb1fd4f --- /dev/null +++ b/bitlogger_async/BitLoggerAsync_test.mbt @@ -0,0 +1,116 @@ +async test "shutdown drains pending records" { + let written : Ref[Array[String]] = Ref::new([]) + let logger = async_logger( + @bitlogger.callback_sink(fn(rec) { + written.val.push(rec.message) + }), + config=AsyncLoggerConfig::new( + max_pending=4, + overflow=AsyncOverflowPolicy::Blocking, + ), + min_level=@bitlogger.Level::Info, + target="async.test", + ) + + @async.with_task_group(group => { + group.spawn_bg(() => logger.run()) + logger.info("one") + logger.info("two") + logger.shutdown() + }) + + inspect(logger.is_closed(), content="true") + inspect(logger.is_running(), content="false") + inspect(logger.has_failed(), content="false") + inspect(logger.pending_count(), content="0") + inspect(written.val.length(), content="2") + inspect(written.val[0], content="one") + inspect(written.val[1], content="two") +} + +async test "close clear counts abandoned records as dropped" { + let logger = async_logger( + @bitlogger.callback_sink(fn(_) { + + }), + config=AsyncLoggerConfig::new( + max_pending=4, + overflow=AsyncOverflowPolicy::Blocking, + ), + min_level=@bitlogger.Level::Info, + target="async.clear", + ) + + logger.info("one") + logger.info("two") + inspect(logger.pending_count(), content="2") + inspect(logger.dropped_count(), content="0") + logger.close(clear=true) + inspect(logger.is_closed(), content="true") + inspect(logger.pending_count(), content="0") + inspect(logger.dropped_count(), content="2") +} + +async test "shutdown clear closes without worker startup" { + let logger = async_logger( + @bitlogger.callback_sink(fn(_) { + + }), + config=AsyncLoggerConfig::new( + max_pending=2, + overflow=AsyncOverflowPolicy::Blocking, + ), + min_level=@bitlogger.Level::Info, + target="async.noworker", + ) + + logger.info("one") + logger.shutdown(clear=true) + inspect(logger.is_closed(), content="true") + inspect(logger.is_running(), content="false") + inspect(logger.pending_count(), content="0") + inspect(logger.dropped_count(), content="1") +} + +test "async logger config stringify roundtrips stable fields" { + let text = stringify_async_logger_config( + AsyncLoggerConfig::new( + max_pending=8, + overflow=AsyncOverflowPolicy::DropOldest, + ), + ) + let config = parse_async_logger_config_text(text) + inspect(config.max_pending, content="8") + inspect(match config.overflow { + AsyncOverflowPolicy::Blocking => "Blocking" + AsyncOverflowPolicy::DropOldest => "DropOldest" + AsyncOverflowPolicy::DropNewest => "DropNewest" + }, content="DropOldest") +} + +test "async build config stringify roundtrips nested logger and async fields" { + let text = stringify_async_logger_build_config( + AsyncLoggerBuildConfig::new( + logger=@bitlogger.LoggerConfig::new( + min_level=@bitlogger.Level::Warn, + target="async.roundtrip", + timestamp=true, + sink=@bitlogger.SinkConfig::new(kind=@bitlogger.SinkKind::TextConsole), + ), + async_config=AsyncLoggerConfig::new( + max_pending=2, + overflow=AsyncOverflowPolicy::DropNewest, + ), + ), + ) + let config = parse_async_logger_build_config_text(text) + inspect(config.logger.min_level.label(), content="WARN") + inspect(config.logger.target, content="async.roundtrip") + inspect(config.logger.timestamp, content="true") + inspect(config.async_config.max_pending, content="2") + inspect(match config.async_config.overflow { + AsyncOverflowPolicy::Blocking => "Blocking" + AsyncOverflowPolicy::DropOldest => "DropOldest" + AsyncOverflowPolicy::DropNewest => "DropNewest" + }, content="DropNewest") +} diff --git a/bitlogger_async/async_logger_native.mbt b/bitlogger_async/async_logger_native.mbt index 3be4867..eb2fd2b 100644 --- a/bitlogger_async/async_logger_native.mbt +++ b/bitlogger_async/async_logger_native.mbt @@ -30,7 +30,7 @@ fn parse_async_overflow(name : String) -> AsyncOverflowPolicy raise { } } -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 @@ -53,6 +53,26 @@ fn parse_async_logger_config_text(input : String) -> AsyncLoggerConfig raise { AsyncLoggerConfig::new(max_pending=max_pending, overflow=overflow) } +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()), + "overflow": @json_parser.JsonValue::String(match config.overflow { + AsyncOverflowPolicy::Blocking => "Blocking" + AsyncOverflowPolicy::DropOldest => "DropOldest" + AsyncOverflowPolicy::DropNewest => "DropNewest" + }), + }) +} + +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) + } else { + @json_parser.stringify(value) + } +} + pub struct AsyncLoggerBuildConfig { logger : @bitlogger.LoggerConfig async_config : AsyncLoggerConfig @@ -82,6 +102,27 @@ pub fn parse_async_logger_build_config_text(input : String) -> AsyncLoggerBuildC AsyncLoggerBuildConfig::new(logger=logger, async_config=async_config) } +pub fn async_logger_build_config_to_json( + config : AsyncLoggerBuildConfig, +) -> @json_parser.JsonValue { + @json_parser.JsonValue::Object({ + "logger": @bitlogger.logger_config_to_json(config.logger), + "async_config": async_logger_config_to_json(config.async_config), + }) +} + +pub fn stringify_async_logger_build_config( + config : AsyncLoggerBuildConfig, + pretty~ : Bool = false, +) -> String { + let value = async_logger_build_config_to_json(config) + if pretty { + @json_parser.stringify_pretty(value, 2) + } else { + @json_parser.stringify(value) + } +} + pub struct AsyncLogger[S] { min_level : @bitlogger.Level target : String @@ -351,7 +392,10 @@ pub async fn[S] AsyncLogger::shutdown(self : AsyncLogger[S], clear? : Bool = fal } pub async fn[S] AsyncLogger::wait_idle(self : AsyncLogger[S]) -> Unit { - while self.pending_count() > 0 && self.is_running() { + while self.pending_count() > 0 { + if self.has_failed() { + break + } @async.pause() } } diff --git a/bitlogger_async/async_logger_stub.mbt b/bitlogger_async/async_logger_stub.mbt index 7b1a2d1..b5c605e 100644 --- a/bitlogger_async/async_logger_stub.mbt +++ b/bitlogger_async/async_logger_stub.mbt @@ -39,6 +39,38 @@ pub fn parse_async_logger_build_config_text(input : String) -> AsyncLoggerBuildC abort("bitlogger_async currently only supports native/llvm backends") } +pub fn parse_async_logger_config_text(input : String) -> AsyncLoggerConfig raise { + ignore(input) + abort("bitlogger_async currently only supports native/llvm backends") +} + +pub fn async_logger_config_to_json(config : AsyncLoggerConfig) -> @json_parser.JsonValue { + ignore(config) + abort("bitlogger_async currently only supports native/llvm backends") +} + +pub fn stringify_async_logger_config(config : AsyncLoggerConfig, pretty~ : Bool = false) -> String { + ignore(config) + ignore(pretty) + abort("bitlogger_async currently only supports native/llvm backends") +} + +pub fn async_logger_build_config_to_json( + config : AsyncLoggerBuildConfig, +) -> @json_parser.JsonValue { + ignore(config) + abort("bitlogger_async currently only supports native/llvm backends") +} + +pub fn stringify_async_logger_build_config( + config : AsyncLoggerBuildConfig, + pretty~ : Bool = false, +) -> String { + ignore(config) + ignore(pretty) + abort("bitlogger_async currently only supports native/llvm backends") +} + pub fn async_logger[S : @bitlogger.Sink]( sink : S, config~ : AsyncLoggerConfig = AsyncLoggerConfig::new(),