8 Commits

Author SHA1 Message Date
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
75 changed files with 1683 additions and 286 deletions
+42 -27
View File
@@ -6,28 +6,51 @@ BitLogger 是一个使用 MoonBit 编写的结构化日志库,适合命令行
- [English README](./docs/README-en.md)
- [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")])
ignore(logger.flush())
```bash
moon new log-demo
cd log-demo
moon add Nanaloveyuki/BitLogger@0.7.1
```
推荐从 `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)
## 支持情况
@@ -47,19 +70,11 @@ ignore(logger.flush())
- 组合能力:queue、filter、patch、fanout、split、callback
- 异步日志:独立 `src-async` package
## 示例
- `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)
常用入口:`text_console(...)``file(...)``with_queue(...)``build_logger(...)``build_async_logger(...)`
仓库内的 `examples/` 目录与 Examples 文档一一对应;文档解释选择、前提、运行命令和后续扩展,源码保持为可执行参考。
+122 -22
View File
@@ -104,6 +104,117 @@ 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' },
]
}
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' },
]
}
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({
title: 'BitLogger',
description: 'Structured logging library docs for MoonBit.',
@@ -114,29 +225,18 @@ export default defineConfig({
markdown: {
languages: [createMoonbitLanguageRegistration()],
},
themeConfig: {
siteTitle: 'BitLogger',
nav: [
{ text: 'Home', link: '/' },
{ text: 'API', link: '/api/' },
{ text: 'Changes', link: '/changes/' },
{ text: 'Mooncake', link: 'https://mooncakes.io/docs/Nanaloveyuki/BitLogger' },
],
search: {
provider: 'local',
themeConfig: englishThemeConfig,
locales: {
root: {
label: 'English',
lang: 'en-US',
themeConfig: englishThemeConfig,
},
socialLinks: [{ icon: 'github', link: repository }],
editLink: {
pattern: `${repository}/edit/main/docs/:path`,
text: 'Edit this page on GitHub',
},
sidebar: {
'/api/': buildApiSidebar(),
'/changes/': buildChangesSidebar(),
},
footer: {
message: 'Published from the repository docs folder with VitePress.',
copyright: 'MIT',
zh: {
label: '简体中文',
lang: 'zh-CN',
link: '/zh/',
themeConfig: chineseThemeConfig,
},
},
})
+36 -1
View File
@@ -6,11 +6,46 @@ import HomeDocHub from './components/HomeDocHub.vue'
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 = {
extends: DefaultTheme,
enhanceApp({ app }) {
enhanceApp({ app, router }) {
app.component('ApiOverview', ApiOverview)
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/')
}
},
}
+29 -23
View File
@@ -9,24 +9,38 @@ 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.
## Quick Start
## Start With A Working Flow
```moonbit
let logger = build_logger(
text_console(
min_level=Level::Info,
target="demo",
text_formatter=TextFormatterConfig::new(show_timestamp=false, separator=" | "),
),
)
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.
logger.info("starting", fields=[field("port", "8080")])
ignore(logger.flush())
### 1. Install
```bash
moon new log-demo
cd log-demo
moon add Nanaloveyuki/BitLogger@0.7.1
```
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
@@ -45,18 +59,10 @@ Use `Logger::new(...)` when you want to assemble custom sink graphs directly.
- Composition helpers: queue, filter, patch, fanout, split, callback
- Separate async package under `src-async`
## 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
- [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
- [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
+2 -1
View File
@@ -2,7 +2,7 @@
name: file-sink-reopen-append
group: api
category: sink
update-time: 20260613
update-time: 20260717
description: Reopen a FileSink in append mode.
key-word:
- file
@@ -37,6 +37,7 @@ Detailed rules explaining key parameters and behaviors
- Reopen behavior is fixed to append mode.
- 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.
- After a rotation failure, this is the explicit recovery path once the underlying remove, rename, or open condition has been resolved.
### How to Use
+4 -1
View File
@@ -2,7 +2,7 @@
name: file-sink-rotation-failures
group: api
category: sink
update-time: 20260707
update-time: 20260717
description: Read the number of rotation failures recorded by a FileSink.
key-word:
- file
@@ -36,6 +36,7 @@ Detailed rules explaining key parameters and behaviors
- 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.
- 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.
- 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.
- After a recorded rotation failure, use `reopen_append()` only after the underlying filesystem condition has been resolved.
### Notes
1. Use this helper when diagnosing incomplete direct file rotations.
+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
+1
View File
@@ -2,6 +2,7 @@
Versioned BitLogger change summaries.
- [1.1.1](./1.1.1(0.7.1).md)
- [1.1.0](./1.1.0(0.7.0).md)
- [1.0.1](./1.0.1(0.6.1).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.1
```
```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)
+14
View File
@@ -0,0 +1,14 @@
# 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. |
For exact function signatures, follow each page's links into the [API reference](../api/index.md).
+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:
name: BitLogger
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:
- theme: brand
text: Open API Reference
link: /api/
text: Start Examples
link: /examples/
- theme: alt
text: Read Release Notes
link: /changes/
text: Extend A Logger
link: /extend/
- theme: alt
text: Mooncake Package
link: https://mooncakes.io/docs/Nanaloveyuki/BitLogger
features:
- title: Structured logging
details: Levels, targets, message fields, and configurable text or JSON output.
- title: Sync and async paths
details: API reference covers both the main package and the async package surface.
- title: Config and runtime helpers
details: Browse builders, presets, file helpers, queue helpers, and verification notes quickly.
- title: Examples first
details: Follow complete console, file, config, and async flows before looking up individual symbols.
- title: Extend deliberately
details: Add queues, custom sink graphs, formatting, and target-specific behavior only when the base flow needs them.
- title: API as reference
details: Use the one-symbol-per-page API reference for exact signatures, contracts, and edge cases.
---
## Start Here
<HomeDocHub />
- Use [API Reference](./api/index.md) when you want the canonical one-file-per-public-API docs.
- 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.
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.
3. Use [API Reference](./api/index.md) for exact contracts and [Release Notes](./changes/index.md) for version history.
+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.1
```
在应用的 `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)。
+14
View File
@@ -0,0 +1,14 @@
# 扩展 Logger
先完成一个[示例流程](../examples/index.md),再一次只加入一种扩展能力。这些页面说明某个抽象何时有用,以及它会引入什么运行时边界。
## 扩展地图
| 需求 | 扩展 | 主要取舍 |
| --- | --- | --- |
| 吸收短时输出突发 | [队列](./queue.md) | 必须选择溢出与 flush 行为。 |
| 同时发送到多个位置或按 level 路由 | [Sink 组合](./composition.md) | 比 preset 更需要显式构造。 |
| 调整终端可读性 | [文本格式](./formatting.md) | 格式只改变呈现,不改变记录结构。 |
| 在 native 与 web 目标之间共享代码 | [目标平台边界](./targets.md) | 文件与异步运行时行为因后端而异。 |
需要精确函数签名时,沿页面链接进入[英文 API 参考](../../api/index.md)。
+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"
options(
is_main: true,
)
pkgtype(kind: "executable")
+1 -3
View File
@@ -2,6 +2,4 @@ import {
"Nanaloveyuki/BitLogger/src" @lib,
}
options(
"is-main": true,
)
pkgtype(kind: "executable")
+1 -3
View File
@@ -2,6 +2,4 @@ import {
"Nanaloveyuki/BitLogger/src" @lib,
}
options(
"is-main": true,
)
pkgtype(kind: "executable")
+1 -3
View File
@@ -2,6 +2,4 @@ import {
"Nanaloveyuki/BitLogger/src" @lib,
}
options(
"is-main": true,
)
pkgtype(kind: "executable")
+1 -3
View File
@@ -2,6 +2,4 @@ import {
"Nanaloveyuki/BitLogger/src" @lib,
}
options(
"is-main": true,
)
pkgtype(kind: "executable")
+1 -3
View File
@@ -2,6 +2,4 @@ import {
"Nanaloveyuki/BitLogger/src" @lib,
}
options(
"is-main": true,
)
pkgtype(kind: "executable")
+1 -3
View File
@@ -2,6 +2,4 @@ import {
"Nanaloveyuki/BitLogger/src" @lib,
}
options(
"is-main": true,
)
pkgtype(kind: "executable")
+1 -3
View File
@@ -2,6 +2,4 @@ import {
"Nanaloveyuki/BitLogger/src" @lib,
}
options(
"is-main": true,
)
pkgtype(kind: "executable")
+1
View File
@@ -0,0 +1 @@
+2 -2
View File
@@ -1,10 +1,10 @@
name = "Nanaloveyuki/BitLogger"
version = "0.7.0"
version = "0.7.1"
import {
"maria/json_parser@0.1.1",
"moonbitlang/async@0.20.0",
"moonbitlang/async@0.20.2",
}
readme = "src/README.mbt.md"
+7 -7
View File
@@ -2404,7 +2404,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" {
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 full = logger.to_async_logger()
let direct = build_async_logger(parse_async_logger_build_config_text(raw))
@@ -2920,7 +2920,7 @@ test "library async text logger ignores sink kind and still uses text formatter"
target="async.lib.text.kind",
sink=@bitlogger.SinkConfig::new(
kind=@bitlogger.SinkKind::File,
path="ignored.log",
path="logs/ignored.log",
text_formatter=@bitlogger.TextFormatterConfig::new(
show_timestamp=false,
separator=" | ",
@@ -3233,7 +3233,7 @@ async test "async logger projection preserves file-backed runtime helpers throug
target="async.projected.file",
sink=@bitlogger.SinkConfig::new(
kind=@bitlogger.SinkKind::File,
path="async-projected-file.log",
path="logs/async-projected-file.log",
),
queue=Some(
@bitlogger.QueueConfig::new(
@@ -3530,7 +3530,7 @@ async test "application async builder preserves file-backed runtime helpers" {
target="async.app.file.same-build",
sink=@bitlogger.SinkConfig::new(
kind=@bitlogger.SinkKind::File,
path="async-app-file-same-build.log",
path="logs/async-app-file-same-build.log",
),
queue=Some(
@bitlogger.QueueConfig::new(
@@ -4384,7 +4384,7 @@ test "application text async logger ignores sink kind and still uses text format
target="async.app.text.kind.alias",
sink=@bitlogger.SinkConfig::new(
kind=@bitlogger.SinkKind::File,
path="ignored.log",
path="logs/ignored.log",
text_formatter=@bitlogger.TextFormatterConfig::new(
show_timestamp=false,
separator=" | ",
@@ -4462,7 +4462,7 @@ test "build async text logger ignores sink kind and still uses text formatter" {
target="async.text.kind",
sink=@bitlogger.SinkConfig::new(
kind=@bitlogger.SinkKind::File,
path="ignored.log",
path="logs/ignored.log",
text_formatter=@bitlogger.TextFormatterConfig::new(
show_timestamp=false,
separator=" | ",
@@ -4959,7 +4959,7 @@ async test "application async parse-build matches parsed direct async builder be
///|
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 direct = build_async_logger(parse_async_logger_build_config_text(raw))
+4 -58
View File
@@ -1,6 +1,6 @@
///|
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 + ".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" {
let sink = file_sink("bitlogger-reopen.log")
let sink = file_sink("logs/bitlogger-reopen.log")
if sink.is_available() {
inspect(sink.append_mode(), content="true")
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.rotation_failures(), content="0")
inspect(sink.close(), content="true")
ignore(@utils.remove_file_internal("bitlogger-reopen.log"))
ignore(@utils.remove_file_internal("logs/bitlogger-reopen.log"))
} else {
inspect(sink.append_mode(), content="true")
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" {
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 + ".1"))
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")
}
}
///|
#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(
min_level=Level::Warn,
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)),
),
)
@@ -229,7 +229,10 @@ test "application logger file helpers match direct configured builder behavior"
let config = LoggerConfig::new(
min_level=Level::Warn,
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)),
)
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",
sink=SinkConfig::new(
kind=SinkKind::File,
path="app-file-controls-same-build.log",
path="logs/app-file-controls-same-build.log",
),
)
let application = build_application_logger(config)
+4 -4
View File
@@ -42,7 +42,7 @@ pub fn TextFormatterConfig::new(
style_markup? : @formatting.StyleMarkupMode = @formatting.StyleMarkupMode::Full,
target_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 {
{
show_timestamp,
@@ -380,7 +380,7 @@ fn parse_text_formatter_config(
get_string(obj, "fields_style_markup", default="disabled"),
),
style_tags=match obj.get("style_tags") {
None => {}
None => Map([])
Some(inner) => parse_style_tags_config(inner)
},
)
@@ -407,7 +407,7 @@ fn parse_style_tags_config(
value : @json_parser.JsonValue,
) -> Map[String, @formatting.TextStyle] raise ConfigError {
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 {
style_tags[name] = parse_text_style_config(
item,
@@ -584,7 +584,7 @@ fn text_style_config_to_json(
fn style_tags_config_to_json(
style_tags : Map[String, @formatting.TextStyle],
) -> @json_parser.JsonValue {
let obj : Map[String, @json_parser.JsonValue] = {}
let obj : Map[String, @json_parser.JsonValue] = Map([])
for name, style in style_tags {
obj[name] = text_style_config_to_json(style)
}
+3 -3
View File
@@ -5,7 +5,7 @@ test "configured logger file setters stay aligned with runtime sink setters" {
target="config.file.setters.delegate",
sink=SinkConfig::new(
kind=SinkKind::File,
path="config-file-setters-delegate.log",
path="logs/config-file-setters-delegate.log",
),
),
)
@@ -100,7 +100,7 @@ test "configured logger file reopen helpers stay aligned with runtime sink helpe
LoggerConfig::new(
sink=SinkConfig::new(
kind=SinkKind::File,
path="config-reopen-delegate.log",
path="logs/config-reopen-delegate.log",
),
),
)
@@ -174,7 +174,7 @@ test "configured logger file reset helpers stay aligned with runtime sink helper
LoggerConfig::new(
sink=SinkConfig::new(
kind=SinkKind::File,
path="config-reset-delegate.log",
path="logs/config-reset-delegate.log",
append=false,
auto_flush=false,
rotation=Some(file_rotation(40, max_backups=2)),
+20 -8
View File
@@ -2,7 +2,7 @@
test "configured logger file setters update file sink policy state" {
let logger = build_logger(
LoggerConfig::new(
sink=SinkConfig::new(kind=SinkKind::File, path="config-setters.log"),
sink=SinkConfig::new(kind=SinkKind::File, path="logs/config-setters.log"),
queue=Some(QueueConfig::new(2, overflow=QueueOverflowPolicy::DropNewest)),
),
)
@@ -61,7 +61,7 @@ test "configured logger reset policy restores configured file defaults" {
LoggerConfig::new(
sink=SinkConfig::new(
kind=SinkKind::File,
path="config-reset-policy.log",
path="logs/config-reset-policy.log",
append=false,
auto_flush=false,
rotation=Some(file_rotation(36, max_backups=2)),
@@ -93,7 +93,10 @@ test "configured logger reset policy restores configured file defaults" {
test "configured logger set policy applies bundled runtime file policy" {
let logger = build_logger(
LoggerConfig::new(
sink=SinkConfig::new(kind=SinkKind::File, path="config-set-policy.log"),
sink=SinkConfig::new(
kind=SinkKind::File,
path="logs/config-set-policy.log",
),
),
)
let default_policy = logger.file_default_policy()
@@ -140,7 +143,7 @@ test "configured non-file logger cannot reset file policy" {
test "configured logger can reopen built file sink" {
let logger = build_logger(
LoggerConfig::new(
sink=SinkConfig::new(kind=SinkKind::File, path="config-reopen.log"),
sink=SinkConfig::new(kind=SinkKind::File, path="logs/config-reopen.log"),
),
)
if logger.file_available() {
@@ -199,7 +202,7 @@ test "configured non-file logger cannot reset file failure counters" {
test "configured logger exposes file flush and close helpers" {
let logger = build_logger(
LoggerConfig::new(
sink=SinkConfig::new(kind=SinkKind::File, path="config-control.log"),
sink=SinkConfig::new(kind=SinkKind::File, path="logs/config-control.log"),
),
)
if logger.file_available() {
@@ -218,7 +221,10 @@ test "configured logger exposes file flush and close helpers" {
test "configured queued file logger flushes queue through file helper" {
let logger = build_logger(
LoggerConfig::new(
sink=SinkConfig::new(kind=SinkKind::File, path="config-queued-file.log"),
sink=SinkConfig::new(
kind=SinkKind::File,
path="logs/config-queued-file.log",
),
queue=Some(QueueConfig::new(4, overflow=QueueOverflowPolicy::DropNewest)),
),
)
@@ -257,7 +263,10 @@ test "configured queued file logger flushes queue through file helper" {
test "configured queued file logger blocks policy mutation while queue is pending" {
let logger = build_logger(
LoggerConfig::new(
sink=SinkConfig::new(kind=SinkKind::File, path="config-queued-policy.log"),
sink=SinkConfig::new(
kind=SinkKind::File,
path="logs/config-queued-policy.log",
),
queue=Some(QueueConfig::new(4, overflow=QueueOverflowPolicy::DropNewest)),
),
)
@@ -293,7 +302,10 @@ test "configured queued file logger blocks policy mutation while queue is pendin
test "configured queued file logger close drains queue before teardown" {
let logger = build_logger(
LoggerConfig::new(
sink=SinkConfig::new(kind=SinkKind::File, path="config-close-queued.log"),
sink=SinkConfig::new(
kind=SinkKind::File,
path="logs/config-close-queued.log",
),
queue=Some(QueueConfig::new(4, overflow=QueueOverflowPolicy::DropNewest)),
),
)
@@ -3,7 +3,10 @@ test "configured logger file observation helpers stay aligned with runtime sink
let file_logger = build_logger(
LoggerConfig::new(
target="config.file.delegate",
sink=SinkConfig::new(kind=SinkKind::File, path="config-file-delegate.log"),
sink=SinkConfig::new(
kind=SinkKind::File,
path="logs/config-file-delegate.log",
),
),
)
let file_sink = file_logger.sink
@@ -74,7 +77,7 @@ test "configured logger file policy helpers stay aligned with runtime sink helpe
target="config.file.policy.delegate",
sink=SinkConfig::new(
kind=SinkKind::File,
path="config-file-policy-delegate.log",
path="logs/config-file-policy-delegate.log",
append=false,
auto_flush=true,
rotation=Some(file_rotation(64, max_backups=2)),
@@ -213,7 +216,7 @@ test "configured logger file flush and close stay aligned with runtime sink help
let file_config = LoggerConfig::new(
sink=SinkConfig::new(
kind=SinkKind::File,
path="config-file-helper-delegate.log",
path="logs/config-file-helper-delegate.log",
),
)
let file_logger = build_logger(file_config)
@@ -245,7 +248,7 @@ test "configured logger file state helpers stay aligned with runtime sink helper
target="config.file.state.delegate",
sink=SinkConfig::new(
kind=SinkKind::File,
path="config-file-state-delegate.log",
path="logs/config-file-state-delegate.log",
),
),
)
@@ -297,7 +300,7 @@ test "configured logger file state helpers stay aligned with runtime sink helper
target="config.file.runtime.delegate",
sink=SinkConfig::new(
kind=SinkKind::File,
path="config-file-runtime-delegate.log",
path="logs/config-file-runtime-delegate.log",
),
queue=Some(QueueConfig::new(4, overflow=QueueOverflowPolicy::DropNewest)),
),
+13 -7
View File
@@ -4,7 +4,7 @@ test "configured logger exposes file sink observability helpers" {
LoggerConfig::new(
sink=SinkConfig::new(
kind=SinkKind::File,
path="config-file.log",
path="logs/config-file.log",
auto_flush=false,
rotation=Some(file_rotation(64, max_backups=3)),
),
@@ -13,8 +13,8 @@ test "configured logger exposes file sink observability helpers" {
let state = logger.file_state()
let runtime_state = logger.file_runtime_state()
inspect(logger.file_available() == native_files_supported(), content="true")
inspect(logger.file_path(), content="config-file.log")
inspect(state.path, content="config-file.log")
inspect(logger.file_path(), content="logs/config-file.log")
inspect(state.path, content="logs/config-file.log")
inspect(state.available == logger.file_available(), content="true")
inspect(state.append == logger.file_append_mode(), content="true")
inspect(state.auto_flush == logger.file_auto_flush(), content="true")
@@ -25,7 +25,7 @@ test "configured logger exposes file sink observability helpers" {
inspect(snapshot.queued, content="false")
inspect(snapshot.pending_count, content="0")
inspect(snapshot.dropped_count, content="0")
inspect(snapshot.file.path, content="config-file.log")
inspect(snapshot.file.path, content="logs/config-file.log")
}
None => inspect(false, content="true")
}
@@ -51,7 +51,10 @@ test "configured logger exposes file sink observability helpers" {
test "configured logger reports disabled rotation when file sink has none" {
let logger = build_logger(
LoggerConfig::new(
sink=SinkConfig::new(kind=SinkKind::File, path="config-no-rotation.log"),
sink=SinkConfig::new(
kind=SinkKind::File,
path="logs/config-no-rotation.log",
),
),
)
inspect(logger.file_rotation_enabled(), content="false")
@@ -79,12 +82,15 @@ test "configured non-file logger has no file runtime state" {
test "configured file logger mirrors backend file capability" {
let logger = build_logger(
LoggerConfig::new(
sink=SinkConfig::new(kind=SinkKind::File, path="config-capability.log"),
sink=SinkConfig::new(
kind=SinkKind::File,
path="logs/config-capability.log",
),
),
)
inspect(logger.file_available() == native_files_supported(), content="true")
match logger.file_path_or_none() {
Some(path) => inspect(path, content="config-capability.log")
Some(path) => inspect(path, content="logs/config-capability.log")
None => inspect(false, content="true")
}
inspect(logger.file_policy_or_none() is None, content="false")
+4 -1
View File
@@ -82,7 +82,10 @@ test "configured logger projection preserves file runtime helpers through unwrap
LoggerConfig::new(
min_level=Level::Warn,
target="lib.projected.file",
sink=SinkConfig::new(kind=SinkKind::File, path="lib-projected-file.log"),
sink=SinkConfig::new(
kind=SinkKind::File,
path="logs/lib-projected-file.log",
),
queue=Some(QueueConfig::new(2, overflow=QueueOverflowPolicy::DropOldest)),
),
)
+56 -10
View File
@@ -16,6 +16,24 @@ type Record = @core.Record
///|
type RecordFormatter = @formatting.RecordFormatter
///|
priv struct RotationFileOps {
file_exists : (String) -> Bool
remove_file : (String) -> Bool
rename_file : (String, String) -> Bool
}
///|
fn default_rotation_file_ops() -> RotationFileOps {
{
file_exists: fn(path) { @utils.file_exists_internal(path) },
remove_file: fn(path) { @utils.remove_file_internal(path) },
rename_file: fn(from_path, to_path) {
@utils.rename_file_internal(from_path, to_path)
},
}
}
///|
pub struct FileSink {
priv path : String
@@ -31,6 +49,7 @@ pub struct FileSink {
priv write_failures : Ref[Int]
priv flush_failures : Ref[Int]
priv rotation_failures : Ref[Int]
priv rotation_file_ops : Ref[RotationFileOps]
}
///|
@@ -61,6 +80,7 @@ pub fn file_sink(
write_failures: Ref(0),
flush_failures: Ref(0),
rotation_failures: Ref(0),
rotation_file_ops: Ref(default_rotation_file_ops()),
}
}
@@ -291,6 +311,28 @@ fn rotated_file_path(path : String, index : Int) -> String {
"\{path}.\{index}"
}
///|
fn remove_file_if_present(ops : RotationFileOps, path : String) -> Bool {
if !(ops.file_exists)(path) {
true
} else {
(ops.remove_file)(path) || !(ops.file_exists)(path)
}
}
///|
fn rename_file_if_present(
ops : RotationFileOps,
from_path : String,
to_path : String,
) -> Bool {
if !(ops.file_exists)(from_path) {
true
} else {
(ops.rename_file)(from_path, to_path) || !(ops.file_exists)(from_path)
}
}
///|
fn rotate_file_sink_internal(sink : FileSink, rotation : FileRotation) -> Bool {
let closed = match sink.handle.val {
@@ -304,23 +346,27 @@ fn rotate_file_sink_internal(sink : FileSink, rotation : FileRotation) -> Bool {
if !closed {
return false
}
let ops = sink.rotation_file_ops.val
if rotation.max_backups > 0 {
ignore(
@utils.remove_file_internal(
if !remove_file_if_present(
ops,
rotated_file_path(sink.path, rotation.max_backups),
),
)
) {
return false
}
for index = rotation.max_backups - 1; index >= 1; {
let from_path = rotated_file_path(sink.path, index)
let to_path = rotated_file_path(sink.path, index + 1)
ignore(@utils.rename_file_internal(from_path, to_path))
if !rename_file_if_present(ops, from_path, to_path) {
return false
}
continue index - 1
}
ignore(
@utils.rename_file_internal(sink.path, rotated_file_path(sink.path, 1)),
)
} else {
ignore(@utils.remove_file_internal(sink.path))
if !rename_file_if_present(ops, sink.path, rotated_file_path(sink.path, 1)) {
return false
}
} else if !remove_file_if_present(ops, sink.path) {
return false
}
sink.handle.val = @utils.open_file_handle_internal(sink.path, false)
sink.handle.val is Some(_)
@@ -0,0 +1,111 @@
///|
test "file sink counts an injected partial backup-chain failure" {
let test_path = "logs/file-runtime-rotate-partial-failure.log"
ignore(@utils.remove_file_internal(test_path))
ignore(@utils.remove_file_internal(test_path + ".1"))
ignore(@utils.remove_file_internal(test_path + ".2"))
let rotation = @file_model.file_rotation(20, max_backups=2)
let seed = file_sink(test_path, auto_flush=true, rotation=Some(rotation), formatter=fn(
rec,
) {
rec.message
})
if seed.is_available() {
seed.write(@core.Record::new(@core.Level::Info, "1234567890"))
seed.write(@core.Record::new(@core.Level::Info, "abcdefghij"))
inspect(seed.rotation_failures(), content="0")
inspect(seed.close(), content="true")
let sink = file_sink(test_path, auto_flush=true, rotation=Some(rotation), formatter=fn(
rec,
) {
rec.message
})
sink.rotation_file_ops.val = {
file_exists: fn(candidate) { @utils.file_exists_internal(candidate) },
remove_file: fn(candidate) { @utils.remove_file_internal(candidate) },
rename_file: fn(from_path, to_path) {
if from_path == test_path + ".1" && to_path == test_path + ".2" {
false
} else {
@utils.rename_file_internal(from_path, to_path)
}
},
}
sink.write(@core.Record::new(@core.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")
inspect(sink.reopen_append(), content="true")
inspect(sink.is_available(), content="true")
inspect(sink.close(), content="true")
ignore(@utils.remove_file_internal(test_path))
ignore(@utils.remove_file_internal(test_path + ".1"))
ignore(@utils.remove_file_internal(test_path + ".2"))
} else {
inspect(native_files_supported(), content="false")
}
}
///|
test "file sink counts an injected oldest-backup removal failure" {
let test_path = "logs/file-runtime-rotate-removal-failure.log"
ignore(@utils.remove_file_internal(test_path))
ignore(@utils.remove_file_internal(test_path + ".1"))
ignore(@utils.remove_file_internal(test_path + ".2"))
let rotation = @file_model.file_rotation(20, max_backups=2)
let seed = file_sink(test_path, auto_flush=true, rotation=Some(rotation), formatter=fn(
rec,
) {
rec.message
})
if seed.is_available() {
seed.write(@core.Record::new(@core.Level::Info, "1234567890"))
seed.write(@core.Record::new(@core.Level::Info, "abcdefghij"))
inspect(seed.close(), content="true")
let oldest = @utils.open_file_handle_internal(test_path + ".2", true)
inspect(oldest is Some(_), content="true")
match oldest {
None => ()
Some(handle) =>
inspect(@utils.close_file_handle_internal(handle), content="true")
}
let sink = file_sink(test_path, auto_flush=true, rotation=Some(rotation), formatter=fn(
rec,
) {
rec.message
})
sink.rotation_file_ops.val = {
file_exists: fn(candidate) { @utils.file_exists_internal(candidate) },
remove_file: fn(candidate) {
if candidate == test_path + ".2" {
false
} else {
@utils.remove_file_internal(candidate)
}
},
rename_file: fn(from_path, to_path) {
@utils.rename_file_internal(from_path, to_path)
},
}
sink.write(@core.Record::new(@core.Level::Info, "klmnopqrst"))
inspect(sink.rotation_failures(), content="1")
inspect(sink.write_failures(), content="1")
inspect(sink.is_available(), content="false")
inspect(sink.close(), content="false")
ignore(@utils.remove_file_internal(test_path))
ignore(@utils.remove_file_internal(test_path + ".1"))
ignore(@utils.remove_file_internal(test_path + ".2"))
} else {
inspect(native_files_supported(), content="false")
}
}
+11 -8
View File
@@ -8,11 +8,11 @@ test "native file support flag is queryable" {
///|
test "file sink availability reflects backend support" {
let sink = file_sink("bitlogger-test.log")
let sink = file_sink("logs/bitlogger-test.log")
let state = sink.state()
inspect(sink.path(), content="bitlogger-test.log")
inspect(sink.path(), content="logs/bitlogger-test.log")
inspect(sink.is_available() == native_files_supported(), content="true")
inspect(state.path, content="bitlogger-test.log")
inspect(state.path, content="logs/bitlogger-test.log")
inspect(state.available == sink.is_available(), content="true")
inspect(state.append == sink.append_mode(), content="true")
inspect(state.auto_flush == sink.auto_flush_enabled(), content="true")
@@ -42,7 +42,7 @@ test "file sink availability reflects backend support" {
///|
test "file sink unavailable backend keeps stable fallback state" {
let sink = file_sink(
"bitlogger-unavailable.log",
"logs/bitlogger-unavailable.log",
rotation=Some(file_rotation(64, max_backups=2)),
)
if sink.is_available() {
@@ -73,7 +73,10 @@ test "file sink rotation config normalizes invalid inputs" {
let rotation = file_rotation(0, max_backups=0)
inspect(rotation.max_bytes, content="1")
inspect(rotation.max_backups, content="1")
let sink = file_sink("bitlogger-rotation-config.log", rotation=Some(rotation))
let sink = file_sink(
"logs/bitlogger-rotation-config.log",
rotation=Some(rotation),
)
inspect(sink.rotation_enabled(), content="true")
match sink.rotation_config() {
Some(config) => {
@@ -97,7 +100,7 @@ test "file rotation i64 preserves wide threshold metadata" {
///|
test "file sink setters update auto flush and rotation state" {
let sink = file_sink("bitlogger-setters.log")
let sink = file_sink("logs/bitlogger-setters.log")
let default_policy = sink.default_policy()
inspect(sink.append_mode(), content="true")
inspect(sink.auto_flush_enabled(), content="true")
@@ -146,7 +149,7 @@ test "file sink setters update auto flush and rotation state" {
///|
test "file sink reset policy restores configured defaults" {
let sink = file_sink(
"bitlogger-reset-policy.log",
"logs/bitlogger-reset-policy.log",
append=false,
auto_flush=false,
rotation=Some(file_rotation(24, max_backups=3)),
@@ -173,7 +176,7 @@ test "file sink reset policy restores configured defaults" {
///|
test "file sink set policy applies bundled runtime policy" {
let sink = file_sink("bitlogger-set-policy.log")
let sink = file_sink("logs/bitlogger-set-policy.log")
let default_policy = sink.default_policy()
sink.set_policy(
FileSinkPolicy::new(
+2 -2
View File
@@ -55,7 +55,7 @@ fn normalize_style_tag_name(name : String) -> String {
///|
pub fn style_tag_registry() -> StyleTagRegistry {
{ entries: {} }
{ entries: Map([]) }
}
///|
@@ -1015,7 +1015,7 @@ fn level_ansi_code(level : @core.Level) -> String {
///|
fn fields_to_json(fields : Array[@core.Field]) -> Json {
let obj : Map[String, Json] = {}
let obj : Map[String, Json] = Map([])
for item in fields {
obj[item.key] = Json::string(item.value)
}
+5 -2
View File
@@ -164,7 +164,10 @@ test "library logger builder unwrap preserves file runtime helpers" {
let config = LoggerConfig::new(
min_level=Level::Warn,
target="lib.file.same-build",
sink=SinkConfig::new(kind=SinkKind::File, path="lib-file-same-build.log"),
sink=SinkConfig::new(
kind=SinkKind::File,
path="logs/lib-file-same-build.log",
),
queue=Some(QueueConfig::new(2, overflow=QueueOverflowPolicy::DropOldest)),
)
let logger = build_library_logger(config)
@@ -236,7 +239,7 @@ test "library logger builder unwrap preserves file controls" {
target="lib.file.controls.same-build",
sink=SinkConfig::new(
kind=SinkKind::File,
path="lib-file-controls-same-build.log",
path="logs/lib-file-controls-same-build.log",
),
)
let logger = build_library_logger(config)
+4 -4
View File
@@ -30,7 +30,7 @@ test "config subtype json helpers stringify stable shapes" {
stringify_sink_config(
SinkConfig::new(
kind=SinkKind::File,
path="demo.log",
path="logs/demo.log",
append=false,
auto_flush=false,
rotation=Some(file_rotation(128, max_backups=2)),
@@ -40,7 +40,7 @@ test "config subtype json helpers stringify stable shapes" {
),
),
),
content="{\"kind\":\"file\",\"path\":\"demo.log\",\"append\":false,\"auto_flush\":false,\"text_formatter\":{\"show_timestamp\":false,\"show_level\":true,\"show_target\":true,\"show_fields\":true,\"separator\":\" \",\"field_separator\":\" \",\"template\":\"\",\"color_mode\":\"auto\",\"color_support\":\"truecolor\",\"style_markup\":\"full\",\"target_style_markup\":\"disabled\",\"fields_style_markup\":\"disabled\"},\"rotation\":{\"max_bytes\":128,\"max_backups\":2}}",
content="{\"kind\":\"file\",\"path\":\"logs/demo.log\",\"append\":false,\"auto_flush\":false,\"text_formatter\":{\"show_timestamp\":false,\"show_level\":true,\"show_target\":true,\"show_fields\":true,\"separator\":\" \",\"field_separator\":\" \",\"template\":\"\",\"color_mode\":\"auto\",\"color_support\":\"truecolor\",\"style_markup\":\"full\",\"target_style_markup\":\"disabled\",\"fields_style_markup\":\"disabled\"},\"rotation\":{\"max_bytes\":128,\"max_backups\":2}}",
)
inspect(
@json_parser.stringify(
@@ -141,7 +141,7 @@ test "config json helpers export stable structured shapes" {
let sink_json = sink_config_to_json(
SinkConfig::new(
kind=SinkKind::File,
path="demo.log",
path="logs/demo.log",
append=false,
auto_flush=false,
rotation=Some(file_rotation(128, max_backups=2)),
@@ -161,7 +161,7 @@ test "config json helpers export stable structured shapes" {
inspect(sink_obj.get("kind").unwrap().as_string().unwrap(), content="file")
inspect(
sink_obj.get("path").unwrap().as_string().unwrap(),
content="demo.log",
content="logs/demo.log",
)
inspect(sink_obj.get("append").unwrap().as_bool().unwrap(), content="false")
inspect(
+4 -4
View File
@@ -45,7 +45,7 @@ test "logger config parser reads formatter and queue options" {
///|
test "logger config parser reads file rotation options" {
let config = parse_logger_config_text(
"{\"sink\":{\"kind\":\"file\",\"path\":\"bitlogger.log\",\"rotation\":{\"max_bytes\":128,\"max_backups\":3}}}",
"{\"sink\":{\"kind\":\"file\",\"path\":\"logs/bitlogger.log\",\"rotation\":{\"max_bytes\":128,\"max_backups\":3}}}",
)
inspect(
match config.sink.kind {
@@ -54,7 +54,7 @@ test "logger config parser reads file rotation options" {
},
content="File",
)
inspect(config.sink.path, content="bitlogger.log")
inspect(config.sink.path, content="logs/bitlogger.log")
match config.sink.rotation {
Some(rotation) => {
inspect(rotation.max_bytes, content="128")
@@ -68,9 +68,9 @@ test "logger config parser reads file rotation options" {
///|
test "logger config parser prefers max_bytes_i64 when present" {
let config = parse_logger_config_text(
"{\"sink\":{\"kind\":\"file\",\"path\":\"bitlogger.log\",\"rotation\":{\"max_bytes\":128,\"max_bytes_i64\":\"4294967296\",\"max_backups\":3}}}",
"{\"sink\":{\"kind\":\"file\",\"path\":\"logs/bitlogger.log\",\"rotation\":{\"max_bytes\":128,\"max_bytes_i64\":\"4294967296\",\"max_backups\":3}}}",
)
inspect(config.sink.path, content="bitlogger.log")
inspect(config.sink.path, content="logs/bitlogger.log")
match config.sink.rotation {
Some(rotation) => {
inspect(rotation.max_bytes, content="2147483647")
+4 -4
View File
@@ -43,13 +43,13 @@ test "logger config stringify roundtrips file rotation fields" {
LoggerConfig::new(
sink=SinkConfig::new(
kind=SinkKind::File,
path="bitlogger.log",
path="logs/bitlogger.log",
rotation=Some(file_rotation(256, max_backups=2)),
),
),
)
let config = parse_logger_config_text(text)
inspect(config.sink.path, content="bitlogger.log")
inspect(config.sink.path, content="logs/bitlogger.log")
match config.sink.rotation {
Some(rotation) => {
inspect(rotation.max_bytes, content="256")
@@ -66,13 +66,13 @@ test "logger config stringify roundtrips file rotation i64 metadata" {
LoggerConfig::new(
sink=SinkConfig::new(
kind=SinkKind::File,
path="bitlogger.log",
path="logs/bitlogger.log",
rotation=Some(file_rotation_i64(4294967296L, max_backups=2)),
),
),
)
let config = parse_logger_config_text(text)
inspect(config.sink.path, content="bitlogger.log")
inspect(config.sink.path, content="logs/bitlogger.log")
match config.sink.rotation {
Some(rotation) => {
inspect(rotation.max_bytes, content="2147483647")
+1 -1
View File
@@ -147,7 +147,7 @@ test "application logger parse-build matches parsed direct builder behavior" {
///|
test "parsed application logger keeps direct runtime helper surface" {
let logger : ApplicationLogger = parse_and_build_application_logger(
"{\"min_level\":\"warn\",\"target\":\"app.json.helpers\",\"queue\":{\"max_pending\":2,\"overflow\":\"DropOldest\"},\"sink\":{\"kind\":\"file\",\"path\":\"app-json-helpers.log\"}}",
"{\"min_level\":\"warn\",\"target\":\"app.json.helpers\",\"queue\":{\"max_pending\":2,\"overflow\":\"DropOldest\"},\"sink\":{\"kind\":\"file\",\"path\":\"logs/app-json-helpers.log\"}}",
)
logger.error("one")
+2 -2
View File
@@ -143,7 +143,7 @@ test "library logger parse-build unwrap matches parsed direct builder behavior"
///|
test "library logger parse-build unwrap preserves file runtime helpers" {
let raw = "{\"min_level\":\"warn\",\"target\":\"lib.json.file.same-build\",\"queue\":{\"max_pending\":2,\"overflow\":\"DropOldest\"},\"sink\":{\"kind\":\"file\",\"path\":\"lib-json-file-same-build.log\"}}"
let raw = "{\"min_level\":\"warn\",\"target\":\"lib.json.file.same-build\",\"queue\":{\"max_pending\":2,\"overflow\":\"DropOldest\"},\"sink\":{\"kind\":\"file\",\"path\":\"logs/lib-json-file-same-build.log\"}}"
let logger = parse_and_build_library_logger(raw)
let full = logger.to_logger()
let configured = parse_and_build_logger(raw)
@@ -208,7 +208,7 @@ test "library logger parse-build unwrap preserves file runtime helpers" {
///|
test "library logger parse-build unwrap preserves file controls" {
let raw = "{\"min_level\":\"warn\",\"target\":\"lib.json.file.controls.same-build\",\"sink\":{\"kind\":\"file\",\"path\":\"lib-json-file-controls-same-build.log\"}}"
let raw = "{\"min_level\":\"warn\",\"target\":\"lib.json.file.controls.same-build\",\"sink\":{\"kind\":\"file\",\"path\":\"logs/lib-json-file-controls-same-build.log\"}}"
let logger = parse_and_build_library_logger(raw)
let full = logger.to_logger()
let configured = parse_and_build_logger(raw)
+12 -8
View File
@@ -40,7 +40,7 @@ test "logger config presets use expected defaults" {
///|
test "file preset uses file sink defaults" {
let config = file("bitlogger.log")
let config = file("logs/bitlogger.log")
inspect(config.min_level.label(), content="INFO")
inspect(config.target, content="")
inspect(config.timestamp, content="false")
@@ -51,7 +51,7 @@ test "file preset uses file sink defaults" {
},
content="File",
)
inspect(config.sink.path, content="bitlogger.log")
inspect(config.sink.path, content="logs/bitlogger.log")
inspect(config.sink.append, content="true")
inspect(config.sink.auto_flush, content="true")
inspect(config.sink.rotation is None, content="true")
@@ -86,7 +86,7 @@ test "preset helpers compose queue and file rotation without losing config" {
separator=" | ",
)
let config = file(
"service.log",
"logs/service.log",
min_level=Level::Warn,
target="svc.worker",
timestamp=true,
@@ -102,7 +102,7 @@ test "preset helpers compose queue and file rotation without losing config" {
inspect(config.min_level.label(), content="WARN")
inspect(config.target, content="svc.worker")
inspect(config.timestamp, content="true")
inspect(config.sink.path, content="service.log")
inspect(config.sink.path, content="logs/service.log")
inspect(config.sink.append, content="false")
inspect(config.sink.auto_flush, content="false")
inspect(config.sink.text_formatter.show_timestamp, content="false")
@@ -132,11 +132,15 @@ test "preset helpers compose queue and file rotation without losing config" {
///|
test "queue helper preserves file rotation when applied after rotation" {
let config = with_queue(
with_file_rotation(file("service.log", append=false), 256, max_backups=2),
with_file_rotation(
file("logs/service.log", append=false),
256,
max_backups=2,
),
max_pending=8,
overflow=QueueOverflowPolicy::DropNewest,
)
inspect(config.sink.path, content="service.log")
inspect(config.sink.path, content="logs/service.log")
inspect(config.sink.append, content="false")
match config.queue {
Some(queue) => {
@@ -163,11 +167,11 @@ test "queue helper preserves file rotation when applied after rotation" {
///|
test "file rotation i64 helper preserves file preset shape" {
let config = with_file_rotation_i64(
with_queue(file("service.log", append=false), max_pending=8),
with_queue(file("logs/service.log", append=false), max_pending=8),
4294967296L,
max_backups=2,
)
inspect(config.sink.path, content="service.log")
inspect(config.sink.path, content="logs/service.log")
inspect(config.sink.append, content="false")
match config.sink.rotation {
Some(rotation) => {
+11 -11
View File
@@ -29,7 +29,7 @@ test "file state json helpers stringify stable snapshots" {
let plain = file_sink_state_to_json(
FileSinkState::new(
"demo.log",
"logs/demo.log",
available=true,
append=false,
auto_flush=true,
@@ -48,11 +48,11 @@ test "file state json helpers stringify stable snapshots" {
.unwrap()
inspect(
@json_parser.stringify(plain),
content="{\"path\":\"demo.log\",\"available\":true,\"append\":false,\"auto_flush\":true,\"open_failures\":1,\"write_failures\":2,\"flush_failures\":3,\"rotation_failures\":4,\"rotation\":{\"max_bytes\":64,\"max_backups\":2}}",
content="{\"path\":\"logs/demo.log\",\"available\":true,\"append\":false,\"auto_flush\":true,\"open_failures\":1,\"write_failures\":2,\"flush_failures\":3,\"rotation_failures\":4,\"rotation\":{\"max_bytes\":64,\"max_backups\":2}}",
)
inspect(
plain_obj.get("path").unwrap().as_string().unwrap(),
content="demo.log",
content="logs/demo.log",
)
inspect(
plain_obj.get("available").unwrap().as_bool().unwrap(),
@@ -90,7 +90,7 @@ test "file state json helpers stringify stable snapshots" {
let wide = file_sink_state_to_json(
FileSinkState::new(
"wide.log",
"logs/wide.log",
available=true,
append=true,
auto_flush=false,
@@ -132,7 +132,7 @@ test "file state json helpers stringify stable snapshots" {
inspect(
stringify_file_sink_state(
FileSinkState::new(
"plain.log",
"logs/plain.log",
available=false,
append=true,
auto_flush=false,
@@ -143,7 +143,7 @@ test "file state json helpers stringify stable snapshots" {
rotation_failures=0,
),
),
content="{\"path\":\"plain.log\",\"available\":false,\"append\":true,\"auto_flush\":false,\"open_failures\":0,\"write_failures\":0,\"flush_failures\":0,\"rotation_failures\":0,\"rotation\":null}",
content="{\"path\":\"logs/plain.log\",\"available\":false,\"append\":true,\"auto_flush\":false,\"open_failures\":0,\"write_failures\":0,\"flush_failures\":0,\"rotation_failures\":0,\"rotation\":null}",
)
}
@@ -152,7 +152,7 @@ test "runtime file state json helpers stringify queue snapshots" {
let runtime_json = runtime_file_state_to_json(
RuntimeFileState::new(
FileSinkState::new(
"queue.log",
"logs/queue.log",
available=true,
append=true,
auto_flush=false,
@@ -180,7 +180,7 @@ test "runtime file state json helpers stringify queue snapshots" {
)
inspect(
runtime_file_obj.get("path").unwrap().as_string().unwrap(),
content="queue.log",
content="logs/queue.log",
)
inspect(
runtime_file_obj.get("available").unwrap().as_bool().unwrap(),
@@ -194,7 +194,7 @@ test "runtime file state json helpers stringify queue snapshots" {
let json = stringify_runtime_file_state(
RuntimeFileState::new(
FileSinkState::new(
"queue.log",
"logs/queue.log",
available=true,
append=true,
auto_flush=false,
@@ -211,13 +211,13 @@ test "runtime file state json helpers stringify queue snapshots" {
)
inspect(
json,
content="{\"file\":{\"path\":\"queue.log\",\"available\":true,\"append\":true,\"auto_flush\":false,\"open_failures\":0,\"write_failures\":1,\"flush_failures\":2,\"rotation_failures\":3,\"rotation\":null},\"queued\":true,\"pending_count\":7,\"dropped_count\":5}",
content="{\"file\":{\"path\":\"logs/queue.log\",\"available\":true,\"append\":true,\"auto_flush\":false,\"open_failures\":0,\"write_failures\":1,\"flush_failures\":2,\"rotation_failures\":3,\"rotation\":null},\"queued\":true,\"pending_count\":7,\"dropped_count\":5}",
)
let wide_runtime_json = runtime_file_state_to_json(
RuntimeFileState::new(
FileSinkState::new(
"wide-queue.log",
"logs/wide-queue.log",
available=true,
append=true,
auto_flush=true,
+2 -2
View File
@@ -16,7 +16,7 @@ test "configured logger close stays aligned with runtime sink close" {
let file_config = LoggerConfig::new(
target="config.close.file",
sink=SinkConfig::new(kind=SinkKind::File, path="config-close.log"),
sink=SinkConfig::new(kind=SinkKind::File, path="logs/config-close.log"),
)
let file_logger = build_logger(file_config)
let file_sink = build_logger(file_config).sink
@@ -110,7 +110,7 @@ test "configured queued console close drains pending queue" {
test "runtime sink queued file close drains queue before teardown" {
let sink = RuntimeSink::QueuedFile(
queued_sink(
file_sink("runtime-close-queued.log", auto_flush=false),
file_sink("logs/runtime-close-queued.log", auto_flush=false),
max_pending=4,
),
)
+2 -2
View File
@@ -1,6 +1,6 @@
///|
test "runtime sink plain file reopen helpers and failure resets work directly" {
let sink = RuntimeSink::File(file_sink("runtime-reopen-direct.log"))
let sink = RuntimeSink::File(file_sink("logs/runtime-reopen-direct.log"))
if sink.file_available() {
inspect(sink.close(), content="true")
inspect(sink.file_reopen_append(), content="true")
@@ -52,7 +52,7 @@ test "runtime sink plain file reopen helpers and failure resets work directly" {
///|
test "runtime sink queued file reopen helpers and failure resets work directly" {
let sink = RuntimeSink::QueuedFile(
queued_sink(file_sink("runtime-reopen-queued.log"), max_pending=2),
queued_sink(file_sink("logs/runtime-reopen-queued.log"), max_pending=2),
)
if sink.file_available() {
inspect(sink.file_close(), content="true")
+11 -11
View File
@@ -7,7 +7,7 @@ test "runtime sink plain variants use documented fallback counts" {
inspect(console_sink.drain(), content="0")
let file_sink = RuntimeSink::File(
file_sink("runtime-direct.log", auto_flush=false),
file_sink("logs/runtime-direct.log", auto_flush=false),
)
inspect(file_sink.pending_count(), content="0")
inspect(file_sink.dropped_count(), content="0")
@@ -81,17 +81,17 @@ test "runtime sink non-file variants expose documented file fallbacks" {
test "runtime sink plain file helpers expose direct file state and policy" {
let sink = RuntimeSink::File(
file_sink(
"runtime-file-helpers.log",
"logs/runtime-file-helpers.log",
auto_flush=false,
rotation=Some(file_rotation(40, max_backups=2)),
),
)
inspect(sink.file_available() == native_files_supported(), content="true")
match sink.file_path_or_none() {
Some(path) => inspect(path, content="runtime-file-helpers.log")
Some(path) => inspect(path, content="logs/runtime-file-helpers.log")
None => inspect(false, content="true")
}
inspect(sink.file_path(), content="runtime-file-helpers.log")
inspect(sink.file_path(), content="logs/runtime-file-helpers.log")
inspect(sink.file_append_mode(), content="true")
inspect(sink.file_auto_flush(), content="false")
inspect(sink.file_rotation_enabled(), content="true")
@@ -125,14 +125,14 @@ test "runtime sink plain file helpers expose direct file state and policy" {
}
match sink.file_state_or_none() {
Some(snapshot) => {
inspect(snapshot.path, content="runtime-file-helpers.log")
inspect(snapshot.path, content="logs/runtime-file-helpers.log")
inspect(snapshot.append, content="true")
inspect(snapshot.auto_flush, content="false")
}
None => inspect(false, content="true")
}
let state = sink.file_state()
inspect(state.path, content="runtime-file-helpers.log")
inspect(state.path, content="logs/runtime-file-helpers.log")
inspect(state.available == sink.file_available(), content="true")
inspect(state.append, content="true")
inspect(state.auto_flush, content="false")
@@ -141,7 +141,7 @@ test "runtime sink plain file helpers expose direct file state and policy" {
inspect(snapshot.queued, content="false")
inspect(snapshot.pending_count, content="0")
inspect(snapshot.dropped_count, content="0")
inspect(snapshot.file.path, content="runtime-file-helpers.log")
inspect(snapshot.file.path, content="logs/runtime-file-helpers.log")
}
None => inspect(false, content="true")
}
@@ -170,7 +170,7 @@ test "runtime sink plain file helpers expose direct file state and policy" {
test "runtime sink queued file helpers preserve queue-aware file runtime state" {
let sink = RuntimeSink::QueuedFile(
queued_sink(
file_sink("runtime-queued-file.log", auto_flush=false),
file_sink("logs/runtime-queued-file.log", auto_flush=false),
max_pending=2,
overflow=QueueOverflowPolicy::DropNewest,
),
@@ -185,10 +185,10 @@ test "runtime sink queued file helpers preserve queue-aware file runtime state"
logger.info("three")
inspect(sink.file_available() == native_files_supported(), content="true")
match sink.file_path_or_none() {
Some(path) => inspect(path, content="runtime-queued-file.log")
Some(path) => inspect(path, content="logs/runtime-queued-file.log")
None => inspect(false, content="true")
}
inspect(sink.file_path(), content="runtime-queued-file.log")
inspect(sink.file_path(), content="logs/runtime-queued-file.log")
inspect(sink.file_policy_or_none() is None, content="false")
inspect(sink.file_default_policy_or_none() is None, content="false")
inspect(sink.file_state_or_none() is None, content="false")
@@ -199,7 +199,7 @@ test "runtime sink queued file helpers preserve queue-aware file runtime state"
inspect(snapshot.queued, content="true")
inspect(snapshot.pending_count, content="2")
inspect(snapshot.dropped_count, content="1")
inspect(snapshot.file.path, content="runtime-queued-file.log")
inspect(snapshot.file.path, content="logs/runtime-queued-file.log")
}
None => inspect(false, content="true")
}
+1 -1
View File
@@ -111,7 +111,7 @@ test "runtime sink progress helpers expose queue and plain file steps separately
inspect(queued_progress.file_backed, content="false")
let file_sink = RuntimeSink::File(
file_sink("runtime-progress-direct.log", auto_flush=false),
file_sink("logs/runtime-progress-direct.log", auto_flush=false),
)
let file_progress = file_sink.flush_progress()
inspect(file_progress.queue_advanced_count, content="0")
+6
View File
@@ -12,6 +12,12 @@ pub fn open_file_handle_internal(path : String, append : Bool) -> FileHandle? {
None
}
///|
pub fn file_exists_internal(path : String) -> Bool {
ignore(path)
false
}
///|
pub fn write_file_handle_internal(
handle : FileHandle,
+2
View File
@@ -25,6 +25,8 @@ pub fn default_style_tag_registry() -> @formatting.StyleTagRegistry
pub fn field_equals(String, String) -> (@core.Record) -> Bool
pub fn file_exists_internal(String) -> Bool
pub fn file_size_i64_internal(FileHandle) -> Int64
pub fn flush_file_handle_internal(FileHandle) -> Bool
-1
View File
@@ -1 +0,0 @@
trueVersion = "1.0.0"