mirror of
https://github.com/Nanaloveyuki/BitLogger.git
synced 2026-05-30 15:42:25 +00:00
✨ Add template-based text formatter
This commit is contained in:
@@ -23,7 +23,7 @@ BitLogger 是一个基于 MoonBit 编写的结构化日志库。
|
|||||||
- 🔎 可复用过滤:提供 `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。
|
||||||
- 📮 显式队列:支持 `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(...)`。
|
- 🧾 可配置文本格式:支持 `text_formatter(...)`、`format_text(...)`、`text_console_sink(...)`、`formatted_callback_sink(...)` 与模板化 `template` 输出。
|
||||||
- 💾 Native 文件输出:支持 `file_sink(...)`,但当前仅保证 `native/llvm` backend 可用。
|
- 💾 Native 文件输出:支持 `file_sink(...)`,但当前仅保证 `native/llvm` backend 可用。
|
||||||
- 📦 面向 MoonBit:API 和工程结构围绕 MoonBit 的 package / visibility / toolchain 现实约束设计。
|
- 📦 面向 MoonBit:API 和工程结构围绕 MoonBit 的 package / visibility / toolchain 现实约束设计。
|
||||||
|
|
||||||
@@ -130,7 +130,11 @@ ignore(logger.sink.flush())
|
|||||||
<details><summary>自定义文本 formatter 示例</summary>
|
<details><summary>自定义文本 formatter 示例</summary>
|
||||||
|
|
||||||
```moonbit
|
```moonbit
|
||||||
let formatter = text_formatter(show_timestamp=false, separator=" | ")
|
let formatter = text_formatter(
|
||||||
|
show_timestamp=false,
|
||||||
|
field_separator=",",
|
||||||
|
template="[{level}] {target} {message} :: {fields}",
|
||||||
|
)
|
||||||
let logger = Logger::new(text_console_sink(formatter), target="pretty")
|
let logger = Logger::new(text_console_sink(formatter), target="pretty")
|
||||||
|
|
||||||
logger.info("hello", fields=[field("mode", "pretty")])
|
logger.info("hello", fields=[field("mode", "pretty")])
|
||||||
@@ -142,7 +146,7 @@ logger.info("hello", fields=[field("mode", "pretty")])
|
|||||||
|
|
||||||
```moonbit
|
```moonbit
|
||||||
let config = parse_logger_config_text(
|
let config = parse_logger_config_text(
|
||||||
"{\"min_level\":\"debug\",\"target\":\"config.demo\",\"timestamp\":true,\"sink\":{\"kind\":\"text_console\",\"text_formatter\":{\"separator\":\" | \",\"show_timestamp\":false}},\"queue\":{\"max_pending\":2,\"overflow\":\"DropOldest\"}}",
|
"{\"min_level\":\"debug\",\"target\":\"config.demo\",\"timestamp\":true,\"sink\":{\"kind\":\"text_console\",\"text_formatter\":{\"show_timestamp\":false,\"field_separator\":\",\",\"template\":\"[{level}] {target} {message} :: {fields}\"}},\"queue\":{\"max_pending\":2,\"overflow\":\"DropOldest\"}}",
|
||||||
)
|
)
|
||||||
|
|
||||||
let logger = build_logger(config)
|
let logger = build_logger(config)
|
||||||
@@ -179,6 +183,7 @@ if native_files_supported() {
|
|||||||
|
|
||||||
- 当前提供 JSON 配置层:`parse_logger_config_text(...)`、`stringify_logger_config(...)`、`build_logger(...)`
|
- 当前提供 JSON 配置层:`parse_logger_config_text(...)`、`stringify_logger_config(...)`、`build_logger(...)`
|
||||||
- 已支持字段:`min_level`、`target`、`timestamp`、`sink.kind`、`sink.path`、`sink.append`、`sink.auto_flush`、`sink.text_formatter`、`queue`
|
- 已支持字段:`min_level`、`target`、`timestamp`、`sink.kind`、`sink.path`、`sink.append`、`sink.auto_flush`、`sink.text_formatter`、`queue`
|
||||||
|
- `sink.text_formatter.template` 当前支持固定 token:`{timestamp}`、`{timestamp_ms}`、`{level}`、`{target}`、`{message}`、`{fields}`
|
||||||
- 当前可由配置直接组装的 sink 类型:`console`、`json_console`、`text_console`、`file`
|
- 当前可由配置直接组装的 sink 类型:`console`、`json_console`、`text_console`、`file`
|
||||||
- `queue` 会作为显式包装层附着在最终 sink 外侧;这仍然是同步 drain 模型,不是 async runtime
|
- `queue` 会作为显式包装层附着在最终 sink 外侧;这仍然是同步 drain 模型,不是 async runtime
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ test "logger config parser reads core options" {
|
|||||||
|
|
||||||
test "logger config parser reads formatter and queue options" {
|
test "logger config parser reads formatter and queue options" {
|
||||||
let config = parse_logger_config_text(
|
let config = parse_logger_config_text(
|
||||||
"{\"sink\":{\"kind\":\"text_console\",\"text_formatter\":{\"separator\":\" | \",\"show_timestamp\":false}},\"queue\":{\"max_pending\":32,\"overflow\":\"DropOldest\"}}",
|
"{\"sink\":{\"kind\":\"text_console\",\"text_formatter\":{\"separator\":\" | \",\"show_timestamp\":false,\"template\":\"[{level}] {message}\"}},\"queue\":{\"max_pending\":32,\"overflow\":\"DropOldest\"}}",
|
||||||
)
|
)
|
||||||
inspect(match config.sink.kind {
|
inspect(match config.sink.kind {
|
||||||
SinkKind::TextConsole => "TextConsole"
|
SinkKind::TextConsole => "TextConsole"
|
||||||
@@ -49,6 +49,7 @@ test "logger config parser reads formatter and queue options" {
|
|||||||
}, content="TextConsole")
|
}, content="TextConsole")
|
||||||
inspect(config.sink.text_formatter.separator, content=" | ")
|
inspect(config.sink.text_formatter.separator, content=" | ")
|
||||||
inspect(config.sink.text_formatter.show_timestamp, content="false")
|
inspect(config.sink.text_formatter.show_timestamp, content="false")
|
||||||
|
inspect(config.sink.text_formatter.template, content="[{level}] {message}")
|
||||||
match config.queue {
|
match config.queue {
|
||||||
Some(queue) => {
|
Some(queue) => {
|
||||||
inspect(queue.max_pending, content="32")
|
inspect(queue.max_pending, content="32")
|
||||||
@@ -76,6 +77,7 @@ test "logger config stringify roundtrips stable fields" {
|
|||||||
show_fields=true,
|
show_fields=true,
|
||||||
separator=" | ",
|
separator=" | ",
|
||||||
field_separator=",",
|
field_separator=",",
|
||||||
|
template="[{level}] {target} {message}",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
queue=Some(QueueConfig::new(8, overflow=QueueOverflowPolicy::DropNewest)),
|
queue=Some(QueueConfig::new(8, overflow=QueueOverflowPolicy::DropNewest)),
|
||||||
@@ -86,6 +88,7 @@ test "logger config stringify roundtrips stable fields" {
|
|||||||
inspect(config.target, content="api")
|
inspect(config.target, content="api")
|
||||||
inspect(config.timestamp, content="true")
|
inspect(config.timestamp, content="true")
|
||||||
inspect(config.sink.text_formatter.separator, content=" | ")
|
inspect(config.sink.text_formatter.separator, content=" | ")
|
||||||
|
inspect(config.sink.text_formatter.template, content="[{level}] {target} {message}")
|
||||||
}
|
}
|
||||||
|
|
||||||
test "build logger from config supports queued text console" {
|
test "build logger from config supports queued text console" {
|
||||||
|
|||||||
@@ -41,6 +41,35 @@ test "text formatter can emit message only" {
|
|||||||
inspect(format_text(rec, formatter=message_only), content="just message")
|
inspect(format_text(rec, formatter=message_only), content="just message")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "text formatter supports template rendering" {
|
||||||
|
let rec = record(
|
||||||
|
Level::Info,
|
||||||
|
"template hello",
|
||||||
|
timestamp_ms=123UL,
|
||||||
|
target="svc.api",
|
||||||
|
fields=[field("user", "alice"), field("request_id", "42")],
|
||||||
|
)
|
||||||
|
let formatter = text_formatter(
|
||||||
|
separator=" | ",
|
||||||
|
field_separator=",",
|
||||||
|
template="[{level}] {target} {message} :: {fields} @{timestamp}",
|
||||||
|
)
|
||||||
|
inspect(
|
||||||
|
format_text(rec, formatter=formatter),
|
||||||
|
content="[INFO] svc.api template hello :: user=alice,request_id=42 @123",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
test "text formatter template respects disabled fields" {
|
||||||
|
let rec = record(Level::Warn, "just message", target="svc")
|
||||||
|
let formatter = text_formatter(
|
||||||
|
show_target=false,
|
||||||
|
show_fields=false,
|
||||||
|
template="[{level}] {target}{message}{fields}",
|
||||||
|
)
|
||||||
|
inspect(format_text(rec, formatter=formatter), content="[WARN] just message")
|
||||||
|
}
|
||||||
|
|
||||||
test "formatted callback sink receives rendered text" {
|
test "formatted callback sink receives rendered text" {
|
||||||
let rendered : Ref[String] = Ref::new("")
|
let rendered : Ref[String] = Ref::new("")
|
||||||
let sink = text_callback_sink(
|
let sink = text_callback_sink(
|
||||||
|
|||||||
+14
-4
@@ -36,8 +36,8 @@ BitLogger 是一个基于 MoonBit 的结构化日志库。
|
|||||||
- 支持 `with_patch(...)`、`patch_sink(...)` 以及常见 record patch helper
|
- 支持 `with_patch(...)`、`patch_sink(...)` 以及常见 record patch helper
|
||||||
- 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(...)`, and `text_console_sink(...)`
|
- configurable text formatting via `text_formatter(...)`, `format_text(...)`, `text_console_sink(...)`, and template-driven `template` output
|
||||||
- 支持 `text_formatter(...)`、`format_text(...)`、`text_console_sink(...)` 等文本格式化能力
|
- 支持 `text_formatter(...)`、`format_text(...)`、`text_console_sink(...)` 以及模板化 `template` 文本输出
|
||||||
- JSON config parsing via `parse_logger_config_text(...)` and `stringify_logger_config(...)`
|
- JSON config parsing via `parse_logger_config_text(...)` and `stringify_logger_config(...)`
|
||||||
- 支持 `parse_logger_config_text(...)`、`stringify_logger_config(...)` 进行最小 JSON 配置读写
|
- 支持 `parse_logger_config_text(...)`、`stringify_logger_config(...)` 进行最小 JSON 配置读写
|
||||||
- config-driven logger assembly via `build_logger(...)`
|
- config-driven logger assembly via `build_logger(...)`
|
||||||
@@ -144,7 +144,11 @@ test {
|
|||||||
|
|
||||||
```mbt check
|
```mbt check
|
||||||
test {
|
test {
|
||||||
let formatter = text_formatter(show_timestamp=false, separator=" | ")
|
let formatter = text_formatter(
|
||||||
|
show_timestamp=false,
|
||||||
|
field_separator=",",
|
||||||
|
template="[{level}] {target} {message} :: {fields}",
|
||||||
|
)
|
||||||
let logger = Logger::new(text_console_sink(formatter), target="pretty")
|
let logger = Logger::new(text_console_sink(formatter), target="pretty")
|
||||||
logger.info("hello", fields=[field("mode", "pretty")])
|
logger.info("hello", fields=[field("mode", "pretty")])
|
||||||
}
|
}
|
||||||
@@ -153,7 +157,7 @@ test {
|
|||||||
```mbt check
|
```mbt check
|
||||||
test {
|
test {
|
||||||
let config = parse_logger_config_text(
|
let config = parse_logger_config_text(
|
||||||
"{\"min_level\":\"debug\",\"target\":\"config.demo\",\"timestamp\":true,\"sink\":{\"kind\":\"text_console\",\"text_formatter\":{\"separator\":\" | \",\"show_timestamp\":false}},\"queue\":{\"max_pending\":2,\"overflow\":\"DropOldest\"}}",
|
"{\"min_level\":\"debug\",\"target\":\"config.demo\",\"timestamp\":true,\"sink\":{\"kind\":\"text_console\",\"text_formatter\":{\"show_timestamp\":false,\"field_separator\":\",\",\"template\":\"[{level}] {target} {message} :: {fields}\"}},\"queue\":{\"max_pending\":2,\"overflow\":\"DropOldest\"}}",
|
||||||
)
|
)
|
||||||
let logger = build_logger(config)
|
let logger = build_logger(config)
|
||||||
logger.info("configured from json")
|
logger.info("configured from json")
|
||||||
@@ -161,6 +165,12 @@ test {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Formatter Template / 模板格式
|
||||||
|
|
||||||
|
- supported tokens / 支持的 token: `{timestamp}`, `{timestamp_ms}`, `{level}`, `{target}`, `{message}`, `{fields}`
|
||||||
|
- disabled or missing parts render as empty text / 被关闭或缺失的部分会渲染为空字符串
|
||||||
|
- `template` is intentionally a simple token replacement layer, not a full DSL / `template` 当前刻意保持为轻量 token 替换层,而不是完整 DSL
|
||||||
|
|
||||||
```mbt check
|
```mbt check
|
||||||
test {
|
test {
|
||||||
if native_files_supported() {
|
if native_files_supported() {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ pub struct TextFormatterConfig {
|
|||||||
show_fields : Bool
|
show_fields : Bool
|
||||||
separator : String
|
separator : String
|
||||||
field_separator : String
|
field_separator : String
|
||||||
|
template : String
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn TextFormatterConfig::new(
|
pub fn TextFormatterConfig::new(
|
||||||
@@ -25,6 +26,7 @@ pub fn TextFormatterConfig::new(
|
|||||||
show_fields~ : Bool = true,
|
show_fields~ : Bool = true,
|
||||||
separator~ : String = " ",
|
separator~ : String = " ",
|
||||||
field_separator~ : String = " ",
|
field_separator~ : String = " ",
|
||||||
|
template~ : String = "",
|
||||||
) -> TextFormatterConfig {
|
) -> TextFormatterConfig {
|
||||||
{
|
{
|
||||||
show_timestamp,
|
show_timestamp,
|
||||||
@@ -33,6 +35,7 @@ pub fn TextFormatterConfig::new(
|
|||||||
show_fields,
|
show_fields,
|
||||||
separator,
|
separator,
|
||||||
field_separator,
|
field_separator,
|
||||||
|
template,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,6 +47,7 @@ pub fn TextFormatterConfig::to_formatter(self : TextFormatterConfig) -> TextForm
|
|||||||
show_fields=self.show_fields,
|
show_fields=self.show_fields,
|
||||||
separator=self.separator,
|
separator=self.separator,
|
||||||
field_separator=self.field_separator,
|
field_separator=self.field_separator,
|
||||||
|
template=self.template,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -344,6 +348,7 @@ fn parse_text_formatter_config(value : @json_parser.JsonValue) -> TextFormatterC
|
|||||||
show_fields=get_bool(obj, "show_fields", default=true),
|
show_fields=get_bool(obj, "show_fields", default=true),
|
||||||
separator=get_string(obj, "separator", default=" "),
|
separator=get_string(obj, "separator", default=" "),
|
||||||
field_separator=get_string(obj, "field_separator", default=" "),
|
field_separator=get_string(obj, "field_separator", default=" "),
|
||||||
|
template=get_string(obj, "template", default=""),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -416,6 +421,7 @@ fn text_formatter_config_to_json(config : TextFormatterConfig) -> @json_parser.J
|
|||||||
"show_fields": @json_parser.JsonValue::Bool(config.show_fields),
|
"show_fields": @json_parser.JsonValue::Bool(config.show_fields),
|
||||||
"separator": @json_parser.JsonValue::String(config.separator),
|
"separator": @json_parser.JsonValue::String(config.separator),
|
||||||
"field_separator": @json_parser.JsonValue::String(config.field_separator),
|
"field_separator": @json_parser.JsonValue::String(config.field_separator),
|
||||||
|
"template": @json_parser.JsonValue::String(config.template),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+58
-1
@@ -7,6 +7,7 @@ pub struct TextFormatter {
|
|||||||
show_fields : Bool
|
show_fields : Bool
|
||||||
separator : String
|
separator : String
|
||||||
field_separator : String
|
field_separator : String
|
||||||
|
template : String
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn text_formatter(
|
pub fn text_formatter(
|
||||||
@@ -16,8 +17,17 @@ pub fn text_formatter(
|
|||||||
show_fields~ : Bool = true,
|
show_fields~ : Bool = true,
|
||||||
separator~ : String = " ",
|
separator~ : String = " ",
|
||||||
field_separator~ : String = " ",
|
field_separator~ : String = " ",
|
||||||
|
template~ : String = "",
|
||||||
) -> TextFormatter {
|
) -> TextFormatter {
|
||||||
{ show_timestamp, show_level, show_target, show_fields, separator, field_separator }
|
{
|
||||||
|
show_timestamp,
|
||||||
|
show_level,
|
||||||
|
show_target,
|
||||||
|
show_fields,
|
||||||
|
separator,
|
||||||
|
field_separator,
|
||||||
|
template,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fields_to_json(fields : Array[Field]) -> Json {
|
fn fields_to_json(fields : Array[Field]) -> Json {
|
||||||
@@ -32,7 +42,54 @@ fn format_fields(fields : Array[Field], separator : String) -> String {
|
|||||||
fields.map(fn(f) { "\{f.key}=\{f.value}" }).join(separator)
|
fields.map(fn(f) { "\{f.key}=\{f.value}" }).join(separator)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn timestamp_text(rec : Record, formatter : TextFormatter) -> String {
|
||||||
|
if formatter.show_timestamp && rec.timestamp_ms != 0UL {
|
||||||
|
rec.timestamp_ms.to_string()
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn level_text(rec : Record, formatter : TextFormatter) -> String {
|
||||||
|
if formatter.show_level {
|
||||||
|
rec.level.label()
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn target_text(rec : Record, formatter : TextFormatter) -> String {
|
||||||
|
if formatter.show_target && rec.target != "" {
|
||||||
|
rec.target
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fields_text(rec : Record, formatter : TextFormatter) -> String {
|
||||||
|
if formatter.show_fields && rec.fields.length() != 0 {
|
||||||
|
format_fields(rec.fields, formatter.field_separator)
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_template(rec : Record, formatter : TextFormatter) -> String {
|
||||||
|
formatter.template
|
||||||
|
.replace_all(old="{timestamp}", new=timestamp_text(rec, formatter))
|
||||||
|
.replace_all(old="{timestamp_ms}", new=timestamp_text(rec, formatter))
|
||||||
|
.replace_all(old="{level}", new=level_text(rec, formatter))
|
||||||
|
.replace_all(old="{target}", new=target_text(rec, formatter))
|
||||||
|
.replace_all(old="{message}", new=rec.message)
|
||||||
|
.replace_all(old="{fields}", new=fields_text(rec, formatter))
|
||||||
|
.trim()
|
||||||
|
.to_owned()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn format_text(rec : Record, formatter~ : TextFormatter = text_formatter()) -> String {
|
pub fn format_text(rec : Record, formatter~ : TextFormatter = text_formatter()) -> String {
|
||||||
|
if formatter.template != "" {
|
||||||
|
return render_template(rec, formatter)
|
||||||
|
}
|
||||||
let parts : Array[String] = []
|
let parts : Array[String] = []
|
||||||
if formatter.show_timestamp && rec.timestamp_ms != 0UL {
|
if formatter.show_timestamp && rec.timestamp_ms != 0UL {
|
||||||
parts.push("[\{rec.timestamp_ms.to_string()}]")
|
parts.push("[\{rec.timestamp_ms.to_string()}]")
|
||||||
|
|||||||
+8
-3
@@ -22,7 +22,7 @@ BitLogger currently provides:
|
|||||||
- patch helpers such as `prefix_message(...)`, `append_fields(...)`, and `redact_fields(...)`
|
- patch helpers such as `prefix_message(...)`, `append_fields(...)`, and `redact_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(...)`, and `text_console_sink(...)`
|
- configurable text formatting via `text_formatter(...)`, `format_text(...)`, `text_console_sink(...)`, and template-driven `template` output
|
||||||
- formatter-based callback integration via `formatted_callback_sink(...)`
|
- formatter-based callback integration via `formatted_callback_sink(...)`
|
||||||
- native-only file output via `file_sink(...)`
|
- native-only file output via `file_sink(...)`
|
||||||
- `native_files_supported()` for backend capability detection
|
- `native_files_supported()` for backend capability detection
|
||||||
@@ -124,7 +124,11 @@ ignore(logger.sink.flush())
|
|||||||
Custom text formatter:
|
Custom text formatter:
|
||||||
|
|
||||||
```moonbit
|
```moonbit
|
||||||
let formatter = text_formatter(show_timestamp=false, separator=" | ")
|
let formatter = text_formatter(
|
||||||
|
show_timestamp=false,
|
||||||
|
field_separator=",",
|
||||||
|
template="[{level}] {target} {message} :: {fields}",
|
||||||
|
)
|
||||||
let logger = Logger::new(text_console_sink(formatter), target="pretty")
|
let logger = Logger::new(text_console_sink(formatter), target="pretty")
|
||||||
|
|
||||||
logger.info("hello", fields=[field("mode", "pretty")])
|
logger.info("hello", fields=[field("mode", "pretty")])
|
||||||
@@ -134,7 +138,7 @@ JSON config loading:
|
|||||||
|
|
||||||
```moonbit
|
```moonbit
|
||||||
let config = parse_logger_config_text(
|
let config = parse_logger_config_text(
|
||||||
"{\"min_level\":\"debug\",\"target\":\"config.demo\",\"timestamp\":true,\"sink\":{\"kind\":\"text_console\",\"text_formatter\":{\"separator\":\" | \",\"show_timestamp\":false}},\"queue\":{\"max_pending\":2,\"overflow\":\"DropOldest\"}}",
|
"{\"min_level\":\"debug\",\"target\":\"config.demo\",\"timestamp\":true,\"sink\":{\"kind\":\"text_console\",\"text_formatter\":{\"show_timestamp\":false,\"field_separator\":\",\",\"template\":\"[{level}] {target} {message} :: {fields}\"}},\"queue\":{\"max_pending\":2,\"overflow\":\"DropOldest\"}}",
|
||||||
)
|
)
|
||||||
|
|
||||||
let logger = build_logger(config)
|
let logger = build_logger(config)
|
||||||
@@ -168,6 +172,7 @@ if native_files_supported() {
|
|||||||
|
|
||||||
- BitLogger now includes a JSON config layer via `parse_logger_config_text(...)`, `stringify_logger_config(...)`, and `build_logger(...)`.
|
- BitLogger now includes a JSON config layer via `parse_logger_config_text(...)`, `stringify_logger_config(...)`, and `build_logger(...)`.
|
||||||
- Supported keys include `min_level`, `target`, `timestamp`, `sink.kind`, `sink.path`, `sink.append`, `sink.auto_flush`, `sink.text_formatter`, and `queue`.
|
- Supported keys include `min_level`, `target`, `timestamp`, `sink.kind`, `sink.path`, `sink.append`, `sink.auto_flush`, `sink.text_formatter`, and `queue`.
|
||||||
|
- `sink.text_formatter.template` currently supports fixed tokens: `{timestamp}`, `{timestamp_ms}`, `{level}`, `{target}`, `{message}`, and `{fields}`.
|
||||||
- Config-driven sink assembly currently supports `console`, `json_console`, `text_console`, and `file`.
|
- Config-driven sink assembly currently supports `console`, `json_console`, `text_console`, and `file`.
|
||||||
- `queue` remains a synchronous bounded wrapper around the final sink, not an async runtime.
|
- `queue` remains a synchronous bounded wrapper around the final sink, not an async runtime.
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ version 0.3.0
|
|||||||
- feat: support config keys `min_level`, `target`, `timestamp`, `sink.kind`, `sink.path`, `sink.append`, `sink.auto_flush`, `sink.text_formatter`, and `queue`
|
- feat: support config keys `min_level`, `target`, `timestamp`, `sink.kind`, `sink.path`, `sink.append`, `sink.auto_flush`, `sink.text_formatter`, and `queue`
|
||||||
- feat: support config-driven sink assembly for `console`, `json_console`, `text_console`, and `file`
|
- feat: support config-driven sink assembly for `console`, `json_console`, `text_console`, and `file`
|
||||||
- feat: use `maria/json_parser` as the first external dependency for practical config loading
|
- feat: use `maria/json_parser` as the first external dependency for practical config loading
|
||||||
|
- feat: add `TextFormatter.template` and `TextFormatterConfig.template` for template-driven text rendering
|
||||||
|
- feat: support formatter tokens `{timestamp}`, `{timestamp_ms}`, `{level}`, `{target}`, `{message}`, and `{fields}` in both runtime and JSON config paths
|
||||||
|
|
||||||
### Test
|
### Test
|
||||||
|
|
||||||
@@ -34,6 +36,7 @@ version 0.3.0
|
|||||||
- test: cover config-built queued text logger flushing and pending count behavior
|
- test: cover config-built queued text logger flushing and pending count behavior
|
||||||
- test: cover partial drain behavior for config-built queued logger
|
- test: cover partial drain behavior for config-built queued logger
|
||||||
- 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: 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
|
||||||
|
|
||||||
@@ -44,11 +47,13 @@ version 0.3.0
|
|||||||
- docs: add `examples/async_basic` to show async worker startup and queue-backed logging flow
|
- docs: add `examples/async_basic` to show async worker startup and queue-backed logging flow
|
||||||
- docs: update `examples/async_basic` to use unified JSON-driven async logger config
|
- docs: update `examples/async_basic` to use unified JSON-driven async logger config
|
||||||
- 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 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
|
||||||
|
|
||||||
### Notes
|
### Notes
|
||||||
|
|
||||||
- current config scope is still intentionally constrained to stable built-in sink shapes
|
- current config scope is still intentionally constrained to stable built-in sink shapes
|
||||||
|
- formatter templates are intentionally limited to simple token replacement and do not yet include conditional blocks or alignment/padding controls
|
||||||
- queue wrapping remains synchronous drain-based delivery, not async runtime scheduling
|
- queue wrapping remains synchronous drain-based delivery, not async runtime scheduling
|
||||||
- async logging remains an isolated adapter layer and currently targets `native/llvm` only
|
- async logging remains an isolated adapter layer and currently targets `native/llvm` only
|
||||||
|
|||||||
@@ -35,7 +35,13 @@ fn main {
|
|||||||
callback_logger.info("callback sink ready")
|
callback_logger.info("callback sink ready")
|
||||||
|
|
||||||
let pretty_logger = @lib.Logger::new(
|
let pretty_logger = @lib.Logger::new(
|
||||||
@lib.text_console_sink(@lib.text_formatter(show_timestamp=false, separator=" | ")),
|
@lib.text_console_sink(
|
||||||
|
@lib.text_formatter(
|
||||||
|
show_timestamp=false,
|
||||||
|
field_separator=",",
|
||||||
|
template="[{level}] {target} {message} :: {fields}",
|
||||||
|
),
|
||||||
|
),
|
||||||
min_level=@lib.Level::Info,
|
min_level=@lib.Level::Info,
|
||||||
target="pretty",
|
target="pretty",
|
||||||
)
|
)
|
||||||
@@ -102,7 +108,7 @@ fn main {
|
|||||||
ignore(queued_logger.sink.flush())
|
ignore(queued_logger.sink.flush())
|
||||||
|
|
||||||
let config_logger = @lib.parse_and_build_logger(
|
let config_logger = @lib.parse_and_build_logger(
|
||||||
"{\"min_level\":\"debug\",\"target\":\"config.demo\",\"timestamp\":true,\"sink\":{\"kind\":\"text_console\",\"text_formatter\":{\"separator\":\" | \",\"show_timestamp\":false}},\"queue\":{\"max_pending\":2,\"overflow\":\"DropOldest\"}}",
|
"{\"min_level\":\"debug\",\"target\":\"config.demo\",\"timestamp\":true,\"sink\":{\"kind\":\"text_console\",\"text_formatter\":{\"show_timestamp\":false,\"field_separator\":\",\",\"template\":\"[{level}] {target} {message} :: {fields}\"}},\"queue\":{\"max_pending\":2,\"overflow\":\"DropOldest\"}}",
|
||||||
) catch {
|
) catch {
|
||||||
err => {
|
err => {
|
||||||
ignore(err)
|
ignore(err)
|
||||||
|
|||||||
Reference in New Issue
Block a user