13 Commits

Author SHA1 Message Date
Nanaloveyuki 0b05ee0c94 🔖 release 0.7.3 2026-07-19 17:12:02 +08:00
Nanaloveyuki f9227890bd add wasm coverage and trace context 2026-07-19 17:04:27 +08:00
Nanaloveyuki 06db039a03 add release version helper 2026-07-17 21:26:59 +08:00
Nanaloveyuki 5ed02ec563 🔖 release 0.7.2 2026-07-17 21:22:04 +08:00
Nanaloveyuki a58a0747bb ♻️ migrate to core json 2026-07-17 21:19:11 +08:00
Nanaloveyuki bcfb35d7ae 🩹 harden file rotation failures 2026-07-17 20:10:25 +08:00
Nanaloveyuki 55540f56a2 ⬆️ upgrade async to 0.20.2 2026-07-17 19:42:53 +08:00
Nanaloveyuki 0342603b9e 🔖 prepare 1.1.1(0.7.1) release 2026-07-17 19:16:00 +08:00
Nanaloveyuki ea6113354a 🔧 support MoonBit 0.10.4 2026-07-17 19:04:17 +08:00
Nanaloveyuki b18dcf972d 🧪 relocate test logs 2026-07-17 18:22:04 +08:00
Nanaloveyuki a745cd5172 📝 add Chinese API guides and locale detection 2026-07-17 18:06:30 +08:00
Nanaloveyuki b32de63d57 📝 add Chinese examples and extension docs 2026-07-17 16:31:25 +08:00
Nanaloveyuki 4e2ef69b11 📝 add example-first documentation paths 2026-07-17 16:24:06 +08:00
144 changed files with 2963 additions and 892 deletions
+10 -2
View File
@@ -67,9 +67,15 @@ jobs:
moon check --deny-warn moon check --deny-warn
moon test --deny-warn moon test --deny-warn
moon check --target native --deny-warn moon check --target native --deny-warn
moon check --target wasm --deny-warn
moon check --target wasm-gc --deny-warn moon check --target wasm-gc --deny-warn
moon check --target js --deny-warn moon check --target js --deny-warn
- name: Test wasm path
shell: bash
run: |
moon test --target wasm --deny-warn
- name: Run sync example - name: Run sync example
shell: bash shell: bash
run: | run: |
@@ -105,15 +111,17 @@ jobs:
run: | run: |
moon update moon update
- name: Check async package on wasm-gc and js - name: Check async package on wasm, wasm-gc and js
shell: bash shell: bash
run: | run: |
moon check src-async --target wasm --deny-warn
moon check src-async --target wasm-gc --deny-warn moon check src-async --target wasm-gc --deny-warn
moon check src-async --target js --deny-warn moon check src-async --target js --deny-warn
- name: Test async package on wasm-gc and js - name: Test async package on wasm, wasm-gc and js
shell: bash shell: bash
run: | run: |
moon test src-async --target wasm --deny-warn
moon test src-async --target wasm-gc --deny-warn moon test src-async --target wasm-gc --deny-warn
moon test src-async --target js --deny-warn moon test src-async --target js --deny-warn
+3
View File
@@ -26,5 +26,8 @@ docs/.vitepress/cache/
docs/.vitepress/dist/ docs/.vitepress/dist/
docs/.vitepress/generated/ docs/.vitepress/generated/
# Benchmark package has no public API; moon info emits an empty snapshot.
benchmarks/pkg.generated.mbti
# vibecoding # vibecoding
AGENTS/ AGENTS/
+26
View File
@@ -0,0 +1,26 @@
# Agent Notes
## Version Releases
Do not update package versions and installation snippets by hand. Run the
repository release helper from the root instead:
```powershell
.\scripts\update-version.ps1 <x.x.x>
```
For a normal patch release, the script updates `moon.mod`, all current
installation examples, creates the next `docs/changes/<release>(<version>).md`,
and adds it to `docs/changes/index.md`. Review and replace any generated `TODO`
entries in the release note before committing.
For a major or minor package-version change, supply the public release number
explicitly so it is not inferred:
```powershell
.\scripts\update-version.ps1 <x.x.x> -ReleaseVersion <x.x.x>
```
Use `-WhatIf` to preview the target files. Pass `-Change`, `-Verification`,
and `-Note` to populate release-note sections directly. Validate the edited
repository before committing with a `gitmoji + short desc` message.
+45 -30
View File
@@ -6,34 +6,56 @@ BitLogger 是一个使用 MoonBit 编写的结构化日志库,适合命令行
- [English README](./docs/README-en.md) - [English README](./docs/README-en.md)
- [Wiki](https://bitlogger.naloveyuki.top) - [Wiki](https://bitlogger.naloveyuki.top)
## 介绍 ## 从第一个日志到扩展能力
BitLogger 提供统一的日志级别、目标名、结构化字段和可定制格式,既可以直接输出到控制台,也可以在 native 环境写入文件,并提供异步日志版本 BitLogger 的文档按使用路径组织:先完成一个可运行的日志输出,再按需进入文件、配置、异步和组合能力。API 文档保留为精确参考,不需要从 API 文件名开始阅读
## 快速开始 ### 1. 安装
```moonbit 从一个空项目开始:
let logger = build_logger(
text_console(
min_level=Level::Info,
target="demo",
text_formatter=TextFormatterConfig::new(show_timestamp=false, separator=" | "),
),
)
logger.info("starting", fields=[field("port", "8080")]) ```bash
ignore(logger.flush()) moon new log-demo
cd log-demo
moon add Nanaloveyuki/BitLogger@0.7.3
``` ```
推荐从 `console(...)``json_console(...)``text_console(...)``file(...)` 这几个入口开始,再按需配合 `with_queue(...)``with_file_rotation(...)` ### 2. 写入第一条结构化日志
如果你需要自己组合 sink,比如 `fanout``split``callback`,再使用 `Logger::new(...)` 在应用 package 的 `moon.pkg` 中导入库:
```moonbit
import {
"Nanaloveyuki/BitLogger/src" @log,
}
```
然后在 `main.mbt` 中创建控制台 logger
```moonbit
fn main {
let logger = @log.build_logger(
@log.console(min_level=@log.Level::Info, target="app"),
)
logger.info("server started", fields=[
@log.field("port", "8080"),
@log.field("environment", "development"),
])
}
```
运行 `moon run` 后,记录会携带 level、target、message 和 fields。下一步按实际需求选择:
- [控制台与结构化字段](./docs/examples/console.md)
- [文件输出与轮转](./docs/examples/file-rotation.md)
- [JSON 配置构建](./docs/examples/config.md)
- [异步日志生命周期](./docs/examples/async.md)
## 支持情况 ## 支持情况
- `BitLogger` 当前在 CI 中检查/验证的目标是 `native``js``wasm-gc` - `BitLogger` 当前在 CI 中检查/验证的目标是 `native``wasm``js``wasm-gc`
- `bitlogger_async` 当前在 CI 中检查 `native``js``wasm-gc`,测试覆盖 `native``js``wasm-gc` - `bitlogger_async` 当前在 CI 中检查和测试 `native``wasm``js``wasm-gc`
- `wasm` 目标在源码 `moon.pkg` 中保留声明,但当前未纳入 CI 验证口径
- `llvm` 目前按实验性目标处理,当前环境未完成验证 - `llvm` 目前按实验性目标处理,当前环境未完成验证
- 文件输出是 native 能力;跨端代码里建议先判断 `native_files_supported()` - 文件输出是 native 能力;跨端代码里建议先判断 `native_files_supported()`
- `src-async` 可用,但示例 `examples/async_basic` 目前仍按 native 入口提供 - `src-async` 可用,但示例 `examples/async_basic` 目前仍按 native 入口提供
@@ -46,20 +68,13 @@ ignore(logger.flush())
- 配置驱动构建:`build_logger(...)``build_async_logger(...)` - 配置驱动构建:`build_logger(...)``build_async_logger(...)`
- 组合能力:queue、filter、patch、fanout、split、callback - 组合能力:queue、filter、patch、fanout、split、callback
- 异步日志:独立 `src-async` package - 异步日志:独立 `src-async` package
- Trace context:标准 `trace_id``span_id``trace_flags``trace_state` 字段绑定
## 示例
- `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/`:异步日志示例
## 文档 ## 文档
- [API 索引](./docs/api/index.md) - [Examples:完整使用流程](./docs/examples/index.md)
- [Extend:队列、组合、格式化和跨端边界](./docs/extend/index.md)
- [API 索引:按公开符号查询](./docs/api/index.md)
- [src package README](./src/README.mbt.md) - [src package README](./src/README.mbt.md)
常用入口:`text_console(...)``file(...)``with_queue(...)``build_logger(...)``build_async_logger(...)` 仓库内的 `examples/` 目录与 Examples 文档一一对应;文档解释选择、前提、运行命令和后续扩展,源码保持为可执行参考。
+11
View File
@@ -0,0 +1,11 @@
# BitLogger Benchmarks
Run the native baseline from the repository root:
```powershell
moon bench benchmarks --target native --release --deny-warn
```
The suite measures callback emission, trace-context field binding, synchronous queue enqueue-and-flush, and native file writes. It intentionally has no pass/fail threshold: compare results only on the same machine, toolchain, target, and power profile.
Async throughput remains lifecycle-sensitive and is exercised by the native integration test rather than mixed into these synchronous microbenchmarks. Record its end-to-end measurements separately when profiling an application workload.
+61
View File
@@ -0,0 +1,61 @@
///|
let benchmark_record : @log.Record = @log.Record::new(
@log.Level::Info,
"benchmark record",
target="bench",
fields=[@log.field("component", "bitlogger")],
)
///|
test "bench callback logger emission" (it : @bench.T) {
let writes : Ref[Int] = Ref(0)
let logger = @log.Logger::new(@log.callback_sink(fn(_) { writes.val += 1 }))
it.bench(fn() {
logger.info("benchmark record", fields=[
@log.field("component", "bitlogger"),
])
it.keep(writes.val)
})
}
///|
test "bench trace context field binding" (it : @bench.T) {
let writes : Ref[Int] = Ref(0)
let context = @log.trace_context("trace-bench", "span-bench")
let logger = @log.Logger::new(@log.callback_sink(fn(_) { writes.val += 1 })).with_trace_context(
context,
)
it.bench(fn() {
logger.info("benchmark record", fields=[@log.field("event", "request")])
it.keep(writes.val)
})
}
///|
test "bench queued logger enqueue and flush" (it : @bench.T) {
let writes : Ref[Int] = Ref(0)
let logger = @log.Logger::new(@log.callback_sink(fn(_) { writes.val += 1 })).with_queue(
max_pending=8,
)
it.bench(fn() {
logger.info("benchmark record")
it.keep(logger.sink.flush())
})
}
///|
test "bench native file sink write" (it : @bench.T) {
let sink = @log.file_sink(
"logs/bitlogger-benchmark.log",
append=false,
auto_flush=false,
formatter=fn(rec) { rec.message },
)
if sink.is_available() {
it.bench(fn() {
sink.write(benchmark_record)
it.keep(sink.write_failures())
})
ignore(sink.close())
}
}
+4
View File
@@ -0,0 +1,4 @@
import {
"Nanaloveyuki/BitLogger/src" @log,
"moonbitlang/core/bench",
}
+124 -22
View File
@@ -104,6 +104,119 @@ function buildChangesSidebar(): DefaultTheme.SidebarItem[] {
] ]
} }
function buildExamplesSidebar(): DefaultTheme.SidebarItem[] {
return [
{ text: 'Overview', link: '/examples/' },
{ text: 'Console And Fields', link: '/examples/console' },
{ text: 'File Rotation', link: '/examples/file-rotation' },
{ text: 'Configuration', link: '/examples/config' },
{ text: 'Async Lifecycle', link: '/examples/async' },
]
}
function buildExtendSidebar(): DefaultTheme.SidebarItem[] {
return [
{ text: 'Overview', link: '/extend/' },
{ text: 'Queueing', link: '/extend/queue' },
{ text: 'Sink Composition', link: '/extend/composition' },
{ text: 'Text Formatting', link: '/extend/formatting' },
{ text: 'Target Boundaries', link: '/extend/targets' },
{ text: 'Trace Context', link: '/extend/observability' },
]
}
function buildChineseExamplesSidebar(): DefaultTheme.SidebarItem[] {
return [
{ text: '概览', link: '/zh/examples/' },
{ text: '控制台与结构化字段', link: '/zh/examples/console' },
{ text: '文件输出与轮转', link: '/zh/examples/file-rotation' },
{ text: '配置驱动构建', link: '/zh/examples/config' },
{ text: '异步日志生命周期', link: '/zh/examples/async' },
]
}
function buildChineseExtendSidebar(): DefaultTheme.SidebarItem[] {
return [
{ text: '概览', link: '/zh/extend/' },
{ text: '队列与溢出策略', link: '/zh/extend/queue' },
{ text: 'Sink 组合', link: '/zh/extend/composition' },
{ text: '文本格式与样式', link: '/zh/extend/formatting' },
{ text: '目标平台边界', link: '/zh/extend/targets' },
{ text: 'Trace Context', link: '/zh/extend/observability' },
]
}
function buildChineseApiSidebar(): DefaultTheme.SidebarItem[] {
return [
{ text: '概览', link: '/zh/api/' },
{ text: '基础记录', link: '/zh/api/basics' },
{ text: '配置与队列', link: '/zh/api/configuration' },
{ text: '文件输出', link: '/zh/api/file-output' },
{ text: '文本格式', link: '/zh/api/formatting' },
{ text: '异步生命周期', link: '/zh/api/async' },
{ text: 'Sink 组合', link: '/zh/api/composition' },
]
}
const englishThemeConfig: DefaultTheme.Config = {
siteTitle: 'BitLogger',
nav: [
{ text: 'Home', link: '/' },
{ text: 'Examples', link: '/examples/' },
{ text: 'Extend', link: '/extend/' },
{ text: 'API', link: '/api/' },
{ text: 'Changes', link: '/changes/' },
{ text: 'Mooncake', link: 'https://mooncakes.io/docs/Nanaloveyuki/BitLogger' },
],
search: {
provider: 'local',
},
socialLinks: [{ icon: 'github', link: repository }],
editLink: {
pattern: `${repository}/edit/main/docs/:path`,
text: 'Edit this page on GitHub',
},
sidebar: {
'/examples/': buildExamplesSidebar(),
'/extend/': buildExtendSidebar(),
'/api/': buildApiSidebar(),
'/changes/': buildChangesSidebar(),
},
footer: {
message: 'Published from the repository docs folder with VitePress.',
copyright: 'MIT',
},
}
const chineseThemeConfig: DefaultTheme.Config = {
siteTitle: 'BitLogger',
nav: [
{ text: '首页', link: '/zh/' },
{ text: '示例', link: '/zh/examples/' },
{ text: '扩展', link: '/zh/extend/' },
{ text: 'API', link: '/zh/api/' },
{ text: '更新记录(英文)', link: '/changes/' },
{ text: 'Mooncake', link: 'https://mooncakes.io/docs/Nanaloveyuki/BitLogger' },
],
search: {
provider: 'local',
},
socialLinks: [{ icon: 'github', link: repository }],
editLink: {
pattern: `${repository}/edit/main/docs/:path`,
text: '在 GitHub 上编辑此页',
},
sidebar: {
'/zh/examples/': buildChineseExamplesSidebar(),
'/zh/extend/': buildChineseExtendSidebar(),
'/zh/api/': buildChineseApiSidebar(),
},
footer: {
message: '由仓库中的 VitePress 文档构建。',
copyright: 'MIT',
},
}
export default defineConfig({ export default defineConfig({
title: 'BitLogger', title: 'BitLogger',
description: 'Structured logging library docs for MoonBit.', description: 'Structured logging library docs for MoonBit.',
@@ -114,29 +227,18 @@ export default defineConfig({
markdown: { markdown: {
languages: [createMoonbitLanguageRegistration()], languages: [createMoonbitLanguageRegistration()],
}, },
themeConfig: { themeConfig: englishThemeConfig,
siteTitle: 'BitLogger', locales: {
nav: [ root: {
{ text: 'Home', link: '/' }, label: 'English',
{ text: 'API', link: '/api/' }, lang: 'en-US',
{ text: 'Changes', link: '/changes/' }, themeConfig: englishThemeConfig,
{ text: 'Mooncake', link: 'https://mooncakes.io/docs/Nanaloveyuki/BitLogger' },
],
search: {
provider: 'local',
}, },
socialLinks: [{ icon: 'github', link: repository }], zh: {
editLink: { label: '简体中文',
pattern: `${repository}/edit/main/docs/:path`, lang: 'zh-CN',
text: 'Edit this page on GitHub', link: '/zh/',
}, themeConfig: chineseThemeConfig,
sidebar: {
'/api/': buildApiSidebar(),
'/changes/': buildChangesSidebar(),
},
footer: {
message: 'Published from the repository docs folder with VitePress.',
copyright: 'MIT',
}, },
}, },
}) })
+36 -1
View File
@@ -6,11 +6,46 @@ import HomeDocHub from './components/HomeDocHub.vue'
import './custom.css' import './custom.css'
const localePreferenceKey = 'bitlogger-docs-locale'
function routePath(path: string): string {
return path.split(/[?#]/, 1)[0]
}
function isLocaleRoot(path: string): boolean {
const normalized = routePath(path)
return normalized === '/' || normalized === '/zh/'
}
function browserPrefersChinese(): boolean {
return navigator.languages.some(language => language.toLowerCase().startsWith('zh'))
}
const theme: Theme = { const theme: Theme = {
extends: DefaultTheme, extends: DefaultTheme,
enhanceApp({ app }) { enhanceApp({ app, router }) {
app.component('ApiOverview', ApiOverview) app.component('ApiOverview', ApiOverview)
app.component('HomeDocHub', HomeDocHub) app.component('HomeDocHub', HomeDocHub)
if (typeof window === 'undefined') return
router.onAfterRouteChange = to => {
if (!isLocaleRoot(to)) return
window.localStorage.setItem(localePreferenceKey, routePath(to) === '/zh/' ? 'zh' : 'en')
}
if (router.route.path !== '/') return
const savedLocale = window.localStorage.getItem(localePreferenceKey)
const locale = savedLocale === 'zh' || savedLocale === 'en'
? savedLocale
: browserPrefersChinese()
? 'zh'
: 'en'
if (locale === 'zh') {
void router.go('/zh/')
}
}, },
} }
+31 -24
View File
@@ -9,28 +9,42 @@ BitLogger is a structured logging library for MoonBit projects.
BitLogger gives you consistent levels, targets, structured fields, configurable text output, native file logging, and an async logging layer. BitLogger gives you consistent levels, targets, structured fields, configurable text output, native file logging, and an async logging layer.
## Quick Start ## Start With A Working Flow
```moonbit The documentation is organized around complete usage flows. Start with an executable logger, then move into file output, configuration, async lifecycle, and extensions. The API reference remains the precise symbol-level reference.
let logger = build_logger(
text_console(
min_level=Level::Info,
target="demo",
text_formatter=TextFormatterConfig::new(show_timestamp=false, separator=" | "),
),
)
logger.info("starting", fields=[field("port", "8080")]) ### 1. Install
ignore(logger.flush())
```bash
moon new log-demo
cd log-demo
moon add Nanaloveyuki/BitLogger@0.7.3
``` ```
Start with `console(...)`, `json_console(...)`, `text_console(...)`, or `file(...)`, then add `with_queue(...)` or `with_file_rotation(...)` only when needed. ### 2. Import And Log
Use `Logger::new(...)` when you want to assemble custom sink graphs directly. Add the package import to your application's `moon.pkg`:
```moonbit
import {
"Nanaloveyuki/BitLogger/src" @log,
}
```
```moonbit
fn main {
let logger = @log.build_logger(
@log.console(min_level=@log.Level::Info, target="app"),
)
logger.info("server started", fields=[@log.field("port", "8080")])
}
```
Continue with the [Examples guide](./examples/index.md): console fields, file rotation, JSON configuration, and async lifecycle are each documented as a complete path.
## Support Status ## Support Status
- Current local verification covers `native`, `js`, `wasm`, and `wasm-gc` for the main `src` package and `src-async` - CI checks and tests `native`, `wasm`, `js`, and `wasm-gc` for the main `src` package and `src-async`
- `llvm` is still treated as experimental in the current release context and was not locally re-verified in the current environment - `llvm` is still treated as experimental in the current release context and was not locally re-verified in the current environment
- The source packages still declare `wasm` support alongside `native`, `llvm`, `js`, and `wasm-gc`; see [`target-verification.md`](./api/target-verification.md) for the current release-facing verification boundary - The source packages still declare `wasm` support alongside `native`, `llvm`, `js`, and `wasm-gc`; see [`target-verification.md`](./api/target-verification.md) for the current release-facing verification boundary
- File output is a native capability; check `native_files_supported()` in cross-target code - File output is a native capability; check `native_files_supported()` in cross-target code
@@ -44,19 +58,12 @@ Use `Logger::new(...)` when you want to assemble custom sink graphs directly.
- Config-based builders: `build_logger(...)` and `build_async_logger(...)` - Config-based builders: `build_logger(...)` and `build_async_logger(...)`
- Composition helpers: queue, filter, patch, fanout, split, callback - Composition helpers: queue, filter, patch, fanout, split, callback
- Separate async package under `src-async` - Separate async package under `src-async`
- Trace-context binding through standard `trace_id`, `span_id`, `trace_flags`, and `trace_state` fields
## Examples
- `examples/console_basic/`: minimal console and JSON console example
- `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 ## Documentation
- [Examples](./examples/index.md): complete application-facing usage flows
- [Extend](./extend/index.md): queueing, sink composition, formatting, and target boundaries
- [API index](./api/index.md): canonical API reference, organized as one public API per file - [API index](./api/index.md): canonical API reference, organized as one public API per file
- [src package README](https://github.com/Nanaloveyuki/BitLogger/blob/main/src/README.mbt.md): package-level usage notes and target reminders - [src package README](https://github.com/Nanaloveyuki/BitLogger/blob/main/src/README.mbt.md): package-level usage notes and target reminders
- [`docs/changes/`](./changes/): versioned release notes and publish-facing change summaries - [`docs/changes/`](./changes/): versioned release notes and publish-facing change summaries
@@ -13,14 +13,14 @@ key-word:
## Async-logger-build-config-to-json ## Async-logger-build-config-to-json
Convert `AsyncLoggerBuildConfig` into a `JsonValue`. This helper exports both the base synchronous logger config and the async runtime config as one structured payload. Convert `AsyncLoggerBuildConfig` into a `Json`. This helper exports both the base synchronous logger config and the async runtime config as one structured payload.
### Interface ### Interface
```moonbit ```moonbit
pub fn async_logger_build_config_to_json( pub fn async_logger_build_config_to_json(
config : AsyncLoggerBuildConfig, config : AsyncLoggerBuildConfig,
) -> @json_parser.JsonValue {} ) -> Json {}
``` ```
#### input #### input
@@ -29,7 +29,7 @@ pub fn async_logger_build_config_to_json(
#### output #### output
- `JsonValue` - Structured JSON representation of the full async build config. - `Json` - Structured JSON representation of the full async build config.
### Explanation ### Explanation
+3 -3
View File
@@ -13,12 +13,12 @@ key-word:
## Async-logger-config-to-json ## Async-logger-config-to-json
Convert a typed `AsyncLoggerConfig` into a `JsonValue`. This helper exports async queue capacity, overflow policy, batch sizing, linger timing, and flush behavior in a structured form. Convert a typed `AsyncLoggerConfig` into a `Json`. This helper exports async queue capacity, overflow policy, batch sizing, linger timing, and flush behavior in a structured form.
### Interface ### Interface
```moonbit ```moonbit
pub fn async_logger_config_to_json(config : AsyncLoggerConfig) -> @json_parser.JsonValue {} pub fn async_logger_config_to_json(config : AsyncLoggerConfig) -> Json {}
``` ```
#### input #### input
@@ -27,7 +27,7 @@ pub fn async_logger_config_to_json(config : AsyncLoggerConfig) -> @json_parser.J
#### output #### output
- `JsonValue` - Structured JSON representation of the async config. - `Json` - Structured JSON representation of the async config.
### Explanation ### Explanation
+4 -4
View File
@@ -13,12 +13,12 @@ key-word:
## Async-logger-state-to-json ## Async-logger-state-to-json
Convert `AsyncLoggerState` into a `JsonValue`. This helper is the structured export path for async logger runtime snapshots or manually constructed state values when callers want machine-readable diagnostics instead of a plain string. Convert `AsyncLoggerState` into a `Json`. This helper is the structured export path for async logger runtime snapshots or manually constructed state values when callers want machine-readable diagnostics instead of a plain string.
### Interface ### Interface
```moonbit ```moonbit
pub fn async_logger_state_to_json(state : AsyncLoggerState) -> @json_parser.JsonValue {} pub fn async_logger_state_to_json(state : AsyncLoggerState) -> Json {}
``` ```
#### input #### input
@@ -27,7 +27,7 @@ pub fn async_logger_state_to_json(state : AsyncLoggerState) -> @json_parser.Json
#### output #### output
- `JsonValue` - Structured JSON representation of the async logger snapshot. - `Json` - Structured JSON representation of the async logger snapshot.
### Explanation ### Explanation
@@ -62,7 +62,7 @@ In this example, callers receive a structured value that can be composed into la
When another serializer or pipeline expects a JSON value: When another serializer or pipeline expects a JSON value:
```moonbit ```moonbit
let payload = async_logger_state_to_json(logger.state()) let payload = async_logger_state_to_json(logger.state())
println(@json_parser.stringify(payload)) println(payload.stringify())
``` ```
In this example, the helper stays useful even outside the built-in stringify wrapper. In this example, the helper stays useful even outside the built-in stringify wrapper.
@@ -0,0 +1,21 @@
---
name: async-logger-with-trace-context
group: api
category: async
update-time: 20260719
description: Bind TraceContext fields before records enter an AsyncLogger queue.
key-word:
- async
- trace
- context
---
## Async-logger-with-trace-context
```moonbit
let contextual = logger.with_trace_context(
@bitlogger.trace_context("trace-1", "span-1"),
)
```
The helper stores the mapped fields on the returned async logger and preserves its queue, lifecycle, target, and threshold state. Context is added before records are enqueued.
+3 -3
View File
@@ -13,12 +13,12 @@ key-word:
## Async-runtime-state-to-json ## Async-runtime-state-to-json
Convert `AsyncRuntimeState` into a `JsonValue`. This helper exports the async runtime mode and background worker capability in a structured form. Convert `AsyncRuntimeState` into a `Json`. This helper exports the async runtime mode and background worker capability in a structured form.
### Interface ### Interface
```moonbit ```moonbit
pub fn async_runtime_state_to_json(state : AsyncRuntimeState) -> @json_parser.JsonValue {} pub fn async_runtime_state_to_json(state : AsyncRuntimeState) -> Json {}
``` ```
#### input #### input
@@ -27,7 +27,7 @@ pub fn async_runtime_state_to_json(state : AsyncRuntimeState) -> @json_parser.Js
#### output #### output
- `JsonValue` - Structured JSON representation of the runtime state. - `Json` - Structured JSON representation of the runtime state.
### Explanation ### Explanation
+4 -4
View File
@@ -13,12 +13,12 @@ key-word:
## File-rotation-config-to-json ## File-rotation-config-to-json
Convert `FileRotation` into a `JsonValue`. On the root `src` facade, this helper forwards to the concrete serializer owned by `src/file_model`. Convert `FileRotation` into a `Json`. On the root `src` facade, this helper forwards to the concrete serializer owned by `src/file_model`.
### Interface ### Interface
```moonbit ```moonbit
pub fn file_rotation_config_to_json(config : FileRotation) -> @json_parser.JsonValue {} pub fn file_rotation_config_to_json(config : FileRotation) -> Json {}
``` ```
#### input #### input
@@ -27,7 +27,7 @@ pub fn file_rotation_config_to_json(config : FileRotation) -> @json_parser.JsonV
#### output #### output
- `JsonValue` - Structured JSON representation of the rotation policy. - `Json` - Structured JSON representation of the rotation policy.
### Explanation ### Explanation
@@ -82,7 +82,7 @@ e.g.:
### Notes ### Notes
1. Use this helper when downstream code expects `JsonValue` rather than a typed `FileRotation` value. 1. Use this helper when downstream code expects `Json` rather than a typed `FileRotation` value.
2. Pair it with `file_rotation(...)` when building rotation policy in code before export. 2. Pair it with `file_rotation(...)` when building rotation policy in code before export.
+4 -4
View File
@@ -13,12 +13,12 @@ key-word:
## File-sink-policy-to-json ## File-sink-policy-to-json
Convert `FileSinkPolicy` into a `JsonValue`. On the root `src` facade, this helper forwards to the concrete serializer owned by `src/file_model`. Convert `FileSinkPolicy` into a `Json`. On the root `src` facade, this helper forwards to the concrete serializer owned by `src/file_model`.
### Interface ### Interface
```moonbit ```moonbit
pub fn file_sink_policy_to_json(policy : FileSinkPolicy) -> @json_parser.JsonValue {} pub fn file_sink_policy_to_json(policy : FileSinkPolicy) -> Json {}
``` ```
#### input #### input
@@ -27,7 +27,7 @@ pub fn file_sink_policy_to_json(policy : FileSinkPolicy) -> @json_parser.JsonVal
#### output #### output
- `JsonValue` - Structured JSON representation of the file policy. - `Json` - Structured JSON representation of the file policy.
### Explanation ### Explanation
@@ -73,7 +73,7 @@ e.g.:
### Notes ### Notes
1. Use this helper when downstream code expects `JsonValue` rather than text. 1. Use this helper when downstream code expects `Json` rather than text.
2. It pairs naturally with `FileSink::policy()`, `RuntimeSink::file_policy()`, and related default-policy accessors. 2. It pairs naturally with `FileSink::policy()`, `RuntimeSink::file_policy()`, and related default-policy accessors.
+2 -1
View File
@@ -2,7 +2,7 @@
name: file-sink-reopen-append name: file-sink-reopen-append
group: api group: api
category: sink category: sink
update-time: 20260613 update-time: 20260717
description: Reopen a FileSink in append mode. description: Reopen a FileSink in append mode.
key-word: key-word:
- file - file
@@ -37,6 +37,7 @@ Detailed rules explaining key parameters and behaviors
- Reopen behavior is fixed to append mode. - Reopen behavior is fixed to append mode.
- The stored append policy is updated to `true` as part of the reopen path. - The stored append policy is updated to `true` as part of the reopen path.
- On failure, the sink remains unavailable and `open_failures` is incremented. - On failure, the sink remains unavailable and `open_failures` is incremented.
- After a rotation failure, this is the explicit recovery path once the underlying remove, rename, or open condition has been resolved.
### How to Use ### How to Use
+4 -1
View File
@@ -2,7 +2,7 @@
name: file-sink-rotation-failures name: file-sink-rotation-failures
group: api group: api
category: sink category: sink
update-time: 20260707 update-time: 20260717
description: Read the number of rotation failures recorded by a FileSink. description: Read the number of rotation failures recorded by a FileSink.
key-word: key-word:
- file - file
@@ -36,6 +36,7 @@ Detailed rules explaining key parameters and behaviors
- The sink reports its recorded rotation-failure count directly. - The sink reports its recorded rotation-failure count directly.
- The counter increases when a rotation attempt cannot complete its critical file steps, such as removing the active file with zero backups, renaming a live file into `.1`, shifting an existing backup to a higher slot, or reopening the fresh active file afterward. - The counter increases when a rotation attempt cannot complete its critical file steps, such as removing the active file with zero backups, renaming a live file into `.1`, shifting an existing backup to a higher slot, or reopening the fresh active file afterward.
- Missing optional backup slots do not count by themselves; only steps that fail while their source path still exists are treated as rotation failures. - Missing optional backup slots do not count by themselves; only steps that fail while their source path still exists are treated as rotation failures.
- When a critical rotation step fails, the triggering record is not written and the sink remains unavailable until an explicit reopen succeeds.
- The counter is cumulative until reset. - The counter is cumulative until reset.
- This helper is observation-only and does not mutate sink state. - This helper is observation-only and does not mutate sink state.
@@ -70,6 +71,8 @@ e.g.:
- This counter alone does not say whether the sink is currently available. - This counter alone does not say whether the sink is currently available.
- After a recorded rotation failure, use `reopen_append()` only after the underlying filesystem condition has been resolved.
### Notes ### Notes
1. Use this helper when diagnosing incomplete direct file rotations. 1. Use this helper when diagnosing incomplete direct file rotations.
+4 -4
View File
@@ -13,12 +13,12 @@ key-word:
## File-sink-state-to-json ## File-sink-state-to-json
Convert `FileSinkState` into a `JsonValue`. On the root `src` facade, this helper forwards to the concrete serializer owned by `src/file_model`. Convert `FileSinkState` into a `Json`. On the root `src` facade, this helper forwards to the concrete serializer owned by `src/file_model`.
### Interface ### Interface
```moonbit ```moonbit
pub fn file_sink_state_to_json(state : FileSinkState) -> @json_parser.JsonValue {} pub fn file_sink_state_to_json(state : FileSinkState) -> Json {}
``` ```
#### input #### input
@@ -27,7 +27,7 @@ pub fn file_sink_state_to_json(state : FileSinkState) -> @json_parser.JsonValue
#### output #### output
- `JsonValue` - Structured JSON representation of the file sink state. - `Json` - Structured JSON representation of the file sink state.
### Explanation ### Explanation
@@ -72,7 +72,7 @@ e.g.:
### Notes ### Notes
1. Use this helper when diagnostics consumers expect `JsonValue`. 1. Use this helper when diagnostics consumers expect `Json`.
2. It pairs naturally with `FileSink::state()`, `RuntimeSink::file_state()`, and `ConfiguredLogger::file_state()`. 2. It pairs naturally with `FileSink::state()`, `RuntimeSink::file_state()`, and `ConfiguredLogger::file_state()`.
+8
View File
@@ -31,6 +31,7 @@ BitLogger API navigation.
- [logger-with-timestamp.md](./logger-with-timestamp.md) - [logger-with-timestamp.md](./logger-with-timestamp.md)
- [logger-with-context-fields.md](./logger-with-context-fields.md) - [logger-with-context-fields.md](./logger-with-context-fields.md)
- [logger-bind.md](./logger-bind.md) - [logger-bind.md](./logger-bind.md)
- [logger-with-trace-context.md](./logger-with-trace-context.md)
- [logger-with-filter.md](./logger-with-filter.md) - [logger-with-filter.md](./logger-with-filter.md)
- [logger-with-patch.md](./logger-with-patch.md) - [logger-with-patch.md](./logger-with-patch.md)
- [logger-with-queue.md](./logger-with-queue.md) - [logger-with-queue.md](./logger-with-queue.md)
@@ -99,6 +100,10 @@ BitLogger API navigation.
- [fields.md](./fields.md) - [fields.md](./fields.md)
- [text-formatter-config-type.md](./text-formatter-config-type.md) - [text-formatter-config-type.md](./text-formatter-config-type.md)
## Observability
- [trace-context.md](./trace-context.md)
## Record and level ## Record and level
- [record.md](./record.md) - [record.md](./record.md)
@@ -266,6 +271,7 @@ BitLogger API navigation.
- [async-logger-with-min-level.md](./async-logger-with-min-level.md) - [async-logger-with-min-level.md](./async-logger-with-min-level.md)
- [async-logger-with-timestamp.md](./async-logger-with-timestamp.md) - [async-logger-with-timestamp.md](./async-logger-with-timestamp.md)
- [async-logger-with-context-fields.md](./async-logger-with-context-fields.md) - [async-logger-with-context-fields.md](./async-logger-with-context-fields.md)
- [async-logger-with-trace-context.md](./async-logger-with-trace-context.md)
- [async-logger-with-filter.md](./async-logger-with-filter.md) - [async-logger-with-filter.md](./async-logger-with-filter.md)
- [async-logger-with-patch.md](./async-logger-with-patch.md) - [async-logger-with-patch.md](./async-logger-with-patch.md)
@@ -322,6 +328,7 @@ BitLogger API navigation.
- [library-logger-child.md](./library-logger-child.md) - [library-logger-child.md](./library-logger-child.md)
- [library-logger-with-context-fields.md](./library-logger-with-context-fields.md) - [library-logger-with-context-fields.md](./library-logger-with-context-fields.md)
- [library-logger-bind.md](./library-logger-bind.md) - [library-logger-bind.md](./library-logger-bind.md)
- [library-logger-with-trace-context.md](./library-logger-with-trace-context.md)
- [library-logger-is-enabled.md](./library-logger-is-enabled.md) - [library-logger-is-enabled.md](./library-logger-is-enabled.md)
- [library-logger-log.md](./library-logger-log.md) - [library-logger-log.md](./library-logger-log.md)
- [library-logger-info.md](./library-logger-info.md) - [library-logger-info.md](./library-logger-info.md)
@@ -341,6 +348,7 @@ BitLogger API navigation.
- [library-async-logger-child.md](./library-async-logger-child.md) - [library-async-logger-child.md](./library-async-logger-child.md)
- [library-async-logger-with-context-fields.md](./library-async-logger-with-context-fields.md) - [library-async-logger-with-context-fields.md](./library-async-logger-with-context-fields.md)
- [library-async-logger-bind.md](./library-async-logger-bind.md) - [library-async-logger-bind.md](./library-async-logger-bind.md)
- [library-async-logger-with-trace-context.md](./library-async-logger-with-trace-context.md)
- [library-async-logger-is-enabled.md](./library-async-logger-is-enabled.md) - [library-async-logger-is-enabled.md](./library-async-logger-is-enabled.md)
- [library-async-logger-log.md](./library-async-logger-log.md) - [library-async-logger-log.md](./library-async-logger-log.md)
- [library-async-logger-info.md](./library-async-logger-info.md) - [library-async-logger-info.md](./library-async-logger-info.md)
@@ -0,0 +1,15 @@
---
name: library-async-logger-with-trace-context
group: api
category: facade
update-time: 20260719
description: Bind TraceContext fields through the restricted LibraryAsyncLogger facade.
key-word:
- library
- async
- trace
---
## Library-async-logger-with-trace-context
`LibraryAsyncLogger::with_trace_context(...)` returns a derived library facade with `trace_id`, `span_id`, `trace_flags`, and optional `trace_state` attached to each later record. It does not start a worker or change lifecycle state.
@@ -0,0 +1,20 @@
---
name: library-logger-with-trace-context
group: api
category: facade
update-time: 20260719
description: Bind TraceContext fields through the restricted LibraryLogger facade.
key-word:
- library
- trace
- context
---
## Library-logger-with-trace-context
```moonbit
let logger = LibraryLogger::new(console_sink())
.with_trace_context(trace_context("trace-1", "span-1"))
```
The derived facade preserves its library-facing API while wrapping the sink with context fields. Use `to_logger()` only when broader composition is required.
+4 -4
View File
@@ -13,12 +13,12 @@ key-word:
## Logger-config-to-json ## Logger-config-to-json
Convert a typed `LoggerConfig` into a `JsonValue`. This helper is the structured export path when config should be persisted, inspected, or embedded into larger JSON payloads. Convert a typed `LoggerConfig` into a `Json`. This helper is the structured export path when config should be persisted, inspected, or embedded into larger JSON payloads.
### Interface ### Interface
```moonbit ```moonbit
pub fn logger_config_to_json(config : LoggerConfig) -> @json_parser.JsonValue {} pub fn logger_config_to_json(config : LoggerConfig) -> Json {}
``` ```
#### input #### input
@@ -27,7 +27,7 @@ pub fn logger_config_to_json(config : LoggerConfig) -> @json_parser.JsonValue {}
#### output #### output
- `JsonValue` - JSON representation of the logger config. - `Json` - JSON representation of the logger config.
### Explanation ### Explanation
@@ -73,5 +73,5 @@ e.g.:
1. Use this helper when you need a reusable JSON value rather than a final JSON string. 1. Use this helper when you need a reusable JSON value rather than a final JSON string.
2. Use `stringify_logger_config(...)` when the next consumer expects JSON text instead of `JsonValue`. 2. Use `stringify_logger_config(...)` when the next consumer expects JSON text instead of `Json`.
+22
View File
@@ -0,0 +1,22 @@
---
name: logger-with-trace-context
group: api
category: logger
update-time: 20260719
description: Bind a TraceContext to every record from a synchronous Logger.
key-word:
- logger
- trace
- context
---
## Logger-with-trace-context
```moonbit
pub fn[S] Logger::with_trace_context(
self : Logger[S],
context : TraceContext,
) -> Logger[ContextSink[S]]
```
Returns a derived logger that prepends the context fields to every record. The original logger is unchanged. This is equivalent to `with_context_fields(context.as_fields())`, while making trace intent explicit.
+4 -4
View File
@@ -13,12 +13,12 @@ key-word:
## Queue-config-to-json ## Queue-config-to-json
Convert a typed `QueueConfig` into a `JsonValue`. This helper is the structured export path for synchronous queue wrapper configuration when callers want machine-readable config output. Convert a typed `QueueConfig` into a `Json`. This helper is the structured export path for synchronous queue wrapper configuration when callers want machine-readable config output.
### Interface ### Interface
```moonbit ```moonbit
pub fn queue_config_to_json(queue : QueueConfig) -> @json_parser.JsonValue {} pub fn queue_config_to_json(queue : QueueConfig) -> Json {}
``` ```
#### input #### input
@@ -27,7 +27,7 @@ pub fn queue_config_to_json(queue : QueueConfig) -> @json_parser.JsonValue {}
#### output #### output
- `JsonValue` - Structured JSON representation of the queue config. - `Json` - Structured JSON representation of the queue config.
### Explanation ### Explanation
@@ -74,5 +74,5 @@ e.g.:
1. Use this helper when you need a reusable JSON value rather than a final JSON string. 1. Use this helper when you need a reusable JSON value rather than a final JSON string.
2. Use `stringify_queue_config(...)` when the next consumer expects text instead of `JsonValue`. 2. Use `stringify_queue_config(...)` when the next consumer expects text instead of `Json`.
+3 -3
View File
@@ -13,12 +13,12 @@ key-word:
## Runtime-file-state-to-json ## Runtime-file-state-to-json
Convert `RuntimeFileState` into a `JsonValue`. On the root `src` facade, this helper forwards to the concrete serializer owned by `src/file_model`. Convert `RuntimeFileState` into a `Json`. On the root `src` facade, this helper forwards to the concrete serializer owned by `src/file_model`.
### Interface ### Interface
```moonbit ```moonbit
pub fn runtime_file_state_to_json(state : RuntimeFileState) -> @json_parser.JsonValue {} pub fn runtime_file_state_to_json(state : RuntimeFileState) -> Json {}
``` ```
#### input #### input
@@ -27,7 +27,7 @@ pub fn runtime_file_state_to_json(state : RuntimeFileState) -> @json_parser.Json
#### output #### output
- `JsonValue` - Structured JSON representation of the runtime file state. - `Json` - Structured JSON representation of the runtime file state.
### Explanation ### Explanation
+3 -3
View File
@@ -13,12 +13,12 @@ key-word:
## Sink-config-to-json ## Sink-config-to-json
Convert `SinkConfig` into a `JsonValue`. This helper is used directly for sink export and indirectly when exporting `LoggerConfig`. Convert `SinkConfig` into a `Json`. This helper is used directly for sink export and indirectly when exporting `LoggerConfig`.
### Interface ### Interface
```moonbit ```moonbit
pub fn sink_config_to_json(config : SinkConfig) -> @json_parser.JsonValue {} pub fn sink_config_to_json(config : SinkConfig) -> Json {}
``` ```
#### input #### input
@@ -27,7 +27,7 @@ pub fn sink_config_to_json(config : SinkConfig) -> @json_parser.JsonValue {}
#### output #### output
- `JsonValue` - JSON representation of the sink configuration. - `Json` - JSON representation of the sink configuration.
### Explanation ### Explanation
@@ -40,7 +40,7 @@ Detailed rules explaining key parameters and behaviors
- `pretty=false` returns compact JSON. - `pretty=false` returns compact JSON.
- `pretty=true` returns indented JSON for human inspection. - `pretty=true` returns indented JSON for human inspection.
- This helper is built on top of `async_logger_build_config_to_json(...)`. - This helper is built on top of `async_logger_build_config_to_json(...)`.
- Internally it serializes the `JsonValue` result with `@json_parser.stringify(...)` or `@json_parser.stringify_pretty(value, 2)`, so the text form stays aligned with the structured async build-config export helper. - Internally it serializes the `Json` result with `value.stringify(...)` or `value.stringify(indent=2)`, so the text form stays aligned with the structured async build-config export helper.
- The output keeps `logger` and `async_config` as separate sections, matching supported parser input. - The output keeps `logger` and `async_config` as separate sections, matching supported parser input.
- The serialized `logger` section preserves the full `LoggerConfig` shape, even though `build_async_text_logger(...)` later consumes only the selected text-oriented subset of that logger config. - The serialized `logger` section preserves the full `LoggerConfig` shape, even though `build_async_text_logger(...)` later consumes only the selected text-oriented subset of that logger config.
- That includes preserving whatever `logger.sink.kind` text is present in the config, even though the later text-specific builder path still ignores that sink-kind branch and constructs `FormattedConsoleSink` from `logger.sink.text_formatter`. - That includes preserving whatever `logger.sink.kind` text is present in the config, even though the later text-specific builder path still ignores that sink-kind branch and constructs `FormattedConsoleSink` from `logger.sink.text_formatter`.
@@ -80,7 +80,7 @@ In this example, compact JSON is returned without extra formatting.
### Error Case ### Error Case
e.g.: e.g.:
- If callers need a `JsonValue` for further composition, they should use `async_logger_build_config_to_json(...)` instead. - If callers need a `Json` for further composition, they should use `async_logger_build_config_to_json(...)` instead.
- If only one layer of config is required, this helper may be broader than necessary. - If only one layer of config is required, this helper may be broader than necessary.
@@ -96,7 +96,7 @@ e.g.:
4. In particular, serialized `logger.sink.kind` text is descriptive config data, not a guarantee that the text-specific builder path will branch on that sink kind later. 4. In particular, serialized `logger.sink.kind` text is descriptive config data, not a guarantee that the text-specific builder path will branch on that sink kind later.
5. Use `async_logger_build_config_to_json(...)` when the next consumer still needs a `JsonValue` for composition before final stringification. 5. Use `async_logger_build_config_to_json(...)` when the next consumer still needs a `Json` for composition before final stringification.
6. After parsing, the same text can also feed the application or library facade builders; string export preserves one shared build-config shape, not a direct-builder-only route. 6. After parsing, the same text can also feed the application or library facade builders; string export preserves one shared build-config shape, not a direct-builder-only route.
+3 -3
View File
@@ -37,7 +37,7 @@ Detailed rules explaining key parameters and behaviors
- `pretty=false` returns compact JSON suitable for transport and snapshots. - `pretty=false` returns compact JSON suitable for transport and snapshots.
- `pretty=true` returns indented JSON for humans. - `pretty=true` returns indented JSON for humans.
- This helper is built on top of `async_logger_config_to_json(...)`. - This helper is built on top of `async_logger_config_to_json(...)`.
- Internally it serializes the `JsonValue` result with `@json_parser.stringify(...)` or `@json_parser.stringify_pretty(value, 2)`, so the text form stays aligned with the structured async-config export helper. - Internally it serializes the `Json` result with `value.stringify(...)` or `value.stringify(indent=2)`, so the text form stays aligned with the structured async-config export helper.
- The exported text follows the supported async config schema rather than internal queue implementation details. - The exported text follows the supported async config schema rather than internal queue implementation details.
- Canonical policy labels such as `DropNewest` and `Never` are emitted even though the parser also accepts aliases like `DropLatest` and `None`. - Canonical policy labels such as `DropNewest` and `Never` are emitted even though the parser also accepts aliases like `DropLatest` and `None`.
@@ -68,7 +68,7 @@ In this example, compact JSON is returned without extra formatting.
### Error Case ### Error Case
e.g.: e.g.:
- If callers need a `JsonValue` for composition, they should use `async_logger_config_to_json(...)` instead. - If callers need a `Json` for composition, they should use `async_logger_config_to_json(...)` instead.
- If invalid constructor inputs were normalized earlier, the resulting text contains the normalized config values. - If invalid constructor inputs were normalized earlier, the resulting text contains the normalized config values.
@@ -78,5 +78,5 @@ e.g.:
2. If negative `max_pending` semantics matter, remember that the serialized text preserves the config value while runtime queue creation later clamps the queue limit to `0`. 2. If negative `max_pending` semantics matter, remember that the serialized text preserves the config value while runtime queue creation later clamps the queue limit to `0`.
3. Use `async_logger_config_to_json(...)` when the next consumer still needs a `JsonValue` for composition before final stringification. 3. Use `async_logger_config_to_json(...)` when the next consumer still needs a `Json` for composition before final stringification.
+2 -2
View File
@@ -40,7 +40,7 @@ Detailed rules explaining key parameters and behaviors
- `pretty=false` returns compact JSON. - `pretty=false` returns compact JSON.
- `pretty=true` returns indented JSON suitable for human diagnostics. - `pretty=true` returns indented JSON suitable for human diagnostics.
- This helper is built on top of `async_logger_state_to_json(...)`. - This helper is built on top of `async_logger_state_to_json(...)`.
- Internally it stringifies the same JSON snapshot shape returned by the public export helper, using `@json_parser.stringify(...)` or `@json_parser.stringify_pretty(value, 2)`. - Internally it stringifies the same JSON snapshot shape returned by the public export helper, using `value.stringify(...)` or `value.stringify(indent=2)`.
- The compact form matches snapshots such as `{"runtime":{"mode":"native_worker","background_worker":true},"phase":"ready","pending_count":0,...}`. - The compact form matches snapshots such as `{"runtime":{"mode":"native_worker","background_worker":true},"phase":"ready","pending_count":0,...}`.
- String output is convenient for logs, CLI output, and startup diagnostics. - String output is convenient for logs, CLI output, and startup diagnostics.
- The serializer does not care whether the input came from a live logger read or a synthetic `AsyncLoggerState::new(...)` call. - The serializer does not care whether the input came from a live logger read or a synthetic `AsyncLoggerState::new(...)` call.
@@ -83,7 +83,7 @@ e.g.:
2. The compact output shape is already locked by the async runtime snapshot tests, so it is suitable for stable diagnostics and assertions. 2. The compact output shape is already locked by the async runtime snapshot tests, so it is suitable for stable diagnostics and assertions.
3. Use `async_logger_state_to_json(...)` when the next consumer still needs a `JsonValue` for composition before final stringification. 3. Use `async_logger_state_to_json(...)` when the next consumer still needs a `Json` for composition before final stringification.
4. Pair it with `AsyncLogger::state()` for current logger diagnostics, or with `AsyncLoggerState::new(...)` when exporting a manual snapshot built by tests or adapters. 4. Pair it with `AsyncLogger::state()` for current logger diagnostics, or with `AsyncLoggerState::new(...)` when exporting a manual snapshot built by tests or adapters.
+2 -2
View File
@@ -40,7 +40,7 @@ Detailed rules explaining key parameters and behaviors
- `pretty=false` returns compact JSON. - `pretty=false` returns compact JSON.
- `pretty=true` returns indented JSON for human diagnostics. - `pretty=true` returns indented JSON for human diagnostics.
- This helper is built on top of `async_runtime_state_to_json(...)`. - This helper is built on top of `async_runtime_state_to_json(...)`.
- Internally it serializes the `JsonValue` result with `@json_parser.stringify(...)` or `@json_parser.stringify_pretty(value, 2)`, so the text form stays aligned with the structured runtime-state export helper. - Internally it serializes the `Json` result with `value.stringify(...)` or `value.stringify(indent=2)`, so the text form stays aligned with the structured runtime-state export helper.
- The compact form matches the tested snapshot shape such as `{"mode":"native_worker","background_worker":true}`. - The compact form matches the tested snapshot shape such as `{"mode":"native_worker","background_worker":true}`.
- It is well-suited for startup banners, support reports, and target capability logs. - It is well-suited for startup banners, support reports, and target capability logs.
@@ -79,5 +79,5 @@ e.g.:
2. This helper is the direct text form of the same two-field runtime snapshot exported by `async_runtime_state_to_json(...)`. 2. This helper is the direct text form of the same two-field runtime snapshot exported by `async_runtime_state_to_json(...)`.
3. Use `async_runtime_state_to_json(...)` when the next consumer still needs a `JsonValue` for composition before final stringification. 3. Use `async_runtime_state_to_json(...)` when the next consumer still needs a `Json` for composition before final stringification.
+1 -1
View File
@@ -66,7 +66,7 @@ In this example, compact JSON is produced without extra formatting logic.
### Error Case ### Error Case
e.g.: e.g.:
- If callers need a `JsonValue` for composition rather than text, `file_sink_policy_to_json(...)` is the better API. - If callers need a `Json` for composition rather than text, `file_sink_policy_to_json(...)` is the better API.
- If rotation is disabled, the output still includes `rotation` as `null`. - If rotation is disabled, the output still includes `rotation` as `null`.
+2 -2
View File
@@ -37,7 +37,7 @@ Detailed rules explaining key parameters and behaviors
- `pretty=false` produces compact JSON. - `pretty=false` produces compact JSON.
- `pretty=true` produces indented human-readable JSON. - `pretty=true` produces indented human-readable JSON.
- This helper builds on top of `logger_config_to_json(...)`. - This helper builds on top of `logger_config_to_json(...)`.
- Internally it serializes the `JsonValue` result with `@json_parser.stringify(...)` or `@json_parser.stringify_pretty(value, 2)`, so the text form stays aligned with the structured logger-config export helper. - Internally it serializes the `Json` result with `value.stringify(...)` or `value.stringify(indent=2)`, so the text form stays aligned with the structured logger-config export helper.
- Output is stable and suited for roundtrip config workflows. - Output is stable and suited for roundtrip config workflows.
### How to Use ### How to Use
@@ -71,7 +71,7 @@ e.g.:
### Notes ### Notes
1. Use this helper when the next consumer expects JSON text instead of `JsonValue`. 1. Use this helper when the next consumer expects JSON text instead of `Json`.
2. Use `logger_config_to_json(...)` when you still need to embed the config inside a larger JSON object before final stringification. 2. Use `logger_config_to_json(...)` when you still need to embed the config inside a larger JSON object before final stringification.
+3 -3
View File
@@ -37,7 +37,7 @@ Detailed rules explaining key parameters and behaviors
- `pretty=false` returns compact JSON suitable for logs and snapshots. - `pretty=false` returns compact JSON suitable for logs and snapshots.
- `pretty=true` returns indented JSON for human inspection. - `pretty=true` returns indented JSON for human inspection.
- This helper is built on top of `queue_config_to_json(...)`. - This helper is built on top of `queue_config_to_json(...)`.
- Internally it serializes the `JsonValue` result with `@json_parser.stringify(...)` or `@json_parser.stringify_pretty(value, 2)`, so the text form stays aligned with the structured export helper. - Internally it serializes the `Json` result with `value.stringify(...)` or `value.stringify(indent=2)`, so the text form stays aligned with the structured export helper.
- The resulting text follows the same queue schema accepted by config parsing flows. - The resulting text follows the same queue schema accepted by config parsing flows.
### How to Use ### How to Use
@@ -67,13 +67,13 @@ In this example, compact JSON is returned without extra formatting.
### Error Case ### Error Case
e.g.: e.g.:
- If callers need a `JsonValue` for further composition, this API is the wrong layer and `queue_config_to_json(...)` should be used instead. - If callers need a `Json` for further composition, this API is the wrong layer and `queue_config_to_json(...)` should be used instead.
- If queue policy is too aggressive for workload burst size, serialization still succeeds because this helper only exports config. - If queue policy is too aggressive for workload burst size, serialization still succeeds because this helper only exports config.
### Notes ### Notes
1. Use this helper when the next consumer expects JSON text instead of `JsonValue`. 1. Use this helper when the next consumer expects JSON text instead of `Json`.
2. Use `queue_config_to_json(...)` when you still need to embed the queue config inside a larger JSON object before final stringification. 2. Use `queue_config_to_json(...)` when you still need to embed the queue config inside a larger JSON object before final stringification.
+1 -1
View File
@@ -69,7 +69,7 @@ In this example, compact JSON is returned without extra formatting logic.
### Error Case ### Error Case
e.g.: e.g.:
- If callers need a `JsonValue` rather than text, `runtime_file_state_to_json(...)` is the better API. - If callers need a `Json` rather than text, `runtime_file_state_to_json(...)` is the better API.
- If queue wrapping is not relevant, `stringify_file_sink_state(...)` may be the simpler API. - If queue wrapping is not relevant, `stringify_file_sink_state(...)` may be the simpler API.
+2 -2
View File
@@ -37,7 +37,7 @@ Detailed rules explaining key parameters and behaviors
- `pretty=false` gives compact JSON. - `pretty=false` gives compact JSON.
- `pretty=true` gives indented output. - `pretty=true` gives indented output.
- This helper builds on top of `sink_config_to_json(...)`. - This helper builds on top of `sink_config_to_json(...)`.
- Internally it serializes the `JsonValue` result with `@json_parser.stringify(...)` or `@json_parser.stringify_pretty(value, 2)`, so the text form stays aligned with the structured sink export helper. - Internally it serializes the `Json` result with `value.stringify(...)` or `value.stringify(indent=2)`, so the text form stays aligned with the structured sink export helper.
- It is useful when examples or generated docs want to show only sink-specific config. - It is useful when examples or generated docs want to show only sink-specific config.
### How to Use ### How to Use
@@ -71,7 +71,7 @@ e.g.:
### Notes ### Notes
1. Use this helper when the next consumer expects JSON text instead of `JsonValue`. 1. Use this helper when the next consumer expects JSON text instead of `Json`.
2. Use `sink_config_to_json(...)` when you still need to embed the sink config inside a larger JSON object before final stringification. 2. Use `sink_config_to_json(...)` when you still need to embed the sink config inside a larger JSON object before final stringification.
+2 -2
View File
@@ -40,7 +40,7 @@ Detailed rules explaining key parameters and behaviors
- `pretty=false` returns compact JSON. - `pretty=false` returns compact JSON.
- `pretty=true` returns indented JSON for humans. - `pretty=true` returns indented JSON for humans.
- This helper is built on top of `text_formatter_config_to_json(...)`. - This helper is built on top of `text_formatter_config_to_json(...)`.
- Internally it serializes the `JsonValue` result with `@json_parser.stringify(...)` or `@json_parser.stringify_pretty(value, 2)`, so the text form stays aligned with the structured formatter export helper. - Internally it serializes the `Json` result with `value.stringify(...)` or `value.stringify(indent=2)`, so the text form stays aligned with the structured formatter export helper.
- The output preserves the supported formatter config schema instead of any runtime-only formatter instance details. - The output preserves the supported formatter config schema instead of any runtime-only formatter instance details.
### How to Use ### How to Use
@@ -76,7 +76,7 @@ e.g.:
### Notes ### Notes
1. Use this helper when the next consumer expects JSON text instead of `JsonValue`. 1. Use this helper when the next consumer expects JSON text instead of `Json`.
2. Use `text_formatter_config_to_json(...)` when you still need to embed formatter config inside a larger JSON object before final stringification. 2. Use `text_formatter_config_to_json(...)` when you still need to embed formatter config inside a larger JSON object before final stringification.
+3 -3
View File
@@ -13,12 +13,12 @@ key-word:
## Text-formatter-config-to-json ## Text-formatter-config-to-json
Convert a typed `TextFormatterConfig` into a `JsonValue`. This helper exports formatter toggles, separators, color settings, markup behavior, and optional style tags in a machine-readable form. Convert a typed `TextFormatterConfig` into a `Json`. This helper exports formatter toggles, separators, color settings, markup behavior, and optional style tags in a machine-readable form.
### Interface ### Interface
```moonbit ```moonbit
pub fn text_formatter_config_to_json(config : TextFormatterConfig) -> @json_parser.JsonValue {} pub fn text_formatter_config_to_json(config : TextFormatterConfig) -> Json {}
``` ```
#### input #### input
@@ -27,7 +27,7 @@ pub fn text_formatter_config_to_json(config : TextFormatterConfig) -> @json_pars
#### output #### output
- `JsonValue` - Structured JSON representation of the formatter config. - `Json` - Structured JSON representation of the formatter config.
### Explanation ### Explanation
+25
View File
@@ -0,0 +1,25 @@
---
name: trace-context
group: api
category: observability
update-time: 20260719
description: Represent and bind distributed trace metadata using stable structured-log fields.
key-word:
- trace
- context
- observability
---
## Trace-context
`TraceContext` carries the standard log fields used to correlate records with distributed traces: `trace_id`, `span_id`, `trace_flags`, and optional `trace_state`.
```moonbit
let context = trace_context(
"4bf92f3577b34da6a3ce929d0e0e4736",
"00f067aa0ba902b7",
trace_state="vendor=value",
)
```
Use `context.as_fields()` when integrating with a custom carrier, or pass the value to a logger's `with_trace_context(...)` helper. BitLogger does not parse HTTP headers or export OTLP data; an application-owned tracing library remains responsible for propagation and sampling.
+26
View File
@@ -0,0 +1,26 @@
## BitLogger Update Changes
version 1.1.1(0.7.1)
### Toolchain
- fix: migrate runnable example packages to `pkgtype(kind: "executable")` for MoonBit 0.10.4 formatter compatibility
- fix: replace ambiguous empty map literals with `Map([])` so `--deny-warn` checks remain clean on MoonBit 0.10.4
### Test
- test: write file-backed test output under `logs/` instead of the repository root
- test: keep the tracked `logs/.gitkeep` directory available for clean test checkouts
### Docs
- docs: update installation snippets to `Nanaloveyuki/BitLogger@0.7.1`
- docs: add this 1.1.1(0.7.1) release entry to the change index
### Verification
- verify: pass formatting, warning-denied checks, synchronous and async cross-target tests, examples, and coverage on MoonBit 0.10.4
### Notes
- this is a compatibility and test-output maintenance release with no public API changes
+21
View File
@@ -0,0 +1,21 @@
## BitLogger Update Changes
version 1.1.2(0.7.2)
### JSON
- refactor: replace `maria/json_parser` with the official `moonbitlang/core/json` implementation
- refactor: preserve existing serialized JSON field shapes while JSON conversion APIs now return the built-in `Json` type
### Docs
- docs: update installation snippets to `Nanaloveyuki/BitLogger@0.7.2`
- docs: add this 1.1.2(0.7.2) release entry to the change index
### Verification
- verify: pass formatting, warning-denied checks, and default, native, JavaScript, WebAssembly, and Wasm-GC test targets
### Notes
- LLVM was not tested because the local MoonBit toolchain is missing the experimental LLVM core bundle required by that target
+19
View File
@@ -0,0 +1,19 @@
## BitLogger Update Changes
version 1.1.3(0.7.3)
### Changes
- Add wasm CI checks and tests for the sync and async packages.
- Add TraceContext binding for sync and async logger facades.
- Add native async file lifecycle integration coverage and reproducible benchmarks.
### Verification
- Pass moon fmt --check, warning-denied checks, and tests across native, wasm, wasm-gc, and js.
- Build the VitePress documentation site and run the native benchmark suite.
### Notes
- LLVM remains experimental and was not verified locally.
- TraceContext maps structured fields only; HTTP propagation and OTLP export remain application-owned.
+3
View File
@@ -2,6 +2,9 @@
Versioned BitLogger change summaries. Versioned BitLogger change summaries.
- [1.1.3](./1.1.3(0.7.3).md)
- [1.1.2](./1.1.2(0.7.2).md)
- [1.1.1](./1.1.1(0.7.1).md)
- [1.1.0](./1.1.0(0.7.0).md) - [1.1.0](./1.1.0(0.7.0).md)
- [1.0.1](./1.0.1(0.6.1).md) - [1.0.1](./1.0.1(0.6.1).md)
- [1.0.0](./1.0.0(0.6.0).md) - [1.0.0](./1.0.0(0.6.0).md)
+47
View File
@@ -0,0 +1,47 @@
# Async Logger Lifecycle
Use the async package when a native application needs logging to run through an async queue. The library supports broader targets, but the shipped `async fn main` example is native-focused.
## Import Both Packages
```moonbit
import {
"Nanaloveyuki/BitLogger/src" @log,
"Nanaloveyuki/BitLogger/src-async" @async_log,
"moonbitlang/async",
}
```
## Build, Run, Then Shut Down
```moonbit
async fn main {
let raw = "{\"logger\":{\"min_level\":\"info\",\"target\":\"service.async\",\"sink\":{\"kind\":\"text_console\"}},\"async_config\":{\"max_pending\":64,\"overflow\":\"DropOldest\",\"max_batch\":16,\"linger_ms\":5,\"flush\":\"Batch\"}}"
let config = @async_log.parse_async_logger_build_config_text(raw) catch {
err => {
ignore(err)
return
}
}
let logger = @async_log.build_async_logger(config)
@async.with_task_group(group => {
group.spawn_bg(allow_failure=true, () => logger.run())
logger.info("worker started", fields=[@log.field("mode", "async")])
logger.shutdown()
})
}
```
`run()` owns the worker loop. Start it before producing records, then call `shutdown()` so pending work can follow the configured flush policy.
## Run The Repository Example
```bash
moon run examples/async_basic --target native
```
## Next Steps
- Inspect queue policy and flush choices in [Queueing](../extend/queue.md).
- Reference: [`parse_async_logger_build_config_text(...)`](../api/parse-async-logger-build-config-text.md), [`build_async_logger(...)`](../api/build-async-logger.md), [`run()`](../api/async-logger-run.md), and [`shutdown()`](../api/async-logger-shutdown.md).
+35
View File
@@ -0,0 +1,35 @@
# Configuration-Driven Logging
Use configuration when deployment should select levels, output, formatting, or queue behavior without changing application code.
## Parse, Build, Then Log
```moonbit
let raw = "{\"min_level\":\"info\",\"target\":\"service\",\"sink\":{\"kind\":\"text_console\",\"text_formatter\":{\"show_timestamp\":false,\"separator\":\" | \"}},\"queue\":{\"max_pending\":64,\"overflow\":\"DropOldest\"}}"
let config = @log.parse_logger_config_text(raw) catch {
err => {
ignore(err)
println("invalid logger configuration")
return
}
}
let logger = @log.build_logger(config)
logger.info("configured logger ready")
ignore(logger.flush())
```
Parsing validates the data before runtime construction. Keep the raw configuration outside application code in a real deployment, but handle parsing errors at the boundary where configuration enters the process.
## Run The Repository Example
```bash
moon run examples/config_build
```
## Next Steps
- Use [Queueing](../extend/queue.md) to choose overflow behavior deliberately.
- Use [Async lifecycle](./async.md) when the queue needs a background worker.
- Reference: [`parse_logger_config_text(...)`](../api/parse-logger-config-text.md), [`build_logger(...)`](../api/build-logger.md), and [`with_queue(...)`](../api/with-queue.md).
+51
View File
@@ -0,0 +1,51 @@
# Console And Structured Fields
Use this flow for command-line tools and services that need a human-readable starting point. A record always has a level, target, message, and optional fields.
## Install And Import
```bash
moon add Nanaloveyuki/BitLogger@0.7.3
```
```moonbit
import {
"Nanaloveyuki/BitLogger/src" @log,
}
```
## Build A Console Logger
```moonbit
fn main {
let logger = @log.build_logger(
@log.console(min_level=@log.Level::Info, target="service.http"),
)
logger.info("listening", fields=[
@log.field("port", "8080"),
@log.field("environment", "development"),
])
logger.warn("slow request", fields=[@log.field("path", "/health")])
}
```
`min_level` filters records before they reach the sink. `target` identifies the subsystem, while fields carry queryable context without interpolating it into the message.
## Switch To JSON
Keep the logging calls unchanged and replace the preset:
```moonbit
let logger = @log.build_logger(
@log.json_console(min_level=@log.Level::Info, target="service.http"),
)
```
This is the preferred output for a collector that consumes one structured record per line.
## Next Steps
- Use [Text formatting](../extend/formatting.md) for a custom terminal format.
- Use [Configuration](./config.md) when levels, targets, and sinks come from JSON.
- Reference: [`console(...)`](../api/console.md), [`json_console(...)`](../api/json-console.md), [`field(...)`](../api/field.md), and [`build_logger(...)`](../api/build-logger.md).
+51
View File
@@ -0,0 +1,51 @@
# File Output And Rotation
File output is for native applications that need local persistence. Treat it as a native-only capability and keep a console fallback for portable code.
## Check The Capability
```moonbit
if !@log.native_files_supported() {
println("file logging is unavailable on this target")
return
}
```
## Build A Rotating File Logger
```moonbit
let config = @log.with_file_rotation(
@log.file(
"service.log",
min_level=@log.Level::Info,
target="service.file",
auto_flush=true,
) catch {
err => {
ignore(err)
return
}
},
1024 * 1024,
max_backups=3,
)
let logger = @log.build_logger(config)
logger.info("file logger ready")
ignore(logger.flush())
ignore(logger.file_close())
```
The rotation limit is the size of the active file in bytes. `max_backups=3` retains the rotated backup chain. Always close a file-backed logger during orderly application shutdown.
## Run The Repository Example
```bash
moon run examples/file_rotation --target native
```
## Next Steps
- Add a bounded queue with [Queueing](../extend/queue.md) when bursts should not write synchronously.
- Read [Target boundaries](../extend/targets.md) before sharing the same logging setup with web targets.
- Reference: [`file(...)`](../api/file.md), [`with_file_rotation(...)`](../api/with-file-rotation.md), and [`file_close(...)`](../api/configured-logger-file-close.md).
+24
View File
@@ -0,0 +1,24 @@
# Examples
Examples are the primary BitLogger learning path. Each page starts from an application concern, shows the complete minimum code, and then points to the API reference for exact contracts.
## Choose A Flow
| Need | Start here | Then extend with |
| --- | --- | --- |
| Print structured logs to a terminal | [Console and fields](./console.md) | [Text formatting](../extend/formatting.md) |
| Keep local logs with bounded disk use | [File rotation](./file-rotation.md) | [Target boundaries](../extend/targets.md) |
| Build logging from app configuration | [Configuration](./config.md) | [Queueing](../extend/queue.md) |
| Avoid blocking a native application on writes | [Async lifecycle](./async.md) | [Sink composition](../extend/composition.md) |
## Repository Examples
The runnable sources live under `examples/`. The guides below explain their intended use instead of duplicating every API detail:
- `examples/console_basic` for terminal and JSON records
- `examples/config_build` for parsed configuration
- `examples/file_rotation` for native file output
- `examples/async_basic` for the async lifecycle
- `examples/presets`, `examples/text_formatter`, and `examples/style_tags` for follow-on customization
Use the [API reference](../api/index.md) when a guide links to a symbol and you need its full signature or edge-case behavior.
+42
View File
@@ -0,0 +1,42 @@
# Sink Composition
Presets cover common console and file configurations. Build a `Logger::new(...)` directly when records need routing or multiple outputs.
## Send A Record To Two Destinations
```moonbit
let logger = @log.Logger::new(
@log.fanout_sink(@log.console_sink(), @log.json_console_sink()),
min_level=@log.Level::Info,
target="service",
)
logger.info("request complete", fields=[@log.field("status", "200")])
```
`fanout_sink(...)` writes the same record to every child sink. This keeps human-oriented terminal output and machine-oriented JSON output in one application path.
## Route Warnings Separately
```moonbit
let logger = @log.Logger::new(
@log.split_by_level(
@log.callback_sink(fn(rec) {
println("alert: \{rec.message}")
}),
@log.console_sink(),
min_level=@log.Level::Warn,
),
min_level=@log.Level::Trace,
target="service",
)
```
Use direct sink composition only when the routing behavior is part of the application design. A preset remains easier to audit when one sink is sufficient.
## API Reference
- [`Logger::new(...)`](../api/logger-new.md)
- [`fanout_sink(...)`](../api/fanout-sink.md)
- [`split_by_level(...)`](../api/split-by-level.md)
- [`callback_sink(...)`](../api/callback-sink.md)
+44
View File
@@ -0,0 +1,44 @@
# Text Formatting And Style Tags
Formatting changes presentation while keeping the record's level, target, message, and fields intact. Use text output for people; use JSON output when another system parses the record.
## Customize The Text Shape
```moonbit
let logger = @log.build_logger(
@log.text_console(
min_level=@log.Level::Info,
target="service",
text_formatter=@log.TextFormatterConfig::new(
show_timestamp=false,
field_separator=",",
template="[{level}] {target} {message} :: {fields}",
),
),
)
logger.info("ready", fields=[@log.field("port", "8080")])
```
The template controls the visible order. Keep fields separate from the message so they can still be rendered consistently across sinks.
## Add Named Styles
```moonbit
let formatter = @log.text_formatter(
show_timestamp=false,
color_mode=@log.ColorMode::Always,
).with_style_tags(
@log.default_style_tag_registry()
.set_tag("accent", fg=Some("#4cc9f0"), bold=true),
)
```
Style tags are terminal presentation metadata. Do not use them as the only way to encode operational meaning; preserve that meaning in level, target, and fields.
## API Reference
- [`text_console(...)`](../api/text-console.md)
- [`TextFormatterConfig`](../api/text-formatter-config.md)
- [`text_formatter(...)`](../api/text-formatter.md)
- [`ColorMode`](../api/color-mode.md)
+15
View File
@@ -0,0 +1,15 @@
# Extend A Logger
Start from an [Example flow](../examples/index.md), then add one extension at a time. These pages explain when an abstraction is useful and which operational boundary it introduces.
## Extension Map
| Need | Extension | Main trade-off |
| --- | --- | --- |
| Absorb short output bursts | [Queueing](./queue.md) | You must choose overflow and flush behavior. |
| Send records to multiple destinations or route by level | [Sink composition](./composition.md) | More explicit construction than presets. |
| Control terminal appearance | [Text formatting](./formatting.md) | Formatting affects presentation, not record structure. |
| Share code across native and web targets | [Target boundaries](./targets.md) | File and async runtime behavior differ by backend. |
| Correlate logs with distributed requests | [Trace context](./observability.md) | Propagation and export stay application-owned. |
For exact function signatures, follow each page's links into the [API reference](../api/index.md).
+18
View File
@@ -0,0 +1,18 @@
# Trace Context
BitLogger keeps trace interoperability in its structured record model rather than owning HTTP propagation or a telemetry transport. Use the tracing library already selected by the application to extract a request context, then bind its identifiers to the logger.
```moonbit
let request_logger = logger.with_trace_context(
@log.trace_context(
trace_id,
span_id,
trace_flags="01",
trace_state=trace_state,
),
)
```
Every record from `request_logger` receives `trace_id`, `span_id`, `trace_flags`, and, when supplied, `trace_state`. The base logger is unchanged, so a child request cannot leak identifiers into unrelated work.
Use `context.as_fields()` for a custom logger facade. BitLogger intentionally does not parse `traceparent` headers, create spans, make sampling decisions, or export OTLP data; those concerns remain with the application tracing implementation.
+30
View File
@@ -0,0 +1,30 @@
# Queueing And Overflow
Use a queue when a short burst of log writes should not immediately reach the output sink. Queueing is explicit because loss behavior is an application decision.
## Add A Synchronous Queue
```moonbit
let config = @log.with_queue(
@log.text_console(target="service"),
max_pending=128,
overflow=@log.QueueOverflowPolicy::DropOldest,
)
let logger = @log.build_logger(config)
logger.info("queued record")
ignore(logger.flush())
```
`DropOldest` preserves the newest operational information when the queue is full. Use `DropNewest` when preserving earlier records matters more. Calling `flush()` at a shutdown boundary makes the pending-record policy visible in application code.
## When To Use Async Instead
The synchronous queue is configuration around a normal runtime logger. When a native async application needs a worker lifecycle and batching, follow [Async logger lifecycle](../examples/async.md) instead.
## API Reference
- [`with_queue(...)`](../api/with-queue.md)
- [`QueueConfig`](../api/queue-config.md)
- [`QueueOverflowPolicy`](../api/queue-overflow-policy.md)
- [`flush()`](../api/configured-logger-flush.md)
+33
View File
@@ -0,0 +1,33 @@
# Target Boundaries
BitLogger keeps a portable structured logging surface, but native file output and async worker behavior are target-sensitive. Keep those boundaries in application code instead of assuming every sink behaves identically everywhere.
## Portable Default
`console(...)`, `json_console(...)`, structured fields, filters, patches, and the ordinary logger surface are the appropriate starting point for code shared across targets.
## Native-Only File Output
```moonbit
if @log.native_files_supported() {
let logger = @log.build_logger(@log.file("service.log") catch {
err => {
ignore(err)
return
}
})
logger.info("file output enabled")
}
```
File sinks need native filesystem support. Do not construct a file-only configuration unconditionally in code intended for web targets.
## Async Library Versus Async Entry Point
The async library exposes a compatibility surface across declared targets, while an executable `async fn main` still has stricter entry-point support. Keep a native-only executable example separate from portable library code.
## API Reference
- [`native_files_supported()`](../api/native-files-supported.md)
- [Target verification](../api/target-verification.md)
- [Async logger lifecycle](../examples/async.md)
+14 -16
View File
@@ -4,31 +4,29 @@ layout: home
hero: hero:
name: BitLogger name: BitLogger
text: Structured logging for MoonBit text: Structured logging for MoonBit
tagline: API reference, examples, and release notes for BitLogger. tagline: Start with a runnable logging flow, then extend it with precise API reference.
actions: actions:
- theme: brand - theme: brand
text: Open API Reference text: Start Examples
link: /api/ link: /examples/
- theme: alt - theme: alt
text: Read Release Notes text: Extend A Logger
link: /changes/ link: /extend/
- theme: alt - theme: alt
text: Mooncake Package text: Mooncake Package
link: https://mooncakes.io/docs/Nanaloveyuki/BitLogger link: https://mooncakes.io/docs/Nanaloveyuki/BitLogger
features: features:
- title: Structured logging - title: Examples first
details: Levels, targets, message fields, and configurable text or JSON output. details: Follow complete console, file, config, and async flows before looking up individual symbols.
- title: Sync and async paths - title: Extend deliberately
details: API reference covers both the main package and the async package surface. details: Add queues, custom sink graphs, formatting, and target-specific behavior only when the base flow needs them.
- title: Config and runtime helpers - title: API as reference
details: Browse builders, presets, file helpers, queue helpers, and verification notes quickly. details: Use the one-symbol-per-page API reference for exact signatures, contracts, and edge cases.
--- ---
## Start Here ## Start Here
<HomeDocHub /> 1. Read [Examples](./examples/index.md) for an end-to-end path from installation to a running logger.
2. Read [Extend](./extend/index.md) when the base console flow needs queueing, custom routing, formatting, files, or target-aware behavior.
- Use [API Reference](./api/index.md) when you want the canonical one-file-per-public-API docs. 3. Use [API Reference](./api/index.md) for exact contracts and [Release Notes](./changes/index.md) for version history.
- Use [Release Notes](./changes/index.md) when you want version-scoped changes and publish-facing summaries.
- Use [Chinese README](https://github.com/Nanaloveyuki/BitLogger/blob/main/README.md) or [English README](./README-en.md) when you want a shorter project overview first.
+50
View File
@@ -0,0 +1,50 @@
# 异步生命周期 API
异步 API 从 combined config 构建 Logger,再以单 worker 生命周期处理待写入记录。可执行 `async fn main` 仍应按目标平台边界使用。
## 解析与构建
```moonbit
pub fn parse_async_logger_build_config_text(
input : String,
) -> AsyncLoggerBuildConfig raise
pub fn build_async_logger(
config : AsyncLoggerBuildConfig,
) -> AsyncLogger[RuntimeSink]
```
解析函数同时验证 `logger``async_config` 两部分;缺少任一部分时使用对应默认配置。构建函数先复用同步 `build_logger(config.logger)` 路径,再包裹异步层,因此保留同步 sink、文件与可选同步队列的运行时语义。
```moonbit
let config = @async_log.parse_async_logger_build_config_text(raw) catch {
err => { ignore(err); return }
}
let logger = @async_log.build_async_logger(config)
```
## `run()`
```moonbit
pub async fn AsyncLogger::run(self : AsyncLogger[S]) -> Unit
```
启动 drain worker。单个 Logger 只能同时拥有一个 worker;已经运行时再次调用会失败,关闭后的 Logger 也不能重新启动。worker 正常在队列关闭时退出;worker 失败时会记录状态并将错误向上传递。
## `shutdown()`
```moonbit
pub async fn AsyncLogger::shutdown(
self : AsyncLogger[S],
clear? : Bool = false,
) -> Unit
```
`clear=false` 先等待 idle 再关闭;`clear=true` 立即关闭并放弃待处理记录。两条路径都会等待活动 worker 退出。没有运行 worker 且仍有待处理记录时,`clear=false` 可能无法推进,因此应用应保证先启动 `run()`
## 英文原始 API
- [`parse_async_logger_build_config_text(...)`](../../api/parse-async-logger-build-config-text.md)
- [`build_async_logger(...)`](../../api/build-async-logger.md)
- [`run()`](../../api/async-logger-run.md)
- [`shutdown()`](../../api/async-logger-shutdown.md)
+55
View File
@@ -0,0 +1,55 @@
# 基础记录 API
控制台示例依赖以下四个入口:先用 preset 描述输出,再由 `build_logger(...)` 构建运行时 Logger,最后用 `field(...)` 附加结构化上下文。
## `console(...)` 与 `json_console(...)`
```moonbit
pub fn console(
min_level~ : Level = Level::Info,
target~ : String = "",
timestamp~ : Bool = false,
) -> LoggerConfig
pub fn json_console(
min_level~ : Level = Level::Info,
target~ : String = "",
timestamp~ : Bool = false,
) -> LoggerConfig
```
两者都只生成 `LoggerConfig`,不会直接创建运行时 Logger。`console(...)` 适合人读输出,`json_console(...)` 适合按行采集的机器读取输出;默认都不带队列。
## `field(...)`
```moonbit
pub fn field(key : String, value : String) -> Field
```
创建一个结构化字段。它保留原始 key/value,不做去重、归一化或校验;应使用稳定字段名,便于下游筛选。
```moonbit
logger.info("accepted", fields=[@log.field("user", "alice")])
```
## `build_logger(...)`
```moonbit
pub fn build_logger(config : LoggerConfig) -> ConfiguredLogger
```
这是同步 config 到运行时的桥梁:它先按 `config.sink` 构建 `RuntimeSink`,再应用可选的 `config.queue`。返回值仍保留普通日志方法,以及适用时的队列和文件控制方法。
```moonbit
let logger = @log.build_logger(
@log.console(min_level=@log.Level::Info, target="service"),
)
logger.info("ready", fields=[@log.field("port", "8080")])
```
## 英文原始 API
- [`console(...)`](../../api/console.md)
- [`json_console(...)`](../../api/json-console.md)
- [`field(...)`](../../api/field.md)
- [`build_logger(...)`](../../api/build-logger.md)
+52
View File
@@ -0,0 +1,52 @@
# Sink 组合 API
当预设输出不足以表达路由规则时,使用 typed `Logger::new(...)` 和 sink 组合函数。它们接收完整 `Record`,因此仍保留 level、target、message 与 fields。
## `Logger::new(...)`
```moonbit
pub fn Logger::new(
sink : S,
min_level~ : Level = Level::Info,
target~ : String = "",
) -> Logger[S]
```
从任意 `Sink` 实现创建 typed Logger。`min_level` 在任何 sink 写入前生效;`target` 是默认值,之后可由 `with_target(...)``child(...)` 或单次 `log(..., target=...)` 覆盖。
## `fanout_sink(...)`
```moonbit
pub fn fanout_sink(left : A, right : B) -> FanoutSink[A, B]
```
每条记录写入两个目的地,适合控制台加 JSON,或控制台加 callback。右侧获得独立记录副本,两个写入在记录值层面互不影响。
## `split_by_level(...)`
```moonbit
pub fn split_by_level(
left : A,
right : B,
min_level~ : Level = Level::Warn,
) -> SplitSink[A, B]
```
达到阈值的记录进入 `left`,低于阈值的记录进入 `right`。典型用法是把 warning/error 单独输出,而 info/debug 留在普通控制台。
## `callback_sink(...)`
```moonbit
pub fn callback_sink(
callback : (Record) -> Unit,
) -> CallbackSink
```
将完整结构化 `Record` 交给应用回调。适合测试、自定义桥接与集成;它不自动格式化记录。
## 英文原始 API
- [`Logger::new(...)`](../../api/logger-new.md)
- [`fanout_sink(...)`](../../api/fanout-sink.md)
- [`split_by_level(...)`](../../api/split-by-level.md)
- [`callback_sink(...)`](../../api/callback-sink.md)
+53
View File
@@ -0,0 +1,53 @@
# 配置与队列 API
这一组 API 将外部 JSON 或 typed config 变成同步 Logger,并明确规定满队列和关闭时的处理方式。
## `parse_logger_config_text(...)`
```moonbit
pub fn parse_logger_config_text(input : String) -> LoggerConfig raise ConfigError
```
解析并验证 JSON,但不构建运行时 Logger。缺省字段使用构造器默认值;无效 JSON、错误类型、未知枚举值或空文件路径都会抛出 `ConfigError`
```moonbit
let config = @log.parse_logger_config_text(raw) catch {
err => { ignore(err); return }
}
let logger = @log.build_logger(config)
```
## `with_queue(...)`、`QueueConfig` 与溢出策略
```moonbit
pub fn with_queue(
config : LoggerConfig,
max_pending~ : Int = 0,
overflow~ : QueueOverflowPolicy = QueueOverflowPolicy::DropNewest,
) -> LoggerConfig
pub fn QueueConfig::new(
max_pending : Int,
overflow~ : QueueOverflowPolicy = QueueOverflowPolicy::DropNewest,
) -> QueueConfig
```
`with_queue(...)` 是 preset 组合入口;`QueueConfig::new(...)` 用于手写 `LoggerConfig`。两者配置同步 `QueuedSink`,不是异步包的 worker 队列。
`DropNewest` 丢弃新到记录,`DropOldest` 丢弃最早待处理记录。选择取决于业务更需要保留早期还是最新状态。
## `flush()`
```moonbit
pub fn ConfiguredLogger::flush(self : ConfiguredLogger) -> Bool
```
在退出或需要确认输出边界时调用。对于队列和文件 sink,它会走对应运行时 flush 路径;返回值表示该路径是否成功。
## 英文原始 API
- [`parse_logger_config_text(...)`](../../api/parse-logger-config-text.md)
- [`with_queue(...)`](../../api/with-queue.md)
- [`QueueConfig`](../../api/queue-config.md)
- [`QueueOverflowPolicy`](../../api/queue-overflow-policy.md)
- [`flush()`](../../api/configured-logger-flush.md)
+49
View File
@@ -0,0 +1,49 @@
# 文件输出 API
文件输出是 native 能力。先检查能力,再创建文件配置,最后在有序退出时 flush 并关闭。
## `native_files_supported()`
```moonbit
pub fn native_files_supported() -> Bool
```
native 后端当前返回 `true`,非文件能力后端返回 `false`。这是运行时能力检查,不保证后续文件操作一定成功,路径、权限和文件系统状态仍可能导致失败。
## `file(...)` 与 `with_file_rotation(...)`
```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
pub fn with_file_rotation(
config : LoggerConfig,
max_bytes : Int,
max_backups~ : Int = 1,
) -> LoggerConfig
```
`file(...)` 要求非空路径。`with_file_rotation(...)` 只对 file config 生效;输入不是文件 sink 时原样返回。`max_bytes` 是活动文件上限,`max_backups` 是保留的轮转备份数。
## `file_close()`
```moonbit
pub fn ConfiguredLogger::file_close(self : ConfiguredLogger) -> Bool
```
关闭 config 构建的文件 sink。队列文件 sink 会依次 drain、flush、close;只有整条路径成功且期间没有新的写入、flush 或轮转失败时才返回 `true`。非文件 sink 返回 `false`
## 英文原始 API
- [`native_files_supported()`](../../api/native-files-supported.md)
- [`file(...)`](../../api/file.md)
- [`with_file_rotation(...)`](../../api/with-file-rotation.md)
- [`file_close()`](../../api/configured-logger-file-close.md)
+40
View File
@@ -0,0 +1,40 @@
# 文本格式 API
文本格式只改变呈现,不改变 `Record` 的结构。需要稳定机器消费时,优先使用 `json_console(...)`
## `text_console(...)`
```moonbit
pub fn text_console(
min_level~ : Level = Level::Info,
target~ : String = "",
timestamp~ : Bool = false,
text_formatter~ : TextFormatterConfig = default_text_formatter_config(),
) -> LoggerConfig
```
生成 `SinkKind::TextConsole` 配置,并复制 formatter 配置。和其他 preset 一样,它不直接构建 Logger,也默认不带队列。
## `TextFormatterConfig` 与 `text_formatter(...)`
`TextFormatterConfig::new(...)` 用于 config-first 路径,控制 timestamp、level、target、fields、分隔符与 template。`text_formatter(...)` 构建直接传给 `text_console_sink(...)` 的运行时 formatter。
```moonbit
let config = @log.text_console(
text_formatter=@log.TextFormatterConfig::new(
show_timestamp=false,
template="[{level}] {target} {message} :: {fields}",
),
)
```
## `ColorMode`
`ColorMode` 控制文本 formatter 的颜色策略。终端样式只服务可读性;业务语义仍应放在 level、target 和 fields 中。
## 英文原始 API
- [`text_console(...)`](../../api/text-console.md)
- [`TextFormatterConfig`](../../api/text-formatter-config.md)
- [`text_formatter(...)`](../../api/text-formatter.md)
- [`ColorMode`](../../api/color-mode.md)
+14
View File
@@ -0,0 +1,14 @@
# 高频 API
这一层只覆盖 Examples 和 Extend 中直接使用、或应用启动时高频需要的 API。它按使用主题归纳,避免把阅读路径重新拆回一函数一页。
| 场景 | 中文说明 | 英文精确参考 |
| --- | --- | --- |
| 第一条结构化日志 | [基础记录](./basics.md) | [完整 API 索引](../../api/index.md) |
| 配置、队列与 flush | [配置与队列](./configuration.md) | [config 分类](../../api/index.md) |
| native 文件输出 | [文件输出](./file-output.md) | [file/sink 分类](../../api/index.md) |
| 终端可读性 | [文本格式](./formatting.md) | [formatter 分类](../../api/index.md) |
| 后台异步处理 | [异步生命周期](./async.md) | [async 分类](../../api/index.md) |
| 多目的地与按 level 路由 | [Sink 组合](./composition.md) | [sink 分类](../../api/index.md) |
每个中文页面保留高频签名与行为边界;需要全部参数、错误条件或较少使用的公开符号时,沿页面内链接进入英文原始 API。
+47
View File
@@ -0,0 +1,47 @@
# 异步日志生命周期
当 native 异步应用需要通过异步队列处理日志写入时,使用异步包。库声明的可用目标比可执行 `async fn main` 示例更广,仓库示例仍以 native 为主。
## 导入两个包
```moonbit
import {
"Nanaloveyuki/BitLogger/src" @log,
"Nanaloveyuki/BitLogger/src-async" @async_log,
"moonbitlang/async",
}
```
## 构建、运行、关闭
```moonbit
async fn main {
let raw = "{\"logger\":{\"min_level\":\"info\",\"target\":\"service.async\",\"sink\":{\"kind\":\"text_console\"}},\"async_config\":{\"max_pending\":64,\"overflow\":\"DropOldest\",\"max_batch\":16,\"linger_ms\":5,\"flush\":\"Batch\"}}"
let config = @async_log.parse_async_logger_build_config_text(raw) catch {
err => {
ignore(err)
return
}
}
let logger = @async_log.build_async_logger(config)
@async.with_task_group(group => {
group.spawn_bg(allow_failure=true, () => logger.run())
logger.info("worker started", fields=[@log.field("mode", "async")])
logger.shutdown()
})
}
```
`run()` 持有 worker 循环。先启动它再记录日志,并调用 `shutdown()`,使待处理记录按配置的 flush 策略结束。
## 运行仓库示例
```bash
moon run examples/async_basic --target native
```
## 下一步
- 查看[队列](../extend/queue.md)中的溢出与 flush 选择。
- 阅读[异步生命周期 API](../api/async.md);完整契约仍可查询[英文 API 索引](../../api/index.md)。
+35
View File
@@ -0,0 +1,35 @@
# 配置驱动构建
当 level、目标名、输出方式或队列策略应由部署配置决定时,使用这一流程。
## 解析、构建、记录
```moonbit
let raw = "{\"min_level\":\"info\",\"target\":\"service\",\"sink\":{\"kind\":\"text_console\",\"text_formatter\":{\"show_timestamp\":false,\"separator\":\" | \"}},\"queue\":{\"max_pending\":64,\"overflow\":\"DropOldest\"}}"
let config = @log.parse_logger_config_text(raw) catch {
err => {
ignore(err)
println("invalid logger configuration")
return
}
}
let logger = @log.build_logger(config)
logger.info("configured logger ready")
ignore(logger.flush())
```
解析先验证数据,再构造运行时 Logger。真实部署应将原始配置放在应用代码之外,并在配置进入进程的边界处理错误。
## 运行仓库示例
```bash
moon run examples/config_build
```
## 下一步
- 需要明确选择满队列时的行为,阅读[队列](../extend/queue.md)。
- 需要后台 worker 生命周期时,阅读[异步日志生命周期](./async.md)。
- 阅读[配置与队列 API](../api/configuration.md);完整契约仍可查询[英文 API 索引](../../api/index.md)。
+55
View File
@@ -0,0 +1,55 @@
# 控制台与结构化字段
命令行工具和服务都可以从这一流程开始。一条记录由 level、target、message 与可选 fields 构成。
## 安装与导入
```bash
moon new log-demo
cd log-demo
moon add Nanaloveyuki/BitLogger@0.7.3
```
在应用的 `moon.pkg` 中加入:
```moonbit
import {
"Nanaloveyuki/BitLogger/src" @log,
}
```
## 构建控制台 Logger
```moonbit
fn main {
let logger = @log.build_logger(
@log.console(min_level=@log.Level::Info, target="service.http"),
)
logger.info("listening", fields=[
@log.field("port", "8080"),
@log.field("environment", "development"),
])
logger.warn("slow request", fields=[@log.field("path", "/health")])
}
```
`min_level` 在记录到达 sink 前过滤日志;`target` 用于识别子系统;fields 用于携带可查询上下文,而不是拼接到 message 中。
## 切换为 JSON
保持日志调用不变,只替换配置:
```moonbit
let logger = @log.build_logger(
@log.json_console(min_level=@log.Level::Info, target="service.http"),
)
```
当日志采集器按行处理结构化记录时,JSON 输出更合适。
## 下一步
- 需要自定义终端输出时,阅读[文本格式](../extend/formatting.md)。
- 需要由 JSON 配置构建时,阅读[配置构建](./config.md)。
- 阅读[基础记录 API](../api/basics.md);完整契约仍可查询[英文 API 索引](../../api/index.md)。
+51
View File
@@ -0,0 +1,51 @@
# 文件输出与轮转
文件输出面向需要本地持久化的 native 应用。跨端代码应先判断能力,并保留控制台回退方案。
## 检查能力
```moonbit
if !@log.native_files_supported() {
println("file logging is unavailable on this target")
return
}
```
## 构建可轮转文件 Logger
```moonbit
let config = @log.with_file_rotation(
@log.file(
"service.log",
min_level=@log.Level::Info,
target="service.file",
auto_flush=true,
) catch {
err => {
ignore(err)
return
}
},
1024 * 1024,
max_backups=3,
)
let logger = @log.build_logger(config)
logger.info("file logger ready")
ignore(logger.flush())
ignore(logger.file_close())
```
轮转阈值是活动文件的字节数,`max_backups=3` 保留备份链。应用有序退出时必须关闭文件 Logger。
## 运行仓库示例
```bash
moon run examples/file_rotation --target native
```
## 下一步
- 写入突发较多时,加入[队列](../extend/queue.md)。
- 与 web 目标共享代码前,阅读[目标平台边界](../extend/targets.md)。
- 阅读[文件输出 API](../api/file-output.md);完整契约仍可查询[英文 API 索引](../../api/index.md)。
+24
View File
@@ -0,0 +1,24 @@
# 示例
示例是 BitLogger 的主学习路径。每页从一个应用需求开始,给出完整最小代码,再链接到英文 API 参考查看精确契约。
## 选择流程
| 需求 | 从这里开始 | 后续扩展 |
| --- | --- | --- |
| 在终端输出结构化日志 | [控制台与字段](./console.md) | [文本格式](../extend/formatting.md) |
| 保存本地日志并限制磁盘占用 | [文件轮转](./file-rotation.md) | [目标平台边界](../extend/targets.md) |
| 由应用配置决定日志行为 | [配置构建](./config.md) | [队列](../extend/queue.md) |
| 在 native 异步应用中处理日志写入 | [异步生命周期](./async.md) | [Sink 组合](../extend/composition.md) |
## 可运行源码
仓库中的 `examples/` 是可执行参考;本目录说明何时选择它们、如何运行,以及下一步应该扩展什么:
- `examples/console_basic`
- `examples/config_build`
- `examples/file_rotation`
- `examples/async_basic`
- `examples/presets``examples/text_formatter``examples/style_tags`
遇到函数签名、错误语义或边界条件时,请查询[英文 API 参考](../../api/index.md)。
+39
View File
@@ -0,0 +1,39 @@
# Sink 组合
Preset 覆盖常见的控制台和文件配置。只有当记录需要路由或多个输出目的地时,才直接构造 `Logger::new(...)`
## 同时发送到两个目的地
```moonbit
let logger = @log.Logger::new(
@log.fanout_sink(@log.console_sink(), @log.json_console_sink()),
min_level=@log.Level::Info,
target="service",
)
logger.info("request complete", fields=[@log.field("status", "200")])
```
`fanout_sink(...)` 将同一记录写入每个子 Sink,因此应用可以同时保留面向人的控制台输出和面向采集器的 JSON 输出。
## 单独处理警告以上日志
```moonbit
let logger = @log.Logger::new(
@log.split_by_level(
@log.callback_sink(fn(rec) {
println("alert: \{rec.message}")
}),
@log.console_sink(),
min_level=@log.Level::Warn,
),
min_level=@log.Level::Trace,
target="service",
)
```
只有当路由规则属于应用设计时才使用直接 Sink 组合;单一输出时,preset 更容易审查和维护。
## API
阅读[Sink 组合 API](../api/composition.md);完整契约仍可查询[英文 API 索引](../../api/index.md)。
+41
View File
@@ -0,0 +1,41 @@
# 文本格式与样式
格式化只改变呈现,记录本身的 level、target、message 和 fields 不变。面向人时使用文本输出;由其他系统解析时使用 JSON。
## 自定义文本形状
```moonbit
let logger = @log.build_logger(
@log.text_console(
min_level=@log.Level::Info,
target="service",
text_formatter=@log.TextFormatterConfig::new(
show_timestamp=false,
field_separator=",",
template="[{level}] {target} {message} :: {fields}",
),
),
)
logger.info("ready", fields=[@log.field("port", "8080")])
```
模板控制可见顺序。fields 应保持独立于 message,以便不同 Sink 一致渲染。
## 添加命名样式
```moonbit
let formatter = @log.text_formatter(
show_timestamp=false,
color_mode=@log.ColorMode::Always,
).with_style_tags(
@log.default_style_tag_registry()
.set_tag("accent", fg=Some("#4cc9f0"), bold=true),
)
```
样式标签是终端呈现元数据,不应作为唯一的业务或运维语义;语义仍应保存在 level、target 与 fields 中。
## API
阅读[文本格式 API](../api/formatting.md);完整契约仍可查询[英文 API 索引](../../api/index.md)。
+15
View File
@@ -0,0 +1,15 @@
# 扩展 Logger
先完成一个[示例流程](../examples/index.md),再一次只加入一种扩展能力。这些页面说明某个抽象何时有用,以及它会引入什么运行时边界。
## 扩展地图
| 需求 | 扩展 | 主要取舍 |
| --- | --- | --- |
| 吸收短时输出突发 | [队列](./queue.md) | 必须选择溢出与 flush 行为。 |
| 同时发送到多个位置或按 level 路由 | [Sink 组合](./composition.md) | 比 preset 更需要显式构造。 |
| 调整终端可读性 | [文本格式](./formatting.md) | 格式只改变呈现,不改变记录结构。 |
| 在 native 与 web 目标之间共享代码 | [目标平台边界](./targets.md) | 文件与异步运行时行为因后端而异。 |
| 将日志关联到分布式请求 | [Trace context](./observability.md) | 传播与导出仍由应用负责。 |
需要精确函数签名时,沿页面链接进入[英文 API 参考](../../api/index.md)。
+13
View File
@@ -0,0 +1,13 @@
# Trace Context
BitLogger 通过结构化字段承载 trace 上下文,不负责 HTTP 传播或遥测传输。应用应继续使用已选定的 tracing 库提取请求上下文,再把标识绑定到 logger。
```moonbit
let request_logger = logger.with_trace_context(
@log.trace_context(trace_id, span_id, trace_flags="01"),
)
```
派生 logger 的每条记录都会带有 `trace_id``span_id``trace_flags`,以及传入时的 `trace_state`。原 logger 不会被修改,避免请求上下文泄漏到无关任务。
`context.as_fields()` 可用于自定义 facade。`traceparent` 解析、span 创建、采样与 OTLP 导出仍由应用选用的 tracing 实现负责。
+27
View File
@@ -0,0 +1,27 @@
# 队列与溢出策略
当短时日志突发不应立即写入输出 Sink 时使用队列。队列是显式能力,因为丢弃策略必须由应用决定。
## 加入同步队列
```moonbit
let config = @log.with_queue(
@log.text_console(target="service"),
max_pending=128,
overflow=@log.QueueOverflowPolicy::DropOldest,
)
let logger = @log.build_logger(config)
logger.info("queued record")
ignore(logger.flush())
```
`DropOldest` 在队列已满时保留最新运行信息。若更重视先到的记录,可选择 `DropNewest`。在退出边界调用 `flush()`,让待处理记录策略在应用代码中清晰可见。
## 何时改用异步 Logger
同步队列只是普通运行时 Logger 的一层配置。当 native 异步应用需要 worker 生命周期和批处理时,转到[异步日志生命周期](../examples/async.md)。
## API
阅读[配置与队列 API](../api/configuration.md);完整契约仍可查询[英文 API 索引](../../api/index.md)。
+31
View File
@@ -0,0 +1,31 @@
# 目标平台边界
BitLogger 保持可移植的结构化日志表面,但 native 文件输出与异步 worker 行为依赖目标平台。应把这些边界写在应用代码中,而不是假设每种 Sink 在所有后端都相同。
## 可移植默认值
`console(...)``json_console(...)`、结构化 fields、filter、patch 与普通 Logger 表面适合作为跨端共享代码的起点。
## 仅 native 的文件输出
```moonbit
if @log.native_files_supported() {
let logger = @log.build_logger(@log.file("service.log") catch {
err => {
ignore(err)
return
}
})
logger.info("file output enabled")
}
```
文件 Sink 需要 native 文件系统支持。面向 web 目标的代码不应无条件构造只包含文件输出的配置。
## 异步库与异步入口
异步库在声明目标上提供兼容表面,但可执行 `async fn main` 的入口支持更严格。请把 native-only 可执行示例与可移植库代码分开。
## API
阅读[文件输出 API](../api/file-output.md)和[异步生命周期](../examples/async.md)。完整目标验证记录仍在[英文 API](../../api/target-verification.md)。
+32
View File
@@ -0,0 +1,32 @@
---
layout: home
hero:
name: BitLogger
text: MoonBit 结构化日志库
tagline: 先完成一条可运行的日志流程,再按需扩展并查询 API。
actions:
- theme: brand
text: 从示例开始
link: /zh/examples/
- theme: alt
text: 扩展 Logger
link: /zh/extend/
- theme: alt
text: API 参考(英文)
link: /api/
features:
- title: 示例优先
details: 从控制台、文件、配置和异步四条完整流程开始,而不是从 API 文件名开始。
- title: 按需扩展
details: 只在基础流程需要时再加入队列、组合 Sink、格式化和跨端处理。
- title: API 作为参考
details: 英文 API 页面保留精确签名、契约与边界说明。
---
## 从这里开始
1. 阅读[示例](./examples/index.md),完成从安装到运行 Logger 的全流程。
2. 当基础控制台流程需要更多能力时,阅读[扩展](./extend/index.md)。
3. 需要高频签名和边界时,查询[中文高频 API](./api/index.md);较少使用的符号仍进入[英文完整 API 参考](../api/index.md)。
+1 -3
View File
@@ -6,6 +6,4 @@ import {
supported_targets = "+native" supported_targets = "+native"
options( pkgtype(kind: "executable")
is_main: true,
)
+1 -3
View File
@@ -2,6 +2,4 @@ import {
"Nanaloveyuki/BitLogger/src" @lib, "Nanaloveyuki/BitLogger/src" @lib,
} }
options( pkgtype(kind: "executable")
"is-main": true,
)
+1 -3
View File
@@ -2,6 +2,4 @@ import {
"Nanaloveyuki/BitLogger/src" @lib, "Nanaloveyuki/BitLogger/src" @lib,
} }
options( pkgtype(kind: "executable")
"is-main": true,
)
+1 -3
View File
@@ -2,6 +2,4 @@ import {
"Nanaloveyuki/BitLogger/src" @lib, "Nanaloveyuki/BitLogger/src" @lib,
} }
options( pkgtype(kind: "executable")
"is-main": true,
)
+1 -3
View File
@@ -2,6 +2,4 @@ import {
"Nanaloveyuki/BitLogger/src" @lib, "Nanaloveyuki/BitLogger/src" @lib,
} }
options( pkgtype(kind: "executable")
"is-main": true,
)
+1 -3
View File
@@ -2,6 +2,4 @@ import {
"Nanaloveyuki/BitLogger/src" @lib, "Nanaloveyuki/BitLogger/src" @lib,
} }
options( pkgtype(kind: "executable")
"is-main": true,
)
+1 -3
View File
@@ -2,6 +2,4 @@ import {
"Nanaloveyuki/BitLogger/src" @lib, "Nanaloveyuki/BitLogger/src" @lib,
} }
options( pkgtype(kind: "executable")
"is-main": true,
)
+1 -3
View File
@@ -2,6 +2,4 @@ import {
"Nanaloveyuki/BitLogger/src" @lib, "Nanaloveyuki/BitLogger/src" @lib,
} }
options( pkgtype(kind: "executable")
"is-main": true,
)
+1
View File
@@ -0,0 +1 @@
+2 -3
View File
@@ -1,10 +1,9 @@
name = "Nanaloveyuki/BitLogger" name = "Nanaloveyuki/BitLogger"
version = "0.7.0" version = "0.7.3"
import { import {
"maria/json_parser@0.1.1", "moonbitlang/async@0.20.2",
"moonbitlang/async@0.20.0",
} }
readme = "src/README.mbt.md" readme = "src/README.mbt.md"
+184
View File
@@ -0,0 +1,184 @@
<#
.SYNOPSIS
Updates BitLogger's package version, installation snippets, and release-note entry.
.DESCRIPTION
For patch releases, the public release version is inferred from the latest
changes entry. For example, after 1.1.2(0.7.2), running:
.\scripts\update-version.ps1 0.7.3
creates 1.1.3(0.7.3). A major or minor package-version change requires an
explicit -ReleaseVersion so the public release sequence is never guessed.
The generated release note uses supplied -Change, -Verification, and -Note
entries. Without them it contains TODO markers that must be completed before
the release is committed.
#>
[CmdletBinding(SupportsShouldProcess)]
param(
[Parameter(Mandatory, Position = 0)]
[ValidatePattern('^\d+\.\d+\.\d+$')]
[string]$Version,
[ValidatePattern('^\d+\.\d+\.\d+$')]
[string]$ReleaseVersion,
[string[]]$Change,
[string[]]$Verification,
[string[]]$Note
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
function Read-TextFile([string]$Path) {
[System.IO.File]::ReadAllText($Path)
}
function Write-TextFile([string]$Path, [string]$Content) {
$encoding = New-Object System.Text.UTF8Encoding($false)
[System.IO.File]::WriteAllText($Path, $Content, $encoding)
}
function Replace-ExactlyOnce(
[string]$Content,
[string]$Pattern,
[string]$Replacement,
[string]$Path
) {
$matches = [regex]::Matches($Content, $Pattern)
if ($matches.Count -ne 1) {
throw "Expected exactly one match for '$Pattern' in $Path, found $($matches.Count)."
}
[regex]::Replace($Content, $Pattern, $Replacement, 1)
}
function Format-BulletList([string[]]$Items, [string]$Fallback) {
$entries = if ($null -eq $Items -or $Items.Count -eq 0) {
@($Fallback)
} else {
$Items
}
($entries | ForEach-Object { "- $_" }) -join [Environment]::NewLine
}
$repoRoot = Split-Path -Parent $PSScriptRoot
$moonModPath = Join-Path $repoRoot 'moon.mod'
$changesIndexPath = Join-Path $repoRoot 'docs/changes/index.md'
foreach ($path in @($moonModPath, $changesIndexPath)) {
if (-not (Test-Path -LiteralPath $path)) {
throw "Required repository file is missing: $path"
}
}
$moonMod = Read-TextFile $moonModPath
$currentVersionMatch = [regex]::Match($moonMod, '(?m)^version\s*=\s*"(?<version>\d+\.\d+\.\d+)"\s*$')
if (-not $currentVersionMatch.Success) {
throw "Could not read the package version from $moonModPath."
}
$currentVersion = [version]$currentVersionMatch.Groups['version'].Value
$targetVersion = [version]$Version
if ($targetVersion -le $currentVersion) {
throw "Target version $Version must be greater than the current version $currentVersion."
}
$changesIndex = Read-TextFile $changesIndexPath
$latestReleaseMatch = [regex]::Match(
$changesIndex,
'(?m)^- \[(?<release>\d+\.\d+\.\d+)\]\(\./(?<file>\d+\.\d+\.\d+\(\d+\.\d+\.\d+\)\.md)\)\s*$'
)
if (-not $latestReleaseMatch.Success) {
throw "Could not read the latest release entry from $changesIndexPath."
}
$latestReleaseVersion = [version]$latestReleaseMatch.Groups['release'].Value
$latestPackageVersionMatch = [regex]::Match(
$latestReleaseMatch.Groups['file'].Value,
'\((?<version>\d+\.\d+\.\d+)\)\.md$'
)
if (-not $latestPackageVersionMatch.Success -or $latestPackageVersionMatch.Groups['version'].Value -ne $currentVersion.ToString()) {
throw 'The latest changes entry must describe the current moon.mod package version before starting a new release.'
}
if ([string]::IsNullOrWhiteSpace($ReleaseVersion)) {
if ($targetVersion.Major -ne $currentVersion.Major -or $targetVersion.Minor -ne $currentVersion.Minor) {
throw 'A major or minor package-version change requires -ReleaseVersion, for example: -ReleaseVersion 1.2.0.'
}
$ReleaseVersion = "$($latestReleaseVersion.Major).$($latestReleaseVersion.Minor).$($targetVersion.Build)"
}
$releaseFileName = "$ReleaseVersion($Version).md"
$releasePath = Join-Path $repoRoot "docs/changes/$releaseFileName"
if (Test-Path -LiteralPath $releasePath) {
throw "Release note already exists: $releasePath"
}
if ($changesIndex.Contains("./$releaseFileName")) {
throw "Release index already contains: $releaseFileName"
}
$updatedMoonMod = Replace-ExactlyOnce `
$moonMod `
'(?m)^version\s*=\s*"\d+\.\d+\.\d+"\s*$' `
"version = `"$Version`"" `
$moonModPath
$installFiles = @(
'README.md',
'docs/README-en.md',
'docs/examples/console.md',
'docs/zh/examples/console.md'
)
$updatedFiles = @{}
foreach ($relativePath in $installFiles) {
$path = Join-Path $repoRoot $relativePath
if (-not (Test-Path -LiteralPath $path)) {
throw "Required installation example is missing: $path"
}
$content = Read-TextFile $path
$updatedFiles[$path] = Replace-ExactlyOnce `
$content `
'Nanaloveyuki/BitLogger@\d+\.\d+\.\d+' `
"Nanaloveyuki/BitLogger@$Version" `
$path
}
$updatedChangesIndex = $changesIndex.Replace(
$latestReleaseMatch.Value,
"- [$ReleaseVersion](./$releaseFileName)$([Environment]::NewLine)$($latestReleaseMatch.Value)"
)
$releaseNote = @"
## BitLogger Update Changes
version $ReleaseVersion($Version)
### Changes
$(Format-BulletList $Change 'TODO: summarize the user-visible changes in this release.')
### Verification
$(Format-BulletList $Verification 'TODO: record the commands and targets verified for this release.')
### Notes
$(Format-BulletList $Note 'TODO: record release limitations or compatibility notes when applicable.')
"@
$writeTargets = @($moonModPath, $changesIndexPath, $releasePath) + $updatedFiles.Keys
if ($PSCmdlet.ShouldProcess(($writeTargets -join ', '), "update to $Version")) {
Write-TextFile $moonModPath $updatedMoonMod
foreach ($path in $updatedFiles.Keys) {
Write-TextFile $path $updatedFiles[$path]
}
Write-TextFile $changesIndexPath $updatedChangesIndex
Write-TextFile $releasePath $releaseNote
}
if ($WhatIfPreference) {
Write-Output "Previewed release $ReleaseVersion($Version)."
} else {
Write-Output "Prepared release $ReleaseVersion($Version)."
}
Write-Output "Release notes: $releasePath"
+95 -73
View File
@@ -455,6 +455,30 @@ async test "async logger with_context_fields prepends shared fields without muta
inspect(written_fields.val[2].value, content="test") inspect(written_fields.val[2].value, content="test")
} }
///|
test "async facades bind trace context through standard fields" {
let context = @bitlogger.trace_context("trace-async", "span-async")
let base = async_logger(
@bitlogger.callback_sink(fn(_) { }),
config=AsyncLoggerConfig::new(max_pending=4),
min_level=@bitlogger.Level::Info,
)
let derived = base.with_trace_context(context)
inspect(base.context_fields.length(), content="0")
inspect(derived.context_fields.length(), content="3")
inspect(derived.context_fields[0].key, content="trace_id")
inspect(derived.context_fields[0].value, content="trace-async")
inspect(derived.context_fields[1].key, content="span_id")
inspect(derived.context_fields[2].value, content="01")
let library = LibraryAsyncLogger::new(@bitlogger.callback_sink(fn(_) { })).with_trace_context(
context,
)
let full = library.to_async_logger()
inspect(full.context_fields.length(), content="3")
inspect(full.context_fields[1].value, content="span-async")
}
///| ///|
async test "async logger child composes target and preserves other state" { async test "async logger child composes target and preserves other state" {
let written_target : Ref[String] = Ref("") let written_target : Ref[String] = Ref("")
@@ -831,29 +855,29 @@ test "async json helpers export stable structured shapes" {
flush=AsyncFlushPolicy::Batch, flush=AsyncFlushPolicy::Batch,
), ),
) )
let config_obj = config_json.as_object().unwrap() let config_obj = config_json.expect_json_object()
inspect( inspect(
@json_parser.stringify(config_json), config_json.stringify(),
content="{\"max_pending\":8,\"max_batch\":3,\"linger_ms\":25,\"overflow\":\"DropOldest\",\"flush\":\"Batch\"}", content="{\"max_pending\":8,\"max_batch\":3,\"linger_ms\":25,\"overflow\":\"DropOldest\",\"flush\":\"Batch\"}",
) )
inspect( inspect(
config_obj.get("max_pending").unwrap().as_number().unwrap().to_int(), config_obj.get("max_pending").unwrap().expect_json_number().to_int(),
content="8", content="8",
) )
inspect( inspect(
config_obj.get("max_batch").unwrap().as_number().unwrap().to_int(), config_obj.get("max_batch").unwrap().expect_json_number().to_int(),
content="3", content="3",
) )
inspect( inspect(
config_obj.get("linger_ms").unwrap().as_number().unwrap().to_int(), config_obj.get("linger_ms").unwrap().expect_json_number().to_int(),
content="25", content="25",
) )
inspect( inspect(
config_obj.get("overflow").unwrap().as_string().unwrap(), config_obj.get("overflow").unwrap().expect_json_string(),
content="DropOldest", content="DropOldest",
) )
inspect( inspect(
config_obj.get("flush").unwrap().as_string().unwrap(), config_obj.get("flush").unwrap().expect_json_string(),
content="Batch", content="Batch",
) )
@@ -874,85 +898,84 @@ test "async json helpers export stable structured shapes" {
), ),
), ),
) )
let build_obj = build_json.as_object().unwrap() let build_obj = build_json.expect_json_object()
let logger_obj = build_obj.get("logger").unwrap().as_object().unwrap() let logger_obj = build_obj.get("logger").unwrap().expect_json_object()
let sink_obj = logger_obj.get("sink").unwrap().as_object().unwrap() let sink_obj = logger_obj.get("sink").unwrap().expect_json_object()
let formatter_obj = sink_obj let formatter_obj = sink_obj
.get("text_formatter") .get("text_formatter")
.unwrap() .unwrap()
.as_object() .expect_json_object()
.unwrap() let async_obj = build_obj.get("async_config").unwrap().expect_json_object()
let async_obj = build_obj.get("async_config").unwrap().as_object().unwrap()
inspect( inspect(
logger_obj.get("min_level").unwrap().as_string().unwrap(), logger_obj.get("min_level").unwrap().expect_json_string(),
content="WARN", content="WARN",
) )
inspect( inspect(
logger_obj.get("target").unwrap().as_string().unwrap(), logger_obj.get("target").unwrap().expect_json_string(),
content="async.roundtrip", content="async.roundtrip",
) )
inspect( inspect(
logger_obj.get("timestamp").unwrap().as_bool().unwrap(), logger_obj.get("timestamp").unwrap().expect_json_bool(),
content="true", content="true",
) )
inspect(logger_obj.get("queue") is None, content="true") inspect(logger_obj.get("queue") is None, content="true")
inspect( inspect(
sink_obj.get("kind").unwrap().as_string().unwrap(), sink_obj.get("kind").unwrap().expect_json_string(),
content="text_console", content="text_console",
) )
inspect(sink_obj.get("path").unwrap().as_string().unwrap(), content="") inspect(sink_obj.get("path").unwrap().expect_json_string(), content="")
inspect(sink_obj.get("append").unwrap().as_bool().unwrap(), content="true") inspect(sink_obj.get("append").unwrap().expect_json_bool(), content="true")
inspect( inspect(
sink_obj.get("auto_flush").unwrap().as_bool().unwrap(), sink_obj.get("auto_flush").unwrap().expect_json_bool(),
content="true", content="true",
) )
inspect( inspect(
formatter_obj.get("show_timestamp").unwrap().as_bool().unwrap(), formatter_obj.get("show_timestamp").unwrap().expect_json_bool(),
content="true", content="true",
) )
inspect( inspect(
formatter_obj.get("separator").unwrap().as_string().unwrap(), formatter_obj.get("separator").unwrap().expect_json_string(),
content=" ", content=" ",
) )
inspect( inspect(
formatter_obj.get("field_separator").unwrap().as_string().unwrap(), formatter_obj.get("field_separator").unwrap().expect_json_string(),
content=" ", content=" ",
) )
inspect( inspect(
formatter_obj.get("template").unwrap().as_string().unwrap(), formatter_obj.get("template").unwrap().expect_json_string(),
content="", content="",
) )
inspect( inspect(
formatter_obj.get("color_mode").unwrap().as_string().unwrap(), formatter_obj.get("color_mode").unwrap().expect_json_string(),
content="never", content="never",
) )
inspect( inspect(
formatter_obj.get("color_support").unwrap().as_string().unwrap(), formatter_obj.get("color_support").unwrap().expect_json_string(),
content="truecolor", content="truecolor",
) )
inspect( inspect(
formatter_obj.get("style_markup").unwrap().as_string().unwrap(), formatter_obj.get("style_markup").unwrap().expect_json_string(),
content="full", content="full",
) )
inspect( inspect(
async_obj.get("max_pending").unwrap().as_number().unwrap().to_int(), async_obj.get("max_pending").unwrap().expect_json_number().to_int(),
content="2", content="2",
) )
inspect( inspect(
async_obj.get("max_batch").unwrap().as_number().unwrap().to_int(), async_obj.get("max_batch").unwrap().expect_json_number().to_int(),
content="5", content="5",
) )
inspect( inspect(
async_obj.get("linger_ms").unwrap().as_number().unwrap().to_int(), async_obj.get("linger_ms").unwrap().expect_json_number().to_int(),
content="40", content="40",
) )
inspect( inspect(
async_obj.get("overflow").unwrap().as_string().unwrap(), async_obj.get("overflow").unwrap().expect_json_string(),
content="DropNewest", content="DropNewest",
) )
inspect( inspect(
async_obj.get("flush").unwrap().as_string().unwrap(), async_obj.get("flush").unwrap().expect_json_string(),
content="Shutdown", content="Shutdown",
) )
} }
@@ -961,103 +984,102 @@ test "async json helpers export stable structured shapes" {
test "async build config json export materializes parsed omitted defaults" { test "async build config json export materializes parsed omitted defaults" {
let parsed = parse_async_logger_build_config_text("{}") let parsed = parse_async_logger_build_config_text("{}")
let build_json = async_logger_build_config_to_json(parsed) let build_json = async_logger_build_config_to_json(parsed)
let build_obj = build_json.as_object().unwrap() let build_obj = build_json.expect_json_object()
let logger_obj = build_obj.get("logger").unwrap().as_object().unwrap() let logger_obj = build_obj.get("logger").unwrap().expect_json_object()
let sink_obj = logger_obj.get("sink").unwrap().as_object().unwrap() let sink_obj = logger_obj.get("sink").unwrap().expect_json_object()
let formatter_obj = sink_obj let formatter_obj = sink_obj
.get("text_formatter") .get("text_formatter")
.unwrap() .unwrap()
.as_object() .expect_json_object()
.unwrap() let async_obj = build_obj.get("async_config").unwrap().expect_json_object()
let async_obj = build_obj.get("async_config").unwrap().as_object().unwrap()
inspect( inspect(
@json_parser.stringify(build_json), build_json.stringify(),
content="{\"logger\":{\"min_level\":\"INFO\",\"target\":\"\",\"timestamp\":false,\"sink\":{\"kind\":\"console\",\"path\":\"\",\"append\":true,\"auto_flush\":true,\"text_formatter\":{\"show_timestamp\":true,\"show_level\":true,\"show_target\":true,\"show_fields\":true,\"separator\":\" \",\"field_separator\":\" \",\"template\":\"\",\"color_mode\":\"never\",\"color_support\":\"truecolor\",\"style_markup\":\"full\",\"target_style_markup\":\"disabled\",\"fields_style_markup\":\"disabled\"}}},\"async_config\":{\"max_pending\":0,\"max_batch\":1,\"linger_ms\":0,\"overflow\":\"Blocking\",\"flush\":\"Never\"}}", content="{\"logger\":{\"min_level\":\"INFO\",\"target\":\"\",\"timestamp\":false,\"sink\":{\"kind\":\"console\",\"path\":\"\",\"append\":true,\"auto_flush\":true,\"text_formatter\":{\"show_timestamp\":true,\"show_level\":true,\"show_target\":true,\"show_fields\":true,\"separator\":\" \",\"field_separator\":\" \",\"template\":\"\",\"color_mode\":\"never\",\"color_support\":\"truecolor\",\"style_markup\":\"full\",\"target_style_markup\":\"disabled\",\"fields_style_markup\":\"disabled\"}}},\"async_config\":{\"max_pending\":0,\"max_batch\":1,\"linger_ms\":0,\"overflow\":\"Blocking\",\"flush\":\"Never\"}}",
) )
inspect( inspect(
logger_obj.get("min_level").unwrap().as_string().unwrap(), logger_obj.get("min_level").unwrap().expect_json_string(),
content="INFO", content="INFO",
) )
inspect(logger_obj.get("target").unwrap().as_string().unwrap(), content="") inspect(logger_obj.get("target").unwrap().expect_json_string(), content="")
inspect( inspect(
logger_obj.get("timestamp").unwrap().as_bool().unwrap(), logger_obj.get("timestamp").unwrap().expect_json_bool(),
content="false", content="false",
) )
inspect(logger_obj.get("queue") is None, content="true") inspect(logger_obj.get("queue") is None, content="true")
inspect(sink_obj.get("kind").unwrap().as_string().unwrap(), content="console") inspect(sink_obj.get("kind").unwrap().expect_json_string(), content="console")
inspect(sink_obj.get("path").unwrap().as_string().unwrap(), content="") inspect(sink_obj.get("path").unwrap().expect_json_string(), content="")
inspect(sink_obj.get("append").unwrap().as_bool().unwrap(), content="true") inspect(sink_obj.get("append").unwrap().expect_json_bool(), content="true")
inspect( inspect(
sink_obj.get("auto_flush").unwrap().as_bool().unwrap(), sink_obj.get("auto_flush").unwrap().expect_json_bool(),
content="true", content="true",
) )
inspect( inspect(
formatter_obj.get("show_timestamp").unwrap().as_bool().unwrap(), formatter_obj.get("show_timestamp").unwrap().expect_json_bool(),
content="true", content="true",
) )
inspect( inspect(
formatter_obj.get("show_level").unwrap().as_bool().unwrap(), formatter_obj.get("show_level").unwrap().expect_json_bool(),
content="true", content="true",
) )
inspect( inspect(
formatter_obj.get("show_target").unwrap().as_bool().unwrap(), formatter_obj.get("show_target").unwrap().expect_json_bool(),
content="true", content="true",
) )
inspect( inspect(
formatter_obj.get("show_fields").unwrap().as_bool().unwrap(), formatter_obj.get("show_fields").unwrap().expect_json_bool(),
content="true", content="true",
) )
inspect( inspect(
formatter_obj.get("separator").unwrap().as_string().unwrap(), formatter_obj.get("separator").unwrap().expect_json_string(),
content=" ", content=" ",
) )
inspect( inspect(
formatter_obj.get("field_separator").unwrap().as_string().unwrap(), formatter_obj.get("field_separator").unwrap().expect_json_string(),
content=" ", content=" ",
) )
inspect( inspect(
formatter_obj.get("template").unwrap().as_string().unwrap(), formatter_obj.get("template").unwrap().expect_json_string(),
content="", content="",
) )
inspect( inspect(
formatter_obj.get("color_mode").unwrap().as_string().unwrap(), formatter_obj.get("color_mode").unwrap().expect_json_string(),
content="never", content="never",
) )
inspect( inspect(
formatter_obj.get("color_support").unwrap().as_string().unwrap(), formatter_obj.get("color_support").unwrap().expect_json_string(),
content="truecolor", content="truecolor",
) )
inspect( inspect(
formatter_obj.get("style_markup").unwrap().as_string().unwrap(), formatter_obj.get("style_markup").unwrap().expect_json_string(),
content="full", content="full",
) )
inspect( inspect(
formatter_obj.get("target_style_markup").unwrap().as_string().unwrap(), formatter_obj.get("target_style_markup").unwrap().expect_json_string(),
content="disabled", content="disabled",
) )
inspect( inspect(
formatter_obj.get("fields_style_markup").unwrap().as_string().unwrap(), formatter_obj.get("fields_style_markup").unwrap().expect_json_string(),
content="disabled", content="disabled",
) )
inspect(formatter_obj.get("style_tags") is None, content="true") inspect(formatter_obj.get("style_tags") is None, content="true")
inspect( inspect(
async_obj.get("max_pending").unwrap().as_number().unwrap().to_int(), async_obj.get("max_pending").unwrap().expect_json_number().to_int(),
content="0", content="0",
) )
inspect( inspect(
async_obj.get("max_batch").unwrap().as_number().unwrap().to_int(), async_obj.get("max_batch").unwrap().expect_json_number().to_int(),
content="1", content="1",
) )
inspect( inspect(
async_obj.get("linger_ms").unwrap().as_number().unwrap().to_int(), async_obj.get("linger_ms").unwrap().expect_json_number().to_int(),
content="0", content="0",
) )
inspect( inspect(
async_obj.get("overflow").unwrap().as_string().unwrap(), async_obj.get("overflow").unwrap().expect_json_string(),
content="Blocking", content="Blocking",
) )
inspect(async_obj.get("flush").unwrap().as_string().unwrap(), content="Never") inspect(async_obj.get("flush").unwrap().expect_json_string(), content="Never")
} }
///| ///|
@@ -1068,7 +1090,7 @@ test "async config parsers reject malformed input" {
})() catch { })() catch {
err => err.to_string() err => err.to_string()
} }
inspect(invalid_json_error.contains("UnexpectedToken"), content="true") inspect(invalid_json_error != "no error", content="true")
let wrong_type_error = (fn() -> String raise { let wrong_type_error = (fn() -> String raise {
ignore(parse_async_logger_config_text("{\"max_pending\":\"many\"}")) ignore(parse_async_logger_config_text("{\"max_pending\":\"many\"}"))
@@ -1144,7 +1166,7 @@ test "async runtime capability helpers stay consistent" {
) )
inspect(state.background_worker == worker_supported, content="true") inspect(state.background_worker == worker_supported, content="true")
inspect( inspect(
@json_parser.stringify(async_runtime_state_to_json(state)), async_runtime_state_to_json(state).stringify(),
content=if worker_supported { content=if worker_supported {
"{\"mode\":\"native_worker\",\"background_worker\":true}" "{\"mode\":\"native_worker\",\"background_worker\":true}"
} else { } else {
@@ -1225,7 +1247,7 @@ test "async logger state snapshot reflects current counters and runtime" {
content="Shutdown", content="Shutdown",
) )
inspect( inspect(
@json_parser.stringify(async_logger_state_to_json(state)), async_logger_state_to_json(state).stringify(),
content=if async_runtime_supports_background_worker() { content=if async_runtime_supports_background_worker() {
"{\"runtime\":{\"mode\":\"native_worker\",\"background_worker\":true},\"phase\":\"ready\",\"pending_count\":0,\"dropped_count\":0,\"is_closed\":false,\"is_running\":false,\"has_failed\":false,\"backlog_retained\":false,\"can_rerun\":true,\"terminal\":false,\"last_error\":\"\",\"flush_policy\":\"Shutdown\"}" "{\"runtime\":{\"mode\":\"native_worker\",\"background_worker\":true},\"phase\":\"ready\",\"pending_count\":0,\"dropped_count\":0,\"is_closed\":false,\"is_running\":false,\"has_failed\":false,\"backlog_retained\":false,\"can_rerun\":true,\"terminal\":false,\"last_error\":\"\",\"flush_policy\":\"Shutdown\"}"
} else { } else {
@@ -2404,7 +2426,7 @@ async test "library async parse-build unwrap matches parsed direct async builder
///| ///|
async test "library async parse-build unwrap preserves file-backed runtime helpers" { async test "library async parse-build unwrap preserves file-backed runtime helpers" {
let raw = "{\"logger\":{\"min_level\":\"warn\",\"target\":\"async.lib.json.file.same-build\",\"queue\":{\"max_pending\":3,\"overflow\":\"DropOldest\"},\"sink\":{\"kind\":\"file\",\"path\":\"async-lib-json-file-same-build.log\"}},\"async_config\":{\"max_pending\":2,\"overflow\":\"DropOldest\",\"max_batch\":1,\"linger_ms\":0,\"flush\":\"Shutdown\"}}" let raw = "{\"logger\":{\"min_level\":\"warn\",\"target\":\"async.lib.json.file.same-build\",\"queue\":{\"max_pending\":3,\"overflow\":\"DropOldest\"},\"sink\":{\"kind\":\"file\",\"path\":\"logs/async-lib-json-file-same-build.log\"}},\"async_config\":{\"max_pending\":2,\"overflow\":\"DropOldest\",\"max_batch\":1,\"linger_ms\":0,\"flush\":\"Shutdown\"}}"
let logger = parse_and_build_library_async_logger(raw) let logger = parse_and_build_library_async_logger(raw)
let full = logger.to_async_logger() let full = logger.to_async_logger()
let direct = build_async_logger(parse_async_logger_build_config_text(raw)) let direct = build_async_logger(parse_async_logger_build_config_text(raw))
@@ -2920,7 +2942,7 @@ test "library async text logger ignores sink kind and still uses text formatter"
target="async.lib.text.kind", target="async.lib.text.kind",
sink=@bitlogger.SinkConfig::new( sink=@bitlogger.SinkConfig::new(
kind=@bitlogger.SinkKind::File, kind=@bitlogger.SinkKind::File,
path="ignored.log", path="logs/ignored.log",
text_formatter=@bitlogger.TextFormatterConfig::new( text_formatter=@bitlogger.TextFormatterConfig::new(
show_timestamp=false, show_timestamp=false,
separator=" | ", separator=" | ",
@@ -3233,7 +3255,7 @@ async test "async logger projection preserves file-backed runtime helpers throug
target="async.projected.file", target="async.projected.file",
sink=@bitlogger.SinkConfig::new( sink=@bitlogger.SinkConfig::new(
kind=@bitlogger.SinkKind::File, kind=@bitlogger.SinkKind::File,
path="async-projected-file.log", path="logs/async-projected-file.log",
), ),
queue=Some( queue=Some(
@bitlogger.QueueConfig::new( @bitlogger.QueueConfig::new(
@@ -3530,7 +3552,7 @@ async test "application async builder preserves file-backed runtime helpers" {
target="async.app.file.same-build", target="async.app.file.same-build",
sink=@bitlogger.SinkConfig::new( sink=@bitlogger.SinkConfig::new(
kind=@bitlogger.SinkKind::File, kind=@bitlogger.SinkKind::File,
path="async-app-file-same-build.log", path="logs/async-app-file-same-build.log",
), ),
queue=Some( queue=Some(
@bitlogger.QueueConfig::new( @bitlogger.QueueConfig::new(
@@ -4384,7 +4406,7 @@ test "application text async logger ignores sink kind and still uses text format
target="async.app.text.kind.alias", target="async.app.text.kind.alias",
sink=@bitlogger.SinkConfig::new( sink=@bitlogger.SinkConfig::new(
kind=@bitlogger.SinkKind::File, kind=@bitlogger.SinkKind::File,
path="ignored.log", path="logs/ignored.log",
text_formatter=@bitlogger.TextFormatterConfig::new( text_formatter=@bitlogger.TextFormatterConfig::new(
show_timestamp=false, show_timestamp=false,
separator=" | ", separator=" | ",
@@ -4462,7 +4484,7 @@ test "build async text logger ignores sink kind and still uses text formatter" {
target="async.text.kind", target="async.text.kind",
sink=@bitlogger.SinkConfig::new( sink=@bitlogger.SinkConfig::new(
kind=@bitlogger.SinkKind::File, kind=@bitlogger.SinkKind::File,
path="ignored.log", path="logs/ignored.log",
text_formatter=@bitlogger.TextFormatterConfig::new( text_formatter=@bitlogger.TextFormatterConfig::new(
show_timestamp=false, show_timestamp=false,
separator=" | ", separator=" | ",
@@ -4959,7 +4981,7 @@ async test "application async parse-build matches parsed direct async builder be
///| ///|
async test "application async parse-build preserves file-backed runtime helpers" { async test "application async parse-build preserves file-backed runtime helpers" {
let raw = "{\"logger\":{\"min_level\":\"warn\",\"target\":\"async.app.json.file.same-build\",\"queue\":{\"max_pending\":3,\"overflow\":\"DropOldest\"},\"sink\":{\"kind\":\"file\",\"path\":\"async-app-json-file-same-build.log\"}},\"async_config\":{\"max_pending\":2,\"overflow\":\"DropOldest\",\"max_batch\":1,\"linger_ms\":0,\"flush\":\"Shutdown\"}}" let raw = "{\"logger\":{\"min_level\":\"warn\",\"target\":\"async.app.json.file.same-build\",\"queue\":{\"max_pending\":3,\"overflow\":\"DropOldest\"},\"sink\":{\"kind\":\"file\",\"path\":\"logs/async-app-json-file-same-build.log\"}},\"async_config\":{\"max_pending\":2,\"overflow\":\"DropOldest\",\"max_batch\":1,\"linger_ms\":0,\"flush\":\"Shutdown\"}}"
let application = parse_and_build_application_async_logger(raw) let application = parse_and_build_application_async_logger(raw)
let direct = build_async_logger(parse_async_logger_build_config_text(raw)) let direct = build_async_logger(parse_async_logger_build_config_text(raw))
+12 -10
View File
@@ -48,9 +48,7 @@ pub fn async_runtime_state() -> AsyncRuntimeState {
} }
///| ///|
pub fn async_runtime_state_to_json( pub fn async_runtime_state_to_json(state : AsyncRuntimeState) -> Json {
state : AsyncRuntimeState,
) -> @json_parser.JsonValue {
@utils.async_runtime_state_to_json(state) @utils.async_runtime_state_to_json(state)
} }
@@ -63,9 +61,7 @@ pub fn stringify_async_runtime_state(
} }
///| ///|
pub fn async_logger_state_to_json( pub fn async_logger_state_to_json(state : AsyncLoggerState) -> Json {
state : AsyncLoggerState,
) -> @json_parser.JsonValue {
@utils.async_logger_state_to_json(state) @utils.async_logger_state_to_json(state)
} }
@@ -88,9 +84,7 @@ pub fn parse_async_logger_config_text(
} }
///| ///|
pub fn async_logger_config_to_json( pub fn async_logger_config_to_json(config : AsyncLoggerConfig) -> Json {
config : AsyncLoggerConfig,
) -> @json_parser.JsonValue {
@utils.async_logger_config_to_json(config) @utils.async_logger_config_to_json(config)
} }
@@ -115,7 +109,7 @@ pub fn parse_async_logger_build_config_text(
///| ///|
pub fn async_logger_build_config_to_json( pub fn async_logger_build_config_to_json(
config : AsyncLoggerBuildConfig, config : AsyncLoggerBuildConfig,
) -> @json_parser.JsonValue { ) -> Json {
@utils.async_logger_build_config_to_json(config) @utils.async_logger_build_config_to_json(config)
} }
@@ -280,6 +274,14 @@ pub fn[S] AsyncLogger::with_context_fields(
{ ..self, context_fields: fields } { ..self, context_fields: fields }
} }
///|
pub fn[S] AsyncLogger::with_trace_context(
self : AsyncLogger[S],
context : @bitlogger.TraceContext,
) -> AsyncLogger[S] {
self.with_context_fields(context.as_fields())
}
///| ///|
pub fn[S] AsyncLogger::with_filter( pub fn[S] AsyncLogger::with_filter(
self : AsyncLogger[S], self : AsyncLogger[S],
@@ -0,0 +1,90 @@
///|
async test "native async file lifecycle flushes rotates and restarts" {
if !@bitlogger.native_files_supported() {
inspect(@bitlogger.native_files_supported(), content="false")
} else {
let path = "logs/bitlogger-async-native-e2e.log"
let first_flushes : Ref[Int] = Ref(0)
let first_sink = @bitlogger.file_sink(
path,
append=false,
auto_flush=false,
rotation=Some(@bitlogger.file_rotation(8, max_backups=1)),
formatter=fn(rec) { rec.message },
)
let first = async_logger(
first_sink,
config=AsyncLoggerConfig::new(
max_pending=4,
overflow=AsyncOverflowPolicy::Blocking,
max_batch=4,
linger_ms=0,
flush=AsyncFlushPolicy::Batch,
),
min_level=@bitlogger.Level::Info,
target="async.file.e2e",
flush=fn(sink) {
first_flushes.val += 1
ignore(sink.flush())
},
)
@async.with_task_group(group => {
group.spawn_bg(() => first.run())
first.info("first-record")
first.shutdown()
})
inspect(first.is_closed(), content="true")
inspect(first.has_failed(), content="false")
inspect(first.pending_count(), content="0")
inspect(first_flushes.val > 0, content="true")
inspect(first_sink.path(), content="logs/bitlogger-async-native-e2e.log")
inspect(first_sink.rotation_enabled(), content="true")
inspect(first_sink.rotation_failures(), content="0")
inspect(first_sink.write_failures(), content="0")
inspect(first_sink.flush_failures(), content="0")
inspect(first_sink.close(), content="true")
let restart_flushes : Ref[Int] = Ref(0)
let restarted_sink = @bitlogger.file_sink(
path,
append=true,
auto_flush=false,
rotation=Some(@bitlogger.file_rotation(8, max_backups=1)),
formatter=fn(rec) { rec.message },
)
let restarted = async_logger(
restarted_sink,
config=AsyncLoggerConfig::new(
max_pending=4,
overflow=AsyncOverflowPolicy::Blocking,
max_batch=4,
linger_ms=0,
flush=AsyncFlushPolicy::Shutdown,
),
min_level=@bitlogger.Level::Info,
target="async.file.e2e",
flush=fn(sink) {
restart_flushes.val += 1
ignore(sink.flush())
},
)
@async.with_task_group(group => {
group.spawn_bg(() => restarted.run())
restarted.info("second-record")
restarted.shutdown()
})
inspect(restarted.is_closed(), content="true")
inspect(restarted.has_failed(), content="false")
inspect(restarted.pending_count(), content="0")
inspect(restart_flushes.val, content="1")
inspect(restarted_sink.rotation_enabled(), content="true")
inspect(restarted_sink.rotation_failures(), content="0")
inspect(restarted_sink.write_failures(), content="0")
inspect(restarted_sink.flush_failures(), content="0")
inspect(restarted_sink.close(), content="true")
}
}
+32
View File
@@ -0,0 +1,32 @@
///|
fn Json::expect_json_object(self : Json) -> Map[String, Json] {
match self {
Json::Object(value) => value
_ => abort("Expected JSON object")
}
}
///|
fn Json::expect_json_number(self : Json) -> Double {
match self {
Json::Number(value, ..) => value
_ => abort("Expected JSON number")
}
}
///|
fn Json::expect_json_string(self : Json) -> String {
match self {
Json::String(value) => value
_ => abort("Expected JSON string")
}
}
///|
fn Json::expect_json_bool(self : Json) -> Bool {
match self {
Json::True => true
Json::False => false
_ => abort("Expected JSON bool")
}
}
+8
View File
@@ -93,6 +93,14 @@ pub fn[S] LibraryAsyncLogger::bind(
self.with_context_fields(fields) self.with_context_fields(fields)
} }
///|
pub fn[S] LibraryAsyncLogger::with_trace_context(
self : LibraryAsyncLogger[S],
context : @bitlogger.TraceContext,
) -> LibraryAsyncLogger[S] {
library_async_logger(self.inner.with_trace_context(context))
}
///| ///|
pub fn[S] LibraryAsyncLogger::is_enabled( pub fn[S] LibraryAsyncLogger::is_enabled(
self : LibraryAsyncLogger[S], self : LibraryAsyncLogger[S],
+1 -1
View File
@@ -1,7 +1,7 @@
import { import {
"Nanaloveyuki/BitLogger/src" @bitlogger, "Nanaloveyuki/BitLogger/src" @bitlogger,
"Nanaloveyuki/BitLogger/src-async/utils", "Nanaloveyuki/BitLogger/src-async/utils",
"maria/json_parser", "moonbitlang/core/json",
"moonbitlang/async", "moonbitlang/async",
"moonbitlang/async/aqueue", "moonbitlang/async/aqueue",
"moonbitlang/core/env", "moonbitlang/core/env",
+6 -5
View File
@@ -6,7 +6,6 @@ import {
"Nanaloveyuki/BitLogger/src/core", "Nanaloveyuki/BitLogger/src/core",
"Nanaloveyuki/BitLogger/src/runtime", "Nanaloveyuki/BitLogger/src/runtime",
"Nanaloveyuki/BitLogger/src/sink_graph", "Nanaloveyuki/BitLogger/src/sink_graph",
"maria/json_parser",
"moonbitlang/async/aqueue", "moonbitlang/async/aqueue",
"moonbitlang/core/ref", "moonbitlang/core/ref",
} }
@@ -14,11 +13,11 @@ import {
// Values // Values
pub fn[S] async_logger(S, config? : @utils.AsyncLoggerConfig, min_level? : @core.Level, target? : String, flush? : (S) -> Unit raise) -> AsyncLogger[S] pub fn[S] async_logger(S, config? : @utils.AsyncLoggerConfig, min_level? : @core.Level, target? : String, flush? : (S) -> Unit raise) -> AsyncLogger[S]
pub fn async_logger_build_config_to_json(@utils.AsyncLoggerBuildConfig) -> @json_parser.JsonValue pub fn async_logger_build_config_to_json(@utils.AsyncLoggerBuildConfig) -> Json
pub fn async_logger_config_to_json(@utils.AsyncLoggerConfig) -> @json_parser.JsonValue pub fn async_logger_config_to_json(@utils.AsyncLoggerConfig) -> Json
pub fn async_logger_state_to_json(@utils.AsyncLoggerState) -> @json_parser.JsonValue pub fn async_logger_state_to_json(@utils.AsyncLoggerState) -> Json
pub fn async_runtime_mode() -> @utils.AsyncRuntimeMode pub fn async_runtime_mode() -> @utils.AsyncRuntimeMode
@@ -26,7 +25,7 @@ pub fn async_runtime_mode_label(@utils.AsyncRuntimeMode) -> String
pub fn async_runtime_state() -> @utils.AsyncRuntimeState pub fn async_runtime_state() -> @utils.AsyncRuntimeState
pub fn async_runtime_state_to_json(@utils.AsyncRuntimeState) -> @json_parser.JsonValue pub fn async_runtime_state_to_json(@utils.AsyncRuntimeState) -> Json
pub fn async_runtime_supports_background_worker() -> Bool pub fn async_runtime_supports_background_worker() -> Bool
@@ -115,6 +114,7 @@ pub fn[S] AsyncLogger::with_min_level(Self[S], @core.Level) -> Self[S]
pub fn[S] AsyncLogger::with_patch(Self[S], (@core.Record) -> @core.Record) -> Self[S] pub fn[S] AsyncLogger::with_patch(Self[S], (@core.Record) -> @core.Record) -> Self[S]
pub fn[S] AsyncLogger::with_target(Self[S], String) -> Self[S] pub fn[S] AsyncLogger::with_target(Self[S], String) -> Self[S]
pub fn[S] AsyncLogger::with_timestamp(Self[S], enabled? : Bool) -> Self[S] pub fn[S] AsyncLogger::with_timestamp(Self[S], enabled? : Bool) -> Self[S]
pub fn[S] AsyncLogger::with_trace_context(Self[S], @core.TraceContext) -> Self[S]
pub struct LibraryAsyncLogger[S] { pub struct LibraryAsyncLogger[S] {
inner : AsyncLogger[S] inner : AsyncLogger[S]
@@ -132,6 +132,7 @@ pub fn[S] LibraryAsyncLogger::to_async_logger(Self[S]) -> AsyncLogger[S]
pub async fn[S] LibraryAsyncLogger::warn(Self[S], String, fields? : Array[@core.Field]) -> Unit pub async fn[S] LibraryAsyncLogger::warn(Self[S], String, fields? : Array[@core.Field]) -> Unit
pub fn[S] LibraryAsyncLogger::with_context_fields(Self[S], Array[@core.Field]) -> Self[S] pub fn[S] LibraryAsyncLogger::with_context_fields(Self[S], Array[@core.Field]) -> Self[S]
pub fn[S] LibraryAsyncLogger::with_target(Self[S], String) -> Self[S] pub fn[S] LibraryAsyncLogger::with_target(Self[S], String) -> Self[S]
pub fn[S] LibraryAsyncLogger::with_trace_context(Self[S], @core.TraceContext) -> Self[S]
// Type aliases // Type aliases
pub type ApplicationAsyncLogger = AsyncLogger[@runtime.RuntimeSink] pub type ApplicationAsyncLogger = AsyncLogger[@runtime.RuntimeSink]
+59 -93
View File
@@ -154,12 +154,10 @@ pub fn AsyncLoggerState::new(
} }
///| ///|
pub fn async_runtime_state_to_json( pub fn async_runtime_state_to_json(state : AsyncRuntimeState) -> Json {
state : AsyncRuntimeState, Json::object({
) -> @json_parser.JsonValue { "mode": Json::string(async_runtime_mode_label(state.mode)),
@json_parser.JsonValue::Object({ "background_worker": Json::boolean(state.background_worker),
"mode": @json_parser.JsonValue::String(async_runtime_mode_label(state.mode)),
"background_worker": @json_parser.JsonValue::Bool(state.background_worker),
}) })
} }
@@ -170,9 +168,9 @@ pub fn stringify_async_runtime_state(
) -> String { ) -> String {
let value = async_runtime_state_to_json(state) let value = async_runtime_state_to_json(state)
if pretty { if pretty {
@json_parser.stringify_pretty(value, 2) value.stringify(indent=2)
} else { } else {
@json_parser.stringify(value) value.stringify()
} }
} }
@@ -186,37 +184,25 @@ fn async_flush_policy_label(policy : AsyncFlushPolicy) -> String {
} }
///| ///|
fn async_logger_state_to_json_value( fn async_logger_state_to_json_value(state : AsyncLoggerState) -> Json {
state : AsyncLoggerState, Json::object({
) -> @json_parser.JsonValue {
@json_parser.JsonValue::Object({
"runtime": async_runtime_state_to_json(state.runtime), "runtime": async_runtime_state_to_json(state.runtime),
"phase": @json_parser.JsonValue::String( "phase": Json::string(async_lifecycle_phase_label(state.phase)),
async_lifecycle_phase_label(state.phase), "pending_count": Json::number(state.pending_count.to_double()),
), "dropped_count": Json::number(state.dropped_count.to_double()),
"pending_count": @json_parser.JsonValue::Number( "is_closed": Json::boolean(state.is_closed),
state.pending_count.to_double(), "is_running": Json::boolean(state.is_running),
), "has_failed": Json::boolean(state.has_failed),
"dropped_count": @json_parser.JsonValue::Number( "backlog_retained": Json::boolean(state.backlog_retained),
state.dropped_count.to_double(), "can_rerun": Json::boolean(state.can_rerun),
), "terminal": Json::boolean(state.terminal),
"is_closed": @json_parser.JsonValue::Bool(state.is_closed), "last_error": Json::string(state.last_error),
"is_running": @json_parser.JsonValue::Bool(state.is_running), "flush_policy": Json::string(async_flush_policy_label(state.flush_policy)),
"has_failed": @json_parser.JsonValue::Bool(state.has_failed),
"backlog_retained": @json_parser.JsonValue::Bool(state.backlog_retained),
"can_rerun": @json_parser.JsonValue::Bool(state.can_rerun),
"terminal": @json_parser.JsonValue::Bool(state.terminal),
"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( pub fn async_logger_state_to_json(state : AsyncLoggerState) -> Json {
state : AsyncLoggerState,
) -> @json_parser.JsonValue {
async_logger_state_to_json_value(state) async_logger_state_to_json_value(state)
} }
@@ -227,9 +213,9 @@ pub fn stringify_async_logger_state(
) -> String { ) -> String {
let value = async_logger_state_to_json_value(state) let value = async_logger_state_to_json_value(state)
if pretty { if pretty {
@json_parser.stringify_pretty(value, 2) value.stringify(indent=2)
} else { } else {
@json_parser.stringify(value) value.stringify()
} }
} }
@@ -293,53 +279,38 @@ fn parse_async_flush(name : String) -> AsyncFlushPolicy raise {
pub fn parse_async_logger_config_text( pub fn parse_async_logger_config_text(
input : String, input : String,
) -> AsyncLoggerConfig raise { ) -> AsyncLoggerConfig raise {
let root = @json_parser.parse(input) let root = @json.parse(input)
let obj = match root.as_object() { let obj = match root {
Some(obj) => obj Json::Object(obj) => obj
None => raise Failure::Failure("Expected object for async logger config") _ => raise Failure::Failure("Expected object for async logger config")
} }
let max_pending = match obj.get("max_pending") { let max_pending = match obj.get("max_pending") {
Some(value) => Some(Json::Number(number, ..)) => number.to_int()
match value.as_number() { Some(_) =>
Some(number) => number.to_int() raise Failure::Failure("Expected number at async_config.max_pending")
None =>
raise Failure::Failure("Expected number at async_config.max_pending")
}
None => 0 None => 0
} }
let overflow = match obj.get("overflow") { let overflow = match obj.get("overflow") {
Some(value) => Some(Json::String(text)) => parse_async_overflow(text)
match value.as_string() { Some(_) =>
Some(text) => parse_async_overflow(text) raise Failure::Failure("Expected string at async_config.overflow")
None =>
raise Failure::Failure("Expected string at async_config.overflow")
}
None => AsyncOverflowPolicy::Blocking None => AsyncOverflowPolicy::Blocking
} }
let max_batch = match obj.get("max_batch") { let max_batch = match obj.get("max_batch") {
Some(value) => Some(Json::Number(number, ..)) => number.to_int()
match value.as_number() { Some(_) =>
Some(number) => number.to_int() raise Failure::Failure("Expected number at async_config.max_batch")
None =>
raise Failure::Failure("Expected number at async_config.max_batch")
}
None => 1 None => 1
} }
let linger_ms = match obj.get("linger_ms") { let linger_ms = match obj.get("linger_ms") {
Some(value) => Some(Json::Number(number, ..)) => number.to_int()
match value.as_number() { Some(_) =>
Some(number) => number.to_int() raise Failure::Failure("Expected number at async_config.linger_ms")
None =>
raise Failure::Failure("Expected number at async_config.linger_ms")
}
None => 0 None => 0
} }
let flush = match obj.get("flush") { let flush = match obj.get("flush") {
Some(value) => Some(Json::String(text)) => parse_async_flush(text)
match value.as_string() { Some(_) => raise Failure::Failure("Expected string at async_config.flush")
Some(text) => parse_async_flush(text)
None => raise Failure::Failure("Expected string at async_config.flush")
}
None => AsyncFlushPolicy::Never None => AsyncFlushPolicy::Never
} }
AsyncLoggerConfig::new( AsyncLoggerConfig::new(
@@ -352,23 +323,19 @@ pub fn parse_async_logger_config_text(
} }
///| ///|
pub fn async_logger_config_to_json( pub fn async_logger_config_to_json(config : AsyncLoggerConfig) -> Json {
config : AsyncLoggerConfig, Json::object({
) -> @json_parser.JsonValue { "max_pending": Json::number(config.max_pending.to_double()),
@json_parser.JsonValue::Object({ "max_batch": Json::number(config.max_batch.to_double()),
"max_pending": @json_parser.JsonValue::Number( "linger_ms": Json::number(config.linger_ms.to_double()),
config.max_pending.to_double(), "overflow": Json::string(
),
"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 { match config.overflow {
AsyncOverflowPolicy::Blocking => "Blocking" AsyncOverflowPolicy::Blocking => "Blocking"
AsyncOverflowPolicy::DropOldest => "DropOldest" AsyncOverflowPolicy::DropOldest => "DropOldest"
AsyncOverflowPolicy::DropNewest => "DropNewest" AsyncOverflowPolicy::DropNewest => "DropNewest"
}, },
), ),
"flush": @json_parser.JsonValue::String( "flush": Json::string(
match config.flush { match config.flush {
AsyncFlushPolicy::Never => "Never" AsyncFlushPolicy::Never => "Never"
AsyncFlushPolicy::Batch => "Batch" AsyncFlushPolicy::Batch => "Batch"
@@ -385,9 +352,9 @@ pub fn stringify_async_logger_config(
) -> String { ) -> String {
let value = async_logger_config_to_json(config) let value = async_logger_config_to_json(config)
if pretty { if pretty {
@json_parser.stringify_pretty(value, 2) value.stringify(indent=2)
} else { } else {
@json_parser.stringify(value) value.stringify()
} }
} }
@@ -409,21 +376,20 @@ pub fn AsyncLoggerBuildConfig::new(
pub fn parse_async_logger_build_config_text( pub fn parse_async_logger_build_config_text(
input : String, input : String,
) -> AsyncLoggerBuildConfig raise { ) -> AsyncLoggerBuildConfig raise {
let root = @json_parser.parse(input) let root = @json.parse(input)
let obj = match root.as_object() { let obj = match root {
Some(obj) => obj Json::Object(obj) => obj
None => _ =>
raise Failure::Failure( raise Failure::Failure(
"Expected object at async logger build config root", "Expected object at async logger build config root",
) )
} }
let logger = match obj.get("logger") { let logger = match obj.get("logger") {
Some(value) => Some(value) => @bitlogger.parse_logger_config_text(value.stringify())
@bitlogger.parse_logger_config_text(@json_parser.stringify(value))
None => @bitlogger.default_logger_config() None => @bitlogger.default_logger_config()
} }
let async_config = match obj.get("async_config") { let async_config = match obj.get("async_config") {
Some(value) => parse_async_logger_config_text(@json_parser.stringify(value)) Some(value) => parse_async_logger_config_text(value.stringify())
None => AsyncLoggerConfig::new() None => AsyncLoggerConfig::new()
} }
AsyncLoggerBuildConfig::new(logger~, async_config~) AsyncLoggerBuildConfig::new(logger~, async_config~)
@@ -432,8 +398,8 @@ pub fn parse_async_logger_build_config_text(
///| ///|
pub fn async_logger_build_config_to_json( pub fn async_logger_build_config_to_json(
config : AsyncLoggerBuildConfig, config : AsyncLoggerBuildConfig,
) -> @json_parser.JsonValue { ) -> Json {
@json_parser.JsonValue::Object({ Json::object({
"logger": @bitlogger.logger_config_to_json(config.logger), "logger": @bitlogger.logger_config_to_json(config.logger),
"async_config": async_logger_config_to_json(config.async_config), "async_config": async_logger_config_to_json(config.async_config),
}) })
@@ -446,8 +412,8 @@ pub fn stringify_async_logger_build_config(
) -> String { ) -> String {
let value = async_logger_build_config_to_json(config) let value = async_logger_build_config_to_json(config)
if pretty { if pretty {
@json_parser.stringify_pretty(value, 2) value.stringify(indent=2)
} else { } else {
@json_parser.stringify(value) value.stringify()
} }
} }
+1 -1
View File
@@ -1,4 +1,4 @@
import { import {
"Nanaloveyuki/BitLogger/src" @bitlogger, "Nanaloveyuki/BitLogger/src" @bitlogger,
"maria/json_parser", "moonbitlang/core/json",
} }
+4 -5
View File
@@ -3,19 +3,18 @@ package "Nanaloveyuki/BitLogger/src-async/utils"
import { import {
"Nanaloveyuki/BitLogger/src/config_model", "Nanaloveyuki/BitLogger/src/config_model",
"maria/json_parser",
} }
// Values // Values
pub fn async_logger_build_config_to_json(AsyncLoggerBuildConfig) -> @json_parser.JsonValue pub fn async_logger_build_config_to_json(AsyncLoggerBuildConfig) -> Json
pub fn async_logger_config_to_json(AsyncLoggerConfig) -> @json_parser.JsonValue pub fn async_logger_config_to_json(AsyncLoggerConfig) -> Json
pub fn async_logger_state_to_json(AsyncLoggerState) -> @json_parser.JsonValue pub fn async_logger_state_to_json(AsyncLoggerState) -> Json
pub fn async_runtime_mode_label(AsyncRuntimeMode) -> String pub fn async_runtime_mode_label(AsyncRuntimeMode) -> String
pub fn async_runtime_state_to_json(AsyncRuntimeState) -> @json_parser.JsonValue pub fn async_runtime_state_to_json(AsyncRuntimeState) -> Json
pub fn compatibility_async_runtime_mode() -> AsyncRuntimeMode pub fn compatibility_async_runtime_mode() -> AsyncRuntimeMode
+4 -58
View File
@@ -1,6 +1,6 @@
///| ///|
test "file sink tracks rotation failures on unavailable backend" { test "file sink tracks rotation failures on unavailable backend" {
let path = "bitlogger-rotate.log" let path = "logs/bitlogger-rotate.log"
ignore(@utils.remove_file_internal(path)) ignore(@utils.remove_file_internal(path))
ignore(@utils.remove_file_internal(path + ".1")) ignore(@utils.remove_file_internal(path + ".1"))
let sink = file_sink(path, rotation=Some(file_rotation(1, max_backups=1))) let sink = file_sink(path, rotation=Some(file_rotation(1, max_backups=1)))
@@ -19,7 +19,7 @@ test "file sink tracks rotation failures on unavailable backend" {
///| ///|
test "file sink reopen and failure counters reflect backend state" { test "file sink reopen and failure counters reflect backend state" {
let sink = file_sink("bitlogger-reopen.log") let sink = file_sink("logs/bitlogger-reopen.log")
if sink.is_available() { if sink.is_available() {
inspect(sink.append_mode(), content="true") inspect(sink.append_mode(), content="true")
inspect(sink.open_failures(), content="0") inspect(sink.open_failures(), content="0")
@@ -45,7 +45,7 @@ test "file sink reopen and failure counters reflect backend state" {
inspect(sink.flush_failures(), content="0") inspect(sink.flush_failures(), content="0")
inspect(sink.rotation_failures(), content="0") inspect(sink.rotation_failures(), content="0")
inspect(sink.close(), content="true") inspect(sink.close(), content="true")
ignore(@utils.remove_file_internal("bitlogger-reopen.log")) ignore(@utils.remove_file_internal("logs/bitlogger-reopen.log"))
} else { } else {
inspect(sink.append_mode(), content="true") inspect(sink.append_mode(), content="true")
inspect(sink.open_failures(), content="1") inspect(sink.open_failures(), content="1")
@@ -71,7 +71,7 @@ test "file sink reopen and failure counters reflect backend state" {
///| ///|
test "file sink can rotate on native backend" { test "file sink can rotate on native backend" {
let path = "bitlogger-rotate-native.log" let path = "logs/bitlogger-rotate-native.log"
ignore(@utils.remove_file_internal(path)) ignore(@utils.remove_file_internal(path))
ignore(@utils.remove_file_internal(path + ".1")) ignore(@utils.remove_file_internal(path + ".1"))
ignore(@utils.remove_file_internal(path + ".2")) ignore(@utils.remove_file_internal(path + ".2"))
@@ -105,57 +105,3 @@ test "file sink can rotate on native backend" {
inspect(sink.rotation_failures(), content="0") inspect(sink.rotation_failures(), content="0")
} }
} }
///|
#cfg(platform="windows")
test "file sink rotation failure counts partial backup-chain failure on native backend" {
let path = "bitlogger-rotate-partial-failure.log"
ignore(@utils.remove_file_internal(path))
ignore(@utils.remove_file_internal(path + ".1"))
ignore(@utils.remove_file_internal(path + ".2"))
let seed = file_sink(
path,
auto_flush=true,
rotation=Some(file_rotation(20, max_backups=2)),
formatter=fn(rec) { rec.message },
)
if seed.is_available() {
seed.write(record(Level::Info, "1234567890"))
seed.write(record(Level::Info, "abcdefghij"))
inspect(seed.rotation_failures(), content="0")
inspect(seed.close(), content="true")
let locked = @utils.open_file_handle_internal(path + ".2", true)
inspect(locked is Some(_), content="true")
let sink = file_sink(
path,
auto_flush=true,
rotation=Some(file_rotation(20, max_backups=2)),
formatter=fn(rec) { rec.message },
)
inspect(sink.is_available(), content="true")
sink.write(record(Level::Info, "klmnopqrst"))
inspect(sink.rotation_failures(), content="1")
inspect(sink.state().rotation_failures, content="1")
inspect(sink.write_failures(), content="1")
inspect(sink.is_available(), content="false")
match locked {
None => ()
Some(handle) => ignore(@utils.close_file_handle_internal(handle))
}
inspect(sink.reopen_append(), content="true")
inspect(sink.is_available(), content="true")
inspect(sink.close(), content="true")
ignore(@utils.remove_file_internal(path))
ignore(@utils.remove_file_internal(path + ".1"))
ignore(@utils.remove_file_internal(path + ".2"))
} else {
inspect(native_files_supported(), content="false")
}
}
+6 -3
View File
@@ -81,7 +81,7 @@ test "application logger keeps configured runtime helper surface" {
LoggerConfig::new( LoggerConfig::new(
min_level=Level::Warn, min_level=Level::Warn,
target="app.alias.helpers", target="app.alias.helpers",
sink=SinkConfig::new(kind=SinkKind::File, path="app-alias.log"), sink=SinkConfig::new(kind=SinkKind::File, path="logs/app-alias.log"),
queue=Some(QueueConfig::new(2, overflow=QueueOverflowPolicy::DropOldest)), queue=Some(QueueConfig::new(2, overflow=QueueOverflowPolicy::DropOldest)),
), ),
) )
@@ -229,7 +229,10 @@ test "application logger file helpers match direct configured builder behavior"
let config = LoggerConfig::new( let config = LoggerConfig::new(
min_level=Level::Warn, min_level=Level::Warn,
target="app.file.same-build", target="app.file.same-build",
sink=SinkConfig::new(kind=SinkKind::File, path="app-file-same-build.log"), sink=SinkConfig::new(
kind=SinkKind::File,
path="logs/app-file-same-build.log",
),
queue=Some(QueueConfig::new(2, overflow=QueueOverflowPolicy::DropOldest)), queue=Some(QueueConfig::new(2, overflow=QueueOverflowPolicy::DropOldest)),
) )
let application = build_application_logger(config) let application = build_application_logger(config)
@@ -344,7 +347,7 @@ test "application logger file controls match direct configured builder behavior"
target="app.file.controls.same-build", target="app.file.controls.same-build",
sink=SinkConfig::new( sink=SinkConfig::new(
kind=SinkKind::File, kind=SinkKind::File,
path="app-file-controls-same-build.log", path="logs/app-file-controls-same-build.log",
), ),
) )
let application = build_application_logger(config) let application = build_application_logger(config)
+5 -9
View File
@@ -39,7 +39,7 @@ pub fn parse_logger_config_text(
} }
///| ///|
pub fn queue_config_to_json(queue : QueueConfig) -> @json_parser.JsonValue { pub fn queue_config_to_json(queue : QueueConfig) -> Json {
@config_model.queue_config_to_json(queue) @config_model.queue_config_to_json(queue)
} }
@@ -52,9 +52,7 @@ pub fn stringify_queue_config(
} }
///| ///|
pub fn text_formatter_config_to_json( pub fn text_formatter_config_to_json(config : TextFormatterConfig) -> Json {
config : TextFormatterConfig,
) -> @json_parser.JsonValue {
@config_model.text_formatter_config_to_json(config) @config_model.text_formatter_config_to_json(config)
} }
@@ -67,14 +65,12 @@ pub fn stringify_text_formatter_config(
} }
///| ///|
pub fn file_rotation_config_to_json( pub fn file_rotation_config_to_json(config : FileRotation) -> Json {
config : FileRotation,
) -> @json_parser.JsonValue {
@file_model.file_rotation_config_to_json(config) @file_model.file_rotation_config_to_json(config)
} }
///| ///|
pub fn sink_config_to_json(config : SinkConfig) -> @json_parser.JsonValue { pub fn sink_config_to_json(config : SinkConfig) -> Json {
@config_model.sink_config_to_json(config) @config_model.sink_config_to_json(config)
} }
@@ -87,7 +83,7 @@ pub fn stringify_sink_config(
} }
///| ///|
pub fn logger_config_to_json(config : LoggerConfig) -> @json_parser.JsonValue { pub fn logger_config_to_json(config : LoggerConfig) -> Json {
@config_model.logger_config_to_json(config) @config_model.logger_config_to_json(config)
} }
+91 -119
View File
@@ -42,7 +42,7 @@ pub fn TextFormatterConfig::new(
style_markup? : @formatting.StyleMarkupMode = @formatting.StyleMarkupMode::Full, style_markup? : @formatting.StyleMarkupMode = @formatting.StyleMarkupMode::Full,
target_style_markup? : @formatting.StyleMarkupMode = @formatting.StyleMarkupMode::Disabled, target_style_markup? : @formatting.StyleMarkupMode = @formatting.StyleMarkupMode::Disabled,
fields_style_markup? : @formatting.StyleMarkupMode = @formatting.StyleMarkupMode::Disabled, fields_style_markup? : @formatting.StyleMarkupMode = @formatting.StyleMarkupMode::Disabled,
style_tags? : Map[String, @formatting.TextStyle] = {}, style_tags? : Map[String, @formatting.TextStyle] = Map([]),
) -> TextFormatterConfig { ) -> TextFormatterConfig {
{ {
show_timestamp, show_timestamp,
@@ -170,102 +170,84 @@ pub fn default_logger_config() -> LoggerConfig {
///| ///|
fn expect_object( fn expect_object(
value : @json_parser.JsonValue, value : Json,
context : String, context : String,
) -> Map[String, @json_parser.JsonValue] raise ConfigError { ) -> Map[String, Json] raise ConfigError {
match value.as_object() { match value {
Some(obj) => obj Json::Object(obj) => obj
None => raise ConfigError::InvalidConfig("Expected object at " + context) _ => raise ConfigError::InvalidConfig("Expected object at " + context)
} }
} }
///| ///|
fn get_string( fn get_string(
obj : Map[String, @json_parser.JsonValue], obj : Map[String, Json],
key : String, key : String,
default? : String = "", default? : String = "",
) -> String raise ConfigError { ) -> String raise ConfigError {
match obj.get(key) { match obj.get(key) {
None => default None => default
Some(value) => Some(Json::String(text)) => text
match value.as_string() { Some(_) => raise ConfigError::InvalidConfig("Expected string at key " + key)
Some(text) => text
None =>
raise ConfigError::InvalidConfig("Expected string at key " + key)
}
} }
} }
///| ///|
fn get_optional_string( fn get_optional_string(
obj : Map[String, @json_parser.JsonValue], obj : Map[String, Json],
key : String, key : String,
) -> String? raise ConfigError { ) -> String? raise ConfigError {
match obj.get(key) { match obj.get(key) {
None => None None => None
Some(value) => Some(Json::String(text)) => Some(text)
match value.as_string() { Some(_) => raise ConfigError::InvalidConfig("Expected string at key " + key)
Some(text) => Some(text)
None =>
raise ConfigError::InvalidConfig("Expected string at key " + key)
}
} }
} }
///| ///|
fn get_bool( fn get_bool(
obj : Map[String, @json_parser.JsonValue], obj : Map[String, Json],
key : String, key : String,
default~ : Bool, default~ : Bool,
) -> Bool raise ConfigError { ) -> Bool raise ConfigError {
match obj.get(key) { match obj.get(key) {
None => default None => default
Some(value) => Some(Json::True) => true
match value.as_bool() { Some(Json::False) => false
Some(flag) => flag Some(_) => raise ConfigError::InvalidConfig("Expected bool at key " + key)
None => raise ConfigError::InvalidConfig("Expected bool at key " + key)
}
} }
} }
///| ///|
fn get_int( fn get_int(
obj : Map[String, @json_parser.JsonValue], obj : Map[String, Json],
key : String, key : String,
default~ : Int, default~ : Int,
) -> Int raise ConfigError { ) -> Int raise ConfigError {
match obj.get(key) { match obj.get(key) {
None => default None => default
Some(value) => Some(Json::Number(number, ..)) => number.to_int()
match value.as_number() { Some(_) => raise ConfigError::InvalidConfig("Expected number at key " + key)
Some(number) => number.to_int()
None =>
raise ConfigError::InvalidConfig("Expected number at key " + key)
}
} }
} }
///| ///|
fn get_optional_int64_string( fn get_optional_int64_string(
obj : Map[String, @json_parser.JsonValue], obj : Map[String, Json],
key : String, key : String,
) -> Int64? raise ConfigError { ) -> Int64? raise ConfigError {
match obj.get(key) { match obj.get(key) {
None => None None => None
Some(value) => Some(Json::String(text)) =>
match value.as_string() { Some(
Some(text) => @string.parse_int64(text) catch {
Some( _ =>
@string.parse_int64(text) catch { raise ConfigError::InvalidConfig(
_ => "Expected int64 string at key " + key,
raise ConfigError::InvalidConfig( )
"Expected int64 string at key " + key, },
) )
}, Some(_) => raise ConfigError::InvalidConfig("Expected string at key " + key)
)
None =>
raise ConfigError::InvalidConfig("Expected string at key " + key)
}
} }
} }
@@ -355,7 +337,7 @@ fn sink_kind_label(kind : SinkKind) -> String {
///| ///|
fn parse_text_formatter_config( fn parse_text_formatter_config(
value : @json_parser.JsonValue, value : Json,
) -> TextFormatterConfig raise ConfigError { ) -> TextFormatterConfig raise ConfigError {
let obj = expect_object(value, "text_formatter") let obj = expect_object(value, "text_formatter")
TextFormatterConfig::new( TextFormatterConfig::new(
@@ -380,7 +362,7 @@ fn parse_text_formatter_config(
get_string(obj, "fields_style_markup", default="disabled"), get_string(obj, "fields_style_markup", default="disabled"),
), ),
style_tags=match obj.get("style_tags") { style_tags=match obj.get("style_tags") {
None => {} None => Map([])
Some(inner) => parse_style_tags_config(inner) Some(inner) => parse_style_tags_config(inner)
}, },
) )
@@ -388,7 +370,7 @@ fn parse_text_formatter_config(
///| ///|
fn parse_text_style_config( fn parse_text_style_config(
value : @json_parser.JsonValue, value : Json,
context : String, context : String,
) -> @formatting.TextStyle raise ConfigError { ) -> @formatting.TextStyle raise ConfigError {
let obj = expect_object(value, context) let obj = expect_object(value, context)
@@ -404,10 +386,10 @@ fn parse_text_style_config(
///| ///|
fn parse_style_tags_config( fn parse_style_tags_config(
value : @json_parser.JsonValue, value : Json,
) -> Map[String, @formatting.TextStyle] raise ConfigError { ) -> Map[String, @formatting.TextStyle] raise ConfigError {
let obj = expect_object(value, "text_formatter.style_tags") let obj = expect_object(value, "text_formatter.style_tags")
let style_tags : Map[String, @formatting.TextStyle] = {} let style_tags : Map[String, @formatting.TextStyle] = Map([])
for name, item in obj { for name, item in obj {
style_tags[name] = parse_text_style_config( style_tags[name] = parse_text_style_config(
item, item,
@@ -418,9 +400,7 @@ fn parse_style_tags_config(
} }
///| ///|
fn parse_queue_config( fn parse_queue_config(value : Json) -> QueueConfig raise ConfigError {
value : @json_parser.JsonValue,
) -> QueueConfig raise ConfigError {
let obj = expect_object(value, "queue") let obj = expect_object(value, "queue")
QueueConfig::new( QueueConfig::new(
get_int(obj, "max_pending", default=0), get_int(obj, "max_pending", default=0),
@@ -430,7 +410,7 @@ fn parse_queue_config(
///| ///|
fn parse_file_rotation_config( fn parse_file_rotation_config(
value : @json_parser.JsonValue, value : Json,
) -> @file_model.FileRotation raise ConfigError { ) -> @file_model.FileRotation raise ConfigError {
let obj = expect_object(value, "sink.rotation") let obj = expect_object(value, "sink.rotation")
let max_backups = get_int(obj, "max_backups", default=1) let max_backups = get_int(obj, "max_backups", default=1)
@@ -446,9 +426,7 @@ fn parse_file_rotation_config(
} }
///| ///|
fn parse_sink_config( fn parse_sink_config(value : Json) -> SinkConfig raise ConfigError {
value : @json_parser.JsonValue,
) -> SinkConfig raise ConfigError {
let obj = expect_object(value, "sink") let obj = expect_object(value, "sink")
let kind = parse_sink_kind(get_string(obj, "kind", default="console")) let kind = parse_sink_kind(get_string(obj, "kind", default="console"))
let formatter = match obj.get("text_formatter") { let formatter = match obj.get("text_formatter") {
@@ -480,7 +458,7 @@ fn parse_sink_config(
pub fn parse_logger_config_text( pub fn parse_logger_config_text(
input : String, input : String,
) -> LoggerConfig raise ConfigError { ) -> LoggerConfig raise ConfigError {
let root = @json_parser.parse(input) catch { let root = @json.parse(input) catch {
e => raise ConfigError::InvalidConfig("Invalid JSON: " + e.to_string()) e => raise ConfigError::InvalidConfig("Invalid JSON: " + e.to_string())
} }
let obj = expect_object(root, "root") let obj = expect_object(root, "root")
@@ -500,10 +478,10 @@ pub fn parse_logger_config_text(
} }
///| ///|
pub fn queue_config_to_json(queue : QueueConfig) -> @json_parser.JsonValue { pub fn queue_config_to_json(queue : QueueConfig) -> Json {
@json_parser.JsonValue::Object({ Json::object({
"max_pending": @json_parser.JsonValue::Number(queue.max_pending.to_double()), "max_pending": Json::number(queue.max_pending.to_double()),
"overflow": @json_parser.JsonValue::String( "overflow": Json::string(
match queue.overflow { match queue.overflow {
@queue_model.QueueOverflowPolicy::DropNewest => "DropNewest" @queue_model.QueueOverflowPolicy::DropNewest => "DropNewest"
@queue_model.QueueOverflowPolicy::DropOldest => "DropOldest" @queue_model.QueueOverflowPolicy::DropOldest => "DropOldest"
@@ -519,76 +497,70 @@ pub fn stringify_queue_config(
) -> String { ) -> String {
let value = queue_config_to_json(queue) let value = queue_config_to_json(queue)
if pretty { if pretty {
@json_parser.stringify_pretty(value, 2) value.stringify(indent=2)
} else { } else {
@json_parser.stringify(value) value.stringify()
} }
} }
///| ///|
pub fn text_formatter_config_to_json( pub fn text_formatter_config_to_json(config : TextFormatterConfig) -> Json {
config : TextFormatterConfig, let obj : Map[String, Json] = {
) -> @json_parser.JsonValue { "show_timestamp": Json::boolean(config.show_timestamp),
let obj : Map[String, @json_parser.JsonValue] = { "show_level": Json::boolean(config.show_level),
"show_timestamp": @json_parser.JsonValue::Bool(config.show_timestamp), "show_target": Json::boolean(config.show_target),
"show_level": @json_parser.JsonValue::Bool(config.show_level), "show_fields": Json::boolean(config.show_fields),
"show_target": @json_parser.JsonValue::Bool(config.show_target), "separator": Json::string(config.separator),
"show_fields": @json_parser.JsonValue::Bool(config.show_fields), "field_separator": Json::string(config.field_separator),
"separator": @json_parser.JsonValue::String(config.separator), "template": Json::string(config.template),
"field_separator": @json_parser.JsonValue::String(config.field_separator), "color_mode": Json::string(@formatting.color_mode_label(config.color_mode)),
"template": @json_parser.JsonValue::String(config.template), "color_support": Json::string(
"color_mode": @json_parser.JsonValue::String(
@formatting.color_mode_label(config.color_mode),
),
"color_support": @json_parser.JsonValue::String(
@formatting.color_support_label(config.color_support), @formatting.color_support_label(config.color_support),
), ),
"style_markup": @json_parser.JsonValue::String( "style_markup": Json::string(
@formatting.style_markup_mode_label(config.style_markup), @formatting.style_markup_mode_label(config.style_markup),
), ),
"target_style_markup": @json_parser.JsonValue::String( "target_style_markup": Json::string(
@formatting.style_markup_mode_label(config.target_style_markup), @formatting.style_markup_mode_label(config.target_style_markup),
), ),
"fields_style_markup": @json_parser.JsonValue::String( "fields_style_markup": Json::string(
@formatting.style_markup_mode_label(config.fields_style_markup), @formatting.style_markup_mode_label(config.fields_style_markup),
), ),
} }
if config.style_tags.length() != 0 { if config.style_tags.length() != 0 {
obj["style_tags"] = style_tags_config_to_json(config.style_tags) obj["style_tags"] = style_tags_config_to_json(config.style_tags)
} }
@json_parser.JsonValue::Object(obj) Json::object(obj)
} }
///| ///|
fn text_style_config_to_json( fn text_style_config_to_json(style : @formatting.TextStyle) -> Json {
style : @formatting.TextStyle, let obj : Map[String, Json] = {
) -> @json_parser.JsonValue { "bold": Json::boolean(style.bold),
let obj : Map[String, @json_parser.JsonValue] = { "dim": Json::boolean(style.dim),
"bold": @json_parser.JsonValue::Bool(style.bold), "italic": Json::boolean(style.italic),
"dim": @json_parser.JsonValue::Bool(style.dim), "underline": Json::boolean(style.underline),
"italic": @json_parser.JsonValue::Bool(style.italic),
"underline": @json_parser.JsonValue::Bool(style.underline),
} }
match style.fg { match style.fg {
Some(value) => obj["fg"] = @json_parser.JsonValue::String(value) Some(value) => obj["fg"] = Json::string(value)
None => () None => ()
} }
match style.bg { match style.bg {
Some(value) => obj["bg"] = @json_parser.JsonValue::String(value) Some(value) => obj["bg"] = Json::string(value)
None => () None => ()
} }
@json_parser.JsonValue::Object(obj) Json::object(obj)
} }
///| ///|
fn style_tags_config_to_json( fn style_tags_config_to_json(
style_tags : Map[String, @formatting.TextStyle], style_tags : Map[String, @formatting.TextStyle],
) -> @json_parser.JsonValue { ) -> Json {
let obj : Map[String, @json_parser.JsonValue] = {} let obj : Map[String, Json] = Map([])
for name, style in style_tags { for name, style in style_tags {
obj[name] = text_style_config_to_json(style) obj[name] = text_style_config_to_json(style)
} }
@json_parser.JsonValue::Object(obj) Json::object(obj)
} }
///| ///|
@@ -598,19 +570,19 @@ pub fn stringify_text_formatter_config(
) -> String { ) -> String {
let value = text_formatter_config_to_json(config) let value = text_formatter_config_to_json(config)
if pretty { if pretty {
@json_parser.stringify_pretty(value, 2) value.stringify(indent=2)
} else { } else {
@json_parser.stringify(value) value.stringify()
} }
} }
///| ///|
pub fn sink_config_to_json(config : SinkConfig) -> @json_parser.JsonValue { pub fn sink_config_to_json(config : SinkConfig) -> Json {
let obj : Map[String, @json_parser.JsonValue] = { let obj : Map[String, Json] = {
"kind": @json_parser.JsonValue::String(sink_kind_label(config.kind)), "kind": Json::string(sink_kind_label(config.kind)),
"path": @json_parser.JsonValue::String(config.path), "path": Json::string(config.path),
"append": @json_parser.JsonValue::Bool(config.append), "append": Json::boolean(config.append),
"auto_flush": @json_parser.JsonValue::Bool(config.auto_flush), "auto_flush": Json::boolean(config.auto_flush),
"text_formatter": text_formatter_config_to_json(config.text_formatter), "text_formatter": text_formatter_config_to_json(config.text_formatter),
} }
match config.rotation { match config.rotation {
@@ -618,7 +590,7 @@ pub fn sink_config_to_json(config : SinkConfig) -> @json_parser.JsonValue {
Some(rotation) => Some(rotation) =>
obj["rotation"] = @file_model.file_rotation_config_to_json(rotation) obj["rotation"] = @file_model.file_rotation_config_to_json(rotation)
} }
@json_parser.JsonValue::Object(obj) Json::object(obj)
} }
///| ///|
@@ -628,25 +600,25 @@ pub fn stringify_sink_config(
) -> String { ) -> String {
let value = sink_config_to_json(config) let value = sink_config_to_json(config)
if pretty { if pretty {
@json_parser.stringify_pretty(value, 2) value.stringify(indent=2)
} else { } else {
@json_parser.stringify(value) value.stringify()
} }
} }
///| ///|
pub fn logger_config_to_json(config : LoggerConfig) -> @json_parser.JsonValue { pub fn logger_config_to_json(config : LoggerConfig) -> Json {
let obj : Map[String, @json_parser.JsonValue] = { let obj : Map[String, Json] = {
"min_level": @json_parser.JsonValue::String(config.min_level.label()), "min_level": Json::string(config.min_level.label()),
"target": @json_parser.JsonValue::String(config.target), "target": Json::string(config.target),
"timestamp": @json_parser.JsonValue::Bool(config.timestamp), "timestamp": Json::boolean(config.timestamp),
"sink": sink_config_to_json(config.sink), "sink": sink_config_to_json(config.sink),
} }
match config.queue { match config.queue {
None => () None => ()
Some(queue) => obj["queue"] = queue_config_to_json(queue) Some(queue) => obj["queue"] = queue_config_to_json(queue)
} }
@json_parser.JsonValue::Object(obj) Json::object(obj)
} }
///| ///|
@@ -656,8 +628,8 @@ pub fn stringify_logger_config(
) -> String { ) -> String {
let value = logger_config_to_json(config) let value = logger_config_to_json(config)
if pretty { if pretty {
@json_parser.stringify_pretty(value, 2) value.stringify(indent=2)
} else { } else {
@json_parser.stringify(value) value.stringify()
} }
} }

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