mirror of
https://github.com/Nanaloveyuki/BitLogger.git
synced 2026-05-31 16:12:20 +00:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a328414087 | |||
| f609b02377 | |||
| 3124d7a445 | |||
| 4b54005401 | |||
| 2d2388c79f | |||
| e78183d267 | |||
| 4be861acce | |||
| 20f79bbe2a | |||
| f3e903b578 | |||
| f1b223f203 | |||
| 3b6536f980 | |||
| b1b2235160 | |||
| 90af009e93 |
@@ -43,6 +43,18 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
moon check bitlogger_async --target native
|
moon check bitlogger_async --target native
|
||||||
|
|
||||||
|
- name: Check bitlogger_async wasm-gc
|
||||||
|
run: |
|
||||||
|
moon check bitlogger_async --target wasm-gc
|
||||||
|
|
||||||
|
- name: Check bitlogger_async js
|
||||||
|
run: |
|
||||||
|
moon check bitlogger_async --target js
|
||||||
|
|
||||||
|
- name: Test bitlogger_async wasm-gc
|
||||||
|
run: |
|
||||||
|
moon test bitlogger_async --target wasm-gc
|
||||||
|
|
||||||
- name: Run basic example
|
- name: Run basic example
|
||||||
run: |
|
run: |
|
||||||
moon run examples/basic
|
moon run examples/basic
|
||||||
|
|||||||
@@ -14,6 +14,15 @@
|
|||||||
|
|
||||||
BitLogger 是一个使用 MoonBit 编写的结构化日志库
|
BitLogger 是一个使用 MoonBit 编写的结构化日志库
|
||||||
|
|
||||||
|
## 🧭 后端兼容
|
||||||
|
|
||||||
|
| 模块 / 能力 | native / llvm | js / wasm / wasm-gc |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `bitlogger` 主包 | 支持 | 支持 |
|
||||||
|
| `file_sink(...)` | 支持 | 不支持, `native_files_supported()` 返回 `false` |
|
||||||
|
| `bitlogger_async` | 支持原生 worker 语义 | 支持兼容实现 |
|
||||||
|
| `examples/async_basic` | 支持 | 受 `async fn main` 入口限制, 当前不提供 |
|
||||||
|
|
||||||
## ❇️ 特点
|
## ❇️ 特点
|
||||||
|
|
||||||
- 🧩 基础能力: 支持 level, formatter, sink, context field 和全局 logger.
|
- 🧩 基础能力: 支持 level, formatter, sink, context field 和全局 logger.
|
||||||
@@ -26,6 +35,7 @@ BitLogger 是一个使用 MoonBit 编写的结构化日志库
|
|||||||
- 🧷 可绑定上下文: 支持 `bind(...)` 与 `fields(...)`, 便于复用上下文字段.
|
- 🧷 可绑定上下文: 支持 `bind(...)` 与 `fields(...)`, 便于复用上下文字段.
|
||||||
- 📮 显式队列: 支持 `queued_sink(...)` / `with_queue(...)`, 支持有界积压和溢出策略.
|
- 📮 显式队列: 支持 `queued_sink(...)` / `with_queue(...)`, 支持有界积压和溢出策略.
|
||||||
- 🧾 可配置文本格式: 支持 `text_formatter(...)`, `format_text(...)`, `text_console_sink(...)`, `formatted_callback_sink(...)` 和模板化 `template` 输出.
|
- 🧾 可配置文本格式: 支持 `text_formatter(...)`, `format_text(...)`, `text_console_sink(...)`, `formatted_callback_sink(...)` 和模板化 `template` 输出.
|
||||||
|
- 🎨 轻量样式标签: 支持 `color_mode`, inline markup, `TextStyle`, `StyleTagRegistry`, 自定义标签与内置标签覆盖.
|
||||||
- 💾 Native 文件输出: 支持 `file_sink(...)`, 基础 size rotation / backup retention, 显式 `reopen()` / `reopen_with_current_policy()` / `reopen_append()` / `reopen_truncate()` 与失败计数, 仅在 `native/llvm` backend 可用.
|
- 💾 Native 文件输出: 支持 `file_sink(...)`, 基础 size rotation / backup retention, 显式 `reopen()` / `reopen_with_current_policy()` / `reopen_append()` / `reopen_truncate()` 与失败计数, 仅在 `native/llvm` backend 可用.
|
||||||
- 📦 MoonBit 适配: API 和工程结构与 MoonBit 的 package / visibility / toolchain 模型保持一致.
|
- 📦 MoonBit 适配: API 和工程结构与 MoonBit 的 package / visibility / toolchain 模型保持一致.
|
||||||
|
|
||||||
@@ -168,6 +178,7 @@ let formatter = text_formatter(
|
|||||||
show_timestamp=false,
|
show_timestamp=false,
|
||||||
field_separator=",",
|
field_separator=",",
|
||||||
template="[{level}] {target} {message} :: {fields}",
|
template="[{level}] {target} {message} :: {fields}",
|
||||||
|
color_mode=ColorMode::Always,
|
||||||
)
|
)
|
||||||
let logger = Logger::new(text_console_sink(formatter), target="pretty")
|
let logger = Logger::new(text_console_sink(formatter), target="pretty")
|
||||||
|
|
||||||
@@ -176,11 +187,44 @@ logger.info("hello", fields=[field("mode", "pretty")])
|
|||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
<details><summary>inline style tag 示例</summary>
|
||||||
|
|
||||||
|
```moonbit
|
||||||
|
let formatter = text_formatter(
|
||||||
|
show_timestamp=false,
|
||||||
|
color_mode=ColorMode::Always,
|
||||||
|
).with_style_tags(
|
||||||
|
default_style_tag_registry()
|
||||||
|
.set_tag("accent", fg=Some("#4cc9f0"), bold=true)
|
||||||
|
.define_alias("danger", "red"),
|
||||||
|
)
|
||||||
|
|
||||||
|
let logger = Logger::new(text_console_sink(formatter), target="styled")
|
||||||
|
|
||||||
|
logger.info("<accent>styled</> output and <danger>alert</>")
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details><summary>关闭 style markup 解析示例</summary>
|
||||||
|
|
||||||
|
```moonbit
|
||||||
|
let formatter = text_formatter(
|
||||||
|
color_mode=ColorMode::Always,
|
||||||
|
).without_style_markup()
|
||||||
|
|
||||||
|
let logger = Logger::new(text_console_sink(formatter), target="raw")
|
||||||
|
|
||||||
|
logger.info("<red>kept as raw text</>")
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
<details><summary>JSON 配置加载示例</summary>
|
<details><summary>JSON 配置加载示例</summary>
|
||||||
|
|
||||||
```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\":{\"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)
|
let logger = build_logger(config)
|
||||||
@@ -191,6 +235,34 @@ ignore(logger.flush())
|
|||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
<details><summary>JSON style_tags 示例</summary>
|
||||||
|
|
||||||
|
```moonbit
|
||||||
|
let config = parse_logger_config_text(
|
||||||
|
"{\"sink\":{\"kind\":\"text_console\",\"text_formatter\":{\"show_timestamp\":false,\"color_mode\":\"always\",\"style_tags\":{\"accent\":{\"fg\":\"#4cc9f0\",\"bold\":true}}}}}",
|
||||||
|
)
|
||||||
|
|
||||||
|
let logger = build_logger(config)
|
||||||
|
|
||||||
|
logger.info("<accent>styled from json</>")
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details><summary>JSON style_markup 模式示例</summary>
|
||||||
|
|
||||||
|
```moonbit
|
||||||
|
let config = parse_logger_config_text(
|
||||||
|
"{\"sink\":{\"kind\":\"text_console\",\"text_formatter\":{\"color_mode\":\"always\",\"style_markup\":\"disabled\"}}}",
|
||||||
|
)
|
||||||
|
|
||||||
|
let logger = build_logger(config)
|
||||||
|
|
||||||
|
logger.info("<red>still raw</>")
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
<details><summary>native 文件 sink 示例</summary>
|
<details><summary>native 文件 sink 示例</summary>
|
||||||
|
|
||||||
```moonbit
|
```moonbit
|
||||||
@@ -241,6 +313,17 @@ match logger.file_runtime_state() {
|
|||||||
- 提供 JSON 配置层: `parse_logger_config_text(...)`, `stringify_logger_config(...)`, `build_logger(...)`
|
- 提供 JSON 配置层: `parse_logger_config_text(...)`, `stringify_logger_config(...)`, `build_logger(...)`
|
||||||
- `QueueConfig`, `TextFormatterConfig`, `SinkConfig` 可分别通过 `queue_config_to_json(...)` / `stringify_queue_config(...)`, `text_formatter_config_to_json(...)` / `stringify_text_formatter_config(...)`, `sink_config_to_json(...)` / `stringify_sink_config(...)` 单独导出 JSON
|
- `QueueConfig`, `TextFormatterConfig`, `SinkConfig` 可分别通过 `queue_config_to_json(...)` / `stringify_queue_config(...)`, `text_formatter_config_to_json(...)` / `stringify_text_formatter_config(...)`, `sink_config_to_json(...)` / `stringify_sink_config(...)` 单独导出 JSON
|
||||||
- 支持字段: `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_support = basic | truecolor`, 可控制 hex / RGB 样式是否降级到基础 ANSI 色
|
||||||
|
- `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>...</>`
|
||||||
|
- 闭合同时支持简写 `</>` 与具名闭合 `</red>`, `</danger>`, `</b>`
|
||||||
|
- 内置语义标签包括: `<accent>`, `<info>`, `<success>`, `<warning>`, `<danger>`, `<muted>`
|
||||||
|
- 运行期样式标签 API: `TextStyle`, `StyleTagRegistry`, `style_tag_registry()`, `default_style_tag_registry()`, `set_tag(...)`, `define_alias(...)`
|
||||||
|
- 样式标签优先级: formatter 局部 `style_tags` > 全局 style tag registry > 内置标签
|
||||||
|
- `sink.text_formatter.style_tags` 现支持最小对象映射配置, 可声明 `fg`, `bg`, `bold`, `dim`, `italic`, `underline`
|
||||||
|
- `define_alias(...)` 仍为运行期 API, 当前不在 JSON schema 中
|
||||||
- `sink.rotation` 支持 `max_bytes` 与 `max_backups`, 用于基础 size-based rotation 和 backup retention
|
- `sink.rotation` 支持 `max_bytes` 与 `max_backups`, 用于基础 size-based rotation 和 backup retention
|
||||||
- `file_sink(...)` 提供 `reopen()`, `reopen_with_current_policy()`, `reopen_append()`, `reopen_truncate()`, `open_failures()`, `write_failures()`, `flush_failures()`, `rotation_failures()`, 用于基础可观测性
|
- `file_sink(...)` 提供 `reopen()`, `reopen_with_current_policy()`, `reopen_append()`, `reopen_truncate()`, `open_failures()`, `write_failures()`, `flush_failures()`, `rotation_failures()`, 用于基础可观测性
|
||||||
- `file_sink(...)` 提供 `append_mode()`. 显式传入 `reopen(append=...)` 时, 会更新后续 reopen 使用的 append 策略. `reopen_with_current_policy()` 使用当前保存的策略重开文件, `reopen_append()` / `reopen_truncate()` 提供常见的 append 和 truncate 模式
|
- `file_sink(...)` 提供 `append_mode()`. 显式传入 `reopen(append=...)` 时, 会更新后续 reopen 使用的 append 策略. `reopen_with_current_policy()` 使用当前保存的策略重开文件, `reopen_append()` / `reopen_truncate()` 提供常见的 append 和 truncate 模式
|
||||||
@@ -264,6 +347,12 @@ match logger.file_runtime_state() {
|
|||||||
- `file_sink_policy_to_json(...)`, `stringify_file_sink_policy(...)` 可将独立 file policy 直接导出为 JSON, 便于策略快照, 配置对比或诊断上报
|
- `file_sink_policy_to_json(...)`, `stringify_file_sink_policy(...)` 可将独立 file policy 直接导出为 JSON, 便于策略快照, 配置对比或诊断上报
|
||||||
- `file_sink_state_to_json(...)`, `stringify_file_sink_state(...)`, `runtime_file_state_to_json(...)`, `stringify_runtime_file_state(...)` 可直接把 file / queued-file 快照导出为 JSON, 便于排障或上报
|
- `file_sink_state_to_json(...)`, `stringify_file_sink_state(...)`, `runtime_file_state_to_json(...)`, `stringify_runtime_file_state(...)` 可直接把 file / queued-file 快照导出为 JSON, 便于排障或上报
|
||||||
- `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_support` 支持 `basic`, `truecolor`
|
||||||
|
- `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`
|
||||||
|
- `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
|
||||||
|
|
||||||
@@ -275,8 +364,17 @@ match logger.file_runtime_state() {
|
|||||||
- 建议使用 `shutdown()` 停止 worker. 默认模式下, 它会先等待队列清空, 再关闭 queue, 最后等待 worker 退出
|
- 建议使用 `shutdown()` 停止 worker. 默认模式下, 它会先等待队列清空, 再关闭 queue, 最后等待 worker 退出
|
||||||
- 提供基础生命周期观测: `is_closed()`, `is_running()`, `has_failed()`, `last_error()`
|
- 提供基础生命周期观测: `is_closed()`, `is_running()`, `has_failed()`, `last_error()`
|
||||||
- async worker 支持 `max_batch` 批量消费, 以及 `flush=Never|Batch|Shutdown` 的基础 flush 策略
|
- async worker 支持 `max_batch` 批量消费, 以及 `flush=Never|Batch|Shutdown` 的基础 flush 策略
|
||||||
|
- 提供 `async_runtime_mode()` / `async_runtime_mode_label(...)` / `async_runtime_supports_background_worker()` 用于探测当前后端是原生 worker 还是兼容实现
|
||||||
|
- 提供 `async_runtime_state_to_json(...)` / `stringify_async_runtime_state(...)`, 便于在启动日志或诊断输出里直接暴露当前 async runtime 模式
|
||||||
- 示例见 [examples/async_basic/main.mbt](/E:/repo/MooLiteyukiBot/examples/async_basic/main.mbt:1)
|
- 示例见 [examples/async_basic/main.mbt](/E:/repo/MooLiteyukiBot/examples/async_basic/main.mbt:1)
|
||||||
- 仅支持 `native/llvm` backend, 与同步 core 分开维护
|
- `bitlogger_async` 现在支持多端编译: `native/llvm` 保留后台 worker 语义, `js` / `wasm` / `wasm-gc` 提供兼容实现
|
||||||
|
- 由于 `moonbitlang/async` 的 `async fn main` 入口当前仍有限制, `examples/async_basic` 示例仍保持 `native` target
|
||||||
|
|
||||||
|
启动时诊断示例:
|
||||||
|
|
||||||
|
```moonbit
|
||||||
|
println(stringify_async_runtime_state(async_runtime_state(), pretty=true))
|
||||||
|
```
|
||||||
|
|
||||||
### Async Config
|
### Async Config
|
||||||
|
|
||||||
|
|||||||
@@ -50,7 +50,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,\"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 {
|
inspect(match config.sink.kind {
|
||||||
SinkKind::TextConsole => "TextConsole"
|
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.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}")
|
inspect(config.sink.text_formatter.template, content="[{level}] {message}")
|
||||||
|
inspect(color_mode_label(config.sink.text_formatter.color_mode), content="always")
|
||||||
match config.queue {
|
match config.queue {
|
||||||
Some(queue) => {
|
Some(queue) => {
|
||||||
inspect(queue.max_pending, content="32")
|
inspect(queue.max_pending, content="32")
|
||||||
@@ -71,6 +72,32 @@ test "logger config parser reads formatter and queue options" {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "logger config parser reads formatter style tags" {
|
||||||
|
let config = parse_logger_config_text(
|
||||||
|
"{\"sink\":{\"kind\":\"text_console\",\"text_formatter\":{\"color_mode\":\"always\",\"color_support\":\"basic\",\"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(color_support_label(config.sink.text_formatter.color_support), content="basic")
|
||||||
|
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")
|
||||||
|
match config.sink.text_formatter.style_tags.get("accent") {
|
||||||
|
Some(style) => {
|
||||||
|
inspect(style.fg.unwrap(), content="#4cc9f0")
|
||||||
|
inspect(style.bold, content="true")
|
||||||
|
inspect(style.bg is None, content="true")
|
||||||
|
}
|
||||||
|
None => inspect(false, content="true")
|
||||||
|
}
|
||||||
|
match config.sink.text_formatter.style_tags.get("panel") {
|
||||||
|
Some(style) => {
|
||||||
|
inspect(style.bg.unwrap(), content="#202020")
|
||||||
|
inspect(style.underline, content="true")
|
||||||
|
}
|
||||||
|
None => inspect(false, content="true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
test "logger config parser reads file rotation options" {
|
test "logger config parser reads file rotation options" {
|
||||||
let config = parse_logger_config_text(
|
let config = parse_logger_config_text(
|
||||||
"{\"sink\":{\"kind\":\"file\",\"path\":\"bitlogger.log\",\"rotation\":{\"max_bytes\":128,\"max_backups\":3}}}",
|
"{\"sink\":{\"kind\":\"file\",\"path\":\"bitlogger.log\",\"rotation\":{\"max_bytes\":128,\"max_backups\":3}}}",
|
||||||
@@ -118,6 +145,38 @@ test "logger config stringify roundtrips stable fields" {
|
|||||||
inspect(config.sink.text_formatter.template, content="[{level}] {target} {message}")
|
inspect(config.sink.text_formatter.template, content="[{level}] {target} {message}")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "logger config stringify roundtrips formatter style tags" {
|
||||||
|
let text = stringify_logger_config(
|
||||||
|
LoggerConfig::new(
|
||||||
|
sink=SinkConfig::new(
|
||||||
|
kind=SinkKind::TextConsole,
|
||||||
|
text_formatter=TextFormatterConfig::new(
|
||||||
|
color_mode=ColorMode::Always,
|
||||||
|
color_support=ColorSupport::Basic,
|
||||||
|
style_markup=StyleMarkupMode::Builtin,
|
||||||
|
target_style_markup=StyleMarkupMode::Builtin,
|
||||||
|
fields_style_markup=StyleMarkupMode::Disabled,
|
||||||
|
style_tags={
|
||||||
|
"accent": text_style(fg=Some("#4cc9f0"), bold=true),
|
||||||
|
"panel": text_style(bg=Some("#202020"), dim=true),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
let config = parse_logger_config_text(text)
|
||||||
|
inspect(color_mode_label(config.sink.text_formatter.color_mode), content="always")
|
||||||
|
inspect(color_support_label(config.sink.text_formatter.color_support), content="basic")
|
||||||
|
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.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("panel").unwrap().bg.unwrap(), content="#202020")
|
||||||
|
inspect(config.sink.text_formatter.style_tags.get("panel").unwrap().dim, content="true")
|
||||||
|
}
|
||||||
|
|
||||||
test "logger config stringify roundtrips file rotation fields" {
|
test "logger config stringify roundtrips file rotation fields" {
|
||||||
let text = stringify_logger_config(
|
let text = stringify_logger_config(
|
||||||
LoggerConfig::new(
|
LoggerConfig::new(
|
||||||
@@ -156,9 +215,17 @@ test "config subtype json helpers stringify stable shapes" {
|
|||||||
separator=" | ",
|
separator=" | ",
|
||||||
field_separator=",",
|
field_separator=",",
|
||||||
template="[{level}] {message} :: {fields}",
|
template="[{level}] {message} :: {fields}",
|
||||||
|
color_mode=ColorMode::Always,
|
||||||
|
color_support=ColorSupport::Basic,
|
||||||
|
style_markup=StyleMarkupMode::Builtin,
|
||||||
|
target_style_markup=StyleMarkupMode::Builtin,
|
||||||
|
fields_style_markup=StyleMarkupMode::Disabled,
|
||||||
|
style_tags={
|
||||||
|
"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}\"}",
|
content="{\"show_timestamp\":false,\"show_level\":true,\"show_target\":false,\"show_fields\":true,\"separator\":\" | \",\"field_separator\":\",\",\"template\":\"[{level}] {message} :: {fields}\",\"color_mode\":\"always\",\"color_support\":\"basic\",\"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(
|
||||||
@@ -168,10 +235,97 @@ test "config subtype json helpers stringify stable shapes" {
|
|||||||
append=false,
|
append=false,
|
||||||
auto_flush=false,
|
auto_flush=false,
|
||||||
rotation=Some(file_rotation(128, max_backups=2)),
|
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\",\"color_support\":\"truecolor\",\"style_markup\":\"full\",\"target_style_markup\":\"disabled\",\"fields_style_markup\":\"disabled\"},\"rotation\":{\"max_bytes\":128,\"max_backups\":2}}",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
test "config basic color support downgrades hex colors" {
|
||||||
|
let formatter = TextFormatterConfig::new(
|
||||||
|
show_level=false,
|
||||||
|
show_target=false,
|
||||||
|
color_mode=ColorMode::Always,
|
||||||
|
color_support=ColorSupport::Basic,
|
||||||
|
)
|
||||||
|
let rendered = format_text(
|
||||||
|
Record::new(Level::Info, "<#ff0000>hot</> <bg:#010203>bg</>"),
|
||||||
|
formatter=formatter.to_formatter(),
|
||||||
|
)
|
||||||
|
inspect(rendered, content="\u{001b}[31mhot\u{001b}[0m \u{001b}[100mbg\u{001b}[0m")
|
||||||
|
}
|
||||||
|
|
||||||
|
test "config formatter style tags render in built logger" {
|
||||||
|
let formatter = TextFormatterConfig::new(
|
||||||
|
show_timestamp=false,
|
||||||
|
show_target=false,
|
||||||
|
color_mode=ColorMode::Always,
|
||||||
|
style_tags={
|
||||||
|
"accent": text_style(fg=Some("#4cc9f0"), bold=true),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
let rendered = format_text(
|
||||||
|
Record::new(Level::Info, "<accent>tag</>"),
|
||||||
|
formatter=formatter.to_formatter(),
|
||||||
|
)
|
||||||
|
inspect(rendered, content="[\u{001b}[32mINFO\u{001b}[0m] \u{001b}[38;2;76;201;240;1mtag\u{001b}[0m")
|
||||||
|
}
|
||||||
|
|
||||||
|
test "config builtin style markup ignores custom tags" {
|
||||||
|
let formatter = TextFormatterConfig::new(
|
||||||
|
show_level=false,
|
||||||
|
show_target=false,
|
||||||
|
color_mode=ColorMode::Always,
|
||||||
|
style_markup=StyleMarkupMode::Builtin,
|
||||||
|
style_tags={
|
||||||
|
"brand": text_style(fg=Some("#4cc9f0"), bold=true),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
let rendered = format_text(
|
||||||
|
Record::new(Level::Info, "<brand>custom</> <red>builtin</>"),
|
||||||
|
formatter=formatter.to_formatter(),
|
||||||
|
)
|
||||||
|
inspect(rendered, content="<brand>custom</> \u{001b}[31mbuiltin\u{001b}[0m")
|
||||||
|
}
|
||||||
|
|
||||||
|
test "config disabled style markup keeps raw tags" {
|
||||||
|
let formatter = TextFormatterConfig::new(
|
||||||
|
show_level=false,
|
||||||
|
show_target=false,
|
||||||
|
color_mode=ColorMode::Always,
|
||||||
|
style_markup=StyleMarkupMode::Disabled,
|
||||||
|
style_tags={
|
||||||
|
"accent": text_style(fg=Some("#4cc9f0"), bold=true),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
let rendered = format_text(
|
||||||
|
Record::new(Level::Info, "<accent>raw</> <red>tag</>"),
|
||||||
|
formatter=formatter.to_formatter(),
|
||||||
|
)
|
||||||
|
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",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -60,6 +60,286 @@ 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 renders named inline color tags in ansi mode" {
|
||||||
|
let rec = record(Level::Info, "<red>boom</>", target="svc")
|
||||||
|
inspect(
|
||||||
|
format_text(rec, formatter=text_formatter(color_mode=ColorMode::Always)),
|
||||||
|
content="[\u{001b}[32mINFO\u{001b}[0m] [\u{001b}[34msvc\u{001b}[0m] \u{001b}[31mboom\u{001b}[0m",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
test "text formatter strips inline tags in plain mode" {
|
||||||
|
let rec = record(Level::Info, "<red>boom</> <b>bold</>", target="svc")
|
||||||
|
inspect(
|
||||||
|
format_text(rec, formatter=text_formatter(color_mode=ColorMode::Never)),
|
||||||
|
content="[INFO] [svc] boom bold",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
test "text formatter supports nested inline tags" {
|
||||||
|
let rec = record(Level::Info, "<red><b>fatal</></>")
|
||||||
|
inspect(
|
||||||
|
format_text(rec, formatter=text_formatter(show_level=false, show_target=false, color_mode=ColorMode::Always)),
|
||||||
|
content="\u{001b}[31;1mfatal\u{001b}[0m",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
test "text formatter supports named closing tags" {
|
||||||
|
let rec = record(Level::Info, "<red>boom</red>")
|
||||||
|
inspect(
|
||||||
|
format_text(rec, formatter=text_formatter(show_level=false, show_target=false, color_mode=ColorMode::Always)),
|
||||||
|
content="\u{001b}[31mboom\u{001b}[0m",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
test "text formatter supports mixed short and named closing tags" {
|
||||||
|
let rec = record(Level::Info, "<red><b>fatal</b></red>")
|
||||||
|
inspect(
|
||||||
|
format_text(rec, formatter=text_formatter(show_level=false, show_target=false, color_mode=ColorMode::Always)),
|
||||||
|
content="\u{001b}[31;1mfatal\u{001b}[0m",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
test "text formatter keeps unmatched named closing tags as plain text" {
|
||||||
|
let rec = record(Level::Info, "boom</red>")
|
||||||
|
inspect(
|
||||||
|
format_text(rec, formatter=text_formatter(show_level=false, show_target=false)),
|
||||||
|
content="boom</red>",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
test "text formatter supports hex inline colors" {
|
||||||
|
let rec = record(Level::Info, "<#ff0000>hot</> <bg:#010203>bg</>")
|
||||||
|
inspect(
|
||||||
|
format_text(rec, formatter=text_formatter(show_level=false, show_target=false, color_mode=ColorMode::Always)),
|
||||||
|
content="\u{001b}[38;2;255;0;0mhot\u{001b}[0m \u{001b}[48;2;1;2;3mbg\u{001b}[0m",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
test "text formatter can downgrade hex colors to basic ansi" {
|
||||||
|
let rec = record(Level::Info, "<#ff0000>hot</> <bg:#010203>bg</>")
|
||||||
|
inspect(
|
||||||
|
format_text(
|
||||||
|
rec,
|
||||||
|
formatter=text_formatter(
|
||||||
|
show_level=false,
|
||||||
|
show_target=false,
|
||||||
|
color_mode=ColorMode::Always,
|
||||||
|
color_support=ColorSupport::Basic,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
content="\u{001b}[31mhot\u{001b}[0m \u{001b}[100mbg\u{001b}[0m",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
test "text formatter keeps unknown inline tags as plain text" {
|
||||||
|
let rec = record(Level::Info, "<unknown>boom</>")
|
||||||
|
inspect(
|
||||||
|
format_text(rec, formatter=text_formatter(show_level=false, show_target=false)),
|
||||||
|
content="<unknown>boom</>",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
test "text formatter can disable style markup parsing" {
|
||||||
|
let rec = record(Level::Info, "<red>boom</> <b>bold</>", target="svc")
|
||||||
|
inspect(
|
||||||
|
format_text(
|
||||||
|
rec,
|
||||||
|
formatter=text_formatter(color_mode=ColorMode::Always).without_style_markup(),
|
||||||
|
),
|
||||||
|
content="[\u{001b}[32mINFO\u{001b}[0m] [\u{001b}[34msvc\u{001b}[0m] <red>boom</> <b>bold</>",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
test "text formatter builtin style markup mode ignores custom tags" {
|
||||||
|
let formatter = text_formatter(
|
||||||
|
show_level=false,
|
||||||
|
show_target=false,
|
||||||
|
color_mode=ColorMode::Always,
|
||||||
|
style_markup=StyleMarkupMode::Builtin,
|
||||||
|
).with_style_tags(
|
||||||
|
style_tag_registry().set_tag("brand", fg=Some("#4cc9f0"), bold=true),
|
||||||
|
)
|
||||||
|
let rec = record(Level::Info, "<brand>custom</> <red>builtin</>")
|
||||||
|
inspect(
|
||||||
|
format_text(rec, formatter=formatter),
|
||||||
|
content="<brand>custom</> \u{001b}[31mbuiltin\u{001b}[0m",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
test "style markup mode label is stable" {
|
||||||
|
inspect(style_markup_mode_label(StyleMarkupMode::Disabled), content="disabled")
|
||||||
|
inspect(style_markup_mode_label(StyleMarkupMode::Builtin), content="builtin")
|
||||||
|
inspect(style_markup_mode_label(StyleMarkupMode::Full), content="full")
|
||||||
|
}
|
||||||
|
|
||||||
|
test "text formatter supports custom style tags" {
|
||||||
|
let formatter = text_formatter(
|
||||||
|
show_level=false,
|
||||||
|
show_target=false,
|
||||||
|
color_mode=ColorMode::Always,
|
||||||
|
).with_style_tags(
|
||||||
|
style_tag_registry().set_tag("accent", fg=Some("#4cc9f0"), bold=true),
|
||||||
|
)
|
||||||
|
let rec = record(Level::Info, "<accent>api</>")
|
||||||
|
inspect(
|
||||||
|
format_text(rec, formatter=formatter),
|
||||||
|
content="\u{001b}[38;2;76;201;240;1mapi\u{001b}[0m",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
test "text formatter can override builtin style tags" {
|
||||||
|
let formatter = text_formatter(
|
||||||
|
show_level=false,
|
||||||
|
show_target=false,
|
||||||
|
color_mode=ColorMode::Always,
|
||||||
|
).with_style_tags(
|
||||||
|
default_style_tag_registry().set_tag("red", fg=Some("#ff5a5f"), underline=true),
|
||||||
|
)
|
||||||
|
let rec = record(Level::Info, "<red>alert</>")
|
||||||
|
inspect(
|
||||||
|
format_text(rec, formatter=formatter),
|
||||||
|
content="\u{001b}[38;2;255;90;95;4malert\u{001b}[0m",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
test "formatter style tags take priority over global style tags" {
|
||||||
|
let previous = global_style_tag_registry()
|
||||||
|
set_global_style_tag_registry(style_tag_registry().set_tag("accent", fg=Some("#123456")))
|
||||||
|
let formatter = text_formatter(
|
||||||
|
show_level=false,
|
||||||
|
show_target=false,
|
||||||
|
color_mode=ColorMode::Always,
|
||||||
|
).with_style_tags(
|
||||||
|
style_tag_registry().set_tag("accent", fg=Some("#abcdef")),
|
||||||
|
)
|
||||||
|
let rec = record(Level::Info, "<accent>tag</>")
|
||||||
|
inspect(
|
||||||
|
format_text(rec, formatter=formatter),
|
||||||
|
content="\u{001b}[38;2;171;205;239mtag\u{001b}[0m",
|
||||||
|
)
|
||||||
|
set_global_style_tag_registry(previous)
|
||||||
|
}
|
||||||
|
|
||||||
|
test "global style tags apply when formatter has no local registry" {
|
||||||
|
let previous = global_style_tag_registry()
|
||||||
|
set_global_style_tag_registry(style_tag_registry().set_tag("accent", fg=Some("#102030"), dim=true))
|
||||||
|
let rec = record(Level::Info, "<accent>tag</>")
|
||||||
|
inspect(
|
||||||
|
format_text(rec, formatter=text_formatter(show_level=false, show_target=false, color_mode=ColorMode::Always)),
|
||||||
|
content="\u{001b}[38;2;16;32;48;2mtag\u{001b}[0m",
|
||||||
|
)
|
||||||
|
set_global_style_tag_registry(previous)
|
||||||
|
}
|
||||||
|
|
||||||
|
test "style tag alias can reuse builtin tags" {
|
||||||
|
let formatter = text_formatter(
|
||||||
|
show_level=false,
|
||||||
|
show_target=false,
|
||||||
|
color_mode=ColorMode::Always,
|
||||||
|
).with_style_tags(
|
||||||
|
style_tag_registry().define_alias("danger", "red"),
|
||||||
|
)
|
||||||
|
let rec = record(Level::Info, "<danger>boom</>")
|
||||||
|
inspect(
|
||||||
|
format_text(rec, formatter=formatter),
|
||||||
|
content="\u{001b}[31mboom\u{001b}[0m",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
test "builtin semantic style tags are available" {
|
||||||
|
let formatter = text_formatter(
|
||||||
|
show_level=false,
|
||||||
|
show_target=false,
|
||||||
|
color_mode=ColorMode::Always,
|
||||||
|
style_markup=StyleMarkupMode::Builtin,
|
||||||
|
)
|
||||||
|
let rec = record(Level::Info, "<success>ok</> <warning>careful</> <danger>boom</> <muted>quiet</>")
|
||||||
|
inspect(
|
||||||
|
format_text(rec, formatter=formatter),
|
||||||
|
content="\u{001b}[32;1mok\u{001b}[0m \u{001b}[33;1mcareful\u{001b}[0m \u{001b}[31;1mboom\u{001b}[0m \u{001b}[90;2mquiet\u{001b}[0m",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
test "builtin semantic style tags can still be overridden" {
|
||||||
|
let formatter = text_formatter(
|
||||||
|
show_level=false,
|
||||||
|
show_target=false,
|
||||||
|
color_mode=ColorMode::Always,
|
||||||
|
).with_style_tags(
|
||||||
|
default_style_tag_registry().set_tag("success", fg=Some("#00ffaa"), underline=true),
|
||||||
|
)
|
||||||
|
let rec = record(Level::Info, "<success>ok</>")
|
||||||
|
inspect(
|
||||||
|
format_text(rec, formatter=formatter),
|
||||||
|
content="\u{001b}[38;2;0;255;170;4mok\u{001b}[0m",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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(
|
||||||
|
|||||||
+63
-1
@@ -42,8 +42,12 @@ BitLogger 是一个使用 MoonBit 编写的结构化日志库.
|
|||||||
- 支持 `queued_sink(...)`, `with_queue(...)`, 有界积压与溢出策略
|
- 支持 `queued_sink(...)`, `with_queue(...)`, 有界积压与溢出策略
|
||||||
- configurable text formatting via `text_formatter(...)`, `format_text(...)`, `text_console_sink(...)`, and template-driven `template` output
|
- configurable text formatting via `text_formatter(...)`, `format_text(...)`, `text_console_sink(...)`, and template-driven `template` output
|
||||||
- 支持 `text_formatter(...)`, `format_text(...)`, `text_console_sink(...)` 以及模板化 `template` 文本输出
|
- 支持 `text_formatter(...)`, `format_text(...)`, `text_console_sink(...)` 以及模板化 `template` 文本输出
|
||||||
|
- lightweight style tags via `color_mode`, inline markup, `TextStyle`, `StyleTagRegistry`, custom tags, and builtin-tag overrides
|
||||||
|
- 支持 `color_mode`, inline markup, `TextStyle`, `StyleTagRegistry`, 自定义标签与内置标签覆盖
|
||||||
- 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 配置读写
|
||||||
|
- `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` can also be exported independently through dedicated JSON helpers
|
||||||
- `QueueConfig` / `TextFormatterConfig` / `SinkConfig` 也可分别通过专用 JSON helper 单独导出
|
- `QueueConfig` / `TextFormatterConfig` / `SinkConfig` 也可分别通过专用 JSON helper 单独导出
|
||||||
- config-driven logger assembly via `build_logger(...)`
|
- config-driven logger assembly via `build_logger(...)`
|
||||||
@@ -180,16 +184,42 @@ test {
|
|||||||
show_timestamp=false,
|
show_timestamp=false,
|
||||||
field_separator=",",
|
field_separator=",",
|
||||||
template="[{level}] {target} {message} :: {fields}",
|
template="[{level}] {target} {message} :: {fields}",
|
||||||
|
color_mode=ColorMode::Always,
|
||||||
)
|
)
|
||||||
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")])
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
```mbt check
|
||||||
|
test {
|
||||||
|
let formatter = text_formatter(
|
||||||
|
show_timestamp=false,
|
||||||
|
color_mode=ColorMode::Always,
|
||||||
|
).with_style_tags(
|
||||||
|
default_style_tag_registry()
|
||||||
|
.set_tag("accent", fg=Some("#4cc9f0"), bold=true)
|
||||||
|
.define_alias("danger", "red"),
|
||||||
|
)
|
||||||
|
let logger = Logger::new(text_console_sink(formatter), target="styled")
|
||||||
|
logger.info("<accent>styled</> output and <danger>alert</>")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```mbt check
|
||||||
|
test {
|
||||||
|
let formatter = text_formatter(
|
||||||
|
color_mode=ColorMode::Always,
|
||||||
|
).without_style_markup()
|
||||||
|
let logger = Logger::new(text_console_sink(formatter), target="raw")
|
||||||
|
logger.info("<red>kept as raw text</>")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
```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\":{\"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)
|
let logger = build_logger(config)
|
||||||
logger.info("configured from json")
|
logger.info("configured from json")
|
||||||
@@ -197,9 +227,41 @@ test {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
```mbt check
|
||||||
|
test {
|
||||||
|
let config = parse_logger_config_text(
|
||||||
|
"{\"sink\":{\"kind\":\"text_console\",\"text_formatter\":{\"show_timestamp\":false,\"color_mode\":\"always\",\"style_tags\":{\"accent\":{\"fg\":\"#4cc9f0\",\"bold\":true}}}}}",
|
||||||
|
)
|
||||||
|
let logger = build_logger(config)
|
||||||
|
logger.info("<accent>styled from json</>")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```mbt check
|
||||||
|
test {
|
||||||
|
let config = parse_logger_config_text(
|
||||||
|
"{\"sink\":{\"kind\":\"text_console\",\"text_formatter\":{\"color_mode\":\"always\",\"style_markup\":\"disabled\"}}}",
|
||||||
|
)
|
||||||
|
let logger = build_logger(config)
|
||||||
|
logger.info("<red>still raw</>")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Formatter Template / 模板格式
|
## Formatter Template / 模板格式
|
||||||
|
|
||||||
- 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_support` / `color_support`: `basic`, `truecolor`
|
||||||
|
- `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>...</>`
|
||||||
|
- closing tags / 闭合标签: `</>` 以及具名闭合 `</red>`, `</danger>`, `</b>`
|
||||||
|
- 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(...)`
|
||||||
|
- 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`
|
||||||
|
- `fields_style_markup` currently affects values only / `fields_style_markup` 当前仅影响 field value, 不影响 field key
|
||||||
|
- `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
|
||||||
|
|
||||||
|
|||||||
+145
-2
@@ -17,6 +17,12 @@ pub struct TextFormatterConfig {
|
|||||||
separator : String
|
separator : String
|
||||||
field_separator : String
|
field_separator : String
|
||||||
template : String
|
template : String
|
||||||
|
color_mode : ColorMode
|
||||||
|
color_support : ColorSupport
|
||||||
|
style_markup : StyleMarkupMode
|
||||||
|
target_style_markup : StyleMarkupMode
|
||||||
|
fields_style_markup : StyleMarkupMode
|
||||||
|
style_tags : Map[String, TextStyle]
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn TextFormatterConfig::new(
|
pub fn TextFormatterConfig::new(
|
||||||
@@ -27,6 +33,12 @@ pub fn TextFormatterConfig::new(
|
|||||||
separator~ : String = " ",
|
separator~ : String = " ",
|
||||||
field_separator~ : String = " ",
|
field_separator~ : String = " ",
|
||||||
template~ : String = "",
|
template~ : String = "",
|
||||||
|
color_mode~ : ColorMode = ColorMode::Never,
|
||||||
|
color_support~ : ColorSupport = ColorSupport::TrueColor,
|
||||||
|
style_markup~ : StyleMarkupMode = StyleMarkupMode::Full,
|
||||||
|
target_style_markup~ : StyleMarkupMode = StyleMarkupMode::Disabled,
|
||||||
|
fields_style_markup~ : StyleMarkupMode = StyleMarkupMode::Disabled,
|
||||||
|
style_tags~ : Map[String, TextStyle] = {},
|
||||||
) -> TextFormatterConfig {
|
) -> TextFormatterConfig {
|
||||||
{
|
{
|
||||||
show_timestamp,
|
show_timestamp,
|
||||||
@@ -36,9 +48,23 @@ pub fn TextFormatterConfig::new(
|
|||||||
separator,
|
separator,
|
||||||
field_separator,
|
field_separator,
|
||||||
template,
|
template,
|
||||||
|
color_mode,
|
||||||
|
color_support,
|
||||||
|
style_markup,
|
||||||
|
target_style_markup,
|
||||||
|
fields_style_markup,
|
||||||
|
style_tags,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn style_tag_registry_from_config(style_tags : Map[String, TextStyle]) -> StyleTagRegistry {
|
||||||
|
let registry = style_tag_registry()
|
||||||
|
for name, style in style_tags {
|
||||||
|
ignore(registry.set_tag(name, style=style))
|
||||||
|
}
|
||||||
|
registry
|
||||||
|
}
|
||||||
|
|
||||||
pub fn TextFormatterConfig::to_formatter(self : TextFormatterConfig) -> TextFormatter {
|
pub fn TextFormatterConfig::to_formatter(self : TextFormatterConfig) -> TextFormatter {
|
||||||
text_formatter(
|
text_formatter(
|
||||||
show_timestamp=self.show_timestamp,
|
show_timestamp=self.show_timestamp,
|
||||||
@@ -48,6 +74,16 @@ pub fn TextFormatterConfig::to_formatter(self : TextFormatterConfig) -> TextForm
|
|||||||
separator=self.separator,
|
separator=self.separator,
|
||||||
field_separator=self.field_separator,
|
field_separator=self.field_separator,
|
||||||
template=self.template,
|
template=self.template,
|
||||||
|
color_mode=self.color_mode,
|
||||||
|
color_support=self.color_support,
|
||||||
|
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 {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(style_tag_registry_from_config(self.style_tags))
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -755,6 +791,19 @@ fn get_string(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_optional_string(
|
||||||
|
obj : Map[String, @json_parser.JsonValue],
|
||||||
|
key : String,
|
||||||
|
) -> String? raise ConfigError {
|
||||||
|
match obj.get(key) {
|
||||||
|
None => None
|
||||||
|
Some(value) => match value.as_string() {
|
||||||
|
Some(text) => Some(text)
|
||||||
|
None => raise ConfigError::InvalidConfig("Expected string at key " + key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn get_bool(
|
fn get_bool(
|
||||||
obj : Map[String, @json_parser.JsonValue],
|
obj : Map[String, @json_parser.JsonValue],
|
||||||
key : String,
|
key : String,
|
||||||
@@ -815,6 +864,32 @@ 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 parse_color_support(name : String) -> ColorSupport raise ConfigError {
|
||||||
|
match name.to_upper() {
|
||||||
|
"BASIC" => ColorSupport::Basic
|
||||||
|
"TRUECOLOR" => ColorSupport::TrueColor
|
||||||
|
_ => raise ConfigError::InvalidConfig("Unsupported color support: " + name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_style_markup_mode(name : String) -> StyleMarkupMode raise ConfigError {
|
||||||
|
match name.to_upper() {
|
||||||
|
"DISABLED" => StyleMarkupMode::Disabled
|
||||||
|
"BUILTIN" => StyleMarkupMode::Builtin
|
||||||
|
"FULL" => StyleMarkupMode::Full
|
||||||
|
_ => raise ConfigError::InvalidConfig("Unsupported style markup mode: " + name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn sink_kind_label(kind : SinkKind) -> String {
|
fn sink_kind_label(kind : SinkKind) -> String {
|
||||||
match kind {
|
match kind {
|
||||||
SinkKind::Console => "console"
|
SinkKind::Console => "console"
|
||||||
@@ -834,9 +909,42 @@ fn parse_text_formatter_config(value : @json_parser.JsonValue) -> TextFormatterC
|
|||||||
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=""),
|
template=get_string(obj, "template", default=""),
|
||||||
|
color_mode=parse_color_mode(get_string(obj, "color_mode", default="never")),
|
||||||
|
color_support=parse_color_support(get_string(obj, "color_support", default="truecolor")),
|
||||||
|
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") {
|
||||||
|
None => {}
|
||||||
|
Some(inner) => parse_style_tags_config(inner)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_text_style_config(
|
||||||
|
value : @json_parser.JsonValue,
|
||||||
|
context : String,
|
||||||
|
) -> TextStyle raise ConfigError {
|
||||||
|
let obj = expect_object(value, context)
|
||||||
|
text_style(
|
||||||
|
fg=get_optional_string(obj, "fg"),
|
||||||
|
bg=get_optional_string(obj, "bg"),
|
||||||
|
bold=get_bool(obj, "bold", default=false),
|
||||||
|
dim=get_bool(obj, "dim", default=false),
|
||||||
|
italic=get_bool(obj, "italic", default=false),
|
||||||
|
underline=get_bool(obj, "underline", default=false),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_style_tags_config(value : @json_parser.JsonValue) -> Map[String, TextStyle] raise ConfigError {
|
||||||
|
let obj = expect_object(value, "text_formatter.style_tags")
|
||||||
|
let style_tags : Map[String, TextStyle] = {}
|
||||||
|
for name, item in obj {
|
||||||
|
style_tags[name] = parse_text_style_config(item, "text_formatter.style_tags." + name)
|
||||||
|
}
|
||||||
|
style_tags
|
||||||
|
}
|
||||||
|
|
||||||
fn parse_queue_config(value : @json_parser.JsonValue) -> QueueConfig raise ConfigError {
|
fn parse_queue_config(value : @json_parser.JsonValue) -> QueueConfig raise ConfigError {
|
||||||
let obj = expect_object(value, "queue")
|
let obj = expect_object(value, "queue")
|
||||||
QueueConfig::new(
|
QueueConfig::new(
|
||||||
@@ -920,7 +1028,7 @@ pub fn stringify_queue_config(queue : QueueConfig, pretty~ : Bool = false) -> St
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn text_formatter_config_to_json(config : TextFormatterConfig) -> @json_parser.JsonValue {
|
pub fn text_formatter_config_to_json(config : TextFormatterConfig) -> @json_parser.JsonValue {
|
||||||
@json_parser.JsonValue::Object({
|
let obj : Map[String, @json_parser.JsonValue] = {
|
||||||
"show_timestamp": @json_parser.JsonValue::Bool(config.show_timestamp),
|
"show_timestamp": @json_parser.JsonValue::Bool(config.show_timestamp),
|
||||||
"show_level": @json_parser.JsonValue::Bool(config.show_level),
|
"show_level": @json_parser.JsonValue::Bool(config.show_level),
|
||||||
"show_target": @json_parser.JsonValue::Bool(config.show_target),
|
"show_target": @json_parser.JsonValue::Bool(config.show_target),
|
||||||
@@ -928,7 +1036,42 @@ pub fn text_formatter_config_to_json(config : TextFormatterConfig) -> @json_pars
|
|||||||
"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),
|
"template": @json_parser.JsonValue::String(config.template),
|
||||||
})
|
"color_mode": @json_parser.JsonValue::String(color_mode_label(config.color_mode)),
|
||||||
|
"color_support": @json_parser.JsonValue::String(color_support_label(config.color_support)),
|
||||||
|
"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 {
|
||||||
|
obj["style_tags"] = style_tags_config_to_json(config.style_tags)
|
||||||
|
}
|
||||||
|
@json_parser.JsonValue::Object(obj)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn text_style_config_to_json(style : TextStyle) -> @json_parser.JsonValue {
|
||||||
|
let obj : Map[String, @json_parser.JsonValue] = {
|
||||||
|
"bold": @json_parser.JsonValue::Bool(style.bold),
|
||||||
|
"dim": @json_parser.JsonValue::Bool(style.dim),
|
||||||
|
"italic": @json_parser.JsonValue::Bool(style.italic),
|
||||||
|
"underline": @json_parser.JsonValue::Bool(style.underline),
|
||||||
|
}
|
||||||
|
match style.fg {
|
||||||
|
Some(value) => obj["fg"] = @json_parser.JsonValue::String(value)
|
||||||
|
None => ()
|
||||||
|
}
|
||||||
|
match style.bg {
|
||||||
|
Some(value) => obj["bg"] = @json_parser.JsonValue::String(value)
|
||||||
|
None => ()
|
||||||
|
}
|
||||||
|
@json_parser.JsonValue::Object(obj)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn style_tags_config_to_json(style_tags : Map[String, TextStyle]) -> @json_parser.JsonValue {
|
||||||
|
let obj : Map[String, @json_parser.JsonValue] = {}
|
||||||
|
for name, style in style_tags {
|
||||||
|
obj[name] = text_style_config_to_json(style)
|
||||||
|
}
|
||||||
|
@json_parser.JsonValue::Object(obj)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn stringify_text_formatter_config(
|
pub fn stringify_text_formatter_config(
|
||||||
|
|||||||
+791
-14
@@ -1,5 +1,185 @@
|
|||||||
pub type RecordFormatter = (Record) -> String
|
pub type RecordFormatter = (Record) -> String
|
||||||
|
|
||||||
|
pub(all) enum ColorMode {
|
||||||
|
Never
|
||||||
|
Auto
|
||||||
|
Always
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(all) enum ColorSupport {
|
||||||
|
Basic
|
||||||
|
TrueColor
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(all) enum StyleMarkupMode {
|
||||||
|
Disabled
|
||||||
|
Builtin
|
||||||
|
Full
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TextStyle {
|
||||||
|
fg : String?
|
||||||
|
bg : String?
|
||||||
|
bold : Bool
|
||||||
|
dim : Bool
|
||||||
|
italic : Bool
|
||||||
|
underline : Bool
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn text_style(
|
||||||
|
fg~ : String? = None,
|
||||||
|
bg~ : String? = None,
|
||||||
|
bold~ : Bool = false,
|
||||||
|
dim~ : Bool = false,
|
||||||
|
italic~ : Bool = false,
|
||||||
|
underline~ : Bool = false,
|
||||||
|
) -> TextStyle {
|
||||||
|
{ fg, bg, bold, dim, italic, underline }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct StyleTagRegistry {
|
||||||
|
entries : Map[String, TextStyle]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalize_style_tag_name(name : String) -> String {
|
||||||
|
name.trim().to_lower().to_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn style_tag_registry() -> StyleTagRegistry {
|
||||||
|
{ entries: {} }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn merge_text_style(base : TextStyle, overlay : TextStyle) -> TextStyle {
|
||||||
|
{
|
||||||
|
fg: match overlay.fg {
|
||||||
|
Some(_) => overlay.fg
|
||||||
|
None => base.fg
|
||||||
|
},
|
||||||
|
bg: match overlay.bg {
|
||||||
|
Some(_) => overlay.bg
|
||||||
|
None => base.bg
|
||||||
|
},
|
||||||
|
bold: base.bold || overlay.bold,
|
||||||
|
dim: base.dim || overlay.dim,
|
||||||
|
italic: base.italic || overlay.italic,
|
||||||
|
underline: base.underline || overlay.underline,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn StyleTagRegistry::set_tag(
|
||||||
|
self : StyleTagRegistry,
|
||||||
|
name : String,
|
||||||
|
style~ : TextStyle = text_style(),
|
||||||
|
fg~ : String? = None,
|
||||||
|
bg~ : String? = None,
|
||||||
|
bold~ : Bool = false,
|
||||||
|
dim~ : Bool = false,
|
||||||
|
italic~ : Bool = false,
|
||||||
|
underline~ : Bool = false,
|
||||||
|
) -> StyleTagRegistry {
|
||||||
|
let overlay = text_style(fg=fg, bg=bg, bold=bold, dim=dim, italic=italic, underline=underline)
|
||||||
|
self.entries[normalize_style_tag_name(name)] = merge_text_style(style, overlay)
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn StyleTagRegistry::get(self : StyleTagRegistry, name : String) -> TextStyle? {
|
||||||
|
self.entries.get(normalize_style_tag_name(name))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn StyleTagRegistry::contains(self : StyleTagRegistry, name : String) -> Bool {
|
||||||
|
self.entries.contains(normalize_style_tag_name(name))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn StyleTagRegistry::define_alias(
|
||||||
|
self : StyleTagRegistry,
|
||||||
|
name : String,
|
||||||
|
target : String,
|
||||||
|
) -> StyleTagRegistry {
|
||||||
|
match self.get(target) {
|
||||||
|
Some(style) => self.set_tag(name, style=style)
|
||||||
|
None => match global_style_tag_registry().get(target) {
|
||||||
|
Some(style) => self.set_tag(name, style=style)
|
||||||
|
None => match builtin_text_style_for_tag(normalize_style_tag_name(target)) {
|
||||||
|
Some(style) => self.set_tag(name, style=style)
|
||||||
|
None => self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn default_style_tag_registry() -> StyleTagRegistry {
|
||||||
|
style_tag_registry()
|
||||||
|
.set_tag("black", fg=Some("black"))
|
||||||
|
.set_tag("red", fg=Some("red"))
|
||||||
|
.set_tag("green", fg=Some("green"))
|
||||||
|
.set_tag("yellow", fg=Some("yellow"))
|
||||||
|
.set_tag("blue", fg=Some("blue"))
|
||||||
|
.set_tag("magenta", fg=Some("magenta"))
|
||||||
|
.set_tag("cyan", fg=Some("cyan"))
|
||||||
|
.set_tag("white", fg=Some("white"))
|
||||||
|
.set_tag("bright_black", fg=Some("bright_black"))
|
||||||
|
.set_tag("bright_red", fg=Some("bright_red"))
|
||||||
|
.set_tag("bright_green", fg=Some("bright_green"))
|
||||||
|
.set_tag("bright_yellow", fg=Some("bright_yellow"))
|
||||||
|
.set_tag("bright_blue", fg=Some("bright_blue"))
|
||||||
|
.set_tag("bright_magenta", fg=Some("bright_magenta"))
|
||||||
|
.set_tag("bright_cyan", fg=Some("bright_cyan"))
|
||||||
|
.set_tag("bright_white", fg=Some("bright_white"))
|
||||||
|
.set_tag("accent", fg=Some("bright_cyan"), bold=true)
|
||||||
|
.set_tag("info", fg=Some("cyan"))
|
||||||
|
.set_tag("success", fg=Some("green"), bold=true)
|
||||||
|
.set_tag("warning", fg=Some("yellow"), bold=true)
|
||||||
|
.set_tag("danger", fg=Some("red"), bold=true)
|
||||||
|
.set_tag("muted", fg=Some("bright_black"), dim=true)
|
||||||
|
.set_tag("b", bold=true)
|
||||||
|
.set_tag("dim", dim=true)
|
||||||
|
.set_tag("i", italic=true)
|
||||||
|
.set_tag("u", underline=true)
|
||||||
|
}
|
||||||
|
|
||||||
|
let global_style_tag_registry_ref : Ref[StyleTagRegistry] = Ref::new(style_tag_registry())
|
||||||
|
|
||||||
|
pub fn global_style_tag_registry() -> StyleTagRegistry {
|
||||||
|
global_style_tag_registry_ref.val
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_global_style_tag_registry(registry : StyleTagRegistry) -> Unit {
|
||||||
|
global_style_tag_registry_ref.val = registry
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reset_global_style_tag_registry() -> Unit {
|
||||||
|
global_style_tag_registry_ref.val = style_tag_registry()
|
||||||
|
}
|
||||||
|
|
||||||
|
priv struct InlineStyle {
|
||||||
|
fg_code : String?
|
||||||
|
bg_code : String?
|
||||||
|
fg_basic_name : String?
|
||||||
|
bg_basic_name : String?
|
||||||
|
bold : Bool
|
||||||
|
dim : Bool
|
||||||
|
italic : Bool
|
||||||
|
underline : Bool
|
||||||
|
}
|
||||||
|
|
||||||
|
priv struct StyledSegment {
|
||||||
|
text : String
|
||||||
|
style : InlineStyle
|
||||||
|
}
|
||||||
|
|
||||||
|
priv struct StyleFrame {
|
||||||
|
tag : String
|
||||||
|
style : InlineStyle
|
||||||
|
}
|
||||||
|
|
||||||
|
fn code_unit(ch : Char) -> Int {
|
||||||
|
ch.to_int()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn code_unit16(ch : UInt16) -> Int {
|
||||||
|
ch.to_int()
|
||||||
|
}
|
||||||
|
|
||||||
pub struct TextFormatter {
|
pub struct TextFormatter {
|
||||||
show_timestamp : Bool
|
show_timestamp : Bool
|
||||||
show_level : Bool
|
show_level : Bool
|
||||||
@@ -8,6 +188,12 @@ pub struct TextFormatter {
|
|||||||
separator : String
|
separator : String
|
||||||
field_separator : String
|
field_separator : String
|
||||||
template : String
|
template : String
|
||||||
|
color_mode : ColorMode
|
||||||
|
color_support : ColorSupport
|
||||||
|
style_markup : StyleMarkupMode
|
||||||
|
target_style_markup : StyleMarkupMode
|
||||||
|
fields_style_markup : StyleMarkupMode
|
||||||
|
style_tags : StyleTagRegistry?
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn text_formatter(
|
pub fn text_formatter(
|
||||||
@@ -18,6 +204,12 @@ pub fn text_formatter(
|
|||||||
separator~ : String = " ",
|
separator~ : String = " ",
|
||||||
field_separator~ : String = " ",
|
field_separator~ : String = " ",
|
||||||
template~ : String = "",
|
template~ : String = "",
|
||||||
|
color_mode~ : ColorMode = ColorMode::Never,
|
||||||
|
color_support~ : ColorSupport = ColorSupport::TrueColor,
|
||||||
|
style_markup~ : StyleMarkupMode = StyleMarkupMode::Full,
|
||||||
|
target_style_markup~ : StyleMarkupMode = StyleMarkupMode::Disabled,
|
||||||
|
fields_style_markup~ : StyleMarkupMode = StyleMarkupMode::Disabled,
|
||||||
|
style_tags~ : StyleTagRegistry? = None,
|
||||||
) -> TextFormatter {
|
) -> TextFormatter {
|
||||||
{
|
{
|
||||||
show_timestamp,
|
show_timestamp,
|
||||||
@@ -27,6 +219,588 @@ pub fn text_formatter(
|
|||||||
separator,
|
separator,
|
||||||
field_separator,
|
field_separator,
|
||||||
template,
|
template,
|
||||||
|
color_mode,
|
||||||
|
color_support,
|
||||||
|
style_markup,
|
||||||
|
target_style_markup,
|
||||||
|
fields_style_markup,
|
||||||
|
style_tags,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn color_support_label(support : ColorSupport) -> String {
|
||||||
|
match support {
|
||||||
|
ColorSupport::Basic => "basic"
|
||||||
|
ColorSupport::TrueColor => "truecolor"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn TextFormatter::with_color_support(self : TextFormatter, color_support : ColorSupport) -> TextFormatter {
|
||||||
|
{ ..self, color_support }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn style_markup_mode_label(mode : StyleMarkupMode) -> String {
|
||||||
|
match mode {
|
||||||
|
StyleMarkupMode::Disabled => "disabled"
|
||||||
|
StyleMarkupMode::Builtin => "builtin"
|
||||||
|
StyleMarkupMode::Full => "full"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn TextFormatter::with_style_markup(self : TextFormatter, style_markup : StyleMarkupMode) -> TextFormatter {
|
||||||
|
{ ..self, style_markup }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn TextFormatter::without_style_markup(self : TextFormatter) -> TextFormatter {
|
||||||
|
{ ..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 {
|
||||||
|
{ ..self, style_tags: Some(style_tags) }
|
||||||
|
}
|
||||||
|
|
||||||
|
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 ansi_wrap_with_style(text : String, style : InlineStyle, enabled : Bool) -> String {
|
||||||
|
if !enabled || text == "" {
|
||||||
|
text
|
||||||
|
} else {
|
||||||
|
let codes : Array[String] = []
|
||||||
|
match style.fg_code {
|
||||||
|
Some(code) => codes.push(code)
|
||||||
|
None => ()
|
||||||
|
}
|
||||||
|
match style.bg_code {
|
||||||
|
Some(code) => codes.push(code)
|
||||||
|
None => ()
|
||||||
|
}
|
||||||
|
if style.bold {
|
||||||
|
codes.push("1")
|
||||||
|
}
|
||||||
|
if style.dim {
|
||||||
|
codes.push("2")
|
||||||
|
}
|
||||||
|
if style.italic {
|
||||||
|
codes.push("3")
|
||||||
|
}
|
||||||
|
if style.underline {
|
||||||
|
codes.push("4")
|
||||||
|
}
|
||||||
|
if codes.length() == 0 {
|
||||||
|
text
|
||||||
|
} else {
|
||||||
|
let joined = codes.join(";")
|
||||||
|
"\u{001b}[\{joined}m\{text}\u{001b}[0m"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_inline_style() -> InlineStyle {
|
||||||
|
{
|
||||||
|
fg_code: None,
|
||||||
|
bg_code: None,
|
||||||
|
fg_basic_name: None,
|
||||||
|
bg_basic_name: None,
|
||||||
|
bold: false,
|
||||||
|
dim: false,
|
||||||
|
italic: false,
|
||||||
|
underline: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn named_color_code(tag : String) -> String? {
|
||||||
|
match tag {
|
||||||
|
"black" => Some("30")
|
||||||
|
"red" => Some("31")
|
||||||
|
"green" => Some("32")
|
||||||
|
"yellow" => Some("33")
|
||||||
|
"blue" => Some("34")
|
||||||
|
"magenta" => Some("35")
|
||||||
|
"cyan" => Some("36")
|
||||||
|
"white" => Some("37")
|
||||||
|
"bright_black" => Some("90")
|
||||||
|
"bright_red" => Some("91")
|
||||||
|
"bright_green" => Some("92")
|
||||||
|
"bright_yellow" => Some("93")
|
||||||
|
"bright_blue" => Some("94")
|
||||||
|
"bright_magenta" => Some("95")
|
||||||
|
"bright_cyan" => Some("96")
|
||||||
|
"bright_white" => Some("97")
|
||||||
|
_ => None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn named_bg_color_code(tag : String) -> String? {
|
||||||
|
match tag {
|
||||||
|
"black" => Some("40")
|
||||||
|
"red" => Some("41")
|
||||||
|
"green" => Some("42")
|
||||||
|
"yellow" => Some("43")
|
||||||
|
"blue" => Some("44")
|
||||||
|
"magenta" => Some("45")
|
||||||
|
"cyan" => Some("46")
|
||||||
|
"white" => Some("47")
|
||||||
|
"bright_black" => Some("100")
|
||||||
|
"bright_red" => Some("101")
|
||||||
|
"bright_green" => Some("102")
|
||||||
|
"bright_yellow" => Some("103")
|
||||||
|
"bright_blue" => Some("104")
|
||||||
|
"bright_magenta" => Some("105")
|
||||||
|
"bright_cyan" => Some("106")
|
||||||
|
"bright_white" => Some("107")
|
||||||
|
_ => None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn basic_name_from_hex(value : String) -> String {
|
||||||
|
let r = hex_pair_to_int(value.unsafe_get(1), value.unsafe_get(2))
|
||||||
|
let g = hex_pair_to_int(value.unsafe_get(3), value.unsafe_get(4))
|
||||||
|
let b = hex_pair_to_int(value.unsafe_get(5), value.unsafe_get(6))
|
||||||
|
let max_value = if r >= g && r >= b { r } else if g >= b { g } else { b }
|
||||||
|
if max_value < 48 {
|
||||||
|
"bright_black"
|
||||||
|
} else {
|
||||||
|
let min_value = if r <= g && r <= b { r } else if g <= b { g } else { b }
|
||||||
|
let spread = max_value - min_value
|
||||||
|
if spread < 24 {
|
||||||
|
if max_value > 170 {
|
||||||
|
"white"
|
||||||
|
} else if max_value > 96 {
|
||||||
|
"bright_black"
|
||||||
|
} else {
|
||||||
|
"black"
|
||||||
|
}
|
||||||
|
} else if r >= g && r >= b {
|
||||||
|
if g > 96 && b < 96 {
|
||||||
|
"yellow"
|
||||||
|
} else if b > 96 && g < 96 {
|
||||||
|
"magenta"
|
||||||
|
} else {
|
||||||
|
"red"
|
||||||
|
}
|
||||||
|
} else if g >= r && g >= b {
|
||||||
|
if r > 96 && b < 96 {
|
||||||
|
"yellow"
|
||||||
|
} else if b > 96 && r < 96 {
|
||||||
|
"cyan"
|
||||||
|
} else {
|
||||||
|
"green"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if r > 96 && g < 96 {
|
||||||
|
"magenta"
|
||||||
|
} else if g > 96 && r < 96 {
|
||||||
|
"cyan"
|
||||||
|
} else {
|
||||||
|
"blue"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_inline_color_code(
|
||||||
|
formatter : TextFormatter,
|
||||||
|
basic_name : String?,
|
||||||
|
truecolor_code : String,
|
||||||
|
) -> String {
|
||||||
|
match formatter.color_support {
|
||||||
|
ColorSupport::TrueColor => truecolor_code
|
||||||
|
ColorSupport::Basic => match basic_name {
|
||||||
|
Some(name) => named_code_from_basic_name(name).unwrap_or(truecolor_code)
|
||||||
|
None => truecolor_code
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn named_code_from_basic_name(name : String) -> String? {
|
||||||
|
named_color_code(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn named_bg_code_from_basic_name(name : String) -> String? {
|
||||||
|
named_bg_color_code(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_inline_bg_code(
|
||||||
|
formatter : TextFormatter,
|
||||||
|
basic_name : String?,
|
||||||
|
truecolor_code : String,
|
||||||
|
) -> String {
|
||||||
|
match formatter.color_support {
|
||||||
|
ColorSupport::TrueColor => truecolor_code
|
||||||
|
ColorSupport::Basic => match basic_name {
|
||||||
|
Some(name) => named_bg_code_from_basic_name(name).unwrap_or(truecolor_code)
|
||||||
|
None => truecolor_code
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_hex_color(value : String) -> Bool {
|
||||||
|
if value.length() != 7 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if value.unsafe_get(0) != '#' {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for i = 1; i < value.length(); i = i + 1 {
|
||||||
|
let ch = value.unsafe_get(i)
|
||||||
|
let is_digit = ch >= '0' && ch <= '9'
|
||||||
|
let is_lower = ch >= 'a' && ch <= 'f'
|
||||||
|
let is_upper = ch >= 'A' && ch <= 'F'
|
||||||
|
if !(is_digit || is_lower || is_upper) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hex_to_int(ch : UInt16) -> Int {
|
||||||
|
let code = code_unit16(ch)
|
||||||
|
if code >= code_unit('0') && code <= code_unit('9') {
|
||||||
|
code_unit16(ch) - code_unit('0')
|
||||||
|
} else if code >= code_unit('a') && code <= code_unit('f') {
|
||||||
|
code_unit16(ch) - code_unit('a') + 10
|
||||||
|
} else {
|
||||||
|
code_unit16(ch) - code_unit('A') + 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hex_pair_to_int(high : UInt16, low : UInt16) -> Int {
|
||||||
|
hex_to_int(high) * 16 + hex_to_int(low)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rgb_fg_code(value : String) -> String {
|
||||||
|
let r = hex_pair_to_int(value.unsafe_get(1), value.unsafe_get(2))
|
||||||
|
let g = hex_pair_to_int(value.unsafe_get(3), value.unsafe_get(4))
|
||||||
|
let b = hex_pair_to_int(value.unsafe_get(5), value.unsafe_get(6))
|
||||||
|
"38;2;\{r};\{g};\{b}"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rgb_bg_code(value : String) -> String {
|
||||||
|
let r = hex_pair_to_int(value.unsafe_get(4), value.unsafe_get(5))
|
||||||
|
let g = hex_pair_to_int(value.unsafe_get(6), value.unsafe_get(7))
|
||||||
|
let b = hex_pair_to_int(value.unsafe_get(8), value.unsafe_get(9))
|
||||||
|
"48;2;\{r};\{g};\{b}"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rgb_bg_code_from_hex(value : String) -> String {
|
||||||
|
let r = hex_pair_to_int(value.unsafe_get(1), value.unsafe_get(2))
|
||||||
|
let g = hex_pair_to_int(value.unsafe_get(3), value.unsafe_get(4))
|
||||||
|
let b = hex_pair_to_int(value.unsafe_get(5), value.unsafe_get(6))
|
||||||
|
"48;2;\{r};\{g};\{b}"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn inline_style_from_text_style(
|
||||||
|
base : InlineStyle,
|
||||||
|
style : TextStyle,
|
||||||
|
formatter : TextFormatter,
|
||||||
|
) -> InlineStyle? {
|
||||||
|
let fg = match style.fg {
|
||||||
|
None => Some((base.fg_code, base.fg_basic_name))
|
||||||
|
Some(value) => {
|
||||||
|
let normalized = normalize_style_tag_name(value)
|
||||||
|
match named_color_code(normalized) {
|
||||||
|
Some(code) => Some((Some(code), Some(normalized)))
|
||||||
|
None => if is_hex_color(value) {
|
||||||
|
let basic_name = basic_name_from_hex(value)
|
||||||
|
Some((
|
||||||
|
Some(resolve_inline_color_code(formatter, Some(basic_name), rgb_fg_code(value))),
|
||||||
|
Some(basic_name),
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let bg = match style.bg {
|
||||||
|
None => Some((base.bg_code, base.bg_basic_name))
|
||||||
|
Some(value) => {
|
||||||
|
let normalized = normalize_style_tag_name(value)
|
||||||
|
match named_bg_color_code(normalized) {
|
||||||
|
Some(code) => {
|
||||||
|
let bg_name = if normalized.has_prefix("bright_") {
|
||||||
|
normalized
|
||||||
|
} else {
|
||||||
|
normalized
|
||||||
|
}
|
||||||
|
Some((Some(code), Some(bg_name)))
|
||||||
|
}
|
||||||
|
None => if is_hex_color(value) {
|
||||||
|
let basic_name = basic_name_from_hex(value)
|
||||||
|
Some((
|
||||||
|
Some(resolve_inline_bg_code(formatter, Some(basic_name), rgb_bg_code_from_hex(value))),
|
||||||
|
Some(basic_name),
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
match fg {
|
||||||
|
Some((next_fg_code, next_fg_name)) => match bg {
|
||||||
|
Some((next_bg_code, next_bg_name)) => Some({
|
||||||
|
fg_code: next_fg_code,
|
||||||
|
bg_code: next_bg_code,
|
||||||
|
fg_basic_name: next_fg_name,
|
||||||
|
bg_basic_name: next_bg_name,
|
||||||
|
bold: base.bold || style.bold,
|
||||||
|
dim: base.dim || style.dim,
|
||||||
|
italic: base.italic || style.italic,
|
||||||
|
underline: base.underline || style.underline,
|
||||||
|
})
|
||||||
|
None => None
|
||||||
|
}
|
||||||
|
None => None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn builtin_text_style_for_tag(tag : String) -> TextStyle? {
|
||||||
|
match normalize_style_tag_name(tag) {
|
||||||
|
"black" => Some(text_style(fg=Some("black")))
|
||||||
|
"red" => Some(text_style(fg=Some("red")))
|
||||||
|
"green" => Some(text_style(fg=Some("green")))
|
||||||
|
"yellow" => Some(text_style(fg=Some("yellow")))
|
||||||
|
"blue" => Some(text_style(fg=Some("blue")))
|
||||||
|
"magenta" => Some(text_style(fg=Some("magenta")))
|
||||||
|
"cyan" => Some(text_style(fg=Some("cyan")))
|
||||||
|
"white" => Some(text_style(fg=Some("white")))
|
||||||
|
"bright_black" => Some(text_style(fg=Some("bright_black")))
|
||||||
|
"bright_red" => Some(text_style(fg=Some("bright_red")))
|
||||||
|
"bright_green" => Some(text_style(fg=Some("bright_green")))
|
||||||
|
"bright_yellow" => Some(text_style(fg=Some("bright_yellow")))
|
||||||
|
"bright_blue" => Some(text_style(fg=Some("bright_blue")))
|
||||||
|
"bright_magenta" => Some(text_style(fg=Some("bright_magenta")))
|
||||||
|
"bright_cyan" => Some(text_style(fg=Some("bright_cyan")))
|
||||||
|
"bright_white" => Some(text_style(fg=Some("bright_white")))
|
||||||
|
"accent" => Some(text_style(fg=Some("bright_cyan"), bold=true))
|
||||||
|
"info" => Some(text_style(fg=Some("cyan")))
|
||||||
|
"success" => Some(text_style(fg=Some("green"), bold=true))
|
||||||
|
"warning" => Some(text_style(fg=Some("yellow"), bold=true))
|
||||||
|
"danger" => Some(text_style(fg=Some("red"), bold=true))
|
||||||
|
"muted" => Some(text_style(fg=Some("bright_black"), dim=true))
|
||||||
|
"b" => Some(text_style(bold=true))
|
||||||
|
"dim" => Some(text_style(dim=true))
|
||||||
|
"i" => Some(text_style(italic=true))
|
||||||
|
"u" => Some(text_style(underline=true))
|
||||||
|
_ => None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_registered_text_style(tag : String, formatter : TextFormatter) -> TextStyle? {
|
||||||
|
let normalized = normalize_style_tag_name(tag)
|
||||||
|
match formatter.style_markup {
|
||||||
|
StyleMarkupMode::Builtin => return builtin_text_style_for_tag(normalized)
|
||||||
|
_ => ()
|
||||||
|
}
|
||||||
|
match formatter.style_tags {
|
||||||
|
Some(registry) => match registry.get(normalized) {
|
||||||
|
Some(style) => Some(style)
|
||||||
|
None => match global_style_tag_registry().get(normalized) {
|
||||||
|
Some(style) => Some(style)
|
||||||
|
None => builtin_text_style_for_tag(normalized)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => match global_style_tag_registry().get(normalized) {
|
||||||
|
Some(style) => Some(style)
|
||||||
|
None => builtin_text_style_for_tag(normalized)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_inline_tag(style : InlineStyle, tag : String, formatter : TextFormatter) -> InlineStyle? {
|
||||||
|
let normalized = normalize_style_tag_name(tag)
|
||||||
|
if is_hex_color(tag) {
|
||||||
|
let basic_name = basic_name_from_hex(tag)
|
||||||
|
Some({
|
||||||
|
..style,
|
||||||
|
fg_code: Some(resolve_inline_color_code(formatter, Some(basic_name), rgb_fg_code(tag))),
|
||||||
|
fg_basic_name: Some(basic_name),
|
||||||
|
})
|
||||||
|
} else if normalized.length() == 10 && normalized.has_prefix("bg:") && is_hex_color(normalized[3:].to_owned()) {
|
||||||
|
let basic_name = basic_name_from_hex(normalized[3:].to_owned())
|
||||||
|
Some({
|
||||||
|
..style,
|
||||||
|
bg_code: Some(resolve_inline_bg_code(formatter, Some(basic_name), rgb_bg_code(normalized))),
|
||||||
|
bg_basic_name: Some(basic_name),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
match resolve_registered_text_style(normalized, formatter) {
|
||||||
|
Some(spec) => inline_style_from_text_style(style, spec, formatter)
|
||||||
|
None => None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_plain_segment(
|
||||||
|
segments : Array[StyledSegment],
|
||||||
|
buffer : StringBuilder,
|
||||||
|
style : InlineStyle,
|
||||||
|
) -> Unit {
|
||||||
|
let text = buffer.to_string()
|
||||||
|
if text != "" {
|
||||||
|
segments.push({ text, style })
|
||||||
|
buffer.reset()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pop_named_style_frame(stack : Array[StyleFrame], tag : String) -> Bool {
|
||||||
|
let normalized = normalize_style_tag_name(tag)
|
||||||
|
if stack.length() <= 1 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for i = stack.length() - 1; i > 0; i = i - 1 {
|
||||||
|
if stack[i].tag == normalized {
|
||||||
|
while stack.length() - 1 >= i {
|
||||||
|
ignore(stack.pop())
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn has_named_style_frame(stack : Array[StyleFrame], tag : String) -> Bool {
|
||||||
|
let normalized = normalize_style_tag_name(tag)
|
||||||
|
if stack.length() <= 1 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for i = stack.length() - 1; i > 0; i = i - 1 {
|
||||||
|
if stack[i].tag == normalized {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_inline_markup(input : String, formatter : TextFormatter) -> Array[StyledSegment] {
|
||||||
|
let segments : Array[StyledSegment] = []
|
||||||
|
let buffer = StringBuilder::new()
|
||||||
|
let stack : Array[StyleFrame] = [{ tag: "", style: default_inline_style() }]
|
||||||
|
let chars = input.to_array()
|
||||||
|
let current_style = fn() { stack[stack.length() - 1].style }
|
||||||
|
let flush = fn() { push_plain_segment(segments, buffer, current_style()) }
|
||||||
|
let append_raw = fn(start : Int, finish : Int) {
|
||||||
|
for i = start; i < finish; i = i + 1 {
|
||||||
|
buffer.write_char(chars.unsafe_get(i))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let find_tag_end = fn(start : Int) -> Int {
|
||||||
|
for i = start; i < chars.length(); i = i + 1 {
|
||||||
|
if chars.unsafe_get(i) == '>' {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
-1
|
||||||
|
}
|
||||||
|
for i = 0; i < chars.length(); {
|
||||||
|
if chars.unsafe_get(i) != '<' {
|
||||||
|
buffer.write_char(chars.unsafe_get(i))
|
||||||
|
continue i + 1
|
||||||
|
}
|
||||||
|
let end = find_tag_end(i + 1)
|
||||||
|
if end == -1 {
|
||||||
|
buffer.write_char(chars[i])
|
||||||
|
continue i + 1
|
||||||
|
}
|
||||||
|
let tag = input[i + 1:end].to_owned()
|
||||||
|
if tag == "/" {
|
||||||
|
if stack.length() > 1 {
|
||||||
|
flush()
|
||||||
|
ignore(stack.pop())
|
||||||
|
} else {
|
||||||
|
append_raw(i, end + 1)
|
||||||
|
}
|
||||||
|
continue end + 1
|
||||||
|
}
|
||||||
|
if tag.has_prefix("/") {
|
||||||
|
let close_tag = tag[1:].to_owned()
|
||||||
|
if close_tag != "" && has_named_style_frame(stack, close_tag) {
|
||||||
|
flush()
|
||||||
|
ignore(pop_named_style_frame(stack, close_tag))
|
||||||
|
} else {
|
||||||
|
append_raw(i, end + 1)
|
||||||
|
}
|
||||||
|
continue end + 1
|
||||||
|
}
|
||||||
|
match apply_inline_tag(current_style(), tag, formatter) {
|
||||||
|
Some(next_style) => {
|
||||||
|
flush()
|
||||||
|
stack.push({ tag: normalize_style_tag_name(tag), style: next_style })
|
||||||
|
}
|
||||||
|
None => append_raw(i, end + 1)
|
||||||
|
}
|
||||||
|
continue end + 1
|
||||||
|
}
|
||||||
|
flush()
|
||||||
|
segments
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_styled_text(text : String, formatter : TextFormatter, mode : StyleMarkupMode) -> String {
|
||||||
|
match mode {
|
||||||
|
StyleMarkupMode::Disabled => return text
|
||||||
|
_ => ()
|
||||||
|
}
|
||||||
|
let enabled = use_ansi_color(formatter.color_mode)
|
||||||
|
let scoped = { ..formatter, style_markup: mode }
|
||||||
|
let segments = parse_inline_markup(text, scoped)
|
||||||
|
let out = StringBuilder::new()
|
||||||
|
for segment in segments {
|
||||||
|
out.write_string(ansi_wrap_with_style(segment.text, segment.style, enabled))
|
||||||
|
}
|
||||||
|
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 {
|
||||||
|
match level {
|
||||||
|
Level::Trace => "90"
|
||||||
|
Level::Debug => "36"
|
||||||
|
Level::Info => "32"
|
||||||
|
Level::Warn => "33"
|
||||||
|
Level::Error => "31;1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,13 +812,9 @@ 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 {
|
||||||
rec.timestamp_ms.to_string()
|
ansi_wrap(rec.timestamp_ms.to_string(), "90", use_ansi_color(formatter.color_mode))
|
||||||
} else {
|
} else {
|
||||||
""
|
""
|
||||||
}
|
}
|
||||||
@@ -52,7 +822,7 @@ fn timestamp_text(rec : Record, formatter : TextFormatter) -> String {
|
|||||||
|
|
||||||
fn level_text(rec : Record, formatter : TextFormatter) -> String {
|
fn level_text(rec : Record, formatter : TextFormatter) -> String {
|
||||||
if formatter.show_level {
|
if formatter.show_level {
|
||||||
rec.level.label()
|
ansi_wrap(rec.level.label(), level_ansi_code(rec.level), use_ansi_color(formatter.color_mode))
|
||||||
} else {
|
} else {
|
||||||
""
|
""
|
||||||
}
|
}
|
||||||
@@ -60,15 +830,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 != "" {
|
||||||
rec.target
|
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 {
|
||||||
format_fields(rec.fields, formatter.field_separator)
|
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 {
|
||||||
""
|
""
|
||||||
}
|
}
|
||||||
@@ -80,7 +857,7 @@ fn render_template(rec : Record, formatter : TextFormatter) -> String {
|
|||||||
.replace_all(old="{timestamp_ms}", 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="{level}", new=level_text(rec, formatter))
|
||||||
.replace_all(old="{target}", new=target_text(rec, formatter))
|
.replace_all(old="{target}", new=target_text(rec, formatter))
|
||||||
.replace_all(old="{message}", new=rec.message)
|
.replace_all(old="{message}", new=render_inline_markup(rec.message, formatter))
|
||||||
.replace_all(old="{fields}", new=fields_text(rec, formatter))
|
.replace_all(old="{fields}", new=fields_text(rec, formatter))
|
||||||
.trim()
|
.trim()
|
||||||
.to_owned()
|
.to_owned()
|
||||||
@@ -92,20 +869,20 @@ pub fn format_text(rec : Record, formatter~ : TextFormatter = text_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("[\{timestamp_text(rec, formatter)}]")
|
||||||
}
|
}
|
||||||
if formatter.show_level {
|
if formatter.show_level {
|
||||||
parts.push("[\{rec.level.label()}]")
|
parts.push("[\{level_text(rec, formatter)}]")
|
||||||
}
|
}
|
||||||
if formatter.show_target && rec.target != "" {
|
if formatter.show_target && rec.target != "" {
|
||||||
parts.push("[\{rec.target}]")
|
parts.push("[\{target_text(rec, formatter)}]")
|
||||||
}
|
}
|
||||||
parts.push(rec.message)
|
parts.push(render_inline_markup(rec.message, formatter))
|
||||||
let base = parts.join(formatter.separator)
|
let base = parts.join(formatter.separator)
|
||||||
if !formatter.show_fields || rec.fields.length() == 0 {
|
if !formatter.show_fields || rec.fields.length() == 0 {
|
||||||
base
|
base
|
||||||
} else {
|
} else {
|
||||||
let details = format_fields(rec.fields, formatter.field_separator)
|
let details = fields_text(rec, formatter)
|
||||||
"\{base}\{formatter.separator}\{details}"
|
"\{base}\{formatter.separator}\{details}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
async test "shutdown drains pending records" {
|
async test "shutdown drains pending records" {
|
||||||
|
inspect(async_runtime_mode_label(async_runtime_mode()) == "native_worker" || async_runtime_mode_label(async_runtime_mode()) == "compatibility", content="true")
|
||||||
let written : Ref[Array[String]] = Ref::new([])
|
let written : Ref[Array[String]] = Ref::new([])
|
||||||
let flushes : Ref[Int] = Ref::new(0)
|
let flushes : Ref[Int] = Ref::new(0)
|
||||||
let logger = async_logger(
|
let logger = async_logger(
|
||||||
@@ -148,3 +149,59 @@ test "async build config stringify roundtrips nested logger and async fields" {
|
|||||||
AsyncFlushPolicy::Shutdown => "Shutdown"
|
AsyncFlushPolicy::Shutdown => "Shutdown"
|
||||||
}, content="Shutdown")
|
}, content="Shutdown")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "async runtime capability helpers stay consistent" {
|
||||||
|
let mode = async_runtime_mode()
|
||||||
|
let state = async_runtime_state()
|
||||||
|
let worker_supported = match mode {
|
||||||
|
AsyncRuntimeMode::NativeWorker => true
|
||||||
|
AsyncRuntimeMode::Compatibility => false
|
||||||
|
}
|
||||||
|
inspect(
|
||||||
|
async_runtime_mode_label(mode) == "native_worker" || async_runtime_mode_label(mode) == "compatibility",
|
||||||
|
content="true",
|
||||||
|
)
|
||||||
|
inspect(async_runtime_supports_background_worker() == worker_supported, content="true")
|
||||||
|
inspect(async_runtime_mode_label(state.mode) == async_runtime_mode_label(mode), content="true")
|
||||||
|
inspect(state.background_worker == worker_supported, content="true")
|
||||||
|
inspect(
|
||||||
|
stringify_async_runtime_state(state),
|
||||||
|
content=if worker_supported {
|
||||||
|
"{\"mode\":\"native_worker\",\"background_worker\":true}"
|
||||||
|
} else {
|
||||||
|
"{\"mode\":\"compatibility\",\"background_worker\":false}"
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async test "run drains queued records in compatibility backends too" {
|
||||||
|
let written : Ref[Array[String]] = Ref::new([])
|
||||||
|
let logger = async_logger(
|
||||||
|
@bitlogger.callback_sink(fn(rec) {
|
||||||
|
written.val.push(rec.message)
|
||||||
|
}),
|
||||||
|
config=AsyncLoggerConfig::new(
|
||||||
|
max_pending=4,
|
||||||
|
overflow=AsyncOverflowPolicy::DropNewest,
|
||||||
|
max_batch=2,
|
||||||
|
linger_ms=5,
|
||||||
|
flush=AsyncFlushPolicy::Never,
|
||||||
|
),
|
||||||
|
min_level=@bitlogger.Level::Info,
|
||||||
|
target="async.compat",
|
||||||
|
)
|
||||||
|
|
||||||
|
@async.with_task_group(group => {
|
||||||
|
logger.info("one")
|
||||||
|
logger.info("two")
|
||||||
|
inspect(logger.pending_count(), content="2")
|
||||||
|
group.spawn_bg(() => logger.run())
|
||||||
|
logger.shutdown()
|
||||||
|
})
|
||||||
|
|
||||||
|
inspect(logger.is_closed(), content="true")
|
||||||
|
inspect(logger.pending_count(), content="0")
|
||||||
|
inspect(written.val.length(), content="2")
|
||||||
|
inspect(written.val[0], content="one")
|
||||||
|
inspect(written.val[1], content="two")
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,6 +14,62 @@ pub(all) enum AsyncFlushPolicy {
|
|||||||
Shutdown
|
Shutdown
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub enum AsyncRuntimeMode {
|
||||||
|
NativeWorker
|
||||||
|
Compatibility
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn async_runtime_mode() -> AsyncRuntimeMode {
|
||||||
|
AsyncRuntimeMode::NativeWorker
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn async_runtime_mode_label(mode : AsyncRuntimeMode) -> String {
|
||||||
|
match mode {
|
||||||
|
AsyncRuntimeMode::NativeWorker => "native_worker"
|
||||||
|
AsyncRuntimeMode::Compatibility => "compatibility"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn all_async_runtime_modes() -> Array[AsyncRuntimeMode] {
|
||||||
|
[AsyncRuntimeMode::NativeWorker, AsyncRuntimeMode::Compatibility]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn async_runtime_supports_background_worker() -> Bool {
|
||||||
|
ignore(all_async_runtime_modes())
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AsyncRuntimeState {
|
||||||
|
mode : AsyncRuntimeMode
|
||||||
|
background_worker : Bool
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn async_runtime_state() -> AsyncRuntimeState {
|
||||||
|
{
|
||||||
|
mode: async_runtime_mode(),
|
||||||
|
background_worker: async_runtime_supports_background_worker(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn async_runtime_state_to_json(state : AsyncRuntimeState) -> @json_parser.JsonValue {
|
||||||
|
@json_parser.JsonValue::Object({
|
||||||
|
"mode": @json_parser.JsonValue::String(async_runtime_mode_label(state.mode)),
|
||||||
|
"background_worker": @json_parser.JsonValue::Bool(state.background_worker),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn stringify_async_runtime_state(
|
||||||
|
state : AsyncRuntimeState,
|
||||||
|
pretty~ : Bool = false,
|
||||||
|
) -> String {
|
||||||
|
let value = async_runtime_state_to_json(state)
|
||||||
|
if pretty {
|
||||||
|
@json_parser.stringify_pretty(value, 2)
|
||||||
|
} else {
|
||||||
|
@json_parser.stringify(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct AsyncLoggerConfig {
|
pub struct AsyncLoggerConfig {
|
||||||
max_pending : Int
|
max_pending : Int
|
||||||
overflow : AsyncOverflowPolicy
|
overflow : AsyncOverflowPolicy
|
||||||
|
|||||||
@@ -14,6 +14,62 @@ pub(all) enum AsyncFlushPolicy {
|
|||||||
Shutdown
|
Shutdown
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub enum AsyncRuntimeMode {
|
||||||
|
NativeWorker
|
||||||
|
Compatibility
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn async_runtime_mode() -> AsyncRuntimeMode {
|
||||||
|
AsyncRuntimeMode::Compatibility
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn async_runtime_mode_label(mode : AsyncRuntimeMode) -> String {
|
||||||
|
match mode {
|
||||||
|
AsyncRuntimeMode::NativeWorker => "native_worker"
|
||||||
|
AsyncRuntimeMode::Compatibility => "compatibility"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn all_async_runtime_modes() -> Array[AsyncRuntimeMode] {
|
||||||
|
[AsyncRuntimeMode::NativeWorker, AsyncRuntimeMode::Compatibility]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn async_runtime_supports_background_worker() -> Bool {
|
||||||
|
ignore(all_async_runtime_modes())
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AsyncRuntimeState {
|
||||||
|
mode : AsyncRuntimeMode
|
||||||
|
background_worker : Bool
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn async_runtime_state() -> AsyncRuntimeState {
|
||||||
|
{
|
||||||
|
mode: async_runtime_mode(),
|
||||||
|
background_worker: async_runtime_supports_background_worker(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn async_runtime_state_to_json(state : AsyncRuntimeState) -> @json_parser.JsonValue {
|
||||||
|
@json_parser.JsonValue::Object({
|
||||||
|
"mode": @json_parser.JsonValue::String(async_runtime_mode_label(state.mode)),
|
||||||
|
"background_worker": @json_parser.JsonValue::Bool(state.background_worker),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn stringify_async_runtime_state(
|
||||||
|
state : AsyncRuntimeState,
|
||||||
|
pretty~ : Bool = false,
|
||||||
|
) -> String {
|
||||||
|
let value = async_runtime_state_to_json(state)
|
||||||
|
if pretty {
|
||||||
|
@json_parser.stringify_pretty(value, 2)
|
||||||
|
} else {
|
||||||
|
@json_parser.stringify(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct AsyncLoggerConfig {
|
pub struct AsyncLoggerConfig {
|
||||||
max_pending : Int
|
max_pending : Int
|
||||||
overflow : AsyncOverflowPolicy
|
overflow : AsyncOverflowPolicy
|
||||||
@@ -38,7 +94,102 @@ pub fn AsyncLoggerConfig::new(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct AsyncLogger[S] {}
|
fn parse_async_overflow(name : String) -> AsyncOverflowPolicy raise {
|
||||||
|
match name.to_upper() {
|
||||||
|
"BLOCKING" => AsyncOverflowPolicy::Blocking
|
||||||
|
"DROPOLDEST" => AsyncOverflowPolicy::DropOldest
|
||||||
|
"DROPLATEST" => AsyncOverflowPolicy::DropNewest
|
||||||
|
"DROPNEWEST" => AsyncOverflowPolicy::DropNewest
|
||||||
|
_ => raise Failure::Failure("Unsupported async overflow policy: " + name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_async_flush(name : String) -> AsyncFlushPolicy raise {
|
||||||
|
match name.to_upper() {
|
||||||
|
"NEVER" => AsyncFlushPolicy::Never
|
||||||
|
"NONE" => AsyncFlushPolicy::Never
|
||||||
|
"BATCH" => AsyncFlushPolicy::Batch
|
||||||
|
"SHUTDOWN" => AsyncFlushPolicy::Shutdown
|
||||||
|
_ => raise Failure::Failure("Unsupported async flush policy: " + name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_async_logger_config_text(input : String) -> AsyncLoggerConfig raise {
|
||||||
|
let root = @json_parser.parse(input)
|
||||||
|
let obj = match root.as_object() {
|
||||||
|
Some(obj) => obj
|
||||||
|
None => raise Failure::Failure("Expected object for async logger config")
|
||||||
|
}
|
||||||
|
let max_pending = match obj.get("max_pending") {
|
||||||
|
Some(value) => match value.as_number() {
|
||||||
|
Some(number) => number.to_int()
|
||||||
|
None => raise Failure::Failure("Expected number at async_config.max_pending")
|
||||||
|
}
|
||||||
|
None => 0
|
||||||
|
}
|
||||||
|
let overflow = match obj.get("overflow") {
|
||||||
|
Some(value) => match value.as_string() {
|
||||||
|
Some(text) => parse_async_overflow(text)
|
||||||
|
None => raise Failure::Failure("Expected string at async_config.overflow")
|
||||||
|
}
|
||||||
|
None => AsyncOverflowPolicy::Blocking
|
||||||
|
}
|
||||||
|
let max_batch = match obj.get("max_batch") {
|
||||||
|
Some(value) => match value.as_number() {
|
||||||
|
Some(number) => number.to_int()
|
||||||
|
None => raise Failure::Failure("Expected number at async_config.max_batch")
|
||||||
|
}
|
||||||
|
None => 1
|
||||||
|
}
|
||||||
|
let linger_ms = match obj.get("linger_ms") {
|
||||||
|
Some(value) => match value.as_number() {
|
||||||
|
Some(number) => number.to_int()
|
||||||
|
None => raise Failure::Failure("Expected number at async_config.linger_ms")
|
||||||
|
}
|
||||||
|
None => 0
|
||||||
|
}
|
||||||
|
let flush = match obj.get("flush") {
|
||||||
|
Some(value) => match value.as_string() {
|
||||||
|
Some(text) => parse_async_flush(text)
|
||||||
|
None => raise Failure::Failure("Expected string at async_config.flush")
|
||||||
|
}
|
||||||
|
None => AsyncFlushPolicy::Never
|
||||||
|
}
|
||||||
|
AsyncLoggerConfig::new(
|
||||||
|
max_pending=max_pending,
|
||||||
|
overflow=overflow,
|
||||||
|
max_batch=max_batch,
|
||||||
|
linger_ms=linger_ms,
|
||||||
|
flush=flush,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn async_logger_config_to_json(config : AsyncLoggerConfig) -> @json_parser.JsonValue {
|
||||||
|
@json_parser.JsonValue::Object({
|
||||||
|
"max_pending": @json_parser.JsonValue::Number(config.max_pending.to_double()),
|
||||||
|
"max_batch": @json_parser.JsonValue::Number(config.max_batch.to_double()),
|
||||||
|
"linger_ms": @json_parser.JsonValue::Number(config.linger_ms.to_double()),
|
||||||
|
"overflow": @json_parser.JsonValue::String(match config.overflow {
|
||||||
|
AsyncOverflowPolicy::Blocking => "Blocking"
|
||||||
|
AsyncOverflowPolicy::DropOldest => "DropOldest"
|
||||||
|
AsyncOverflowPolicy::DropNewest => "DropNewest"
|
||||||
|
}),
|
||||||
|
"flush": @json_parser.JsonValue::String(match config.flush {
|
||||||
|
AsyncFlushPolicy::Never => "Never"
|
||||||
|
AsyncFlushPolicy::Batch => "Batch"
|
||||||
|
AsyncFlushPolicy::Shutdown => "Shutdown"
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn stringify_async_logger_config(config : AsyncLoggerConfig, pretty~ : Bool = false) -> String {
|
||||||
|
let value = async_logger_config_to_json(config)
|
||||||
|
if pretty {
|
||||||
|
@json_parser.stringify_pretty(value, 2)
|
||||||
|
} else {
|
||||||
|
@json_parser.stringify(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct AsyncLoggerBuildConfig {
|
pub struct AsyncLoggerBuildConfig {
|
||||||
logger : @bitlogger.LoggerConfig
|
logger : @bitlogger.LoggerConfig
|
||||||
@@ -53,182 +204,417 @@ pub fn AsyncLoggerBuildConfig::new(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse_async_logger_build_config_text(input : String) -> AsyncLoggerBuildConfig raise {
|
pub fn parse_async_logger_build_config_text(input : String) -> AsyncLoggerBuildConfig raise {
|
||||||
ignore(input)
|
let root = @json_parser.parse(input)
|
||||||
abort("bitlogger_async currently only supports native/llvm backends")
|
let obj = match root.as_object() {
|
||||||
}
|
Some(obj) => obj
|
||||||
|
None => raise Failure::Failure("Expected object at async logger build config root")
|
||||||
pub fn parse_async_logger_config_text(input : String) -> AsyncLoggerConfig raise {
|
}
|
||||||
ignore(input)
|
let logger = match obj.get("logger") {
|
||||||
abort("bitlogger_async currently only supports native/llvm backends")
|
Some(value) => @bitlogger.parse_logger_config_text(@json_parser.stringify(value))
|
||||||
}
|
None => @bitlogger.default_logger_config()
|
||||||
|
}
|
||||||
pub fn async_logger_config_to_json(config : AsyncLoggerConfig) -> @json_parser.JsonValue {
|
let async_config = match obj.get("async_config") {
|
||||||
ignore(config)
|
Some(value) => parse_async_logger_config_text(@json_parser.stringify(value))
|
||||||
abort("bitlogger_async currently only supports native/llvm backends")
|
None => AsyncLoggerConfig::new()
|
||||||
}
|
}
|
||||||
|
AsyncLoggerBuildConfig::new(logger=logger, async_config=async_config)
|
||||||
pub fn stringify_async_logger_config(config : AsyncLoggerConfig, pretty~ : Bool = false) -> String {
|
|
||||||
ignore(config)
|
|
||||||
ignore(pretty)
|
|
||||||
abort("bitlogger_async currently only supports native/llvm backends")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn async_logger_build_config_to_json(
|
pub fn async_logger_build_config_to_json(
|
||||||
config : AsyncLoggerBuildConfig,
|
config : AsyncLoggerBuildConfig,
|
||||||
) -> @json_parser.JsonValue {
|
) -> @json_parser.JsonValue {
|
||||||
ignore(config)
|
@json_parser.JsonValue::Object({
|
||||||
abort("bitlogger_async currently only supports native/llvm backends")
|
"logger": @bitlogger.logger_config_to_json(config.logger),
|
||||||
|
"async_config": async_logger_config_to_json(config.async_config),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn stringify_async_logger_build_config(
|
pub fn stringify_async_logger_build_config(
|
||||||
config : AsyncLoggerBuildConfig,
|
config : AsyncLoggerBuildConfig,
|
||||||
pretty~ : Bool = false,
|
pretty~ : Bool = false,
|
||||||
) -> String {
|
) -> String {
|
||||||
ignore(config)
|
let value = async_logger_build_config_to_json(config)
|
||||||
ignore(pretty)
|
if pretty {
|
||||||
abort("bitlogger_async currently only supports native/llvm backends")
|
@json_parser.stringify_pretty(value, 2)
|
||||||
|
} else {
|
||||||
|
@json_parser.stringify(value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn async_logger[S : @bitlogger.Sink](
|
pub struct AsyncLogger[S] {
|
||||||
|
min_level : @bitlogger.Level
|
||||||
|
target : String
|
||||||
|
timestamp : Bool
|
||||||
|
overflow : AsyncOverflowPolicy
|
||||||
|
max_batch : Int
|
||||||
|
linger_ms : Int
|
||||||
|
flush_policy : AsyncFlushPolicy
|
||||||
|
sink : S
|
||||||
|
flush_sink : (S) -> Int
|
||||||
|
context_fields : Array[@bitlogger.Field]
|
||||||
|
filter : (@bitlogger.Record) -> Bool
|
||||||
|
patch : @bitlogger.RecordPatch
|
||||||
|
queue : @async.Queue[@bitlogger.Record]
|
||||||
|
pending_count : Ref[Int]
|
||||||
|
dropped_count : Ref[Int]
|
||||||
|
is_closed : Ref[Bool]
|
||||||
|
is_running : Ref[Bool]
|
||||||
|
has_failed : Ref[Bool]
|
||||||
|
last_error : Ref[String]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn[S] async_logger(
|
||||||
sink : S,
|
sink : S,
|
||||||
config~ : AsyncLoggerConfig = AsyncLoggerConfig::new(),
|
config~ : AsyncLoggerConfig = AsyncLoggerConfig::new(),
|
||||||
min_level~ : @bitlogger.Level = @bitlogger.Level::Info,
|
min_level~ : @bitlogger.Level = @bitlogger.Level::Info,
|
||||||
target~ : String = "",
|
target~ : String = "",
|
||||||
flush~ : (S) -> Int = fn(_) { 0 },
|
flush~ : (S) -> Int = fn(_) { 0 },
|
||||||
) -> AsyncLogger[S] {
|
) -> AsyncLogger[S] {
|
||||||
ignore(sink)
|
{
|
||||||
ignore(config)
|
min_level,
|
||||||
ignore(min_level)
|
target,
|
||||||
ignore(target)
|
timestamp: false,
|
||||||
ignore(flush)
|
overflow: config.overflow,
|
||||||
abort("bitlogger_async currently only supports native/llvm backends")
|
max_batch: config.max_batch,
|
||||||
|
linger_ms: config.linger_ms,
|
||||||
|
flush_policy: config.flush,
|
||||||
|
sink,
|
||||||
|
flush_sink: flush,
|
||||||
|
context_fields: [],
|
||||||
|
filter: fn(_) { true },
|
||||||
|
patch: @bitlogger.identity_patch(),
|
||||||
|
queue: @async.Queue::new(kind=queue_kind_of(config)),
|
||||||
|
pending_count: Ref::new(0),
|
||||||
|
dropped_count: Ref::new(0),
|
||||||
|
is_closed: Ref::new(false),
|
||||||
|
is_running: Ref::new(false),
|
||||||
|
has_failed: Ref::new(false),
|
||||||
|
last_error: Ref::new(""),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn queue_kind_of(config : AsyncLoggerConfig) -> @aqueue.Kind {
|
||||||
|
let limit = if config.max_pending < 0 { 0 } else { config.max_pending }
|
||||||
|
match config.overflow {
|
||||||
|
AsyncOverflowPolicy::Blocking => @aqueue.Kind::Blocking(limit)
|
||||||
|
AsyncOverflowPolicy::DropOldest => @aqueue.Kind::DiscardOldest(limit)
|
||||||
|
AsyncOverflowPolicy::DropNewest => @aqueue.Kind::DiscardLatest(limit)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn[S] AsyncLogger::with_timestamp(self : AsyncLogger[S], enabled~ : Bool = true) -> AsyncLogger[S] {
|
pub fn[S] AsyncLogger::with_timestamp(self : AsyncLogger[S], enabled~ : Bool = true) -> AsyncLogger[S] {
|
||||||
ignore(self)
|
{ ..self, timestamp: enabled }
|
||||||
ignore(enabled)
|
|
||||||
abort("bitlogger_async currently only supports native/llvm backends")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn[S] AsyncLogger::with_target(self : AsyncLogger[S], target : String) -> AsyncLogger[S] {
|
pub fn[S] AsyncLogger::with_target(self : AsyncLogger[S], target : String) -> AsyncLogger[S] {
|
||||||
ignore(self)
|
{ ..self, target }
|
||||||
ignore(target)
|
|
||||||
abort("bitlogger_async currently only supports native/llvm backends")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn[S] AsyncLogger::with_context_fields(
|
pub fn[S] AsyncLogger::with_context_fields(
|
||||||
self : AsyncLogger[S],
|
self : AsyncLogger[S],
|
||||||
fields : Array[@bitlogger.Field],
|
fields : Array[@bitlogger.Field],
|
||||||
) -> AsyncLogger[S] {
|
) -> AsyncLogger[S] {
|
||||||
ignore(self)
|
{ ..self, context_fields: fields }
|
||||||
ignore(fields)
|
|
||||||
abort("bitlogger_async currently only supports native/llvm backends")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn[S] AsyncLogger::with_filter(
|
pub fn[S] AsyncLogger::with_filter(
|
||||||
self : AsyncLogger[S],
|
self : AsyncLogger[S],
|
||||||
predicate : (@bitlogger.Record) -> Bool,
|
predicate : (@bitlogger.Record) -> Bool,
|
||||||
) -> AsyncLogger[S] {
|
) -> AsyncLogger[S] {
|
||||||
ignore(self)
|
let current = self.filter
|
||||||
ignore(predicate)
|
{
|
||||||
abort("bitlogger_async currently only supports native/llvm backends")
|
..self,
|
||||||
|
filter: fn(rec) {
|
||||||
|
current(rec) && predicate(rec)
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn[S] AsyncLogger::with_patch(
|
pub fn[S] AsyncLogger::with_patch(
|
||||||
self : AsyncLogger[S],
|
self : AsyncLogger[S],
|
||||||
patch : @bitlogger.RecordPatch,
|
patch : @bitlogger.RecordPatch,
|
||||||
) -> AsyncLogger[S] {
|
) -> AsyncLogger[S] {
|
||||||
ignore(self)
|
let current = self.patch
|
||||||
ignore(patch)
|
{
|
||||||
abort("bitlogger_async currently only supports native/llvm backends")
|
..self,
|
||||||
|
patch: fn(rec) {
|
||||||
|
patch(current(rec))
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn[S] AsyncLogger::with_min_level(
|
pub fn[S] AsyncLogger::with_min_level(
|
||||||
self : AsyncLogger[S],
|
self : AsyncLogger[S],
|
||||||
min_level : @bitlogger.Level,
|
min_level : @bitlogger.Level,
|
||||||
) -> AsyncLogger[S] {
|
) -> AsyncLogger[S] {
|
||||||
ignore(self)
|
{ ..self, min_level }
|
||||||
ignore(min_level)
|
}
|
||||||
abort("bitlogger_async currently only supports native/llvm backends")
|
|
||||||
|
fn combine_targets(parent : String, child : String) -> String {
|
||||||
|
if parent == "" {
|
||||||
|
child
|
||||||
|
} else if child == "" {
|
||||||
|
parent
|
||||||
|
} else {
|
||||||
|
"\{parent}.\{child}"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn[S] AsyncLogger::child(self : AsyncLogger[S], target : String) -> AsyncLogger[S] {
|
pub fn[S] AsyncLogger::child(self : AsyncLogger[S], target : String) -> AsyncLogger[S] {
|
||||||
ignore(self)
|
{ ..self, target: combine_targets(self.target, target) }
|
||||||
ignore(target)
|
|
||||||
abort("bitlogger_async currently only supports native/llvm backends")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn[S] AsyncLogger::is_enabled(self : AsyncLogger[S], level : @bitlogger.Level) -> Bool {
|
pub fn[S] AsyncLogger::is_enabled(self : AsyncLogger[S], level : @bitlogger.Level) -> Bool {
|
||||||
ignore(self)
|
level.enabled(self.min_level)
|
||||||
ignore(level)
|
}
|
||||||
abort("bitlogger_async currently only supports native/llvm backends")
|
|
||||||
|
fn merge_fields(
|
||||||
|
left : Array[@bitlogger.Field],
|
||||||
|
right : Array[@bitlogger.Field],
|
||||||
|
) -> Array[@bitlogger.Field] {
|
||||||
|
if left.length() == 0 {
|
||||||
|
right
|
||||||
|
} else if right.length() == 0 {
|
||||||
|
left
|
||||||
|
} else {
|
||||||
|
left + right
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn[S] AsyncLogger::log(
|
||||||
|
self : AsyncLogger[S],
|
||||||
|
level : @bitlogger.Level,
|
||||||
|
message : String,
|
||||||
|
fields~ : Array[@bitlogger.Field] = [],
|
||||||
|
target? : String = "",
|
||||||
|
) -> Unit {
|
||||||
|
guard !self.is_closed() else {
|
||||||
|
()
|
||||||
|
}
|
||||||
|
guard self.is_enabled(level) else {
|
||||||
|
()
|
||||||
|
}
|
||||||
|
let actual_target = if target == "" { self.target } else { target }
|
||||||
|
let timestamp_ms = if self.timestamp { @env.now() } else { 0UL }
|
||||||
|
let rec = @bitlogger.Record::new(
|
||||||
|
level,
|
||||||
|
message,
|
||||||
|
timestamp_ms=timestamp_ms,
|
||||||
|
target=actual_target,
|
||||||
|
fields=merge_fields(self.context_fields, fields),
|
||||||
|
)
|
||||||
|
let rec = (self.patch)(rec)
|
||||||
|
guard (self.filter)(rec) else {
|
||||||
|
()
|
||||||
|
}
|
||||||
|
let accepted = self.queue.try_put(rec) catch {
|
||||||
|
err if err is AsyncLoggerClosed => false
|
||||||
|
err => raise err
|
||||||
|
}
|
||||||
|
if accepted {
|
||||||
|
self.pending_count.val += 1
|
||||||
|
} else {
|
||||||
|
match self.overflow {
|
||||||
|
AsyncOverflowPolicy::Blocking => {
|
||||||
|
self.queue.put(rec) catch {
|
||||||
|
err if err is AsyncLoggerClosed => ()
|
||||||
|
err => raise err
|
||||||
|
}
|
||||||
|
self.pending_count.val += 1
|
||||||
|
}
|
||||||
|
AsyncOverflowPolicy::DropOldest | AsyncOverflowPolicy::DropNewest => {
|
||||||
|
self.dropped_count.val += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn[S] AsyncLogger::trace(
|
||||||
|
self : AsyncLogger[S],
|
||||||
|
message : String,
|
||||||
|
fields~ : Array[@bitlogger.Field] = [],
|
||||||
|
) -> Unit {
|
||||||
|
self.log(@bitlogger.Level::Trace, message, fields=fields)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn[S] AsyncLogger::debug(
|
||||||
|
self : AsyncLogger[S],
|
||||||
|
message : String,
|
||||||
|
fields~ : Array[@bitlogger.Field] = [],
|
||||||
|
) -> Unit {
|
||||||
|
self.log(@bitlogger.Level::Debug, message, fields=fields)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn[S] AsyncLogger::info(
|
||||||
|
self : AsyncLogger[S],
|
||||||
|
message : String,
|
||||||
|
fields~ : Array[@bitlogger.Field] = [],
|
||||||
|
) -> Unit {
|
||||||
|
self.log(@bitlogger.Level::Info, message, fields=fields)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn[S] AsyncLogger::warn(
|
||||||
|
self : AsyncLogger[S],
|
||||||
|
message : String,
|
||||||
|
fields~ : Array[@bitlogger.Field] = [],
|
||||||
|
) -> Unit {
|
||||||
|
self.log(@bitlogger.Level::Warn, message, fields=fields)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn[S] AsyncLogger::error(
|
||||||
|
self : AsyncLogger[S],
|
||||||
|
message : String,
|
||||||
|
fields~ : Array[@bitlogger.Field] = [],
|
||||||
|
) -> Unit {
|
||||||
|
self.log(@bitlogger.Level::Error, message, fields=fields)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn[S] AsyncLogger::pending_count(self : AsyncLogger[S]) -> Int {
|
pub fn[S] AsyncLogger::pending_count(self : AsyncLogger[S]) -> Int {
|
||||||
ignore(self)
|
self.pending_count.val
|
||||||
abort("bitlogger_async currently only supports native/llvm backends")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn[S] AsyncLogger::dropped_count(self : AsyncLogger[S]) -> Int {
|
pub fn[S] AsyncLogger::dropped_count(self : AsyncLogger[S]) -> Int {
|
||||||
ignore(self)
|
self.dropped_count.val
|
||||||
abort("bitlogger_async currently only supports native/llvm backends")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn[S] AsyncLogger::is_closed(self : AsyncLogger[S]) -> Bool {
|
pub fn[S] AsyncLogger::is_closed(self : AsyncLogger[S]) -> Bool {
|
||||||
ignore(self)
|
self.is_closed.val
|
||||||
abort("bitlogger_async currently only supports native/llvm backends")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn[S] AsyncLogger::is_running(self : AsyncLogger[S]) -> Bool {
|
pub fn[S] AsyncLogger::is_running(self : AsyncLogger[S]) -> Bool {
|
||||||
ignore(self)
|
self.is_running.val
|
||||||
abort("bitlogger_async currently only supports native/llvm backends")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn[S] AsyncLogger::has_failed(self : AsyncLogger[S]) -> Bool {
|
pub fn[S] AsyncLogger::has_failed(self : AsyncLogger[S]) -> Bool {
|
||||||
ignore(self)
|
self.has_failed.val
|
||||||
abort("bitlogger_async currently only supports native/llvm backends")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn[S] AsyncLogger::last_error(self : AsyncLogger[S]) -> String {
|
pub fn[S] AsyncLogger::last_error(self : AsyncLogger[S]) -> String {
|
||||||
ignore(self)
|
self.last_error.val
|
||||||
abort("bitlogger_async currently only supports native/llvm backends")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn[S] AsyncLogger::flush_policy(self : AsyncLogger[S]) -> AsyncFlushPolicy {
|
pub fn[S] AsyncLogger::flush_policy(self : AsyncLogger[S]) -> AsyncFlushPolicy {
|
||||||
ignore(self)
|
self.flush_policy
|
||||||
abort("bitlogger_async currently only supports native/llvm backends")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn[S] AsyncLogger::close(self : AsyncLogger[S], clear? : Bool = false) -> Unit {
|
pub fn[S] AsyncLogger::close(self : AsyncLogger[S], clear? : Bool = false) -> Unit {
|
||||||
ignore(self)
|
self.is_closed.val = true
|
||||||
ignore(clear)
|
if clear {
|
||||||
abort("bitlogger_async currently only supports native/llvm backends")
|
let abandoned = self.pending_count()
|
||||||
|
if abandoned > 0 {
|
||||||
|
self.dropped_count.val += abandoned
|
||||||
|
self.pending_count.val = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.queue.close(error=AsyncLoggerClosed, clear=clear)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn[S] AsyncLogger::wait_idle(self : AsyncLogger[S]) -> Unit {
|
pub async fn[S] AsyncLogger::wait_idle(self : AsyncLogger[S]) -> Unit {
|
||||||
ignore(self)
|
while self.pending_count() > 0 {
|
||||||
abort("bitlogger_async currently only supports native/llvm backends")
|
if self.has_failed() {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
@async.pause()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn[S] AsyncLogger::shutdown(self : AsyncLogger[S], clear? : Bool = false) -> Unit {
|
pub async fn[S] AsyncLogger::shutdown(self : AsyncLogger[S], clear? : Bool = false) -> Unit {
|
||||||
ignore(self)
|
if clear {
|
||||||
ignore(clear)
|
self.close(clear=true)
|
||||||
abort("bitlogger_async currently only supports native/llvm backends")
|
} else {
|
||||||
|
self.wait_idle()
|
||||||
|
self.close()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn[S : @bitlogger.Sink] AsyncLogger::run(self : AsyncLogger[S]) -> Unit {
|
pub async fn[S : @bitlogger.Sink] AsyncLogger::run(self : AsyncLogger[S]) -> Unit {
|
||||||
ignore(self)
|
self.is_running.val = true
|
||||||
abort("bitlogger_async currently only supports native/llvm backends")
|
self.has_failed.val = false
|
||||||
|
self.last_error.val = ""
|
||||||
|
run_worker(self) catch {
|
||||||
|
err => {
|
||||||
|
self.has_failed.val = true
|
||||||
|
self.last_error.val = err.to_string()
|
||||||
|
self.is_running.val = false
|
||||||
|
raise err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.is_running.val = false
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn[S : @bitlogger.Sink] run_worker(logger : AsyncLogger[S]) -> Unit {
|
||||||
|
while true {
|
||||||
|
let rec = logger.queue.get() catch {
|
||||||
|
err if err is AsyncLoggerClosed => break
|
||||||
|
err => raise err
|
||||||
|
}
|
||||||
|
logger.sink.write(rec)
|
||||||
|
if logger.pending_count.val > 0 {
|
||||||
|
logger.pending_count.val -= 1
|
||||||
|
}
|
||||||
|
for drained = 1; drained < logger.max_batch; {
|
||||||
|
let next = logger.queue.try_get() catch {
|
||||||
|
err if err is AsyncLoggerClosed => None
|
||||||
|
err => raise err
|
||||||
|
}
|
||||||
|
match next {
|
||||||
|
Some(next) => {
|
||||||
|
logger.sink.write(next)
|
||||||
|
if logger.pending_count.val > 0 {
|
||||||
|
logger.pending_count.val -= 1
|
||||||
|
}
|
||||||
|
continue drained + 1
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
if logger.linger_ms <= 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
let waited = @async.with_timeout_opt(logger.linger_ms, () => logger.queue.get()) catch {
|
||||||
|
err if err is AsyncLoggerClosed => None
|
||||||
|
err => raise err
|
||||||
|
}
|
||||||
|
match waited {
|
||||||
|
Some(next) => {
|
||||||
|
logger.sink.write(next)
|
||||||
|
if logger.pending_count.val > 0 {
|
||||||
|
logger.pending_count.val -= 1
|
||||||
|
}
|
||||||
|
continue drained + 1
|
||||||
|
}
|
||||||
|
None => break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
match logger.flush_policy {
|
||||||
|
AsyncFlushPolicy::Batch => ignore((logger.flush_sink)(logger.sink))
|
||||||
|
_ => ()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
match logger.flush_policy {
|
||||||
|
AsyncFlushPolicy::Shutdown => ignore((logger.flush_sink)(logger.sink))
|
||||||
|
_ => ()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn build_async_logger(
|
pub fn build_async_logger(
|
||||||
config : AsyncLoggerBuildConfig,
|
config : AsyncLoggerBuildConfig,
|
||||||
) -> AsyncLogger[@bitlogger.RuntimeSink] {
|
) -> AsyncLogger[@bitlogger.RuntimeSink] {
|
||||||
ignore(config)
|
let logger = @bitlogger.build_logger(config.logger)
|
||||||
abort("bitlogger_async currently only supports native/llvm backends")
|
async_logger(
|
||||||
|
logger.sink,
|
||||||
|
config=config.async_config,
|
||||||
|
min_level=logger.min_level,
|
||||||
|
target=logger.target,
|
||||||
|
flush=fn(sink) { sink.flush() },
|
||||||
|
).with_timestamp(enabled=logger.timestamp)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn build_async_text_logger(config : AsyncLoggerBuildConfig) -> AsyncLogger[@bitlogger.FormattedConsoleSink] {
|
pub fn build_async_text_logger(config : AsyncLoggerBuildConfig) -> AsyncLogger[@bitlogger.FormattedConsoleSink] {
|
||||||
ignore(config)
|
async_logger(
|
||||||
abort("bitlogger_async currently only supports native/llvm backends")
|
@bitlogger.text_console_sink(config.logger.sink.text_formatter.to_formatter()),
|
||||||
|
config=config.async_config,
|
||||||
|
min_level=config.logger.min_level,
|
||||||
|
target=config.logger.target,
|
||||||
|
).with_timestamp(enabled=config.logger.timestamp)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ import {
|
|||||||
"moonbitlang/core/ref",
|
"moonbitlang/core/ref",
|
||||||
}
|
}
|
||||||
|
|
||||||
supported_targets = "+native"
|
|
||||||
|
|
||||||
options(
|
options(
|
||||||
targets: {
|
targets: {
|
||||||
"async_logger_native.mbt": [ "native", "llvm" ],
|
"async_logger_native.mbt": [ "native", "llvm" ],
|
||||||
|
|||||||
+92
-2
@@ -6,6 +6,15 @@ BitLogger is a structured logging library written in MoonBit.
|
|||||||
|
|
||||||
BitLogger currently provides:
|
BitLogger currently provides:
|
||||||
|
|
||||||
|
## Backend Compatibility
|
||||||
|
|
||||||
|
| Module / capability | native / llvm | js / wasm / wasm-gc |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `bitlogger` core package | Supported | Supported |
|
||||||
|
| `file_sink(...)` | Supported | Not available, `native_files_supported()` returns `false` |
|
||||||
|
| `bitlogger_async` | Native worker semantics | Compatibility implementation |
|
||||||
|
| `examples/async_basic` | Supported | Not shipped currently because `async fn main` entry support is still limited |
|
||||||
|
|
||||||
- log levels: `Trace`, `Debug`, `Info`, `Warn`, `Error`
|
- log levels: `Trace`, `Debug`, `Info`, `Warn`, `Error`
|
||||||
- structured key-value fields
|
- structured key-value fields
|
||||||
- plain console output
|
- plain console output
|
||||||
@@ -25,6 +34,7 @@ BitLogger currently provides:
|
|||||||
- 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(...)`, `text_console_sink(...)`, and template-driven `template` output
|
- configurable text formatting via `text_formatter(...)`, `format_text(...)`, `text_console_sink(...)`, and template-driven `template` output
|
||||||
|
- lightweight style tags via `color_mode`, inline markup, `TextStyle`, `StyleTagRegistry`, custom tags, and builtin-tag overrides
|
||||||
- formatter-based callback integration via `formatted_callback_sink(...)`
|
- formatter-based callback integration via `formatted_callback_sink(...)`
|
||||||
- native-only file output via `file_sink(...)`, with basic size rotation, backup retention, explicit `reopen()` / `reopen_with_current_policy()` / `reopen_append()` / `reopen_truncate()`, and failure counters
|
- native-only file output via `file_sink(...)`, with basic size rotation, backup retention, explicit `reopen()` / `reopen_with_current_policy()` / `reopen_append()` / `reopen_truncate()`, and failure counters
|
||||||
- `native_files_supported()` for backend capability detection
|
- `native_files_supported()` for backend capability detection
|
||||||
@@ -158,17 +168,47 @@ let formatter = text_formatter(
|
|||||||
show_timestamp=false,
|
show_timestamp=false,
|
||||||
field_separator=",",
|
field_separator=",",
|
||||||
template="[{level}] {target} {message} :: {fields}",
|
template="[{level}] {target} {message} :: {fields}",
|
||||||
|
color_mode=ColorMode::Always,
|
||||||
)
|
)
|
||||||
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")])
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Inline style tags:
|
||||||
|
|
||||||
|
```moonbit
|
||||||
|
let formatter = text_formatter(
|
||||||
|
show_timestamp=false,
|
||||||
|
color_mode=ColorMode::Always,
|
||||||
|
).with_style_tags(
|
||||||
|
default_style_tag_registry()
|
||||||
|
.set_tag("accent", fg=Some("#4cc9f0"), bold=true)
|
||||||
|
.define_alias("danger", "red"),
|
||||||
|
)
|
||||||
|
|
||||||
|
let logger = Logger::new(text_console_sink(formatter), target="styled")
|
||||||
|
|
||||||
|
logger.info("<accent>styled</> output and <danger>alert</>")
|
||||||
|
```
|
||||||
|
|
||||||
|
Disable style markup parsing:
|
||||||
|
|
||||||
|
```moonbit
|
||||||
|
let formatter = text_formatter(
|
||||||
|
color_mode=ColorMode::Always,
|
||||||
|
).without_style_markup()
|
||||||
|
|
||||||
|
let logger = Logger::new(text_console_sink(formatter), target="raw")
|
||||||
|
|
||||||
|
logger.info("<red>kept as raw text</>")
|
||||||
|
```
|
||||||
|
|
||||||
JSON config loading:
|
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\":{\"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)
|
let logger = build_logger(config)
|
||||||
@@ -177,6 +217,30 @@ logger.info("configured from json")
|
|||||||
ignore(logger.flush())
|
ignore(logger.flush())
|
||||||
```
|
```
|
||||||
|
|
||||||
|
JSON `style_tags`:
|
||||||
|
|
||||||
|
```moonbit
|
||||||
|
let config = parse_logger_config_text(
|
||||||
|
"{\"sink\":{\"kind\":\"text_console\",\"text_formatter\":{\"show_timestamp\":false,\"color_mode\":\"always\",\"style_tags\":{\"accent\":{\"fg\":\"#4cc9f0\",\"bold\":true}}}}}",
|
||||||
|
)
|
||||||
|
|
||||||
|
let logger = build_logger(config)
|
||||||
|
|
||||||
|
logger.info("<accent>styled from json</>")
|
||||||
|
```
|
||||||
|
|
||||||
|
JSON `style_markup` mode:
|
||||||
|
|
||||||
|
```moonbit
|
||||||
|
let config = parse_logger_config_text(
|
||||||
|
"{\"sink\":{\"kind\":\"text_console\",\"text_formatter\":{\"color_mode\":\"always\",\"style_markup\":\"disabled\"}}}",
|
||||||
|
)
|
||||||
|
|
||||||
|
let logger = build_logger(config)
|
||||||
|
|
||||||
|
logger.info("<red>still raw</>")
|
||||||
|
```
|
||||||
|
|
||||||
Native file sink:
|
Native file sink:
|
||||||
|
|
||||||
```moonbit
|
```moonbit
|
||||||
@@ -225,6 +289,17 @@ match logger.file_runtime_state() {
|
|||||||
- 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(...)`.
|
||||||
- `QueueConfig`, `TextFormatterConfig`, and `SinkConfig` can also be exported independently through `queue_config_to_json(...)` / `stringify_queue_config(...)`, `text_formatter_config_to_json(...)` / `stringify_text_formatter_config(...)`, and `sink_config_to_json(...)` / `stringify_sink_config(...)`.
|
- `QueueConfig`, `TextFormatterConfig`, and `SinkConfig` can also be exported independently through `queue_config_to_json(...)` / `stringify_queue_config(...)`, `text_formatter_config_to_json(...)` / `stringify_text_formatter_config(...)`, and `sink_config_to_json(...)` / `stringify_sink_config(...)`.
|
||||||
- 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` also include `color_support = basic | truecolor` so hex / RGB styling can be forced to downgrade to basic ANSI colors.
|
||||||
|
- `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>...</>`.
|
||||||
|
- Closing tags now support both the short form `</>` and named closing tags such as `</red>`, `</danger>`, and `</b>`.
|
||||||
|
- 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(...)`.
|
||||||
|
- Style-tag lookup priority is formatter-local `style_tags` > global style tag registry > builtin tags.
|
||||||
|
- `sink.text_formatter.style_tags` now supports a minimal object mapping with `fg`, `bg`, `bold`, `dim`, `italic`, and `underline`.
|
||||||
|
- `define_alias(...)` remains a runtime-only API and is not yet part of the JSON config schema.
|
||||||
- `sink.rotation` currently supports `max_bytes` and `max_backups` for basic size-based rotation and backup retention.
|
- `sink.rotation` currently supports `max_bytes` and `max_backups` for basic size-based rotation and backup retention.
|
||||||
- `file_sink(...)` also exposes `reopen()`, `reopen_with_current_policy()`, `reopen_append()`, `reopen_truncate()`, `open_failures()`, `write_failures()`, `flush_failures()`, and `rotation_failures()` for basic observability.
|
- `file_sink(...)` also exposes `reopen()`, `reopen_with_current_policy()`, `reopen_append()`, `reopen_truncate()`, `open_failures()`, `write_failures()`, `flush_failures()`, and `rotation_failures()` for basic observability.
|
||||||
- `file_sink(...)` also exposes `append_mode()`. Passing `append=...` to `reopen(...)` updates the current append policy used by later reopen calls, `reopen_with_current_policy()` makes that stored-policy reopen path explicit, and `reopen_append()` / `reopen_truncate()` cover the two common policy switches directly.
|
- `file_sink(...)` also exposes `append_mode()`. Passing `append=...` to `reopen(...)` updates the current append policy used by later reopen calls, `reopen_with_current_policy()` makes that stored-policy reopen path explicit, and `reopen_append()` / `reopen_truncate()` cover the two common policy switches directly.
|
||||||
@@ -248,6 +323,12 @@ match logger.file_runtime_state() {
|
|||||||
- `file_sink_policy_to_json(...)` and `stringify_file_sink_policy(...)` can export standalone file-policy snapshots directly as JSON for policy diffing, diagnostics, or reporting.
|
- `file_sink_policy_to_json(...)` and `stringify_file_sink_policy(...)` can export standalone file-policy snapshots directly as JSON for policy diffing, diagnostics, or reporting.
|
||||||
- `file_sink_state_to_json(...)`, `stringify_file_sink_state(...)`, `runtime_file_state_to_json(...)`, and `stringify_runtime_file_state(...)` can export file and queued-file snapshots directly as JSON for diagnostics or reporting.
|
- `file_sink_state_to_json(...)`, `stringify_file_sink_state(...)`, `runtime_file_state_to_json(...)`, and `stringify_runtime_file_state(...)` can export file and queued-file snapshots directly as JSON for diagnostics or reporting.
|
||||||
- `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_support` currently supports `basic` and `truecolor`.
|
||||||
|
- `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`.
|
||||||
|
- `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.
|
||||||
|
|
||||||
@@ -259,8 +340,17 @@ match logger.file_runtime_state() {
|
|||||||
- `shutdown()` is now the recommended way to stop the async worker. By default it waits for the queue to drain, closes the queue, and then waits for the worker to exit.
|
- `shutdown()` is now the recommended way to stop the async worker. By default it waits for the queue to drain, closes the queue, and then waits for the worker to exit.
|
||||||
- Basic lifecycle observability is also available through `is_closed()`, `is_running()`, `has_failed()`, and `last_error()`.
|
- Basic lifecycle observability is also available through `is_closed()`, `is_running()`, `has_failed()`, and `last_error()`.
|
||||||
- The async worker now supports batched queue draining via `max_batch` and basic flush policies through `flush=Never|Batch|Shutdown`.
|
- The async worker now supports batched queue draining via `max_batch` and basic flush policies through `flush=Never|Batch|Shutdown`.
|
||||||
|
- `async_runtime_mode()`, `async_runtime_mode_label(...)`, and `async_runtime_supports_background_worker()` expose whether the current backend is using the native worker path or the compatibility implementation.
|
||||||
|
- `async_runtime_state_to_json(...)` and `stringify_async_runtime_state(...)` make it easy to include the current async runtime mode in startup diagnostics or health output.
|
||||||
- The recommended startup pattern is shown in [examples/async_basic/main.mbt](/E:/repo/MooLiteyukiBot/examples/async_basic/main.mbt:1).
|
- The recommended startup pattern is shown in [examples/async_basic/main.mbt](/E:/repo/MooLiteyukiBot/examples/async_basic/main.mbt:1).
|
||||||
- This layer currently targets `native/llvm` only and remains isolated from the synchronous logger core.
|
- `bitlogger_async` now compiles across multiple targets: `native/llvm` keeps the background worker semantics, while `js` / `wasm` / `wasm-gc` use a compatibility implementation.
|
||||||
|
- Because `moonbitlang/async` still limits `async fn main` entry support, the `examples/async_basic` executable remains `native`-only for now.
|
||||||
|
|
||||||
|
Startup diagnostic example:
|
||||||
|
|
||||||
|
```moonbit
|
||||||
|
println(stringify_async_runtime_state(async_runtime_state(), pretty=true))
|
||||||
|
```
|
||||||
|
|
||||||
### Async Config
|
### Async Config
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
## BitLogger Update Changes
|
||||||
|
|
||||||
|
version 0.4.0
|
||||||
|
|
||||||
|
### Feature
|
||||||
|
- feat: add `ColorMode = Never | Auto | Always` for text formatter color control
|
||||||
|
- feat: add ANSI level, target, timestamp, and field rendering to `format_text(...)`
|
||||||
|
- feat: add `color_mode` to `TextFormatter` and `TextFormatterConfig`
|
||||||
|
- feat: support `text_formatter.color_mode` in JSON config parsing and serialization
|
||||||
|
- feat: add inline style markup support in message text for ANSI text formatter output
|
||||||
|
- feat: support named color tags like `<red>...</>`, style tags like `<b>...</>`, and hex tags like `<#ff0000>...</>` / `<bg:#010203>...</>`
|
||||||
|
- feat: keep JSON formatter output unchanged and limit inline style parsing to text message rendering only
|
||||||
|
- feat: add `TextStyle`, `StyleTagRegistry`, `style_tag_registry()`, and `default_style_tag_registry()` for reusable inline style tags
|
||||||
|
- feat: add formatter-local `style_tags`, global style tag registry helpers, builtin-tag override support, and alias reuse via `define_alias(...)`
|
||||||
|
- feat: support minimal `sink.text_formatter.style_tags` JSON config parsing and serialization for custom formatter tag styles
|
||||||
|
- 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: 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`
|
||||||
|
- feat: support named closing tags like `</red>` alongside the existing short close `</>`
|
||||||
|
- feat: add `ColorSupport = Basic | TrueColor` and support `sink.text_formatter.color_support` so hex / RGB styles can downgrade to basic ANSI colors
|
||||||
|
|
||||||
|
### Test
|
||||||
|
- test: cover ANSI text formatter rendering in `Always` mode
|
||||||
|
- test: cover `Auto` mode fallback behavior when `NO_COLOR` is present
|
||||||
|
- test: cover config parsing and serialization for `color_mode`
|
||||||
|
- test: cover named inline color tags in ANSI mode
|
||||||
|
- test: cover plain mode tag stripping, nested tags, hex tags, and unknown-tag fallback behavior
|
||||||
|
- test: cover custom tags, builtin-tag override, formatter-vs-global priority, global registry fallback, and alias reuse
|
||||||
|
- 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 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
|
||||||
|
- test: cover named closing tags, mixed short/named closing, and unmatched named-close fallback behavior
|
||||||
|
- test: cover basic color-support downgrade for hex foreground/background rendering in runtime and config paths
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
- docs: add `color_mode` usage examples to formatter documentation
|
||||||
|
- docs: add runtime and JSON `style_tags` examples, and document current JSON schema scope
|
||||||
|
- docs: add runtime and JSON examples for toggling style markup parsing
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
- `Auto` currently uses a conservative rule: if `NO_COLOR` exists, ANSI is disabled; otherwise ANSI is enabled
|
||||||
|
- inline style markup supports both short close `</>` and named closing tags like `</red>`
|
||||||
|
- unknown or invalid inline tags currently fall back to plain text and do not raise formatter errors
|
||||||
|
- 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
|
||||||
|
- 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
|
||||||
|
- `basic` color support currently keeps ANSI styling but downgrades hex / RGB colors to the nearest basic ANSI color family
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
## BitLogger Update Changes
|
||||||
|
|
||||||
|
version 0.4.1
|
||||||
|
|
||||||
|
### Feature
|
||||||
|
|
||||||
|
- feat: make `bitlogger_async` compile on `js` / `wasm` / `wasm-gc` via a compatibility backend instead of abort-only stubs
|
||||||
|
- feat: add `async_runtime_mode()` and related helpers so callers can explicitly detect native-worker vs compatibility async behavior
|
||||||
|
- feat: add `AsyncRuntimeState` JSON/stringify helpers for exposing async runtime mode in diagnostics
|
||||||
|
|
||||||
|
### Test
|
||||||
|
|
||||||
|
- test: cover async logger compatibility backend queue drain behavior and keep async config roundtrip available on non-native targets
|
||||||
|
- test: cover async runtime capability helper consistency across backend-specific implementations
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
|
||||||
|
- `bitlogger_async` now provides a compatibility implementation on non-native targets; the standalone async example remains `native`-only because `async fn main` entry support is still toolchain-limited
|
||||||
|
- `examples/async_basic` now prints the current async runtime state at startup so downstream projects have a minimal diagnostic pattern to copy
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
async fn main {
|
async fn main {
|
||||||
|
println(@lib_async.stringify_async_runtime_state(@lib_async.async_runtime_state(), pretty=true))
|
||||||
|
|
||||||
let raw = "{\"logger\":{\"min_level\":\"info\",\"target\":\"async.demo\",\"timestamp\":true,\"sink\":{\"kind\":\"text_console\",\"text_formatter\":{\"show_timestamp\":false,\"separator\":\" | \"}}},\"async_config\":{\"max_pending\":2,\"overflow\":\"DropOldest\",\"max_batch\":4,\"linger_ms\":5,\"flush\":\"Batch\"}}"
|
let raw = "{\"logger\":{\"min_level\":\"info\",\"target\":\"async.demo\",\"timestamp\":true,\"sink\":{\"kind\":\"text_console\",\"text_formatter\":{\"show_timestamp\":false,\"separator\":\" | \"}}},\"async_config\":{\"max_pending\":2,\"overflow\":\"DropOldest\",\"max_batch\":4,\"linger_ms\":5,\"flush\":\"Batch\"}}"
|
||||||
let config = @lib_async.parse_async_logger_build_config_text(raw) catch {
|
let config = @lib_async.parse_async_logger_build_config_text(raw) catch {
|
||||||
err => {
|
err => {
|
||||||
|
|||||||
@@ -68,6 +68,21 @@ fn main {
|
|||||||
)
|
)
|
||||||
pretty_logger.info("custom text format", fields=[@lib.field("mode", "pretty")])
|
pretty_logger.info("custom text format", fields=[@lib.field("mode", "pretty")])
|
||||||
|
|
||||||
|
let styled_formatter = @lib.text_formatter(
|
||||||
|
show_timestamp=false,
|
||||||
|
color_mode=@lib.ColorMode::Always,
|
||||||
|
).with_style_tags(
|
||||||
|
@lib.default_style_tag_registry()
|
||||||
|
.set_tag("accent", fg=Some("#4cc9f0"), bold=true)
|
||||||
|
.define_alias("danger", "red"),
|
||||||
|
)
|
||||||
|
let styled_logger = @lib.Logger::new(
|
||||||
|
@lib.text_console_sink(styled_formatter),
|
||||||
|
min_level=@lib.Level::Info,
|
||||||
|
target="styled",
|
||||||
|
)
|
||||||
|
styled_logger.info("<accent>styled</> output and <danger>alert</>")
|
||||||
|
|
||||||
if @lib.native_files_supported() {
|
if @lib.native_files_supported() {
|
||||||
let file_logger = @lib.Logger::new(
|
let file_logger = @lib.Logger::new(
|
||||||
@lib.file_sink(
|
@lib.file_sink(
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "Nanaloveyuki/BitLogger",
|
"name": "Nanaloveyuki/BitLogger",
|
||||||
"version": "0.3.0",
|
"version": "0.4.1",
|
||||||
"deps": {
|
"deps": {
|
||||||
"maria/json_parser": "0.1.1",
|
"maria/json_parser": "0.1.1",
|
||||||
"moonbitlang/async": "0.18.1"
|
"moonbitlang/async": "0.18.1"
|
||||||
|
|||||||
Reference in New Issue
Block a user