From 845718224b771d6c2d4eac10ce5764660aa0fc8a Mon Sep 17 00:00:00 2001 From: Nanaloveyuki Date: Fri, 8 May 2026 16:24:51 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=93=9D=20Document=200.2=20examples=20and?= =?UTF-8?q?=20usage=20patterns?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 106 ++++++++++++++++++++++++++++++++++++++-- bitlogger/README.mbt.md | 96 +++++++++++++++++++++++++++++++++--- docs/README-en.md | 94 +++++++++++++++++++++++++++++++++++ docs/changes/0.2.0.md | 47 ++++++++++++++++++ examples/basic/main.mbt | 67 +++++++++++++++++++++++++ 5 files changed, 399 insertions(+), 11 deletions(-) create mode 100644 docs/changes/0.2.0.md diff --git a/README.md b/README.md index 431b7f5..9e21b92 100644 --- a/README.md +++ b/README.md @@ -16,9 +16,15 @@ BitLogger 是一个基于 MoonBit 编写的结构化日志库。 ## ❇️ 特点 -- 🧩 核心能力清晰:先把 logging core 做稳,再继续扩展 `FileSink`、buffered/async 等能力。 +- 🧩 核心能力清晰:先把 logging core 做稳,再继续扩展 rotation/async 等能力。 - 🏗️ 结构明确:按 `level / record / formatter / sinks / logger / global` 拆文件,便于继续维护。 - 🔌 可扩展:支持 `fanout_sink(...)` 和 `callback_sink(...)`,方便后续桥接文件、指标或外部系统。 +- 🧱 可组合:支持 `buffered_sink(...)` 和 `filter_sink(...)`,可以在不引入复杂 runtime 的前提下组合输出策略。 +- 🔎 可复用过滤:提供 `target_has_prefix(...)`、`message_contains(...)`、`level_at_least(...)`、`field_equals(...)` 等过滤辅助函数。 +- 🩹 可变换 Record:支持 `with_patch(...)`、`patch_sink(...)` 与脱敏/补字段/message 变换 helper。 +- 📮 显式队列:支持 `queued_sink(...)` / `with_queue(...)`、有界积压与溢出策略,作为后续 async sink 的 runtime-safe 基础。 +- 🧾 可配置文本格式:支持 `text_formatter(...)`、`format_text(...)`、`text_console_sink(...)` 和 `formatted_callback_sink(...)`。 +- 💾 Native 文件输出:支持 `file_sink(...)`,但当前仅保证 `native/llvm` backend 可用。 - 📦 面向 MoonBit:API 和工程结构围绕 MoonBit 的 package / visibility / toolchain 现实约束设计。 ## 🚀 快速开始 @@ -31,14 +37,15 @@ let logger = Logger::new(console_sink(), min_level=Level::Info, target="demo") logger.info("starting", fields=[field("port", "8080")]) ``` -层级 target 示例: +
层级 target 示例 ```moonbit let worker = Logger::new(console_sink(), target="app").child("worker") worker.info("job ready") ``` +
-自定义 callback sink 示例: +
自定义 callback sink 示例 ```moonbit let hook = Logger::new( @@ -50,6 +57,98 @@ let hook = Logger::new( hook.info("hello") ``` +
+ +
基础 buffered sink 示例 + +```moonbit +let sink = buffered_sink(console_sink(), flush_limit=2) +let logger = Logger::new(sink, target="buffered") + +logger.info("one") +logger.info("two") +sink.flush() +``` +
+ +
基础 filter sink 示例 + +```moonbit +let sink = filter_sink(console_sink(), fn(rec) { + rec.target == "kept" +}) + +let kept = Logger::new(sink, target="kept") +let dropped = Logger::new(sink, target="dropped") + +kept.info("visible") +dropped.info("hidden") +``` +
+ +
Logger 链式 filter 示例 + +```moonbit +let logger = Logger::new(console_sink(), target="service") + .with_filter(all_of([ + target_has_prefix("service"), + message_contains("visible"), + ])) + +logger.info("hidden") +logger.child("api").info("visible") +``` +
+ +
Record patch 示例 + +```moonbit +let logger = Logger::new(console_sink(), target="auth") + .with_patch(compose_patches([ + prefix_message("[safe] "), + redact_fields(["token"]), + append_fields([field("service", "bitlogger")]), + ])) + +logger.info("login", fields=[field("user", "alice"), field("token", "secret")]) +``` +
+ +
显式 queue sink 示例 + +```moonbit +let logger = Logger::new(console_sink(), target="queue") + .with_queue(max_pending=2, overflow=QueueOverflowPolicy::DropOldest) + +logger.info("one") +logger.info("two") +logger.info("three") +ignore(logger.sink.flush()) +``` +
+ +
自定义文本 formatter 示例 + +```moonbit +let formatter = text_formatter(show_timestamp=false, separator=" | ") +let logger = Logger::new(text_console_sink(formatter), target="pretty") + +logger.info("hello", fields=[field("mode", "pretty")]) +``` + +
+ +
native 文件 sink 示例 + +```moonbit +if native_files_supported() { + let logger = Logger::new(file_sink("bitlogger.log"), target="file") + logger.info("hello", fields=[field("kind", "file")]) + ignore(logger.sink.flush()) + ignore(logger.sink.close()) +} +``` +
## 📂 仓库结构 @@ -60,4 +159,3 @@ hook.info("hello") - [Mooncake 文档页](https://mooncakes.io/docs/Nanaloveyuki/BitLogger) - [English README](./docs/README-en.md) - diff --git a/bitlogger/README.mbt.md b/bitlogger/README.mbt.md index ccf0089..077222a 100644 --- a/bitlogger/README.mbt.md +++ b/bitlogger/README.mbt.md @@ -26,6 +26,20 @@ BitLogger 是一个基于 MoonBit 的结构化日志库。 - `fanout_sink(...)` 支持多 sink 组合 - custom callback sink via `callback_sink(...)` - `callback_sink(...)` 支持自定义外部集成 +- buffered sink via `buffered_sink(...)` +- `buffered_sink(...)` 支持内存缓冲与 flush +- filter sink via `filter_sink(...)` +- `filter_sink(...)` 支持按 `Record` 条件筛选输出 +- reusable filter helpers such as `target_has_prefix(...)`, `message_contains(...)`, and `field_equals(...)` +- 提供 `target_has_prefix(...)`、`message_contains(...)`、`field_equals(...)` 等可复用过滤辅助函数 +- record patching via `with_patch(...)` and `patch_sink(...)` +- 支持 `with_patch(...)`、`patch_sink(...)` 以及常见 record patch helper +- explicit queued delivery via `queued_sink(...)` and `with_queue(...)` +- 支持 `queued_sink(...)`、`with_queue(...)`、有界积压与溢出策略 +- configurable text formatting via `text_formatter(...)`, `format_text(...)`, and `text_console_sink(...)` +- 支持 `text_formatter(...)`、`format_text(...)`、`text_console_sink(...)` 等文本格式化能力 +- native-only file output via `file_sink(...)` +- 支持 `file_sink(...)`,但当前仅保证 `native/llvm` backend 可用 ## Example / 示例 @@ -55,13 +69,6 @@ test { } ``` -```mbt check -test { - let logger = Logger::new(console_sink(), target="app").child("worker") - logger.info("ready") -} -``` - ```mbt check test { let logger = Logger::new( @@ -74,3 +81,78 @@ test { } ``` +```mbt check +test { + let sink = buffered_sink(console_sink(), flush_limit=2) + let logger = Logger::new(sink, target="buffered") + logger.info("one") + logger.info("two") + sink.flush() +} +``` + +```mbt check +test { + let sink = filter_sink(console_sink(), fn(rec) { + rec.target == "kept" + }) + let kept = Logger::new(sink, target="kept") + let dropped = Logger::new(sink, target="dropped") + kept.info("visible") + dropped.info("hidden") +} +``` + +```mbt check +test { + let logger = Logger::new(console_sink(), target="service") + .with_filter(all_of([ + target_has_prefix("service"), + message_contains("visible"), + ])) + logger.info("hidden") + logger.child("api").info("visible") +} +``` + +```mbt check +test { + let logger = Logger::new(console_sink(), target="auth") + .with_patch(compose_patches([ + prefix_message("[safe] "), + redact_fields(["token"]), + append_fields([field("service", "bitlogger")]), + ])) + logger.info("login", fields=[field("user", "alice"), field("token", "secret")]) +} +``` + +```mbt check +test { + let logger = Logger::new(console_sink(), target="queue") + .with_queue(max_pending=2, overflow=QueueOverflowPolicy::DropOldest) + logger.info("one") + logger.info("two") + logger.info("three") + ignore(logger.sink.flush()) +} +``` + +```mbt check +test { + let formatter = text_formatter(show_timestamp=false, separator=" | ") + let logger = Logger::new(text_console_sink(formatter), target="pretty") + logger.info("hello", fields=[field("mode", "pretty")]) +} +``` + +```mbt check +test { + if native_files_supported() { + let logger = Logger::new(file_sink("bitlogger.log"), target="file") + logger.info("hello", fields=[field("kind", "file")]) + ignore(logger.sink.flush()) + ignore(logger.sink.close()) + } +} +``` diff --git a/docs/README-en.md b/docs/README-en.md index f33524b..5898867 100644 --- a/docs/README-en.md +++ b/docs/README-en.md @@ -15,6 +15,17 @@ BitLogger currently provides: - optional timestamps via `with_timestamp()` - sink fanout via `fanout_sink(...)` - custom integration via `callback_sink(...)` +- in-memory buffering via `buffered_sink(...)` +- record filtering via `filter_sink(...)` +- reusable filter helpers such as `target_has_prefix(...)`, `message_contains(...)`, `level_at_least(...)`, and `field_equals(...)` +- record patching via `with_patch(...)` and `patch_sink(...)` +- patch helpers such as `prefix_message(...)`, `append_fields(...)`, and `redact_fields(...)` +- explicit queued delivery via `queued_sink(...)` and `with_queue(...)` +- bounded backlog with `QueueOverflowPolicy::DropNewest` and `QueueOverflowPolicy::DropOldest` +- configurable text formatting via `text_formatter(...)`, `format_text(...)`, and `text_console_sink(...)` +- formatter-based callback integration via `formatted_callback_sink(...)` +- native-only file output via `file_sink(...)` +- `native_files_supported()` for backend capability detection - default global logger helpers ## Quick Start @@ -47,6 +58,89 @@ let hook = Logger::new( hook.info("hello") ``` +Basic buffered sink: + +```moonbit +let sink = buffered_sink(console_sink(), flush_limit=2) +let logger = Logger::new(sink, target="buffered") + +logger.info("one") +logger.info("two") +sink.flush() +``` + +Basic filter sink: + +```moonbit +let sink = filter_sink(console_sink(), fn(rec) { + rec.target == "kept" +}) + +let kept = Logger::new(sink, target="kept") +let dropped = Logger::new(sink, target="dropped") + +kept.info("visible") +dropped.info("hidden") +``` + +Chained logger filter: + +```moonbit +let logger = Logger::new(console_sink(), target="service") + .with_filter(all_of([ + target_has_prefix("service"), + message_contains("visible"), + ])) + +logger.info("hidden") +logger.child("api").info("visible") +``` + +Record patching: + +```moonbit +let logger = Logger::new(console_sink(), target="auth") + .with_patch(compose_patches([ + prefix_message("[safe] "), + redact_fields(["token"]), + append_fields([field("service", "bitlogger")]), + ])) + +logger.info("login", fields=[field("user", "alice"), field("token", "secret")]) +``` + +Explicit queued sink: + +```moonbit +let logger = Logger::new(console_sink(), target="queue") + .with_queue(max_pending=2, overflow=QueueOverflowPolicy::DropOldest) + +logger.info("one") +logger.info("two") +logger.info("three") +ignore(logger.sink.flush()) +``` + +Custom text formatter: + +```moonbit +let formatter = text_formatter(show_timestamp=false, separator=" | ") +let logger = Logger::new(text_console_sink(formatter), target="pretty") + +logger.info("hello", fields=[field("mode", "pretty")]) +``` + +Native file sink: + +```moonbit +if native_files_supported() { + let logger = Logger::new(file_sink("bitlogger.log"), target="file") + logger.info("hello", fields=[field("kind", "file")]) + ignore(logger.sink.flush()) + ignore(logger.sink.close()) +} +``` + ## Repository Layout - `bitlogger/`: MoonBit library package, tests, and Mooncake package README diff --git a/docs/changes/0.2.0.md b/docs/changes/0.2.0.md new file mode 100644 index 0000000..32bda2c --- /dev/null +++ b/docs/changes/0.2.0.md @@ -0,0 +1,47 @@ +## BitLogger Update Changes + +version 0.2.0 + +### Feature + +- feat: add `BufferedSink[S]` with in-memory buffering support +- feat: add `buffered_sink(...)` constructor with configurable `flush_limit` +- feat: add `BufferedSink::flush()` for manual drain +- feat: add `BufferedSink::pending_count()` for buffer inspection in tests and integration code +- feat: add `FilterSink[S]` for predicate-based record filtering +- feat: add `filter_sink(...)` constructor for composable sink routing +- feat: add `Logger::with_filter(...)` as a higher-level chained filter entry point +- feat: add reusable filter helpers such as `target_is(...)`, `target_has_prefix(...)`, `message_contains(...)`, `level_at_least(...)`, `has_field(...)`, and `field_equals(...)` +- feat: add predicate combinators `all_of(...)`, `any_of(...)`, and `not_(...)` +- feat: add `PatchSink[S]`, `patch_sink(...)`, and `Logger::with_patch(...)` for record transformation +- feat: add record patch helpers such as `prefix_message(...)`, `set_target(...)`, `append_fields(...)`, `redact_field(...)`, `redact_fields(...)`, and `compose_patches(...)` +- feat: add `QueuedSink[S]`, `queued_sink(...)`, and `Logger::with_queue(...)` for explicit queued delivery +- feat: add `QueueOverflowPolicy` with `DropNewest` and `DropOldest` bounded-backlog strategies +- feat: add configurable text formatter APIs `text_formatter(...)`, `format_text(...)`, and `format_json(...)` +- feat: add formatter-based sinks `formatted_console_sink(...)`, `text_console_sink(...)`, `formatted_callback_sink(...)`, and `text_callback_sink(...)` +- feat: add native-only `file_sink(...)` plus `native_files_supported()` backend capability detection +- feat: add explicit file sink lifecycle helpers `FileSink::is_available()`, `flush()`, and `close()` + +### Test + +- test: cover manual flush behavior for buffered sink +- test: cover auto flush when buffered records reach the configured limit +- test: cover predicate-based forwarding for filter sink +- test: cover chained `Logger::with_filter(...)` behavior with child target composition +- test: cover helper-based filter composition for target, level, message, and field matching +- test: cover record patch composition, target rewrite, message rewrite, field append, and field redaction +- test: cover queue drain order, bounded backlog drop behavior, and chained `Logger::with_queue(...)` usage +- test: cover formatter customization, formatter-only message mode, formatted callback output, and JSON formatter shape +- test: cover backend-aware file sink availability and lifecycle behavior +- test: keep callback-based composition validation with child target, context fields, and timestamp enabled + +### Example + +- docs: update `examples/basic` to demonstrate buffered sink usage +- docs: update `examples/basic` to demonstrate filter sink usage +- docs: update examples and README with helper-based chained logger filter usage +- docs: update examples and README with record patch usage for redaction and enrichment +- docs: update examples and README with explicit queued sink usage and overflow policy examples +- docs: update examples and README with configurable text formatter usage +- docs: update examples and README with native file sink usage and backend caveat +- docs: keep README focused on public usage and move change tracking to `docs/changes/` diff --git a/examples/basic/main.mbt b/examples/basic/main.mbt index 82d58ed..2616bc9 100644 --- a/examples/basic/main.mbt +++ b/examples/basic/main.mbt @@ -33,4 +33,71 @@ fn main { target="hook", ) callback_logger.info("callback sink ready") + + let pretty_logger = @lib.Logger::new( + @lib.text_console_sink(@lib.text_formatter(show_timestamp=false, separator=" | ")), + min_level=@lib.Level::Info, + target="pretty", + ) + pretty_logger.info("custom text format", fields=[@lib.field("mode", "pretty")]) + + if @lib.native_files_supported() { + let file_logger = @lib.Logger::new( + @lib.file_sink("bitlogger-example.log"), + min_level=@lib.Level::Info, + target="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") + 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 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") + + let filtered_logger = @lib.Logger::new( + @lib.console_sink(), + min_level=@lib.Level::Info, + target="service", + ).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") + + let patched_logger = @lib.Logger::new( + @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")]) + + let queued_logger = @lib.Logger::new( + @lib.console_sink(), + min_level=@lib.Level::Info, + target="queue", + ).with_queue(max_pending=2, overflow=@lib.QueueOverflowPolicy::DropOldest) + queued_logger.info("queued one") + queued_logger.info("queued two") + queued_logger.info("queued three") + ignore(queued_logger.sink.flush()) }