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) - [English README](./docs/README-en.md)
- [Wiki](https://bitlogger.naloveyuki.top) - [Wiki](https://bitlogger.naloveyuki.top)
## 介绍 ## 从第一个日志到扩展能力
BitLogger 提供统一的日志级别、目标名、结构化字段和可定制格式,既可以直接输出到控制台,也可以在 native 环境写入文件,并提供异步日志版本 BitLogger 的文档按使用路径组织:先完成一个可运行的日志输出,再按需进入文件、配置、异步和组合能力。API 文档保留为精确参考,不需要从 API 文件名开始阅读
## 快速开始 ### 1. 安装
```moonbit 从一个空项目开始:
let logger = build_logger(
text_console(
min_level=Level::Info,
target="demo",
text_formatter=TextFormatterConfig::new(show_timestamp=false, separator=" | "),
),
)
logger.info("starting", fields=[field("port", "8080")]) ```bash
ignore(logger.flush()) moon new log-demo
cd log-demo
moon add Nanaloveyuki/BitLogger@0.7.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 - 组合能力:queue、filter、patch、fanout、split、callback
- 异步日志:独立 `src-async` package - 异步日志:独立 `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) - [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({ export default defineConfig({
title: 'BitLogger', title: 'BitLogger',
description: 'Structured logging library docs for MoonBit.', description: 'Structured logging library docs for MoonBit.',
@@ -114,29 +225,18 @@ export default defineConfig({
markdown: { markdown: {
languages: [createMoonbitLanguageRegistration()], languages: [createMoonbitLanguageRegistration()],
}, },
themeConfig: { themeConfig: englishThemeConfig,
siteTitle: 'BitLogger', locales: {
nav: [ root: {
{ text: 'Home', link: '/' }, label: 'English',
{ text: 'API', link: '/api/' }, lang: 'en-US',
{ text: 'Changes', link: '/changes/' }, themeConfig: englishThemeConfig,
{ text: 'Mooncake', link: 'https://mooncakes.io/docs/Nanaloveyuki/BitLogger' },
],
search: {
provider: 'local',
}, },
socialLinks: [{ icon: 'github', link: repository }], zh: {
editLink: { label: '简体中文',
pattern: `${repository}/edit/main/docs/:path`, lang: 'zh-CN',
text: 'Edit this page on GitHub', link: '/zh/',
}, themeConfig: chineseThemeConfig,
sidebar: {
'/api/': buildApiSidebar(),
'/changes/': buildChangesSidebar(),
},
footer: {
message: 'Published from the repository docs folder with VitePress.',
copyright: 'MIT',
}, },
}, },
}) })
+36 -1
View File
@@ -6,11 +6,46 @@ import HomeDocHub from './components/HomeDocHub.vue'
import './custom.css' import './custom.css'
const localePreferenceKey = 'bitlogger-docs-locale'
function routePath(path: string): string {
return path.split(/[?#]/, 1)[0]
}
function isLocaleRoot(path: string): boolean {
const normalized = routePath(path)
return normalized === '/' || normalized === '/zh/'
}
function browserPrefersChinese(): boolean {
return navigator.languages.some(language => language.toLowerCase().startsWith('zh'))
}
const theme: Theme = { const theme: Theme = {
extends: DefaultTheme, extends: DefaultTheme,
enhanceApp({ app }) { enhanceApp({ app, router }) {
app.component('ApiOverview', ApiOverview) app.component('ApiOverview', ApiOverview)
app.component('HomeDocHub', HomeDocHub) app.component('HomeDocHub', HomeDocHub)
if (typeof window === 'undefined') return
router.onAfterRouteChange = to => {
if (!isLocaleRoot(to)) return
window.localStorage.setItem(localePreferenceKey, routePath(to) === '/zh/' ? 'zh' : 'en')
}
if (router.route.path !== '/') return
const savedLocale = window.localStorage.getItem(localePreferenceKey)
const locale = savedLocale === 'zh' || savedLocale === 'en'
? savedLocale
: browserPrefersChinese()
? 'zh'
: 'en'
if (locale === 'zh') {
void router.go('/zh/')
}
}, },
} }
+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. BitLogger gives you consistent levels, targets, structured fields, configurable text output, native file logging, and an async logging layer.
## Quick Start ## Start With A Working Flow
```moonbit The documentation is organized around complete usage flows. Start with an executable logger, then move into file output, configuration, async lifecycle, and extensions. The API reference remains the precise symbol-level reference.
let logger = build_logger(
text_console(
min_level=Level::Info,
target="demo",
text_formatter=TextFormatterConfig::new(show_timestamp=false, separator=" | "),
),
)
logger.info("starting", fields=[field("port", "8080")]) ### 1. Install
ignore(logger.flush())
```bash
moon new log-demo
cd log-demo
moon add Nanaloveyuki/BitLogger@0.7.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 ## 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 - Composition helpers: queue, filter, patch, fanout, split, callback
- Separate async package under `src-async` - 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 ## Documentation
- [Examples](./examples/index.md): complete application-facing usage flows
- [Extend](./extend/index.md): queueing, sink composition, formatting, and target boundaries
- [API index](./api/index.md): canonical API reference, organized as one public API per file - [API index](./api/index.md): canonical API reference, organized as one public API per file
- [src package README](https://github.com/Nanaloveyuki/BitLogger/blob/main/src/README.mbt.md): package-level usage notes and target reminders - [src package README](https://github.com/Nanaloveyuki/BitLogger/blob/main/src/README.mbt.md): package-level usage notes and target reminders
- [`docs/changes/`](./changes/): versioned release notes and publish-facing change summaries - [`docs/changes/`](./changes/): versioned release notes and publish-facing change summaries
+2 -1
View File
@@ -2,7 +2,7 @@
name: file-sink-reopen-append name: file-sink-reopen-append
group: api group: api
category: sink category: sink
update-time: 20260613 update-time: 20260717
description: Reopen a FileSink in append mode. description: Reopen a FileSink in append mode.
key-word: key-word:
- file - file
@@ -37,6 +37,7 @@ Detailed rules explaining key parameters and behaviors
- Reopen behavior is fixed to append mode. - Reopen behavior is fixed to append mode.
- The stored append policy is updated to `true` as part of the reopen path. - The stored append policy is updated to `true` as part of the reopen path.
- On failure, the sink remains unavailable and `open_failures` is incremented. - On failure, the sink remains unavailable and `open_failures` is incremented.
- After a rotation failure, this is the explicit recovery path once the underlying remove, rename, or open condition has been resolved.
### How to Use ### How to Use
+4 -1
View File
@@ -2,7 +2,7 @@
name: file-sink-rotation-failures name: file-sink-rotation-failures
group: api group: api
category: sink category: sink
update-time: 20260707 update-time: 20260717
description: Read the number of rotation failures recorded by a FileSink. description: Read the number of rotation failures recorded by a FileSink.
key-word: key-word:
- file - file
@@ -36,6 +36,7 @@ Detailed rules explaining key parameters and behaviors
- The sink reports its recorded rotation-failure count directly. - The sink reports its recorded rotation-failure count directly.
- The counter increases when a rotation attempt cannot complete its critical file steps, such as removing the active file with zero backups, renaming a live file into `.1`, shifting an existing backup to a higher slot, or reopening the fresh active file afterward. - The counter increases when a rotation attempt cannot complete its critical file steps, such as removing the active file with zero backups, renaming a live file into `.1`, shifting an existing backup to a higher slot, or reopening the fresh active file afterward.
- Missing optional backup slots do not count by themselves; only steps that fail while their source path still exists are treated as rotation failures. - Missing optional backup slots do not count by themselves; only steps that fail while their source path still exists are treated as rotation failures.
- When a critical rotation step fails, the triggering record is not written and the sink remains unavailable until an explicit reopen succeeds.
- The counter is cumulative until reset. - The counter is cumulative until reset.
- This helper is observation-only and does not mutate sink state. - This helper is observation-only and does not mutate sink state.
@@ -70,6 +71,8 @@ e.g.:
- This counter alone does not say whether the sink is currently available. - This counter alone does not say whether the sink is currently available.
- After a recorded rotation failure, use `reopen_append()` only after the underlying filesystem condition has been resolved.
### Notes ### Notes
1. Use this helper when diagnosing incomplete direct file rotations. 1. Use this helper when diagnosing incomplete direct file rotations.
+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. 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.1.0](./1.1.0(0.7.0).md)
- [1.0.1](./1.0.1(0.6.1).md) - [1.0.1](./1.0.1(0.6.1).md)
- [1.0.0](./1.0.0(0.6.0).md) - [1.0.0](./1.0.0(0.6.0).md)
+47
View File
@@ -0,0 +1,47 @@
# Async Logger Lifecycle
Use the async package when a native application needs logging to run through an async queue. The library supports broader targets, but the shipped `async fn main` example is native-focused.
## Import Both Packages
```moonbit
import {
"Nanaloveyuki/BitLogger/src" @log,
"Nanaloveyuki/BitLogger/src-async" @async_log,
"moonbitlang/async",
}
```
## Build, Run, Then Shut Down
```moonbit
async fn main {
let raw = "{\"logger\":{\"min_level\":\"info\",\"target\":\"service.async\",\"sink\":{\"kind\":\"text_console\"}},\"async_config\":{\"max_pending\":64,\"overflow\":\"DropOldest\",\"max_batch\":16,\"linger_ms\":5,\"flush\":\"Batch\"}}"
let config = @async_log.parse_async_logger_build_config_text(raw) catch {
err => {
ignore(err)
return
}
}
let logger = @async_log.build_async_logger(config)
@async.with_task_group(group => {
group.spawn_bg(allow_failure=true, () => logger.run())
logger.info("worker started", fields=[@log.field("mode", "async")])
logger.shutdown()
})
}
```
`run()` owns the worker loop. Start it before producing records, then call `shutdown()` so pending work can follow the configured flush policy.
## Run The Repository Example
```bash
moon run examples/async_basic --target native
```
## Next Steps
- Inspect queue policy and flush choices in [Queueing](../extend/queue.md).
- Reference: [`parse_async_logger_build_config_text(...)`](../api/parse-async-logger-build-config-text.md), [`build_async_logger(...)`](../api/build-async-logger.md), [`run()`](../api/async-logger-run.md), and [`shutdown()`](../api/async-logger-shutdown.md).
+35
View File
@@ -0,0 +1,35 @@
# Configuration-Driven Logging
Use configuration when deployment should select levels, output, formatting, or queue behavior without changing application code.
## Parse, Build, Then Log
```moonbit
let raw = "{\"min_level\":\"info\",\"target\":\"service\",\"sink\":{\"kind\":\"text_console\",\"text_formatter\":{\"show_timestamp\":false,\"separator\":\" | \"}},\"queue\":{\"max_pending\":64,\"overflow\":\"DropOldest\"}}"
let config = @log.parse_logger_config_text(raw) catch {
err => {
ignore(err)
println("invalid logger configuration")
return
}
}
let logger = @log.build_logger(config)
logger.info("configured logger ready")
ignore(logger.flush())
```
Parsing validates the data before runtime construction. Keep the raw configuration outside application code in a real deployment, but handle parsing errors at the boundary where configuration enters the process.
## Run The Repository Example
```bash
moon run examples/config_build
```
## Next Steps
- Use [Queueing](../extend/queue.md) to choose overflow behavior deliberately.
- Use [Async lifecycle](./async.md) when the queue needs a background worker.
- Reference: [`parse_logger_config_text(...)`](../api/parse-logger-config-text.md), [`build_logger(...)`](../api/build-logger.md), and [`with_queue(...)`](../api/with-queue.md).
+51
View File
@@ -0,0 +1,51 @@
# Console And Structured Fields
Use this flow for command-line tools and services that need a human-readable starting point. A record always has a level, target, message, and optional fields.
## Install And Import
```bash
moon add Nanaloveyuki/BitLogger@0.7.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: hero:
name: BitLogger name: BitLogger
text: Structured logging for MoonBit text: Structured logging for MoonBit
tagline: API reference, examples, and release notes for BitLogger. tagline: Start with a runnable logging flow, then extend it with precise API reference.
actions: actions:
- theme: brand - theme: brand
text: Open API Reference text: Start Examples
link: /api/ link: /examples/
- theme: alt - theme: alt
text: Read Release Notes text: Extend A Logger
link: /changes/ link: /extend/
- theme: alt - theme: alt
text: Mooncake Package text: Mooncake Package
link: https://mooncakes.io/docs/Nanaloveyuki/BitLogger link: https://mooncakes.io/docs/Nanaloveyuki/BitLogger
features: features:
- title: Structured logging - title: Examples first
details: Levels, targets, message fields, and configurable text or JSON output. details: Follow complete console, file, config, and async flows before looking up individual symbols.
- title: Sync and async paths - title: Extend deliberately
details: API reference covers both the main package and the async package surface. details: Add queues, custom sink graphs, formatting, and target-specific behavior only when the base flow needs them.
- title: Config and runtime helpers - title: API as reference
details: Browse builders, presets, file helpers, queue helpers, and verification notes quickly. details: Use the one-symbol-per-page API reference for exact signatures, contracts, and edge cases.
--- ---
## Start Here ## Start Here
<HomeDocHub /> 1. Read [Examples](./examples/index.md) for an end-to-end path from installation to a running logger.
2. Read [Extend](./extend/index.md) when the base console flow needs queueing, custom routing, formatting, files, or target-aware behavior.
- Use [API Reference](./api/index.md) when you want the canonical one-file-per-public-API docs. 3. Use [API Reference](./api/index.md) for exact contracts and [Release Notes](./changes/index.md) for version history.
- Use [Release Notes](./changes/index.md) when you want version-scoped changes and publish-facing summaries.
- Use [Chinese README](https://github.com/Nanaloveyuki/BitLogger/blob/main/README.md) or [English README](./README-en.md) when you want a shorter project overview first.
+50
View File
@@ -0,0 +1,50 @@
# 异步生命周期 API
异步 API 从 combined config 构建 Logger,再以单 worker 生命周期处理待写入记录。可执行 `async fn main` 仍应按目标平台边界使用。
## 解析与构建
```moonbit
pub fn parse_async_logger_build_config_text(
input : String,
) -> AsyncLoggerBuildConfig raise
pub fn build_async_logger(
config : AsyncLoggerBuildConfig,
) -> AsyncLogger[RuntimeSink]
```
解析函数同时验证 `logger``async_config` 两部分;缺少任一部分时使用对应默认配置。构建函数先复用同步 `build_logger(config.logger)` 路径,再包裹异步层,因此保留同步 sink、文件与可选同步队列的运行时语义。
```moonbit
let config = @async_log.parse_async_logger_build_config_text(raw) catch {
err => { ignore(err); return }
}
let logger = @async_log.build_async_logger(config)
```
## `run()`
```moonbit
pub async fn AsyncLogger::run(self : AsyncLogger[S]) -> Unit
```
启动 drain worker。单个 Logger 只能同时拥有一个 worker;已经运行时再次调用会失败,关闭后的 Logger 也不能重新启动。worker 正常在队列关闭时退出;worker 失败时会记录状态并将错误向上传递。
## `shutdown()`
```moonbit
pub async fn AsyncLogger::shutdown(
self : AsyncLogger[S],
clear? : Bool = false,
) -> Unit
```
`clear=false` 先等待 idle 再关闭;`clear=true` 立即关闭并放弃待处理记录。两条路径都会等待活动 worker 退出。没有运行 worker 且仍有待处理记录时,`clear=false` 可能无法推进,因此应用应保证先启动 `run()`
## 英文原始 API
- [`parse_async_logger_build_config_text(...)`](../../api/parse-async-logger-build-config-text.md)
- [`build_async_logger(...)`](../../api/build-async-logger.md)
- [`run()`](../../api/async-logger-run.md)
- [`shutdown()`](../../api/async-logger-shutdown.md)
+55
View File
@@ -0,0 +1,55 @@
# 基础记录 API
控制台示例依赖以下四个入口:先用 preset 描述输出,再由 `build_logger(...)` 构建运行时 Logger,最后用 `field(...)` 附加结构化上下文。
## `console(...)` 与 `json_console(...)`
```moonbit
pub fn console(
min_level~ : Level = Level::Info,
target~ : String = "",
timestamp~ : Bool = false,
) -> LoggerConfig
pub fn json_console(
min_level~ : Level = Level::Info,
target~ : String = "",
timestamp~ : Bool = false,
) -> LoggerConfig
```
两者都只生成 `LoggerConfig`,不会直接创建运行时 Logger。`console(...)` 适合人读输出,`json_console(...)` 适合按行采集的机器读取输出;默认都不带队列。
## `field(...)`
```moonbit
pub fn field(key : String, value : String) -> Field
```
创建一个结构化字段。它保留原始 key/value,不做去重、归一化或校验;应使用稳定字段名,便于下游筛选。
```moonbit
logger.info("accepted", fields=[@log.field("user", "alice")])
```
## `build_logger(...)`
```moonbit
pub fn build_logger(config : LoggerConfig) -> ConfiguredLogger
```
这是同步 config 到运行时的桥梁:它先按 `config.sink` 构建 `RuntimeSink`,再应用可选的 `config.queue`。返回值仍保留普通日志方法,以及适用时的队列和文件控制方法。
```moonbit
let logger = @log.build_logger(
@log.console(min_level=@log.Level::Info, target="service"),
)
logger.info("ready", fields=[@log.field("port", "8080")])
```
## 英文原始 API
- [`console(...)`](../../api/console.md)
- [`json_console(...)`](../../api/json-console.md)
- [`field(...)`](../../api/field.md)
- [`build_logger(...)`](../../api/build-logger.md)
+52
View File
@@ -0,0 +1,52 @@
# Sink 组合 API
当预设输出不足以表达路由规则时,使用 typed `Logger::new(...)` 和 sink 组合函数。它们接收完整 `Record`,因此仍保留 level、target、message 与 fields。
## `Logger::new(...)`
```moonbit
pub fn Logger::new(
sink : S,
min_level~ : Level = Level::Info,
target~ : String = "",
) -> Logger[S]
```
从任意 `Sink` 实现创建 typed Logger。`min_level` 在任何 sink 写入前生效;`target` 是默认值,之后可由 `with_target(...)``child(...)` 或单次 `log(..., target=...)` 覆盖。
## `fanout_sink(...)`
```moonbit
pub fn fanout_sink(left : A, right : B) -> FanoutSink[A, B]
```
每条记录写入两个目的地,适合控制台加 JSON,或控制台加 callback。右侧获得独立记录副本,两个写入在记录值层面互不影响。
## `split_by_level(...)`
```moonbit
pub fn split_by_level(
left : A,
right : B,
min_level~ : Level = Level::Warn,
) -> SplitSink[A, B]
```
达到阈值的记录进入 `left`,低于阈值的记录进入 `right`。典型用法是把 warning/error 单独输出,而 info/debug 留在普通控制台。
## `callback_sink(...)`
```moonbit
pub fn callback_sink(
callback : (Record) -> Unit,
) -> CallbackSink
```
将完整结构化 `Record` 交给应用回调。适合测试、自定义桥接与集成;它不自动格式化记录。
## 英文原始 API
- [`Logger::new(...)`](../../api/logger-new.md)
- [`fanout_sink(...)`](../../api/fanout-sink.md)
- [`split_by_level(...)`](../../api/split-by-level.md)
- [`callback_sink(...)`](../../api/callback-sink.md)
+53
View File
@@ -0,0 +1,53 @@
# 配置与队列 API
这一组 API 将外部 JSON 或 typed config 变成同步 Logger,并明确规定满队列和关闭时的处理方式。
## `parse_logger_config_text(...)`
```moonbit
pub fn parse_logger_config_text(input : String) -> LoggerConfig raise ConfigError
```
解析并验证 JSON,但不构建运行时 Logger。缺省字段使用构造器默认值;无效 JSON、错误类型、未知枚举值或空文件路径都会抛出 `ConfigError`
```moonbit
let config = @log.parse_logger_config_text(raw) catch {
err => { ignore(err); return }
}
let logger = @log.build_logger(config)
```
## `with_queue(...)`、`QueueConfig` 与溢出策略
```moonbit
pub fn with_queue(
config : LoggerConfig,
max_pending~ : Int = 0,
overflow~ : QueueOverflowPolicy = QueueOverflowPolicy::DropNewest,
) -> LoggerConfig
pub fn QueueConfig::new(
max_pending : Int,
overflow~ : QueueOverflowPolicy = QueueOverflowPolicy::DropNewest,
) -> QueueConfig
```
`with_queue(...)` 是 preset 组合入口;`QueueConfig::new(...)` 用于手写 `LoggerConfig`。两者配置同步 `QueuedSink`,不是异步包的 worker 队列。
`DropNewest` 丢弃新到记录,`DropOldest` 丢弃最早待处理记录。选择取决于业务更需要保留早期还是最新状态。
## `flush()`
```moonbit
pub fn ConfiguredLogger::flush(self : ConfiguredLogger) -> Bool
```
在退出或需要确认输出边界时调用。对于队列和文件 sink,它会走对应运行时 flush 路径;返回值表示该路径是否成功。
## 英文原始 API
- [`parse_logger_config_text(...)`](../../api/parse-logger-config-text.md)
- [`with_queue(...)`](../../api/with-queue.md)
- [`QueueConfig`](../../api/queue-config.md)
- [`QueueOverflowPolicy`](../../api/queue-overflow-policy.md)
- [`flush()`](../../api/configured-logger-flush.md)
+49
View File
@@ -0,0 +1,49 @@
# 文件输出 API
文件输出是 native 能力。先检查能力,再创建文件配置,最后在有序退出时 flush 并关闭。
## `native_files_supported()`
```moonbit
pub fn native_files_supported() -> Bool
```
native 后端当前返回 `true`,非文件能力后端返回 `false`。这是运行时能力检查,不保证后续文件操作一定成功,路径、权限和文件系统状态仍可能导致失败。
## `file(...)` 与 `with_file_rotation(...)`
```moonbit
pub fn file(
path : String,
min_level~ : Level = Level::Info,
target~ : String = "",
timestamp~ : Bool = false,
append~ : Bool = true,
auto_flush~ : Bool = true,
rotation~ : FileRotation? = None,
text_formatter~ : TextFormatterConfig = default_text_formatter_config(),
) -> LoggerConfig raise ConfigError
pub fn with_file_rotation(
config : LoggerConfig,
max_bytes : Int,
max_backups~ : Int = 1,
) -> LoggerConfig
```
`file(...)` 要求非空路径。`with_file_rotation(...)` 只对 file config 生效;输入不是文件 sink 时原样返回。`max_bytes` 是活动文件上限,`max_backups` 是保留的轮转备份数。
## `file_close()`
```moonbit
pub fn ConfiguredLogger::file_close(self : ConfiguredLogger) -> Bool
```
关闭 config 构建的文件 sink。队列文件 sink 会依次 drain、flush、close;只有整条路径成功且期间没有新的写入、flush 或轮转失败时才返回 `true`。非文件 sink 返回 `false`
## 英文原始 API
- [`native_files_supported()`](../../api/native-files-supported.md)
- [`file(...)`](../../api/file.md)
- [`with_file_rotation(...)`](../../api/with-file-rotation.md)
- [`file_close()`](../../api/configured-logger-file-close.md)
+40
View File
@@ -0,0 +1,40 @@
# 文本格式 API
文本格式只改变呈现,不改变 `Record` 的结构。需要稳定机器消费时,优先使用 `json_console(...)`
## `text_console(...)`
```moonbit
pub fn text_console(
min_level~ : Level = Level::Info,
target~ : String = "",
timestamp~ : Bool = false,
text_formatter~ : TextFormatterConfig = default_text_formatter_config(),
) -> LoggerConfig
```
生成 `SinkKind::TextConsole` 配置,并复制 formatter 配置。和其他 preset 一样,它不直接构建 Logger,也默认不带队列。
## `TextFormatterConfig` 与 `text_formatter(...)`
`TextFormatterConfig::new(...)` 用于 config-first 路径,控制 timestamp、level、target、fields、分隔符与 template。`text_formatter(...)` 构建直接传给 `text_console_sink(...)` 的运行时 formatter。
```moonbit
let config = @log.text_console(
text_formatter=@log.TextFormatterConfig::new(
show_timestamp=false,
template="[{level}] {target} {message} :: {fields}",
),
)
```
## `ColorMode`
`ColorMode` 控制文本 formatter 的颜色策略。终端样式只服务可读性;业务语义仍应放在 level、target 和 fields 中。
## 英文原始 API
- [`text_console(...)`](../../api/text-console.md)
- [`TextFormatterConfig`](../../api/text-formatter-config.md)
- [`text_formatter(...)`](../../api/text-formatter.md)
- [`ColorMode`](../../api/color-mode.md)
+14
View File
@@ -0,0 +1,14 @@
# 高频 API
这一层只覆盖 Examples 和 Extend 中直接使用、或应用启动时高频需要的 API。它按使用主题归纳,避免把阅读路径重新拆回一函数一页。
| 场景 | 中文说明 | 英文精确参考 |
| --- | --- | --- |
| 第一条结构化日志 | [基础记录](./basics.md) | [完整 API 索引](../../api/index.md) |
| 配置、队列与 flush | [配置与队列](./configuration.md) | [config 分类](../../api/index.md) |
| native 文件输出 | [文件输出](./file-output.md) | [file/sink 分类](../../api/index.md) |
| 终端可读性 | [文本格式](./formatting.md) | [formatter 分类](../../api/index.md) |
| 后台异步处理 | [异步生命周期](./async.md) | [async 分类](../../api/index.md) |
| 多目的地与按 level 路由 | [Sink 组合](./composition.md) | [sink 分类](../../api/index.md) |
每个中文页面保留高频签名与行为边界;需要全部参数、错误条件或较少使用的公开符号时,沿页面内链接进入英文原始 API。
+47
View File
@@ -0,0 +1,47 @@
# 异步日志生命周期
当 native 异步应用需要通过异步队列处理日志写入时,使用异步包。库声明的可用目标比可执行 `async fn main` 示例更广,仓库示例仍以 native 为主。
## 导入两个包
```moonbit
import {
"Nanaloveyuki/BitLogger/src" @log,
"Nanaloveyuki/BitLogger/src-async" @async_log,
"moonbitlang/async",
}
```
## 构建、运行、关闭
```moonbit
async fn main {
let raw = "{\"logger\":{\"min_level\":\"info\",\"target\":\"service.async\",\"sink\":{\"kind\":\"text_console\"}},\"async_config\":{\"max_pending\":64,\"overflow\":\"DropOldest\",\"max_batch\":16,\"linger_ms\":5,\"flush\":\"Batch\"}}"
let config = @async_log.parse_async_logger_build_config_text(raw) catch {
err => {
ignore(err)
return
}
}
let logger = @async_log.build_async_logger(config)
@async.with_task_group(group => {
group.spawn_bg(allow_failure=true, () => logger.run())
logger.info("worker started", fields=[@log.field("mode", "async")])
logger.shutdown()
})
}
```
`run()` 持有 worker 循环。先启动它再记录日志,并调用 `shutdown()`,使待处理记录按配置的 flush 策略结束。
## 运行仓库示例
```bash
moon run examples/async_basic --target native
```
## 下一步
- 查看[队列](../extend/queue.md)中的溢出与 flush 选择。
- 阅读[异步生命周期 API](../api/async.md);完整契约仍可查询[英文 API 索引](../../api/index.md)。
+35
View File
@@ -0,0 +1,35 @@
# 配置驱动构建
当 level、目标名、输出方式或队列策略应由部署配置决定时,使用这一流程。
## 解析、构建、记录
```moonbit
let raw = "{\"min_level\":\"info\",\"target\":\"service\",\"sink\":{\"kind\":\"text_console\",\"text_formatter\":{\"show_timestamp\":false,\"separator\":\" | \"}},\"queue\":{\"max_pending\":64,\"overflow\":\"DropOldest\"}}"
let config = @log.parse_logger_config_text(raw) catch {
err => {
ignore(err)
println("invalid logger configuration")
return
}
}
let logger = @log.build_logger(config)
logger.info("configured logger ready")
ignore(logger.flush())
```
解析先验证数据,再构造运行时 Logger。真实部署应将原始配置放在应用代码之外,并在配置进入进程的边界处理错误。
## 运行仓库示例
```bash
moon run examples/config_build
```
## 下一步
- 需要明确选择满队列时的行为,阅读[队列](../extend/queue.md)。
- 需要后台 worker 生命周期时,阅读[异步日志生命周期](./async.md)。
- 阅读[配置与队列 API](../api/configuration.md);完整契约仍可查询[英文 API 索引](../../api/index.md)。
+55
View File
@@ -0,0 +1,55 @@
# 控制台与结构化字段
命令行工具和服务都可以从这一流程开始。一条记录由 level、target、message 与可选 fields 构成。
## 安装与导入
```bash
moon new log-demo
cd log-demo
moon add Nanaloveyuki/BitLogger@0.7.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" supported_targets = "+native"
options( pkgtype(kind: "executable")
is_main: true,
)
+1 -3
View File
@@ -2,6 +2,4 @@ import {
"Nanaloveyuki/BitLogger/src" @lib, "Nanaloveyuki/BitLogger/src" @lib,
} }
options( pkgtype(kind: "executable")
"is-main": true,
)
+1 -3
View File
@@ -2,6 +2,4 @@ import {
"Nanaloveyuki/BitLogger/src" @lib, "Nanaloveyuki/BitLogger/src" @lib,
} }
options( pkgtype(kind: "executable")
"is-main": true,
)
+1 -3
View File
@@ -2,6 +2,4 @@ import {
"Nanaloveyuki/BitLogger/src" @lib, "Nanaloveyuki/BitLogger/src" @lib,
} }
options( pkgtype(kind: "executable")
"is-main": true,
)
+1 -3
View File
@@ -2,6 +2,4 @@ import {
"Nanaloveyuki/BitLogger/src" @lib, "Nanaloveyuki/BitLogger/src" @lib,
} }
options( pkgtype(kind: "executable")
"is-main": true,
)
+1 -3
View File
@@ -2,6 +2,4 @@ import {
"Nanaloveyuki/BitLogger/src" @lib, "Nanaloveyuki/BitLogger/src" @lib,
} }
options( pkgtype(kind: "executable")
"is-main": true,
)
+1 -3
View File
@@ -2,6 +2,4 @@ import {
"Nanaloveyuki/BitLogger/src" @lib, "Nanaloveyuki/BitLogger/src" @lib,
} }
options( pkgtype(kind: "executable")
"is-main": true,
)
+1 -3
View File
@@ -2,6 +2,4 @@ import {
"Nanaloveyuki/BitLogger/src" @lib, "Nanaloveyuki/BitLogger/src" @lib,
} }
options( pkgtype(kind: "executable")
"is-main": true,
)
+1
View File
@@ -0,0 +1 @@
+2 -2
View File
@@ -1,10 +1,10 @@
name = "Nanaloveyuki/BitLogger" name = "Nanaloveyuki/BitLogger"
version = "0.7.0" version = "0.7.1"
import { import {
"maria/json_parser@0.1.1", "maria/json_parser@0.1.1",
"moonbitlang/async@0.20.0", "moonbitlang/async@0.20.2",
} }
readme = "src/README.mbt.md" 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" { async test "library async parse-build unwrap preserves file-backed runtime helpers" {
let raw = "{\"logger\":{\"min_level\":\"warn\",\"target\":\"async.lib.json.file.same-build\",\"queue\":{\"max_pending\":3,\"overflow\":\"DropOldest\"},\"sink\":{\"kind\":\"file\",\"path\":\"async-lib-json-file-same-build.log\"}},\"async_config\":{\"max_pending\":2,\"overflow\":\"DropOldest\",\"max_batch\":1,\"linger_ms\":0,\"flush\":\"Shutdown\"}}" let raw = "{\"logger\":{\"min_level\":\"warn\",\"target\":\"async.lib.json.file.same-build\",\"queue\":{\"max_pending\":3,\"overflow\":\"DropOldest\"},\"sink\":{\"kind\":\"file\",\"path\":\"logs/async-lib-json-file-same-build.log\"}},\"async_config\":{\"max_pending\":2,\"overflow\":\"DropOldest\",\"max_batch\":1,\"linger_ms\":0,\"flush\":\"Shutdown\"}}"
let logger = parse_and_build_library_async_logger(raw) let logger = parse_and_build_library_async_logger(raw)
let full = logger.to_async_logger() let full = logger.to_async_logger()
let direct = build_async_logger(parse_async_logger_build_config_text(raw)) let direct = build_async_logger(parse_async_logger_build_config_text(raw))
@@ -2920,7 +2920,7 @@ test "library async text logger ignores sink kind and still uses text formatter"
target="async.lib.text.kind", target="async.lib.text.kind",
sink=@bitlogger.SinkConfig::new( sink=@bitlogger.SinkConfig::new(
kind=@bitlogger.SinkKind::File, kind=@bitlogger.SinkKind::File,
path="ignored.log", path="logs/ignored.log",
text_formatter=@bitlogger.TextFormatterConfig::new( text_formatter=@bitlogger.TextFormatterConfig::new(
show_timestamp=false, show_timestamp=false,
separator=" | ", separator=" | ",
@@ -3233,7 +3233,7 @@ async test "async logger projection preserves file-backed runtime helpers throug
target="async.projected.file", target="async.projected.file",
sink=@bitlogger.SinkConfig::new( sink=@bitlogger.SinkConfig::new(
kind=@bitlogger.SinkKind::File, kind=@bitlogger.SinkKind::File,
path="async-projected-file.log", path="logs/async-projected-file.log",
), ),
queue=Some( queue=Some(
@bitlogger.QueueConfig::new( @bitlogger.QueueConfig::new(
@@ -3530,7 +3530,7 @@ async test "application async builder preserves file-backed runtime helpers" {
target="async.app.file.same-build", target="async.app.file.same-build",
sink=@bitlogger.SinkConfig::new( sink=@bitlogger.SinkConfig::new(
kind=@bitlogger.SinkKind::File, kind=@bitlogger.SinkKind::File,
path="async-app-file-same-build.log", path="logs/async-app-file-same-build.log",
), ),
queue=Some( queue=Some(
@bitlogger.QueueConfig::new( @bitlogger.QueueConfig::new(
@@ -4384,7 +4384,7 @@ test "application text async logger ignores sink kind and still uses text format
target="async.app.text.kind.alias", target="async.app.text.kind.alias",
sink=@bitlogger.SinkConfig::new( sink=@bitlogger.SinkConfig::new(
kind=@bitlogger.SinkKind::File, kind=@bitlogger.SinkKind::File,
path="ignored.log", path="logs/ignored.log",
text_formatter=@bitlogger.TextFormatterConfig::new( text_formatter=@bitlogger.TextFormatterConfig::new(
show_timestamp=false, show_timestamp=false,
separator=" | ", separator=" | ",
@@ -4462,7 +4462,7 @@ test "build async text logger ignores sink kind and still uses text formatter" {
target="async.text.kind", target="async.text.kind",
sink=@bitlogger.SinkConfig::new( sink=@bitlogger.SinkConfig::new(
kind=@bitlogger.SinkKind::File, kind=@bitlogger.SinkKind::File,
path="ignored.log", path="logs/ignored.log",
text_formatter=@bitlogger.TextFormatterConfig::new( text_formatter=@bitlogger.TextFormatterConfig::new(
show_timestamp=false, show_timestamp=false,
separator=" | ", separator=" | ",
@@ -4959,7 +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" { async test "application async parse-build preserves file-backed runtime helpers" {
let raw = "{\"logger\":{\"min_level\":\"warn\",\"target\":\"async.app.json.file.same-build\",\"queue\":{\"max_pending\":3,\"overflow\":\"DropOldest\"},\"sink\":{\"kind\":\"file\",\"path\":\"async-app-json-file-same-build.log\"}},\"async_config\":{\"max_pending\":2,\"overflow\":\"DropOldest\",\"max_batch\":1,\"linger_ms\":0,\"flush\":\"Shutdown\"}}" let raw = "{\"logger\":{\"min_level\":\"warn\",\"target\":\"async.app.json.file.same-build\",\"queue\":{\"max_pending\":3,\"overflow\":\"DropOldest\"},\"sink\":{\"kind\":\"file\",\"path\":\"logs/async-app-json-file-same-build.log\"}},\"async_config\":{\"max_pending\":2,\"overflow\":\"DropOldest\",\"max_batch\":1,\"linger_ms\":0,\"flush\":\"Shutdown\"}}"
let application = parse_and_build_application_async_logger(raw) let application = parse_and_build_application_async_logger(raw)
let direct = build_async_logger(parse_async_logger_build_config_text(raw)) let direct = build_async_logger(parse_async_logger_build_config_text(raw))
+4 -58
View File
@@ -1,6 +1,6 @@
///| ///|
test "file sink tracks rotation failures on unavailable backend" { test "file sink tracks rotation failures on unavailable backend" {
let path = "bitlogger-rotate.log" let path = "logs/bitlogger-rotate.log"
ignore(@utils.remove_file_internal(path)) ignore(@utils.remove_file_internal(path))
ignore(@utils.remove_file_internal(path + ".1")) ignore(@utils.remove_file_internal(path + ".1"))
let sink = file_sink(path, rotation=Some(file_rotation(1, max_backups=1))) let sink = file_sink(path, rotation=Some(file_rotation(1, max_backups=1)))
@@ -19,7 +19,7 @@ test "file sink tracks rotation failures on unavailable backend" {
///| ///|
test "file sink reopen and failure counters reflect backend state" { test "file sink reopen and failure counters reflect backend state" {
let sink = file_sink("bitlogger-reopen.log") let sink = file_sink("logs/bitlogger-reopen.log")
if sink.is_available() { if sink.is_available() {
inspect(sink.append_mode(), content="true") inspect(sink.append_mode(), content="true")
inspect(sink.open_failures(), content="0") inspect(sink.open_failures(), content="0")
@@ -45,7 +45,7 @@ test "file sink reopen and failure counters reflect backend state" {
inspect(sink.flush_failures(), content="0") inspect(sink.flush_failures(), content="0")
inspect(sink.rotation_failures(), content="0") inspect(sink.rotation_failures(), content="0")
inspect(sink.close(), content="true") inspect(sink.close(), content="true")
ignore(@utils.remove_file_internal("bitlogger-reopen.log")) ignore(@utils.remove_file_internal("logs/bitlogger-reopen.log"))
} else { } else {
inspect(sink.append_mode(), content="true") inspect(sink.append_mode(), content="true")
inspect(sink.open_failures(), content="1") inspect(sink.open_failures(), content="1")
@@ -71,7 +71,7 @@ test "file sink reopen and failure counters reflect backend state" {
///| ///|
test "file sink can rotate on native backend" { test "file sink can rotate on native backend" {
let path = "bitlogger-rotate-native.log" let path = "logs/bitlogger-rotate-native.log"
ignore(@utils.remove_file_internal(path)) ignore(@utils.remove_file_internal(path))
ignore(@utils.remove_file_internal(path + ".1")) ignore(@utils.remove_file_internal(path + ".1"))
ignore(@utils.remove_file_internal(path + ".2")) ignore(@utils.remove_file_internal(path + ".2"))
@@ -105,57 +105,3 @@ test "file sink can rotate on native backend" {
inspect(sink.rotation_failures(), content="0") inspect(sink.rotation_failures(), content="0")
} }
} }
///|
#cfg(platform="windows")
test "file sink rotation failure counts partial backup-chain failure on native backend" {
let path = "bitlogger-rotate-partial-failure.log"
ignore(@utils.remove_file_internal(path))
ignore(@utils.remove_file_internal(path + ".1"))
ignore(@utils.remove_file_internal(path + ".2"))
let seed = file_sink(
path,
auto_flush=true,
rotation=Some(file_rotation(20, max_backups=2)),
formatter=fn(rec) { rec.message },
)
if seed.is_available() {
seed.write(record(Level::Info, "1234567890"))
seed.write(record(Level::Info, "abcdefghij"))
inspect(seed.rotation_failures(), content="0")
inspect(seed.close(), content="true")
let locked = @utils.open_file_handle_internal(path + ".2", true)
inspect(locked is Some(_), content="true")
let sink = file_sink(
path,
auto_flush=true,
rotation=Some(file_rotation(20, max_backups=2)),
formatter=fn(rec) { rec.message },
)
inspect(sink.is_available(), content="true")
sink.write(record(Level::Info, "klmnopqrst"))
inspect(sink.rotation_failures(), content="1")
inspect(sink.state().rotation_failures, content="1")
inspect(sink.write_failures(), content="1")
inspect(sink.is_available(), content="false")
match locked {
None => ()
Some(handle) => ignore(@utils.close_file_handle_internal(handle))
}
inspect(sink.reopen_append(), content="true")
inspect(sink.is_available(), content="true")
inspect(sink.close(), content="true")
ignore(@utils.remove_file_internal(path))
ignore(@utils.remove_file_internal(path + ".1"))
ignore(@utils.remove_file_internal(path + ".2"))
} else {
inspect(native_files_supported(), content="false")
}
}
+6 -3
View File
@@ -81,7 +81,7 @@ test "application logger keeps configured runtime helper surface" {
LoggerConfig::new( LoggerConfig::new(
min_level=Level::Warn, min_level=Level::Warn,
target="app.alias.helpers", target="app.alias.helpers",
sink=SinkConfig::new(kind=SinkKind::File, path="app-alias.log"), sink=SinkConfig::new(kind=SinkKind::File, path="logs/app-alias.log"),
queue=Some(QueueConfig::new(2, overflow=QueueOverflowPolicy::DropOldest)), queue=Some(QueueConfig::new(2, overflow=QueueOverflowPolicy::DropOldest)),
), ),
) )
@@ -229,7 +229,10 @@ test "application logger file helpers match direct configured builder behavior"
let config = LoggerConfig::new( let config = LoggerConfig::new(
min_level=Level::Warn, min_level=Level::Warn,
target="app.file.same-build", target="app.file.same-build",
sink=SinkConfig::new(kind=SinkKind::File, path="app-file-same-build.log"), sink=SinkConfig::new(
kind=SinkKind::File,
path="logs/app-file-same-build.log",
),
queue=Some(QueueConfig::new(2, overflow=QueueOverflowPolicy::DropOldest)), queue=Some(QueueConfig::new(2, overflow=QueueOverflowPolicy::DropOldest)),
) )
let application = build_application_logger(config) let application = build_application_logger(config)
@@ -344,7 +347,7 @@ test "application logger file controls match direct configured builder behavior"
target="app.file.controls.same-build", target="app.file.controls.same-build",
sink=SinkConfig::new( sink=SinkConfig::new(
kind=SinkKind::File, kind=SinkKind::File,
path="app-file-controls-same-build.log", path="logs/app-file-controls-same-build.log",
), ),
) )
let application = build_application_logger(config) let application = build_application_logger(config)
+4 -4
View File
@@ -42,7 +42,7 @@ pub fn TextFormatterConfig::new(
style_markup? : @formatting.StyleMarkupMode = @formatting.StyleMarkupMode::Full, style_markup? : @formatting.StyleMarkupMode = @formatting.StyleMarkupMode::Full,
target_style_markup? : @formatting.StyleMarkupMode = @formatting.StyleMarkupMode::Disabled, target_style_markup? : @formatting.StyleMarkupMode = @formatting.StyleMarkupMode::Disabled,
fields_style_markup? : @formatting.StyleMarkupMode = @formatting.StyleMarkupMode::Disabled, fields_style_markup? : @formatting.StyleMarkupMode = @formatting.StyleMarkupMode::Disabled,
style_tags? : Map[String, @formatting.TextStyle] = {}, style_tags? : Map[String, @formatting.TextStyle] = Map([]),
) -> TextFormatterConfig { ) -> TextFormatterConfig {
{ {
show_timestamp, show_timestamp,
@@ -380,7 +380,7 @@ fn parse_text_formatter_config(
get_string(obj, "fields_style_markup", default="disabled"), get_string(obj, "fields_style_markup", default="disabled"),
), ),
style_tags=match obj.get("style_tags") { style_tags=match obj.get("style_tags") {
None => {} None => Map([])
Some(inner) => parse_style_tags_config(inner) Some(inner) => parse_style_tags_config(inner)
}, },
) )
@@ -407,7 +407,7 @@ fn parse_style_tags_config(
value : @json_parser.JsonValue, value : @json_parser.JsonValue,
) -> Map[String, @formatting.TextStyle] raise ConfigError { ) -> Map[String, @formatting.TextStyle] raise ConfigError {
let obj = expect_object(value, "text_formatter.style_tags") let obj = expect_object(value, "text_formatter.style_tags")
let style_tags : Map[String, @formatting.TextStyle] = {} let style_tags : Map[String, @formatting.TextStyle] = Map([])
for name, item in obj { for name, item in obj {
style_tags[name] = parse_text_style_config( style_tags[name] = parse_text_style_config(
item, item,
@@ -584,7 +584,7 @@ fn text_style_config_to_json(
fn style_tags_config_to_json( fn style_tags_config_to_json(
style_tags : Map[String, @formatting.TextStyle], style_tags : Map[String, @formatting.TextStyle],
) -> @json_parser.JsonValue { ) -> @json_parser.JsonValue {
let obj : Map[String, @json_parser.JsonValue] = {} let obj : Map[String, @json_parser.JsonValue] = Map([])
for name, style in style_tags { for name, style in style_tags {
obj[name] = text_style_config_to_json(style) obj[name] = text_style_config_to_json(style)
} }
+3 -3
View File
@@ -5,7 +5,7 @@ test "configured logger file setters stay aligned with runtime sink setters" {
target="config.file.setters.delegate", target="config.file.setters.delegate",
sink=SinkConfig::new( sink=SinkConfig::new(
kind=SinkKind::File, 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( LoggerConfig::new(
sink=SinkConfig::new( sink=SinkConfig::new(
kind=SinkKind::File, 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( LoggerConfig::new(
sink=SinkConfig::new( sink=SinkConfig::new(
kind=SinkKind::File, kind=SinkKind::File,
path="config-reset-delegate.log", path="logs/config-reset-delegate.log",
append=false, append=false,
auto_flush=false, auto_flush=false,
rotation=Some(file_rotation(40, max_backups=2)), 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" { test "configured logger file setters update file sink policy state" {
let logger = build_logger( let logger = build_logger(
LoggerConfig::new( 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)), queue=Some(QueueConfig::new(2, overflow=QueueOverflowPolicy::DropNewest)),
), ),
) )
@@ -61,7 +61,7 @@ test "configured logger reset policy restores configured file defaults" {
LoggerConfig::new( LoggerConfig::new(
sink=SinkConfig::new( sink=SinkConfig::new(
kind=SinkKind::File, kind=SinkKind::File,
path="config-reset-policy.log", path="logs/config-reset-policy.log",
append=false, append=false,
auto_flush=false, auto_flush=false,
rotation=Some(file_rotation(36, max_backups=2)), 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" { test "configured logger set policy applies bundled runtime file policy" {
let logger = build_logger( let logger = build_logger(
LoggerConfig::new( 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() 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" { test "configured logger can reopen built file sink" {
let logger = build_logger( let logger = build_logger(
LoggerConfig::new( 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() { 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" { test "configured logger exposes file flush and close helpers" {
let logger = build_logger( let logger = build_logger(
LoggerConfig::new( 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() { 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" { test "configured queued file logger flushes queue through file helper" {
let logger = build_logger( let logger = build_logger(
LoggerConfig::new( 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)), 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" { test "configured queued file logger blocks policy mutation while queue is pending" {
let logger = build_logger( let logger = build_logger(
LoggerConfig::new( 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)), 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" { test "configured queued file logger close drains queue before teardown" {
let logger = build_logger( let logger = build_logger(
LoggerConfig::new( 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)), 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( let file_logger = build_logger(
LoggerConfig::new( LoggerConfig::new(
target="config.file.delegate", 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 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", target="config.file.policy.delegate",
sink=SinkConfig::new( sink=SinkConfig::new(
kind=SinkKind::File, kind=SinkKind::File,
path="config-file-policy-delegate.log", path="logs/config-file-policy-delegate.log",
append=false, append=false,
auto_flush=true, auto_flush=true,
rotation=Some(file_rotation(64, max_backups=2)), 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( let file_config = LoggerConfig::new(
sink=SinkConfig::new( sink=SinkConfig::new(
kind=SinkKind::File, kind=SinkKind::File,
path="config-file-helper-delegate.log", path="logs/config-file-helper-delegate.log",
), ),
) )
let file_logger = build_logger(file_config) 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", target="config.file.state.delegate",
sink=SinkConfig::new( sink=SinkConfig::new(
kind=SinkKind::File, 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", target="config.file.runtime.delegate",
sink=SinkConfig::new( sink=SinkConfig::new(
kind=SinkKind::File, kind=SinkKind::File,
path="config-file-runtime-delegate.log", path="logs/config-file-runtime-delegate.log",
), ),
queue=Some(QueueConfig::new(4, overflow=QueueOverflowPolicy::DropNewest)), 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( LoggerConfig::new(
sink=SinkConfig::new( sink=SinkConfig::new(
kind=SinkKind::File, kind=SinkKind::File,
path="config-file.log", path="logs/config-file.log",
auto_flush=false, auto_flush=false,
rotation=Some(file_rotation(64, max_backups=3)), 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 state = logger.file_state()
let runtime_state = logger.file_runtime_state() let runtime_state = logger.file_runtime_state()
inspect(logger.file_available() == native_files_supported(), content="true") inspect(logger.file_available() == native_files_supported(), content="true")
inspect(logger.file_path(), content="config-file.log") inspect(logger.file_path(), content="logs/config-file.log")
inspect(state.path, content="config-file.log") inspect(state.path, content="logs/config-file.log")
inspect(state.available == logger.file_available(), content="true") inspect(state.available == logger.file_available(), content="true")
inspect(state.append == logger.file_append_mode(), content="true") inspect(state.append == logger.file_append_mode(), content="true")
inspect(state.auto_flush == logger.file_auto_flush(), 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.queued, content="false")
inspect(snapshot.pending_count, content="0") inspect(snapshot.pending_count, content="0")
inspect(snapshot.dropped_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") 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" { test "configured logger reports disabled rotation when file sink has none" {
let logger = build_logger( let logger = build_logger(
LoggerConfig::new( 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") 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" { test "configured file logger mirrors backend file capability" {
let logger = build_logger( let logger = build_logger(
LoggerConfig::new( 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") inspect(logger.file_available() == native_files_supported(), content="true")
match logger.file_path_or_none() { 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") None => inspect(false, content="true")
} }
inspect(logger.file_policy_or_none() is None, content="false") 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( LoggerConfig::new(
min_level=Level::Warn, min_level=Level::Warn,
target="lib.projected.file", 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)), queue=Some(QueueConfig::new(2, overflow=QueueOverflowPolicy::DropOldest)),
), ),
) )
+56 -10
View File
@@ -16,6 +16,24 @@ type Record = @core.Record
///| ///|
type RecordFormatter = @formatting.RecordFormatter 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 { pub struct FileSink {
priv path : String priv path : String
@@ -31,6 +49,7 @@ pub struct FileSink {
priv write_failures : Ref[Int] priv write_failures : Ref[Int]
priv flush_failures : Ref[Int] priv flush_failures : Ref[Int]
priv rotation_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), write_failures: Ref(0),
flush_failures: Ref(0), flush_failures: Ref(0),
rotation_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}" "\{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 { fn rotate_file_sink_internal(sink : FileSink, rotation : FileRotation) -> Bool {
let closed = match sink.handle.val { let closed = match sink.handle.val {
@@ -304,23 +346,27 @@ fn rotate_file_sink_internal(sink : FileSink, rotation : FileRotation) -> Bool {
if !closed { if !closed {
return false return false
} }
let ops = sink.rotation_file_ops.val
if rotation.max_backups > 0 { if rotation.max_backups > 0 {
ignore( if !remove_file_if_present(
@utils.remove_file_internal( ops,
rotated_file_path(sink.path, rotation.max_backups), rotated_file_path(sink.path, rotation.max_backups),
), ) {
) return false
}
for index = rotation.max_backups - 1; index >= 1; { for index = rotation.max_backups - 1; index >= 1; {
let from_path = rotated_file_path(sink.path, index) let from_path = rotated_file_path(sink.path, index)
let to_path = rotated_file_path(sink.path, index + 1) 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 continue index - 1
} }
ignore( if !rename_file_if_present(ops, sink.path, rotated_file_path(sink.path, 1)) {
@utils.rename_file_internal(sink.path, rotated_file_path(sink.path, 1)), return false
) }
} else { } else if !remove_file_if_present(ops, sink.path) {
ignore(@utils.remove_file_internal(sink.path)) return false
} }
sink.handle.val = @utils.open_file_handle_internal(sink.path, false) sink.handle.val = @utils.open_file_handle_internal(sink.path, false)
sink.handle.val is Some(_) 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" { 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() 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(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.available == sink.is_available(), content="true")
inspect(state.append == sink.append_mode(), content="true") inspect(state.append == sink.append_mode(), content="true")
inspect(state.auto_flush == sink.auto_flush_enabled(), 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" { test "file sink unavailable backend keeps stable fallback state" {
let sink = file_sink( let sink = file_sink(
"bitlogger-unavailable.log", "logs/bitlogger-unavailable.log",
rotation=Some(file_rotation(64, max_backups=2)), rotation=Some(file_rotation(64, max_backups=2)),
) )
if sink.is_available() { if sink.is_available() {
@@ -73,7 +73,10 @@ test "file sink rotation config normalizes invalid inputs" {
let rotation = file_rotation(0, max_backups=0) let rotation = file_rotation(0, max_backups=0)
inspect(rotation.max_bytes, content="1") inspect(rotation.max_bytes, content="1")
inspect(rotation.max_backups, 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") inspect(sink.rotation_enabled(), content="true")
match sink.rotation_config() { match sink.rotation_config() {
Some(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" { 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() let default_policy = sink.default_policy()
inspect(sink.append_mode(), content="true") inspect(sink.append_mode(), content="true")
inspect(sink.auto_flush_enabled(), 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" { test "file sink reset policy restores configured defaults" {
let sink = file_sink( let sink = file_sink(
"bitlogger-reset-policy.log", "logs/bitlogger-reset-policy.log",
append=false, append=false,
auto_flush=false, auto_flush=false,
rotation=Some(file_rotation(24, max_backups=3)), 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" { 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() let default_policy = sink.default_policy()
sink.set_policy( sink.set_policy(
FileSinkPolicy::new( FileSinkPolicy::new(
+2 -2
View File
@@ -55,7 +55,7 @@ fn normalize_style_tag_name(name : String) -> String {
///| ///|
pub fn style_tag_registry() -> StyleTagRegistry { 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 { fn fields_to_json(fields : Array[@core.Field]) -> Json {
let obj : Map[String, Json] = {} let obj : Map[String, Json] = Map([])
for item in fields { for item in fields {
obj[item.key] = Json::string(item.value) 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( let config = LoggerConfig::new(
min_level=Level::Warn, min_level=Level::Warn,
target="lib.file.same-build", 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)), queue=Some(QueueConfig::new(2, overflow=QueueOverflowPolicy::DropOldest)),
) )
let logger = build_library_logger(config) let logger = build_library_logger(config)
@@ -236,7 +239,7 @@ test "library logger builder unwrap preserves file controls" {
target="lib.file.controls.same-build", target="lib.file.controls.same-build",
sink=SinkConfig::new( sink=SinkConfig::new(
kind=SinkKind::File, kind=SinkKind::File,
path="lib-file-controls-same-build.log", path="logs/lib-file-controls-same-build.log",
), ),
) )
let logger = build_library_logger(config) 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( stringify_sink_config(
SinkConfig::new( SinkConfig::new(
kind=SinkKind::File, kind=SinkKind::File,
path="demo.log", path="logs/demo.log",
append=false, append=false,
auto_flush=false, auto_flush=false,
rotation=Some(file_rotation(128, max_backups=2)), 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( inspect(
@json_parser.stringify( @json_parser.stringify(
@@ -141,7 +141,7 @@ test "config json helpers export stable structured shapes" {
let sink_json = sink_config_to_json( let sink_json = sink_config_to_json(
SinkConfig::new( SinkConfig::new(
kind=SinkKind::File, kind=SinkKind::File,
path="demo.log", path="logs/demo.log",
append=false, append=false,
auto_flush=false, auto_flush=false,
rotation=Some(file_rotation(128, max_backups=2)), 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("kind").unwrap().as_string().unwrap(), content="file")
inspect( inspect(
sink_obj.get("path").unwrap().as_string().unwrap(), 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(sink_obj.get("append").unwrap().as_bool().unwrap(), content="false")
inspect( 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" { test "logger config parser reads file rotation options" {
let config = parse_logger_config_text( 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( inspect(
match config.sink.kind { match config.sink.kind {
@@ -54,7 +54,7 @@ test "logger config parser reads file rotation options" {
}, },
content="File", content="File",
) )
inspect(config.sink.path, content="bitlogger.log") inspect(config.sink.path, content="logs/bitlogger.log")
match config.sink.rotation { match config.sink.rotation {
Some(rotation) => { Some(rotation) => {
inspect(rotation.max_bytes, content="128") 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" { test "logger config parser prefers max_bytes_i64 when present" {
let config = parse_logger_config_text( 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 { match config.sink.rotation {
Some(rotation) => { Some(rotation) => {
inspect(rotation.max_bytes, content="2147483647") inspect(rotation.max_bytes, content="2147483647")
+4 -4
View File
@@ -43,13 +43,13 @@ test "logger config stringify roundtrips file rotation fields" {
LoggerConfig::new( LoggerConfig::new(
sink=SinkConfig::new( sink=SinkConfig::new(
kind=SinkKind::File, kind=SinkKind::File,
path="bitlogger.log", path="logs/bitlogger.log",
rotation=Some(file_rotation(256, max_backups=2)), rotation=Some(file_rotation(256, max_backups=2)),
), ),
), ),
) )
let config = parse_logger_config_text(text) 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 { match config.sink.rotation {
Some(rotation) => { Some(rotation) => {
inspect(rotation.max_bytes, content="256") inspect(rotation.max_bytes, content="256")
@@ -66,13 +66,13 @@ test "logger config stringify roundtrips file rotation i64 metadata" {
LoggerConfig::new( LoggerConfig::new(
sink=SinkConfig::new( sink=SinkConfig::new(
kind=SinkKind::File, kind=SinkKind::File,
path="bitlogger.log", path="logs/bitlogger.log",
rotation=Some(file_rotation_i64(4294967296L, max_backups=2)), rotation=Some(file_rotation_i64(4294967296L, max_backups=2)),
), ),
), ),
) )
let config = parse_logger_config_text(text) 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 { match config.sink.rotation {
Some(rotation) => { Some(rotation) => {
inspect(rotation.max_bytes, content="2147483647") 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" { test "parsed application logger keeps direct runtime helper surface" {
let logger : ApplicationLogger = parse_and_build_application_logger( 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") 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" { 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 logger = parse_and_build_library_logger(raw)
let full = logger.to_logger() let full = logger.to_logger()
let configured = parse_and_build_logger(raw) 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" { 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 logger = parse_and_build_library_logger(raw)
let full = logger.to_logger() let full = logger.to_logger()
let configured = parse_and_build_logger(raw) 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" { 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.min_level.label(), content="INFO")
inspect(config.target, content="") inspect(config.target, content="")
inspect(config.timestamp, content="false") inspect(config.timestamp, content="false")
@@ -51,7 +51,7 @@ test "file preset uses file sink defaults" {
}, },
content="File", 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.append, content="true")
inspect(config.sink.auto_flush, content="true") inspect(config.sink.auto_flush, content="true")
inspect(config.sink.rotation is None, 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=" | ", separator=" | ",
) )
let config = file( let config = file(
"service.log", "logs/service.log",
min_level=Level::Warn, min_level=Level::Warn,
target="svc.worker", target="svc.worker",
timestamp=true, 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.min_level.label(), content="WARN")
inspect(config.target, content="svc.worker") inspect(config.target, content="svc.worker")
inspect(config.timestamp, content="true") 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.append, content="false")
inspect(config.sink.auto_flush, content="false") inspect(config.sink.auto_flush, content="false")
inspect(config.sink.text_formatter.show_timestamp, 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" { test "queue helper preserves file rotation when applied after rotation" {
let config = with_queue( 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, max_pending=8,
overflow=QueueOverflowPolicy::DropNewest, overflow=QueueOverflowPolicy::DropNewest,
) )
inspect(config.sink.path, content="service.log") inspect(config.sink.path, content="logs/service.log")
inspect(config.sink.append, content="false") inspect(config.sink.append, content="false")
match config.queue { match config.queue {
Some(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" { test "file rotation i64 helper preserves file preset shape" {
let config = with_file_rotation_i64( 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, 4294967296L,
max_backups=2, max_backups=2,
) )
inspect(config.sink.path, content="service.log") inspect(config.sink.path, content="logs/service.log")
inspect(config.sink.append, content="false") inspect(config.sink.append, content="false")
match config.sink.rotation { match config.sink.rotation {
Some(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( let plain = file_sink_state_to_json(
FileSinkState::new( FileSinkState::new(
"demo.log", "logs/demo.log",
available=true, available=true,
append=false, append=false,
auto_flush=true, auto_flush=true,
@@ -48,11 +48,11 @@ test "file state json helpers stringify stable snapshots" {
.unwrap() .unwrap()
inspect( inspect(
@json_parser.stringify(plain), @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( inspect(
plain_obj.get("path").unwrap().as_string().unwrap(), plain_obj.get("path").unwrap().as_string().unwrap(),
content="demo.log", content="logs/demo.log",
) )
inspect( inspect(
plain_obj.get("available").unwrap().as_bool().unwrap(), 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( let wide = file_sink_state_to_json(
FileSinkState::new( FileSinkState::new(
"wide.log", "logs/wide.log",
available=true, available=true,
append=true, append=true,
auto_flush=false, auto_flush=false,
@@ -132,7 +132,7 @@ test "file state json helpers stringify stable snapshots" {
inspect( inspect(
stringify_file_sink_state( stringify_file_sink_state(
FileSinkState::new( FileSinkState::new(
"plain.log", "logs/plain.log",
available=false, available=false,
append=true, append=true,
auto_flush=false, auto_flush=false,
@@ -143,7 +143,7 @@ test "file state json helpers stringify stable snapshots" {
rotation_failures=0, 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( let runtime_json = runtime_file_state_to_json(
RuntimeFileState::new( RuntimeFileState::new(
FileSinkState::new( FileSinkState::new(
"queue.log", "logs/queue.log",
available=true, available=true,
append=true, append=true,
auto_flush=false, auto_flush=false,
@@ -180,7 +180,7 @@ test "runtime file state json helpers stringify queue snapshots" {
) )
inspect( inspect(
runtime_file_obj.get("path").unwrap().as_string().unwrap(), runtime_file_obj.get("path").unwrap().as_string().unwrap(),
content="queue.log", content="logs/queue.log",
) )
inspect( inspect(
runtime_file_obj.get("available").unwrap().as_bool().unwrap(), 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( let json = stringify_runtime_file_state(
RuntimeFileState::new( RuntimeFileState::new(
FileSinkState::new( FileSinkState::new(
"queue.log", "logs/queue.log",
available=true, available=true,
append=true, append=true,
auto_flush=false, auto_flush=false,
@@ -211,13 +211,13 @@ test "runtime file state json helpers stringify queue snapshots" {
) )
inspect( inspect(
json, 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( let wide_runtime_json = runtime_file_state_to_json(
RuntimeFileState::new( RuntimeFileState::new(
FileSinkState::new( FileSinkState::new(
"wide-queue.log", "logs/wide-queue.log",
available=true, available=true,
append=true, append=true,
auto_flush=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( let file_config = LoggerConfig::new(
target="config.close.file", 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_logger = build_logger(file_config)
let file_sink = build_logger(file_config).sink 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" { test "runtime sink queued file close drains queue before teardown" {
let sink = RuntimeSink::QueuedFile( let sink = RuntimeSink::QueuedFile(
queued_sink( queued_sink(
file_sink("runtime-close-queued.log", auto_flush=false), file_sink("logs/runtime-close-queued.log", auto_flush=false),
max_pending=4, max_pending=4,
), ),
) )
+2 -2
View File
@@ -1,6 +1,6 @@
///| ///|
test "runtime sink plain file reopen helpers and failure resets work directly" { 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() { if sink.file_available() {
inspect(sink.close(), content="true") inspect(sink.close(), content="true")
inspect(sink.file_reopen_append(), 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" { test "runtime sink queued file reopen helpers and failure resets work directly" {
let sink = RuntimeSink::QueuedFile( 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() { if sink.file_available() {
inspect(sink.file_close(), content="true") 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") inspect(console_sink.drain(), content="0")
let file_sink = RuntimeSink::File( 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.pending_count(), content="0")
inspect(file_sink.dropped_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" { test "runtime sink plain file helpers expose direct file state and policy" {
let sink = RuntimeSink::File( let sink = RuntimeSink::File(
file_sink( file_sink(
"runtime-file-helpers.log", "logs/runtime-file-helpers.log",
auto_flush=false, auto_flush=false,
rotation=Some(file_rotation(40, max_backups=2)), rotation=Some(file_rotation(40, max_backups=2)),
), ),
) )
inspect(sink.file_available() == native_files_supported(), content="true") inspect(sink.file_available() == native_files_supported(), content="true")
match sink.file_path_or_none() { 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") 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_append_mode(), content="true")
inspect(sink.file_auto_flush(), content="false") inspect(sink.file_auto_flush(), content="false")
inspect(sink.file_rotation_enabled(), content="true") 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() { match sink.file_state_or_none() {
Some(snapshot) => { 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.append, content="true")
inspect(snapshot.auto_flush, content="false") inspect(snapshot.auto_flush, content="false")
} }
None => inspect(false, content="true") None => inspect(false, content="true")
} }
let state = sink.file_state() 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.available == sink.file_available(), content="true")
inspect(state.append, content="true") inspect(state.append, content="true")
inspect(state.auto_flush, content="false") 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.queued, content="false")
inspect(snapshot.pending_count, content="0") inspect(snapshot.pending_count, content="0")
inspect(snapshot.dropped_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") 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" { test "runtime sink queued file helpers preserve queue-aware file runtime state" {
let sink = RuntimeSink::QueuedFile( let sink = RuntimeSink::QueuedFile(
queued_sink( queued_sink(
file_sink("runtime-queued-file.log", auto_flush=false), file_sink("logs/runtime-queued-file.log", auto_flush=false),
max_pending=2, max_pending=2,
overflow=QueueOverflowPolicy::DropNewest, overflow=QueueOverflowPolicy::DropNewest,
), ),
@@ -185,10 +185,10 @@ test "runtime sink queued file helpers preserve queue-aware file runtime state"
logger.info("three") logger.info("three")
inspect(sink.file_available() == native_files_supported(), content="true") inspect(sink.file_available() == native_files_supported(), content="true")
match sink.file_path_or_none() { 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") 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_policy_or_none() is None, content="false")
inspect(sink.file_default_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") 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.queued, content="true")
inspect(snapshot.pending_count, content="2") inspect(snapshot.pending_count, content="2")
inspect(snapshot.dropped_count, content="1") 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") 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") inspect(queued_progress.file_backed, content="false")
let file_sink = RuntimeSink::File( 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() let file_progress = file_sink.flush_progress()
inspect(file_progress.queue_advanced_count, content="0") 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 None
} }
///|
pub fn file_exists_internal(path : String) -> Bool {
ignore(path)
false
}
///| ///|
pub fn write_file_handle_internal( pub fn write_file_handle_internal(
handle : FileHandle, 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 field_equals(String, String) -> (@core.Record) -> Bool
pub fn file_exists_internal(String) -> Bool
pub fn file_size_i64_internal(FileHandle) -> Int64 pub fn file_size_i64_internal(FileHandle) -> Int64
pub fn flush_file_handle_internal(FileHandle) -> Bool pub fn flush_file_handle_internal(FileHandle) -> Bool
-1
View File
@@ -1 +0,0 @@
trueVersion = "1.0.0"