Add scoped target and field style markup

This commit is contained in:
Nanaloveyuki
2026-05-10 15:17:13 +08:00
parent 4be861acce
commit e78183d267
8 changed files with 136 additions and 13 deletions
+3
View File
@@ -306,6 +306,7 @@ match logger.file_runtime_state() {
- 支持字段: `min_level`, `target`, `timestamp`, `sink.kind`, `sink.path`, `sink.append`, `sink.auto_flush`, `sink.rotation`, `sink.text_formatter`, `queue` - 支持字段: `min_level`, `target`, `timestamp`, `sink.kind`, `sink.path`, `sink.append`, `sink.auto_flush`, `sink.rotation`, `sink.text_formatter`, `queue`
- `TextFormatter` / `TextFormatterConfig` 提供 `color_mode = Never | Auto | Always`, 可控制 ANSI 文本着色 - `TextFormatter` / `TextFormatterConfig` 提供 `color_mode = Never | Auto | Always`, 可控制 ANSI 文本着色
- `TextFormatter` / `TextFormatterConfig` 提供 `style_markup = disabled | builtin | full`, 可决定是否解析 style markup 以及是否启用 custom style tag - `TextFormatter` / `TextFormatterConfig` 提供 `style_markup = disabled | builtin | full`, 可决定是否解析 style markup 以及是否启用 custom style tag
- `target_style_markup``fields_style_markup` 可独立控制 `target``fields` 是否解析 style markup
- `message` 支持轻量 inline style tag: `<red>...</>`, `<b>...</>`, `<#ff0000>...</>`, `<bg:#202020>...</>` - `message` 支持轻量 inline style tag: `<red>...</>`, `<b>...</>`, `<#ff0000>...</>`, `<bg:#202020>...</>`
- 内置语义标签包括: `<accent>`, `<info>`, `<success>`, `<warning>`, `<danger>`, `<muted>` - 内置语义标签包括: `<accent>`, `<info>`, `<success>`, `<warning>`, `<danger>`, `<muted>`
- 运行期样式标签 API: `TextStyle`, `StyleTagRegistry`, `style_tag_registry()`, `default_style_tag_registry()`, `set_tag(...)`, `define_alias(...)` - 运行期样式标签 API: `TextStyle`, `StyleTagRegistry`, `style_tag_registry()`, `default_style_tag_registry()`, `set_tag(...)`, `define_alias(...)`
@@ -337,7 +338,9 @@ match logger.file_runtime_state() {
- `sink.text_formatter.template` 支持固定 token: `{timestamp}`, `{timestamp_ms}`, `{level}`, `{target}`, `{message}`, `{fields}` - `sink.text_formatter.template` 支持固定 token: `{timestamp}`, `{timestamp_ms}`, `{level}`, `{target}`, `{message}`, `{fields}`
- `sink.text_formatter.color_mode` 支持 `never`, `auto`, `always` - `sink.text_formatter.color_mode` 支持 `never`, `auto`, `always`
- `sink.text_formatter.style_markup` 支持 `disabled`, `builtin`, `full` - `sink.text_formatter.style_markup` 支持 `disabled`, `builtin`, `full`
- `sink.text_formatter.target_style_markup``sink.text_formatter.fields_style_markup` 支持 `disabled`, `builtin`, `full`
- `sink.text_formatter.style_tags.<name>` 支持 `fg`, `bg`, `bold`, `dim`, `italic`, `underline` - `sink.text_formatter.style_tags.<name>` 支持 `fg`, `bg`, `bold`, `dim`, `italic`, `underline`
- `fields_style_markup` 当前只解析 field value, 不解析 field key
- 可由配置直接组装的 sink 类型: `console`, `json_console`, `text_console`, `file` - 可由配置直接组装的 sink 类型: `console`, `json_console`, `text_console`, `file`
- `queue` 作为显式包装层附着在最终 sink 外侧. 这仍然是同步 drain 模型, 不是 async runtime - `queue` 作为显式包装层附着在最终 sink 外侧. 这仍然是同步 drain 模型, 不是 async runtime
+34 -3
View File
@@ -74,9 +74,11 @@ test "logger config parser reads formatter and queue options" {
test "logger config parser reads formatter style tags" { test "logger config parser reads formatter style tags" {
let config = parse_logger_config_text( let config = parse_logger_config_text(
"{\"sink\":{\"kind\":\"text_console\",\"text_formatter\":{\"color_mode\":\"always\",\"style_markup\":\"builtin\",\"style_tags\":{\"accent\":{\"fg\":\"#4cc9f0\",\"bold\":true},\"panel\":{\"bg\":\"#202020\",\"underline\":true}}}}}", "{\"sink\":{\"kind\":\"text_console\",\"text_formatter\":{\"color_mode\":\"always\",\"style_markup\":\"builtin\",\"target_style_markup\":\"builtin\",\"fields_style_markup\":\"disabled\",\"style_tags\":{\"accent\":{\"fg\":\"#4cc9f0\",\"bold\":true},\"panel\":{\"bg\":\"#202020\",\"underline\":true}}}}}",
) )
inspect(style_markup_mode_label(config.sink.text_formatter.style_markup), content="builtin") inspect(style_markup_mode_label(config.sink.text_formatter.style_markup), content="builtin")
inspect(style_markup_mode_label(config.sink.text_formatter.target_style_markup), content="builtin")
inspect(style_markup_mode_label(config.sink.text_formatter.fields_style_markup), content="disabled")
inspect(config.sink.text_formatter.style_tags.length(), content="2") inspect(config.sink.text_formatter.style_tags.length(), content="2")
match config.sink.text_formatter.style_tags.get("accent") { match config.sink.text_formatter.style_tags.get("accent") {
Some(style) => { Some(style) => {
@@ -150,6 +152,8 @@ test "logger config stringify roundtrips formatter style tags" {
text_formatter=TextFormatterConfig::new( text_formatter=TextFormatterConfig::new(
color_mode=ColorMode::Always, color_mode=ColorMode::Always,
style_markup=StyleMarkupMode::Builtin, style_markup=StyleMarkupMode::Builtin,
target_style_markup=StyleMarkupMode::Builtin,
fields_style_markup=StyleMarkupMode::Disabled,
style_tags={ style_tags={
"accent": text_style(fg=Some("#4cc9f0"), bold=true), "accent": text_style(fg=Some("#4cc9f0"), bold=true),
"panel": text_style(bg=Some("#202020"), dim=true), "panel": text_style(bg=Some("#202020"), dim=true),
@@ -161,6 +165,8 @@ test "logger config stringify roundtrips formatter style tags" {
let config = parse_logger_config_text(text) let config = parse_logger_config_text(text)
inspect(color_mode_label(config.sink.text_formatter.color_mode), content="always") inspect(color_mode_label(config.sink.text_formatter.color_mode), content="always")
inspect(style_markup_mode_label(config.sink.text_formatter.style_markup), content="builtin") inspect(style_markup_mode_label(config.sink.text_formatter.style_markup), content="builtin")
inspect(style_markup_mode_label(config.sink.text_formatter.target_style_markup), content="builtin")
inspect(style_markup_mode_label(config.sink.text_formatter.fields_style_markup), content="disabled")
inspect(config.sink.text_formatter.style_tags.length(), content="2") inspect(config.sink.text_formatter.style_tags.length(), content="2")
inspect(config.sink.text_formatter.style_tags.get("accent").unwrap().fg.unwrap(), content="#4cc9f0") inspect(config.sink.text_formatter.style_tags.get("accent").unwrap().fg.unwrap(), content="#4cc9f0")
inspect(config.sink.text_formatter.style_tags.get("accent").unwrap().bold, content="true") inspect(config.sink.text_formatter.style_tags.get("accent").unwrap().bold, content="true")
@@ -208,12 +214,14 @@ test "config subtype json helpers stringify stable shapes" {
template="[{level}] {message} :: {fields}", template="[{level}] {message} :: {fields}",
color_mode=ColorMode::Always, color_mode=ColorMode::Always,
style_markup=StyleMarkupMode::Builtin, style_markup=StyleMarkupMode::Builtin,
target_style_markup=StyleMarkupMode::Builtin,
fields_style_markup=StyleMarkupMode::Disabled,
style_tags={ style_tags={
"accent": text_style(fg=Some("#4cc9f0"), bold=true), "accent": text_style(fg=Some("#4cc9f0"), bold=true),
}, },
), ),
), ),
content="{\"show_timestamp\":false,\"show_level\":true,\"show_target\":false,\"show_fields\":true,\"separator\":\" | \",\"field_separator\":\",\",\"template\":\"[{level}] {message} :: {fields}\",\"color_mode\":\"always\",\"style_markup\":\"builtin\",\"style_tags\":{\"accent\":{\"bold\":true,\"dim\":false,\"italic\":false,\"underline\":false,\"fg\":\"#4cc9f0\"}}}", content="{\"show_timestamp\":false,\"show_level\":true,\"show_target\":false,\"show_fields\":true,\"separator\":\" | \",\"field_separator\":\",\",\"template\":\"[{level}] {message} :: {fields}\",\"color_mode\":\"always\",\"style_markup\":\"builtin\",\"target_style_markup\":\"builtin\",\"fields_style_markup\":\"disabled\",\"style_tags\":{\"accent\":{\"bold\":true,\"dim\":false,\"italic\":false,\"underline\":false,\"fg\":\"#4cc9f0\"}}}",
) )
inspect( inspect(
stringify_sink_config( stringify_sink_config(
@@ -226,7 +234,7 @@ test "config subtype json helpers stringify stable shapes" {
text_formatter=TextFormatterConfig::new(show_timestamp=false, color_mode=ColorMode::Auto), text_formatter=TextFormatterConfig::new(show_timestamp=false, color_mode=ColorMode::Auto),
), ),
), ),
content="{\"kind\":\"file\",\"path\":\"demo.log\",\"append\":false,\"auto_flush\":false,\"text_formatter\":{\"show_timestamp\":false,\"show_level\":true,\"show_target\":true,\"show_fields\":true,\"separator\":\" \",\"field_separator\":\" \",\"template\":\"\",\"color_mode\":\"auto\",\"style_markup\":\"full\"},\"rotation\":{\"max_bytes\":128,\"max_backups\":2}}", content="{\"kind\":\"file\",\"path\":\"demo.log\",\"append\":false,\"auto_flush\":false,\"text_formatter\":{\"show_timestamp\":false,\"show_level\":true,\"show_target\":true,\"show_fields\":true,\"separator\":\" \",\"field_separator\":\" \",\"template\":\"\",\"color_mode\":\"auto\",\"style_markup\":\"full\",\"target_style_markup\":\"disabled\",\"fields_style_markup\":\"disabled\"},\"rotation\":{\"max_bytes\":128,\"max_backups\":2}}",
) )
} }
@@ -280,6 +288,29 @@ test "config disabled style markup keeps raw tags" {
inspect(rendered, content="<accent>raw</> <red>tag</>") inspect(rendered, content="<accent>raw</> <red>tag</>")
} }
test "config target and fields markup modes render separately" {
let formatter = TextFormatterConfig::new(
show_level=false,
color_mode=ColorMode::Always,
style_markup=StyleMarkupMode::Disabled,
target_style_markup=StyleMarkupMode::Builtin,
fields_style_markup=StyleMarkupMode::Builtin,
)
let rendered = format_text(
Record::new(
Level::Info,
"<danger>message stays raw</>",
target="<danger>svc</>",
fields=[field("status", "<success>ok</>")],
),
formatter=formatter.to_formatter(),
)
inspect(
rendered,
content="[\u{001b}[34m\u{001b}[31;1msvc\u{001b}[0m\u{001b}[0m] <danger>message stays raw</> \u{001b}[35mstatus=\u{001b}[32;1mok\u{001b}[0m\u{001b}[0m",
)
}
test "build logger from config supports queued text console" { test "build logger from config supports queued text console" {
let logger = build_logger( let logger = build_logger(
LoggerConfig::new( LoggerConfig::new(
+41
View File
@@ -259,6 +259,47 @@ test "builtin semantic style tags can still be overridden" {
) )
} }
test "text formatter can enable markup for target separately" {
let formatter = text_formatter(
show_level=false,
color_mode=ColorMode::Always,
target_style_markup=StyleMarkupMode::Builtin,
)
let rec = record(Level::Info, "hello", target="<danger>svc</>")
inspect(
format_text(rec, formatter=formatter),
content="[\u{001b}[34m\u{001b}[31;1msvc\u{001b}[0m\u{001b}[0m] hello",
)
}
test "text formatter can enable markup for field values separately" {
let formatter = text_formatter(
show_level=false,
show_target=false,
color_mode=ColorMode::Always,
fields_style_markup=StyleMarkupMode::Builtin,
)
let rec = record(Level::Info, "hello", fields=[field("status", "<success>ok</>")])
inspect(
format_text(rec, formatter=formatter),
content="hello \u{001b}[35mstatus=\u{001b}[32;1mok\u{001b}[0m\u{001b}[0m",
)
}
test "text formatter leaves field keys raw when field markup is enabled" {
let formatter = text_formatter(
show_level=false,
show_target=false,
color_mode=ColorMode::Always,
fields_style_markup=StyleMarkupMode::Builtin,
)
let rec = record(Level::Info, "hello", fields=[field("<danger>status</>", "ok")])
inspect(
format_text(rec, formatter=formatter),
content="hello \u{001b}[35m<danger>status</>=ok\u{001b}[0m",
)
}
test "text formatter template respects disabled fields" { test "text formatter template respects disabled fields" {
let rec = record(Level::Warn, "just message", target="svc") let rec = record(Level::Warn, "just message", target="svc")
let formatter = text_formatter( let formatter = text_formatter(
+2
View File
@@ -252,11 +252,13 @@ test {
- supported tokens / 支持的 token: `{timestamp}`, `{timestamp_ms}`, `{level}`, `{target}`, `{message}`, `{fields}` - supported tokens / 支持的 token: `{timestamp}`, `{timestamp_ms}`, `{level}`, `{target}`, `{message}`, `{fields}`
- `color_mode` / `color_mode`: `never`, `auto`, `always` - `color_mode` / `color_mode`: `never`, `auto`, `always`
- `style_markup` / `style_markup`: `disabled`, `builtin`, `full` - `style_markup` / `style_markup`: `disabled`, `builtin`, `full`
- `target_style_markup` / `target_style_markup`, `fields_style_markup` / `fields_style_markup`: `disabled`, `builtin`, `full`
- inline style tags / inline 样式标签: `<red>...</>`, `<b>...</>`, `<#ff0000>...</>`, `<bg:#202020>...</>` - inline style tags / inline 样式标签: `<red>...</>`, `<b>...</>`, `<#ff0000>...</>`, `<bg:#202020>...</>`
- builtin semantic tags / 内置语义标签: `<accent>`, `<info>`, `<success>`, `<warning>`, `<danger>`, `<muted>` - builtin semantic tags / 内置语义标签: `<accent>`, `<info>`, `<success>`, `<warning>`, `<danger>`, `<muted>`
- runtime style tags / 运行期样式标签: `TextStyle`, `StyleTagRegistry`, `style_tag_registry()`, `default_style_tag_registry()`, `set_tag(...)`, `define_alias(...)` - runtime style tags / 运行期样式标签: `TextStyle`, `StyleTagRegistry`, `style_tag_registry()`, `default_style_tag_registry()`, `set_tag(...)`, `define_alias(...)`
- style tag priority / 标签优先级: formatter local `style_tags` > global style tag registry > builtin tags - style tag priority / 标签优先级: formatter local `style_tags` > global style tag registry > builtin tags
- `sink.text_formatter.style_tags` / `sink.text_formatter.style_tags` 现支持最小对象映射: `fg`, `bg`, `bold`, `dim`, `italic`, `underline` - `sink.text_formatter.style_tags` / `sink.text_formatter.style_tags` 现支持最小对象映射: `fg`, `bg`, `bold`, `dim`, `italic`, `underline`
- `fields_style_markup` currently affects values only / `fields_style_markup` 当前仅影响 field value, 不影响 field key
- `define_alias(...)` is still runtime-only / `define_alias(...)` 目前仍为运行期 API - `define_alias(...)` is still runtime-only / `define_alias(...)` 目前仍为运行期 API
- disabled or missing parts render as empty text / 被关闭或缺失的部分会渲染为空字符串 - disabled or missing parts render as empty text / 被关闭或缺失的部分会渲染为空字符串
- `template` is intentionally a simple token replacement layer, not a full DSL / `template` 使用轻量 token 替换方式, 不是完整 DSL - `template` is intentionally a simple token replacement layer, not a full DSL / `template` 使用轻量 token 替换方式, 不是完整 DSL
+12
View File
@@ -19,6 +19,8 @@ pub struct TextFormatterConfig {
template : String template : String
color_mode : ColorMode color_mode : ColorMode
style_markup : StyleMarkupMode style_markup : StyleMarkupMode
target_style_markup : StyleMarkupMode
fields_style_markup : StyleMarkupMode
style_tags : Map[String, TextStyle] style_tags : Map[String, TextStyle]
} }
@@ -32,6 +34,8 @@ pub fn TextFormatterConfig::new(
template~ : String = "", template~ : String = "",
color_mode~ : ColorMode = ColorMode::Never, color_mode~ : ColorMode = ColorMode::Never,
style_markup~ : StyleMarkupMode = StyleMarkupMode::Full, style_markup~ : StyleMarkupMode = StyleMarkupMode::Full,
target_style_markup~ : StyleMarkupMode = StyleMarkupMode::Disabled,
fields_style_markup~ : StyleMarkupMode = StyleMarkupMode::Disabled,
style_tags~ : Map[String, TextStyle] = {}, style_tags~ : Map[String, TextStyle] = {},
) -> TextFormatterConfig { ) -> TextFormatterConfig {
{ {
@@ -44,6 +48,8 @@ pub fn TextFormatterConfig::new(
template, template,
color_mode, color_mode,
style_markup, style_markup,
target_style_markup,
fields_style_markup,
style_tags, style_tags,
} }
} }
@@ -67,6 +73,8 @@ pub fn TextFormatterConfig::to_formatter(self : TextFormatterConfig) -> TextForm
template=self.template, template=self.template,
color_mode=self.color_mode, color_mode=self.color_mode,
style_markup=self.style_markup, style_markup=self.style_markup,
target_style_markup=self.target_style_markup,
fields_style_markup=self.fields_style_markup,
style_tags=if self.style_tags.length() == 0 { style_tags=if self.style_tags.length() == 0 {
None None
} else { } else {
@@ -891,6 +899,8 @@ fn parse_text_formatter_config(value : @json_parser.JsonValue) -> TextFormatterC
template=get_string(obj, "template", default=""), template=get_string(obj, "template", default=""),
color_mode=parse_color_mode(get_string(obj, "color_mode", default="never")), color_mode=parse_color_mode(get_string(obj, "color_mode", default="never")),
style_markup=parse_style_markup_mode(get_string(obj, "style_markup", default="full")), style_markup=parse_style_markup_mode(get_string(obj, "style_markup", default="full")),
target_style_markup=parse_style_markup_mode(get_string(obj, "target_style_markup", default="disabled")),
fields_style_markup=parse_style_markup_mode(get_string(obj, "fields_style_markup", default="disabled")),
style_tags=match obj.get("style_tags") { style_tags=match obj.get("style_tags") {
None => {} None => {}
Some(inner) => parse_style_tags_config(inner) Some(inner) => parse_style_tags_config(inner)
@@ -1015,6 +1025,8 @@ pub fn text_formatter_config_to_json(config : TextFormatterConfig) -> @json_pars
"template": @json_parser.JsonValue::String(config.template), "template": @json_parser.JsonValue::String(config.template),
"color_mode": @json_parser.JsonValue::String(color_mode_label(config.color_mode)), "color_mode": @json_parser.JsonValue::String(color_mode_label(config.color_mode)),
"style_markup": @json_parser.JsonValue::String(style_markup_mode_label(config.style_markup)), "style_markup": @json_parser.JsonValue::String(style_markup_mode_label(config.style_markup)),
"target_style_markup": @json_parser.JsonValue::String(style_markup_mode_label(config.target_style_markup)),
"fields_style_markup": @json_parser.JsonValue::String(style_markup_mode_label(config.fields_style_markup)),
} }
if config.style_tags.length() != 0 { if config.style_tags.length() != 0 {
obj["style_tags"] = style_tags_config_to_json(config.style_tags) obj["style_tags"] = style_tags_config_to_json(config.style_tags)
+38 -10
View File
@@ -178,6 +178,8 @@ pub struct TextFormatter {
template : String template : String
color_mode : ColorMode color_mode : ColorMode
style_markup : StyleMarkupMode style_markup : StyleMarkupMode
target_style_markup : StyleMarkupMode
fields_style_markup : StyleMarkupMode
style_tags : StyleTagRegistry? style_tags : StyleTagRegistry?
} }
@@ -191,6 +193,8 @@ pub fn text_formatter(
template~ : String = "", template~ : String = "",
color_mode~ : ColorMode = ColorMode::Never, color_mode~ : ColorMode = ColorMode::Never,
style_markup~ : StyleMarkupMode = StyleMarkupMode::Full, style_markup~ : StyleMarkupMode = StyleMarkupMode::Full,
target_style_markup~ : StyleMarkupMode = StyleMarkupMode::Disabled,
fields_style_markup~ : StyleMarkupMode = StyleMarkupMode::Disabled,
style_tags~ : StyleTagRegistry? = None, style_tags~ : StyleTagRegistry? = None,
) -> TextFormatter { ) -> TextFormatter {
{ {
@@ -203,6 +207,8 @@ pub fn text_formatter(
template, template,
color_mode, color_mode,
style_markup, style_markup,
target_style_markup,
fields_style_markup,
style_tags, style_tags,
} }
} }
@@ -223,6 +229,20 @@ pub fn TextFormatter::without_style_markup(self : TextFormatter) -> TextFormatte
{ ..self, style_markup: StyleMarkupMode::Disabled } { ..self, style_markup: StyleMarkupMode::Disabled }
} }
pub fn TextFormatter::with_target_style_markup(
self : TextFormatter,
style_markup : StyleMarkupMode,
) -> TextFormatter {
{ ..self, target_style_markup: style_markup }
}
pub fn TextFormatter::with_fields_style_markup(
self : TextFormatter,
style_markup : StyleMarkupMode,
) -> TextFormatter {
{ ..self, fields_style_markup: style_markup }
}
pub fn TextFormatter::with_style_tags(self : TextFormatter, style_tags : StyleTagRegistry) -> TextFormatter { pub fn TextFormatter::with_style_tags(self : TextFormatter, style_tags : StyleTagRegistry) -> TextFormatter {
{ ..self, style_tags: Some(style_tags) } { ..self, style_tags: Some(style_tags) }
} }
@@ -562,13 +582,14 @@ fn parse_inline_markup(input : String, formatter : TextFormatter) -> Array[Style
segments segments
} }
fn render_inline_markup(message : String, formatter : TextFormatter) -> String { fn render_styled_text(text : String, formatter : TextFormatter, mode : StyleMarkupMode) -> String {
match formatter.style_markup { match mode {
StyleMarkupMode::Disabled => return message StyleMarkupMode::Disabled => return text
_ => () _ => ()
} }
let enabled = use_ansi_color(formatter.color_mode) let enabled = use_ansi_color(formatter.color_mode)
let segments = parse_inline_markup(message, formatter) let scoped = { ..formatter, style_markup: mode }
let segments = parse_inline_markup(text, scoped)
let out = StringBuilder::new() let out = StringBuilder::new()
for segment in segments { for segment in segments {
out.write_string(ansi_wrap_with_style(segment.text, segment.style, enabled)) out.write_string(ansi_wrap_with_style(segment.text, segment.style, enabled))
@@ -576,6 +597,10 @@ fn render_inline_markup(message : String, formatter : TextFormatter) -> String {
out.to_string() out.to_string()
} }
fn render_inline_markup(message : String, formatter : TextFormatter) -> String {
render_styled_text(message, formatter, formatter.style_markup)
}
fn level_ansi_code(level : Level) -> String { fn level_ansi_code(level : Level) -> String {
match level { match level {
Level::Trace => "90" Level::Trace => "90"
@@ -594,10 +619,6 @@ fn fields_to_json(fields : Array[Field]) -> Json {
Json::object(obj) Json::object(obj)
} }
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 { fn timestamp_text(rec : Record, formatter : TextFormatter) -> String {
if formatter.show_timestamp && rec.timestamp_ms != 0UL { if formatter.show_timestamp && rec.timestamp_ms != 0UL {
ansi_wrap(rec.timestamp_ms.to_string(), "90", use_ansi_color(formatter.color_mode)) ansi_wrap(rec.timestamp_ms.to_string(), "90", use_ansi_color(formatter.color_mode))
@@ -616,15 +637,22 @@ fn level_text(rec : Record, formatter : TextFormatter) -> String {
fn target_text(rec : Record, formatter : TextFormatter) -> String { fn target_text(rec : Record, formatter : TextFormatter) -> String {
if formatter.show_target && rec.target != "" { if formatter.show_target && rec.target != "" {
ansi_wrap(rec.target, "34", use_ansi_color(formatter.color_mode)) let rendered = render_styled_text(rec.target, formatter, formatter.target_style_markup)
ansi_wrap(rendered, "34", use_ansi_color(formatter.color_mode))
} else { } else {
"" ""
} }
} }
fn format_field_text(field : Field, formatter : TextFormatter) -> String {
let value = render_styled_text(field.value, formatter, formatter.fields_style_markup)
"\{field.key}=\{value}"
}
fn fields_text(rec : Record, formatter : TextFormatter) -> String { fn fields_text(rec : Record, formatter : TextFormatter) -> String {
if formatter.show_fields && rec.fields.length() != 0 { if formatter.show_fields && rec.fields.length() != 0 {
ansi_wrap(format_fields(rec.fields, formatter.field_separator), "35", use_ansi_color(formatter.color_mode)) let content = rec.fields.map(fn(field) { format_field_text(field, formatter) }).join(formatter.field_separator)
ansi_wrap(content, "35", use_ansi_color(formatter.color_mode))
} else { } else {
"" ""
} }
+3
View File
@@ -282,6 +282,7 @@ match logger.file_runtime_state() {
- Supported keys include `min_level`, `target`, `timestamp`, `sink.kind`, `sink.path`, `sink.append`, `sink.auto_flush`, `sink.rotation`, `sink.text_formatter`, and `queue`. - Supported keys include `min_level`, `target`, `timestamp`, `sink.kind`, `sink.path`, `sink.append`, `sink.auto_flush`, `sink.rotation`, `sink.text_formatter`, and `queue`.
- `TextFormatter` and `TextFormatterConfig` now include `color_mode = Never | Auto | Always` for ANSI text coloring control. - `TextFormatter` and `TextFormatterConfig` now include `color_mode = Never | Auto | Always` for ANSI text coloring control.
- `TextFormatter` and `TextFormatterConfig` also include `style_markup = disabled | builtin | full` so callers can choose whether style markup is parsed and whether custom tags are active. - `TextFormatter` and `TextFormatterConfig` also include `style_markup = disabled | builtin | full` so callers can choose whether style markup is parsed and whether custom tags are active.
- `target_style_markup` and `fields_style_markup` independently control whether `target` and `fields` are parsed for style markup.
- `message` also supports lightweight inline style tags such as `<red>...</>`, `<b>...</>`, `<#ff0000>...</>`, and `<bg:#202020>...</>`. - `message` also supports lightweight inline style tags such as `<red>...</>`, `<b>...</>`, `<#ff0000>...</>`, and `<bg:#202020>...</>`.
- Builtin semantic tags now include `<accent>`, `<info>`, `<success>`, `<warning>`, `<danger>`, and `<muted>`. - Builtin semantic tags now include `<accent>`, `<info>`, `<success>`, `<warning>`, `<danger>`, and `<muted>`.
- Runtime style-tag APIs now include `TextStyle`, `StyleTagRegistry`, `style_tag_registry()`, `default_style_tag_registry()`, `set_tag(...)`, and `define_alias(...)`. - Runtime style-tag APIs now include `TextStyle`, `StyleTagRegistry`, `style_tag_registry()`, `default_style_tag_registry()`, `set_tag(...)`, and `define_alias(...)`.
@@ -313,7 +314,9 @@ match logger.file_runtime_state() {
- `sink.text_formatter.template` currently supports fixed tokens: `{timestamp}`, `{timestamp_ms}`, `{level}`, `{target}`, `{message}`, and `{fields}`. - `sink.text_formatter.template` currently supports fixed tokens: `{timestamp}`, `{timestamp_ms}`, `{level}`, `{target}`, `{message}`, and `{fields}`.
- `sink.text_formatter.color_mode` currently supports `never`, `auto`, and `always`. - `sink.text_formatter.color_mode` currently supports `never`, `auto`, and `always`.
- `sink.text_formatter.style_markup` currently supports `disabled`, `builtin`, and `full`. - `sink.text_formatter.style_markup` currently supports `disabled`, `builtin`, and `full`.
- `sink.text_formatter.target_style_markup` and `sink.text_formatter.fields_style_markup` currently support `disabled`, `builtin`, and `full`.
- `sink.text_formatter.style_tags.<name>` currently supports `fg`, `bg`, `bold`, `dim`, `italic`, and `underline`. - `sink.text_formatter.style_tags.<name>` currently supports `fg`, `bg`, `bold`, `dim`, `italic`, and `underline`.
- `fields_style_markup` currently applies to field values only, not field keys.
- 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.
+3
View File
@@ -17,6 +17,7 @@ version 0.4.0
- feat: add `StyleMarkupMode = Disabled | Builtin | Full` plus formatter helpers so callers can explicitly disable style parsing or allow builtin-only parsing - feat: add `StyleMarkupMode = Disabled | Builtin | Full` plus formatter helpers so callers can explicitly disable style parsing or allow builtin-only parsing
- feat: support `sink.text_formatter.style_markup` in JSON config parsing and serialization - feat: support `sink.text_formatter.style_markup` in JSON config parsing and serialization
- feat: add builtin semantic style tags such as `accent`, `info`, `success`, `warning`, `danger`, and `muted` - feat: add builtin semantic style tags such as `accent`, `info`, `success`, `warning`, `danger`, and `muted`
- feat: add independent `target_style_markup` and `fields_style_markup` controls for `TextFormatter` and `TextFormatterConfig`
### Test ### Test
@@ -29,6 +30,7 @@ version 0.4.0
- test: cover formatter `style_tags` JSON parsing, config roundtrip, JSON helper export, and config-driven styled formatter rendering - test: cover formatter `style_tags` JSON parsing, config roundtrip, JSON helper export, and config-driven styled formatter rendering
- test: cover disabled markup mode, builtin-only mode, and config-driven `style_markup` behavior - test: cover disabled markup mode, builtin-only mode, and config-driven `style_markup` behavior
- test: cover builtin semantic tag rendering and confirm user overrides still take precedence - test: cover builtin semantic tag rendering and confirm user overrides still take precedence
- test: cover target and field markup rendering, plus config roundtrip for the new formatter markup scopes
### Example ### Example
@@ -44,3 +46,4 @@ version 0.4.0
- formatter-local tag lookup currently takes precedence over global tag lookup, and global lookup takes precedence over builtin tags - formatter-local tag lookup currently takes precedence over global tag lookup, and global lookup takes precedence over builtin tags
- JSON config currently supports concrete style objects only; alias-style declarations remain runtime-only - JSON config currently supports concrete style objects only; alias-style declarations remain runtime-only
- users can now decide whether custom style parsing is enabled through runtime formatter APIs or `sink.text_formatter.style_markup` - users can now decide whether custom style parsing is enabled through runtime formatter APIs or `sink.text_formatter.style_markup`
- `fields_style_markup` currently styles field values only and intentionally leaves field keys raw