Add bind-style context helpers

This commit is contained in:
Nanaloveyuki
2026-05-10 12:12:11 +08:00
parent 2e008b649c
commit a26ec6399c
9 changed files with 86 additions and 0 deletions
+12
View File
@@ -23,6 +23,7 @@ BitLogger 是一个基于 MoonBit 编写的结构化日志库。
- 🧱 可组合:支持 `buffered_sink(...)``filter_sink(...)`,可以在不引入复杂 runtime 的前提下组合输出策略。 - 🧱 可组合:支持 `buffered_sink(...)``filter_sink(...)`,可以在不引入复杂 runtime 的前提下组合输出策略。
- 🔎 可复用过滤:提供 `target_has_prefix(...)``message_contains(...)``level_at_least(...)``field_equals(...)` 等过滤辅助函数。 - 🔎 可复用过滤:提供 `target_has_prefix(...)``message_contains(...)``level_at_least(...)``field_equals(...)` 等过滤辅助函数。
- 🩹 可变换 Record:支持 `with_patch(...)``patch_sink(...)` 与脱敏/补字段/message 变换 helper。 - 🩹 可变换 Record:支持 `with_patch(...)``patch_sink(...)` 与脱敏/补字段/message 变换 helper。
- 🧷 可绑定上下文:支持 `bind(...)``fields(...)`,更方便地封装复用字段上下文。
- 📮 显式队列:支持 `queued_sink(...)` / `with_queue(...)`、有界积压与溢出策略,作为后续 async sink 的 runtime-safe 基础。 - 📮 显式队列:支持 `queued_sink(...)` / `with_queue(...)`、有界积压与溢出策略,作为后续 async sink 的 runtime-safe 基础。
- 🧾 可配置文本格式:支持 `text_formatter(...)``format_text(...)``text_console_sink(...)``formatted_callback_sink(...)` 与模板化 `template` 输出。 - 🧾 可配置文本格式:支持 `text_formatter(...)``format_text(...)``text_console_sink(...)``formatted_callback_sink(...)` 与模板化 `template` 输出。
- 💾 Native 文件输出:支持 `file_sink(...)`,并已提供基础 size rotation / backup retention;当前仅保证 `native/llvm` backend 可用。 - 💾 Native 文件输出:支持 `file_sink(...)`,并已提供基础 size rotation / backup retention;当前仅保证 `native/llvm` backend 可用。
@@ -115,6 +116,17 @@ logger.info("login", fields=[field("user", "alice"), field("token", "secret")])
``` ```
</details> </details>
<details><summary>bind 上下文示例</summary>
```moonbit
let logger = Logger::new(console_sink(), target="audit")
.bind(fields([("service", "bitlogger"), ("scope", "login")]))
logger.info("accepted", fields=[field("user", "alice")])
```
</details>
<details><summary>显式 queue sink 示例</summary> <details><summary>显式 queue sink 示例</summary>
```moonbit ```moonbit
+9
View File
@@ -14,6 +14,15 @@ test "context sink merges fields" {
logger.info("hello", fields=[field("mode", "test")]) 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")
inspect(items[0].key, content="service")
inspect(items[0].value, content="bitlogger")
inspect(items[1].key, content="mode")
inspect(items[1].value, content="test")
}
test "fanout sink can write to plain and json outputs" { test "fanout sink can write to plain and json outputs" {
let logger = Logger::new( let logger = Logger::new(
fanout_sink(console_sink(), json_console_sink()), fanout_sink(console_sink(), json_console_sink()),
+25
View File
@@ -259,6 +259,31 @@ test "callback sink sees child target and context logger shape" {
inspect(captured_timestamp.val > 0UL, content="true") inspect(captured_timestamp.val > 0UL, content="true")
} }
test "bind aliases context fields ergonomically" {
let captured_target : Ref[String] = Ref::new("")
let captured_message : Ref[String] = Ref::new("")
let captured_fields : Ref[Array[Field]] = Ref::new([])
let logger = Logger::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="bind",
).bind(fields([("service", "bitlogger"), ("scope", "audit")]))
logger.info("ready", fields=[field("mode", "test")])
inspect(captured_target.val, content="bind")
inspect(captured_message.val, content="ready")
inspect(captured_fields.val.length(), content="3")
inspect(captured_fields.val[0].key, content="service")
inspect(captured_fields.val[0].value, content="bitlogger")
inspect(captured_fields.val[1].key, content="scope")
inspect(captured_fields.val[1].value, content="audit")
inspect(captured_fields.val[2].key, content="mode")
inspect(captured_fields.val[2].value, content="test")
}
test "buffered sink flushes manually" { test "buffered sink flushes manually" {
let flushed_messages : Ref[Array[String]] = Ref::new([]) let flushed_messages : Ref[Array[String]] = Ref::new([])
let sink = buffered_sink( let sink = buffered_sink(
+10
View File
@@ -36,6 +36,8 @@ BitLogger 是一个基于 MoonBit 的结构化日志库。
- 提供 `target_has_prefix(...)``message_contains(...)``field_equals(...)` 等可复用过滤辅助函数 - 提供 `target_has_prefix(...)``message_contains(...)``field_equals(...)` 等可复用过滤辅助函数
- record patching via `with_patch(...)` and `patch_sink(...)` - record patching via `with_patch(...)` and `patch_sink(...)`
- 支持 `with_patch(...)``patch_sink(...)` 以及常见 record patch helper - 支持 `with_patch(...)``patch_sink(...)` 以及常见 record patch helper
- context binding via `bind(...)` and `fields(...)`
- 支持 `bind(...)``fields(...)`,更顺手地封装上下文字段
- explicit queued delivery via `queued_sink(...)` and `with_queue(...)` - explicit queued delivery via `queued_sink(...)` and `with_queue(...)`
- 支持 `queued_sink(...)``with_queue(...)`、有界积压与溢出策略 - 支持 `queued_sink(...)``with_queue(...)`、有界积压与溢出策略
- configurable text formatting via `text_formatter(...)`, `format_text(...)`, `text_console_sink(...)`, and template-driven `template` output - configurable text formatting via `text_formatter(...)`, `format_text(...)`, `text_console_sink(...)`, and template-driven `template` output
@@ -133,6 +135,14 @@ test {
} }
``` ```
```mbt check
test {
let logger = Logger::new(console_sink(), target="audit")
.bind(fields([("service", "bitlogger"), ("scope", "login")]))
logger.info("accepted", fields=[field("user", "alice")])
}
```
```mbt check ```mbt check
test { test {
let logger = Logger::new(console_sink(), target="queue") let logger = Logger::new(console_sink(), target="queue")
+4
View File
@@ -36,6 +36,10 @@ 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]] {
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, min_level: self.min_level,
+6
View File
@@ -7,6 +7,12 @@ pub fn field(key : String, value : String) -> Field {
{ key, value } { key, value }
} }
pub fn fields(entries : Array[(String, String)]) -> Array[Field] {
entries.map(fn(entry) {
field(entry.0, entry.1)
})
}
pub struct Record { pub struct Record {
level : Level level : Level
timestamp_ms : UInt64 timestamp_ms : UInt64
+10
View File
@@ -21,6 +21,7 @@ BitLogger currently provides:
- reusable filter helpers such as `target_has_prefix(...)`, `message_contains(...)`, `level_at_least(...)`, and `field_equals(...)` - reusable filter helpers such as `target_has_prefix(...)`, `message_contains(...)`, `level_at_least(...)`, and `field_equals(...)`
- record patching via `with_patch(...)` and `patch_sink(...)` - record patching via `with_patch(...)` and `patch_sink(...)`
- patch helpers such as `prefix_message(...)`, `append_fields(...)`, and `redact_fields(...)` - patch helpers such as `prefix_message(...)`, `append_fields(...)`, and `redact_fields(...)`
- context binding via `bind(...)` and `fields(...)`
- explicit queued delivery via `queued_sink(...)` and `with_queue(...)` - explicit queued delivery via `queued_sink(...)` and `with_queue(...)`
- bounded backlog with `QueueOverflowPolicy::DropNewest` and `QueueOverflowPolicy::DropOldest` - bounded backlog with `QueueOverflowPolicy::DropNewest` and `QueueOverflowPolicy::DropOldest`
- configurable text formatting via `text_formatter(...)`, `format_text(...)`, `text_console_sink(...)`, and template-driven `template` output - configurable text formatting via `text_formatter(...)`, `format_text(...)`, `text_console_sink(...)`, and template-driven `template` output
@@ -110,6 +111,15 @@ let logger = Logger::new(console_sink(), target="auth")
logger.info("login", fields=[field("user", "alice"), field("token", "secret")]) logger.info("login", fields=[field("user", "alice"), field("token", "secret")])
``` ```
Context binding:
```moonbit
let logger = Logger::new(console_sink(), target="audit")
.bind(fields([("service", "bitlogger"), ("scope", "login")]))
logger.info("accepted", fields=[field("user", "alice")])
```
Explicit queued sink: Explicit queued sink:
```moonbit ```moonbit
+3
View File
@@ -30,6 +30,7 @@ version 0.3.0
- feat: add `FileRotation`, `file_rotation(...)`, and size-based file rotation with retained backups for `file_sink(...)` - 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: 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 - feat: add `SplitSink`, `split_sink(...)`, and `split_by_level(...)` for routing records into different sinks by predicate or level
- feat: add `Logger::bind(...)` as an ergonomic context-binding alias and `fields(...)` helper for tuple-based field construction
### Test ### Test
@@ -42,6 +43,7 @@ version 0.3.0
- test: cover template-based formatter rendering and disabled token behavior - test: cover template-based formatter rendering and disabled token behavior
- test: cover file rotation config parsing, config roundtrip, and native rotation 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: cover split sink predicate routing and level-based routing behavior
- test: cover `bind(...)` context composition and `fields(...)` helper behavior
- test: add async logger lifecycle, config roundtrip, and batching/flush policy test seeds - 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 - build: verify `bitlogger_async --target native` and `examples/async_basic --target native` compile successfully
@@ -55,6 +57,7 @@ version 0.3.0
- docs: update formatter examples to demonstrate template-based text rendering - docs: update formatter examples to demonstrate template-based text rendering
- docs: update file sink examples to demonstrate rotation and backup retention - docs: update file sink examples to demonstrate rotation and backup retention
- docs: add split sink examples for level-based routing - docs: add split sink examples for level-based routing
- docs: add `bind(...)` examples for reusable context binding
- docs: update root README and English README with async adapter notes and current scope - docs: update root README and English README with async adapter notes and current scope
- chore: ignore local `.mooncakes/` cache directory in git - chore: ignore local `.mooncakes/` cache directory in git
+7
View File
@@ -7,6 +7,13 @@ fn main {
.with_context_fields([@lib.field("service", "bitlogger")]) .with_context_fields([@lib.field("service", "bitlogger")])
logger.debug("custom logger ready", fields=[@lib.field("sink", "console")]) logger.debug("custom logger ready", fields=[@lib.field("sink", "console")])
let bound_logger = @lib.Logger::new(
@lib.console_sink(),
min_level=@lib.Level::Info,
target="bound",
).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")]) json_logger.info("json output", fields=[@lib.field("kind", "example")])