diff --git a/README.md b/README.md index eaa5812..d6c035f 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ BitLogger 是一个基于 MoonBit 编写的结构化日志库。 - 🧩 核心能力清晰:先把 logging core 做稳,再继续扩展 rotation/async 等能力。 - 🏗️ 结构明确:按 `level / record / formatter / sinks / logger / global` 拆文件,便于继续维护。 - 🔌 可扩展:支持 `fanout_sink(...)` 和 `callback_sink(...)`,方便后续桥接文件、指标或外部系统。 +- 🔀 可分流:支持 `split_sink(...)` / `split_by_level(...)`,可按谓词或 level 将日志路由到不同 sink。 - 🧱 可组合:支持 `buffered_sink(...)` 和 `filter_sink(...)`,可以在不引入复杂 runtime 的前提下组合输出策略。 - 🔎 可复用过滤:提供 `target_has_prefix(...)`、`message_contains(...)`、`level_at_least(...)`、`field_equals(...)` 等过滤辅助函数。 - 🩹 可变换 Record:支持 `with_patch(...)`、`patch_sink(...)` 与脱敏/补字段/message 变换 helper。 @@ -127,6 +128,27 @@ ignore(logger.sink.flush()) ``` +
按 level 分流 sink 示例 + +```moonbit +let logger = Logger::new( + split_by_level( + callback_sink(fn(rec) { + println("high priority: \{rec.level.label()} \{rec.message}") + }), + console_sink(), + min_level=Level::Warn, + ), + min_level=Level::Trace, + target="split", +) + +logger.info("normal output") +logger.warn("warning output") +``` + +
+
自定义文本 formatter 示例 ```moonbit diff --git a/bitlogger/BitLogger_wbtest.mbt b/bitlogger/BitLogger_wbtest.mbt index bc9abc2..de2827f 100644 --- a/bitlogger/BitLogger_wbtest.mbt +++ b/bitlogger/BitLogger_wbtest.mbt @@ -182,6 +182,58 @@ 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::new([]) + let right_messages : Ref[Array[String]] = Ref::new([]) + let logger = Logger::new( + split_sink( + callback_sink(fn(rec) { + left_messages.val.push(rec.message) + }), + callback_sink(fn(rec) { + right_messages.val.push(rec.message) + }), + fn(rec) { + rec.target == "audit" + }, + ), + min_level=Level::Info, + target="main", + ) + logger.info("drop to right") + logger.log(Level::Info, "keep on left", target="audit") + inspect(left_messages.val.length(), content="1") + inspect(left_messages.val[0], content="keep on left") + inspect(right_messages.val.length(), content="1") + inspect(right_messages.val[0], content="drop to right") +} + +test "split_by_level routes warn and error separately" { + let high_messages : Ref[Array[String]] = Ref::new([]) + let low_messages : Ref[Array[String]] = Ref::new([]) + let logger = Logger::new( + split_by_level( + callback_sink(fn(rec) { + high_messages.val.push(rec.level.label() + ":" + rec.message) + }), + callback_sink(fn(rec) { + low_messages.val.push(rec.level.label() + ":" + rec.message) + }), + min_level=Level::Warn, + ), + min_level=Level::Trace, + target="split", + ) + logger.info("info") + logger.warn("warn") + logger.error("error") + inspect(high_messages.val.length(), content="2") + inspect(high_messages.val[0], content="WARN:warn") + inspect(high_messages.val[1], content="ERROR:error") + inspect(low_messages.val.length(), content="1") + inspect(low_messages.val[0], content="INFO:info") +} + test "callback sink sees child target and context logger shape" { let captured_target : Ref[String] = Ref::new("") let captured_message : Ref[String] = Ref::new("") diff --git a/bitlogger/README.mbt.md b/bitlogger/README.mbt.md index 7c1090f..9d7469e 100644 --- a/bitlogger/README.mbt.md +++ b/bitlogger/README.mbt.md @@ -24,6 +24,8 @@ BitLogger 是一个基于 MoonBit 的结构化日志库。 - `json_console_sink()` 提供 JSON 控制台输出 - sink composition via `fanout_sink(...)` - `fanout_sink(...)` 支持多 sink 组合 +- sink routing via `split_sink(...)` and `split_by_level(...)` +- `split_sink(...)`、`split_by_level(...)` 支持按谓词或 level 将日志路由到不同 sink - custom callback sink via `callback_sink(...)` - `callback_sink(...)` 支持自定义外部集成 - buffered sink via `buffered_sink(...)` @@ -142,6 +144,24 @@ test { } ``` +```mbt check +test { + let logger = Logger::new( + split_by_level( + callback_sink(fn(rec) { + println("high priority: \{rec.level.label()} \{rec.message}") + }), + console_sink(), + min_level=Level::Warn, + ), + min_level=Level::Trace, + target="split", + ) + logger.info("normal output") + logger.warn("warning output") +} +``` + ```mbt check test { let formatter = text_formatter( diff --git a/bitlogger/sinks.mbt b/bitlogger/sinks.mbt index 04a9ed8..caafdcb 100644 --- a/bitlogger/sinks.mbt +++ b/bitlogger/sinks.mbt @@ -247,6 +247,34 @@ pub impl[A : Sink, B : Sink] Sink for FanoutSink[A, B] with write(self, rec) { self.right.write({ ..rec }) } +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] { + { left, right, predicate } +} + +pub fn[A, B] split_by_level( + left : A, + right : B, + min_level~ : Level = Level::Warn, +) -> SplitSink[A, B] { + 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) { + if (self.predicate)(rec) { + self.left.write(rec) + } else { + self.right.write(rec) + } +} + pub struct CallbackSink { callback : (Record) -> Unit } diff --git a/docs/README-en.md b/docs/README-en.md index 8b7e431..64fe8a7 100644 --- a/docs/README-en.md +++ b/docs/README-en.md @@ -14,6 +14,7 @@ BitLogger currently provides: - context fields via `with_context_fields(...)` - optional timestamps via `with_timestamp()` - sink fanout via `fanout_sink(...)` +- sink routing via `split_sink(...)` and `split_by_level(...)` - custom integration via `callback_sink(...)` - in-memory buffering via `buffered_sink(...)` - record filtering via `filter_sink(...)` @@ -121,6 +122,25 @@ logger.info("three") ignore(logger.sink.flush()) ``` +Level-based split sink: + +```moonbit +let logger = Logger::new( + split_by_level( + callback_sink(fn(rec) { + println("high priority: \{rec.level.label()} \{rec.message}") + }), + console_sink(), + min_level=Level::Warn, + ), + min_level=Level::Trace, + target="split", +) + +logger.info("normal output") +logger.warn("warning output") +``` + Custom text formatter: ```moonbit diff --git a/docs/changes/0.3.0.md b/docs/changes/0.3.0.md index 7902649..9dec585 100644 --- a/docs/changes/0.3.0.md +++ b/docs/changes/0.3.0.md @@ -29,6 +29,7 @@ version 0.3.0 - feat: support formatter tokens `{timestamp}`, `{timestamp_ms}`, `{level}`, `{target}`, `{message}`, and `{fields}` in both runtime and JSON config paths - feat: add `FileRotation`, `file_rotation(...)`, and size-based file rotation with retained backups for `file_sink(...)` - feat: support `sink.rotation.max_bytes` and `sink.rotation.max_backups` in JSON logger config +- feat: add `SplitSink`, `split_sink(...)`, and `split_by_level(...)` for routing records into different sinks by predicate or level ### Test @@ -40,6 +41,7 @@ version 0.3.0 - test: cover dropped-count reporting for bounded config-built queue - test: cover template-based formatter rendering and disabled token behavior - test: cover file rotation config parsing, config roundtrip, and native rotation behavior +- test: cover split sink predicate routing and level-based routing behavior - test: add async logger lifecycle, config roundtrip, and batching/flush policy test seeds - build: verify `bitlogger_async --target native` and `examples/async_basic --target native` compile successfully @@ -52,6 +54,7 @@ version 0.3.0 - docs: update root README, English README, and Mooncake README with config usage notes - docs: update formatter examples to demonstrate template-based text rendering - docs: update file sink examples to demonstrate rotation and backup retention +- docs: add split sink examples for level-based routing - docs: update root README and English README with async adapter notes and current scope - chore: ignore local `.mooncakes/` cache directory in git diff --git a/examples/basic/main.mbt b/examples/basic/main.mbt index 9fa39d6..de9ab64 100644 --- a/examples/basic/main.mbt +++ b/examples/basic/main.mbt @@ -17,6 +17,20 @@ fn main { ) fanout_logger.info("dual output", fields=[@lib.field("kind", "fanout")]) + let split_logger = @lib.Logger::new( + @lib.split_by_level( + @lib.callback_sink(fn(rec) { + println("high priority: \{rec.level.label()} \{rec.message}") + }), + @lib.console_sink(), + min_level=@lib.Level::Warn, + ), + min_level=@lib.Level::Trace, + target="split", + ) + 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() timed_logger.info("timestamp enabled", fields=[@lib.field("kind", "time")])