7 Commits

Author SHA1 Message Date
Nanaloveyuki fbd63b5e4a 🔖 bump version to 0.5.2 2026-06-07 20:49:10 +08:00
Nanaloveyuki a3d0a695de 🔖 prepare 0.5.2 release notes 2026-06-07 20:34:25 +08:00
Nanaloveyuki c0fe7999c4 🔖 prepare 0.5.1 release notes 2026-05-21 14:56:04 +08:00
Nanaloveyuki 5c8067d009 📝 simplify bilingual readme copy 2026-05-21 14:22:49 +08:00
Nanaloveyuki 6c19708d7b 📝 clarify api target verification status 2026-05-21 14:14:15 +08:00
Nanaloveyuki 3eee5893f5 tighten cross-target capability contracts 2026-05-21 14:13:17 +08:00
Nanaloveyuki 3e8d4c50a9 📝 align portable examples and release messaging 2026-05-21 14:12:56 +08:00
28 changed files with 483 additions and 191 deletions
+4
View File
@@ -59,6 +59,10 @@ jobs:
run: | run: |
moon check src-async --target js moon check src-async --target js
- name: Test bitlogger_async native
run: |
moon test src-async --target native
- name: Test bitlogger_async wasm-gc - name: Test bitlogger_async wasm-gc
run: | run: |
moon test src-async --target wasm-gc moon test src-async --target wasm-gc
+41 -67
View File
@@ -1,49 +1,22 @@
<html> # BitLogger
<div style="display: flex; justify-content: center; align-items: center; height: 15vh;">
<h3 title="https://moonbitlang.github.io/OSC2026/index.html#top">2026 MoonBit 国产基础软件生态开源大赛参赛作品</h3>
</div>
<div style="display: flex; justify-content: center; align-items: center; height: 8vh;">
<a href="https://mooncakes.io/docs/Nanaloveyuki/BitLogger" title="点击前往 Mooncake 页面"><b>Mooncake@Nanaloveyuki/BitLogger</b></a>
</div>
<div style="display: flex; justify-content: center; align-items: center; height: 2vh;">
<b>中文 | <a href="./docs/README-en.md">English</a></b>
</div>
</html>
## 📖 介绍 BitLogger 是一个使用 MoonBit 编写的结构化日志库,适合命令行工具、服务和需要统一日志输出的项目。
BitLogger 是一个使用 MoonBit 编写的结构化日志库,目标是提供可组合、可配置、可跨端编译的日志基础设施。 - [Mooncake 文档页](https://mooncakes.io/docs/Nanaloveyuki/BitLogger)
- [English README](./docs/README-en.md)
## 🧭 后端兼容 ## 介绍
| 模块 / 能力 | native / llvm | js / wasm / wasm-gc | BitLogger 提供统一的日志级别、目标名、结构化字段和可定制格式,既可以直接输出到控制台,也可以在 native 环境写入文件,并提供异步日志版本。
| --- | --- | --- |
| `src` 主包 | 支持 | 支持 |
| `file_sink(...)` | 支持 | 不支持, `native_files_supported()` 返回 `false` |
| `src-async` | 支持原生 worker 语义 | 支持兼容实现 |
| `examples/async_basic` | 支持 | 受 `async fn main` 入口限制, 当前不提供 |
## ❇️ 关键特性 ## 快速开始
- 结构化日志基础能力:level、record、formatter、sink、target、context field。
- 组合式设计:支持 filter、patch、fanout、split、callback、queued sink 等组合能力。
- 配置驱动:支持 JSON 配置解析、导出与 `build_logger(...)` / `build_async_logger(...)` 运行时组装。
- 文本格式化:支持 `text_formatter(...)`、template、style tag、`color_mode` / `style_markup`
- Native 文件输出:支持 file sink、基础 size rotation、reopen、failure counter 与运行时状态读取。
- 异步层:提供独立 `src-async` package,支持 queue、worker lifecycle、runtime state 和跨端兼容实现。
## 🚀 快速开始
```moonbit ```moonbit
let logger = build_logger( let logger = build_logger(
with_queue( text_console(
text_console( min_level=Level::Info,
min_level=Level::Info, target="demo",
target="demo", text_formatter=TextFormatterConfig::new(show_timestamp=false, separator=" | "),
text_formatter=TextFormatterConfig::new(show_timestamp=false, separator=" | "),
),
max_pending=32,
overflow=QueueOverflowPolicy::DropOldest,
), ),
) )
@@ -51,40 +24,41 @@ logger.info("starting", fields=[field("port", "8080")])
ignore(logger.flush()) ignore(logger.flush())
``` ```
推荐`presets + build_logger(...)` 路径开始:`console(...)` / `json_console(...)` / `text_console(...)` / `file(...)` 负责拼装 `LoggerConfig``with_queue(...)` / `with_file_rotation(...)` 负责做小范围组合,然后再交给 `build_logger(...)` 构建运行时 logger 推荐从 `console(...)``json_console(...)``text_console(...)``file(...)` 这几个入口开始,再按需配合 `with_queue(...)` `with_file_rotation(...)`
你需要 `fanout``split``callback``patch` 这类自定义 sink 图组合时,再优先使用 `Logger::new(...)` 直接进行运行时拼装 如果你需要自己组合 sink,比如 `fanout``split``callback`,再使用 `Logger::new(...)`
异步入口示例: ## 支持情况
```moonbit - `BitLogger` 当前在 CI 中检查/验证的目标是 `native``js``wasm-gc`
let logger = async_logger(console_sink(), target="async.demo") - `bitlogger_async` 当前在 CI 中检查 `native``js``wasm-gc`,测试覆盖 `native``js``wasm-gc`
@async.with_task_group(group => { - `wasm` 目标在源码 `moon.pkg` 中保留声明,但当前未纳入 CI 验证口径
group.spawn_bg(() => logger.run()) - `llvm` 目前按实验性目标处理,当前环境未完成验证
logger.info("started") - 文件输出是 native 能力;跨端代码里建议先判断 `native_files_supported()`
logger.shutdown() - `src-async` 可用,但示例 `examples/async_basic` 目前仍按 native 入口提供
})
```
## 📂 仓库结构 ## 主要能力
- `src/`: 主日志库 package。 - 结构化日志:level、target、message、fields
- `src-async/`: 基于 `moonbitlang/async` 的异步日志层。 - 多种输出方式:console、json console、text console、file
- `docs/api/`: 单接口粒度 API 文档。 - 可定制文本格式:模板、style tag、颜色控制
- `examples/basic/`: breadth 示例;文件开头就是推荐的 presets + `build_logger(...)` 同步入口,后续继续覆盖更广能力面。 - 配置驱动构建:`build_logger(...)``build_async_logger(...)`
- `examples/console_basic/`: console 与 json console 最小输出示例。 - 组合能力:queue、filter、patch、fanout、split、callback
- `examples/text_formatter/`: text formatter / template 示例。 - 异步日志:独立 `src-async` package
- `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 示例。
## 🔗 文档入口 ## 示例
- `examples/console_basic/`:最小 console / json console 示例
- `examples/text_formatter/`:文本格式与模板示例
- `examples/style_tags/`style tag 与彩色输出示例
- `examples/config_build/`:配置构建示例
- `examples/presets/`:常用预设组合示例
- `examples/file_rotation/`:文件输出与轮转示例,仅适用于 native
- `examples/async_basic/`:异步日志示例
## 文档
- [Mooncake 文档页](https://mooncakes.io/docs/Nanaloveyuki/BitLogger)
- [English README](./docs/README-en.md)
- [src package README](./src/README.mbt.md)
- [API 索引](./docs/api/index.md) - [API 索引](./docs/api/index.md)
- 推荐起步 API: `text_console(...)` / `file(...)` / `with_queue(...)` / `build_logger(...)` - [src package README](./src/README.mbt.md)
- facade API: `build_application_logger(...)` / `build_library_logger(...)` / `build_application_async_logger(...)`
常用入口:`text_console(...)``file(...)``with_queue(...)``build_logger(...)``build_async_logger(...)`
+35 -69
View File
@@ -1,43 +1,22 @@
# BitLogger # BitLogger
BitLogger is a structured logging library written in MoonBit. BitLogger is a structured logging library for MoonBit projects.
This README focuses on project positioning, core capabilities, and entry points. Detailed API coverage now lives in `docs/api/`. - [Mooncake package page](https://mooncakes.io/docs/Nanaloveyuki/BitLogger)
- [Chinese README](../README.md)
## Overview ## Overview
BitLogger is designed to provide composable, configurable, and cross-target logging infrastructure for MoonBit projects. BitLogger gives you consistent levels, targets, structured fields, configurable text output, native file logging, and an async logging layer.
## Backend Compatibility
| Module / capability | native / llvm | js / wasm / wasm-gc |
| --- | --- | --- |
| `src` core package | Supported | Supported |
| `file_sink(...)` | Supported | Not available, `native_files_supported()` returns `false` |
| `src-async` | Native worker semantics | Compatibility implementation |
| `examples/async_basic` | Supported | Not shipped currently because `async fn main` entry support is still limited |
## Key Features
- Structured logging with levels, targets, and key-value fields.
- Composable sinks, filters, patches, fanout, routing, and queue wrappers.
- Config-driven runtime assembly through JSON parse/export helpers and `build_logger(...)` / `build_async_logger(...)`.
- Configurable text formatting with templates, style tags, and color control.
- Native file sink support with basic rotation, reopen helpers, and runtime observability.
- Separate async layer with bounded queueing, worker lifecycle control, runtime state, and cross-target compatibility.
## Quick Start ## Quick Start
```moonbit ```moonbit
let logger = build_logger( let logger = build_logger(
with_queue( text_console(
text_console( min_level=Level::Info,
min_level=Level::Info, target="demo",
target="demo", text_formatter=TextFormatterConfig::new(show_timestamp=false, separator=" | "),
text_formatter=TextFormatterConfig::new(show_timestamp=false, separator=" | "),
),
max_pending=32,
overflow=QueueOverflowPolicy::DropOldest,
), ),
) )
@@ -45,52 +24,39 @@ logger.info("starting", fields=[field("port", "8080")])
ignore(logger.flush()) 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(...)`. Start with `console(...)`, `json_console(...)`, `text_console(...)`, or `file(...)`, then add `with_queue(...)` or `with_file_rotation(...)` only when needed.
Switch to `Logger::new(...)` when you need direct runtime sink composition such as `fanout`, `split`, `callback`, or custom patch/filter graphs. Use `Logger::new(...)` when you want to assemble custom sink graphs directly.
Async entry example: ## Support Status
```moonbit - Currently verified targets: `native`, `js`, `wasm`, `wasm-gc`
let logger = async_logger(console_sink(), target="async.demo") - `llvm` is still treated as experimental in the current release context
@async.with_task_group(group => { - File output is a native capability; check `native_files_supported()` in cross-target code
group.spawn_bg(() => logger.run()) - `src-async` is available, while `examples/async_basic` is still shipped as a native entry example
logger.info("started")
logger.shutdown()
})
```
## Repository Layout ## Main Features
- `src/`: core logging package. - Structured logs with levels, targets, messages, and fields
- `src-async/`: async logging layer built on `moonbitlang/async`. - Multiple outputs: console, JSON console, text console, and file
- `docs/api/`: one-file-per-interface API documentation. - Custom text formatting with templates, style tags, and color control
- `examples/basic/`: breadth example; starts with the recommended presets-based sync path and then sweeps broader capabilities. - Config-based builders: `build_logger(...)` and `build_async_logger(...)`
- `examples/console_basic/`: minimal console and JSON console output. - Composition helpers: queue, filter, patch, fanout, split, callback
- `examples/text_formatter/`: text formatter and template example. - Separate async package under `src-async`
- `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 ## Examples
- [Mooncake package page](https://mooncakes.io/docs/Nanaloveyuki/BitLogger) - `examples/console_basic/`: minimal console and JSON console example
- [Chinese README](../README.md) - `examples/text_formatter/`: text formatting and template example
- `examples/style_tags/`: style tags and colored output example
- `examples/config_build/`: config-based build example
- `examples/presets/`: common preset combinations
- `examples/file_rotation/`: native file logging and rotation example
- `examples/async_basic/`: async logging example
## Documentation
- [API index](./api/index.md)
- [src package README](../src/README.mbt.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 Common entry points: `text_console(...)`, `file(...)`, `with_queue(...)`, `build_logger(...)`, `build_async_logger(...)`
- `docs/README-en.md` no longer acts as an API catalog.
- Detailed API references, config fields, runtime control helpers, and lifecycle surfaces now live under `docs/api/`.
- For concrete runnable flows, prefer `examples/`.
+5
View File
@@ -45,6 +45,8 @@ Detailed rules explaining key parameters and behaviors
- `async_logger(...)` only builds the logger. Actual background draining is started by `run()`. - `async_logger(...)` only builds the logger. Actual background draining is started by `run()`.
- In non-native targets, the implementation uses compatibility behavior while keeping the same public surface. - In non-native targets, the implementation uses compatibility behavior while keeping the same public surface.
- `src-async` is designed for `native / llvm / js / wasm / wasm-gc`, but current release-facing local verification is stronger for `native / js / wasm / wasm-gc` than for `llvm`.
- `llvm` should currently be read as experimental and locally unverified in this environment rather than as a stable checked target.
- `flush` is used only when batch or shutdown policy wants explicit flushing. - `flush` is used only when batch or shutdown policy wants explicit flushing.
- Queue overflow behavior depends on `AsyncOverflowPolicy`. - Queue overflow behavior depends on `AsyncOverflowPolicy`.
@@ -98,3 +100,6 @@ e.g.:
2. Use `state()`, `pending_count()`, and `dropped_count()` for runtime diagnostics. 2. Use `state()`, `pending_count()`, and `dropped_count()` for runtime diagnostics.
3. Example entrypoint limitations such as `async fn main` support are separate from the library-level portability of this API.
4. See [target-verification.md](./target-verification.md) for the current local verification matrix.
+5
View File
@@ -34,9 +34,11 @@ pub fn async_runtime_mode() -> AsyncRuntimeMode {}
Detailed rules explaining key parameters and behaviors Detailed rules explaining key parameters and behaviors
- The return value is determined by the active backend implementation. - The return value is determined by the active backend implementation.
- `NativeWorker` is the expected mode on native-style backends, while `Compatibility` is the expected mode on targets without native background-worker semantics.
- `async_runtime_mode_label(...)` converts the enum into a stable string value. - `async_runtime_mode_label(...)` converts the enum into a stable string value.
- `async_runtime_supports_background_worker()` is a narrower boolean probe built on the same idea. - `async_runtime_supports_background_worker()` is a narrower boolean probe built on the same idea.
- This API is intentionally small and useful for lightweight branching. - This API is intentionally small and useful for lightweight branching.
- The mode result describes runtime behavior only; it should not be read as proof that every backend has been equally re-verified in the current release cycle.
### How to Use ### How to Use
@@ -76,3 +78,6 @@ e.g.:
2. Use `async_runtime_state()` when you also want worker support packaged into one object. 2. Use `async_runtime_state()` when you also want worker support packaged into one object.
3. This mode distinction is about runtime behavior, not whether `src-async` itself is expected to compile for the target.
4. See [target-verification.md](./target-verification.md) for the current verification status of individual targets.
@@ -36,6 +36,7 @@ Detailed rules explaining key parameters and behaviors
- `true` indicates native worker capability. - `true` indicates native worker capability.
- `false` indicates compatibility-mode behavior. - `false` indicates compatibility-mode behavior.
- This helper is derived from backend-specific async runtime implementation choice. - This helper is derived from backend-specific async runtime implementation choice.
- The async library still targets multiple backends even when this helper returns `false`.
- Use it when an enum branch is unnecessary and a boolean capability check is enough. - Use it when an enum branch is unnecessary and a boolean capability check is enough.
### How to Use ### How to Use
@@ -75,3 +76,6 @@ e.g.:
2. Prefer `async_runtime_state()` when you want the same information in a richer object. 2. Prefer `async_runtime_state()` when you want the same information in a richer object.
3. A `false` result should be read as "compatibility-mode runtime behavior" rather than "async library unsupported on this target".
4. This helper does not by itself imply that every non-worker backend has been equally re-verified for the current release; see [target-verification.md](./target-verification.md).
+7
View File
@@ -37,6 +37,10 @@ Detailed rules explaining key parameters and behaviors
- The resulting async logger inherits `min_level`, `target`, and timestamp behavior from the built synchronous logger. - The resulting async logger inherits `min_level`, `target`, and timestamp behavior from the built synchronous logger.
- File, queue, and formatter choices all come from config rather than direct code-side sink wiring. - File, queue, and formatter choices all come from config rather than direct code-side sink wiring.
- The returned sink type is `RuntimeSink`, which keeps configured control helpers available where relevant. - The returned sink type is `RuntimeSink`, which keeps configured control helpers available where relevant.
- The `src-async` library is designed to compile on `native / llvm / js / wasm / wasm-gc`, but runtime mode differs by backend.
- Current local release-facing verification is explicit for `native / js / wasm / wasm-gc`.
- `llvm` remains experimental and did not complete local verification in this environment.
- On non-native targets, the async library still compiles and exposes the same public surface through compatibility-mode behavior rather than native background-worker semantics.
### How to Use ### How to Use
@@ -79,3 +83,6 @@ e.g.:
2. Use `async_logger(...)` directly when you want explicit code-defined sink wiring. 2. Use `async_logger(...)` directly when you want explicit code-defined sink wiring.
3. Library portability is broader than example portability: a runnable `async fn main` example may still be target-limited even when the async library itself compiles for that backend.
4. See [target-verification.md](./target-verification.md) for the current verification boundary.
+3
View File
@@ -36,6 +36,7 @@ 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(...)`. - 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`. - 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`. - This helper is best suited to text-console output paths where callers want the concrete formatted sink type instead of `RuntimeSink`.
- This async text path follows the same target story as the broader async library: `native / js / wasm / wasm-gc` have stronger local verification, while `llvm` remains experimental and locally unverified in this environment.
### How to Use ### How to Use
@@ -67,3 +68,5 @@ e.g.:
1. This API is narrower than `build_async_logger(...)` because it preserves a concrete text sink type. 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. 2. It is the base builder used by the application and library async text facades.
3. See [target-verification.md](./target-verification.md) for the current local verification matrix.
+6 -1
View File
@@ -43,7 +43,9 @@ pub fn file_sink(
Detailed rules explaining key parameters and behaviors Detailed rules explaining key parameters and behaviors
- This sink is only available on `native/llvm`; use `native_files_supported()` for capability detection. - This sink is designed for host-file capable backends, with current local verification centered on `native`.
- `llvm` should be treated as experimental in the current release context and was not successfully re-verified in this environment.
- The `src` library is still designed for multi-target compilation, but file sink availability remains backend-sensitive inside that broader portable surface.
- `append` is persistent policy state and also affects later reopen behavior. - `append` is persistent policy state and also affects later reopen behavior.
- `auto_flush` trades durability for more flush work per record. - `auto_flush` trades durability for more flush work per record.
- Rotation is currently size-based and rename-retention-oriented, not time-based. - Rotation is currently size-based and rename-retention-oriented, not time-based.
@@ -91,3 +93,6 @@ e.g.:
2. Prefer `state()`, `policy()`, and failure counters when integrating diagnostics. 2. Prefer `state()`, `policy()`, and failure counters when integrating diagnostics.
3. Non-native targets can still compile code referencing this API, but callers should treat actual file availability and successful writes as target-sensitive runtime behavior.
4. See [target-verification.md](./target-verification.md) for the current verification boundary between design intent and locally re-checked targets.
+4
View File
@@ -15,6 +15,10 @@ key-word:
BitLogger API navigation. BitLogger API navigation.
## Target and verification
- [target-verification.md](./target-verification.md)
## Core logger ## Core logger
- [logger-new.md](./logger-new.md) - [logger-new.md](./logger-new.md)
+63
View File
@@ -0,0 +1,63 @@
---
name: target-verification
group: api
category: verification
update-time: 20260521
description: Current release-facing local verification boundary for BitLogger multi-target support claims.
key-word:
- target
- verification
- matrix
- public
---
## Target-verification
This note records the current release-facing local verification boundary for BitLogger's multi-target claims. It is intentionally small: the goal is to separate design intent from what was actually re-checked in the current environment.
### Verification Matrix
| Area | native | js | wasm | wasm-gc | llvm |
| --- | --- | --- | --- | --- | --- |
| `src` compile check | verified | verified | verified | verified | not locally verified |
| `src-async` compile check | verified | verified | verified | verified | not locally verified |
| `moon test` | verified in current local environment | not separately re-run | not separately re-run | not separately re-run | not run |
### Explanation
Detailed rules explaining key parameters and behaviors
- `verified` here means a local verification command was re-run successfully in the current environment.
- `not locally verified` means the release should not imply fresh local confirmation even if the package is still designed for that target.
- `llvm` is currently experimental in practice for this release context and did not complete local verification in this environment.
- `wasm` has been re-checked and should be distinguished from the earlier state where only `wasm-gc` and `js` had been explicitly re-confirmed.
- Example-level limitations, such as `async fn main` entry support, are separate from the library-level compile matrix above.
### Commands Used
The current local verification evidence for this note is based on commands such as:
```text
moon test
moon check --target native
moon check --target js
moon check --target wasm
moon check --target wasm-gc
moon check src-async --target native
moon check src-async --target js
moon check src-async --target wasm
moon check src-async --target wasm-gc
```
### Error Case
e.g.:
- If a target is described as part of the design intent but not locally re-verified here, readers should not treat it as freshly confirmed release evidence.
- If `llvm` toolchain support changes later, this page should be updated together with the relevant verification commands rather than silently relying on old wording.
### Notes
1. This page is a release-facing verification note, not a portability promise by itself.
2. API pages that mention target-sensitive behavior should defer to this page when readers need the current verification boundary.
+22
View File
@@ -0,0 +1,22 @@
## BitLogger Update Changes
version 0.5.1
### Docs
- docs: simplify the Chinese and English README entry pages so they focus on what BitLogger is, how to start, and where to find examples and API docs
- docs: remove misplaced banner-style content and reduce overly dense engineering wording in release-facing README sections
- docs: clarify API-side target verification wording and add a small verification note for current release claims
### Example
- docs: align portable examples with the recommended `build_logger(...)` entry path and keep native-only example boundaries explicit
### Test
- test: strengthen cross-target capability checks for configured file logging, fallback file behavior, and async runtime mode reporting
### Notes
- this is a small follow-up release focused on release-facing clarity rather than new core functionality
- verified release-facing targets remain `native`, `js`, `wasm`, and `wasm-gc`, while `llvm` is still treated as experimental in the current environment
+23
View File
@@ -0,0 +1,23 @@
## BitLogger Update Changes
version 0.5.2
### Docs
- docs: align the README support matrix with the actual CI validation scope for both `BitLogger` and `bitlogger_async`
### Test
- test: add sync config parser failure-path coverage for malformed JSON, wrong field types, and invalid enum values
- test: add async config parser failure-path coverage for malformed input, wrong async field types, invalid build roots, and nested sync config parse failures
- test: cover async worker failure reporting and `wait_idle()` early-stop behavior when batch flush fails
- test: extend CI so `bitlogger_async` native tests run alongside the existing `js` and `wasm-gc` coverage
### Runtime
- runtime: allow async flush callbacks to raise so async worker failure state and `last_error()` reporting are reachable through the public API
### Notes
- this release is a focused validation follow-up that closes the gap between published verification claims and the real test matrix
- release verification now covers sync checks/tests plus async checks/tests across `native`, `js`, and `wasm-gc`
+1
View File
@@ -1,4 +1,5 @@
async fn main { async fn main {
println("examples/async_basic is shipped as a native-only example because async fn main entry support is still limited on non-native targets, even though src-async itself keeps compatibility behavior.")
println(@lib_async.stringify_async_runtime_state(@lib_async.async_runtime_state(), pretty=true)) println(@lib_async.stringify_async_runtime_state(@lib_async.async_runtime_state(), pretty=true))
let raw = "{\"logger\":{\"min_level\":\"info\",\"target\":\"async.demo\",\"timestamp\":true,\"sink\":{\"kind\":\"text_console\",\"text_formatter\":{\"show_timestamp\":false,\"separator\":\" | \"}}},\"async_config\":{\"max_pending\":2,\"overflow\":\"DropOldest\",\"max_batch\":4,\"linger_ms\":5,\"flush\":\"Batch\"}}" let raw = "{\"logger\":{\"min_level\":\"info\",\"target\":\"async.demo\",\"timestamp\":true,\"sink\":{\"kind\":\"text_console\",\"text_formatter\":{\"show_timestamp\":false,\"separator\":\" | \"}}},\"async_config\":{\"max_pending\":2,\"overflow\":\"DropOldest\",\"max_batch\":4,\"linger_ms\":5,\"flush\":\"Batch\"}}"
+6 -3
View File
@@ -1,8 +1,11 @@
fn main { fn main {
let logger = @lib.Logger::new(@lib.console_sink(), min_level=@lib.Level::Info, target="demo.console") let logger = @lib.build_logger(
.with_context_fields([@lib.field("service", "bitlogger")]) @lib.console(min_level=@lib.Level::Info, target="demo.console"),
)
logger.info("hello console", fields=[@lib.field("mode", "basic")]) 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") let structured = @lib.build_logger(
@lib.json_console(min_level=@lib.Level::Info, target="demo.json"),
)
structured.info("hello json", fields=[@lib.field("kind", "structured")]) structured.info("hello json", fields=[@lib.field("kind", "structured")])
} }
+19 -10
View File
@@ -1,19 +1,28 @@
fn main { fn main {
if !@lib.native_files_supported() { if !@lib.native_files_supported() {
println("native file sink is not available on this backend") println("native file sink is not available on this backend; examples/presets stays portable, while file_rotation remains native-focused")
return return
} }
let logger = @lib.Logger::new( let logger = @lib.build_logger(
@lib.file_sink( @lib.with_file_rotation(
"bitlogger-example.log", @lib.file(
auto_flush=true, "bitlogger-example.log",
rotation=Some(@lib.file_rotation(128, max_backups=2)), min_level=@lib.Level::Info,
target="demo.file",
auto_flush=true,
) catch {
err => {
ignore(err)
println("invalid file preset config")
return
}
},
128,
max_backups=2,
), ),
min_level=@lib.Level::Info,
target="demo.file",
) )
logger.info("file rotation ready", fields=[@lib.field("kind", "file")]) logger.info("file rotation ready", fields=[@lib.field("kind", "file")])
ignore(logger.sink.flush()) ignore(logger.flush())
ignore(logger.sink.close()) ignore(logger.file_close())
} }
+33 -15
View File
@@ -1,27 +1,45 @@
fn main { fn main {
let config = @lib.with_queue( let config = @lib.with_queue(
@lib.with_file_rotation( @lib.text_console(
min_level=@lib.Level::Info,
target="demo.preset",
text_formatter=@lib.TextFormatterConfig::new(
show_timestamp=false,
separator=" | ",
),
),
max_pending=4,
overflow=@lib.QueueOverflowPolicy::DropOldest,
)
let logger = @lib.build_logger(config)
logger.info("preset logger ready", fields=[@lib.field("kind", "portable")])
logger.info("preset logger queued", fields=[@lib.field("step", "two")])
ignore(logger.flush())
if @lib.native_files_supported() {
let file_config = @lib.with_file_rotation(
@lib.file( @lib.file(
"preset-example.log", "preset-example.log",
min_level=@lib.Level::Info, min_level=@lib.Level::Info,
target="demo.preset", target="demo.preset.file",
auto_flush=true, auto_flush=true,
), ),
256, 256,
max_backups=2, max_backups=2,
), ) catch {
max_pending=4, err => {
overflow=@lib.QueueOverflowPolicy::DropOldest, ignore(err)
) catch { println("invalid file preset config")
err => { return
ignore(err) }
println("invalid preset config")
return
} }
}
let logger = @lib.build_logger(config) let file_logger = @lib.build_logger(file_config)
logger.info("preset logger ready", fields=[@lib.field("kind", "preset")]) file_logger.info("file preset ready", fields=[@lib.field("kind", "native-only")])
ignore(logger.flush()) ignore(file_logger.flush())
ignore(logger.file_close()) ignore(file_logger.file_close())
} else {
println("file presets are skipped on this backend because native file support is unavailable")
}
} }
+12 -12
View File
@@ -1,16 +1,16 @@
fn main { fn main {
let formatter = @lib.text_formatter( let logger = @lib.build_logger(
show_timestamp=false, @lib.text_console(
color_mode=@lib.ColorMode::Always, min_level=@lib.Level::Info,
).with_style_tags( target="demo.style",
@lib.default_style_tag_registry() text_formatter=@lib.TextFormatterConfig::new(
.set_tag("accent", fg=Some("#4cc9f0"), bold=true) show_timestamp=false,
.define_alias("danger", "red"), color_mode=@lib.ColorMode::Always,
) style_tags={
let logger = @lib.Logger::new( "accent": @lib.text_style(fg=Some("#4cc9f0"), bold=true),
@lib.text_console_sink(formatter), },
min_level=@lib.Level::Info, ),
target="demo.style", ),
) )
logger.info("<accent>styled</> output with <danger>alert</>") logger.info("<accent>styled</> output with <danger>alert</>")
} }
+11 -10
View File
@@ -1,14 +1,15 @@
fn main { fn main {
let formatter = @lib.text_formatter( let logger = @lib.build_logger(
show_timestamp=false, @lib.text_console(
separator=" | ", min_level=@lib.Level::Info,
field_separator=",", target="demo.formatter",
template="[{level}] {target} {message} :: {fields}", text_formatter=@lib.TextFormatterConfig::new(
) show_timestamp=false,
let logger = @lib.Logger::new( separator=" | ",
@lib.text_console_sink(formatter), field_separator=",",
min_level=@lib.Level::Info, template="[{level}] {target} {message} :: {fields}",
target="demo.formatter", ),
),
) )
logger.info("formatted output", fields=[@lib.field("user", "alice"), @lib.field("request_id", "42")]) logger.info("formatted output", fields=[@lib.field("user", "alice"), @lib.field("request_id", "42")])
} }
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "Nanaloveyuki/BitLogger", "name": "Nanaloveyuki/BitLogger",
"version": "0.5.0", "version": "0.5.2",
"deps": { "deps": {
"maria/json_parser": "0.1.1", "maria/json_parser": "0.1.1",
"moonbitlang/async": "0.19.0" "moonbitlang/async": "0.19.0"
+89
View File
@@ -1,3 +1,7 @@
suberror TestFlushError {
TestFlushError(String)
}
async test "shutdown drains pending records" { async test "shutdown drains pending records" {
inspect(async_runtime_mode_label(async_runtime_mode()) == "native_worker" || async_runtime_mode_label(async_runtime_mode()) == "compatibility", content="true") inspect(async_runtime_mode_label(async_runtime_mode()) == "native_worker" || async_runtime_mode_label(async_runtime_mode()) == "compatibility", content="true")
let written : Ref[Array[String]] = Ref([]) let written : Ref[Array[String]] = Ref([])
@@ -150,6 +154,48 @@ test "async build config stringify roundtrips nested logger and async fields" {
}, content="Shutdown") }, content="Shutdown")
} }
test "async config parsers reject malformed input" {
let invalid_json_error = (fn() -> String raise {
ignore(parse_async_logger_config_text("{"))
"no error"
})() catch {
err => err.to_string()
}
inspect(invalid_json_error.contains("UnexpectedToken"), content="true")
let wrong_type_error = (fn() -> String raise {
ignore(parse_async_logger_config_text("{\"max_pending\":\"many\"}"))
"no error"
})() catch {
err => err.to_string()
}
inspect(wrong_type_error.contains("Expected number at async_config.max_pending"), content="true")
let invalid_enum_error = (fn() -> String raise {
ignore(parse_async_logger_config_text("{\"overflow\":\"Burst\"}"))
"no error"
})() catch {
err => err.to_string()
}
inspect(invalid_enum_error.contains("Unsupported async overflow policy: Burst"), content="true")
let invalid_build_root_error = (fn() -> String raise {
ignore(parse_async_logger_build_config_text("[]"))
"no error"
})() catch {
err => err.to_string()
}
inspect(invalid_build_root_error.contains("Expected object at async logger build config root"), content="true")
let nested_sync_error = (fn() -> String raise {
ignore(parse_async_logger_build_config_text("{\"logger\":{\"timestamp\":\"true\"}}"))
"no error"
})() catch {
err => err.to_string()
}
inspect(nested_sync_error.contains("ConfigError.InvalidConfig"), content="true")
}
test "async runtime capability helpers stay consistent" { test "async runtime capability helpers stay consistent" {
let mode = async_runtime_mode() let mode = async_runtime_mode()
let state = async_runtime_state() let state = async_runtime_state()
@@ -174,6 +220,14 @@ test "async runtime capability helpers stay consistent" {
) )
} }
test "async runtime mode and worker flag encode target contract" {
let mode_label = async_runtime_mode_label(async_runtime_mode())
let worker = async_runtime_supports_background_worker()
inspect(mode_label == "native_worker" || mode_label == "compatibility", content="true")
inspect((mode_label == "native_worker") == worker, content="true")
inspect((mode_label == "compatibility") == !worker, content="true")
}
test "async logger state snapshot reflects current counters and runtime" { test "async logger state snapshot reflects current counters and runtime" {
let logger = async_logger( let logger = async_logger(
@bitlogger.callback_sink(fn(_) { @bitlogger.callback_sink(fn(_) {
@@ -243,6 +297,41 @@ async test "run drains queued records in compatibility backends too" {
inspect(written.val[1], content="two") inspect(written.val[1], content="two")
} }
async test "async logger records worker failures and wait_idle stops early" {
let writes : Ref[Int] = Ref(0)
let logger = async_logger(
@bitlogger.callback_sink(fn(_) {
writes.val += 1
}),
config=AsyncLoggerConfig::new(
max_pending=4,
overflow=AsyncOverflowPolicy::Blocking,
flush=AsyncFlushPolicy::Batch,
),
min_level=@bitlogger.Level::Info,
target="async.failure",
flush=fn(_) -> Int raise {
raise TestFlushError("flush exploded")
},
)
@async.with_task_group(group => {
group.spawn_bg(allow_failure=true, () => logger.run())
logger.info("ok")
logger.info("flush-now")
logger.wait_idle()
inspect(logger.has_failed(), content="true")
inspect(logger.pending_count(), content="1")
logger.close(clear=true)
})
inspect(writes.val, content="1")
inspect(logger.has_failed(), content="true")
inspect(logger.last_error().contains("TestFlushError"), content="true")
inspect(logger.is_running(), content="false")
inspect(logger.pending_count(), content="0")
}
async test "library async logger keeps a smaller async facade" { async test "library async logger keeps a smaller async facade" {
let written_targets : Ref[Array[String]] = Ref([]) let written_targets : Ref[Array[String]] = Ref([])
let written_messages : Ref[Array[String]] = Ref([]) let written_messages : Ref[Array[String]] = Ref([])
+2 -2
View File
@@ -88,7 +88,7 @@ pub struct AsyncLogger[S] {
linger_ms : Int linger_ms : Int
flush_policy : AsyncFlushPolicy flush_policy : AsyncFlushPolicy
sink : S sink : S
flush_sink : (S) -> Int flush_sink : (S) -> Int raise
context_fields : Array[@bitlogger.Field] context_fields : Array[@bitlogger.Field]
filter : (@bitlogger.Record) -> Bool filter : (@bitlogger.Record) -> Bool
patch : @bitlogger.RecordPatch patch : @bitlogger.RecordPatch
@@ -106,7 +106,7 @@ pub fn[S] async_logger(
config~ : AsyncLoggerConfig = AsyncLoggerConfig::new(), config~ : AsyncLoggerConfig = AsyncLoggerConfig::new(),
min_level~ : @bitlogger.Level = @bitlogger.Level::Info, min_level~ : @bitlogger.Level = @bitlogger.Level::Info,
target~ : String = "", target~ : String = "",
flush~ : (S) -> Int = fn(_) { 0 }, flush~ : (S) -> Int raise = fn(_) { 0 },
) -> AsyncLogger[S] { ) -> AsyncLogger[S] {
{ {
min_level, min_level,
+1 -1
View File
@@ -15,7 +15,7 @@ pub fn[S] LibraryAsyncLogger::new(
config~ : AsyncLoggerConfig = AsyncLoggerConfig::new(), config~ : AsyncLoggerConfig = AsyncLoggerConfig::new(),
min_level~ : @bitlogger.Level = @bitlogger.Level::Info, min_level~ : @bitlogger.Level = @bitlogger.Level::Info,
target~ : String = "", target~ : String = "",
flush~ : (S) -> Int = fn(_) { 0 }, flush~ : (S) -> Int raise = fn(_) { 0 },
) -> LibraryAsyncLogger[S] { ) -> LibraryAsyncLogger[S] {
library_async_logger( library_async_logger(
async_logger( async_logger(
+3
View File
@@ -10,6 +10,9 @@ import {
options( options(
targets: { targets: {
"async_logger_shared.mbt": [ "native", "llvm", "js", "wasm", "wasm-gc" ],
"application_async_logger.mbt": [ "native", "llvm", "js", "wasm", "wasm-gc" ],
"library_async_logger.mbt": [ "native", "llvm", "js", "wasm", "wasm-gc" ],
"async_logger_native.mbt": [ "native", "llvm" ], "async_logger_native.mbt": [ "native", "llvm" ],
"async_logger_stub.mbt": [ "js", "wasm", "wasm-gc" ], "async_logger_stub.mbt": [ "js", "wasm", "wasm-gc" ],
}, },
+43
View File
@@ -116,6 +116,32 @@ test "logger config parser reads file rotation options" {
} }
} }
test "logger config parser rejects malformed sync config input" {
let invalid_json_error = (fn() -> String raise ConfigError {
ignore(parse_logger_config_text("{"))
"no error"
})() catch {
ConfigError::InvalidConfig(message) => message
}
inspect(invalid_json_error.contains("Invalid JSON:"), content="true")
let wrong_type_error = (fn() -> String raise ConfigError {
ignore(parse_logger_config_text("{\"timestamp\":\"true\"}"))
"no error"
})() catch {
ConfigError::InvalidConfig(message) => message
}
inspect(wrong_type_error, content="Expected bool at key timestamp")
let invalid_enum_error = (fn() -> String raise ConfigError {
ignore(parse_logger_config_text("{\"sink\":{\"kind\":\"console\",\"text_formatter\":{\"color_mode\":\"loud\"}}}"))
"no error"
})() catch {
ConfigError::InvalidConfig(message) => message
}
inspect(invalid_enum_error, content="Unsupported color mode: loud")
}
test "logger config stringify roundtrips stable fields" { test "logger config stringify roundtrips stable fields" {
let text = stringify_logger_config( let text = stringify_logger_config(
LoggerConfig::new( LoggerConfig::new(
@@ -560,6 +586,23 @@ test "configured non-file logger has no file runtime state" {
inspect(logger.file_runtime_state() is None, content="true") inspect(logger.file_runtime_state() is None, content="true")
} }
test "configured file logger mirrors backend file capability" {
let logger = build_logger(
LoggerConfig::new(
sink=SinkConfig::new(kind=SinkKind::File, path="config-capability.log"),
),
)
inspect(logger.file_available() == native_files_supported(), content="true")
if logger.file_available() {
inspect(logger.file_state().available, content="true")
ignore(logger.close())
} else {
inspect(logger.file_state().available, content="false")
inspect(logger.file_flush(), content="false")
inspect(logger.file_close(), content="false")
}
}
test "configured logger file setters update file sink policy state" { test "configured logger file setters update file sink policy state" {
let logger = build_logger( let logger = build_logger(
LoggerConfig::new( LoggerConfig::new(
+25
View File
@@ -406,6 +406,31 @@ test "file sink availability reflects backend support" {
} }
} }
test "file sink unavailable backend keeps stable fallback state" {
let sink = file_sink("bitlogger-unavailable.log", rotation=Some(file_rotation(64, max_backups=2)))
if sink.is_available() {
inspect(sink.state().available, content="true")
ignore(sink.close())
} else {
let state = sink.state()
inspect(native_files_supported(), content="false")
inspect(state.available, content="false")
inspect(sink.flush(), content="false")
inspect(sink.close(), content="false")
inspect(sink.write_failures(), content="0")
sink.write(record(Level::Info, "dropped"))
inspect(sink.write_failures(), content="1")
inspect(sink.rotation_failures(), content="0")
match state.rotation {
Some(rotation) => {
inspect(rotation.max_bytes, content="64")
inspect(rotation.max_backups, content="2")
}
None => inspect(false, content="true")
}
}
}
test "file sink rotation config normalizes invalid inputs" { test "file sink rotation config normalizes invalid inputs" {
let rotation = file_rotation(0, max_backups=0) let rotation = file_rotation(0, max_backups=0)
inspect(rotation.max_bytes, content="1") inspect(rotation.max_bytes, content="1")
+13
View File
@@ -46,6 +46,15 @@ Use `Logger::new(...)` instead when you need direct sink composition such as `fa
如果你需要 `fanout_sink(...)``split_by_level(...)``callback_sink(...)` 或更自由的 patch/filter 组合,再改用 `Logger::new(...)` 直接拼装运行时 sink 图。 如果你需要 `fanout_sink(...)``split_by_level(...)``callback_sink(...)` 或更自由的 patch/filter 组合,再改用 `Logger::new(...)` 直接拼装运行时 sink 图。
Portable-first note / 跨端优先说明:
- prefer `console(...)`, `json_console(...)`, `text_console(...)`, parsed config, and `build_logger(...)` for cross-target examples
- 跨端示例优先使用这些 console / config 路径
- file sink and `file(...)` remain native-sensitive; gate them with `native_files_supported()` when portability matters
- `file(...)` / file sink 仍然是 native 敏感能力,跨端代码里应先做 `native_files_supported()` 判断
- verified README targets currently center on `native`, `js`, `wasm`, and `wasm-gc`; treat `llvm` as experimental until it is locally re-verified with the required toolchain
- 当前 README 的已验证目标以 `native``js``wasm``wasm-gc` 为准;`llvm` 在完成本地 toolchain 复核前应按实验性目标理解
Project command note / 项目命令说明: Project command note / 项目命令说明:
- use `moon check` / `moon test` for local project verification - use `moon check` / `moon test` for local project verification
@@ -64,6 +73,10 @@ Project command note / 项目命令说明:
- `../examples/config_build/` - `../examples/config_build/`
- `../examples/presets/` - `../examples/presets/`
- `../examples/async_basic/` - `../examples/async_basic/`
- `console_basic` / `text_formatter` / `style_tags` / `config_build` / `presets` are the portable-first set
- `console_basic` / `text_formatter` / `style_tags` / `config_build` / `presets` 是 portable-first 示例集
- `file_rotation` is native-sensitive, and `async_basic` stays native-only because of the current `async fn main` entry limitation
- `file_rotation` 是 native-sensitive`async_basic` 的 native-only 限制来自当前 `async fn main` 入口能力
- package-level API docs / 单接口 API 文档: - package-level API docs / 单接口 API 文档:
- `../docs/api/` - `../docs/api/`
- common entry points / 常用入口: - common entry points / 常用入口:
+2
View File
@@ -12,6 +12,8 @@ import {
options( options(
targets: { targets: {
"sinks_file.mbt": [ "native", "llvm", "js", "wasm", "wasm-gc" ],
"runtime_file_controls.mbt": [ "native", "llvm", "js", "wasm", "wasm-gc" ],
"file_backend_native.mbt": [ "native", "llvm" ], "file_backend_native.mbt": [ "native", "llvm" ],
"file_backend_stub.mbt": [ "js", "wasm", "wasm-gc" ], "file_backend_stub.mbt": [ "js", "wasm", "wasm-gc" ],
}, },