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:
@@ -41,7 +41,7 @@ test "logger config parser reads core options" {
|
||||
|
||||
test "logger config parser reads formatter and queue options" {
|
||||
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 {
|
||||
SinkKind::TextConsole => "TextConsole"
|
||||
@@ -49,6 +49,7 @@ test "logger config parser reads formatter and queue options" {
|
||||
}, content="TextConsole")
|
||||
inspect(config.sink.text_formatter.separator, content=" | ")
|
||||
inspect(config.sink.text_formatter.show_timestamp, content="false")
|
||||
inspect(config.sink.text_formatter.template, content="[{level}] {message}")
|
||||
match config.queue {
|
||||
Some(queue) => {
|
||||
inspect(queue.max_pending, content="32")
|
||||
@@ -76,6 +77,7 @@ test "logger config stringify roundtrips stable fields" {
|
||||
show_fields=true,
|
||||
separator=" | ",
|
||||
field_separator=",",
|
||||
template="[{level}] {target} {message}",
|
||||
),
|
||||
),
|
||||
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.timestamp, content="true")
|
||||
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" {
|
||||
|
||||
@@ -41,6 +41,35 @@ test "text formatter can emit message only" {
|
||||
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" {
|
||||
let rendered : Ref[String] = Ref::new("")
|
||||
let sink = text_callback_sink(
|
||||
|
||||
+14
-4
@@ -36,8 +36,8 @@ BitLogger 是一个基于 MoonBit 的结构化日志库。
|
||||
- 支持 `with_patch(...)`、`patch_sink(...)` 以及常见 record patch helper
|
||||
- explicit queued delivery via `queued_sink(...)` and `with_queue(...)`
|
||||
- 支持 `queued_sink(...)`、`with_queue(...)`、有界积压与溢出策略
|
||||
- configurable text formatting via `text_formatter(...)`, `format_text(...)`, and `text_console_sink(...)`
|
||||
- 支持 `text_formatter(...)`、`format_text(...)`、`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(...)` 以及模板化 `template` 文本输出
|
||||
- JSON config parsing via `parse_logger_config_text(...)` and `stringify_logger_config(...)`
|
||||
- 支持 `parse_logger_config_text(...)`、`stringify_logger_config(...)` 进行最小 JSON 配置读写
|
||||
- config-driven logger assembly via `build_logger(...)`
|
||||
@@ -144,7 +144,11 @@ test {
|
||||
|
||||
```mbt check
|
||||
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")
|
||||
logger.info("hello", fields=[field("mode", "pretty")])
|
||||
}
|
||||
@@ -153,7 +157,7 @@ test {
|
||||
```mbt check
|
||||
test {
|
||||
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)
|
||||
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
|
||||
test {
|
||||
if native_files_supported() {
|
||||
|
||||
@@ -16,6 +16,7 @@ pub struct TextFormatterConfig {
|
||||
show_fields : Bool
|
||||
separator : String
|
||||
field_separator : String
|
||||
template : String
|
||||
}
|
||||
|
||||
pub fn TextFormatterConfig::new(
|
||||
@@ -25,6 +26,7 @@ pub fn TextFormatterConfig::new(
|
||||
show_fields~ : Bool = true,
|
||||
separator~ : String = " ",
|
||||
field_separator~ : String = " ",
|
||||
template~ : String = "",
|
||||
) -> TextFormatterConfig {
|
||||
{
|
||||
show_timestamp,
|
||||
@@ -33,6 +35,7 @@ pub fn TextFormatterConfig::new(
|
||||
show_fields,
|
||||
separator,
|
||||
field_separator,
|
||||
template,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +47,7 @@ pub fn TextFormatterConfig::to_formatter(self : TextFormatterConfig) -> TextForm
|
||||
show_fields=self.show_fields,
|
||||
separator=self.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),
|
||||
separator=get_string(obj, "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),
|
||||
"separator": @json_parser.JsonValue::String(config.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
|
||||
separator : String
|
||||
field_separator : String
|
||||
template : String
|
||||
}
|
||||
|
||||
pub fn text_formatter(
|
||||
@@ -16,8 +17,17 @@ pub fn text_formatter(
|
||||
show_fields~ : Bool = true,
|
||||
separator~ : String = " ",
|
||||
field_separator~ : String = " ",
|
||||
template~ : String = "",
|
||||
) -> 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 {
|
||||
@@ -32,7 +42,54 @@ fn format_fields(fields : Array[Field], separator : String) -> String {
|
||||
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 {
|
||||
if formatter.template != "" {
|
||||
return render_template(rec, formatter)
|
||||
}
|
||||
let parts : Array[String] = []
|
||||
if formatter.show_timestamp && rec.timestamp_ms != 0UL {
|
||||
parts.push("[\{rec.timestamp_ms.to_string()}]")
|
||||
|
||||
Reference in New Issue
Block a user