mirror of
https://github.com/Nanaloveyuki/BitLogger.git
synced 2026-05-30 15:42:25 +00:00
✨ Add ANSI color mode
This commit is contained in:
@@ -50,7 +50,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,\"template\":\"[{level}] {message}\"}},\"queue\":{\"max_pending\":32,\"overflow\":\"DropOldest\"}}",
|
||||
"{\"sink\":{\"kind\":\"text_console\",\"text_formatter\":{\"separator\":\" | \",\"show_timestamp\":false,\"template\":\"[{level}] {message}\",\"color_mode\":\"always\"}},\"queue\":{\"max_pending\":32,\"overflow\":\"DropOldest\"}}",
|
||||
)
|
||||
inspect(match config.sink.kind {
|
||||
SinkKind::TextConsole => "TextConsole"
|
||||
@@ -59,6 +59,7 @@ test "logger config parser reads formatter and queue options" {
|
||||
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}")
|
||||
inspect(color_mode_label(config.sink.text_formatter.color_mode), content="always")
|
||||
match config.queue {
|
||||
Some(queue) => {
|
||||
inspect(queue.max_pending, content="32")
|
||||
@@ -156,9 +157,10 @@ test "config subtype json helpers stringify stable shapes" {
|
||||
separator=" | ",
|
||||
field_separator=",",
|
||||
template="[{level}] {message} :: {fields}",
|
||||
color_mode=ColorMode::Always,
|
||||
),
|
||||
),
|
||||
content="{\"show_timestamp\":false,\"show_level\":true,\"show_target\":false,\"show_fields\":true,\"separator\":\" | \",\"field_separator\":\",\",\"template\":\"[{level}] {message} :: {fields}\"}",
|
||||
content="{\"show_timestamp\":false,\"show_level\":true,\"show_target\":false,\"show_fields\":true,\"separator\":\" | \",\"field_separator\":\",\",\"template\":\"[{level}] {message} :: {fields}\",\"color_mode\":\"always\"}",
|
||||
)
|
||||
inspect(
|
||||
stringify_sink_config(
|
||||
@@ -168,10 +170,10 @@ test "config subtype json helpers stringify stable shapes" {
|
||||
append=false,
|
||||
auto_flush=false,
|
||||
rotation=Some(file_rotation(128, max_backups=2)),
|
||||
text_formatter=TextFormatterConfig::new(show_timestamp=false),
|
||||
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\":\"\"},\"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\"},\"rotation\":{\"max_bytes\":128,\"max_backups\":2}}",
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -60,6 +60,29 @@ test "text formatter supports template rendering" {
|
||||
)
|
||||
}
|
||||
|
||||
test "text formatter can render ansi level colors" {
|
||||
let rec = record(Level::Error, "boom", target="svc")
|
||||
let formatter = text_formatter(color_mode=ColorMode::Always)
|
||||
inspect(
|
||||
format_text(rec, formatter=formatter),
|
||||
content="[\u{001b}[31;1mERROR\u{001b}[0m] [\u{001b}[34msvc\u{001b}[0m] boom",
|
||||
)
|
||||
}
|
||||
|
||||
test "text formatter auto color respects NO_COLOR" {
|
||||
let rec = record(Level::Warn, "boom", target="svc")
|
||||
let previous = @env.get_env_var("NO_COLOR")
|
||||
@env.set_env_var("NO_COLOR", "1")
|
||||
inspect(
|
||||
format_text(rec, formatter=text_formatter(color_mode=ColorMode::Auto)),
|
||||
content="[WARN] [svc] boom",
|
||||
)
|
||||
match previous {
|
||||
Some(value) => @env.set_env_var("NO_COLOR", value)
|
||||
None => @env.unset_env_var("NO_COLOR")
|
||||
}
|
||||
}
|
||||
|
||||
test "text formatter template respects disabled fields" {
|
||||
let rec = record(Level::Warn, "just message", target="svc")
|
||||
let formatter = text_formatter(
|
||||
|
||||
@@ -44,6 +44,8 @@ BitLogger 是一个使用 MoonBit 编写的结构化日志库.
|
||||
- 支持 `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 配置读写
|
||||
- `TextFormatter` / `TextFormatterConfig` now support `color_mode = Never | Auto | Always`
|
||||
- `TextFormatter` / `TextFormatterConfig` 现支持 `color_mode = Never | Auto | Always`
|
||||
- `QueueConfig` / `TextFormatterConfig` / `SinkConfig` can also be exported independently through dedicated JSON helpers
|
||||
- `QueueConfig` / `TextFormatterConfig` / `SinkConfig` 也可分别通过专用 JSON helper 单独导出
|
||||
- config-driven logger assembly via `build_logger(...)`
|
||||
@@ -180,6 +182,7 @@ test {
|
||||
show_timestamp=false,
|
||||
field_separator=",",
|
||||
template="[{level}] {target} {message} :: {fields}",
|
||||
color_mode=ColorMode::Always,
|
||||
)
|
||||
let logger = Logger::new(text_console_sink(formatter), target="pretty")
|
||||
logger.info("hello", fields=[field("mode", "pretty")])
|
||||
@@ -189,7 +192,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\":{\"show_timestamp\":false,\"field_separator\":\",\",\"template\":\"[{level}] {target} {message} :: {fields}\"}},\"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}\",\"color_mode\":\"always\"}},\"queue\":{\"max_pending\":2,\"overflow\":\"DropOldest\"}}",
|
||||
)
|
||||
let logger = build_logger(config)
|
||||
logger.info("configured from json")
|
||||
@@ -200,6 +203,7 @@ test {
|
||||
## Formatter Template / 模板格式
|
||||
|
||||
- supported tokens / 支持的 token: `{timestamp}`, `{timestamp_ms}`, `{level}`, `{target}`, `{message}`, `{fields}`
|
||||
- `color_mode` / `color_mode`: `never`, `auto`, `always`
|
||||
- disabled or missing parts render as empty text / 被关闭或缺失的部分会渲染为空字符串
|
||||
- `template` is intentionally a simple token replacement layer, not a full DSL / `template` 使用轻量 token 替换方式, 不是完整 DSL
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ pub struct TextFormatterConfig {
|
||||
separator : String
|
||||
field_separator : String
|
||||
template : String
|
||||
color_mode : ColorMode
|
||||
}
|
||||
|
||||
pub fn TextFormatterConfig::new(
|
||||
@@ -27,6 +28,7 @@ pub fn TextFormatterConfig::new(
|
||||
separator~ : String = " ",
|
||||
field_separator~ : String = " ",
|
||||
template~ : String = "",
|
||||
color_mode~ : ColorMode = ColorMode::Never,
|
||||
) -> TextFormatterConfig {
|
||||
{
|
||||
show_timestamp,
|
||||
@@ -36,6 +38,7 @@ pub fn TextFormatterConfig::new(
|
||||
separator,
|
||||
field_separator,
|
||||
template,
|
||||
color_mode,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,6 +51,7 @@ pub fn TextFormatterConfig::to_formatter(self : TextFormatterConfig) -> TextForm
|
||||
separator=self.separator,
|
||||
field_separator=self.field_separator,
|
||||
template=self.template,
|
||||
color_mode=self.color_mode,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -815,6 +819,15 @@ fn parse_sink_kind(name : String) -> SinkKind raise ConfigError {
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_color_mode(name : String) -> ColorMode raise ConfigError {
|
||||
match name.to_upper() {
|
||||
"NEVER" => ColorMode::Never
|
||||
"AUTO" => ColorMode::Auto
|
||||
"ALWAYS" => ColorMode::Always
|
||||
_ => raise ConfigError::InvalidConfig("Unsupported color mode: " + name)
|
||||
}
|
||||
}
|
||||
|
||||
fn sink_kind_label(kind : SinkKind) -> String {
|
||||
match kind {
|
||||
SinkKind::Console => "console"
|
||||
@@ -834,6 +847,7 @@ fn parse_text_formatter_config(value : @json_parser.JsonValue) -> TextFormatterC
|
||||
separator=get_string(obj, "separator", default=" "),
|
||||
field_separator=get_string(obj, "field_separator", default=" "),
|
||||
template=get_string(obj, "template", default=""),
|
||||
color_mode=parse_color_mode(get_string(obj, "color_mode", default="never")),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -928,6 +942,7 @@ pub fn text_formatter_config_to_json(config : TextFormatterConfig) -> @json_pars
|
||||
"separator": @json_parser.JsonValue::String(config.separator),
|
||||
"field_separator": @json_parser.JsonValue::String(config.field_separator),
|
||||
"template": @json_parser.JsonValue::String(config.template),
|
||||
"color_mode": @json_parser.JsonValue::String(color_mode_label(config.color_mode)),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
+54
-8
@@ -1,5 +1,11 @@
|
||||
pub type RecordFormatter = (Record) -> String
|
||||
|
||||
pub(all) enum ColorMode {
|
||||
Never
|
||||
Auto
|
||||
Always
|
||||
}
|
||||
|
||||
pub struct TextFormatter {
|
||||
show_timestamp : Bool
|
||||
show_level : Bool
|
||||
@@ -8,6 +14,7 @@ pub struct TextFormatter {
|
||||
separator : String
|
||||
field_separator : String
|
||||
template : String
|
||||
color_mode : ColorMode
|
||||
}
|
||||
|
||||
pub fn text_formatter(
|
||||
@@ -18,6 +25,7 @@ pub fn text_formatter(
|
||||
separator~ : String = " ",
|
||||
field_separator~ : String = " ",
|
||||
template~ : String = "",
|
||||
color_mode~ : ColorMode = ColorMode::Never,
|
||||
) -> TextFormatter {
|
||||
{
|
||||
show_timestamp,
|
||||
@@ -27,6 +35,44 @@ pub fn text_formatter(
|
||||
separator,
|
||||
field_separator,
|
||||
template,
|
||||
color_mode,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn color_mode_label(mode : ColorMode) -> String {
|
||||
match mode {
|
||||
ColorMode::Never => "never"
|
||||
ColorMode::Auto => "auto"
|
||||
ColorMode::Always => "always"
|
||||
}
|
||||
}
|
||||
|
||||
fn use_ansi_color(mode : ColorMode) -> Bool {
|
||||
match mode {
|
||||
ColorMode::Never => false
|
||||
ColorMode::Always => true
|
||||
ColorMode::Auto => match @env.get_env_var("NO_COLOR") {
|
||||
Some(_) => false
|
||||
None => true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ansi_wrap(text : String, code : String, enabled : Bool) -> String {
|
||||
if !enabled || text == "" {
|
||||
text
|
||||
} else {
|
||||
"\u{001b}[\{code}m\{text}\u{001b}[0m"
|
||||
}
|
||||
}
|
||||
|
||||
fn level_ansi_code(level : Level) -> String {
|
||||
match level {
|
||||
Level::Trace => "90"
|
||||
Level::Debug => "36"
|
||||
Level::Info => "32"
|
||||
Level::Warn => "33"
|
||||
Level::Error => "31;1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +90,7 @@ fn format_fields(fields : Array[Field], separator : String) -> String {
|
||||
|
||||
fn timestamp_text(rec : Record, formatter : TextFormatter) -> String {
|
||||
if formatter.show_timestamp && rec.timestamp_ms != 0UL {
|
||||
rec.timestamp_ms.to_string()
|
||||
ansi_wrap(rec.timestamp_ms.to_string(), "90", use_ansi_color(formatter.color_mode))
|
||||
} else {
|
||||
""
|
||||
}
|
||||
@@ -52,7 +98,7 @@ fn timestamp_text(rec : Record, formatter : TextFormatter) -> String {
|
||||
|
||||
fn level_text(rec : Record, formatter : TextFormatter) -> String {
|
||||
if formatter.show_level {
|
||||
rec.level.label()
|
||||
ansi_wrap(rec.level.label(), level_ansi_code(rec.level), use_ansi_color(formatter.color_mode))
|
||||
} else {
|
||||
""
|
||||
}
|
||||
@@ -60,7 +106,7 @@ fn level_text(rec : Record, formatter : TextFormatter) -> String {
|
||||
|
||||
fn target_text(rec : Record, formatter : TextFormatter) -> String {
|
||||
if formatter.show_target && rec.target != "" {
|
||||
rec.target
|
||||
ansi_wrap(rec.target, "34", use_ansi_color(formatter.color_mode))
|
||||
} else {
|
||||
""
|
||||
}
|
||||
@@ -68,7 +114,7 @@ fn target_text(rec : Record, formatter : TextFormatter) -> String {
|
||||
|
||||
fn fields_text(rec : Record, formatter : TextFormatter) -> String {
|
||||
if formatter.show_fields && rec.fields.length() != 0 {
|
||||
format_fields(rec.fields, formatter.field_separator)
|
||||
ansi_wrap(format_fields(rec.fields, formatter.field_separator), "35", use_ansi_color(formatter.color_mode))
|
||||
} else {
|
||||
""
|
||||
}
|
||||
@@ -92,20 +138,20 @@ pub fn format_text(rec : Record, formatter~ : TextFormatter = text_formatter())
|
||||
}
|
||||
let parts : Array[String] = []
|
||||
if formatter.show_timestamp && rec.timestamp_ms != 0UL {
|
||||
parts.push("[\{rec.timestamp_ms.to_string()}]")
|
||||
parts.push("[\{timestamp_text(rec, formatter)}]")
|
||||
}
|
||||
if formatter.show_level {
|
||||
parts.push("[\{rec.level.label()}]")
|
||||
parts.push("[\{level_text(rec, formatter)}]")
|
||||
}
|
||||
if formatter.show_target && rec.target != "" {
|
||||
parts.push("[\{rec.target}]")
|
||||
parts.push("[\{target_text(rec, formatter)}]")
|
||||
}
|
||||
parts.push(rec.message)
|
||||
let base = parts.join(formatter.separator)
|
||||
if !formatter.show_fields || rec.fields.length() == 0 {
|
||||
base
|
||||
} else {
|
||||
let details = format_fields(rec.fields, formatter.field_separator)
|
||||
let details = fields_text(rec, formatter)
|
||||
"\{base}\{formatter.separator}\{details}"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user