diff --git a/README.md b/README.md
index d6c035f..a1ea4a2 100644
--- a/README.md
+++ b/README.md
@@ -23,6 +23,7 @@ BitLogger 是一个基于 MoonBit 编写的结构化日志库。
- 🧱 可组合:支持 `buffered_sink(...)` 和 `filter_sink(...)`,可以在不引入复杂 runtime 的前提下组合输出策略。
- 🔎 可复用过滤:提供 `target_has_prefix(...)`、`message_contains(...)`、`level_at_least(...)`、`field_equals(...)` 等过滤辅助函数。
- 🩹 可变换 Record:支持 `with_patch(...)`、`patch_sink(...)` 与脱敏/补字段/message 变换 helper。
+- 🧷 可绑定上下文:支持 `bind(...)` 与 `fields(...)`,更方便地封装复用字段上下文。
- 📮 显式队列:支持 `queued_sink(...)` / `with_queue(...)`、有界积压与溢出策略,作为后续 async sink 的 runtime-safe 基础。
- 🧾 可配置文本格式:支持 `text_formatter(...)`、`format_text(...)`、`text_console_sink(...)`、`formatted_callback_sink(...)` 与模板化 `template` 输出。
- 💾 Native 文件输出:支持 `file_sink(...)`,并已提供基础 size rotation / backup retention;当前仅保证 `native/llvm` backend 可用。
@@ -115,6 +116,17 @@ logger.info("login", fields=[field("user", "alice"), field("token", "secret")])
```
+bind 上下文示例
+
+```moonbit
+let logger = Logger::new(console_sink(), target="audit")
+ .bind(fields([("service", "bitlogger"), ("scope", "login")]))
+
+logger.info("accepted", fields=[field("user", "alice")])
+```
+
+
+
显式 queue sink 示例
```moonbit
diff --git a/bitlogger/BitLogger_test.mbt b/bitlogger/BitLogger_test.mbt
index 3a1d9e8..55c80fd 100644
--- a/bitlogger/BitLogger_test.mbt
+++ b/bitlogger/BitLogger_test.mbt
@@ -14,6 +14,15 @@ test "context sink merges fields" {
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" {
let logger = Logger::new(
fanout_sink(console_sink(), json_console_sink()),
diff --git a/bitlogger/BitLogger_wbtest.mbt b/bitlogger/BitLogger_wbtest.mbt
index de2827f..f69745e 100644
--- a/bitlogger/BitLogger_wbtest.mbt
+++ b/bitlogger/BitLogger_wbtest.mbt
@@ -259,6 +259,31 @@ test "callback sink sees child target and context logger shape" {
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" {
let flushed_messages : Ref[Array[String]] = Ref::new([])
let sink = buffered_sink(
diff --git a/bitlogger/README.mbt.md b/bitlogger/README.mbt.md
index 9d7469e..7316b65 100644
--- a/bitlogger/README.mbt.md
+++ b/bitlogger/README.mbt.md
@@ -36,6 +36,8 @@ BitLogger 是一个基于 MoonBit 的结构化日志库。
- 提供 `target_has_prefix(...)`、`message_contains(...)`、`field_equals(...)` 等可复用过滤辅助函数
- record patching via `with_patch(...)` and `patch_sink(...)`
- 支持 `with_patch(...)`、`patch_sink(...)` 以及常见 record patch helper
+- context binding via `bind(...)` and `fields(...)`
+- 支持 `bind(...)`、`fields(...)`,更顺手地封装上下文字段
- explicit queued delivery via `queued_sink(...)` and `with_queue(...)`
- 支持 `queued_sink(...)`、`with_queue(...)`、有界积压与溢出策略
- 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
test {
let logger = Logger::new(console_sink(), target="queue")
diff --git a/bitlogger/logger.mbt b/bitlogger/logger.mbt
index 2305b67..617bdeb 100644
--- a/bitlogger/logger.mbt
+++ b/bitlogger/logger.mbt
@@ -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]] {
{
min_level: self.min_level,
diff --git a/bitlogger/record.mbt b/bitlogger/record.mbt
index 027b37e..7502e9a 100644
--- a/bitlogger/record.mbt
+++ b/bitlogger/record.mbt
@@ -7,6 +7,12 @@ pub fn field(key : String, value : String) -> Field {
{ key, value }
}
+pub fn fields(entries : Array[(String, String)]) -> Array[Field] {
+ entries.map(fn(entry) {
+ field(entry.0, entry.1)
+ })
+}
+
pub struct Record {
level : Level
timestamp_ms : UInt64
diff --git a/docs/README-en.md b/docs/README-en.md
index 64fe8a7..f0b1668 100644
--- a/docs/README-en.md
+++ b/docs/README-en.md
@@ -21,6 +21,7 @@ BitLogger currently provides:
- 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(...)`
+- context binding via `bind(...)` and `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(...)`, `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")])
```
+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:
```moonbit
diff --git a/docs/changes/0.3.0.md b/docs/changes/0.3.0.md
index 9dec585..6c4ef38 100644
--- a/docs/changes/0.3.0.md
+++ b/docs/changes/0.3.0.md
@@ -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: 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 `Logger::bind(...)` as an ergonomic context-binding alias and `fields(...)` helper for tuple-based field construction
### Test
@@ -42,6 +43,7 @@ version 0.3.0
- 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: cover `bind(...)` context composition and `fields(...)` helper 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
@@ -55,6 +57,7 @@ version 0.3.0
- 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: add `bind(...)` examples for reusable context binding
- 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 de9ab64..86d479d 100644
--- a/examples/basic/main.mbt
+++ b/examples/basic/main.mbt
@@ -7,6 +7,13 @@ fn main {
.with_context_fields([@lib.field("service", "bitlogger")])
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")
json_logger.info("json output", fields=[@lib.field("kind", "example")])