20 Commits

Author SHA1 Message Date
Nanaloveyuki 73bcf78d89 🔖 Prepare 0.5.0 release metadata 2026-05-20 11:40:52 +08:00
Nanaloveyuki 1ca5ab0835 Add coverage for release helper APIs 2026-05-20 11:40:37 +08:00
Nanaloveyuki e019db11d6 📝 Polish onboarding and add feature examples 2026-05-20 11:40:23 +08:00
Nanaloveyuki 5f12991592 ♻️ Extract file sink implementation 2026-05-20 11:40:02 +08:00
Nanaloveyuki 25a6a973d2 📝 Update More API Document 2026-05-20 11:37:49 +08:00
Nanaloveyuki 55af0b664f 🙈 Update Vibecoding gitignore 2026-05-20 11:37:03 +08:00
Nanaloveyuki 0a098915af 📝 Add agent handoff note templates 2026-05-20 10:08:11 +08:00
Nanaloveyuki 4860d1e08b 📝 Document logger config presets 2026-05-20 10:07:52 +08:00
Nanaloveyuki dca34bc114 ♻️ Split global logger emitters 2026-05-20 10:07:08 +08:00
Nanaloveyuki 4c25a81b03 Add logger config presets 2026-05-20 10:06:51 +08:00
Nanaloveyuki 3c15d8ed13 Add logger config presets 2026-05-20 09:40:21 +08:00
Nanaloveyuki 6998df0ee4 ♻️ Split logger emission helpers 2026-05-20 09:40:15 +08:00
Nanaloveyuki 414d6a0ee8 ♻️ Split runtime file control helpers 2026-05-20 09:34:07 +08:00
Nanaloveyuki 2823a67d53 ♻️ Extract async config and state models 2026-05-20 09:22:56 +08:00
Nanaloveyuki a766dd09ac ♻️ Extract runtime file state helpers 2026-05-20 09:12:02 +08:00
Nanaloveyuki d0989d6308 ♻️ Extract config into utils subpackage 2026-05-20 09:02:44 +08:00
Nanaloveyuki 8b752a4b4d ♻️ Extract sink models into utils subpackage 2026-05-20 08:51:48 +08:00
Nanaloveyuki ac45ec2b03 ♻️ Extract file backend into utils subpackage 2026-05-20 08:44:54 +08:00
Nanaloveyuki 41d221af46 ♻️ Extract formatter into utils subpackage 2026-05-20 08:31:55 +08:00
Nanaloveyuki d6e47d4bb8 ♻️ Extract core and utils subpackages 2026-05-20 08:15:31 +08:00
103 changed files with 6997 additions and 2794 deletions
+3
View File
@@ -19,3 +19,6 @@ desktop.ini
# Dev Documentations
docs/dev/*
# vibecoding
AGENTS/
+25 -4
View File
@@ -35,13 +35,26 @@ BitLogger 是一个使用 MoonBit 编写的结构化日志库,目标是提供
## 🚀 快速开始
```moonbit
let logger = Logger::new(console_sink(), min_level=Level::Info, target="demo")
.with_timestamp()
.with_context_fields([field("service", "bitlogger")])
let logger = build_logger(
with_queue(
text_console(
min_level=Level::Info,
target="demo",
text_formatter=TextFormatterConfig::new(show_timestamp=false, separator=" | "),
),
max_pending=32,
overflow=QueueOverflowPolicy::DropOldest,
),
)
logger.info("starting", fields=[field("port", "8080")])
ignore(logger.flush())
```
推荐先从 `presets + build_logger(...)` 路径开始:`console(...)` / `json_console(...)` / `text_console(...)` / `file(...)` 负责拼装 `LoggerConfig``with_queue(...)` / `with_file_rotation(...)` 负责做小范围组合,然后再交给 `build_logger(...)` 构建运行时 logger。
当你需要 `fanout``split``callback``patch` 这类自定义 sink 图组合时,再优先使用 `Logger::new(...)` 直接进行运行时拼装。
异步入口示例:
```moonbit
@@ -58,7 +71,13 @@ let logger = async_logger(console_sink(), target="async.demo")
- `src/`: 主日志库 package。
- `src-async/`: 基于 `moonbitlang/async` 的异步日志层。
- `docs/api/`: 单接口粒度 API 文档。
- `examples/basic/`: 最小同步示例
- `examples/basic/`: breadth 示例;文件开头就是推荐的 presets + `build_logger(...)` 同步入口,后续继续覆盖更广能力面
- `examples/console_basic/`: console 与 json console 最小输出示例。
- `examples/text_formatter/`: text formatter / template 示例。
- `examples/style_tags/`: style tag / colored formatter 示例。
- `examples/file_rotation/`: file sink / rotation 示例。
- `examples/config_build/`: `build_logger(...)` 配置驱动示例。
- `examples/presets/`: presets + `build_logger(...)` 示例。
- `examples/async_basic/`: 异步 logger 示例。
## 🔗 文档入口
@@ -67,3 +86,5 @@ let logger = async_logger(console_sink(), target="async.demo")
- [English README](./docs/README-en.md)
- [src package README](./src/README.mbt.md)
- [API 索引](./docs/api/index.md)
- 推荐起步 API: `text_console(...)` / `file(...)` / `with_queue(...)` / `build_logger(...)`
- facade API: `build_application_logger(...)` / `build_library_logger(...)` / `build_application_async_logger(...)`
+27 -4
View File
@@ -29,13 +29,26 @@ BitLogger is designed to provide composable, configurable, and cross-target logg
## Quick Start
```moonbit
let logger = Logger::new(console_sink(), min_level=Level::Info, target="demo")
.with_timestamp()
.with_context_fields([field("service", "bitlogger")])
let logger = build_logger(
with_queue(
text_console(
min_level=Level::Info,
target="demo",
text_formatter=TextFormatterConfig::new(show_timestamp=false, separator=" | "),
),
max_pending=32,
overflow=QueueOverflowPolicy::DropOldest,
),
)
logger.info("starting", fields=[field("port", "8080")])
ignore(logger.flush())
```
Start new synchronous code with `presets + build_logger(...)`: use `console(...)`, `json_console(...)`, `text_console(...)`, or `file(...)` to assemble `LoggerConfig`, optionally extend it with `with_queue(...)` or `with_file_rotation(...)`, then call `build_logger(...)`.
Switch to `Logger::new(...)` when you need direct runtime sink composition such as `fanout`, `split`, `callback`, or custom patch/filter graphs.
Async entry example:
```moonbit
@@ -52,7 +65,13 @@ let logger = async_logger(console_sink(), target="async.demo")
- `src/`: core logging package.
- `src-async/`: async logging layer built on `moonbitlang/async`.
- `docs/api/`: one-file-per-interface API documentation.
- `examples/basic/`: minimal synchronous example.
- `examples/basic/`: breadth example; starts with the recommended presets-based sync path and then sweeps broader capabilities.
- `examples/console_basic/`: minimal console and JSON console output.
- `examples/text_formatter/`: text formatter and template example.
- `examples/style_tags/`: style tag and colored formatter example.
- `examples/file_rotation/`: file sink and rotation example.
- `examples/config_build/`: config-driven `build_logger(...)` example.
- `examples/presets/`: presets-based construction example.
- `examples/async_basic/`: async logger example.
## Documentation Entry Points
@@ -60,11 +79,15 @@ let logger = async_logger(console_sink(), target="async.demo")
- [Mooncake package page](https://mooncakes.io/docs/Nanaloveyuki/BitLogger)
- [Chinese README](../README.md)
- [src package README](../src/README.mbt.md)
- Recommended sync starting APIs: `text_console(...)`, `file(...)`, `with_queue(...)`, `build_logger(...)`
- Selected API docs in `docs/api/`:
- [logger-new.md](./api/logger-new.md)
- [async-logger.md](./api/async-logger.md)
- [build-logger.md](./api/build-logger.md)
- [build-async-logger.md](./api/build-async-logger.md)
- [build-application-logger.md](./api/build-application-logger.md)
- [build-library-logger.md](./api/build-library-logger.md)
- [build-application-async-logger.md](./api/build-application-async-logger.md)
## Notes
+66
View File
@@ -0,0 +1,66 @@
---
name: buffered-sink
group: api
category: sink
update-time: 20260520
description: Create a sink that buffers records and flushes them manually or at a threshold.
key-word:
- sink
- buffer
- flush
- public
---
## Buffered-sink
Create a sink that buffers records before forwarding them to another sink. This helper is useful when callers want explicit or threshold-based sync batching without using the queue wrapper API.
### Interface
```moonbit
pub fn[S] buffered_sink(sink : S, flush_limit~ : Int = 1) -> BufferedSink[S] {
```
#### input
- `sink : S` - Wrapped sink that receives flushed records.
- `flush_limit : Int` - Buffer length threshold that triggers automatic flush.
#### output
- `BufferedSink[S]` - Buffering sink with `pending_count()` and `flush()` helpers.
### Explanation
Detailed rules explaining key parameters and behaviors
- Records are stored in an in-memory buffer until flushed.
- `flush_limit <= 0` is normalized to `1`.
- Flushing forwards the buffered records in order to the wrapped sink.
### How to Use
Here are some specific examples provided.
#### When Need Manual Or Threshold-based Batch Delivery
When writes should accumulate before they reach the destination:
```moonbit
let sink = buffered_sink(console_sink(), flush_limit=2)
let logger = Logger::new(sink, target="buffered")
```
In this example, records stay buffered until the threshold is reached or `flush()` is called.
### Error Case
e.g.:
- If callers never flush a buffer whose threshold is not reached, records remain pending.
- If bounded dropping behavior is required instead of simple buffering, use `queued_sink(...)` or `Logger::with_queue(...)`.
### Notes
1. This helper is simpler than explicit queue overflow management.
2. It is useful for synchronous batching scenarios and tests.
@@ -0,0 +1,71 @@
---
name: build-application-async-logger
group: api
category: facade
update-time: 20260520
description: Build the application-facing async logger facade from an AsyncLoggerBuildConfig.
key-word:
- application
- async
- facade
- public
---
## Build-application-async-logger
Build an `ApplicationAsyncLogger` from `AsyncLoggerBuildConfig`. This is the application-facing async facade over `build_async_logger(...)`.
### Interface
```moonbit
pub fn build_application_async_logger(
config : AsyncLoggerBuildConfig,
) -> ApplicationAsyncLogger {
```
#### input
- `config : AsyncLoggerBuildConfig` - Combined sync logger config and async queue/runtime config.
#### output
- `ApplicationAsyncLogger` - Application-facing async runtime logger.
### Explanation
Detailed rules explaining key parameters and behaviors
- This API delegates to `build_async_logger(...)`.
- The returned logger keeps the standard async lifecycle and state helper surface.
- Use this facade when application code wants a dedicated async app-level entry point.
### How to Use
Here are some specific examples provided.
#### When Need Config-driven App Async Boot
When both sync sink shape and async queue policy are assembled as typed config:
```moonbit
let logger = build_application_async_logger(
AsyncLoggerBuildConfig::new(
logger=LoggerConfig::new(target="app.async"),
async_config=AsyncLoggerConfig::new(max_pending=8),
),
)
```
In this example, the app-facing async facade is built directly from typed config.
### Error Case
e.g.:
- If file output is selected on a backend without native file support, backend behavior still applies when the worker drains records.
- If the logger is never `run()`, enqueue behavior and lifecycle state still follow the normal async logger rules.
### Notes
1. This is a facade over the existing async runtime logger builder.
2. Use `parse_and_build_application_async_logger(...)` when starting from JSON text.
+66
View File
@@ -0,0 +1,66 @@
---
name: build-application-logger
group: api
category: facade
update-time: 20260520
description: Build the application-facing configured logger facade from a LoggerConfig.
key-word:
- application
- facade
- logger
- public
---
## Build-application-logger
Build an `ApplicationLogger` from `LoggerConfig`. This facade is the application-oriented sync entry point and currently aliases the configured runtime logger shape returned by `build_logger(...)`.
### Interface
```moonbit
pub fn build_application_logger(config : LoggerConfig) -> ApplicationLogger {
```
#### input
- `config : LoggerConfig` - Fully assembled sync logger config.
#### output
- `ApplicationLogger` - Application-facing configured runtime logger.
### Explanation
Detailed rules explaining key parameters and behaviors
- This API delegates to `build_logger(...)`.
- The returned value keeps the same public logging, queue, and file runtime helper surface as `ConfiguredLogger`.
- Use this facade when application boot code wants an app-specific entry name without exposing lower-level builder naming in its own code.
### How to Use
Here are some specific examples provided.
#### When Need An App-level Sync Builder Entry
When boot code assembles config values before runtime construction:
```moonbit
let logger = build_application_logger(
LoggerConfig::new(target="app", sink=SinkConfig::new(kind=SinkKind::Console)),
)
```
In this example, the application facade builds the same configured runtime logger shape as `build_logger(...)`.
### Error Case
e.g.:
- If the config uses file output on a backend without native file support, backend runtime limitations still apply after construction.
- If queueing is not configured, queue helper values simply reflect the non-queued runtime shape.
### Notes
1. This is a facade API, not a separate runtime implementation.
2. Use `parse_and_build_application_logger(...)` when starting from JSON text.
@@ -0,0 +1,71 @@
---
name: build-application-text-async-logger
group: api
category: facade
update-time: 20260520
description: Build the application-facing text-console async logger facade from an AsyncLoggerBuildConfig.
key-word:
- application
- async
- text
- public
---
## Build-application-text-async-logger
Build an `ApplicationTextAsyncLogger` from `AsyncLoggerBuildConfig`. This facade is the application-oriented async builder for the text-console runtime sink shape returned by `build_async_text_logger(...)`.
### Interface
```moonbit
pub fn build_application_text_async_logger(
config : AsyncLoggerBuildConfig,
) -> ApplicationTextAsyncLogger {
```
#### input
- `config : AsyncLoggerBuildConfig` - Combined sync logger config and async queue/runtime config.
#### output
- `ApplicationTextAsyncLogger` - Application-facing async logger backed by `FormattedConsoleSink`.
### Explanation
Detailed rules explaining key parameters and behaviors
- This API delegates to `build_async_text_logger(...)`.
- It is intended for config-driven async text console output where callers want the concrete text sink shape rather than the broader runtime sink enum wrapper.
- The returned logger keeps the usual async lifecycle helpers.
### How to Use
Here are some specific examples provided.
#### When Need An App Async Builder With Text Sink Shape
When async output should stay on text console formatting:
```moonbit
let logger = build_application_text_async_logger(
AsyncLoggerBuildConfig::new(
logger=text_console(target="app.text.async"),
async_config=AsyncLoggerConfig::new(max_pending=4),
),
)
```
In this example, the async logger is built for text-console output specifically.
### Error Case
e.g.:
- If the embedded logger config selects a non-text sink shape, the caller should use the general async builder facade instead.
- If runtime draining is never started, records still follow the normal async queue lifecycle rules.
### Notes
1. This is a narrower text-console async facade than `build_application_async_logger(...)`.
2. It is most useful when callers want the `FormattedConsoleSink`-backed async type explicitly.
+69
View File
@@ -0,0 +1,69 @@
---
name: build-async-text-logger
group: api
category: async
update-time: 20260520
description: Build an async logger with a concrete text-console sink from combined logger and async config.
key-word:
- async
- text
- builder
- public
---
## Build-async-text-logger
Build an async logger directly from `AsyncLoggerBuildConfig`, but keep the concrete sink type as `FormattedConsoleSink` instead of the broader runtime sink wrapper. This helper is the text-console specific counterpart to `build_async_logger(...)`.
### Interface
```moonbit
pub fn build_async_text_logger(config : AsyncLoggerBuildConfig) -> AsyncLogger[@bitlogger.FormattedConsoleSink] {
```
#### input
- `config : AsyncLoggerBuildConfig` - Combined sync logger config plus async queue and flush config.
#### output
- `AsyncLogger[FormattedConsoleSink]` - Config-built async logger backed by a concrete text console sink.
### Explanation
Detailed rules explaining key parameters and behaviors
- This builder converts `config.logger.sink.text_formatter` into a runtime `TextFormatter` and wires it into `text_console_sink(...)`.
- The returned logger inherits `min_level`, `target`, and timestamp behavior from `config.logger`.
- This helper is best suited to text-console output paths where callers want the concrete formatted sink type instead of `RuntimeSink`.
### How to Use
Here are some specific examples provided.
#### When Need Config-built Async Text Console Output
When async queue behavior is config-driven and output should stay on text console formatting:
```moonbit
let logger = build_async_text_logger(
AsyncLoggerBuildConfig::new(
logger=text_console(target="async.text"),
async_config=AsyncLoggerConfig::new(max_pending=4),
),
)
```
In this example, the async logger is built around a text console sink rather than the generic runtime sink enum.
### Error Case
e.g.:
- If the logger config was not intended for text-console style output, the broader `build_async_logger(...)` path may be a better fit.
- If the logger is never `run()`, pending records still follow the normal async queue lifecycle rules.
### Notes
1. This API is narrower than `build_async_logger(...)` because it preserves a concrete text sink type.
2. It is the base builder used by the application and library async text facades.
+71
View File
@@ -0,0 +1,71 @@
---
name: build-library-async-logger
group: api
category: facade
update-time: 20260520
description: Build the library-facing async logger facade from an AsyncLoggerBuildConfig.
key-word:
- library
- async
- facade
- public
---
## Build-library-async-logger
Build a `LibraryAsyncLogger[RuntimeSink]` from `AsyncLoggerBuildConfig`. This is the library-facing async facade over the general config-driven async builder.
### Interface
```moonbit
pub fn build_library_async_logger(
config : AsyncLoggerBuildConfig,
) -> LibraryAsyncLogger[RuntimeSink] {
```
#### input
- `config : AsyncLoggerBuildConfig` - Combined sync logger config and async queue/runtime config.
#### output
- `LibraryAsyncLogger[RuntimeSink]` - Library-facing async logger wrapper.
### Explanation
Detailed rules explaining key parameters and behaviors
- This API builds the general async runtime logger and then wraps it in the narrower `LibraryAsyncLogger` facade.
- The result keeps async lifecycle operations such as `run()` and `shutdown()` while narrowing the public shape.
- `to_async_logger()` can be used to recover the underlying full async logger.
### How to Use
Here are some specific examples provided.
#### When Need A Narrower Async Type For Libraries
When a reusable package should expose async logging but not the full runtime type directly:
```moonbit
let logger = build_library_async_logger(
AsyncLoggerBuildConfig::new(
logger=LoggerConfig::new(target="lib.async"),
async_config=AsyncLoggerConfig::new(max_pending=4),
),
)
```
In this example, async runtime construction is hidden behind the library facade.
### Error Case
e.g.:
- If backend-specific sink limitations exist, they still apply under the facade.
- If callers need methods outside the library facade, they must unwrap with `to_async_logger()`.
### Notes
1. Prefer this API when library boundaries should stay narrow.
2. Use `parse_and_build_library_async_logger(...)` when starting from JSON text.
@@ -0,0 +1,71 @@
---
name: build-library-async-text-logger
group: api
category: facade
update-time: 20260520
description: Build the library-facing text-console async logger facade from an AsyncLoggerBuildConfig.
key-word:
- library
- async
- text
- public
---
## Build-library-async-text-logger
Build a `LibraryAsyncLogger[FormattedConsoleSink]` from `AsyncLoggerBuildConfig`. This facade is the library-oriented async builder for text-console runtime output.
### Interface
```moonbit
pub fn build_library_async_text_logger(
config : AsyncLoggerBuildConfig,
) -> LibraryAsyncLogger[FormattedConsoleSink] {
```
#### input
- `config : AsyncLoggerBuildConfig` - Combined sync logger config and async queue/runtime config.
#### output
- `LibraryAsyncLogger[FormattedConsoleSink]` - Library-facing async logger backed by formatted console output.
### Explanation
Detailed rules explaining key parameters and behaviors
- This API delegates to `build_async_text_logger(...)` and then wraps the result as `LibraryAsyncLogger`.
- It is useful when library code wants a narrow async facade while preserving a concrete text-console sink type.
- `to_async_logger()` can recover the underlying full async logger if needed.
### How to Use
Here are some specific examples provided.
#### When Need A Narrow Async Text Logger For Libraries
When a library wants text-console async output and a narrower public type:
```moonbit
let logger = build_library_async_text_logger(
AsyncLoggerBuildConfig::new(
logger=text_console(target="lib.text.async"),
async_config=AsyncLoggerConfig::new(max_pending=4),
),
)
```
In this example, the async text sink shape is preserved under the library facade.
### Error Case
e.g.:
- If the embedded logger config does not describe text-console output, the caller should use the broader async facade instead.
- Normal async lifecycle expectations still apply if the logger is never run.
### Notes
1. This is the library-side counterpart to `build_application_text_async_logger(...)`.
2. It is most useful when a concrete text-console async sink type matters to the caller boundary.
+66
View File
@@ -0,0 +1,66 @@
---
name: build-library-logger
group: api
category: facade
update-time: 20260520
description: Build the library-facing sync logger facade from a LoggerConfig.
key-word:
- library
- facade
- logger
- public
---
## Build-library-logger
Build a `LibraryLogger[RuntimeSink]` from `LoggerConfig`. This facade keeps a smaller library-oriented sync surface while still using config-driven runtime assembly underneath.
### Interface
```moonbit
pub fn build_library_logger(config : LoggerConfig) -> LibraryLogger[RuntimeSink] {
```
#### input
- `config : LoggerConfig` - Fully assembled sync logger config.
#### output
- `LibraryLogger[RuntimeSink]` - Library-facing logger wrapper over the configured runtime sink.
### Explanation
Detailed rules explaining key parameters and behaviors
- This API builds a configured runtime logger first and then wraps it as `LibraryLogger`.
- The facade intentionally exposes a smaller logging surface than the full configured runtime logger.
- Call `to_logger()` if a caller must recover the underlying full logger object.
### How to Use
Here are some specific examples provided.
#### When Need A Smaller Library-facing Logging Type
When package code should accept or produce a narrower logger facade:
```moonbit
let logger = build_library_logger(
LoggerConfig::new(target="lib", sink=SinkConfig::new(kind=SinkKind::Console)),
)
```
In this example, the logger is built from config and then narrowed to the library facade.
### Error Case
e.g.:
- If backend-specific sink limitations exist, they still apply after the facade is built.
- If code later needs methods outside the library facade, it must unwrap with `to_logger()`.
### Notes
1. Prefer this facade when library APIs should not expose the full configured runtime logger type.
2. Use `parse_and_build_library_logger(...)` when starting from JSON text.
+69
View File
@@ -0,0 +1,69 @@
---
name: callback-sink
group: api
category: sink
update-time: 20260520
description: Create a sink that forwards records to a user callback.
key-word:
- sink
- callback
- record
- public
---
## Callback-sink
Create a sink that forwards each `Record` to a callback. This is the most direct built-in integration hook for tests, adapters, and custom side effects.
### Interface
```moonbit
pub fn callback_sink(callback : (Record) -> Unit) -> CallbackSink {
```
#### input
- `callback : (Record) -> Unit` - Function called for each emitted record.
#### output
- `CallbackSink` - Sink that forwards records to the callback.
### Explanation
Detailed rules explaining key parameters and behaviors
- The callback receives the full structured record.
- This sink is useful for tests, custom bridges, or integration code that wants raw record access.
- Formatting is not applied automatically because the callback works on `Record` values directly.
### How to Use
Here are some specific examples provided.
#### When Need To Capture Structured Records
When tests or adapters want direct access to target, message, and fields:
```moonbit
let logger = Logger::new(
callback_sink(fn(rec) {
println(rec.target)
}),
target="hook",
)
```
In this example, the callback sees the structured record rather than pre-rendered text.
### Error Case
e.g.:
- If text output is needed instead of raw records, use `text_callback_sink(...)`.
- Callback behavior is fully user-defined, so failures inside the callback are outside the sink's own API contract.
### Notes
1. This sink is commonly useful in tests and adapters.
2. It composes naturally with filter, patch, fanout, and queue wrappers.
+64
View File
@@ -0,0 +1,64 @@
---
name: color-mode-label
group: api
category: formatter
update-time: 20260520
description: Convert a ColorMode value into its stable string label.
key-word:
- color
- formatter
- label
- public
---
## Color-mode-label
Convert `ColorMode` into its stable string label. This helper is useful for diagnostics, tests, and config inspection output that should mirror the built-in color mode names.
### Interface
```moonbit
pub fn color_mode_label(mode : ColorMode) -> String {
```
#### input
- `mode : ColorMode` - Color mode enum value to label.
#### output
- `String` - Stable label such as `never`, `auto`, or `always`.
### Explanation
Detailed rules explaining key parameters and behaviors
- The returned strings match the built-in color mode vocabulary.
- This helper is presentation-oriented and does not by itself enable or disable color rendering.
- It is useful when tests or diagnostics should expose the configured color policy clearly.
### How to Use
Here are some specific examples provided.
#### When Need A Readable Color Mode Name
When config or test output should include the current color policy:
```moonbit
let label = color_mode_label(ColorMode::Always)
```
In this example, `label` becomes `"always"`.
### Error Case
e.g.:
- There is no failure path for valid `ColorMode` values.
- If code needs rendering behavior rather than display text, the enum value itself is usually more useful than the label string.
### Notes
1. This helper is mostly useful for readable inspection and assertions.
2. It is a natural companion to `color_support_label(...)`.
+64
View File
@@ -0,0 +1,64 @@
---
name: color-support-label
group: api
category: formatter
update-time: 20260520
description: Convert a ColorSupport value into its stable string label.
key-word:
- color
- formatter
- label
- public
---
## Color-support-label
Convert `ColorSupport` into its stable string label. This helper is useful for diagnostics, tests, and config-oriented output that should mirror the built-in color support names.
### Interface
```moonbit
pub fn color_support_label(support : ColorSupport) -> String {
```
#### input
- `support : ColorSupport` - Color support enum value to label.
#### output
- `String` - Stable label such as `basic` or `truecolor`.
### Explanation
Detailed rules explaining key parameters and behaviors
- The returned strings are stable enum labels used by config and tests.
- This helper is presentation-oriented and does not change formatter behavior by itself.
- It is useful when code should display or assert a readable color support mode.
### How to Use
Here are some specific examples provided.
#### When Need A Readable Color Support Name
When diagnostics or tests should show the selected color capability:
```moonbit
let label = color_support_label(ColorSupport::Basic)
```
In this example, `label` becomes `"basic"`.
### Error Case
e.g.:
- There is no failure path for valid `ColorSupport` values.
- If code needs to choose rendering behavior, the enum value itself is usually more useful than its label string.
### Notes
1. This helper is mostly useful for readable output and assertions.
2. It pairs naturally with config parsing and formatter inspection tests.
+61
View File
@@ -0,0 +1,61 @@
---
name: console-sink
group: api
category: sink
update-time: 20260520
description: Create the built-in plain console sink for synchronous loggers.
key-word:
- sink
- console
- sync
- public
---
## Console-sink
Create the built-in plain console sink. This is the minimal terminal output sink commonly passed to `Logger::new(...)`.
### Interface
```moonbit
pub fn console_sink() -> ConsoleSink {
```
#### output
- `ConsoleSink` - Sink that writes records to the console using default text formatting.
### Explanation
Detailed rules explaining key parameters and behaviors
- This sink writes formatted text to the console.
- It is the simplest built-in sink for direct synchronous logging.
- Use other sink constructors when JSON, custom text formatting, callbacks, routing, or buffering are needed.
### How to Use
Here are some specific examples provided.
#### When Need Minimal Console Output
When a logger only needs default terminal output:
```moonbit
let logger = Logger::new(console_sink(), target="app")
logger.info("ready")
```
In this example, the logger writes directly to the console sink.
### Error Case
e.g.:
- If richer formatting or routing is required, this sink may be too minimal and another sink constructor should be used.
- Console output behavior still depends on the current runtime environment.
### Notes
1. This is the most common sink constructor for simple sync examples.
2. Use `json_console_sink()` or `text_console_sink(...)` when output shape matters more explicitly.
+73
View File
@@ -0,0 +1,73 @@
---
name: console
group: api
category: config
update-time: 20260520
description: Create a minimal logger config preset for the built-in console sink.
key-word:
- preset
- console
- config
- public
---
## Console
Create a `LoggerConfig` preset for the built-in plain console sink. This helper is the shortest way to assemble a config-driven logger that writes directly to the terminal without text formatter customization.
### Interface
```moonbit
pub fn console(
min_level~ : Level = Level::Info,
target~ : String = "",
timestamp~ : Bool = false,
) -> LoggerConfig {
```
#### input
- `min_level : Level` - Minimum enabled level for the preset.
- `target : String` - Default target stored in the returned config.
- `timestamp : Bool` - Whether the built logger should emit timestamps.
#### output
- `LoggerConfig` - Config using `SinkKind::Console` with no queue wrapper by default.
### Explanation
Detailed rules explaining key parameters and behaviors
- This preset always returns `sink.kind=SinkKind::Console`.
- `queue=None` by default, so output remains unqueued unless `with_queue(...)` is applied later.
- This helper only selects the sink shape and top-level logger config fields; it does not build a runtime logger by itself.
### How to Use
Here are some specific examples provided.
#### When Need A Minimal Terminal Config
When application startup wants a simple console config:
```moonbit
let config = console(min_level=Level::Debug, target="cli")
let logger = build_logger(config)
```
In this example, the logger uses the built-in console sink.
And no extra queue or file policy is configured.
### Error Case
e.g.:
- If `target` is empty, the config is still valid.
- If later runtime assembly targets an environment with sink-specific limitations, those runtime rules still apply outside this preset.
### Notes
1. Use this preset when you want the shortest path to console output.
2. Apply `with_queue(...)` separately if bounded synchronous buffering is needed.
+63
View File
@@ -0,0 +1,63 @@
---
name: default-library-logger
group: api
category: facade
update-time: 20260520
description: Create the default library-facing console logger facade from shared global defaults.
key-word:
- library
- facade
- default
- public
---
## Default-library-logger
Create a `LibraryLogger[ConsoleSink]` from the current shared default console logger settings. This is the narrow library-facing counterpart to `default_logger()`.
### Interface
```moonbit
pub fn default_library_logger() -> LibraryLogger[ConsoleSink] {
```
#### output
- `LibraryLogger[ConsoleSink]` - Library-facing console logger built from the current shared defaults.
### Explanation
Detailed rules explaining key parameters and behaviors
- This API wraps `default_logger()` as a library facade.
- Each call reflects the current shared default minimum level and default target at that moment.
- The returned value exposes the narrower `LibraryLogger` surface rather than the full `Logger` surface.
### How to Use
Here are some specific examples provided.
#### When Need A Default Logger But Want A Narrower Facade
When a library should adopt the shared console defaults without exposing the full logger type:
```moonbit
let logger = default_library_logger()
if logger.is_enabled(Level::Info) {
logger.info("ready")
}
```
In this example, the library facade mirrors the current global defaults.
### Error Case
e.g.:
- If the shared default target is empty, the returned logger is still valid.
- Later changes to shared defaults do not mutate an already-created facade value.
### Notes
1. Use `to_logger()` if callers need the full sync logger surface.
2. This helper is useful for library-facing APIs that should stay narrower than `Logger`.
+60
View File
@@ -0,0 +1,60 @@
---
name: default-logger-config
group: api
category: config
update-time: 20260520
description: Create the default LoggerConfig used by config-driven logger builders.
key-word:
- logger
- config
- default
- public
---
## Default-logger-config
Create the default `LoggerConfig` used by config-driven logger builders. This helper is useful when callers want the library's baseline top-level config value explicitly before customization.
### Interface
```moonbit
pub fn default_logger_config() -> LoggerConfig {
```
#### output
- `LoggerConfig` - Default top-level logger config.
### Explanation
Detailed rules explaining key parameters and behaviors
- This helper returns the same baseline config shape used by `LoggerConfig::new()` defaults.
- It includes the default minimum level, empty target, disabled timestamp, default sink config, and no queue wrapper.
- It is useful for explicit config composition when callers want a known baseline object.
### How to Use
Here are some specific examples provided.
#### When Need A Baseline Top-level Config Value
When config code prefers to start from the full default object:
```moonbit
let config = default_logger_config()
```
In this example, the default logger config is available as a concrete value for later adjustment or serialization.
### Error Case
e.g.:
- There is no failure path for retrieving the default config value.
- If runtime construction is needed immediately, pass the config to `build_logger(...)` or use a runtime logger constructor directly.
### Notes
1. This helper is the top-level default config entry point.
2. It is useful for explicit config-first workflows and tests.
+60
View File
@@ -0,0 +1,60 @@
---
name: default-sink-config
group: api
category: config
update-time: 20260520
description: Create the default SinkConfig used by logger config helpers.
key-word:
- sink
- config
- default
- public
---
## Default-sink-config
Create the default `SinkConfig` used by logger config helpers. This helper is useful when callers want the library's baseline sink config value explicitly.
### Interface
```moonbit
pub fn default_sink_config() -> SinkConfig {
```
#### output
- `SinkConfig` - Default sink config value.
### Explanation
Detailed rules explaining key parameters and behaviors
- This helper returns the same baseline sink config used by `LoggerConfig::new(...)` defaults.
- The default sink config is console-oriented unless later replaced by explicit config composition.
- It is a typed config object, not a runtime sink instance.
### How to Use
Here are some specific examples provided.
#### When Need An Explicit Baseline Sink Config
When config assembly wants the default sink as a concrete value:
```moonbit
let sink = default_sink_config()
```
In this example, the baseline sink config can be reused or embedded explicitly.
### Error Case
e.g.:
- There is no failure path for retrieving the default sink config.
- If a real sink instance is required for direct `Logger::new(...)` code paths, use a sink constructor such as `console_sink()` instead.
### Notes
1. This helper is for config-driven assembly, not runtime sink wiring.
2. It is most useful in explicit config composition code.
+60
View File
@@ -0,0 +1,60 @@
---
name: default-style-tag-registry
group: api
category: formatter
update-time: 20260520
description: Create the built-in style tag registry containing default semantic and color tags.
key-word:
- style
- registry
- default
- public
---
## Default-style-tag-registry
Create the built-in `StyleTagRegistry` containing the library's default style tags. This helper is useful when callers want to start from the standard tag set and then extend or override it.
### Interface
```moonbit
pub fn default_style_tag_registry() -> StyleTagRegistry {
```
#### output
- `StyleTagRegistry` - Registry seeded with built-in tags and aliases.
### Explanation
Detailed rules explaining key parameters and behaviors
- The returned registry contains the library's built-in style tag set.
- Callers can extend or override entries before attaching the registry to a formatter.
- This helper is useful when custom tags should coexist with built-in semantic tags.
### How to Use
Here are some specific examples provided.
#### When Need Built-in Tags Plus Local Overrides
When a formatter should keep defaults but customize one tag:
```moonbit
let tags = default_style_tag_registry().set_tag("success", fg=Some("#00ffaa"), underline=true)
```
In this example, the built-in registry is reused and then locally overridden.
### Error Case
e.g.:
- If the formatter disables style markup, built-in tags may not be applied even though the registry exists.
- If callers need a clean registry with no built-in tags, use `style_tag_registry()` instead.
### Notes
1. This helper is the easiest way to preserve the standard tag vocabulary.
2. It is commonly paired with `text_formatter(...).with_style_tags(...)`.
+60
View File
@@ -0,0 +1,60 @@
---
name: default-text-formatter-config
group: api
category: config
update-time: 20260520
description: Create the default TextFormatterConfig used by config and preset helpers.
key-word:
- formatter
- config
- default
- public
---
## Default-text-formatter-config
Create the default `TextFormatterConfig` used by config builders and presets. This helper is useful when callers want the library default config value explicitly rather than relying on optional parameter defaults.
### Interface
```moonbit
pub fn default_text_formatter_config() -> TextFormatterConfig {
```
#### output
- `TextFormatterConfig` - Default serializable formatter config value.
### Explanation
Detailed rules explaining key parameters and behaviors
- This helper returns the same baseline config value used by `text_console(...)`, `file(...)`, and `SinkConfig::new(...)` defaults.
- It is a config object, not a runtime `TextFormatter`.
- Call `to_formatter()` when a runtime formatter is needed.
### How to Use
Here are some specific examples provided.
#### When Need To Start From The Default Formatter Config Explicitly
When config code wants the baseline value before adjusting fields:
```moonbit
let base = default_text_formatter_config()
```
In this example, the default formatter config is available as a concrete value instead of an implicit optional argument default.
### Error Case
e.g.:
- There is no failure path for retrieving the default config value.
- If a runtime formatter is required immediately, this helper alone is not enough because it returns config rather than a formatter object.
### Notes
1. Use this helper for explicit config composition.
2. Use `text_formatter(...)` when you want a runtime formatter directly.
+68
View File
@@ -0,0 +1,68 @@
---
name: fanout-sink
group: api
category: sink
update-time: 20260520
description: Create a sink that writes each record to two sinks.
key-word:
- sink
- fanout
- routing
- public
---
## Fanout-sink
Create a sink that writes every record to two underlying sinks. This helper is useful when the same log stream should be duplicated across multiple destinations.
### Interface
```moonbit
pub fn[A, B] fanout_sink(left : A, right : B) -> FanoutSink[A, B] {
```
#### input
- `left : A` - First sink destination.
- `right : B` - Second sink destination.
#### output
- `FanoutSink[A, B]` - Sink that duplicates records to both destinations.
### Explanation
Detailed rules explaining key parameters and behaviors
- Each record is written to both sinks.
- The right side receives a copied record so the two writes remain independent at the record value level.
- This helper is useful for combinations such as console plus JSON, or console plus callback capture.
### How to Use
Here are some specific examples provided.
#### When Need Dual Output Paths
When the same records should go to both human and machine-oriented outputs:
```moonbit
let logger = Logger::new(
fanout_sink(console_sink(), json_console_sink()),
target="dual",
)
```
In this example, each record is written to both console sinks.
### Error Case
e.g.:
- If only one path should receive a record conditionally, use `split_sink(...)` instead.
- Sink-specific failures still follow the behavior of the wrapped sinks.
### Notes
1. This helper is a duplication primitive, not a conditional router.
2. It is often useful in examples and test capture setups.
+100
View File
@@ -0,0 +1,100 @@
---
name: file
group: api
category: config
update-time: 20260520
description: Create a logger config preset for the built-in file sink with optional rotation and formatter settings.
key-word:
- preset
- file
- rotation
- public
---
## File
Create a `LoggerConfig` preset for the built-in file sink. This helper packages file path, append policy, auto-flush behavior, optional rotation, and text formatter settings into a single config object for runtime logger assembly.
### Interface
```moonbit
pub fn file(
path : String,
min_level~ : Level = Level::Info,
target~ : String = "",
timestamp~ : Bool = false,
append~ : Bool = true,
auto_flush~ : Bool = true,
rotation~ : FileRotation? = None,
text_formatter~ : TextFormatterConfig = default_text_formatter_config(),
) -> LoggerConfig raise ConfigError {
```
#### input
- `path : String` - Output file path for the file sink.
- `min_level : Level` - Minimum enabled level for the preset.
- `target : String` - Default target stored in the returned config.
- `timestamp : Bool` - Whether the built logger should emit timestamps.
- `append : Bool` - Whether file writes should append instead of truncate-on-open behavior.
- `auto_flush : Bool` - Whether writes should be flushed automatically.
- `rotation : FileRotation?` - Optional size-based file rotation policy.
- `text_formatter : TextFormatterConfig` - Formatter config used for file text rendering.
#### output
- `LoggerConfig` - Config using `SinkKind::File` and the supplied file-specific settings.
### Explanation
Detailed rules explaining key parameters and behaviors
- This preset always returns `sink.kind=SinkKind::File`.
- `path` must be non-empty.
- `rotation=None` leaves file rotation disabled until a rotation policy is provided directly or through `with_file_rotation(...)`.
- `queue=None` by default, so buffering is opt-in through `with_queue(...)`.
### How to Use
Here are some specific examples provided.
#### When Need A Config-driven File Logger
When application boot wants typed file logging config:
```moonbit
let config = file(
"service.log",
min_level=Level::Warn,
append=true,
auto_flush=true,
)
```
In this example, the returned config targets the built-in file sink.
And rotation remains disabled until configured.
#### When Need File Config With Rotation Prepared Up Front
When file retention should be included immediately:
```moonbit
let config = file(
"service.log",
rotation=Some(file_rotation(1024 * 1024, max_backups=3)),
)
```
In this example, the file preset carries both path and rotation policy.
### Error Case
e.g.:
- If `path` is empty, this preset raises `ConfigError::InvalidConfig("File sink requires non-empty path")`.
- If you need console output instead of file output, use a console preset because file-only settings are meaningful only for `SinkKind::File`.
### Notes
1. This is the only preset in this set that accepts file path and file rotation settings directly.
2. `with_file_rotation(...)` is designed to extend this preset or any other file-based `LoggerConfig`.
+67
View File
@@ -0,0 +1,67 @@
---
name: filter-sink
group: api
category: sink
update-time: 20260520
description: Create a sink that forwards only records matching a predicate.
key-word:
- sink
- filter
- predicate
- public
---
## Filter-sink
Create a sink that forwards only records matching a predicate. This helper is the sink-level counterpart to `Logger::with_filter(...)`.
### Interface
```moonbit
pub fn[S] filter_sink(sink : S, predicate : (Record) -> Bool) -> FilterSink[S] {
```
#### input
- `sink : S` - Wrapped sink receiving records that pass the predicate.
- `predicate : (Record) -> Bool` - Filtering function evaluated for each record.
#### output
- `FilterSink[S]` - Predicate-filtering sink.
### Explanation
Detailed rules explaining key parameters and behaviors
- Only records for which the predicate returns `true` are forwarded.
- This helper is useful for sink-first composition and explicit routing graphs.
- Use the logger-level helper when the filter should be attached after `Logger::new(...)` instead.
### How to Use
Here are some specific examples provided.
#### When Need Predicate Filtering At Sink Construction Time
When a sink graph should reject records before they reach the destination:
```moonbit
let sink = filter_sink(console_sink(), fn(rec) {
rec.target == "kept"
})
```
In this example, only matching records are forwarded to the wrapped sink.
### Error Case
e.g.:
- If record selection should be attached to an already-built logger, use `with_filter(...)` instead.
- Predicate logic is caller-defined, so accidental over-filtering comes from predicate choice.
### Notes
1. This helper is useful for sink-first composition graphs.
2. It works well with predicate builders such as `target_is(...)` and `all_of(...)`.
+62
View File
@@ -0,0 +1,62 @@
---
name: global-style-tag-registry
group: api
category: formatter
update-time: 20260520
description: Read the shared global style tag registry used by formatters without local tag overrides.
key-word:
- style
- registry
- global
- public
---
## Global-style-tag-registry
Read the shared global `StyleTagRegistry`. This helper is useful when code wants to inspect or reuse the current process-wide formatter tag registry.
### Interface
```moonbit
pub fn global_style_tag_registry() -> StyleTagRegistry {
```
#### output
- `StyleTagRegistry` - Current shared global style tag registry.
### Explanation
Detailed rules explaining key parameters and behaviors
- This helper reads the shared global registry used when a formatter has no local registry override.
- It is useful for inspection, temporary replacement, or restoration workflows.
- Local formatter registries still take priority over the global registry.
### How to Use
Here are some specific examples provided.
#### When Need To Snapshot And Restore Global Tags
When tests or boot code temporarily replace the shared registry:
```moonbit
let previous = global_style_tag_registry()
set_global_style_tag_registry(style_tag_registry().set_tag("accent", fg=Some("#123456")))
set_global_style_tag_registry(previous)
```
In this example, the current global registry is captured and later restored.
### Error Case
e.g.:
- If a formatter already has local style tags, global tag changes may not affect that formatter.
- This helper only reads the shared registry; it does not mutate it.
### Notes
1. Use `set_global_style_tag_registry(...)` or `reset_global_style_tag_registry()` for mutation.
2. Global registry changes should be made carefully because they affect subsequent formatting without local overrides.
+49
View File
@@ -43,6 +43,9 @@ BitLogger API navigation.
- [set-default-min-level.md](./set-default-min-level.md)
- [set-default-target.md](./set-default-target.md)
- [default-logger.md](./default-logger.md)
- [global-style-tag-registry.md](./global-style-tag-registry.md)
- [set-global-style-tag-registry.md](./set-global-style-tag-registry.md)
- [reset-global-style-tag-registry.md](./reset-global-style-tag-registry.md)
- [global-log.md](./global-log.md)
- [global-trace.md](./global-trace.md)
- [global-debug.md](./global-debug.md)
@@ -53,10 +56,17 @@ BitLogger API navigation.
## Formatter and fields
- [field.md](./field.md)
- [text-style.md](./text-style.md)
- [style-tag-registry.md](./style-tag-registry.md)
- [default-style-tag-registry.md](./default-style-tag-registry.md)
- [text-formatter.md](./text-formatter.md)
- [text-formatter-config.md](./text-formatter-config.md)
- [default-text-formatter-config.md](./default-text-formatter-config.md)
- [text-formatter-config-to-json.md](./text-formatter-config-to-json.md)
- [stringify-text-formatter-config.md](./stringify-text-formatter-config.md)
- [color-support-label.md](./color-support-label.md)
- [style-markup-mode-label.md](./style-markup-mode-label.md)
- [color-mode-label.md](./color-mode-label.md)
- [fields.md](./fields.md)
## Record and level
@@ -68,6 +78,18 @@ BitLogger API navigation.
## Sink and file
- [console-sink.md](./console-sink.md)
- [json-console-sink.md](./json-console-sink.md)
- [text-console-sink.md](./text-console-sink.md)
- [callback-sink.md](./callback-sink.md)
- [text-callback-sink.md](./text-callback-sink.md)
- [fanout-sink.md](./fanout-sink.md)
- [split-sink.md](./split-sink.md)
- [split-by-level.md](./split-by-level.md)
- [buffered-sink.md](./buffered-sink.md)
- [queued-sink.md](./queued-sink.md)
- [filter-sink.md](./filter-sink.md)
- [patch-sink.md](./patch-sink.md)
- [file-sink.md](./file-sink.md)
- [file-rotation.md](./file-rotation.md)
- [file-sink-policy-to-json.md](./file-sink-policy-to-json.md)
@@ -104,13 +126,24 @@ BitLogger API navigation.
- [queue-config.md](./queue-config.md)
- [queue-config-to-json.md](./queue-config-to-json.md)
- [stringify-queue-config.md](./stringify-queue-config.md)
- [default-sink-config.md](./default-sink-config.md)
- [logger-config.md](./logger-config.md)
- [default-logger-config.md](./default-logger-config.md)
- [logger-config-to-json.md](./logger-config-to-json.md)
- [stringify-logger-config.md](./stringify-logger-config.md)
- [parse-logger-config-text.md](./parse-logger-config-text.md)
- [build-logger.md](./build-logger.md)
- [parse-and-build-logger.md](./parse-and-build-logger.md)
## Presets
- [console.md](./console.md)
- [json-console.md](./json-console.md)
- [text-console.md](./text-console.md)
- [file.md](./file.md)
- [with-queue.md](./with-queue.md)
- [with-file-rotation.md](./with-file-rotation.md)
## Async logger
- [async-logger.md](./async-logger.md)
@@ -157,13 +190,29 @@ BitLogger API navigation.
- [async-runtime-state-to-json.md](./async-runtime-state-to-json.md)
- [stringify-async-runtime-state.md](./stringify-async-runtime-state.md)
- [async-logger-config.md](./async-logger-config.md)
- [parse-async-logger-config-text.md](./parse-async-logger-config-text.md)
- [async-logger-config-to-json.md](./async-logger-config-to-json.md)
- [stringify-async-logger-config.md](./stringify-async-logger-config.md)
- [parse-async-logger-build-config-text.md](./parse-async-logger-build-config-text.md)
- [build-async-logger.md](./build-async-logger.md)
- [build-async-text-logger.md](./build-async-text-logger.md)
- [async-logger-build-config-to-json.md](./async-logger-build-config-to-json.md)
- [stringify-async-logger-build-config.md](./stringify-async-logger-build-config.md)
## Application and library facades
- [build-application-logger.md](./build-application-logger.md)
- [parse-and-build-application-logger.md](./parse-and-build-application-logger.md)
- [build-library-logger.md](./build-library-logger.md)
- [parse-and-build-library-logger.md](./parse-and-build-library-logger.md)
- [default-library-logger.md](./default-library-logger.md)
- [build-application-async-logger.md](./build-application-async-logger.md)
- [build-application-text-async-logger.md](./build-application-text-async-logger.md)
- [parse-and-build-application-async-logger.md](./parse-and-build-application-async-logger.md)
- [build-library-async-logger.md](./build-library-async-logger.md)
- [build-library-async-text-logger.md](./build-library-async-text-logger.md)
- [parse-and-build-library-async-logger.md](./parse-and-build-library-async-logger.md)
## Configured logger runtime
- [configured-logger-flush.md](./configured-logger-flush.md)
+61
View File
@@ -0,0 +1,61 @@
---
name: json-console-sink
group: api
category: sink
update-time: 20260520
description: Create the built-in JSON console sink for structured synchronous logging.
key-word:
- sink
- json
- console
- public
---
## Json-console-sink
Create the built-in JSON console sink. This sink writes records as structured JSON text and is useful when stdout is consumed by machines rather than humans.
### Interface
```moonbit
pub fn json_console_sink() -> JsonConsoleSink {
```
#### output
- `JsonConsoleSink` - Sink that writes records to the console as JSON.
### Explanation
Detailed rules explaining key parameters and behaviors
- This sink emits JSON rather than human-focused text formatting.
- It is a direct synchronous sink that can be passed to `Logger::new(...)`.
- Use it when structured logs should be parsed, shipped, or inspected programmatically.
### How to Use
Here are some specific examples provided.
#### When Need Structured Stdout Logs
When log consumers expect machine-readable output:
```moonbit
let logger = Logger::new(json_console_sink(), target="api")
logger.info("ready", fields=[field("service", "bitlogger")])
```
In this example, the emitted console line is JSON-shaped.
### Error Case
e.g.:
- If human-focused text formatting is required, use a text console sink instead.
- Console output still depends on the current runtime environment.
### Notes
1. This sink is useful for structured stdout pipelines.
2. It pairs naturally with field-heavy logging.
+73
View File
@@ -0,0 +1,73 @@
---
name: json-console
group: api
category: config
update-time: 20260520
description: Create a logger config preset for the built-in JSON console sink.
key-word:
- preset
- json
- console
- public
---
## Json-console
Create a `LoggerConfig` preset for structured JSON output on the console. This preset is intended for config-driven logging pipelines that want machine-readable terminal output.
### Interface
```moonbit
pub fn json_console(
min_level~ : Level = Level::Info,
target~ : String = "",
timestamp~ : Bool = false,
) -> LoggerConfig {
```
#### input
- `min_level : Level` - Minimum enabled level for the preset.
- `target : String` - Default target stored in the returned config.
- `timestamp : Bool` - Whether emitted records should include timestamps.
#### output
- `LoggerConfig` - Config using `SinkKind::JsonConsole` with no queue wrapper by default.
### Explanation
Detailed rules explaining key parameters and behaviors
- This preset always returns `sink.kind=SinkKind::JsonConsole`.
- `queue=None` by default, so JSON records are not buffered unless `with_queue(...)` is added later.
- The preset does not carry file-only fields such as `path` or `rotation` in a meaningful way because the sink kind is not file-based.
### How to Use
Here are some specific examples provided.
#### When Need Structured Console Output
When logs should be easy to collect or parse from stdout:
```moonbit
let config = json_console(min_level=Level::Info, target="api", timestamp=true)
let logger = build_logger(config)
```
In this example, records are emitted through the JSON console sink.
And timestamps are enabled at the top-level logger config.
### Error Case
e.g.:
- If `target` is empty, the preset still returns a valid config.
- If you need file rotation or file paths, this preset is the wrong sink shape and should be replaced with `file(...)`.
### Notes
1. Use this preset when downstream tooling expects structured console logs.
2. `with_file_rotation(...)` does not change this preset because it only applies to file configs.
@@ -0,0 +1,68 @@
---
name: parse-and-build-application-async-logger
group: api
category: facade
update-time: 20260520
description: Parse JSON async build config text and build the application-facing async logger facade.
key-word:
- application
- async
- parse
- public
---
## Parse-and-build-application-async-logger
Parse raw JSON async build config text and build an `ApplicationAsyncLogger` in one step. This facade is the application-oriented counterpart to `parse_async_logger_build_config_text(...)` plus `build_application_async_logger(...)`.
### Interface
```moonbit
pub fn parse_and_build_application_async_logger(
input : String,
) -> ApplicationAsyncLogger raise {
```
#### input
- `input : String` - Raw JSON async logger build config text.
#### output
- `ApplicationAsyncLogger` - Application-facing async runtime logger.
### Explanation
Detailed rules explaining key parameters and behaviors
- This API parses async build config text first, then builds the async application facade.
- Both the embedded sync logger config and async queue/runtime config are validated by the parser layer.
- The returned logger keeps the normal async lifecycle and state helpers.
### How to Use
Here are some specific examples provided.
#### When Need App Async Boot Directly From JSON
When async config is sourced as text and should become a running-capable facade immediately:
```moonbit
let logger = parse_and_build_application_async_logger(
"{\"logger\":{\"target\":\"app.async\",\"sink\":{\"kind\":\"console\"}},\"async_config\":{\"max_pending\":4,\"overflow\":\"DropNewest\",\"max_batch\":1,\"linger_ms\":0,\"flush\":\"Never\"}}",
)
```
In this example, text parsing and async logger construction happen in one facade call.
### Error Case
e.g.:
- If the JSON text is malformed, parsing raises an error.
- If the embedded config is invalid, parsing raises before the async facade is returned.
### Notes
1. Use this facade when application boot starts from JSON text.
2. Use `build_application_async_logger(...)` when config is already typed as `AsyncLoggerBuildConfig`.
@@ -0,0 +1,68 @@
---
name: parse-and-build-application-logger
group: api
category: facade
update-time: 20260520
description: Parse JSON logger config text and build the application-facing sync logger facade.
key-word:
- application
- facade
- parse
- public
---
## Parse-and-build-application-logger
Parse raw JSON config text and build an `ApplicationLogger` in one step. This facade is the application-oriented counterpart to `parse_and_build_logger(...)`.
### Interface
```moonbit
pub fn parse_and_build_application_logger(
input : String,
) -> ApplicationLogger raise ConfigError {
```
#### input
- `input : String` - Raw JSON logger config text.
#### output
- `ApplicationLogger` - Application-facing configured runtime logger built from parsed config.
### Explanation
Detailed rules explaining key parameters and behaviors
- This API delegates to `parse_and_build_logger(...)`.
- JSON parsing and config validation happen before the logger is built.
- The returned logger keeps the same queue and file helper surface as other configured sync runtime loggers.
### How to Use
Here are some specific examples provided.
#### When Need Direct JSON Bootstrapping For Applications
When config is stored as text and should be built immediately:
```moonbit
let logger = parse_and_build_application_logger(
"{\"min_level\":\"warn\",\"target\":\"app\",\"sink\":{\"kind\":\"console\"}}",
)
```
In this example, parsing and runtime construction are combined into one facade call.
### Error Case
e.g.:
- If the JSON is malformed, a `ConfigError` is raised.
- If the parsed config is invalid, such as an empty file sink path, a `ConfigError` is raised.
### Notes
1. Use this facade when application code wants a text-to-runtime entry point.
2. Use `build_application_logger(...)` when the config is already typed as `LoggerConfig`.
@@ -0,0 +1,68 @@
---
name: parse-and-build-library-async-logger
group: api
category: facade
update-time: 20260520
description: Parse JSON async build config text and build the library-facing async logger facade.
key-word:
- library
- async
- parse
- public
---
## Parse-and-build-library-async-logger
Parse raw JSON async build config text and build a `LibraryAsyncLogger[RuntimeSink]` in one step. This facade is the text-driven library counterpart to the general async config parser plus builder flow.
### Interface
```moonbit
pub fn parse_and_build_library_async_logger(
input : String,
) -> LibraryAsyncLogger[RuntimeSink] raise {
```
#### input
- `input : String` - Raw JSON async logger build config text.
#### output
- `LibraryAsyncLogger[RuntimeSink]` - Library-facing async runtime logger wrapper.
### Explanation
Detailed rules explaining key parameters and behaviors
- This API parses async build config text, validates it, builds the async runtime logger, and narrows it to the library facade.
- The resulting facade keeps async lifecycle helpers while exposing a smaller public surface.
- `to_async_logger()` can recover the underlying async logger when a wider API is required.
### How to Use
Here are some specific examples provided.
#### When Need Text-driven Async Library Bootstrapping
When a package accepts raw config text but wants a narrower async facade output:
```moonbit
let logger = parse_and_build_library_async_logger(
"{\"logger\":{\"target\":\"lib.async\",\"sink\":{\"kind\":\"console\"}},\"async_config\":{\"max_pending\":4,\"overflow\":\"DropNewest\",\"max_batch\":1,\"linger_ms\":0,\"flush\":\"Never\"}}",
)
```
In this example, parsing and library-facade construction happen together.
### Error Case
e.g.:
- If the JSON text is malformed, parsing raises an error.
- If the embedded config is invalid, parsing raises before the library facade is returned.
### Notes
1. This is the narrow async library parse-and-build facade.
2. Use `build_library_async_logger(...)` when the config is already typed.
@@ -0,0 +1,68 @@
---
name: parse-and-build-library-logger
group: api
category: facade
update-time: 20260520
description: Parse JSON logger config text and build the library-facing sync logger facade.
key-word:
- library
- facade
- parse
- public
---
## Parse-and-build-library-logger
Parse raw JSON config text and build a `LibraryLogger[RuntimeSink]` in one step. This facade is the text-driven library counterpart to `parse_and_build_logger(...)`.
### Interface
```moonbit
pub fn parse_and_build_library_logger(
input : String,
) -> LibraryLogger[RuntimeSink] raise ConfigError {
```
#### input
- `input : String` - Raw JSON logger config text.
#### output
- `LibraryLogger[RuntimeSink]` - Library-facing wrapper around the configured runtime logger.
### Explanation
Detailed rules explaining key parameters and behaviors
- This API parses config text, validates it, builds the configured logger, and wraps it as a library facade.
- The returned facade keeps a narrower surface than the underlying configured logger.
- `to_logger()` can be used to recover the underlying full logger object when necessary.
### How to Use
Here are some specific examples provided.
#### When Need Text-driven Library Bootstrapping
When a reusable package wants config text input but a narrow logger facade output:
```moonbit
let logger = parse_and_build_library_logger(
"{\"min_level\":\"warn\",\"target\":\"lib\",\"sink\":{\"kind\":\"console\"}}",
)
```
In this example, parsing and library-facade construction happen in one call.
### Error Case
e.g.:
- If the JSON text is malformed, a `ConfigError` is raised.
- If the parsed config is invalid, a `ConfigError` is raised before the library facade is returned.
### Notes
1. This is the narrow library-oriented parse-and-build sync entry point.
2. Use `build_library_logger(...)` when the config is already typed.
@@ -0,0 +1,66 @@
---
name: parse-async-logger-config-text
group: api
category: async
update-time: 20260520
description: Parse JSON text into an AsyncLoggerConfig.
key-word:
- async
- config
- parse
- public
---
## Parse-async-logger-config-text
Parse raw JSON text into `AsyncLoggerConfig`. This helper is the text-entry counterpart to `AsyncLoggerConfig::new(...)` when async queue and flush policy should be configured from serialized data.
### Interface
```moonbit
pub fn parse_async_logger_config_text(input : String) -> AsyncLoggerConfig raise {
```
#### input
- `input : String` - Raw JSON text describing async queue, batching, linger, and flush settings.
#### output
- `AsyncLoggerConfig` - Parsed async runtime config value.
### Explanation
Detailed rules explaining key parameters and behaviors
- This helper parses only the async config portion, not the embedded sync `LoggerConfig`.
- Parsed values follow the same normalization and enum validation rules as other async config helpers.
- Use this API when async policy comes from config text but sink choice is still assembled elsewhere.
### How to Use
Here are some specific examples provided.
#### When Need Async Queue Policy From JSON
When async queue behavior is loaded separately from sync sink config:
```moonbit
let config = parse_async_logger_config_text(
"{\"max_pending\":8,\"overflow\":\"DropOldest\",\"max_batch\":3,\"linger_ms\":25,\"flush\":\"Batch\"}",
)
```
In this example, async queue and flush policy are parsed from text into a typed config object.
### Error Case
e.g.:
- If the JSON text is malformed, parsing raises an error.
- If enum names such as `overflow` or `flush` are unsupported, parsing raises an error.
### Notes
1. Use this helper when only async policy is text-driven.
2. Use `parse_async_logger_build_config_text(...)` when both sync logger config and async config come from the same JSON payload.
+66
View File
@@ -0,0 +1,66 @@
---
name: patch-sink
group: api
category: sink
update-time: 20260520
description: Create a sink that rewrites records with a patch before forwarding them.
key-word:
- sink
- patch
- record
- public
---
## Patch-sink
Create a sink that rewrites each record with a `RecordPatch` before forwarding it to another sink. This helper is the sink-level counterpart to `Logger::with_patch(...)`.
### Interface
```moonbit
pub fn[S] patch_sink(sink : S, patch : RecordPatch) -> PatchSink[S] {
```
#### input
- `sink : S` - Wrapped sink receiving patched records.
- `patch : RecordPatch` - Record transformation applied before forwarding.
#### output
- `PatchSink[S]` - Patch-applying sink.
### Explanation
Detailed rules explaining key parameters and behaviors
- Each record is transformed before it reaches the wrapped sink.
- This helper is useful for sink-first composition graphs and adapters.
- Use the logger-level helper when the patch should be attached after `Logger::new(...)` instead.
### How to Use
Here are some specific examples provided.
#### When Need Record Rewriting At Sink Construction Time
When sanitization or target rewriting should happen before sink delivery:
```moonbit
let sink = patch_sink(console_sink(), prefix_message("[safe] "))
let logger = Logger::new(sink, target="auth")
```
In this example, records are patched before they are written to the wrapped sink.
### Error Case
e.g.:
- If patching should be attached to an already-built logger, use `with_patch(...)` instead.
- Patch behavior is defined by the supplied function, so semantic mistakes come from patch logic rather than sink mechanics.
### Notes
1. This helper is useful for explicit sink graphs.
2. It composes naturally with helpers such as `compose_patches(...)`.
+71
View File
@@ -0,0 +1,71 @@
---
name: queued-sink
group: api
category: sink
update-time: 20260520
description: Create a sink that buffers records in an explicit queue with overflow policy control.
key-word:
- sink
- queue
- overflow
- public
---
## Queued-sink
Create a sink that stores records in an explicit synchronous queue before forwarding them to another sink. This is the low-level sink constructor behind queue-style sync buffering behavior.
### Interface
```moonbit
pub fn[S] queued_sink(
sink : S,
max_pending~ : Int = 0,
overflow~ : QueueOverflowPolicy = QueueOverflowPolicy::DropNewest,
) -> QueuedSink[S] {
```
#### input
- `sink : S` - Wrapped sink receiving drained records.
- `max_pending : Int` - Queue capacity before overflow policy applies. `0` means unbounded.
- `overflow : QueueOverflowPolicy` - Overflow behavior such as `DropNewest` or `DropOldest`.
#### output
- `QueuedSink[S]` - Explicit queue sink with `pending_count()`, `dropped_count()`, `drain()`, and `flush()` helpers.
### Explanation
Detailed rules explaining key parameters and behaviors
- Records are enqueued first and only reach the wrapped sink when drained or flushed.
- `max_pending=0` means the queue is not bounded by capacity.
- Overflow behavior controls whether the incoming record is dropped or the oldest queued record is replaced.
### How to Use
Here are some specific examples provided.
#### When Need Low-level Explicit Queue Composition
When a sync logger should own a queue sink directly:
```moonbit
let sink = queued_sink(console_sink(), max_pending=2, overflow=QueueOverflowPolicy::DropOldest)
let logger = Logger::new(sink, target="queue")
```
In this example, records stay pending until the queue is drained or flushed.
### Error Case
e.g.:
- If the queue is bounded too tightly, overflow may drop records during bursts.
- If callers never drain or flush, queued records remain pending.
### Notes
1. This helper is the sink-level alternative to `Logger::with_queue(...)` and config-side `with_queue(...)`.
2. Use the higher-level APIs when you do not need to manipulate the queue sink directly.
@@ -0,0 +1,57 @@
---
name: reset-global-style-tag-registry
group: api
category: formatter
update-time: 20260520
description: Reset the shared global style tag registry back to the library default.
key-word:
- style
- registry
- reset
- public
---
## Reset-global-style-tag-registry
Reset the shared global `StyleTagRegistry` back to the library default. This helper is useful after temporary global overrides in tests or boot code.
### Interface
```moonbit
pub fn reset_global_style_tag_registry() -> Unit {
```
### Explanation
Detailed rules explaining key parameters and behaviors
- This helper restores the shared global registry to the built-in default tag set.
- It is the simplest cleanup path after temporary global registry customization.
- Local formatter registries remain unaffected because they are not sourced from the shared global registry.
### How to Use
Here are some specific examples provided.
#### When Need To Undo Temporary Global Overrides
When tests or boot code changed the shared registry and want the default back:
```moonbit
set_global_style_tag_registry(style_tag_registry().set_tag("accent", fg=Some("#123456")))
reset_global_style_tag_registry()
```
In this example, subsequent formatters without local registries return to the library default global tag set.
### Error Case
e.g.:
- If a formatter already carries local tags, resetting the global registry does not change that formatter's local tag behavior.
- This helper only resets shared global formatter state; it does not alter serialized formatter configs.
### Notes
1. This is the cleanup helper paired with `set_global_style_tag_registry(...)`.
2. It is especially useful in tests that temporarily customize shared style behavior.
+62
View File
@@ -0,0 +1,62 @@
---
name: set-global-style-tag-registry
group: api
category: formatter
update-time: 20260520
description: Replace the shared global style tag registry used by formatters without local overrides.
key-word:
- style
- registry
- global
- public
---
## Set-global-style-tag-registry
Replace the shared global `StyleTagRegistry`. This helper changes the default tag source used by formatters that do not define their own local registry.
### Interface
```moonbit
pub fn set_global_style_tag_registry(registry : StyleTagRegistry) -> Unit {
```
#### input
- `registry : StyleTagRegistry` - New shared global registry value.
### Explanation
Detailed rules explaining key parameters and behaviors
- This helper mutates process-wide shared formatter state.
- Only formatters without local style tags read from the global registry.
- It is useful for centralized theming or tests that need predictable tag behavior.
### How to Use
Here are some specific examples provided.
#### When Need A Process-wide Custom Tag Default
When multiple formatters should share a custom default tag registry:
```moonbit
set_global_style_tag_registry(
style_tag_registry().set_tag("accent", fg=Some("#102030"), dim=true),
)
```
In this example, subsequent formatters without local tag registries can resolve `accent` from the shared global registry.
### Error Case
e.g.:
- If local formatter tags are already present, those local tags still take priority.
- Global mutation can affect later formatting broadly, so test code should usually restore or reset after temporary changes.
### Notes
1. Use `global_style_tag_registry()` to snapshot the previous value before replacement.
2. Use `reset_global_style_tag_registry()` to restore the library default global registry.
+72
View File
@@ -0,0 +1,72 @@
---
name: split-by-level
group: api
category: sink
update-time: 20260520
description: Create a split sink that routes records by minimum enabled level.
key-word:
- sink
- split
- level
- public
---
## Split-by-level
Create a split sink that routes records to one of two sinks based on whether the record level is enabled at a given minimum level.
### Interface
```moonbit
pub fn[A, B] split_by_level(
left : A,
right : B,
min_level~ : Level = Level::Warn,
) -> SplitSink[A, B] {
```
#### input
- `left : A` - Sink receiving records at or above `min_level`.
- `right : B` - Sink receiving records below `min_level`.
- `min_level : Level` - Threshold used for routing.
#### output
- `SplitSink[A, B]` - Level-based routing sink.
### Explanation
Detailed rules explaining key parameters and behaviors
- This is a convenience wrapper over `split_sink(...)`.
- High-severity and low-severity records can be separated without writing the predicate manually.
- It is useful for patterns such as warnings/errors to one destination and info/debug to another.
### How to Use
Here are some specific examples provided.
#### When Need Severity-based Routing
When warnings and errors should go to a special path:
```moonbit
let sink = split_by_level(callback_sink(fn(rec) {
println(rec.message)
}), console_sink(), min_level=Level::Warn)
```
In this example, `WARN` and above go to the left sink.
### Error Case
e.g.:
- If routing should use target, message, or fields instead of level, use `split_sink(...)` with a custom predicate.
- Sink-specific write behavior still follows the wrapped sinks.
### Notes
1. This is a convenience routing helper for a common policy.
2. It preserves the same one-side-only routing model as `split_sink(...)`.
+68
View File
@@ -0,0 +1,68 @@
---
name: split-sink
group: api
category: sink
update-time: 20260520
description: Create a sink that routes records to one of two sinks using a predicate.
key-word:
- sink
- split
- routing
- public
---
## Split-sink
Create a sink that routes each record to one of two sinks based on a predicate. This helper is useful for target-based or policy-based sink routing.
### Interface
```moonbit
pub fn[A, B] split_sink(left : A, right : B, predicate : (Record) -> Bool) -> SplitSink[A, B] {
```
#### input
- `left : A` - Sink receiving records when the predicate returns `true`.
- `right : B` - Sink receiving records when the predicate returns `false`.
- `predicate : (Record) -> Bool` - Routing function evaluated for each record.
#### output
- `SplitSink[A, B]` - Conditional routing sink.
### Explanation
Detailed rules explaining key parameters and behaviors
- The predicate decides which side receives each record.
- Unlike `fanout_sink(...)`, a record is routed to one side only.
- This helper is useful for target-aware and content-aware routing policies.
### How to Use
Here are some specific examples provided.
#### When Need Predicate-based Routing
When audit records should go to a different destination:
```moonbit
let sink = split_sink(console_sink(), json_console_sink(), fn(rec) {
rec.target == "audit"
})
```
In this example, only records matching the predicate go to the left sink.
### Error Case
e.g.:
- If both destinations should always receive the same record, use `fanout_sink(...)` instead.
- Predicate logic is caller-defined, so misrouting comes from predicate choice rather than sink mechanics.
### Notes
1. This helper is a routing primitive for synchronous sink composition.
2. `split_by_level(...)` is a convenience wrapper for level-based routing.
+64
View File
@@ -0,0 +1,64 @@
---
name: style-markup-mode-label
group: api
category: formatter
update-time: 20260520
description: Convert a StyleMarkupMode value into its stable string label.
key-word:
- style
- formatter
- label
- public
---
## Style-markup-mode-label
Convert `StyleMarkupMode` into its stable string label. This helper is useful for diagnostics, tests, and config inspection output.
### Interface
```moonbit
pub fn style_markup_mode_label(mode : StyleMarkupMode) -> String {
```
#### input
- `mode : StyleMarkupMode` - Markup mode enum value to label.
#### output
- `String` - Stable label such as `disabled`, `builtin`, or `full`.
### Explanation
Detailed rules explaining key parameters and behaviors
- The returned strings match the built-in markup mode vocabulary.
- This helper is presentation-oriented and does not change formatting behavior by itself.
- It is especially useful when tests or logs should expose the active markup policy clearly.
### How to Use
Here are some specific examples provided.
#### When Need A Readable Markup Mode Name
When diagnostics should show which markup mode is active:
```moonbit
let label = style_markup_mode_label(StyleMarkupMode::Builtin)
```
In this example, `label` becomes `"builtin"`.
### Error Case
e.g.:
- There is no failure path for valid `StyleMarkupMode` values.
- If code needs behavior rather than display text, the enum value itself is usually more useful than the returned label.
### Notes
1. This helper is mainly useful for human-readable inspection and assertions.
2. It pairs naturally with formatter config tests and debug output.
+60
View File
@@ -0,0 +1,60 @@
---
name: style-tag-registry
group: api
category: formatter
update-time: 20260520
description: Create an empty style tag registry for custom formatter markup tags.
key-word:
- style
- registry
- formatter
- public
---
## Style-tag-registry
Create an empty `StyleTagRegistry` for custom formatter style tags. This is the starting point for defining named tags such as `accent` or aliases such as `danger`.
### Interface
```moonbit
pub fn style_tag_registry() -> StyleTagRegistry {
```
#### output
- `StyleTagRegistry` - Empty registry ready for `set_tag(...)` and alias operations.
### Explanation
Detailed rules explaining key parameters and behaviors
- The returned registry starts empty.
- This helper is intended for runtime formatter customization in code.
- Registries created here can be attached to `text_formatter(...)` through `with_style_tags(...)`.
### How to Use
Here are some specific examples provided.
#### When Need Custom Named Tags
When formatter markup should define project-specific tag names:
```moonbit
let tags = style_tag_registry().set_tag("accent", fg=Some("#4cc9f0"), bold=true)
```
In this example, the registry becomes the container for custom named styles.
### Error Case
e.g.:
- If the formatter never enables the relevant style markup mode, tags in the registry may remain unused.
- An empty registry is valid, but it does not add any custom tags by itself.
### Notes
1. This helper creates a runtime registry, not a serializable config map.
2. Use `default_style_tag_registry()` when you want the built-in tag set as a starting point.
+73
View File
@@ -0,0 +1,73 @@
---
name: text-callback-sink
group: api
category: sink
update-time: 20260520
description: Create a callback sink that forwards rendered text using a TextFormatter.
key-word:
- sink
- callback
- text
- public
---
## Text-callback-sink
Create a callback sink that renders each record with a `TextFormatter` and forwards the resulting string to a callback. This is useful when integrations need formatted text without writing to the console directly.
### Interface
```moonbit
pub fn text_callback_sink(
formatter : TextFormatter,
callback : (String) -> Unit,
) -> FormattedCallbackSink {
```
#### input
- `formatter : TextFormatter` - Formatter used to render each record.
- `callback : (String) -> Unit` - Function receiving the rendered text.
#### output
- `FormattedCallbackSink` - Callback sink working on rendered text.
### Explanation
Detailed rules explaining key parameters and behaviors
- This helper combines text rendering and callback delivery in one sink.
- It is the text-oriented counterpart to `callback_sink(...)`.
- Use it when a custom destination needs the final rendered text rather than the structured record object.
### How to Use
Here are some specific examples provided.
#### When Need Rendered Text In A Custom Integration
When a callback destination wants the final human-readable line:
```moonbit
let sink = text_callback_sink(
text_formatter(show_timestamp=false),
fn(line) {
println(line)
},
)
```
In this example, the callback receives rendered text instead of a `Record`.
### Error Case
e.g.:
- If structured field access is needed, use `callback_sink(...)` instead.
- Callback failures are outside the sink's own API contract.
### Notes
1. This helper is useful for tests and adapters that want human-readable output.
2. `formatted_callback_sink(...)` is the lower-level variant when the formatter input is already a `RecordFormatter`.
+67
View File
@@ -0,0 +1,67 @@
---
name: text-console-sink
group: api
category: sink
update-time: 20260520
description: Create a formatted console sink from a TextFormatter.
key-word:
- sink
- text
- formatter
- public
---
## Text-console-sink
Create a formatted console sink from a `TextFormatter`. This sink is the direct sync constructor for human-readable console output with explicit text formatting rules.
### Interface
```moonbit
pub fn text_console_sink(formatter : TextFormatter) -> FormattedConsoleSink {
```
#### input
- `formatter : TextFormatter` - Text formatter used to render each record.
#### output
- `FormattedConsoleSink` - Console sink that renders through the supplied formatter.
### Explanation
Detailed rules explaining key parameters and behaviors
- This helper adapts a `TextFormatter` into a console sink.
- It is the direct sync sink counterpart to config-driven `text_console(...)` presets.
- Use it when code-side formatter composition is preferable to config-driven assembly.
### How to Use
Here are some specific examples provided.
#### When Need Explicit Text Formatting On The Console
When console output should use a custom separator or template:
```moonbit
let logger = Logger::new(
text_console_sink(text_formatter(show_timestamp=false, separator=" | ")),
target="pretty",
)
```
In this example, the sink renders through the supplied formatter.
### Error Case
e.g.:
- If structured JSON output is required, use `json_console_sink()` instead.
- If formatting should be data-driven rather than code-driven, prefer config or presets APIs.
### Notes
1. This is the most direct sync sink constructor for custom text output.
2. `formatted_console_sink(...)` is the lower-level variant when the formatter input is already a `RecordFormatter`.
+75
View File
@@ -0,0 +1,75 @@
---
name: text-console
group: api
category: config
update-time: 20260520
description: Create a logger config preset for the built-in text console sink with formatter settings.
key-word:
- preset
- text
- console
- public
---
## Text-console
Create a `LoggerConfig` preset for text-formatted console output. This helper bundles `SinkKind::TextConsole` together with a `TextFormatterConfig` so config-driven logger assembly can control text rendering without manual `SinkConfig::new(...)` calls.
### Interface
```moonbit
pub fn text_console(
min_level~ : Level = Level::Info,
target~ : String = "",
timestamp~ : Bool = false,
text_formatter~ : TextFormatterConfig = default_text_formatter_config(),
) -> LoggerConfig {
```
#### input
- `min_level : Level` - Minimum enabled level for the preset.
- `target : String` - Default target stored in the returned config.
- `timestamp : Bool` - Whether the built logger should emit timestamps.
- `text_formatter : TextFormatterConfig` - Formatter config used by the text console sink.
#### output
- `LoggerConfig` - Config using `SinkKind::TextConsole` with the supplied formatter and no queue wrapper by default.
### Explanation
Detailed rules explaining key parameters and behaviors
- This preset always returns `sink.kind=SinkKind::TextConsole`.
- `text_formatter` is copied into `config.sink.text_formatter`.
- `queue=None` by default, so buffering remains opt-in through `with_queue(...)`.
### How to Use
Here are some specific examples provided.
#### When Need Customized Human-readable Console Output
When console logs should use a specific separator or timestamp display:
```moonbit
let formatter = TextFormatterConfig::new(show_timestamp=false, separator=" | ")
let config = text_console(target="worker", text_formatter=formatter)
```
In this example, the text console sink uses the supplied formatter config.
And the preset stays fully compatible with later `build_logger(...)` assembly.
### Error Case
e.g.:
- If `target` is empty, the preset still returns a valid config.
- If you later apply `with_file_rotation(...)`, the config stays unchanged because this is not a file sink preset.
### Notes
1. Use this preset instead of `console(...)` when text formatting must be configured explicitly.
2. File-only settings such as `path` and `rotation` are not part of this preset.
+76
View File
@@ -0,0 +1,76 @@
---
name: text-style
group: api
category: formatter
update-time: 20260520
description: Create a reusable text style used by style tags and formatter config helpers.
key-word:
- style
- formatter
- color
- public
---
## Text-style
Create a `TextStyle` describing foreground color, background color, and emphasis flags such as bold or underline. This helper is the basic style value used by formatter style tags and config-driven formatter settings.
### Interface
```moonbit
pub fn text_style(
fg~ : String? = None,
bg~ : String? = None,
bold~ : Bool = false,
dim~ : Bool = false,
italic~ : Bool = false,
underline~ : Bool = false,
) -> TextStyle {
```
#### input
- `fg : String?` - Optional foreground color, usually a named color or hex string.
- `bg : String?` - Optional background color.
- `bold : Bool` - Whether bold emphasis is enabled.
- `dim : Bool` - Whether dim emphasis is enabled.
- `italic : Bool` - Whether italic emphasis is enabled.
- `underline : Bool` - Whether underline emphasis is enabled.
#### output
- `TextStyle` - Reusable style value.
### Explanation
Detailed rules explaining key parameters and behaviors
- This helper creates a plain style value; it does not register tags by itself.
- The returned style is commonly used in `style_tag_registry()` and `TextFormatterConfig::new(style_tags=...)`.
- Color interpretation still depends on formatter color settings and runtime support.
### How to Use
Here are some specific examples provided.
#### When Need A Reusable Custom Tag Style
When a formatter should define a named style tag:
```moonbit
let accent = text_style(fg=Some("#4cc9f0"), bold=true)
```
In this example, `accent` becomes a reusable style value that can be attached to a style tag registry or config map.
### Error Case
e.g.:
- If a style uses colors but the formatter disables markup or color output, visible rendering may not reflect the full style.
- This helper does not validate higher-level tag naming because it only creates the style value itself.
### Notes
1. This is the basic building block for style-tag customization.
2. Use `style_tag_registry()` when you need named tags rather than a raw style value.
+86
View File
@@ -0,0 +1,86 @@
---
name: with-file-rotation
group: api
category: config
update-time: 20260520
description: Add or replace size-based file rotation on an existing file logger config preset.
key-word:
- preset
- file
- rotation
- public
---
## With-file-rotation
Add or replace a size-based file rotation policy on an existing `LoggerConfig`. This helper is intentionally narrow: it only mutates configs whose sink kind is `File` and leaves every non-file config unchanged.
### Interface
```moonbit
pub fn with_file_rotation(
config : LoggerConfig,
max_bytes : Int,
max_backups~ : Int = 1,
) -> LoggerConfig {
```
#### input
- `config : LoggerConfig` - Base logger config to inspect and possibly update.
- `max_bytes : Int` - Maximum active file size before rotation.
- `max_backups : Int` - Number of rotated backup files to retain.
#### output
- `LoggerConfig` - Updated file config with rotation, or the original config unchanged when the sink is not file-based.
### Explanation
Detailed rules explaining key parameters and behaviors
- `with_file_rotation(...)` only applies to file presets or other configs whose `sink.kind=SinkKind::File`.
- If the input config is not file-based, the helper returns the config unchanged.
- When the input config is file-based, the helper preserves `min_level`, `target`, `timestamp`, file path, append mode, auto-flush, formatter, and queue settings while replacing `sink.rotation`.
- Rotation policy creation follows `file_rotation(...)` normalization rules for `max_bytes` and `max_backups`.
### How to Use
Here are some specific examples provided.
#### When Need To Extend A File Preset
When file rotation should be added after the base preset is assembled:
```moonbit
let config = with_file_rotation(file("service.log"), 1024 * 1024, max_backups=3)
```
In this example, the file preset gains a size-based rotation policy.
And all other file sink settings remain unchanged.
#### When Compose Queue And Rotation Together
When the file logger also needs bounded queueing:
```moonbit
let config = with_file_rotation(
with_queue(file("service.log"), max_pending=32),
4096,
max_backups=2,
)
```
In this example, queue settings are preserved while file rotation is added.
### Error Case
e.g.:
- If the input config is not file-based, no error is raised and the config is returned unchanged.
- If `max_bytes` or `max_backups` are non-positive, normalization behavior follows `file_rotation(...)`.
### Notes
1. This helper is a no-op for `console(...)`, `json_console(...)`, and `text_console(...)` presets.
2. Use `file(...)` when you need to provide the initial file path, because `with_file_rotation(...)` does not create a file sink from a non-file config.
+76
View File
@@ -0,0 +1,76 @@
---
name: with-queue
group: api
category: config
update-time: 20260520
description: Add a queue wrapper to an existing logger config preset.
key-word:
- preset
- queue
- config
- public
---
## With-queue
Add a synchronous queue wrapper to an existing `LoggerConfig`. This preset helper is the config-side counterpart to `Logger::with_queue(...)`, allowing queue behavior to be expressed before the runtime logger is built.
### Interface
```moonbit
pub fn with_queue(
config : LoggerConfig,
max_pending~ : Int = 0,
overflow~ : QueueOverflowPolicy = QueueOverflowPolicy::DropNewest,
) -> LoggerConfig {
```
#### input
- `config : LoggerConfig` - Base logger config to wrap.
- `max_pending : Int` - Maximum number of pending records allowed in the queue.
- `overflow : QueueOverflowPolicy` - Overflow behavior such as `DropNewest` or `DropOldest`.
#### output
- `LoggerConfig` - A copy of the input config with `queue=Some(QueueConfig::new(...))`.
### Explanation
Detailed rules explaining key parameters and behaviors
- This helper preserves the original config's `min_level`, `target`, `timestamp`, and `sink` fields.
- Only the top-level `queue` field is added or replaced.
- The helper can be composed with any preset, including `console(...)`, `json_console(...)`, `text_console(...)`, and `file(...)`.
### How to Use
Here are some specific examples provided.
#### When Need Config-side Queueing
When queue behavior should be configured before calling `build_logger(...)`:
```moonbit
let config = with_queue(
text_console(target="svc"),
max_pending=32,
overflow=QueueOverflowPolicy::DropOldest,
)
```
In this example, the text console preset is wrapped with a bounded queue config.
And sink-specific settings remain unchanged.
### Error Case
e.g.:
- If `max_pending` is too small, burst traffic may trigger overflow handling sooner.
- If the wrapped runtime logger is never flushed or drained, queued records may remain pending depending on how the built sink is used.
### Notes
1. This helper is config composition only; it does not build or run the logger.
2. Queue wrapping can be combined with `with_file_rotation(...)` when the base config is file-based.
+25
View File
@@ -0,0 +1,25 @@
## BitLogger Update Changes
version 0.5.0
### Feature
- feat: add config presets `console(...)`, `json_console(...)`, `text_console(...)`, `file(...)`, `with_queue(...)`, and `with_file_rotation(...)` for faster assembly of common logger configurations
- feat: add application/library facade builders and default library logger helpers for clearer integration entry points
- feat: expand the published API reference so core logger, sink, preset, configured runtime, and async facade surfaces are documented as navigable product APIs
- feat: add feature-scoped runnable examples for console logging, config-driven setup, presets, text formatter styling, and file rotation flows
### Test
- test: cover preset defaults and preset composition behavior so release-facing config shortcuts stay stable
- test: keep runtime/config/logger integration checks green across the expanded release surface
### Example
- docs: add release-facing examples under `examples/console_basic`, `examples/config_build`, `examples/presets`, `examples/style_tags`, `examples/text_formatter`, and `examples/file_rotation`
- docs: expand API navigation and per-symbol coverage so users can discover both synchronous and async entry points from `docs/api/`
### Notes
- this release packages BitLogger as a more complete end-user logging toolkit, with presets, broader API reference coverage, and scenario-specific examples complementing the existing core/runtime work
- release metadata now reflects `0.5.0`, and temporary collaboration-specific ignore entries are no longer treated as product repo policy
+17 -10
View File
@@ -1,8 +1,25 @@
fn main {
let preset_logger = @lib.build_logger(
@lib.with_queue(
@lib.text_console(
min_level=@lib.Level::Info,
target="preset",
text_formatter=@lib.TextFormatterConfig::new(show_timestamp=false, separator=" | "),
),
max_pending=2,
overflow=@lib.QueueOverflowPolicy::DropOldest,
),
)
preset_logger.info("queued one")
preset_logger.info("queued two")
preset_logger.info("queued three")
ignore(preset_logger.flush())
@lib.set_default_min_level(@lib.Level::Debug)
@lib.set_default_target("bitlogger")
@lib.info("hello from BitLogger", fields=[@lib.field("mode", "demo")])
// Direct Logger::new(...) composition stays useful for custom sink graphs.
let logger = @lib.Logger::new(@lib.console_sink(), min_level=@lib.Level::Trace, target="custom")
.with_context_fields([@lib.field("service", "bitlogger")])
logger.debug("custom logger ready", fields=[@lib.field("sink", "console")])
@@ -136,16 +153,6 @@ fn main {
]))
patched_logger.info("login", fields=[@lib.field("user", "alice"), @lib.field("token", "secret")])
let queued_logger = @lib.Logger::new(
@lib.console_sink(),
min_level=@lib.Level::Info,
target="queue",
).with_queue(max_pending=2, overflow=@lib.QueueOverflowPolicy::DropOldest)
queued_logger.info("queued one")
queued_logger.info("queued two")
queued_logger.info("queued three")
ignore(queued_logger.sink.flush())
let config_logger = @lib.parse_and_build_logger(
"{\"min_level\":\"debug\",\"target\":\"config.demo\",\"timestamp\":true,\"sink\":{\"kind\":\"text_console\",\"text_formatter\":{\"show_timestamp\":false,\"field_separator\":\",\",\"template\":\"[{level}] {target} {message} :: {fields}\"}},\"queue\":{\"max_pending\":2,\"overflow\":\"DropOldest\"}}",
) catch {
+17
View File
@@ -0,0 +1,17 @@
fn main {
let config = @lib.parse_logger_config_text(
"{\"min_level\":\"debug\",\"target\":\"demo.config\",\"sink\":{\"kind\":\"text_console\",\"text_formatter\":{\"show_timestamp\":false,\"separator\":\" | \",\"template\":\"[{level}] {target} {message}\"}},\"queue\":{\"max_pending\":2,\"overflow\":\"DropOldest\"}}",
) catch {
err => {
ignore(err)
println("invalid config")
return
}
}
let logger = @lib.build_logger(config)
logger.info("queued one")
logger.info("queued two")
logger.info("queued three")
ignore(logger.flush())
}
+7
View File
@@ -0,0 +1,7 @@
import {
"Nanaloveyuki/BitLogger/src" @lib,
}
options(
"is-main": true,
)
+8
View File
@@ -0,0 +1,8 @@
fn main {
let logger = @lib.Logger::new(@lib.console_sink(), min_level=@lib.Level::Info, target="demo.console")
.with_context_fields([@lib.field("service", "bitlogger")])
logger.info("hello console", fields=[@lib.field("mode", "basic")])
let structured = @lib.Logger::new(@lib.json_console_sink(), min_level=@lib.Level::Info, target="demo.json")
structured.info("hello json", fields=[@lib.field("kind", "structured")])
}
+7
View File
@@ -0,0 +1,7 @@
import {
"Nanaloveyuki/BitLogger/src" @lib,
}
options(
"is-main": true,
)
+19
View File
@@ -0,0 +1,19 @@
fn main {
if !@lib.native_files_supported() {
println("native file sink is not available on this backend")
return
}
let logger = @lib.Logger::new(
@lib.file_sink(
"bitlogger-example.log",
auto_flush=true,
rotation=Some(@lib.file_rotation(128, max_backups=2)),
),
min_level=@lib.Level::Info,
target="demo.file",
)
logger.info("file rotation ready", fields=[@lib.field("kind", "file")])
ignore(logger.sink.flush())
ignore(logger.sink.close())
}
+7
View File
@@ -0,0 +1,7 @@
import {
"Nanaloveyuki/BitLogger/src" @lib,
}
options(
"is-main": true,
)
+27
View File
@@ -0,0 +1,27 @@
fn main {
let config = @lib.with_queue(
@lib.with_file_rotation(
@lib.file(
"preset-example.log",
min_level=@lib.Level::Info,
target="demo.preset",
auto_flush=true,
),
256,
max_backups=2,
),
max_pending=4,
overflow=@lib.QueueOverflowPolicy::DropOldest,
) catch {
err => {
ignore(err)
println("invalid preset config")
return
}
}
let logger = @lib.build_logger(config)
logger.info("preset logger ready", fields=[@lib.field("kind", "preset")])
ignore(logger.flush())
ignore(logger.file_close())
}
+7
View File
@@ -0,0 +1,7 @@
import {
"Nanaloveyuki/BitLogger/src" @lib,
}
options(
"is-main": true,
)
+16
View File
@@ -0,0 +1,16 @@
fn main {
let 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 logger = @lib.Logger::new(
@lib.text_console_sink(formatter),
min_level=@lib.Level::Info,
target="demo.style",
)
logger.info("<accent>styled</> output with <danger>alert</>")
}
+7
View File
@@ -0,0 +1,7 @@
import {
"Nanaloveyuki/BitLogger/src" @lib,
}
options(
"is-main": true,
)
+14
View File
@@ -0,0 +1,14 @@
fn main {
let formatter = @lib.text_formatter(
show_timestamp=false,
separator=" | ",
field_separator=",",
template="[{level}] {target} {message} :: {fields}",
)
let logger = @lib.Logger::new(
@lib.text_console_sink(formatter),
min_level=@lib.Level::Info,
target="demo.formatter",
)
logger.info("formatted output", fields=[@lib.field("user", "alice"), @lib.field("request_id", "42")])
}
+7
View File
@@ -0,0 +1,7 @@
import {
"Nanaloveyuki/BitLogger/src" @lib,
}
options(
"is-main": true,
)
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "Nanaloveyuki/BitLogger",
"version": "0.4.1",
"version": "0.5.0",
"deps": {
"maria/json_parser": "0.1.1",
"moonbitlang/async": "0.19.0"
+59
View File
@@ -280,6 +280,23 @@ async test "library async logger can be built from config" {
inspect(full.target, content="async.lib.config")
}
test "library async text logger can be built from typed config" {
let logger = build_library_async_text_logger(
AsyncLoggerBuildConfig::new(
logger=@bitlogger.text_console(
min_level=@bitlogger.Level::Warn,
target="async.lib.text",
text_formatter=@bitlogger.TextFormatterConfig::new(show_timestamp=false, separator=" | "),
),
async_config=AsyncLoggerConfig::new(max_pending=2),
),
)
let full = logger.to_async_logger()
inspect(logger.is_enabled(@bitlogger.Level::Error), content="true")
inspect(logger.is_enabled(@bitlogger.Level::Info), content="false")
inspect(full.target, content="async.lib.text")
}
async test "async logger can project to library async logger" {
let logger = build_async_logger(
AsyncLoggerBuildConfig::new(
@@ -304,6 +321,48 @@ test "application async logger aliases runtime async entry" {
inspect(logger.target, content="async.app")
}
test "application text async logger uses text facade build path" {
let logger = build_application_text_async_logger(
AsyncLoggerBuildConfig::new(
logger=@bitlogger.text_console(
min_level=@bitlogger.Level::Warn,
target="async.app.text",
text_formatter=@bitlogger.TextFormatterConfig::new(show_timestamp=false, separator=" | "),
),
async_config=AsyncLoggerConfig::new(max_pending=2),
),
)
inspect(logger.is_enabled(@bitlogger.Level::Error), content="true")
inspect(logger.is_enabled(@bitlogger.Level::Info), content="false")
inspect(logger.target, content="async.app.text")
}
test "build async text logger keeps text-console config fields" {
let logger = build_async_text_logger(
AsyncLoggerBuildConfig::new(
logger=@bitlogger.LoggerConfig::new(
min_level=@bitlogger.Level::Warn,
target="async.text.direct",
timestamp=true,
sink=@bitlogger.SinkConfig::new(
kind=@bitlogger.SinkKind::TextConsole,
text_formatter=@bitlogger.TextFormatterConfig::new(show_timestamp=false, separator=" | "),
),
),
async_config=AsyncLoggerConfig::new(max_pending=3, overflow=AsyncOverflowPolicy::DropOldest),
),
)
inspect(logger.is_enabled(@bitlogger.Level::Error), content="true")
inspect(logger.is_enabled(@bitlogger.Level::Info), content="false")
inspect(logger.target, content="async.text.direct")
inspect(logger.timestamp, content="true")
inspect(match logger.flush_policy() {
AsyncFlushPolicy::Never => "Never"
AsyncFlushPolicy::Batch => "Batch"
AsyncFlushPolicy::Shutdown => "Shutdown"
}, content="Never")
}
test "application async logger can be built from config text" {
let logger = parse_and_build_application_async_logger(
"{\"logger\":{\"min_level\":\"warn\",\"target\":\"async.app.json\",\"sink\":{\"kind\":\"console\"}},\"async_config\":{\"max_pending\":2,\"overflow\":\"DropNewest\",\"max_batch\":1,\"linger_ms\":0,\"flush\":\"Never\"}}",
+1 -1
View File
@@ -1,5 +1,5 @@
pub fn async_runtime_mode() -> AsyncRuntimeMode {
AsyncRuntimeMode::NativeWorker
@utils.native_worker_async_runtime_mode()
}
pub fn async_runtime_supports_background_worker() -> Bool {
+32 -233
View File
@@ -2,282 +2,81 @@ pub(all) suberror AsyncLoggerClosed {
AsyncLoggerClosed
}
pub(all) enum AsyncOverflowPolicy {
Blocking
DropOldest
DropNewest
}
pub type AsyncOverflowPolicy = @utils.AsyncOverflowPolicy
pub(all) enum AsyncFlushPolicy {
Never
Batch
Shutdown
}
pub type AsyncFlushPolicy = @utils.AsyncFlushPolicy
pub enum AsyncRuntimeMode {
NativeWorker
Compatibility
}
pub fn async_runtime_mode_label(mode : AsyncRuntimeMode) -> String {
match mode {
AsyncRuntimeMode::NativeWorker => "native_worker"
AsyncRuntimeMode::Compatibility => "compatibility"
}
}
pub type AsyncRuntimeMode = @utils.AsyncRuntimeMode
fn all_async_runtime_modes() -> Array[AsyncRuntimeMode] {
[AsyncRuntimeMode::NativeWorker, AsyncRuntimeMode::Compatibility]
[@utils.native_worker_async_runtime_mode(), @utils.compatibility_async_runtime_mode()]
}
pub struct AsyncRuntimeState {
mode : AsyncRuntimeMode
background_worker : Bool
}
pub type AsyncRuntimeState = @utils.AsyncRuntimeState
pub struct AsyncLoggerState {
runtime : AsyncRuntimeState
pending_count : Int
dropped_count : Int
is_closed : Bool
is_running : Bool
has_failed : Bool
last_error : String
flush_policy : AsyncFlushPolicy
pub type AsyncLoggerState = @utils.AsyncLoggerState
pub fn async_runtime_mode_label(mode : AsyncRuntimeMode) -> String {
@utils.async_runtime_mode_label(mode)
}
pub fn async_runtime_state() -> AsyncRuntimeState {
{
mode: async_runtime_mode(),
background_worker: async_runtime_supports_background_worker(),
}
AsyncRuntimeState::new(async_runtime_mode(), 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),
})
@utils.async_runtime_state_to_json(state)
}
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)
}
}
fn async_flush_policy_label(policy : AsyncFlushPolicy) -> String {
match policy {
AsyncFlushPolicy::Never => "Never"
AsyncFlushPolicy::Batch => "Batch"
AsyncFlushPolicy::Shutdown => "Shutdown"
}
}
fn async_logger_state_to_json_value(state : AsyncLoggerState) -> @json_parser.JsonValue {
@json_parser.JsonValue::Object({
"runtime": async_runtime_state_to_json(state.runtime),
"pending_count": @json_parser.JsonValue::Number(state.pending_count.to_double()),
"dropped_count": @json_parser.JsonValue::Number(state.dropped_count.to_double()),
"is_closed": @json_parser.JsonValue::Bool(state.is_closed),
"is_running": @json_parser.JsonValue::Bool(state.is_running),
"has_failed": @json_parser.JsonValue::Bool(state.has_failed),
"last_error": @json_parser.JsonValue::String(state.last_error),
"flush_policy": @json_parser.JsonValue::String(async_flush_policy_label(state.flush_policy)),
})
@utils.stringify_async_runtime_state(state, pretty=pretty)
}
pub fn async_logger_state_to_json(state : AsyncLoggerState) -> @json_parser.JsonValue {
async_logger_state_to_json_value(state)
@utils.async_logger_state_to_json(state)
}
pub fn stringify_async_logger_state(
state : AsyncLoggerState,
pretty~ : Bool = false,
) -> String {
let value = async_logger_state_to_json_value(state)
if pretty {
@json_parser.stringify_pretty(value, 2)
} else {
@json_parser.stringify(value)
}
@utils.stringify_async_logger_state(state, pretty=pretty)
}
pub struct AsyncLoggerConfig {
max_pending : Int
overflow : AsyncOverflowPolicy
max_batch : Int
linger_ms : Int
flush : AsyncFlushPolicy
}
pub fn AsyncLoggerConfig::new(
max_pending~ : Int = 0,
overflow~ : AsyncOverflowPolicy = AsyncOverflowPolicy::Blocking,
max_batch~ : Int = 1,
linger_ms~ : Int = 0,
flush~ : AsyncFlushPolicy = AsyncFlushPolicy::Never,
) -> AsyncLoggerConfig {
{
max_pending,
overflow,
max_batch: if max_batch <= 1 { 1 } else { max_batch },
linger_ms: if linger_ms < 0 { 0 } else { linger_ms },
flush,
}
}
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 type AsyncLoggerConfig = @utils.AsyncLoggerConfig
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,
)
@utils.parse_async_logger_config_text(input)
}
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"
}),
})
@utils.async_logger_config_to_json(config)
}
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)
}
@utils.stringify_async_logger_config(config, pretty=pretty)
}
pub struct AsyncLoggerBuildConfig {
logger : @bitlogger.LoggerConfig
async_config : AsyncLoggerConfig
}
pub fn AsyncLoggerBuildConfig::new(
logger~ : @bitlogger.LoggerConfig = @bitlogger.default_logger_config(),
async_config~ : AsyncLoggerConfig = AsyncLoggerConfig::new(),
) -> AsyncLoggerBuildConfig {
{ logger, async_config }
}
pub type AsyncLoggerBuildConfig = @utils.AsyncLoggerBuildConfig
pub fn parse_async_logger_build_config_text(input : String) -> AsyncLoggerBuildConfig raise {
let root = @json_parser.parse(input)
let obj = match root.as_object() {
Some(obj) => obj
None => raise Failure::Failure("Expected object at async logger build config root")
}
let logger = match obj.get("logger") {
Some(value) => @bitlogger.parse_logger_config_text(@json_parser.stringify(value))
None => @bitlogger.default_logger_config()
}
let async_config = match obj.get("async_config") {
Some(value) => parse_async_logger_config_text(@json_parser.stringify(value))
None => AsyncLoggerConfig::new()
}
AsyncLoggerBuildConfig::new(logger=logger, async_config=async_config)
@utils.parse_async_logger_build_config_text(input)
}
pub fn async_logger_build_config_to_json(
config : AsyncLoggerBuildConfig,
) -> @json_parser.JsonValue {
@json_parser.JsonValue::Object({
"logger": @bitlogger.logger_config_to_json(config.logger),
"async_config": async_logger_config_to_json(config.async_config),
})
@utils.async_logger_build_config_to_json(config)
}
pub fn stringify_async_logger_build_config(
config : AsyncLoggerBuildConfig,
pretty~ : Bool = false,
) -> String {
let value = async_logger_build_config_to_json(config)
if pretty {
@json_parser.stringify_pretty(value, 2)
} else {
@json_parser.stringify(value)
}
@utils.stringify_async_logger_build_config(config, pretty=pretty)
}
pub struct AsyncLogger[S] {
@@ -537,16 +336,16 @@ pub fn[S] AsyncLogger::flush_policy(self : AsyncLogger[S]) -> AsyncFlushPolicy {
}
pub fn[S] AsyncLogger::state(self : AsyncLogger[S]) -> AsyncLoggerState {
{
runtime: async_runtime_state(),
pending_count: self.pending_count(),
dropped_count: self.dropped_count(),
is_closed: self.is_closed(),
is_running: self.is_running(),
has_failed: self.has_failed(),
last_error: self.last_error(),
flush_policy: self.flush_policy(),
}
AsyncLoggerState::new(
async_runtime_state(),
self.pending_count(),
self.dropped_count(),
self.is_closed(),
self.is_running(),
self.has_failed(),
self.last_error(),
self.flush_policy(),
)
}
pub fn[S] AsyncLogger::close(self : AsyncLogger[S], clear? : Bool = false) -> Unit {
+1 -1
View File
@@ -1,5 +1,5 @@
pub fn async_runtime_mode() -> AsyncRuntimeMode {
AsyncRuntimeMode::Compatibility
@utils.compatibility_async_runtime_mode()
}
pub fn async_runtime_supports_background_worker() -> Bool {
+1
View File
@@ -1,5 +1,6 @@
import {
"Nanaloveyuki/BitLogger/src" @bitlogger,
"Nanaloveyuki/BitLogger/src-async/utils" @utils,
"maria/json_parser" @json_parser,
"moonbitlang/async" @async,
"moonbitlang/async/aqueue" @aqueue,
+303
View File
@@ -0,0 +1,303 @@
pub(all) enum AsyncOverflowPolicy {
Blocking
DropOldest
DropNewest
}
pub(all) enum AsyncFlushPolicy {
Never
Batch
Shutdown
}
pub enum AsyncRuntimeMode {
NativeWorker
Compatibility
}
pub fn async_runtime_mode_label(mode : AsyncRuntimeMode) -> String {
match mode {
AsyncRuntimeMode::NativeWorker => "native_worker"
AsyncRuntimeMode::Compatibility => "compatibility"
}
}
pub fn native_worker_async_runtime_mode() -> AsyncRuntimeMode {
AsyncRuntimeMode::NativeWorker
}
pub fn compatibility_async_runtime_mode() -> AsyncRuntimeMode {
AsyncRuntimeMode::Compatibility
}
pub struct AsyncRuntimeState {
mode : AsyncRuntimeMode
background_worker : Bool
}
pub fn AsyncRuntimeState::new(
mode : AsyncRuntimeMode,
background_worker : Bool,
) -> AsyncRuntimeState {
{ mode, background_worker }
}
pub struct AsyncLoggerState {
runtime : AsyncRuntimeState
pending_count : Int
dropped_count : Int
is_closed : Bool
is_running : Bool
has_failed : Bool
last_error : String
flush_policy : AsyncFlushPolicy
}
pub fn AsyncLoggerState::new(
runtime : AsyncRuntimeState,
pending_count : Int,
dropped_count : Int,
is_closed : Bool,
is_running : Bool,
has_failed : Bool,
last_error : String,
flush_policy : AsyncFlushPolicy,
) -> AsyncLoggerState {
{
runtime,
pending_count,
dropped_count,
is_closed,
is_running,
has_failed,
last_error,
flush_policy,
}
}
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)
}
}
fn async_flush_policy_label(policy : AsyncFlushPolicy) -> String {
match policy {
AsyncFlushPolicy::Never => "Never"
AsyncFlushPolicy::Batch => "Batch"
AsyncFlushPolicy::Shutdown => "Shutdown"
}
}
fn async_logger_state_to_json_value(state : AsyncLoggerState) -> @json_parser.JsonValue {
@json_parser.JsonValue::Object({
"runtime": async_runtime_state_to_json(state.runtime),
"pending_count": @json_parser.JsonValue::Number(state.pending_count.to_double()),
"dropped_count": @json_parser.JsonValue::Number(state.dropped_count.to_double()),
"is_closed": @json_parser.JsonValue::Bool(state.is_closed),
"is_running": @json_parser.JsonValue::Bool(state.is_running),
"has_failed": @json_parser.JsonValue::Bool(state.has_failed),
"last_error": @json_parser.JsonValue::String(state.last_error),
"flush_policy": @json_parser.JsonValue::String(async_flush_policy_label(state.flush_policy)),
})
}
pub fn async_logger_state_to_json(state : AsyncLoggerState) -> @json_parser.JsonValue {
async_logger_state_to_json_value(state)
}
pub fn stringify_async_logger_state(
state : AsyncLoggerState,
pretty~ : Bool = false,
) -> String {
let value = async_logger_state_to_json_value(state)
if pretty {
@json_parser.stringify_pretty(value, 2)
} else {
@json_parser.stringify(value)
}
}
pub struct AsyncLoggerConfig {
max_pending : Int
overflow : AsyncOverflowPolicy
max_batch : Int
linger_ms : Int
flush : AsyncFlushPolicy
}
pub fn AsyncLoggerConfig::new(
max_pending~ : Int = 0,
overflow~ : AsyncOverflowPolicy = AsyncOverflowPolicy::Blocking,
max_batch~ : Int = 1,
linger_ms~ : Int = 0,
flush~ : AsyncFlushPolicy = AsyncFlushPolicy::Never,
) -> AsyncLoggerConfig {
{
max_pending,
overflow,
max_batch: if max_batch <= 1 { 1 } else { max_batch },
linger_ms: if linger_ms < 0 { 0 } else { linger_ms },
flush,
}
}
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 {
logger : @bitlogger.LoggerConfig
async_config : AsyncLoggerConfig
}
pub fn AsyncLoggerBuildConfig::new(
logger~ : @bitlogger.LoggerConfig = @bitlogger.default_logger_config(),
async_config~ : AsyncLoggerConfig = AsyncLoggerConfig::new(),
) -> AsyncLoggerBuildConfig {
{ logger, async_config }
}
pub fn parse_async_logger_build_config_text(input : String) -> AsyncLoggerBuildConfig raise {
let root = @json_parser.parse(input)
let obj = match root.as_object() {
Some(obj) => obj
None => raise Failure::Failure("Expected object at async logger build config root")
}
let logger = match obj.get("logger") {
Some(value) => @bitlogger.parse_logger_config_text(@json_parser.stringify(value))
None => @bitlogger.default_logger_config()
}
let async_config = match obj.get("async_config") {
Some(value) => parse_async_logger_config_text(@json_parser.stringify(value))
None => AsyncLoggerConfig::new()
}
AsyncLoggerBuildConfig::new(logger=logger, async_config=async_config)
}
pub fn async_logger_build_config_to_json(
config : AsyncLoggerBuildConfig,
) -> @json_parser.JsonValue {
@json_parser.JsonValue::Object({
"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(
config : AsyncLoggerBuildConfig,
pretty~ : Bool = false,
) -> String {
let value = async_logger_build_config_to_json(config)
if pretty {
@json_parser.stringify_pretty(value, 2)
} else {
@json_parser.stringify(value)
}
}
+4
View File
@@ -0,0 +1,4 @@
import {
"Nanaloveyuki/BitLogger/src" @bitlogger,
"maria/json_parser" @json_parser,
}
+35
View File
@@ -242,6 +242,32 @@ test "config subtype json helpers stringify stable shapes" {
)
}
test "default config helpers expose baseline config shapes" {
let formatter = default_text_formatter_config()
inspect(formatter.show_timestamp, content="true")
inspect(color_mode_label(formatter.color_mode), content="never")
inspect(color_support_label(formatter.color_support), content="truecolor")
inspect(style_markup_mode_label(formatter.style_markup), content="full")
let sink = default_sink_config()
inspect(match sink.kind {
SinkKind::Console => "Console"
_ => "other"
}, content="Console")
inspect(sink.path, content="")
inspect(sink.rotation is None, content="true")
let logger = default_logger_config()
inspect(logger.min_level.label(), content="INFO")
inspect(logger.target, content="")
inspect(logger.timestamp, content="false")
inspect(logger.queue is None, content="true")
inspect(match logger.sink.kind {
SinkKind::Console => "Console"
_ => "other"
}, content="Console")
}
test "config basic color support downgrades hex colors" {
let formatter = TextFormatterConfig::new(
show_level=false,
@@ -835,6 +861,15 @@ test "configured logger can project to library logger" {
inspect(logger.to_logger().target, content="lib.projected")
}
test "default library logger mirrors shared sync defaults" {
set_default_min_level(Level::Warn)
set_default_target("lib.default")
let logger = default_library_logger()
inspect(logger.is_enabled(Level::Error), content="true")
inspect(logger.is_enabled(Level::Info), content="false")
inspect(logger.to_logger().target, content="lib.default")
}
test "application logger aliases configured runtime entry" {
let logger = build_application_logger(
LoggerConfig::new(min_level=Level::Warn, target="app.runtime"),
+13
View File
@@ -255,6 +255,19 @@ test "global style tags apply when formatter has no local registry" {
set_global_style_tag_registry(previous)
}
test "reset global style tag registry restores builtin defaults" {
set_global_style_tag_registry(style_tag_registry().set_tag("accent", fg=Some("#123456"), bold=true))
reset_global_style_tag_registry()
let rec = record(Level::Info, "<success>ok</>")
inspect(
format_text(
rec,
formatter=text_formatter(show_level=false, show_target=false, color_mode=ColorMode::Always),
),
content="\u{001b}[32;1mok\u{001b}[0m",
)
}
test "style tag alias can reuse builtin tags" {
let formatter = text_formatter(
show_level=false,
+32 -4
View File
@@ -21,13 +21,31 @@ BitLogger 是一个使用 MoonBit 编写的结构化日志库.
```moonbit
test {
let logger = Logger::new(console_sink(), min_level=Level::Debug, target="demo")
.with_timestamp()
.with_context_fields([field("service", "bitlogger")])
let logger = build_logger(
text_console(
min_level=Level::Debug,
target="demo",
text_formatter=TextFormatterConfig::new(show_timestamp=false, separator=" | "),
),
)
logger.info("starting", fields=[field("port", "8080")])
ignore(logger.flush())
}
```
Recommended sync path / 推荐同步起步路径:
- start with `console(...)`, `json_console(...)`, `text_console(...)`, or `file(...)`
- 先用这些 presets 组装 `LoggerConfig`
- then apply small config helpers such as `with_queue(...)` or `with_file_rotation(...)`
- 再按需叠加 `with_queue(...)` / `with_file_rotation(...)`
- finally call `build_logger(...)` to get the runtime logger
- 最后交给 `build_logger(...)` 构建运行时 logger
Use `Logger::new(...)` instead when you need direct sink composition such as `fanout_sink(...)`, `split_by_level(...)`, `callback_sink(...)`, or custom patch/filter graphs.
如果你需要 `fanout_sink(...)``split_by_level(...)``callback_sink(...)` 或更自由的 patch/filter 组合,再改用 `Logger::new(...)` 直接拼装运行时 sink 图。
Project command note / 项目命令说明:
- use `moon check` / `moon test` for local project verification
@@ -39,13 +57,23 @@ Project command note / 项目命令说明:
- examples / 示例:
- `../examples/basic/`
- `../examples/console_basic/`
- `../examples/text_formatter/`
- `../examples/style_tags/`
- `../examples/file_rotation/`
- `../examples/config_build/`
- `../examples/presets/`
- `../examples/async_basic/`
- package-level API docs / 单接口 API 文档:
- `../docs/api/`
- common entry points / 常用入口:
- `text_console(...)` + `build_logger(...)`
- `file(...)` + `with_file_rotation(...)` + `build_logger(...)`
- `Logger::new(...)`
- `async_logger(...)`
- `build_logger(...)`
- `build_application_logger(...)`
- `build_library_logger(...)`
- `async_logger(...)`
- `build_async_logger(...)`
## Notes / 说明
+20 -491
View File
@@ -1,537 +1,66 @@
pub(all) suberror ConfigError {
InvalidConfig(String)
}
pub type ConfigError = @utils.ConfigError
pub(all) enum SinkKind {
Console
JsonConsole
TextConsole
File
}
pub type SinkKind = @utils.SinkKind
pub struct TextFormatterConfig {
show_timestamp : Bool
show_level : Bool
show_target : Bool
show_fields : Bool
separator : String
field_separator : 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 type TextFormatterConfig = @utils.TextFormatterConfig
pub fn TextFormatterConfig::new(
show_timestamp~ : Bool = true,
show_level~ : Bool = true,
show_target~ : Bool = true,
show_fields~ : Bool = true,
separator~ : String = " ",
field_separator~ : 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 {
{
show_timestamp,
show_level,
show_target,
show_fields,
separator,
field_separator,
template,
color_mode,
color_support,
style_markup,
target_style_markup,
fields_style_markup,
style_tags,
}
}
pub type QueueConfig = @utils.QueueConfig
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 type SinkConfig = @utils.SinkConfig
pub fn TextFormatterConfig::to_formatter(self : TextFormatterConfig) -> TextFormatter {
text_formatter(
show_timestamp=self.show_timestamp,
show_level=self.show_level,
show_target=self.show_target,
show_fields=self.show_fields,
separator=self.separator,
field_separator=self.field_separator,
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))
},
)
}
pub struct QueueConfig {
max_pending : Int
overflow : QueueOverflowPolicy
}
pub fn QueueConfig::new(
max_pending : Int,
overflow~ : QueueOverflowPolicy = QueueOverflowPolicy::DropNewest,
) -> QueueConfig {
{ max_pending, overflow }
}
pub struct SinkConfig {
kind : SinkKind
path : String
append : Bool
auto_flush : Bool
rotation : FileRotation?
text_formatter : TextFormatterConfig
}
pub fn SinkConfig::new(
kind~ : SinkKind = SinkKind::Console,
path~ : String = "",
append~ : Bool = true,
auto_flush~ : Bool = true,
rotation~ : FileRotation? = None,
text_formatter~ : TextFormatterConfig = default_text_formatter_config(),
) -> SinkConfig {
{
kind,
path,
append,
auto_flush,
rotation,
text_formatter,
}
}
pub struct LoggerConfig {
min_level : Level
target : String
timestamp : Bool
sink : SinkConfig
queue : QueueConfig?
}
pub fn LoggerConfig::new(
min_level~ : Level = Level::Info,
target~ : String = "",
timestamp~ : Bool = false,
sink~ : SinkConfig = default_sink_config(),
queue~ : QueueConfig? = None,
) -> LoggerConfig {
{
min_level,
target,
timestamp,
sink,
queue,
}
}
pub type LoggerConfig = @utils.LoggerConfig
pub fn default_text_formatter_config() -> TextFormatterConfig {
TextFormatterConfig::new()
@utils.default_text_formatter_config()
}
pub fn default_sink_config() -> SinkConfig {
SinkConfig::new()
@utils.default_sink_config()
}
pub fn default_logger_config() -> LoggerConfig {
LoggerConfig::new()
}
fn expect_object(
value : @json_parser.JsonValue,
context : String,
) -> Map[String, @json_parser.JsonValue] raise ConfigError {
match value.as_object() {
Some(obj) => obj
None => raise ConfigError::InvalidConfig("Expected object at " + context)
}
}
fn get_string(
obj : Map[String, @json_parser.JsonValue],
key : String,
default~ : String = "",
) -> String raise ConfigError {
match obj.get(key) {
None => default
Some(value) => match value.as_string() {
Some(text) => text
None => raise ConfigError::InvalidConfig("Expected string at key " + key)
}
}
}
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(
obj : Map[String, @json_parser.JsonValue],
key : String,
default~ : Bool,
) -> Bool raise ConfigError {
match obj.get(key) {
None => default
Some(value) => match value.as_bool() {
Some(flag) => flag
None => raise ConfigError::InvalidConfig("Expected bool at key " + key)
}
}
}
fn get_int(
obj : Map[String, @json_parser.JsonValue],
key : String,
default~ : Int,
) -> Int raise ConfigError {
match obj.get(key) {
None => default
Some(value) => match value.as_number() {
Some(number) => number.to_int()
None => raise ConfigError::InvalidConfig("Expected number at key " + key)
}
}
}
fn parse_level(name : String) -> Level raise ConfigError {
match name.to_upper() {
"TRACE" => Level::Trace
"DEBUG" => Level::Debug
"INFO" => Level::Info
"WARN" => Level::Warn
"ERROR" => Level::Error
_ => raise ConfigError::InvalidConfig("Unsupported level: " + name)
}
}
fn parse_overflow(name : String) -> QueueOverflowPolicy raise ConfigError {
match name.to_upper() {
"DROPNEWEST" => QueueOverflowPolicy::DropNewest
"DROPPOLDEST" => QueueOverflowPolicy::DropOldest
"DROPOLDEST" => QueueOverflowPolicy::DropOldest
_ => raise ConfigError::InvalidConfig("Unsupported queue overflow policy: " + name)
}
}
fn parse_sink_kind(name : String) -> SinkKind raise ConfigError {
match name.to_upper() {
"CONSOLE" => SinkKind::Console
"JSON_CONSOLE" => SinkKind::JsonConsole
"JSONCONSOLE" => SinkKind::JsonConsole
"TEXT_CONSOLE" => SinkKind::TextConsole
"TEXTCONSOLE" => SinkKind::TextConsole
"FILE" => SinkKind::File
_ => raise ConfigError::InvalidConfig("Unsupported sink kind: " + name)
}
}
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 {
match kind {
SinkKind::Console => "console"
SinkKind::JsonConsole => "json_console"
SinkKind::TextConsole => "text_console"
SinkKind::File => "file"
}
}
fn parse_text_formatter_config(value : @json_parser.JsonValue) -> TextFormatterConfig raise ConfigError {
let obj = expect_object(value, "text_formatter")
TextFormatterConfig::new(
show_timestamp=get_bool(obj, "show_timestamp", default=true),
show_level=get_bool(obj, "show_level", default=true),
show_target=get_bool(obj, "show_target", default=true),
show_fields=get_bool(obj, "show_fields", default=true),
separator=get_string(obj, "separator", default=" "),
field_separator=get_string(obj, "field_separator", default=" "),
template=get_string(obj, "template", default=""),
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 {
let obj = expect_object(value, "queue")
QueueConfig::new(
get_int(obj, "max_pending", default=0),
overflow=parse_overflow(get_string(obj, "overflow", default="DropNewest")),
)
}
fn parse_file_rotation_config(value : @json_parser.JsonValue) -> FileRotation raise ConfigError {
let obj = expect_object(value, "sink.rotation")
file_rotation(
get_int(obj, "max_bytes", default=1),
max_backups=get_int(obj, "max_backups", default=1),
)
}
fn parse_sink_config(value : @json_parser.JsonValue) -> SinkConfig raise ConfigError {
let obj = expect_object(value, "sink")
let kind = parse_sink_kind(get_string(obj, "kind", default="console"))
let formatter = match obj.get("text_formatter") {
None => default_text_formatter_config()
Some(inner) => parse_text_formatter_config(inner)
}
let path = get_string(obj, "path", default="")
match kind {
SinkKind::File => if path == "" {
raise ConfigError::InvalidConfig("File sink requires non-empty path")
}
_ => ()
}
SinkConfig::new(
kind=kind,
path=path,
append=get_bool(obj, "append", default=true),
auto_flush=get_bool(obj, "auto_flush", default=true),
rotation=match obj.get("rotation") {
None => None
Some(inner) => Some(parse_file_rotation_config(inner))
},
text_formatter=formatter,
)
@utils.default_logger_config()
}
pub fn parse_logger_config_text(input : String) -> LoggerConfig raise ConfigError {
let root = @json_parser.parse(input) catch {
e => raise ConfigError::InvalidConfig("Invalid JSON: " + e.to_string())
}
let obj = expect_object(root, "root")
LoggerConfig::new(
min_level=parse_level(get_string(obj, "min_level", default="INFO")),
target=get_string(obj, "target", default=""),
timestamp=get_bool(obj, "timestamp", default=false),
sink=match obj.get("sink") {
None => default_sink_config()
Some(value) => parse_sink_config(value)
},
queue=match obj.get("queue") {
None => None
Some(value) => Some(parse_queue_config(value))
},
)
@utils.parse_logger_config_text(input)
}
pub fn queue_config_to_json(queue : QueueConfig) -> @json_parser.JsonValue {
@json_parser.JsonValue::Object({
"max_pending": @json_parser.JsonValue::Number(queue.max_pending.to_double()),
"overflow": @json_parser.JsonValue::String(match queue.overflow {
QueueOverflowPolicy::DropNewest => "DropNewest"
QueueOverflowPolicy::DropOldest => "DropOldest"
}),
})
@utils.queue_config_to_json(queue)
}
pub fn stringify_queue_config(queue : QueueConfig, pretty~ : Bool = false) -> String {
let value = queue_config_to_json(queue)
if pretty {
@json_parser.stringify_pretty(value, 2)
} else {
@json_parser.stringify(value)
}
@utils.stringify_queue_config(queue, pretty=pretty)
}
pub fn text_formatter_config_to_json(config : TextFormatterConfig) -> @json_parser.JsonValue {
let obj : Map[String, @json_parser.JsonValue] = {
"show_timestamp": @json_parser.JsonValue::Bool(config.show_timestamp),
"show_level": @json_parser.JsonValue::Bool(config.show_level),
"show_target": @json_parser.JsonValue::Bool(config.show_target),
"show_fields": @json_parser.JsonValue::Bool(config.show_fields),
"separator": @json_parser.JsonValue::String(config.separator),
"field_separator": @json_parser.JsonValue::String(config.field_separator),
"template": @json_parser.JsonValue::String(config.template),
"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)
@utils.text_formatter_config_to_json(config)
}
pub fn stringify_text_formatter_config(
config : TextFormatterConfig,
pretty~ : Bool = false,
) -> String {
let value = text_formatter_config_to_json(config)
if pretty {
@json_parser.stringify_pretty(value, 2)
} else {
@json_parser.stringify(value)
}
@utils.stringify_text_formatter_config(config, pretty=pretty)
}
fn file_rotation_config_to_json(config : FileRotation) -> @json_parser.JsonValue {
@json_parser.JsonValue::Object({
"max_bytes": @json_parser.JsonValue::Number(config.max_bytes.to_double()),
"max_backups": @json_parser.JsonValue::Number(config.max_backups.to_double()),
})
pub fn file_rotation_config_to_json(config : FileRotation) -> @json_parser.JsonValue {
@utils.file_rotation_config_to_json(config)
}
pub fn sink_config_to_json(config : SinkConfig) -> @json_parser.JsonValue {
let obj : Map[String, @json_parser.JsonValue] = {
"kind": @json_parser.JsonValue::String(sink_kind_label(config.kind)),
"path": @json_parser.JsonValue::String(config.path),
"append": @json_parser.JsonValue::Bool(config.append),
"auto_flush": @json_parser.JsonValue::Bool(config.auto_flush),
"text_formatter": text_formatter_config_to_json(config.text_formatter),
}
match config.rotation {
None => ()
Some(rotation) => obj["rotation"] = file_rotation_config_to_json(rotation)
}
@json_parser.JsonValue::Object(obj)
@utils.sink_config_to_json(config)
}
pub fn stringify_sink_config(config : SinkConfig, pretty~ : Bool = false) -> String {
let value = sink_config_to_json(config)
if pretty {
@json_parser.stringify_pretty(value, 2)
} else {
@json_parser.stringify(value)
}
@utils.stringify_sink_config(config, pretty=pretty)
}
pub fn logger_config_to_json(config : LoggerConfig) -> @json_parser.JsonValue {
let obj : Map[String, @json_parser.JsonValue] = {
"min_level": @json_parser.JsonValue::String(config.min_level.label()),
"target": @json_parser.JsonValue::String(config.target),
"timestamp": @json_parser.JsonValue::Bool(config.timestamp),
"sink": sink_config_to_json(config.sink),
}
match config.queue {
None => ()
Some(queue) => obj["queue"] = queue_config_to_json(queue)
}
@json_parser.JsonValue::Object(obj)
@utils.logger_config_to_json(config)
}
pub fn stringify_logger_config(config : LoggerConfig, pretty~ : Bool = false) -> String {
let value = logger_config_to_json(config)
if pretty {
@json_parser.stringify_pretty(value, 2)
} else {
@json_parser.stringify(value)
}
@utils.stringify_logger_config(config, pretty=pretty)
}
+31
View File
@@ -0,0 +1,31 @@
pub(all) enum Level {
Trace
Debug
Info
Warn
Error
}
pub fn Level::priority(self : Level) -> Int {
match self {
Level::Trace => 10
Level::Debug => 20
Level::Info => 30
Level::Warn => 40
Level::Error => 50
}
}
pub fn Level::label(self : Level) -> String {
match self {
Level::Trace => "TRACE"
Level::Debug => "DEBUG"
Level::Info => "INFO"
Level::Warn => "WARN"
Level::Error => "ERROR"
}
}
pub fn Level::enabled(self : Level, min_level : Level) -> Bool {
self.priority() >= min_level.priority()
}
+2
View File
@@ -0,0 +1,2 @@
import {
}
+76
View File
@@ -0,0 +1,76 @@
pub struct Field {
key : String
value : String
}
pub fn field(key : String, value : String) -> Field {
{ key, value }
}
pub fn fields(entries : Array[(String, String)]) -> Array[Field] {
entries.map(fn(entry) {
field(entry.0, entry.1)
})
}
pub struct Record {
level : Level
timestamp_ms : UInt64
target : String
message : String
fields : Array[Field]
}
pub fn Record::new(
level : Level,
message : String,
timestamp_ms~ : UInt64 = 0UL,
target~ : String = "",
fields~ : Array[Field] = [],
) -> Record {
{ level, timestamp_ms, target, message, fields }
}
pub fn Field::with_value(self : Field, value : String) -> Field {
field(self.key, value)
}
pub fn Record::with_target(self : Record, target : String) -> Record {
Record::new(
self.level,
self.message,
timestamp_ms=self.timestamp_ms,
target=target,
fields=self.fields,
)
}
pub fn Record::with_message(self : Record, message : String) -> Record {
Record::new(
self.level,
message,
timestamp_ms=self.timestamp_ms,
target=self.target,
fields=self.fields,
)
}
pub fn Record::with_fields(self : Record, fields : Array[Field]) -> Record {
Record::new(
self.level,
self.message,
timestamp_ms=self.timestamp_ms,
target=self.target,
fields=fields,
)
}
pub fn Record::copy(self : Record) -> Record {
Record::new(
self.level,
self.message,
timestamp_ms=self.timestamp_ms,
target=self.target,
fields=self.fields,
)
}
+20 -100
View File
@@ -1,117 +1,37 @@
fn string_to_c_bytes(str : String) -> Bytes {
let res : Array[Byte] = []
let len = str.length()
let mut i = 0
while i < len {
let mut c = str.code_unit_at(i).to_int()
if 0xD800 <= c && c <= 0xDBFF {
c -= 0xD800
i = i + 1
let l = str.code_unit_at(i).to_int() - 0xDC00
c = (c << 10) + l + 0x10000
}
if c < 0x80 {
res.push(c.to_byte())
} else if c < 0x800 {
res.push((0xc0 + (c >> 6)).to_byte())
res.push((0x80 + (c & 0x3f)).to_byte())
} else if c < 0x10000 {
res.push((0xe0 + (c >> 12)).to_byte())
res.push((0x80 + ((c >> 6) & 0x3f)).to_byte())
res.push((0x80 + (c & 0x3f)).to_byte())
} else {
res.push((0xf0 + (c >> 18)).to_byte())
res.push((0x80 + ((c >> 12) & 0x3f)).to_byte())
res.push((0x80 + ((c >> 6) & 0x3f)).to_byte())
res.push((0x80 + (c & 0x3f)).to_byte())
}
i = i + 1
}
res.push((0).to_byte())
Bytes::from_array(res)
pub type FileHandle = @utils.FileHandle
pub fn open_file_handle_internal(path : String, append : Bool) -> FileHandle? {
@utils.open_file_handle_internal(path, append)
}
#external
type NativeFileHandle
#borrow(path, mode)
extern "C" fn file_open_ffi(path : Bytes, mode : Bytes) -> NativeFileHandle = "fopen"
extern "C" fn file_is_null_ffi(handle : NativeFileHandle) -> Bool = "moonbitlang_async_pointer_is_null"
#borrow(buffer)
extern "C" fn file_write_ffi(
buffer : Bytes,
size : Int,
count : Int,
handle : NativeFileHandle,
) -> Int = "fwrite"
extern "C" fn file_flush_ffi(handle : NativeFileHandle) -> Int = "fflush"
extern "C" fn file_close_ffi(handle : NativeFileHandle) -> Int = "fclose"
extern "C" fn file_seek_ffi(handle : NativeFileHandle, offset : Int, origin : Int) -> Int = "fseek"
extern "C" fn file_tell_ffi(handle : NativeFileHandle) -> Int = "ftell"
#borrow(from_path, to_path)
extern "C" fn file_rename_ffi(from_path : Bytes, to_path : Bytes) -> Int = "rename"
#borrow(path)
extern "C" fn file_remove_ffi(path : Bytes) -> Int = "remove"
pub struct FileHandle {
path : String
raw : NativeFileHandle
pub fn write_file_handle_internal(handle : FileHandle, content : String) -> Bool {
@utils.write_file_handle_internal(handle, content)
}
fn open_file_handle_internal(path : String, append : Bool) -> FileHandle? {
let mode = if append { "ab" } else { "wb" }
let raw = file_open_ffi(string_to_c_bytes(path), string_to_c_bytes(mode))
if file_is_null_ffi(raw) {
None
} else {
Some({ raw, path })
}
pub fn flush_file_handle_internal(handle : FileHandle) -> Bool {
@utils.flush_file_handle_internal(handle)
}
fn write_file_handle_internal(handle : FileHandle, content : String) -> Bool {
let bytes = string_to_c_bytes(content)
let written = file_write_ffi(bytes, 1, bytes.length() - 1, handle.raw)
written == bytes.length() - 1
pub fn close_file_handle_internal(handle : FileHandle) -> Bool {
@utils.close_file_handle_internal(handle)
}
fn flush_file_handle_internal(handle : FileHandle) -> Bool {
file_flush_ffi(handle.raw) == 0
pub fn file_size_internal(handle : FileHandle) -> Int {
@utils.file_size_internal(handle)
}
fn close_file_handle_internal(handle : FileHandle) -> Bool {
file_close_ffi(handle.raw) == 0
pub fn rename_file_internal(from_path : String, to_path : String) -> Bool {
@utils.rename_file_internal(from_path, to_path)
}
fn file_size_internal(handle : FileHandle) -> Int {
ignore(file_seek_ffi(handle.raw, 0, 2))
let size = file_tell_ffi(handle.raw)
if size < 0 {
0
} else {
size
}
pub fn remove_file_internal(path : String) -> Bool {
@utils.remove_file_internal(path)
}
fn rename_file_internal(from_path : String, to_path : String) -> Bool {
file_rename_ffi(string_to_c_bytes(from_path), string_to_c_bytes(to_path)) == 0
pub fn string_byte_length_internal(content : String) -> Int {
@utils.string_byte_length_internal(content)
}
fn remove_file_internal(path : String) -> Bool {
file_remove_ffi(string_to_c_bytes(path)) == 0
}
fn string_byte_length_internal(content : String) -> Int {
string_to_c_bytes(content).length() - 1
}
fn native_files_supported_internal() -> Bool {
true
pub fn native_files_supported_internal() -> Bool {
@utils.native_files_supported_internal()
}
+20 -34
View File
@@ -1,51 +1,37 @@
pub struct FileHandle {
path : String
pub type FileHandle = @utils.FileHandle
pub fn open_file_handle_internal(path : String, append : Bool) -> FileHandle? {
@utils.open_file_handle_internal(path, append)
}
fn open_file_handle_internal(path : String, append : Bool) -> FileHandle? {
ignore(append)
ignore(path)
let _unused : FileHandle = { path: "" }
ignore(_unused)
None
pub fn write_file_handle_internal(handle : FileHandle, content : String) -> Bool {
@utils.write_file_handle_internal(handle, content)
}
fn write_file_handle_internal(handle : FileHandle, content : String) -> Bool {
ignore(handle)
ignore(content)
false
pub fn flush_file_handle_internal(handle : FileHandle) -> Bool {
@utils.flush_file_handle_internal(handle)
}
fn flush_file_handle_internal(handle : FileHandle) -> Bool {
ignore(handle)
false
pub fn close_file_handle_internal(handle : FileHandle) -> Bool {
@utils.close_file_handle_internal(handle)
}
fn close_file_handle_internal(handle : FileHandle) -> Bool {
ignore(handle)
false
pub fn file_size_internal(handle : FileHandle) -> Int {
@utils.file_size_internal(handle)
}
fn file_size_internal(handle : FileHandle) -> Int {
ignore(handle)
0
pub fn rename_file_internal(from_path : String, to_path : String) -> Bool {
@utils.rename_file_internal(from_path, to_path)
}
fn rename_file_internal(from_path : String, to_path : String) -> Bool {
ignore(from_path)
ignore(to_path)
false
pub fn remove_file_internal(path : String) -> Bool {
@utils.remove_file_internal(path)
}
fn remove_file_internal(path : String) -> Bool {
ignore(path)
false
pub fn string_byte_length_internal(content : String) -> Int {
@utils.string_byte_length_internal(content)
}
fn string_byte_length_internal(content : String) -> Int {
content.length()
}
fn native_files_supported_internal() -> Bool {
false
pub fn native_files_supported_internal() -> Bool {
@utils.native_files_supported_internal()
}
+10 -48
View File
@@ -1,75 +1,37 @@
pub type RecordPredicate = (Record) -> Bool
pub type RecordPredicate = @utils.RecordPredicate
pub fn level_at_least(min_level : Level) -> RecordPredicate {
fn(rec) {
rec.level.priority() >= min_level.priority()
}
@utils.level_at_least(min_level)
}
pub fn target_is(target : String) -> RecordPredicate {
fn(rec) {
rec.target == target
}
@utils.target_is(target)
}
pub fn target_has_prefix(prefix : String) -> RecordPredicate {
fn(rec) {
rec.target.has_prefix(prefix)
}
@utils.target_has_prefix(prefix)
}
pub fn message_contains(fragment : String) -> RecordPredicate {
fn(rec) {
rec.message.contains(fragment)
}
@utils.message_contains(fragment)
}
pub fn has_field(key : String) -> RecordPredicate {
fn(rec) {
for field in rec.fields {
if field.key == key {
return true
}
}
false
}
@utils.has_field(key)
}
pub fn field_equals(key : String, value : String) -> RecordPredicate {
fn(rec) {
for field in rec.fields {
if field.key == key && field.value == value {
return true
}
}
false
}
@utils.field_equals(key, value)
}
pub fn not_(predicate : RecordPredicate) -> RecordPredicate {
fn(rec) {
!(predicate(rec))
}
@utils.not_(predicate)
}
pub fn all_of(predicates : Array[RecordPredicate]) -> RecordPredicate {
fn(rec) {
for predicate in predicates {
if !(predicate(rec)) {
return false
}
}
true
}
@utils.all_of(predicates)
}
pub fn any_of(predicates : Array[RecordPredicate]) -> RecordPredicate {
fn(rec) {
for predicate in predicates {
if predicate(rec) {
return true
}
}
false
}
@utils.any_of(predicates)
}
+40 -842
View File
@@ -1,30 +1,12 @@
pub type RecordFormatter = (Record) -> String
pub type RecordFormatter = @utils.RecordFormatter
pub(all) enum ColorMode {
Never
Auto
Always
}
pub type ColorMode = @utils.ColorMode
pub(all) enum ColorSupport {
Basic
TrueColor
}
pub type ColorSupport = @utils.ColorSupport
pub(all) enum StyleMarkupMode {
Disabled
Builtin
Full
}
pub type StyleMarkupMode = @utils.StyleMarkupMode
pub struct TextStyle {
fg : String?
bg : String?
bold : Bool
dim : Bool
italic : Bool
underline : Bool
}
pub type TextStyle = @utils.TextStyle
pub fn text_style(
fg~ : String? = None,
@@ -34,167 +16,39 @@ pub fn text_style(
italic~ : Bool = false,
underline~ : Bool = false,
) -> TextStyle {
{ fg, bg, bold, dim, italic, underline }
@utils.text_style(
fg=fg,
bg=bg,
bold=bold,
dim=dim,
italic=italic,
underline=underline,
)
}
pub struct StyleTagRegistry {
entries : Map[String, TextStyle]
}
fn normalize_style_tag_name(name : String) -> String {
name.trim().to_lower().to_owned()
}
pub type StyleTagRegistry = @utils.StyleTagRegistry
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
}
}
}
@utils.style_tag_registry()
}
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)
@utils.default_style_tag_registry()
}
let global_style_tag_registry_ref : Ref[StyleTagRegistry] = Ref(style_tag_registry())
pub fn global_style_tag_registry() -> StyleTagRegistry {
global_style_tag_registry_ref.val
@utils.global_style_tag_registry()
}
pub fn set_global_style_tag_registry(registry : StyleTagRegistry) -> Unit {
global_style_tag_registry_ref.val = registry
@utils.set_global_style_tag_registry(registry)
}
pub fn reset_global_style_tag_registry() -> Unit {
global_style_tag_registry_ref.val = style_tag_registry()
@utils.reset_global_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 {
show_timestamp : Bool
show_level : Bool
show_target : Bool
show_fields : Bool
separator : String
field_separator : String
template : String
color_mode : ColorMode
color_support : ColorSupport
style_markup : StyleMarkupMode
target_style_markup : StyleMarkupMode
fields_style_markup : StyleMarkupMode
style_tags : StyleTagRegistry?
}
pub type TextFormatter = @utils.TextFormatter
pub fn text_formatter(
show_timestamp~ : Bool = true,
@@ -211,695 +65,39 @@ pub fn text_formatter(
fields_style_markup~ : StyleMarkupMode = StyleMarkupMode::Disabled,
style_tags~ : StyleTagRegistry? = None,
) -> TextFormatter {
{
show_timestamp,
show_level,
show_target,
show_fields,
separator,
field_separator,
template,
color_mode,
color_support,
style_markup,
target_style_markup,
fields_style_markup,
style_tags,
}
@utils.text_formatter(
show_timestamp=show_timestamp,
show_level=show_level,
show_target=show_target,
show_fields=show_fields,
separator=separator,
field_separator=field_separator,
template=template,
color_mode=color_mode,
color_support=color_support,
style_markup=style_markup,
target_style_markup=target_style_markup,
fields_style_markup=fields_style_markup,
style_tags=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 }
@utils.color_support_label(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) }
@utils.style_markup_mode_label(mode)
}
pub fn color_mode_label(mode : ColorMode) -> String {
match mode {
ColorMode::Never => "never"
ColorMode::Auto => "auto"
ColorMode::Always => "always"
}
}
fn use_ansi_color(mode : ColorMode) -> Bool {
match mode {
ColorMode::Never => false
ColorMode::Always => true
ColorMode::Auto => match @env.get_env_var("NO_COLOR") {
Some(_) => false
None => true
}
}
}
fn ansi_wrap(text : String, code : String, enabled : Bool) -> String {
if !enabled || text == "" {
text
} else {
"\u{001b}[\{code}m\{text}\u{001b}[0m"
}
}
fn 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"
}
}
fn fields_to_json(fields : Array[Field]) -> Json {
let obj : Map[String, Json] = {}
for item in fields {
obj[item.key] = Json::string(item.value)
}
Json::object(obj)
}
fn timestamp_text(rec : Record, formatter : TextFormatter) -> String {
if formatter.show_timestamp && rec.timestamp_ms != 0UL {
ansi_wrap(rec.timestamp_ms.to_string(), "90", use_ansi_color(formatter.color_mode))
} else {
""
}
}
fn level_text(rec : Record, formatter : TextFormatter) -> String {
if formatter.show_level {
ansi_wrap(rec.level.label(), level_ansi_code(rec.level), use_ansi_color(formatter.color_mode))
} else {
""
}
}
fn target_text(rec : Record, formatter : TextFormatter) -> String {
if formatter.show_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 {
""
}
}
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 {
if formatter.show_fields && rec.fields.length() != 0 {
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 {
""
}
}
fn render_template(rec : Record, formatter : TextFormatter) -> String {
formatter.template
.replace_all(old="{timestamp}", new=timestamp_text(rec, formatter))
.replace_all(old="{timestamp_ms}", new=timestamp_text(rec, formatter))
.replace_all(old="{level}", new=level_text(rec, formatter))
.replace_all(old="{target}", new=target_text(rec, formatter))
.replace_all(old="{message}", new=render_inline_markup(rec.message, formatter))
.replace_all(old="{fields}", new=fields_text(rec, formatter))
.trim()
.to_owned()
@utils.color_mode_label(mode)
}
pub fn format_text(rec : Record, formatter~ : TextFormatter = text_formatter()) -> String {
if formatter.template != "" {
return render_template(rec, formatter)
}
let parts : Array[String] = []
if formatter.show_timestamp && rec.timestamp_ms != 0UL {
parts.push("[\{timestamp_text(rec, formatter)}]")
}
if formatter.show_level {
parts.push("[\{level_text(rec, formatter)}]")
}
if formatter.show_target && rec.target != "" {
parts.push("[\{target_text(rec, formatter)}]")
}
parts.push(render_inline_markup(rec.message, formatter))
let base = parts.join(formatter.separator)
if !formatter.show_fields || rec.fields.length() == 0 {
base
} else {
let details = fields_text(rec, formatter)
"\{base}\{formatter.separator}\{details}"
}
@utils.format_text(rec, formatter=formatter)
}
pub fn format_json(rec : Record) -> String {
let obj : Map[String, Json] = {
"level": Json::string(rec.level.label()),
"message": Json::string(rec.message),
"fields": fields_to_json(rec.fields),
}
if rec.timestamp_ms != 0UL {
obj["timestamp_ms"] = rec.timestamp_ms.to_json()
}
if rec.target == "" {
Json::object(obj).stringify()
} else {
obj["target"] = Json::string(rec.target)
Json::object(obj).stringify()
}
@utils.format_json(rec)
}
-24
View File
@@ -13,27 +13,3 @@ pub fn set_default_target(target : String) -> Unit {
pub fn default_logger() -> Logger[ConsoleSink] {
Logger::new(default_console_sink, min_level=default_min_level_ref.val, target=default_target_ref.val)
}
pub fn log(level : Level, message : String, fields~ : Array[Field] = []) -> Unit {
default_logger().log(level, message, fields=fields)
}
pub fn trace(message : String, fields~ : Array[Field] = []) -> Unit {
default_logger().trace(message, fields=fields)
}
pub fn debug(message : String, fields~ : Array[Field] = []) -> Unit {
default_logger().debug(message, fields=fields)
}
pub fn info(message : String, fields~ : Array[Field] = []) -> Unit {
default_logger().info(message, fields=fields)
}
pub fn warn(message : String, fields~ : Array[Field] = []) -> Unit {
default_logger().warn(message, fields=fields)
}
pub fn error(message : String, fields~ : Array[Field] = []) -> Unit {
default_logger().error(message, fields=fields)
}
+23
View File
@@ -0,0 +1,23 @@
pub fn log(level : Level, message : String, fields~ : Array[Field] = []) -> Unit {
default_logger().log(level, message, fields=fields)
}
pub fn trace(message : String, fields~ : Array[Field] = []) -> Unit {
default_logger().trace(message, fields=fields)
}
pub fn debug(message : String, fields~ : Array[Field] = []) -> Unit {
default_logger().debug(message, fields=fields)
}
pub fn info(message : String, fields~ : Array[Field] = []) -> Unit {
default_logger().info(message, fields=fields)
}
pub fn warn(message : String, fields~ : Array[Field] = []) -> Unit {
default_logger().warn(message, fields=fields)
}
pub fn error(message : String, fields~ : Array[Field] = []) -> Unit {
default_logger().error(message, fields=fields)
}
+1 -31
View File
@@ -1,31 +1 @@
pub(all) enum Level {
Trace
Debug
Info
Warn
Error
}
pub fn Level::priority(self : Level) -> Int {
match self {
Level::Trace => 10
Level::Debug => 20
Level::Info => 30
Level::Warn => 40
Level::Error => 50
}
}
pub fn Level::label(self : Level) -> String {
match self {
Level::Trace => "TRACE"
Level::Debug => "DEBUG"
Level::Info => "INFO"
Level::Warn => "WARN"
Level::Error => "ERROR"
}
}
pub fn Level::enabled(self : Level, min_level : Level) -> Bool {
self.priority() >= min_level.priority()
}
pub type Level = @core.Level
-38
View File
@@ -82,41 +82,3 @@ pub fn[S] Logger::with_timestamp(self : Logger[S], enabled~ : Bool = true) -> Lo
pub fn[S] Logger::is_enabled(self : Logger[S], level : Level) -> Bool {
level.enabled(self.min_level)
}
pub fn[S : Sink] Logger::log(
self : Logger[S],
level : Level,
message : String,
fields~ : Array[Field] = [],
target? : String = "",
) -> Unit {
if !self.is_enabled(level) {
()
} else {
let actual_target = if target == "" { self.target } else { target }
let timestamp_ms = if self.timestamp { @env.now() } else { 0UL }
self.sink.write(
record(level, message, timestamp_ms=timestamp_ms, target=actual_target, fields=fields),
)
}
}
pub fn[S : Sink] Logger::trace(self : Logger[S], message : String, fields~ : Array[Field] = []) -> Unit {
self.log(Level::Trace, message, fields=fields)
}
pub fn[S : Sink] Logger::debug(self : Logger[S], message : String, fields~ : Array[Field] = []) -> Unit {
self.log(Level::Debug, message, fields=fields)
}
pub fn[S : Sink] Logger::info(self : Logger[S], message : String, fields~ : Array[Field] = []) -> Unit {
self.log(Level::Info, message, fields=fields)
}
pub fn[S : Sink] Logger::warn(self : Logger[S], message : String, fields~ : Array[Field] = []) -> Unit {
self.log(Level::Warn, message, fields=fields)
}
pub fn[S : Sink] Logger::error(self : Logger[S], message : String, fields~ : Array[Field] = []) -> Unit {
self.log(Level::Error, message, fields=fields)
}
+37
View File
@@ -0,0 +1,37 @@
pub fn[S : Sink] Logger::log(
self : Logger[S],
level : Level,
message : String,
fields~ : Array[Field] = [],
target? : String = "",
) -> Unit {
if !self.is_enabled(level) {
()
} else {
let actual_target = if target == "" { self.target } else { target }
let timestamp_ms = if self.timestamp { @env.now() } else { 0UL }
self.sink.write(
record(level, message, timestamp_ms=timestamp_ms, target=actual_target, fields=fields),
)
}
}
pub fn[S : Sink] Logger::trace(self : Logger[S], message : String, fields~ : Array[Field] = []) -> Unit {
self.log(Level::Trace, message, fields=fields)
}
pub fn[S : Sink] Logger::debug(self : Logger[S], message : String, fields~ : Array[Field] = []) -> Unit {
self.log(Level::Debug, message, fields=fields)
}
pub fn[S : Sink] Logger::info(self : Logger[S], message : String, fields~ : Array[Field] = []) -> Unit {
self.log(Level::Info, message, fields=fields)
}
pub fn[S : Sink] Logger::warn(self : Logger[S], message : String, fields~ : Array[Field] = []) -> Unit {
self.log(Level::Warn, message, fields=fields)
}
pub fn[S : Sink] Logger::error(self : Logger[S], message : String, fields~ : Array[Field] = []) -> Unit {
self.log(Level::Error, message, fields=fields)
}
+2
View File
@@ -1,4 +1,6 @@
import {
"Nanaloveyuki/BitLogger/src/core" @core,
"Nanaloveyuki/BitLogger/src/utils" @utils,
"maria/json_parser" @json_parser,
"moonbitlang/core/array",
"moonbitlang/core/builtin",
+8 -48
View File
@@ -1,69 +1,29 @@
pub type RecordPatch = (Record) -> Record
pub type RecordPatch = @utils.RecordPatch
pub fn identity_patch() -> RecordPatch {
fn(rec) { rec }
@utils.identity_patch()
}
pub fn set_target(target : String) -> RecordPatch {
fn(rec) {
{ ..rec, target }
}
@utils.set_target(target)
}
pub fn prefix_message(prefix : String) -> RecordPatch {
fn(rec) {
{ ..rec, message: "\{prefix}\{rec.message}" }
}
@utils.prefix_message(prefix)
}
pub fn append_fields(extra_fields : Array[Field]) -> RecordPatch {
fn(rec) {
if extra_fields.length() == 0 {
rec
} else if rec.fields.length() == 0 {
{ ..rec, fields: extra_fields }
} else {
{ ..rec, fields: rec.fields + extra_fields }
}
}
@utils.append_fields(extra_fields)
}
pub fn redact_field(key : String, placeholder~ : String = "***") -> RecordPatch {
fn(rec) {
{
..rec,
fields: rec.fields.map(fn(field) {
if field.key == key {
{ ..field, value: placeholder }
} else {
field
}
}),
}
}
@utils.redact_field(key, placeholder=placeholder)
}
pub fn redact_fields(keys : Array[String], placeholder~ : String = "***") -> RecordPatch {
fn(rec) {
{
..rec,
fields: rec.fields.map(fn(field) {
if keys.contains(field.key) {
{ ..field, value: placeholder }
} else {
field
}
}),
}
}
@utils.redact_fields(keys, placeholder=placeholder)
}
pub fn compose_patches(patches : Array[RecordPatch]) -> RecordPatch {
fn(rec) {
let mut current = rec
for patch in patches {
current = patch(current)
}
current
}
@utils.compose_patches(patches)
}
+106
View File
@@ -0,0 +1,106 @@
pub fn console(
min_level~ : Level = Level::Info,
target~ : String = "",
timestamp~ : Bool = false,
) -> LoggerConfig {
LoggerConfig::new(
min_level=min_level,
target=target,
timestamp=timestamp,
sink=SinkConfig::new(kind=SinkKind::Console),
)
}
pub fn json_console(
min_level~ : Level = Level::Info,
target~ : String = "",
timestamp~ : Bool = false,
) -> LoggerConfig {
LoggerConfig::new(
min_level=min_level,
target=target,
timestamp=timestamp,
sink=SinkConfig::new(kind=SinkKind::JsonConsole),
)
}
pub fn text_console(
min_level~ : Level = Level::Info,
target~ : String = "",
timestamp~ : Bool = false,
text_formatter~ : TextFormatterConfig = default_text_formatter_config(),
) -> LoggerConfig {
LoggerConfig::new(
min_level=min_level,
target=target,
timestamp=timestamp,
sink=SinkConfig::new(kind=SinkKind::TextConsole, text_formatter=text_formatter),
)
}
pub fn file(
path : String,
min_level~ : Level = Level::Info,
target~ : String = "",
timestamp~ : Bool = false,
append~ : Bool = true,
auto_flush~ : Bool = true,
rotation~ : FileRotation? = None,
text_formatter~ : TextFormatterConfig = default_text_formatter_config(),
) -> LoggerConfig raise ConfigError {
if path == "" {
raise ConfigError::InvalidConfig("File sink requires non-empty path")
}
LoggerConfig::new(
min_level=min_level,
target=target,
timestamp=timestamp,
sink=SinkConfig::new(
kind=SinkKind::File,
path=path,
append=append,
auto_flush=auto_flush,
rotation=rotation,
text_formatter=text_formatter,
),
)
}
pub fn with_queue(
config : LoggerConfig,
max_pending~ : Int = 0,
overflow~ : QueueOverflowPolicy = QueueOverflowPolicy::DropNewest,
) -> LoggerConfig {
LoggerConfig::new(
min_level=config.min_level,
target=config.target,
timestamp=config.timestamp,
sink=config.sink,
queue=Some(QueueConfig::new(max_pending, overflow=overflow)),
)
}
pub fn with_file_rotation(
config : LoggerConfig,
max_bytes : Int,
max_backups~ : Int = 1,
) -> LoggerConfig {
match config.sink.kind {
SinkKind::File =>
LoggerConfig::new(
min_level=config.min_level,
target=config.target,
timestamp=config.timestamp,
sink=SinkConfig::new(
kind=config.sink.kind,
path=config.sink.path,
append=config.sink.append,
auto_flush=config.sink.auto_flush,
rotation=Some(file_rotation(max_bytes, max_backups=max_backups)),
text_formatter=config.sink.text_formatter,
),
queue=config.queue,
)
_ => config
}
}
+179
View File
@@ -0,0 +1,179 @@
test "logger config presets use expected defaults" {
let console_config = console()
inspect(console_config.min_level.label(), content="INFO")
inspect(console_config.target, content="")
inspect(console_config.timestamp, content="false")
inspect(match console_config.sink.kind {
SinkKind::Console => "Console"
_ => "other"
}, content="Console")
inspect(console_config.queue is None, content="true")
let json_config = json_console()
inspect(match json_config.sink.kind {
SinkKind::JsonConsole => "JsonConsole"
_ => "other"
}, content="JsonConsole")
inspect(json_config.queue is None, content="true")
let text_config = text_console()
inspect(match text_config.sink.kind {
SinkKind::TextConsole => "TextConsole"
_ => "other"
}, content="TextConsole")
inspect(text_config.sink.text_formatter.show_timestamp, content="true")
inspect(color_mode_label(text_config.sink.text_formatter.color_mode), content="never")
}
test "file preset uses file sink defaults" {
let config = file("bitlogger.log")
inspect(config.min_level.label(), content="INFO")
inspect(config.target, content="")
inspect(config.timestamp, content="false")
inspect(match config.sink.kind {
SinkKind::File => "File"
_ => "other"
}, content="File")
inspect(config.sink.path, content="bitlogger.log")
inspect(config.sink.append, content="true")
inspect(config.sink.auto_flush, content="true")
inspect(config.sink.rotation is None, content="true")
inspect(config.queue is None, content="true")
}
test "file preset rejects empty path like parser file sink config" {
let preset_error = (fn() -> String raise ConfigError {
ignore(file(""))
"no error"
})() catch {
ConfigError::InvalidConfig(message) => message
}
inspect(preset_error, content="File sink requires non-empty path")
let parser_error = (fn() -> String raise ConfigError {
ignore(parse_logger_config_text("{\"sink\":{\"kind\":\"file\",\"path\":\"\"}}"))
"no error"
})() catch {
ConfigError::InvalidConfig(message) => message
}
inspect(parser_error, content="File sink requires non-empty path")
}
test "preset helpers compose queue and file rotation without losing config" {
let formatter = TextFormatterConfig::new(show_timestamp=false, separator=" | ")
let config = file(
"service.log",
min_level=Level::Warn,
target="svc.worker",
timestamp=true,
append=false,
auto_flush=false,
text_formatter=formatter,
)
let config = with_file_rotation(
with_queue(config, max_pending=32, overflow=QueueOverflowPolicy::DropOldest),
128,
max_backups=3,
)
inspect(config.min_level.label(), content="WARN")
inspect(config.target, content="svc.worker")
inspect(config.timestamp, content="true")
inspect(config.sink.path, content="service.log")
inspect(config.sink.append, content="false")
inspect(config.sink.auto_flush, content="false")
inspect(config.sink.text_formatter.show_timestamp, content="false")
inspect(config.sink.text_formatter.separator, content=" | ")
match config.queue {
Some(queue) => {
inspect(queue.max_pending, content="32")
inspect(match queue.overflow {
QueueOverflowPolicy::DropNewest => "DropNewest"
QueueOverflowPolicy::DropOldest => "DropOldest"
}, content="DropOldest")
}
None => inspect(false, content="true")
}
match config.sink.rotation {
Some(rotation) => {
inspect(rotation.max_bytes, content="128")
inspect(rotation.max_backups, content="3")
}
None => inspect(false, content="true")
}
}
test "queue helper preserves file rotation when applied after rotation" {
let config = with_queue(
with_file_rotation(file("service.log", append=false), 256, max_backups=2),
max_pending=8,
overflow=QueueOverflowPolicy::DropNewest,
)
inspect(config.sink.path, content="service.log")
inspect(config.sink.append, content="false")
match config.queue {
Some(queue) => {
inspect(queue.max_pending, content="8")
inspect(match queue.overflow {
QueueOverflowPolicy::DropNewest => "DropNewest"
QueueOverflowPolicy::DropOldest => "DropOldest"
}, content="DropNewest")
}
None => inspect(false, content="true")
}
match config.sink.rotation {
Some(rotation) => {
inspect(rotation.max_bytes, content="256")
inspect(rotation.max_backups, content="2")
}
None => inspect(false, content="true")
}
}
test "file rotation helper leaves non-file presets unchanged" {
let console_config = with_file_rotation(console(target="console"), 64, max_backups=2)
inspect(match console_config.sink.kind {
SinkKind::Console => "Console"
_ => "other"
}, content="Console")
inspect(console_config.target, content="console")
inspect(console_config.sink.rotation is None, content="true")
let text_config = with_file_rotation(
text_console(target="text", text_formatter=TextFormatterConfig::new(separator=" :: ")),
96,
max_backups=4,
)
inspect(match text_config.sink.kind {
SinkKind::TextConsole => "TextConsole"
_ => "other"
}, content="TextConsole")
inspect(text_config.target, content="text")
inspect(text_config.sink.text_formatter.separator, content=" :: ")
inspect(text_config.sink.rotation is None, content="true")
}
test "file rotation helper keeps queued non-file presets unchanged" {
let config = with_queue(
json_console(min_level=Level::Error, target="json"),
max_pending=4,
overflow=QueueOverflowPolicy::DropOldest,
)
let rotated = with_file_rotation(config, 512, max_backups=5)
inspect(rotated.min_level.label(), content="ERROR")
inspect(rotated.target, content="json")
inspect(match rotated.sink.kind {
SinkKind::JsonConsole => "JsonConsole"
_ => "other"
}, content="JsonConsole")
inspect(rotated.sink.rotation is None, content="true")
match rotated.queue {
Some(queue) => {
inspect(queue.max_pending, content="4")
inspect(match queue.overflow {
QueueOverflowPolicy::DropNewest => "DropNewest"
QueueOverflowPolicy::DropOldest => "DropOldest"
}, content="DropOldest")
}
None => inspect(false, content="true")
}
}
+5 -26
View File
@@ -1,35 +1,14 @@
pub struct Field {
key : String
value : String
}
pub type Field = @core.Field
pub fn field(key : String, value : String) -> Field {
{ key, value }
@core.field(key, value)
}
pub fn fields(entries : Array[(String, String)]) -> Array[Field] {
entries.map(fn(entry) {
field(entry.0, entry.1)
})
@core.fields(entries)
}
pub struct Record {
level : Level
timestamp_ms : UInt64
target : String
message : String
fields : Array[Field]
}
pub fn Record::new(
level : Level,
message : String,
timestamp_ms~ : UInt64 = 0UL,
target~ : String = "",
fields~ : Array[Field] = [],
) -> Record {
{ level, timestamp_ms, target, message, fields }
}
pub type Record = @core.Record
fn record(
level : Level,
@@ -38,5 +17,5 @@ fn record(
target~ : String = "",
fields~ : Array[Field] = [],
) -> Record {
Record::new(level, message, timestamp_ms=timestamp_ms, target=target, fields=fields)
@core.Record::new(level, message, timestamp_ms=timestamp_ms, target=target, fields=fields)
}
+403
View File
@@ -0,0 +1,403 @@
pub fn RuntimeSink::file_available(self : RuntimeSink) -> Bool {
match self {
File(sink) => sink.is_available()
QueuedFile(sink) => sink.sink.is_available()
_ => false
}
}
pub fn RuntimeSink::file_reopen(self : RuntimeSink, append~ : Bool? = None) -> Bool {
match self {
File(sink) => sink.reopen(append=append)
QueuedFile(sink) => sink.sink.reopen(append=append)
_ => false
}
}
pub fn RuntimeSink::file_reopen_with_current_policy(self : RuntimeSink) -> Bool {
match self {
File(sink) => sink.reopen_with_current_policy()
QueuedFile(sink) => sink.sink.reopen_with_current_policy()
_ => false
}
}
pub fn RuntimeSink::file_reopen_append(self : RuntimeSink) -> Bool {
match self {
File(sink) => sink.reopen_append()
QueuedFile(sink) => sink.sink.reopen_append()
_ => false
}
}
pub fn RuntimeSink::file_reopen_truncate(self : RuntimeSink) -> Bool {
match self {
File(sink) => sink.reopen_truncate()
QueuedFile(sink) => sink.sink.reopen_truncate()
_ => false
}
}
pub fn RuntimeSink::file_append_mode(self : RuntimeSink) -> Bool {
match self {
File(sink) => sink.append_mode()
QueuedFile(sink) => sink.sink.append_mode()
_ => false
}
}
pub fn RuntimeSink::file_set_append_mode(self : RuntimeSink, append : Bool) -> Bool {
match self {
File(sink) => {
sink.set_append_mode(append)
true
}
QueuedFile(sink) => {
sink.sink.set_append_mode(append)
true
}
_ => false
}
}
pub fn RuntimeSink::file_path(self : RuntimeSink) -> String {
match self {
File(sink) => sink.path()
QueuedFile(sink) => sink.sink.path()
_ => ""
}
}
pub fn RuntimeSink::file_auto_flush(self : RuntimeSink) -> Bool {
match self {
File(sink) => sink.auto_flush_enabled()
QueuedFile(sink) => sink.sink.auto_flush_enabled()
_ => false
}
}
pub fn RuntimeSink::file_rotation_enabled(self : RuntimeSink) -> Bool {
match self {
File(sink) => sink.rotation_enabled()
QueuedFile(sink) => sink.sink.rotation_enabled()
_ => false
}
}
pub fn RuntimeSink::file_rotation_config(self : RuntimeSink) -> FileRotation? {
match self {
File(sink) => sink.rotation_config()
QueuedFile(sink) => sink.sink.rotation_config()
_ => None
}
}
pub fn RuntimeSink::file_set_auto_flush(self : RuntimeSink, enabled : Bool) -> Bool {
match self {
File(sink) => {
sink.set_auto_flush(enabled)
true
}
QueuedFile(sink) => {
sink.sink.set_auto_flush(enabled)
true
}
_ => false
}
}
pub fn RuntimeSink::file_set_policy(self : RuntimeSink, policy : FileSinkPolicy) -> Bool {
match self {
File(sink) => {
sink.set_policy(policy)
true
}
QueuedFile(sink) => {
sink.sink.set_policy(policy)
true
}
_ => false
}
}
pub fn RuntimeSink::file_set_rotation(self : RuntimeSink, rotation : FileRotation?) -> Bool {
match self {
File(sink) => {
sink.set_rotation(rotation)
true
}
QueuedFile(sink) => {
sink.sink.set_rotation(rotation)
true
}
_ => false
}
}
pub fn RuntimeSink::file_clear_rotation(self : RuntimeSink) -> Bool {
match self {
File(sink) => {
sink.clear_rotation()
true
}
QueuedFile(sink) => {
sink.sink.clear_rotation()
true
}
_ => false
}
}
pub fn RuntimeSink::file_flush(self : RuntimeSink) -> Bool {
match self {
File(sink) => sink.flush()
QueuedFile(sink) => {
ignore(sink.flush())
sink.sink.flush()
}
_ => false
}
}
pub fn RuntimeSink::file_close(self : RuntimeSink) -> Bool {
match self {
File(sink) => sink.close()
QueuedFile(sink) => {
ignore(sink.flush())
sink.sink.close()
}
_ => false
}
}
pub fn RuntimeSink::file_open_failures(self : RuntimeSink) -> Int {
match self {
File(sink) => sink.open_failures()
QueuedFile(sink) => sink.sink.open_failures()
_ => 0
}
}
pub fn RuntimeSink::file_write_failures(self : RuntimeSink) -> Int {
match self {
File(sink) => sink.write_failures()
QueuedFile(sink) => sink.sink.write_failures()
_ => 0
}
}
pub fn RuntimeSink::file_flush_failures(self : RuntimeSink) -> Int {
match self {
File(sink) => sink.flush_failures()
QueuedFile(sink) => sink.sink.flush_failures()
_ => 0
}
}
pub fn RuntimeSink::file_rotation_failures(self : RuntimeSink) -> Int {
match self {
File(sink) => sink.rotation_failures()
QueuedFile(sink) => sink.sink.rotation_failures()
_ => 0
}
}
pub fn RuntimeSink::file_reset_failure_counters(self : RuntimeSink) -> Bool {
match self {
File(sink) => {
sink.reset_failure_counters()
true
}
QueuedFile(sink) => {
sink.sink.reset_failure_counters()
true
}
_ => false
}
}
pub fn RuntimeSink::file_reset_policy(self : RuntimeSink) -> Bool {
match self {
File(sink) => {
sink.reset_policy()
true
}
QueuedFile(sink) => {
sink.sink.reset_policy()
true
}
_ => false
}
}
pub fn RuntimeSink::file_policy(self : RuntimeSink) -> FileSinkPolicy {
match self {
File(sink) => sink.policy()
QueuedFile(sink) => sink.sink.policy()
_ => FileSinkPolicy::new(append=false, auto_flush=false, rotation=None)
}
}
pub fn RuntimeSink::file_default_policy(self : RuntimeSink) -> FileSinkPolicy {
match self {
File(sink) => sink.default_policy()
QueuedFile(sink) => sink.sink.default_policy()
_ => FileSinkPolicy::new(append=false, auto_flush=false, rotation=None)
}
}
pub fn RuntimeSink::file_policy_matches_default(self : RuntimeSink) -> Bool {
match self {
File(sink) => sink.policy_matches_default()
QueuedFile(sink) => sink.sink.policy_matches_default()
_ => false
}
}
pub fn RuntimeSink::file_state(self : RuntimeSink) -> FileSinkState {
match self {
File(sink) => sink.state()
QueuedFile(sink) => sink.sink.state()
_ => FileSinkState::new(
"",
available=false,
append=false,
auto_flush=false,
rotation=None,
open_failures=0,
write_failures=0,
flush_failures=0,
rotation_failures=0,
)
}
}
pub fn RuntimeSink::file_runtime_state(self : RuntimeSink) -> RuntimeFileState? {
match self {
File(sink) => Some(RuntimeFileState::new(sink.state()))
QueuedFile(sink) => Some(
RuntimeFileState::new(
sink.sink.state(),
queued=true,
pending_count=sink.pending_count(),
dropped_count=sink.dropped_count(),
),
)
_ => None
}
}
pub fn ConfiguredLogger::file_available(self : ConfiguredLogger) -> Bool {
self.sink.file_available()
}
pub fn ConfiguredLogger::file_reopen(self : ConfiguredLogger, append~ : Bool? = None) -> Bool {
self.sink.file_reopen(append=append)
}
pub fn ConfiguredLogger::file_reopen_with_current_policy(self : ConfiguredLogger) -> Bool {
self.sink.file_reopen_with_current_policy()
}
pub fn ConfiguredLogger::file_reopen_append(self : ConfiguredLogger) -> Bool {
self.sink.file_reopen_append()
}
pub fn ConfiguredLogger::file_reopen_truncate(self : ConfiguredLogger) -> Bool {
self.sink.file_reopen_truncate()
}
pub fn ConfiguredLogger::file_append_mode(self : ConfiguredLogger) -> Bool {
self.sink.file_append_mode()
}
pub fn ConfiguredLogger::file_set_append_mode(self : ConfiguredLogger, append : Bool) -> Bool {
self.sink.file_set_append_mode(append)
}
pub fn ConfiguredLogger::file_path(self : ConfiguredLogger) -> String {
self.sink.file_path()
}
pub fn ConfiguredLogger::file_auto_flush(self : ConfiguredLogger) -> Bool {
self.sink.file_auto_flush()
}
pub fn ConfiguredLogger::file_rotation_enabled(self : ConfiguredLogger) -> Bool {
self.sink.file_rotation_enabled()
}
pub fn ConfiguredLogger::file_rotation_config(self : ConfiguredLogger) -> FileRotation? {
self.sink.file_rotation_config()
}
pub fn ConfiguredLogger::file_set_auto_flush(self : ConfiguredLogger, enabled : Bool) -> Bool {
self.sink.file_set_auto_flush(enabled)
}
pub fn ConfiguredLogger::file_set_policy(self : ConfiguredLogger, policy : FileSinkPolicy) -> Bool {
self.sink.file_set_policy(policy)
}
pub fn ConfiguredLogger::file_set_rotation(
self : ConfiguredLogger,
rotation : FileRotation?,
) -> Bool {
self.sink.file_set_rotation(rotation)
}
pub fn ConfiguredLogger::file_clear_rotation(self : ConfiguredLogger) -> Bool {
self.sink.file_clear_rotation()
}
pub fn ConfiguredLogger::file_flush(self : ConfiguredLogger) -> Bool {
self.sink.file_flush()
}
pub fn ConfiguredLogger::file_close(self : ConfiguredLogger) -> Bool {
self.sink.file_close()
}
pub fn ConfiguredLogger::file_open_failures(self : ConfiguredLogger) -> Int {
self.sink.file_open_failures()
}
pub fn ConfiguredLogger::file_write_failures(self : ConfiguredLogger) -> Int {
self.sink.file_write_failures()
}
pub fn ConfiguredLogger::file_flush_failures(self : ConfiguredLogger) -> Int {
self.sink.file_flush_failures()
}
pub fn ConfiguredLogger::file_rotation_failures(self : ConfiguredLogger) -> Int {
self.sink.file_rotation_failures()
}
pub fn ConfiguredLogger::file_reset_failure_counters(self : ConfiguredLogger) -> Bool {
self.sink.file_reset_failure_counters()
}
pub fn ConfiguredLogger::file_reset_policy(self : ConfiguredLogger) -> Bool {
self.sink.file_reset_policy()
}
pub fn ConfiguredLogger::file_policy(self : ConfiguredLogger) -> FileSinkPolicy {
self.sink.file_policy()
}
pub fn ConfiguredLogger::file_default_policy(self : ConfiguredLogger) -> FileSinkPolicy {
self.sink.file_default_policy()
}
pub fn ConfiguredLogger::file_policy_matches_default(self : ConfiguredLogger) -> Bool {
self.sink.file_policy_matches_default()
}
pub fn ConfiguredLogger::file_state(self : ConfiguredLogger) -> FileSinkState {
self.sink.file_state()
}
pub fn ConfiguredLogger::file_runtime_state(self : ConfiguredLogger) -> RuntimeFileState? {
self.sink.file_runtime_state()
}
+7 -475
View File
@@ -9,94 +9,30 @@ pub(all) enum RuntimeSink {
QueuedFile(QueuedSink[FileSink])
}
pub struct RuntimeFileState {
file : FileSinkState
queued : Bool
pending_count : Int
dropped_count : Int
}
pub fn RuntimeFileState::new(
file : FileSinkState,
queued~ : Bool = false,
pending_count~ : Int = 0,
dropped_count~ : Int = 0,
) -> RuntimeFileState {
{ file, queued, pending_count, dropped_count }
}
fn file_sink_policy_to_json_value(policy : FileSinkPolicy) -> @json_parser.JsonValue {
let obj : Map[String, @json_parser.JsonValue] = {
"append": @json_parser.JsonValue::Bool(policy.append),
"auto_flush": @json_parser.JsonValue::Bool(policy.auto_flush),
}
match policy.rotation {
None => obj["rotation"] = @json_parser.JsonValue::Null
Some(rotation) => obj["rotation"] = file_rotation_config_to_json(rotation)
}
@json_parser.JsonValue::Object(obj)
}
pub type RuntimeFileState = @utils.RuntimeFileState
pub fn file_sink_policy_to_json(policy : FileSinkPolicy) -> @json_parser.JsonValue {
file_sink_policy_to_json_value(policy)
@utils.file_sink_policy_to_json(policy)
}
pub fn stringify_file_sink_policy(policy : FileSinkPolicy, pretty~ : Bool = false) -> String {
let value = file_sink_policy_to_json_value(policy)
if pretty {
@json_parser.stringify_pretty(value, 2)
} else {
@json_parser.stringify(value)
}
}
fn file_sink_state_to_json_value(state : FileSinkState) -> @json_parser.JsonValue {
let obj : Map[String, @json_parser.JsonValue] = {
"path": @json_parser.JsonValue::String(state.path),
"available": @json_parser.JsonValue::Bool(state.available),
"append": @json_parser.JsonValue::Bool(state.append),
"auto_flush": @json_parser.JsonValue::Bool(state.auto_flush),
"open_failures": @json_parser.JsonValue::Number(state.open_failures.to_double()),
"write_failures": @json_parser.JsonValue::Number(state.write_failures.to_double()),
"flush_failures": @json_parser.JsonValue::Number(state.flush_failures.to_double()),
"rotation_failures": @json_parser.JsonValue::Number(state.rotation_failures.to_double()),
}
match state.rotation {
None => obj["rotation"] = @json_parser.JsonValue::Null
Some(rotation) => obj["rotation"] = file_rotation_config_to_json(rotation)
}
@json_parser.JsonValue::Object(obj)
@utils.stringify_file_sink_policy(policy, pretty=pretty)
}
pub fn file_sink_state_to_json(state : FileSinkState) -> @json_parser.JsonValue {
file_sink_state_to_json_value(state)
@utils.file_sink_state_to_json(state)
}
pub fn stringify_file_sink_state(state : FileSinkState, pretty~ : Bool = false) -> String {
let value = file_sink_state_to_json_value(state)
if pretty {
@json_parser.stringify_pretty(value, 2)
} else {
@json_parser.stringify(value)
}
@utils.stringify_file_sink_state(state, pretty=pretty)
}
pub fn runtime_file_state_to_json(state : RuntimeFileState) -> @json_parser.JsonValue {
@json_parser.JsonValue::Object({
"file": file_sink_state_to_json_value(state.file),
"queued": @json_parser.JsonValue::Bool(state.queued),
"pending_count": @json_parser.JsonValue::Number(state.pending_count.to_double()),
"dropped_count": @json_parser.JsonValue::Number(state.dropped_count.to_double()),
})
@utils.runtime_file_state_to_json(state)
}
pub fn stringify_runtime_file_state(state : RuntimeFileState, pretty~ : Bool = false) -> String {
let value = runtime_file_state_to_json(state)
if pretty {
@json_parser.stringify_pretty(value, 2)
} else {
@json_parser.stringify(value)
}
@utils.stringify_runtime_file_state(state, pretty=pretty)
}
pub impl Sink for RuntimeSink with write(self, rec) {
@@ -177,295 +113,6 @@ pub fn RuntimeSink::dropped_count(self : RuntimeSink) -> Int {
}
}
pub fn RuntimeSink::file_available(self : RuntimeSink) -> Bool {
match self {
File(sink) => sink.is_available()
QueuedFile(sink) => sink.sink.is_available()
_ => false
}
}
pub fn RuntimeSink::file_reopen(self : RuntimeSink, append~ : Bool? = None) -> Bool {
match self {
File(sink) => sink.reopen(append=append)
QueuedFile(sink) => sink.sink.reopen(append=append)
_ => false
}
}
pub fn RuntimeSink::file_reopen_with_current_policy(self : RuntimeSink) -> Bool {
match self {
File(sink) => sink.reopen_with_current_policy()
QueuedFile(sink) => sink.sink.reopen_with_current_policy()
_ => false
}
}
pub fn RuntimeSink::file_reopen_append(self : RuntimeSink) -> Bool {
match self {
File(sink) => sink.reopen_append()
QueuedFile(sink) => sink.sink.reopen_append()
_ => false
}
}
pub fn RuntimeSink::file_reopen_truncate(self : RuntimeSink) -> Bool {
match self {
File(sink) => sink.reopen_truncate()
QueuedFile(sink) => sink.sink.reopen_truncate()
_ => false
}
}
pub fn RuntimeSink::file_append_mode(self : RuntimeSink) -> Bool {
match self {
File(sink) => sink.append_mode()
QueuedFile(sink) => sink.sink.append_mode()
_ => false
}
}
pub fn RuntimeSink::file_set_append_mode(self : RuntimeSink, append : Bool) -> Bool {
match self {
File(sink) => {
sink.set_append_mode(append)
true
}
QueuedFile(sink) => {
sink.sink.set_append_mode(append)
true
}
_ => false
}
}
pub fn RuntimeSink::file_path(self : RuntimeSink) -> String {
match self {
File(sink) => sink.path()
QueuedFile(sink) => sink.sink.path()
_ => ""
}
}
pub fn RuntimeSink::file_auto_flush(self : RuntimeSink) -> Bool {
match self {
File(sink) => sink.auto_flush_enabled()
QueuedFile(sink) => sink.sink.auto_flush_enabled()
_ => false
}
}
pub fn RuntimeSink::file_rotation_enabled(self : RuntimeSink) -> Bool {
match self {
File(sink) => sink.rotation_enabled()
QueuedFile(sink) => sink.sink.rotation_enabled()
_ => false
}
}
pub fn RuntimeSink::file_rotation_config(self : RuntimeSink) -> FileRotation? {
match self {
File(sink) => sink.rotation_config()
QueuedFile(sink) => sink.sink.rotation_config()
_ => None
}
}
pub fn RuntimeSink::file_set_auto_flush(self : RuntimeSink, enabled : Bool) -> Bool {
match self {
File(sink) => {
sink.set_auto_flush(enabled)
true
}
QueuedFile(sink) => {
sink.sink.set_auto_flush(enabled)
true
}
_ => false
}
}
pub fn RuntimeSink::file_set_policy(self : RuntimeSink, policy : FileSinkPolicy) -> Bool {
match self {
File(sink) => {
sink.set_policy(policy)
true
}
QueuedFile(sink) => {
sink.sink.set_policy(policy)
true
}
_ => false
}
}
pub fn RuntimeSink::file_set_rotation(self : RuntimeSink, rotation : FileRotation?) -> Bool {
match self {
File(sink) => {
sink.set_rotation(rotation)
true
}
QueuedFile(sink) => {
sink.sink.set_rotation(rotation)
true
}
_ => false
}
}
pub fn RuntimeSink::file_clear_rotation(self : RuntimeSink) -> Bool {
match self {
File(sink) => {
sink.clear_rotation()
true
}
QueuedFile(sink) => {
sink.sink.clear_rotation()
true
}
_ => false
}
}
pub fn RuntimeSink::file_flush(self : RuntimeSink) -> Bool {
match self {
File(sink) => sink.flush()
QueuedFile(sink) => {
ignore(sink.flush())
sink.sink.flush()
}
_ => false
}
}
pub fn RuntimeSink::file_close(self : RuntimeSink) -> Bool {
match self {
File(sink) => sink.close()
QueuedFile(sink) => {
ignore(sink.flush())
sink.sink.close()
}
_ => false
}
}
pub fn RuntimeSink::file_open_failures(self : RuntimeSink) -> Int {
match self {
File(sink) => sink.open_failures()
QueuedFile(sink) => sink.sink.open_failures()
_ => 0
}
}
pub fn RuntimeSink::file_write_failures(self : RuntimeSink) -> Int {
match self {
File(sink) => sink.write_failures()
QueuedFile(sink) => sink.sink.write_failures()
_ => 0
}
}
pub fn RuntimeSink::file_flush_failures(self : RuntimeSink) -> Int {
match self {
File(sink) => sink.flush_failures()
QueuedFile(sink) => sink.sink.flush_failures()
_ => 0
}
}
pub fn RuntimeSink::file_rotation_failures(self : RuntimeSink) -> Int {
match self {
File(sink) => sink.rotation_failures()
QueuedFile(sink) => sink.sink.rotation_failures()
_ => 0
}
}
pub fn RuntimeSink::file_reset_failure_counters(self : RuntimeSink) -> Bool {
match self {
File(sink) => {
sink.reset_failure_counters()
true
}
QueuedFile(sink) => {
sink.sink.reset_failure_counters()
true
}
_ => false
}
}
pub fn RuntimeSink::file_reset_policy(self : RuntimeSink) -> Bool {
match self {
File(sink) => {
sink.reset_policy()
true
}
QueuedFile(sink) => {
sink.sink.reset_policy()
true
}
_ => false
}
}
pub fn RuntimeSink::file_policy(self : RuntimeSink) -> FileSinkPolicy {
match self {
File(sink) => sink.policy()
QueuedFile(sink) => sink.sink.policy()
_ => FileSinkPolicy::new(append=false, auto_flush=false, rotation=None)
}
}
pub fn RuntimeSink::file_default_policy(self : RuntimeSink) -> FileSinkPolicy {
match self {
File(sink) => sink.default_policy()
QueuedFile(sink) => sink.sink.default_policy()
_ => FileSinkPolicy::new(append=false, auto_flush=false, rotation=None)
}
}
pub fn RuntimeSink::file_policy_matches_default(self : RuntimeSink) -> Bool {
match self {
File(sink) => sink.policy_matches_default()
QueuedFile(sink) => sink.sink.policy_matches_default()
_ => false
}
}
pub fn RuntimeSink::file_state(self : RuntimeSink) -> FileSinkState {
match self {
File(sink) => sink.state()
QueuedFile(sink) => sink.sink.state()
_ => FileSinkState::new(
"",
available=false,
append=false,
auto_flush=false,
rotation=None,
open_failures=0,
write_failures=0,
flush_failures=0,
rotation_failures=0,
)
}
}
pub fn RuntimeSink::file_runtime_state(self : RuntimeSink) -> RuntimeFileState? {
match self {
File(sink) => Some(RuntimeFileState::new(sink.state()))
QueuedFile(sink) => Some(
RuntimeFileState::new(
sink.sink.state(),
queued=true,
pending_count=sink.pending_count(),
dropped_count=sink.dropped_count(),
),
)
_ => None
}
}
pub type ConfiguredLogger = Logger[RuntimeSink]
pub fn ConfiguredLogger::flush(self : ConfiguredLogger) -> Int {
@@ -488,121 +135,6 @@ pub fn ConfiguredLogger::dropped_count(self : ConfiguredLogger) -> Int {
self.sink.dropped_count()
}
pub fn ConfiguredLogger::file_available(self : ConfiguredLogger) -> Bool {
self.sink.file_available()
}
pub fn ConfiguredLogger::file_reopen(self : ConfiguredLogger, append~ : Bool? = None) -> Bool {
self.sink.file_reopen(append=append)
}
pub fn ConfiguredLogger::file_reopen_with_current_policy(self : ConfiguredLogger) -> Bool {
self.sink.file_reopen_with_current_policy()
}
pub fn ConfiguredLogger::file_reopen_append(self : ConfiguredLogger) -> Bool {
self.sink.file_reopen_append()
}
pub fn ConfiguredLogger::file_reopen_truncate(self : ConfiguredLogger) -> Bool {
self.sink.file_reopen_truncate()
}
pub fn ConfiguredLogger::file_append_mode(self : ConfiguredLogger) -> Bool {
self.sink.file_append_mode()
}
pub fn ConfiguredLogger::file_set_append_mode(self : ConfiguredLogger, append : Bool) -> Bool {
self.sink.file_set_append_mode(append)
}
pub fn ConfiguredLogger::file_path(self : ConfiguredLogger) -> String {
self.sink.file_path()
}
pub fn ConfiguredLogger::file_auto_flush(self : ConfiguredLogger) -> Bool {
self.sink.file_auto_flush()
}
pub fn ConfiguredLogger::file_rotation_enabled(self : ConfiguredLogger) -> Bool {
self.sink.file_rotation_enabled()
}
pub fn ConfiguredLogger::file_rotation_config(self : ConfiguredLogger) -> FileRotation? {
self.sink.file_rotation_config()
}
pub fn ConfiguredLogger::file_set_auto_flush(self : ConfiguredLogger, enabled : Bool) -> Bool {
self.sink.file_set_auto_flush(enabled)
}
pub fn ConfiguredLogger::file_set_policy(self : ConfiguredLogger, policy : FileSinkPolicy) -> Bool {
self.sink.file_set_policy(policy)
}
pub fn ConfiguredLogger::file_set_rotation(
self : ConfiguredLogger,
rotation : FileRotation?,
) -> Bool {
self.sink.file_set_rotation(rotation)
}
pub fn ConfiguredLogger::file_clear_rotation(self : ConfiguredLogger) -> Bool {
self.sink.file_clear_rotation()
}
pub fn ConfiguredLogger::file_flush(self : ConfiguredLogger) -> Bool {
self.sink.file_flush()
}
pub fn ConfiguredLogger::file_close(self : ConfiguredLogger) -> Bool {
self.sink.file_close()
}
pub fn ConfiguredLogger::file_open_failures(self : ConfiguredLogger) -> Int {
self.sink.file_open_failures()
}
pub fn ConfiguredLogger::file_write_failures(self : ConfiguredLogger) -> Int {
self.sink.file_write_failures()
}
pub fn ConfiguredLogger::file_flush_failures(self : ConfiguredLogger) -> Int {
self.sink.file_flush_failures()
}
pub fn ConfiguredLogger::file_rotation_failures(self : ConfiguredLogger) -> Int {
self.sink.file_rotation_failures()
}
pub fn ConfiguredLogger::file_reset_failure_counters(self : ConfiguredLogger) -> Bool {
self.sink.file_reset_failure_counters()
}
pub fn ConfiguredLogger::file_reset_policy(self : ConfiguredLogger) -> Bool {
self.sink.file_reset_policy()
}
pub fn ConfiguredLogger::file_policy(self : ConfiguredLogger) -> FileSinkPolicy {
self.sink.file_policy()
}
pub fn ConfiguredLogger::file_default_policy(self : ConfiguredLogger) -> FileSinkPolicy {
self.sink.file_default_policy()
}
pub fn ConfiguredLogger::file_policy_matches_default(self : ConfiguredLogger) -> Bool {
self.sink.file_policy_matches_default()
}
pub fn ConfiguredLogger::file_state(self : ConfiguredLogger) -> FileSinkState {
self.sink.file_state()
}
pub fn ConfiguredLogger::file_runtime_state(self : ConfiguredLogger) -> RuntimeFileState? {
self.sink.file_runtime_state()
}
fn build_runtime_sink(config : SinkConfig) -> RuntimeSink {
match config.kind {
SinkKind::Console => RuntimeSink::Console(console_sink())
+3 -379
View File
@@ -28,7 +28,7 @@ pub impl[S : Sink] Sink for ContextSink[S] with write(self, rec) {
} else {
self.context_fields + rec.fields
}
self.sink.write({ ..rec, fields: merged })
self.sink.write(rec.with_fields(merged))
}
pub struct JsonConsoleSink {
@@ -44,379 +44,6 @@ pub impl Sink for JsonConsoleSink with write(self, rec) {
println(format_json(rec))
}
pub struct FileSink {
path : String
append : Ref[Bool]
default_append : Bool
handle : Ref[FileHandle?]
formatter : RecordFormatter
auto_flush : Ref[Bool]
default_auto_flush : Bool
rotation : Ref[FileRotation?]
default_rotation : FileRotation?
open_failures : Ref[Int]
write_failures : Ref[Int]
flush_failures : Ref[Int]
rotation_failures : Ref[Int]
}
pub struct FileRotation {
max_bytes : Int
max_backups : Int
}
pub struct FileSinkState {
path : String
available : Bool
append : Bool
auto_flush : Bool
rotation : FileRotation?
open_failures : Int
write_failures : Int
flush_failures : Int
rotation_failures : Int
}
pub struct FileSinkPolicy {
append : Bool
auto_flush : Bool
rotation : FileRotation?
}
pub fn FileSinkPolicy::new(
append~ : Bool = true,
auto_flush~ : Bool = true,
rotation~ : FileRotation? = None,
) -> FileSinkPolicy {
{ append, auto_flush, rotation }
}
pub fn FileSinkState::new(
path : String,
available~ : Bool = false,
append~ : Bool = true,
auto_flush~ : Bool = true,
rotation~ : FileRotation? = None,
open_failures~ : Int = 0,
write_failures~ : Int = 0,
flush_failures~ : Int = 0,
rotation_failures~ : Int = 0,
) -> FileSinkState {
{
path,
available,
append,
auto_flush,
rotation,
open_failures,
write_failures,
flush_failures,
rotation_failures,
}
}
pub fn file_rotation(max_bytes : Int, max_backups~ : Int = 1) -> FileRotation {
{
max_bytes: if max_bytes <= 0 { 1 } else { max_bytes },
max_backups: if max_backups <= 0 { 1 } else { max_backups },
}
}
pub fn native_files_supported() -> Bool {
native_files_supported_internal()
}
pub fn file_sink(
path : String,
append~ : Bool = true,
auto_flush~ : Bool = true,
rotation~ : FileRotation? = None,
formatter~ : RecordFormatter = fn(rec) {
format_text(rec)
},
) -> FileSink {
let handle = open_file_handle_internal(path, append)
{
path,
append: Ref(append),
default_append: append,
handle: Ref(handle),
formatter,
auto_flush: Ref(auto_flush),
default_auto_flush: auto_flush,
rotation: Ref(rotation),
default_rotation: rotation,
open_failures: Ref(if handle is Some(_) { 0 } else { 1 }),
write_failures: Ref(0),
flush_failures: Ref(0),
rotation_failures: Ref(0),
}
}
pub fn FileSink::is_available(self : FileSink) -> Bool {
self.handle.val is Some(_)
}
pub fn FileSink::flush(self : FileSink) -> Bool {
match self.handle.val {
None => false
Some(handle) => {
let ok = flush_file_handle_internal(handle)
if !ok {
self.flush_failures.val += 1
}
ok
}
}
}
pub fn FileSink::append_mode(self : FileSink) -> Bool {
self.append.val
}
pub fn FileSink::set_append_mode(self : FileSink, append : Bool) -> Unit {
self.append.val = append
}
pub fn FileSink::path(self : FileSink) -> String {
self.path
}
pub fn FileSink::auto_flush_enabled(self : FileSink) -> Bool {
self.auto_flush.val
}
pub fn FileSink::rotation_enabled(self : FileSink) -> Bool {
self.rotation.val is Some(_)
}
pub fn FileSink::rotation_config(self : FileSink) -> FileRotation? {
self.rotation.val
}
pub fn FileSink::set_auto_flush(self : FileSink, enabled : Bool) -> Unit {
self.auto_flush.val = enabled
}
pub fn FileSink::set_policy(self : FileSink, policy : FileSinkPolicy) -> Unit {
self.append.val = policy.append
self.auto_flush.val = policy.auto_flush
self.rotation.val = policy.rotation
}
pub fn FileSink::set_rotation(self : FileSink, rotation : FileRotation?) -> Unit {
self.rotation.val = rotation
}
pub fn FileSink::clear_rotation(self : FileSink) -> Unit {
self.rotation.val = None
}
pub fn FileSink::close(self : FileSink) -> Bool {
match self.handle.val {
None => false
Some(handle) => {
let ok = close_file_handle_internal(handle)
self.handle.val = None
ok
}
}
}
pub fn FileSink::rotation_failures(self : FileSink) -> Int {
self.rotation_failures.val
}
pub fn FileSink::open_failures(self : FileSink) -> Int {
self.open_failures.val
}
pub fn FileSink::write_failures(self : FileSink) -> Int {
self.write_failures.val
}
pub fn FileSink::flush_failures(self : FileSink) -> Int {
self.flush_failures.val
}
pub fn FileSink::reset_failure_counters(self : FileSink) -> Unit {
self.open_failures.val = 0
self.write_failures.val = 0
self.flush_failures.val = 0
self.rotation_failures.val = 0
}
pub fn FileSink::reset_policy(self : FileSink) -> Unit {
self.append.val = self.default_append
self.auto_flush.val = self.default_auto_flush
self.rotation.val = self.default_rotation
}
pub fn FileSink::policy(self : FileSink) -> FileSinkPolicy {
FileSinkPolicy::new(
append=self.append.val,
auto_flush=self.auto_flush.val,
rotation=self.rotation.val,
)
}
pub fn FileSink::default_policy(self : FileSink) -> FileSinkPolicy {
FileSinkPolicy::new(
append=self.default_append,
auto_flush=self.default_auto_flush,
rotation=self.default_rotation,
)
}
pub fn FileSink::policy_matches_default(self : FileSink) -> Bool {
let current = self.policy()
let default = self.default_policy()
current.append == default.append &&
current.auto_flush == default.auto_flush &&
policy_rotation_equals_internal(current.rotation, default.rotation)
}
fn policy_rotation_equals_internal(left : FileRotation?, right : FileRotation?) -> Bool {
match (left, right) {
(None, None) => true
(Some(a), Some(b)) => a.max_bytes == b.max_bytes && a.max_backups == b.max_backups
_ => false
}
}
pub fn FileSink::state(self : FileSink) -> FileSinkState {
{
path: self.path,
available: self.is_available(),
append: self.append.val,
auto_flush: self.auto_flush.val,
rotation: self.rotation.val,
open_failures: self.open_failures.val,
write_failures: self.write_failures.val,
flush_failures: self.flush_failures.val,
rotation_failures: self.rotation_failures.val,
}
}
pub fn FileSink::reopen(self : FileSink, append~ : Bool? = None) -> Bool {
let append_mode = append.unwrap_or(self.append.val)
self.append.val = append_mode
match self.handle.val {
None => ()
Some(handle) => {
ignore(close_file_handle_internal(handle))
self.handle.val = None
}
}
let reopened = open_file_handle_internal(self.path, append_mode)
self.handle.val = reopened
if reopened is Some(_) {
true
} else {
self.open_failures.val += 1
false
}
}
pub fn FileSink::reopen_with_current_policy(self : FileSink) -> Bool {
self.reopen()
}
pub fn FileSink::reopen_append(self : FileSink) -> Bool {
self.reopen(append=Some(true))
}
pub fn FileSink::reopen_truncate(self : FileSink) -> Bool {
self.reopen(append=Some(false))
}
fn rotated_file_path(path : String, index : Int) -> String {
"\{path}.\{index}"
}
fn rotate_file_sink_internal(sink : FileSink, rotation : FileRotation) -> Bool {
let closed = match sink.handle.val {
None => true
Some(handle) => {
let ok = close_file_handle_internal(handle)
sink.handle.val = None
ok
}
}
if !closed {
return false
}
if rotation.max_backups > 0 {
ignore(remove_file_internal(rotated_file_path(sink.path, rotation.max_backups)))
for index = rotation.max_backups - 1; index >= 1; {
let from_path = rotated_file_path(sink.path, index)
let to_path = rotated_file_path(sink.path, index + 1)
ignore(rename_file_internal(from_path, to_path))
continue index - 1
}
ignore(rename_file_internal(sink.path, rotated_file_path(sink.path, 1)))
} else {
ignore(remove_file_internal(sink.path))
}
sink.handle.val = open_file_handle_internal(sink.path, false)
sink.handle.val is Some(_)
}
fn rotate_if_needed_internal(sink : FileSink, next_line_bytes : Int) -> Bool {
match sink.rotation.val {
None => true
Some(rotation) => match sink.handle.val {
None => false
Some(handle) => {
let size = file_size_internal(handle)
if size + next_line_bytes <= rotation.max_bytes {
true
} else {
let rotated = rotate_file_sink_internal(sink, rotation)
if !rotated {
sink.rotation_failures.val += 1
}
rotated
}
}
}
}
}
pub impl Sink for FileSink with write(self, rec) {
match self.handle.val {
None => {
self.write_failures.val += 1
}
Some(_) => {
let line = "\{(self.formatter)(rec)}\n"
let can_write = rotate_if_needed_internal(self, string_byte_length_internal(line))
if can_write {
match self.handle.val {
None => {
self.write_failures.val += 1
}
Some(active) => {
let wrote = write_file_handle_internal(active, line)
if wrote {
if self.auto_flush.val {
let flushed = flush_file_handle_internal(active)
if !flushed {
self.flush_failures.val += 1
}
}
} else {
self.write_failures.val += 1
}
}
}
} else {
self.write_failures.val += 1
}
}
}
}
pub struct FormattedConsoleSink {
formatter : RecordFormatter
}
@@ -471,7 +98,7 @@ pub fn[A, B] fanout_sink(left : A, right : B) -> FanoutSink[A, B] {
pub impl[A : Sink, B : Sink] Sink for FanoutSink[A, B] with write(self, rec) {
self.left.write(rec)
self.right.write({ ..rec })
self.right.write(rec.copy())
}
pub struct SplitSink[A, B] {
@@ -548,10 +175,7 @@ pub impl[S : Sink] Sink for BufferedSink[S] with write(self, rec) {
}
}
pub(all) enum QueueOverflowPolicy {
DropNewest
DropOldest
}
pub type QueueOverflowPolicy = @utils.QueueOverflowPolicy
pub struct QueuedSink[S] {
sink : S
+320
View File
@@ -0,0 +1,320 @@
pub struct FileSink {
path : String
append : Ref[Bool]
default_append : Bool
handle : Ref[FileHandle?]
formatter : RecordFormatter
auto_flush : Ref[Bool]
default_auto_flush : Bool
rotation : Ref[FileRotation?]
default_rotation : FileRotation?
open_failures : Ref[Int]
write_failures : Ref[Int]
flush_failures : Ref[Int]
rotation_failures : Ref[Int]
}
pub type FileRotation = @utils.FileRotation
pub type FileSinkState = @utils.FileSinkState
pub type FileSinkPolicy = @utils.FileSinkPolicy
pub fn file_rotation(max_bytes : Int, max_backups~ : Int = 1) -> FileRotation {
@utils.file_rotation(max_bytes, max_backups=max_backups)
}
pub fn native_files_supported() -> Bool {
native_files_supported_internal()
}
pub fn file_sink(
path : String,
append~ : Bool = true,
auto_flush~ : Bool = true,
rotation~ : FileRotation? = None,
formatter~ : RecordFormatter = fn(rec) {
format_text(rec)
},
) -> FileSink {
let handle = open_file_handle_internal(path, append)
{
path,
append: Ref(append),
default_append: append,
handle: Ref(handle),
formatter,
auto_flush: Ref(auto_flush),
default_auto_flush: auto_flush,
rotation: Ref(rotation),
default_rotation: rotation,
open_failures: Ref(if handle is Some(_) { 0 } else { 1 }),
write_failures: Ref(0),
flush_failures: Ref(0),
rotation_failures: Ref(0),
}
}
pub fn FileSink::is_available(self : FileSink) -> Bool {
self.handle.val is Some(_)
}
pub fn FileSink::flush(self : FileSink) -> Bool {
match self.handle.val {
None => false
Some(handle) => {
let ok = flush_file_handle_internal(handle)
if !ok {
self.flush_failures.val += 1
}
ok
}
}
}
pub fn FileSink::append_mode(self : FileSink) -> Bool {
self.append.val
}
pub fn FileSink::set_append_mode(self : FileSink, append : Bool) -> Unit {
self.append.val = append
}
pub fn FileSink::path(self : FileSink) -> String {
self.path
}
pub fn FileSink::auto_flush_enabled(self : FileSink) -> Bool {
self.auto_flush.val
}
pub fn FileSink::rotation_enabled(self : FileSink) -> Bool {
self.rotation.val is Some(_)
}
pub fn FileSink::rotation_config(self : FileSink) -> FileRotation? {
self.rotation.val
}
pub fn FileSink::set_auto_flush(self : FileSink, enabled : Bool) -> Unit {
self.auto_flush.val = enabled
}
pub fn FileSink::set_policy(self : FileSink, policy : FileSinkPolicy) -> Unit {
self.append.val = policy.append
self.auto_flush.val = policy.auto_flush
self.rotation.val = policy.rotation
}
pub fn FileSink::set_rotation(self : FileSink, rotation : FileRotation?) -> Unit {
self.rotation.val = rotation
}
pub fn FileSink::clear_rotation(self : FileSink) -> Unit {
self.rotation.val = None
}
pub fn FileSink::close(self : FileSink) -> Bool {
match self.handle.val {
None => false
Some(handle) => {
let ok = close_file_handle_internal(handle)
self.handle.val = None
ok
}
}
}
pub fn FileSink::rotation_failures(self : FileSink) -> Int {
self.rotation_failures.val
}
pub fn FileSink::open_failures(self : FileSink) -> Int {
self.open_failures.val
}
pub fn FileSink::write_failures(self : FileSink) -> Int {
self.write_failures.val
}
pub fn FileSink::flush_failures(self : FileSink) -> Int {
self.flush_failures.val
}
pub fn FileSink::reset_failure_counters(self : FileSink) -> Unit {
self.open_failures.val = 0
self.write_failures.val = 0
self.flush_failures.val = 0
self.rotation_failures.val = 0
}
pub fn FileSink::reset_policy(self : FileSink) -> Unit {
self.append.val = self.default_append
self.auto_flush.val = self.default_auto_flush
self.rotation.val = self.default_rotation
}
pub fn FileSink::policy(self : FileSink) -> FileSinkPolicy {
FileSinkPolicy::new(
append=self.append.val,
auto_flush=self.auto_flush.val,
rotation=self.rotation.val,
)
}
pub fn FileSink::default_policy(self : FileSink) -> FileSinkPolicy {
FileSinkPolicy::new(
append=self.default_append,
auto_flush=self.default_auto_flush,
rotation=self.default_rotation,
)
}
pub fn FileSink::policy_matches_default(self : FileSink) -> Bool {
let current = self.policy()
let default = self.default_policy()
current.append == default.append &&
current.auto_flush == default.auto_flush &&
policy_rotation_equals_internal(current.rotation, default.rotation)
}
fn policy_rotation_equals_internal(left : FileRotation?, right : FileRotation?) -> Bool {
match (left, right) {
(None, None) => true
(Some(a), Some(b)) => a.max_bytes == b.max_bytes && a.max_backups == b.max_backups
_ => false
}
}
pub fn FileSink::state(self : FileSink) -> FileSinkState {
FileSinkState::new(
self.path,
available=self.is_available(),
append=self.append.val,
auto_flush=self.auto_flush.val,
rotation=self.rotation.val,
open_failures=self.open_failures.val,
write_failures=self.write_failures.val,
flush_failures=self.flush_failures.val,
rotation_failures=self.rotation_failures.val,
)
}
pub fn FileSink::reopen(self : FileSink, append~ : Bool? = None) -> Bool {
let append_mode = append.unwrap_or(self.append.val)
self.append.val = append_mode
match self.handle.val {
None => ()
Some(handle) => {
ignore(close_file_handle_internal(handle))
self.handle.val = None
}
}
let reopened = open_file_handle_internal(self.path, append_mode)
self.handle.val = reopened
if reopened is Some(_) {
true
} else {
self.open_failures.val += 1
false
}
}
pub fn FileSink::reopen_with_current_policy(self : FileSink) -> Bool {
self.reopen()
}
pub fn FileSink::reopen_append(self : FileSink) -> Bool {
self.reopen(append=Some(true))
}
pub fn FileSink::reopen_truncate(self : FileSink) -> Bool {
self.reopen(append=Some(false))
}
fn rotated_file_path(path : String, index : Int) -> String {
"\{path}.\{index}"
}
fn rotate_file_sink_internal(sink : FileSink, rotation : FileRotation) -> Bool {
let closed = match sink.handle.val {
None => true
Some(handle) => {
let ok = close_file_handle_internal(handle)
sink.handle.val = None
ok
}
}
if !closed {
return false
}
if rotation.max_backups > 0 {
ignore(remove_file_internal(rotated_file_path(sink.path, rotation.max_backups)))
for index = rotation.max_backups - 1; index >= 1; {
let from_path = rotated_file_path(sink.path, index)
let to_path = rotated_file_path(sink.path, index + 1)
ignore(rename_file_internal(from_path, to_path))
continue index - 1
}
ignore(rename_file_internal(sink.path, rotated_file_path(sink.path, 1)))
} else {
ignore(remove_file_internal(sink.path))
}
sink.handle.val = open_file_handle_internal(sink.path, false)
sink.handle.val is Some(_)
}
fn rotate_if_needed_internal(sink : FileSink, next_line_bytes : Int) -> Bool {
match sink.rotation.val {
None => true
Some(rotation) => match sink.handle.val {
None => false
Some(handle) => {
let size = file_size_internal(handle)
if size + next_line_bytes <= rotation.max_bytes {
true
} else {
let rotated = rotate_file_sink_internal(sink, rotation)
if !rotated {
sink.rotation_failures.val += 1
}
rotated
}
}
}
}
}
pub impl Sink for FileSink with write(self, rec) {
match self.handle.val {
None => {
self.write_failures.val += 1
}
Some(_) => {
let line = "\{(self.formatter)(rec)}\n"
let can_write = rotate_if_needed_internal(self, string_byte_length_internal(line))
if can_write {
match self.handle.val {
None => {
self.write_failures.val += 1
}
Some(active) => {
let wrote = write_file_handle_internal(active, line)
if wrote {
if self.auto_flush.val {
let flushed = flush_file_handle_internal(active)
if !flushed {
self.flush_failures.val += 1
}
}
} else {
self.write_failures.val += 1
}
}
}
} else {
self.write_failures.val += 1
}
}
}
}
+536
View File
@@ -0,0 +1,536 @@
pub(all) suberror ConfigError {
InvalidConfig(String)
}
pub(all) enum SinkKind {
Console
JsonConsole
TextConsole
File
}
pub struct TextFormatterConfig {
show_timestamp : Bool
show_level : Bool
show_target : Bool
show_fields : Bool
separator : String
field_separator : 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(
show_timestamp~ : Bool = true,
show_level~ : Bool = true,
show_target~ : Bool = true,
show_fields~ : Bool = true,
separator~ : String = " ",
field_separator~ : 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 {
{
show_timestamp,
show_level,
show_target,
show_fields,
separator,
field_separator,
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 {
text_formatter(
show_timestamp=self.show_timestamp,
show_level=self.show_level,
show_target=self.show_target,
show_fields=self.show_fields,
separator=self.separator,
field_separator=self.field_separator,
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))
},
)
}
pub struct QueueConfig {
max_pending : Int
overflow : QueueOverflowPolicy
}
pub fn QueueConfig::new(
max_pending : Int,
overflow~ : QueueOverflowPolicy = QueueOverflowPolicy::DropNewest,
) -> QueueConfig {
{ max_pending, overflow }
}
pub struct SinkConfig {
kind : SinkKind
path : String
append : Bool
auto_flush : Bool
rotation : FileRotation?
text_formatter : TextFormatterConfig
}
pub fn SinkConfig::new(
kind~ : SinkKind = SinkKind::Console,
path~ : String = "",
append~ : Bool = true,
auto_flush~ : Bool = true,
rotation~ : FileRotation? = None,
text_formatter~ : TextFormatterConfig = default_text_formatter_config(),
) -> SinkConfig {
{
kind,
path,
append,
auto_flush,
rotation,
text_formatter,
}
}
pub struct LoggerConfig {
min_level : @core.Level
target : String
timestamp : Bool
sink : SinkConfig
queue : QueueConfig?
}
pub fn LoggerConfig::new(
min_level~ : @core.Level = @core.Level::Info,
target~ : String = "",
timestamp~ : Bool = false,
sink~ : SinkConfig = default_sink_config(),
queue~ : QueueConfig? = None,
) -> LoggerConfig {
{
min_level,
target,
timestamp,
sink,
queue,
}
}
pub fn default_text_formatter_config() -> TextFormatterConfig {
TextFormatterConfig::new()
}
pub fn default_sink_config() -> SinkConfig {
SinkConfig::new()
}
pub fn default_logger_config() -> LoggerConfig {
LoggerConfig::new()
}
fn expect_object(
value : @json_parser.JsonValue,
context : String,
) -> Map[String, @json_parser.JsonValue] raise ConfigError {
match value.as_object() {
Some(obj) => obj
None => raise ConfigError::InvalidConfig("Expected object at " + context)
}
}
fn get_string(
obj : Map[String, @json_parser.JsonValue],
key : String,
default~ : String = "",
) -> String raise ConfigError {
match obj.get(key) {
None => default
Some(value) => match value.as_string() {
Some(text) => text
None => raise ConfigError::InvalidConfig("Expected string at key " + key)
}
}
}
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(
obj : Map[String, @json_parser.JsonValue],
key : String,
default~ : Bool,
) -> Bool raise ConfigError {
match obj.get(key) {
None => default
Some(value) => match value.as_bool() {
Some(flag) => flag
None => raise ConfigError::InvalidConfig("Expected bool at key " + key)
}
}
}
fn get_int(
obj : Map[String, @json_parser.JsonValue],
key : String,
default~ : Int,
) -> Int raise ConfigError {
match obj.get(key) {
None => default
Some(value) => match value.as_number() {
Some(number) => number.to_int()
None => raise ConfigError::InvalidConfig("Expected number at key " + key)
}
}
}
fn parse_level(name : String) -> @core.Level raise ConfigError {
match name.to_upper() {
"TRACE" => @core.Level::Trace
"DEBUG" => @core.Level::Debug
"INFO" => @core.Level::Info
"WARN" => @core.Level::Warn
"ERROR" => @core.Level::Error
_ => raise ConfigError::InvalidConfig("Unsupported level: " + name)
}
}
fn parse_overflow(name : String) -> QueueOverflowPolicy raise ConfigError {
match name.to_upper() {
"DROPNEWEST" => QueueOverflowPolicy::DropNewest
"DROPPOLDEST" => QueueOverflowPolicy::DropOldest
"DROPOLDEST" => QueueOverflowPolicy::DropOldest
_ => raise ConfigError::InvalidConfig("Unsupported queue overflow policy: " + name)
}
}
fn parse_sink_kind(name : String) -> SinkKind raise ConfigError {
match name.to_upper() {
"CONSOLE" => SinkKind::Console
"JSON_CONSOLE" => SinkKind::JsonConsole
"JSONCONSOLE" => SinkKind::JsonConsole
"TEXT_CONSOLE" => SinkKind::TextConsole
"TEXTCONSOLE" => SinkKind::TextConsole
"FILE" => SinkKind::File
_ => raise ConfigError::InvalidConfig("Unsupported sink kind: " + name)
}
}
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 {
match kind {
SinkKind::Console => "console"
SinkKind::JsonConsole => "json_console"
SinkKind::TextConsole => "text_console"
SinkKind::File => "file"
}
}
fn parse_text_formatter_config(value : @json_parser.JsonValue) -> TextFormatterConfig raise ConfigError {
let obj = expect_object(value, "text_formatter")
TextFormatterConfig::new(
show_timestamp=get_bool(obj, "show_timestamp", default=true),
show_level=get_bool(obj, "show_level", default=true),
show_target=get_bool(obj, "show_target", default=true),
show_fields=get_bool(obj, "show_fields", default=true),
separator=get_string(obj, "separator", default=" "),
field_separator=get_string(obj, "field_separator", default=" "),
template=get_string(obj, "template", default=""),
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 {
let obj = expect_object(value, "queue")
QueueConfig::new(
get_int(obj, "max_pending", default=0),
overflow=parse_overflow(get_string(obj, "overflow", default="DropNewest")),
)
}
fn parse_file_rotation_config(value : @json_parser.JsonValue) -> FileRotation raise ConfigError {
let obj = expect_object(value, "sink.rotation")
file_rotation(
get_int(obj, "max_bytes", default=1),
max_backups=get_int(obj, "max_backups", default=1),
)
}
fn parse_sink_config(value : @json_parser.JsonValue) -> SinkConfig raise ConfigError {
let obj = expect_object(value, "sink")
let kind = parse_sink_kind(get_string(obj, "kind", default="console"))
let formatter = match obj.get("text_formatter") {
None => default_text_formatter_config()
Some(inner) => parse_text_formatter_config(inner)
}
let path = get_string(obj, "path", default="")
match kind {
SinkKind::File => if path == "" {
raise ConfigError::InvalidConfig("File sink requires non-empty path")
}
_ => ()
}
SinkConfig::new(
kind=kind,
path=path,
append=get_bool(obj, "append", default=true),
auto_flush=get_bool(obj, "auto_flush", default=true),
rotation=match obj.get("rotation") {
None => None
Some(inner) => Some(parse_file_rotation_config(inner))
},
text_formatter=formatter,
)
}
pub fn parse_logger_config_text(input : String) -> LoggerConfig raise ConfigError {
let root = @json_parser.parse(input) catch {
e => raise ConfigError::InvalidConfig("Invalid JSON: " + e.to_string())
}
let obj = expect_object(root, "root")
LoggerConfig::new(
min_level=parse_level(get_string(obj, "min_level", default="INFO")),
target=get_string(obj, "target", default=""),
timestamp=get_bool(obj, "timestamp", default=false),
sink=match obj.get("sink") {
None => default_sink_config()
Some(value) => parse_sink_config(value)
},
queue=match obj.get("queue") {
None => None
Some(value) => Some(parse_queue_config(value))
},
)
}
pub fn queue_config_to_json(queue : QueueConfig) -> @json_parser.JsonValue {
@json_parser.JsonValue::Object({
"max_pending": @json_parser.JsonValue::Number(queue.max_pending.to_double()),
"overflow": @json_parser.JsonValue::String(match queue.overflow {
QueueOverflowPolicy::DropNewest => "DropNewest"
QueueOverflowPolicy::DropOldest => "DropOldest"
}),
})
}
pub fn stringify_queue_config(queue : QueueConfig, pretty~ : Bool = false) -> String {
let value = queue_config_to_json(queue)
if pretty {
@json_parser.stringify_pretty(value, 2)
} else {
@json_parser.stringify(value)
}
}
pub fn text_formatter_config_to_json(config : TextFormatterConfig) -> @json_parser.JsonValue {
let obj : Map[String, @json_parser.JsonValue] = {
"show_timestamp": @json_parser.JsonValue::Bool(config.show_timestamp),
"show_level": @json_parser.JsonValue::Bool(config.show_level),
"show_target": @json_parser.JsonValue::Bool(config.show_target),
"show_fields": @json_parser.JsonValue::Bool(config.show_fields),
"separator": @json_parser.JsonValue::String(config.separator),
"field_separator": @json_parser.JsonValue::String(config.field_separator),
"template": @json_parser.JsonValue::String(config.template),
"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(
config : TextFormatterConfig,
pretty~ : Bool = false,
) -> String {
let value = text_formatter_config_to_json(config)
if pretty {
@json_parser.stringify_pretty(value, 2)
} else {
@json_parser.stringify(value)
}
}
pub fn file_rotation_config_to_json(config : FileRotation) -> @json_parser.JsonValue {
@json_parser.JsonValue::Object({
"max_bytes": @json_parser.JsonValue::Number(config.max_bytes.to_double()),
"max_backups": @json_parser.JsonValue::Number(config.max_backups.to_double()),
})
}
pub fn sink_config_to_json(config : SinkConfig) -> @json_parser.JsonValue {
let obj : Map[String, @json_parser.JsonValue] = {
"kind": @json_parser.JsonValue::String(sink_kind_label(config.kind)),
"path": @json_parser.JsonValue::String(config.path),
"append": @json_parser.JsonValue::Bool(config.append),
"auto_flush": @json_parser.JsonValue::Bool(config.auto_flush),
"text_formatter": text_formatter_config_to_json(config.text_formatter),
}
match config.rotation {
None => ()
Some(rotation) => obj["rotation"] = file_rotation_config_to_json(rotation)
}
@json_parser.JsonValue::Object(obj)
}
pub fn stringify_sink_config(config : SinkConfig, pretty~ : Bool = false) -> String {
let value = sink_config_to_json(config)
if pretty {
@json_parser.stringify_pretty(value, 2)
} else {
@json_parser.stringify(value)
}
}
pub fn logger_config_to_json(config : LoggerConfig) -> @json_parser.JsonValue {
let obj : Map[String, @json_parser.JsonValue] = {
"min_level": @json_parser.JsonValue::String(config.min_level.label()),
"target": @json_parser.JsonValue::String(config.target),
"timestamp": @json_parser.JsonValue::Bool(config.timestamp),
"sink": sink_config_to_json(config.sink),
}
match config.queue {
None => ()
Some(queue) => obj["queue"] = queue_config_to_json(queue)
}
@json_parser.JsonValue::Object(obj)
}
pub fn stringify_logger_config(config : LoggerConfig, pretty~ : Bool = false) -> String {
let value = logger_config_to_json(config)
if pretty {
@json_parser.stringify_pretty(value, 2)
} else {
@json_parser.stringify(value)
}
}
+117
View File
@@ -0,0 +1,117 @@
fn string_to_c_bytes(str : String) -> Bytes {
let res : Array[Byte] = []
let len = str.length()
let mut i = 0
while i < len {
let mut c = str.code_unit_at(i).to_int()
if 0xD800 <= c && c <= 0xDBFF {
c -= 0xD800
i = i + 1
let l = str.code_unit_at(i).to_int() - 0xDC00
c = (c << 10) + l + 0x10000
}
if c < 0x80 {
res.push(c.to_byte())
} else if c < 0x800 {
res.push((0xc0 + (c >> 6)).to_byte())
res.push((0x80 + (c & 0x3f)).to_byte())
} else if c < 0x10000 {
res.push((0xe0 + (c >> 12)).to_byte())
res.push((0x80 + ((c >> 6) & 0x3f)).to_byte())
res.push((0x80 + (c & 0x3f)).to_byte())
} else {
res.push((0xf0 + (c >> 18)).to_byte())
res.push((0x80 + ((c >> 12) & 0x3f)).to_byte())
res.push((0x80 + ((c >> 6) & 0x3f)).to_byte())
res.push((0x80 + (c & 0x3f)).to_byte())
}
i = i + 1
}
res.push((0).to_byte())
Bytes::from_array(res)
}
#external
type NativeFileHandle
#borrow(path, mode)
extern "C" fn file_open_ffi(path : Bytes, mode : Bytes) -> NativeFileHandle = "fopen"
extern "C" fn file_is_null_ffi(handle : NativeFileHandle) -> Bool = "moonbitlang_async_pointer_is_null"
#borrow(buffer)
extern "C" fn file_write_ffi(
buffer : Bytes,
size : Int,
count : Int,
handle : NativeFileHandle,
) -> Int = "fwrite"
extern "C" fn file_flush_ffi(handle : NativeFileHandle) -> Int = "fflush"
extern "C" fn file_close_ffi(handle : NativeFileHandle) -> Int = "fclose"
extern "C" fn file_seek_ffi(handle : NativeFileHandle, offset : Int, origin : Int) -> Int = "fseek"
extern "C" fn file_tell_ffi(handle : NativeFileHandle) -> Int = "ftell"
#borrow(from_path, to_path)
extern "C" fn file_rename_ffi(from_path : Bytes, to_path : Bytes) -> Int = "rename"
#borrow(path)
extern "C" fn file_remove_ffi(path : Bytes) -> Int = "remove"
pub struct FileHandle {
path : String
raw : NativeFileHandle
}
pub fn open_file_handle_internal(path : String, append : Bool) -> FileHandle? {
let mode = if append { "ab" } else { "wb" }
let raw = file_open_ffi(string_to_c_bytes(path), string_to_c_bytes(mode))
if file_is_null_ffi(raw) {
None
} else {
Some({ raw, path })
}
}
pub fn write_file_handle_internal(handle : FileHandle, content : String) -> Bool {
let bytes = string_to_c_bytes(content)
let written = file_write_ffi(bytes, 1, bytes.length() - 1, handle.raw)
written == bytes.length() - 1
}
pub fn flush_file_handle_internal(handle : FileHandle) -> Bool {
file_flush_ffi(handle.raw) == 0
}
pub fn close_file_handle_internal(handle : FileHandle) -> Bool {
file_close_ffi(handle.raw) == 0
}
pub fn file_size_internal(handle : FileHandle) -> Int {
ignore(file_seek_ffi(handle.raw, 0, 2))
let size = file_tell_ffi(handle.raw)
if size < 0 {
0
} else {
size
}
}
pub fn rename_file_internal(from_path : String, to_path : String) -> Bool {
file_rename_ffi(string_to_c_bytes(from_path), string_to_c_bytes(to_path)) == 0
}
pub fn remove_file_internal(path : String) -> Bool {
file_remove_ffi(string_to_c_bytes(path)) == 0
}
pub fn string_byte_length_internal(content : String) -> Int {
string_to_c_bytes(content).length() - 1
}
pub fn native_files_supported_internal() -> Bool {
true
}
+51
View File
@@ -0,0 +1,51 @@
pub struct FileHandle {
path : String
}
pub fn open_file_handle_internal(path : String, append : Bool) -> FileHandle? {
ignore(append)
ignore(path)
let _unused : FileHandle = { path: "" }
ignore(_unused)
None
}
pub fn write_file_handle_internal(handle : FileHandle, content : String) -> Bool {
ignore(handle)
ignore(content)
false
}
pub fn flush_file_handle_internal(handle : FileHandle) -> Bool {
ignore(handle)
false
}
pub fn close_file_handle_internal(handle : FileHandle) -> Bool {
ignore(handle)
false
}
pub fn file_size_internal(handle : FileHandle) -> Int {
ignore(handle)
0
}
pub fn rename_file_internal(from_path : String, to_path : String) -> Bool {
ignore(from_path)
ignore(to_path)
false
}
pub fn remove_file_internal(path : String) -> Bool {
ignore(path)
false
}
pub fn string_byte_length_internal(content : String) -> Int {
content.length()
}
pub fn native_files_supported_internal() -> Bool {
false
}
+75
View File
@@ -0,0 +1,75 @@
pub type RecordPredicate = (@core.Record) -> Bool
pub fn level_at_least(min_level : @core.Level) -> RecordPredicate {
fn(rec) {
rec.level.priority() >= min_level.priority()
}
}
pub fn target_is(target : String) -> RecordPredicate {
fn(rec) {
rec.target == target
}
}
pub fn target_has_prefix(prefix : String) -> RecordPredicate {
fn(rec) {
rec.target.has_prefix(prefix)
}
}
pub fn message_contains(fragment : String) -> RecordPredicate {
fn(rec) {
rec.message.contains(fragment)
}
}
pub fn has_field(key : String) -> RecordPredicate {
fn(rec) {
for field in rec.fields {
if field.key == key {
return true
}
}
false
}
}
pub fn field_equals(key : String, value : String) -> RecordPredicate {
fn(rec) {
for field in rec.fields {
if field.key == key && field.value == value {
return true
}
}
false
}
}
pub fn not_(predicate : RecordPredicate) -> RecordPredicate {
fn(rec) {
!(predicate(rec))
}
}
pub fn all_of(predicates : Array[RecordPredicate]) -> RecordPredicate {
fn(rec) {
for predicate in predicates {
if !(predicate(rec)) {
return false
}
}
true
}
}
pub fn any_of(predicates : Array[RecordPredicate]) -> RecordPredicate {
fn(rec) {
for predicate in predicates {
if predicate(rec) {
return true
}
}
false
}
}
+975
View File
@@ -0,0 +1,975 @@
pub type RecordFormatter = (@core.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(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 {
show_timestamp : Bool
show_level : Bool
show_target : Bool
show_fields : Bool
separator : String
field_separator : 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(
show_timestamp~ : Bool = true,
show_level~ : Bool = true,
show_target~ : Bool = true,
show_fields~ : Bool = true,
separator~ : String = " ",
field_separator~ : 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 {
{
show_timestamp,
show_level,
show_target,
show_fields,
separator,
field_separator,
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 {
text_formatter(
show_timestamp=self.show_timestamp,
show_level=self.show_level,
show_target=self.show_target,
show_fields=self.show_fields,
separator=self.separator,
field_separator=self.field_separator,
template=self.template,
color_mode=self.color_mode,
color_support=color_support,
style_markup=self.style_markup,
target_style_markup=self.target_style_markup,
fields_style_markup=self.fields_style_markup,
style_tags=self.style_tags,
)
}
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 {
text_formatter(
show_timestamp=self.show_timestamp,
show_level=self.show_level,
show_target=self.show_target,
show_fields=self.show_fields,
separator=self.separator,
field_separator=self.field_separator,
template=self.template,
color_mode=self.color_mode,
color_support=self.color_support,
style_markup=style_markup,
target_style_markup=self.target_style_markup,
fields_style_markup=self.fields_style_markup,
style_tags=self.style_tags,
)
}
pub fn TextFormatter::without_style_markup(self : TextFormatter) -> TextFormatter {
self.with_style_markup(StyleMarkupMode::Disabled)
}
pub fn TextFormatter::with_target_style_markup(
self : TextFormatter,
style_markup : StyleMarkupMode,
) -> TextFormatter {
text_formatter(
show_timestamp=self.show_timestamp,
show_level=self.show_level,
show_target=self.show_target,
show_fields=self.show_fields,
separator=self.separator,
field_separator=self.field_separator,
template=self.template,
color_mode=self.color_mode,
color_support=self.color_support,
style_markup=self.style_markup,
target_style_markup=style_markup,
fields_style_markup=self.fields_style_markup,
style_tags=self.style_tags,
)
}
pub fn TextFormatter::with_fields_style_markup(
self : TextFormatter,
style_markup : StyleMarkupMode,
) -> TextFormatter {
text_formatter(
show_timestamp=self.show_timestamp,
show_level=self.show_level,
show_target=self.show_target,
show_fields=self.show_fields,
separator=self.separator,
field_separator=self.field_separator,
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=style_markup,
style_tags=self.style_tags,
)
}
pub fn TextFormatter::with_style_tags(self : TextFormatter, style_tags : StyleTagRegistry) -> TextFormatter {
text_formatter(
show_timestamp=self.show_timestamp,
show_level=self.show_level,
show_target=self.show_target,
show_fields=self.show_fields,
separator=self.separator,
field_separator=self.field_separator,
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=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.with_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 : @core.Level) -> String {
match level {
@core.Level::Trace => "90"
@core.Level::Debug => "36"
@core.Level::Info => "32"
@core.Level::Warn => "33"
@core.Level::Error => "31;1"
}
}
fn fields_to_json(fields : Array[@core.Field]) -> Json {
let obj : Map[String, Json] = {}
for item in fields {
obj[item.key] = Json::string(item.value)
}
Json::object(obj)
}
fn timestamp_text(rec : @core.Record, formatter : TextFormatter) -> String {
if formatter.show_timestamp && rec.timestamp_ms != 0UL {
ansi_wrap(rec.timestamp_ms.to_string(), "90", use_ansi_color(formatter.color_mode))
} else {
""
}
}
fn level_text(rec : @core.Record, formatter : TextFormatter) -> String {
if formatter.show_level {
ansi_wrap(rec.level.label(), level_ansi_code(rec.level), use_ansi_color(formatter.color_mode))
} else {
""
}
}
fn target_text(rec : @core.Record, formatter : TextFormatter) -> String {
if formatter.show_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 {
""
}
}
fn format_field_text(field : @core.Field, formatter : TextFormatter) -> String {
let value = render_styled_text(field.value, formatter, formatter.fields_style_markup)
"\{field.key}=\{value}"
}
fn fields_text(rec : @core.Record, formatter : TextFormatter) -> String {
if formatter.show_fields && rec.fields.length() != 0 {
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 {
""
}
}
fn render_template(rec : @core.Record, formatter : TextFormatter) -> String {
formatter.template
.replace_all(old="{timestamp}", new=timestamp_text(rec, formatter))
.replace_all(old="{timestamp_ms}", new=timestamp_text(rec, formatter))
.replace_all(old="{level}", new=level_text(rec, formatter))
.replace_all(old="{target}", new=target_text(rec, formatter))
.replace_all(old="{message}", new=render_inline_markup(rec.message, formatter))
.replace_all(old="{fields}", new=fields_text(rec, formatter))
.trim()
.to_owned()
}
pub fn format_text(rec : @core.Record, formatter~ : TextFormatter = text_formatter()) -> String {
if formatter.template != "" {
return render_template(rec, formatter)
}
let parts : Array[String] = []
if formatter.show_timestamp && rec.timestamp_ms != 0UL {
parts.push("[\{timestamp_text(rec, formatter)}]")
}
if formatter.show_level {
parts.push("[\{level_text(rec, formatter)}]")
}
if formatter.show_target && rec.target != "" {
parts.push("[\{target_text(rec, formatter)}]")
}
parts.push(render_inline_markup(rec.message, formatter))
let base = parts.join(formatter.separator)
if !formatter.show_fields || rec.fields.length() == 0 {
base
} else {
let details = fields_text(rec, formatter)
"\{base}\{formatter.separator}\{details}"
}
}
pub fn format_json(rec : @core.Record) -> String {
let obj : Map[String, Json] = {
"level": Json::string(rec.level.label()),
"message": Json::string(rec.message),
"fields": fields_to_json(rec.fields),
}
if rec.timestamp_ms != 0UL {
obj["timestamp_ms"] = rec.timestamp_ms.to_json()
}
if rec.target == "" {
Json::object(obj).stringify()
} else {
obj["target"] = Json::string(rec.target)
Json::object(obj).stringify()
}
}
+14
View File
@@ -0,0 +1,14 @@
import {
"Nanaloveyuki/BitLogger/src/core" @core,
"maria/json_parser" @json_parser,
"moonbitlang/core/env" @env,
"moonbitlang/core/json",
"moonbitlang/core/ref",
}
options(
targets: {
"file_backend_native.mbt": [ "native", "llvm" ],
"file_backend_stub.mbt": [ "js", "wasm", "wasm-gc" ],
},
)

Some files were not shown because too many files have changed in this diff Show More