From 1b56e1e20aa0e2a908d508fb1970a89e7ff52316 Mon Sep 17 00:00:00 2001 From: Nanaloveyuki Date: Fri, 15 May 2026 11:15:34 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Add=20library=20logger=20facades?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src-async/BitLoggerAsync_test.mbt | 37 ++++++++ src-async/library_async_logger.mbt | 133 +++++++++++++++++++++++++++++ src/BitLogger_test.mbt | 54 ++++++++++++ src/library_logger.mbt | 97 +++++++++++++++++++++ 4 files changed, 321 insertions(+) create mode 100644 src-async/library_async_logger.mbt create mode 100644 src/library_logger.mbt diff --git a/src-async/BitLoggerAsync_test.mbt b/src-async/BitLoggerAsync_test.mbt index 0449dd7..7aab5c7 100644 --- a/src-async/BitLoggerAsync_test.mbt +++ b/src-async/BitLoggerAsync_test.mbt @@ -242,3 +242,40 @@ async test "run drains queued records in compatibility backends too" { inspect(written.val[0], content="one") inspect(written.val[1], content="two") } + +async test "library async logger keeps a smaller async facade" { + let written_targets : Ref[Array[String]] = Ref::new([]) + let written_messages : Ref[Array[String]] = Ref::new([]) + let written_field_counts : Ref[Array[Int]] = Ref::new([]) + 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") + + @async.with_task_group(group => { + group.spawn_bg(() => logger.run()) + logger.info("ready", fields=[@bitlogger.field("mode", "test")]) + logger.shutdown() + }) + + inspect(written_targets.val.length(), content="1") + inspect(written_targets.val[0], content="async.lib.worker") + inspect(written_messages.val[0], content="ready") + inspect(written_field_counts.val[0], content="2") +} + +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\"}}", + ) + let full = logger.to_async_logger() + 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") +} diff --git a/src-async/library_async_logger.mbt b/src-async/library_async_logger.mbt new file mode 100644 index 0000000..15e35f7 --- /dev/null +++ b/src-async/library_async_logger.mbt @@ -0,0 +1,133 @@ +pub struct LibraryAsyncLogger[S] { + inner : AsyncLogger[S] +} + +pub fn[S] library_async_logger(logger : AsyncLogger[S]) -> LibraryAsyncLogger[S] { + { inner: logger } +} + +pub fn[S] LibraryAsyncLogger::new( + sink : S, + config~ : AsyncLoggerConfig = AsyncLoggerConfig::new(), + min_level~ : @bitlogger.Level = @bitlogger.Level::Info, + target~ : String = "", + flush~ : (S) -> Int = fn(_) { 0 }, +) -> LibraryAsyncLogger[S] { + library_async_logger( + async_logger( + sink, + config=config, + min_level=min_level, + target=target, + flush=flush, + ), + ) +} + +pub fn[S] LibraryAsyncLogger::to_async_logger(self : LibraryAsyncLogger[S]) -> AsyncLogger[S] { + self.inner +} + +pub 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, +) -> LibraryAsyncLogger[S] { + library_async_logger(self.inner.with_target(target)) +} + +pub fn[S] LibraryAsyncLogger::child( + self : LibraryAsyncLogger[S], + target : String, +) -> LibraryAsyncLogger[S] { + library_async_logger(self.inner.child(target)) +} + +pub fn[S] LibraryAsyncLogger::with_context_fields( + self : LibraryAsyncLogger[S], + fields : Array[@bitlogger.Field], +) -> LibraryAsyncLogger[S] { + library_async_logger(self.inner.with_context_fields(fields)) +} + +pub fn[S] LibraryAsyncLogger::bind( + self : LibraryAsyncLogger[S], + fields : Array[@bitlogger.Field], +) -> LibraryAsyncLogger[S] { + self.with_context_fields(fields) +} + +pub fn[S] LibraryAsyncLogger::is_enabled( + self : LibraryAsyncLogger[S], + level : @bitlogger.Level, +) -> Bool { + self.inner.is_enabled(level) +} + +pub async fn[S] LibraryAsyncLogger::log( + self : LibraryAsyncLogger[S], + level : @bitlogger.Level, + message : String, + fields~ : Array[@bitlogger.Field] = [], + target? : String = "", +) -> Unit { + self.inner.log(level, message, fields=fields, target=target) +} + +pub async fn[S] LibraryAsyncLogger::info( + self : LibraryAsyncLogger[S], + message : String, + fields~ : Array[@bitlogger.Field] = [], +) -> Unit { + self.inner.info(message, fields=fields) +} + +pub async fn[S] LibraryAsyncLogger::warn( + self : LibraryAsyncLogger[S], + message : String, + fields~ : Array[@bitlogger.Field] = [], +) -> Unit { + self.inner.warn(message, fields=fields) +} + +pub async fn[S] LibraryAsyncLogger::error( + self : LibraryAsyncLogger[S], + message : String, + fields~ : Array[@bitlogger.Field] = [], +) -> Unit { + self.inner.error(message, fields=fields) +} + +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) +} diff --git a/src/BitLogger_test.mbt b/src/BitLogger_test.mbt index 5f7ee67..9ef1d34 100644 --- a/src/BitLogger_test.mbt +++ b/src/BitLogger_test.mbt @@ -770,3 +770,57 @@ test "configured queued file logger flushes queue through file helper" { inspect(logger.file_close(), content="false") } } + +test "library logger keeps a smaller stable facade" { + let captured_target : Ref[String] = Ref::new("") + let captured_message : Ref[String] = Ref::new("") + let captured_fields : Ref[Array[Field]] = Ref::new([]) + let logger = LibraryLogger::new( + callback_sink(fn(rec) { + captured_target.val = rec.target + captured_message.val = rec.message + captured_fields.val = rec.fields + }), + min_level=Level::Info, + target="svc", + ).with_context_fields([field("service", "bitlogger")]).child("worker") + logger.info("ready", fields=[field("mode", "test")]) + inspect(captured_target.val, content="svc.worker") + inspect(captured_message.val, content="ready") + inspect(captured_fields.val.length(), content="2") + inspect(captured_fields.val[0].key, content="service") + inspect(captured_fields.val[1].key, content="mode") +} + +test "library logger can unwrap to full logger when needed" { + let base = LibraryLogger::new(console_sink(), min_level=Level::Warn, target="lib") + let full = base.to_logger().with_timestamp() + inspect(base.is_enabled(Level::Error), content="true") + inspect(base.is_enabled(Level::Info), content="false") + inspect(full.timestamp, content="true") + inspect(full.target, content="lib") +} + +test "library logger can be built from configured runtime path" { + let logger = build_library_logger( + LoggerConfig::new( + min_level=Level::Warn, + target="lib.config", + sink=SinkConfig::new(kind=SinkKind::Console), + ), + ) + let full = logger.to_logger() + inspect(logger.is_enabled(Level::Error), content="true") + inspect(logger.is_enabled(Level::Info), content="false") + inspect(full.target, content="lib.config") +} + +test "library logger can be parsed and built from config text" { + let logger = parse_and_build_library_logger( + "{\"min_level\":\"warn\",\"target\":\"lib.json\",\"sink\":{\"kind\":\"console\"}}", + ) + let full = logger.to_logger() + inspect(logger.is_enabled(Level::Error), content="true") + inspect(logger.is_enabled(Level::Info), content="false") + inspect(full.target, content="lib.json") +} diff --git a/src/library_logger.mbt b/src/library_logger.mbt new file mode 100644 index 0000000..5ce10e8 --- /dev/null +++ b/src/library_logger.mbt @@ -0,0 +1,97 @@ +pub struct LibraryLogger[S] { + inner : Logger[S] +} + +pub fn[S] library_logger(logger : Logger[S]) -> LibraryLogger[S] { + { inner: logger } +} + +pub fn[S] LibraryLogger::new( + sink : S, + min_level~ : Level = Level::Info, + target~ : String = "", +) -> LibraryLogger[S] { + library_logger(Logger::new(sink, min_level=min_level, target=target)) +} + +pub fn[S] LibraryLogger::to_logger(self : LibraryLogger[S]) -> Logger[S] { + self.inner +} + +pub fn configured_library_logger(logger : ConfiguredLogger) -> LibraryLogger[RuntimeSink] { + library_logger(logger) +} + +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] { + library_logger(self.inner.with_target(target)) +} + +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], +) -> LibraryLogger[ContextSink[S]] { + library_logger(self.inner.with_context_fields(fields)) +} + +pub fn[S] LibraryLogger::bind( + self : LibraryLogger[S], + fields : Array[Field], +) -> LibraryLogger[ContextSink[S]] { + self.with_context_fields(fields) +} + +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] = [], + target? : String = "", +) -> Unit { + self.inner.log(level, message, fields=fields, target=target) +} + +pub fn[S : Sink] LibraryLogger::info( + self : LibraryLogger[S], + message : String, + fields~ : Array[Field] = [], +) -> Unit { + self.inner.info(message, fields=fields) +} + +pub fn[S : Sink] LibraryLogger::warn( + self : LibraryLogger[S], + message : String, + fields~ : Array[Field] = [], +) -> Unit { + self.inner.warn(message, fields=fields) +} + +pub fn[S : Sink] LibraryLogger::error( + self : LibraryLogger[S], + message : String, + fields~ : Array[Field] = [], +) -> Unit { + self.inner.error(message, fields=fields) +} + +pub fn default_library_logger() -> LibraryLogger[ConsoleSink] { + library_logger(default_logger()) +}