mirror of
https://github.com/Nanaloveyuki/BitLogger.git
synced 2026-05-30 15:42:25 +00:00
✨ Add split sink routing helpers
This commit is contained in:
@@ -19,6 +19,7 @@ BitLogger 是一个基于 MoonBit 编写的结构化日志库。
|
|||||||
- 🧩 核心能力清晰:先把 logging core 做稳,再继续扩展 rotation/async 等能力。
|
- 🧩 核心能力清晰:先把 logging core 做稳,再继续扩展 rotation/async 等能力。
|
||||||
- 🏗️ 结构明确:按 `level / record / formatter / sinks / logger / global` 拆文件,便于继续维护。
|
- 🏗️ 结构明确:按 `level / record / formatter / sinks / logger / global` 拆文件,便于继续维护。
|
||||||
- 🔌 可扩展:支持 `fanout_sink(...)` 和 `callback_sink(...)`,方便后续桥接文件、指标或外部系统。
|
- 🔌 可扩展:支持 `fanout_sink(...)` 和 `callback_sink(...)`,方便后续桥接文件、指标或外部系统。
|
||||||
|
- 🔀 可分流:支持 `split_sink(...)` / `split_by_level(...)`,可按谓词或 level 将日志路由到不同 sink。
|
||||||
- 🧱 可组合:支持 `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。
|
||||||
@@ -127,6 +128,27 @@ ignore(logger.sink.flush())
|
|||||||
```
|
```
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
<details><summary>按 level 分流 sink 示例</summary>
|
||||||
|
|
||||||
|
```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")
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
<details><summary>自定义文本 formatter 示例</summary>
|
<details><summary>自定义文本 formatter 示例</summary>
|
||||||
|
|
||||||
```moonbit
|
```moonbit
|
||||||
|
|||||||
@@ -182,6 +182,58 @@ test "callback sink receives record" {
|
|||||||
inspect(captured_message.val, content="hello")
|
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" {
|
test "callback sink sees child target and context logger shape" {
|
||||||
let captured_target : Ref[String] = Ref::new("")
|
let captured_target : Ref[String] = Ref::new("")
|
||||||
let captured_message : Ref[String] = Ref::new("")
|
let captured_message : Ref[String] = Ref::new("")
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ BitLogger 是一个基于 MoonBit 的结构化日志库。
|
|||||||
- `json_console_sink()` 提供 JSON 控制台输出
|
- `json_console_sink()` 提供 JSON 控制台输出
|
||||||
- sink composition via `fanout_sink(...)`
|
- sink composition via `fanout_sink(...)`
|
||||||
- `fanout_sink(...)` 支持多 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(...)`
|
- custom callback sink via `callback_sink(...)`
|
||||||
- `callback_sink(...)` 支持自定义外部集成
|
- `callback_sink(...)` 支持自定义外部集成
|
||||||
- buffered sink via `buffered_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
|
```mbt check
|
||||||
test {
|
test {
|
||||||
let formatter = text_formatter(
|
let formatter = text_formatter(
|
||||||
|
|||||||
@@ -247,6 +247,34 @@ pub impl[A : Sink, B : Sink] Sink for FanoutSink[A, B] with write(self, rec) {
|
|||||||
self.right.write({ ..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 {
|
pub struct CallbackSink {
|
||||||
callback : (Record) -> Unit
|
callback : (Record) -> Unit
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ BitLogger currently provides:
|
|||||||
- context fields via `with_context_fields(...)`
|
- context fields via `with_context_fields(...)`
|
||||||
- optional timestamps via `with_timestamp()`
|
- optional timestamps via `with_timestamp()`
|
||||||
- sink fanout via `fanout_sink(...)`
|
- sink fanout via `fanout_sink(...)`
|
||||||
|
- sink routing via `split_sink(...)` and `split_by_level(...)`
|
||||||
- custom integration via `callback_sink(...)`
|
- custom integration via `callback_sink(...)`
|
||||||
- in-memory buffering via `buffered_sink(...)`
|
- in-memory buffering via `buffered_sink(...)`
|
||||||
- record filtering via `filter_sink(...)`
|
- record filtering via `filter_sink(...)`
|
||||||
@@ -121,6 +122,25 @@ logger.info("three")
|
|||||||
ignore(logger.sink.flush())
|
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:
|
Custom text formatter:
|
||||||
|
|
||||||
```moonbit
|
```moonbit
|
||||||
|
|||||||
@@ -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: 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: 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
|
||||||
|
|
||||||
### Test
|
### Test
|
||||||
|
|
||||||
@@ -40,6 +41,7 @@ version 0.3.0
|
|||||||
- test: cover dropped-count reporting for bounded config-built queue
|
- test: cover dropped-count reporting for bounded config-built queue
|
||||||
- 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: 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
|
||||||
|
|
||||||
@@ -52,6 +54,7 @@ version 0.3.0
|
|||||||
- docs: update root README, English README, and Mooncake README with config usage notes
|
- 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 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: 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
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,20 @@ fn main {
|
|||||||
)
|
)
|
||||||
fanout_logger.info("dual output", fields=[@lib.field("kind", "fanout")])
|
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")
|
let timed_logger = @lib.Logger::new(@lib.console_sink(), min_level=@lib.Level::Info, target="timed")
|
||||||
.with_timestamp()
|
.with_timestamp()
|
||||||
timed_logger.info("timestamp enabled", fields=[@lib.field("kind", "time")])
|
timed_logger.info("timestamp enabled", fields=[@lib.field("kind", "time")])
|
||||||
|
|||||||
Reference in New Issue
Block a user