mirror of
https://github.com/Nanaloveyuki/BitLogger.git
synced 2026-05-30 15:42:25 +00:00
✨ Add bind-style context helpers
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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()),
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,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")])
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user