mirror of
https://github.com/Nanaloveyuki/BitLogger.git
synced 2026-07-23 16:32:19 +00:00
Compare commits
20 Commits
91096a9e0d
...
73bcf78d89
| Author | SHA1 | Date | |
|---|---|---|---|
| 73bcf78d89 | |||
| 1ca5ab0835 | |||
| e019db11d6 | |||
| 5f12991592 | |||
| 25a6a973d2 | |||
| 55af0b664f | |||
| 0a098915af | |||
| 4860d1e08b | |||
| dca34bc114 | |||
| 4c25a81b03 | |||
| 3c15d8ed13 | |||
| 6998df0ee4 | |||
| 414d6a0ee8 | |||
| 2823a67d53 | |||
| a766dd09ac | |||
| d0989d6308 | |||
| 8b752a4b4d | |||
| ac45ec2b03 | |||
| 41d221af46 | |||
| d6e47d4bb8 |
@@ -19,3 +19,6 @@ desktop.ini
|
||||
|
||||
# Dev Documentations
|
||||
docs/dev/*
|
||||
|
||||
# vibecoding
|
||||
AGENTS/
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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(...)`.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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`.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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(...)`.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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`.
|
||||
@@ -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(...)`.
|
||||
@@ -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.
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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(...)`.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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(...)`.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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`.
|
||||
@@ -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`.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import {
|
||||
"Nanaloveyuki/BitLogger/src" @lib,
|
||||
}
|
||||
|
||||
options(
|
||||
"is-main": true,
|
||||
)
|
||||
@@ -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")])
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import {
|
||||
"Nanaloveyuki/BitLogger/src" @lib,
|
||||
}
|
||||
|
||||
options(
|
||||
"is-main": true,
|
||||
)
|
||||
@@ -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())
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import {
|
||||
"Nanaloveyuki/BitLogger/src" @lib,
|
||||
}
|
||||
|
||||
options(
|
||||
"is-main": true,
|
||||
)
|
||||
@@ -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())
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import {
|
||||
"Nanaloveyuki/BitLogger/src" @lib,
|
||||
}
|
||||
|
||||
options(
|
||||
"is-main": true,
|
||||
)
|
||||
@@ -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</>")
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import {
|
||||
"Nanaloveyuki/BitLogger/src" @lib,
|
||||
}
|
||||
|
||||
options(
|
||||
"is-main": true,
|
||||
)
|
||||
@@ -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")])
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import {
|
||||
"Nanaloveyuki/BitLogger/src" @lib,
|
||||
}
|
||||
|
||||
options(
|
||||
"is-main": true,
|
||||
)
|
||||
+1
-1
@@ -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"
|
||||
|
||||
@@ -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,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 {
|
||||
|
||||
@@ -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,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,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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import {
|
||||
"Nanaloveyuki/BitLogger/src" @bitlogger,
|
||||
"maria/json_parser" @json_parser,
|
||||
}
|
||||
@@ -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"),
|
||||
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
import {
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user