From 69967badfddd3c43cc886c40ac509bd5b88e90ea Mon Sep 17 00:00:00 2001 From: Nanaloveyuki Date: Sun, 10 May 2026 12:19:52 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Add=20file=20sink=20reopen=20and=20?= =?UTF-8?q?failure=20counters?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 3 +- bitlogger/BitLogger_wbtest.mbt | 28 ++++++++++++++ bitlogger/README.mbt.md | 7 +++- bitlogger/sinks.mbt | 71 ++++++++++++++++++++++++++++++---- docs/README-en.md | 3 +- docs/changes/0.3.0.md | 4 ++ 6 files changed, 105 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index a1ea4a2..8f2807b 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ BitLogger 是一个基于 MoonBit 编写的结构化日志库。 - 🧷 可绑定上下文:支持 `bind(...)` 与 `fields(...)`,更方便地封装复用字段上下文。 - 📮 显式队列:支持 `queued_sink(...)` / `with_queue(...)`、有界积压与溢出策略,作为后续 async sink 的 runtime-safe 基础。 - 🧾 可配置文本格式:支持 `text_formatter(...)`、`format_text(...)`、`text_console_sink(...)`、`formatted_callback_sink(...)` 与模板化 `template` 输出。 -- 💾 Native 文件输出:支持 `file_sink(...)`,并已提供基础 size rotation / backup retention;当前仅保证 `native/llvm` backend 可用。 +- 💾 Native 文件输出:支持 `file_sink(...)`、基础 size rotation / backup retention、显式 `reopen()` 与失败计数;当前仅保证 `native/llvm` backend 可用。 - 📦 面向 MoonBit:API 和工程结构围绕 MoonBit 的 package / visibility / toolchain 现实约束设计。 ## 🚀 快速开始 @@ -221,6 +221,7 @@ if native_files_supported() { - 当前提供 JSON 配置层:`parse_logger_config_text(...)`、`stringify_logger_config(...)`、`build_logger(...)` - 已支持字段:`min_level`、`target`、`timestamp`、`sink.kind`、`sink.path`、`sink.append`、`sink.auto_flush`、`sink.rotation`、`sink.text_formatter`、`queue` - `sink.rotation` 当前支持 `max_bytes` 与 `max_backups`,提供基础 size-based rotation 和 backup retention +- `file_sink(...)` 还提供 `reopen()`、`open_failures()`、`write_failures()`、`flush_failures()`、`rotation_failures()`,用于基础可观测性 - `sink.text_formatter.template` 当前支持固定 token:`{timestamp}`、`{timestamp_ms}`、`{level}`、`{target}`、`{message}`、`{fields}` - 当前可由配置直接组装的 sink 类型:`console`、`json_console`、`text_console`、`file` - `queue` 会作为显式包装层附着在最终 sink 外侧;这仍然是同步 drain 模型,不是 async runtime diff --git a/bitlogger/BitLogger_wbtest.mbt b/bitlogger/BitLogger_wbtest.mbt index f69745e..b0f7ad2 100644 --- a/bitlogger/BitLogger_wbtest.mbt +++ b/bitlogger/BitLogger_wbtest.mbt @@ -90,6 +90,9 @@ test "native file support flag is queryable" { test "file sink availability reflects backend support" { let sink = file_sink("bitlogger-test.log") inspect(sink.is_available() == native_files_supported(), content="true") + inspect(sink.open_failures(), content=if sink.is_available() { "0" } else { "1" }) + inspect(sink.write_failures(), content="0") + inspect(sink.flush_failures(), content="0") if sink.is_available() { inspect(sink.flush(), content="true") inspect(sink.close(), content="true") @@ -110,9 +113,34 @@ test "file sink tracks rotation failures on unavailable backend" { sink.write(record(Level::Info, "a")) if sink.is_available() { inspect(sink.rotation_failures(), content="0") + inspect(sink.write_failures(), content="0") ignore(sink.close()) } else { inspect(sink.rotation_failures(), content="0") + inspect(sink.write_failures(), content="1") + } +} + +test "file sink reopen and failure counters reflect backend state" { + let sink = file_sink("bitlogger-reopen.log") + if sink.is_available() { + inspect(sink.open_failures(), content="0") + inspect(sink.close(), content="true") + inspect(sink.reopen(), content="true") + inspect(sink.is_available(), content="true") + inspect(sink.open_failures(), content="0") + sink.write(record(Level::Info, "reopened")) + inspect(sink.write_failures(), content="0") + inspect(sink.flush_failures(), content="0") + inspect(sink.close(), content="true") + ignore(remove_file_internal("bitlogger-reopen.log")) + } else { + inspect(sink.open_failures(), content="1") + sink.write(record(Level::Info, "dropped")) + inspect(sink.write_failures(), content="1") + inspect(sink.reopen(), content="false") + inspect(sink.open_failures(), content="2") + inspect(sink.flush_failures(), content="0") } } diff --git a/bitlogger/README.mbt.md b/bitlogger/README.mbt.md index 7316b65..448ce0a 100644 --- a/bitlogger/README.mbt.md +++ b/bitlogger/README.mbt.md @@ -46,8 +46,8 @@ BitLogger 是一个基于 MoonBit 的结构化日志库。 - 支持 `parse_logger_config_text(...)`、`stringify_logger_config(...)` 进行最小 JSON 配置读写 - config-driven logger assembly via `build_logger(...)` - 支持 `build_logger(...)` 将配置组装为可直接使用的 logger -- native-only file output via `file_sink(...)`, with basic size rotation and backup retention -- 支持 `file_sink(...)`、基础 size rotation 与 backup retention,但当前仅保证 `native/llvm` backend 可用 +- native-only file output via `file_sink(...)`, with basic size rotation, backup retention, explicit `reopen()`, and failure counters +- 支持 `file_sink(...)`、基础 size rotation / backup retention、显式 `reopen()` 与失败计数,但当前仅保证 `native/llvm` backend 可用 ## Example / 示例 @@ -220,3 +220,6 @@ test { - basic rotation is size-based / 当前基础轮转为按文件大小触发 - `file_rotation(max_bytes, max_backups=...)` controls threshold and retained backups / `file_rotation(max_bytes, max_backups=...)` 控制触发阈值和保留备份数 - JSON config uses `sink.rotation.max_bytes` and `sink.rotation.max_backups` / JSON 配置使用 `sink.rotation.max_bytes` 与 `sink.rotation.max_backups` +- `FileSink::reopen()` can explicitly reopen the current file handle / `FileSink::reopen()` 可显式重开当前文件句柄 +- `open_failures()`、`write_failures()`、`flush_failures()`、`rotation_failures()` expose basic sink health counters / `open_failures()`、`write_failures()`、`flush_failures()`、`rotation_failures()` 可用于观察基础 sink 健康状态 +- current scope is observability-first, not a full self-healing sink runtime / 当前定位仍以可观测性优先,不是完整自愈型 sink runtime diff --git a/bitlogger/sinks.mbt b/bitlogger/sinks.mbt index caafdcb..d609312 100644 --- a/bitlogger/sinks.mbt +++ b/bitlogger/sinks.mbt @@ -51,6 +51,9 @@ pub struct FileSink { formatter : RecordFormatter auto_flush : Bool rotation : FileRotation? + open_failures : Ref[Int] + write_failures : Ref[Int] + flush_failures : Ref[Int] rotation_failures : Ref[Int] } @@ -79,13 +82,17 @@ pub fn file_sink( format_text(rec) }, ) -> FileSink { + let handle = open_file_handle_internal(path, append) { path, append, - handle: Ref::new(open_file_handle_internal(path, append)), + handle: Ref::new(handle), formatter, auto_flush, rotation, + open_failures: Ref::new(if handle is Some(_) { 0 } else { 1 }), + write_failures: Ref::new(0), + flush_failures: Ref::new(0), rotation_failures: Ref::new(0), } } @@ -97,7 +104,13 @@ pub fn FileSink::is_available(self : FileSink) -> Bool { pub fn FileSink::flush(self : FileSink) -> Bool { match self.handle.val { None => false - Some(handle) => flush_file_handle_internal(handle) + Some(handle) => { + let ok = flush_file_handle_internal(handle) + if !ok { + self.flush_failures.val += 1 + } + ok + } } } @@ -116,6 +129,37 @@ pub fn FileSink::rotation_failures(self : FileSink) -> Int { self.rotation_failures.val } +pub fn FileSink::open_failures(self : FileSink) -> Int { + self.open_failures.val +} + +pub fn FileSink::write_failures(self : FileSink) -> Int { + self.write_failures.val +} + +pub fn FileSink::flush_failures(self : FileSink) -> Int { + self.flush_failures.val +} + +pub fn FileSink::reopen(self : FileSink, append~ : Bool? = None) -> Bool { + let append_mode = append.unwrap_or(self.append) + match self.handle.val { + None => () + Some(handle) => { + ignore(close_file_handle_internal(handle)) + self.handle.val = None + } + } + let reopened = open_file_handle_internal(self.path, append_mode) + self.handle.val = reopened + if reopened is Some(_) { + true + } else { + self.open_failures.val += 1 + false + } +} + fn rotated_file_path(path : String, index : Int) -> String { "\{path}.\{index}" } @@ -171,20 +215,33 @@ fn rotate_if_needed_internal(sink : FileSink, next_line_bytes : Int) -> Bool { pub impl Sink for FileSink with write(self, rec) { match self.handle.val { - None => () + None => { + self.write_failures.val += 1 + } Some(_) => { let line = "\{(self.formatter)(rec)}\n" let can_write = rotate_if_needed_internal(self, string_byte_length_internal(line)) if can_write { match self.handle.val { - None => () + None => { + self.write_failures.val += 1 + } Some(active) => { - ignore(write_file_handle_internal(active, line)) - if self.auto_flush { - ignore(flush_file_handle_internal(active)) + let wrote = write_file_handle_internal(active, line) + if wrote { + if self.auto_flush { + let flushed = flush_file_handle_internal(active) + if !flushed { + self.flush_failures.val += 1 + } + } + } else { + self.write_failures.val += 1 } } } + } else { + self.write_failures.val += 1 } } } diff --git a/docs/README-en.md b/docs/README-en.md index f0b1668..ada88a7 100644 --- a/docs/README-en.md +++ b/docs/README-en.md @@ -26,7 +26,7 @@ BitLogger currently provides: - bounded backlog with `QueueOverflowPolicy::DropNewest` and `QueueOverflowPolicy::DropOldest` - configurable text formatting via `text_formatter(...)`, `format_text(...)`, `text_console_sink(...)`, and template-driven `template` output - formatter-based callback integration via `formatted_callback_sink(...)` -- native-only file output via `file_sink(...)`, with basic size rotation and backup retention +- native-only file output via `file_sink(...)`, with basic size rotation, backup retention, explicit `reopen()`, and failure counters - `native_files_supported()` for backend capability detection - default global logger helpers @@ -206,6 +206,7 @@ if native_files_supported() { - BitLogger now includes a JSON config layer via `parse_logger_config_text(...)`, `stringify_logger_config(...)`, and `build_logger(...)`. - Supported keys include `min_level`, `target`, `timestamp`, `sink.kind`, `sink.path`, `sink.append`, `sink.auto_flush`, `sink.rotation`, `sink.text_formatter`, and `queue`. - `sink.rotation` currently supports `max_bytes` and `max_backups` for basic size-based rotation and backup retention. +- `file_sink(...)` also exposes `reopen()`, `open_failures()`, `write_failures()`, `flush_failures()`, and `rotation_failures()` for basic observability. - `sink.text_formatter.template` currently supports fixed tokens: `{timestamp}`, `{timestamp_ms}`, `{level}`, `{target}`, `{message}`, and `{fields}`. - Config-driven sink assembly currently supports `console`, `json_console`, `text_console`, and `file`. - `queue` remains a synchronous bounded wrapper around the final sink, not an async runtime. diff --git a/docs/changes/0.3.0.md b/docs/changes/0.3.0.md index 6c4ef38..2fc7dd1 100644 --- a/docs/changes/0.3.0.md +++ b/docs/changes/0.3.0.md @@ -29,6 +29,7 @@ version 0.3.0 - feat: support formatter tokens `{timestamp}`, `{timestamp_ms}`, `{level}`, `{target}`, `{message}`, and `{fields}` in both runtime and JSON config paths - feat: add `FileRotation`, `file_rotation(...)`, and size-based file rotation with retained backups for `file_sink(...)` - feat: support `sink.rotation.max_bytes` and `sink.rotation.max_backups` in JSON logger config +- feat: add `FileSink::reopen()` and basic file sink failure counters via `open_failures()`, `write_failures()`, and `flush_failures()` - feat: add `SplitSink`, `split_sink(...)`, and `split_by_level(...)` for routing records into different sinks by predicate or level - feat: add `Logger::bind(...)` as an ergonomic context-binding alias and `fields(...)` helper for tuple-based field construction @@ -42,6 +43,7 @@ version 0.3.0 - test: cover dropped-count reporting for bounded config-built queue - test: cover template-based formatter rendering and disabled token behavior - test: cover file rotation config parsing, config roundtrip, and native rotation behavior +- test: cover file sink reopen behavior and backend-dependent failure counter paths - test: cover split sink predicate routing and level-based routing behavior - test: cover `bind(...)` context composition and `fields(...)` helper behavior - test: add async logger lifecycle, config roundtrip, and batching/flush policy test seeds @@ -56,6 +58,7 @@ version 0.3.0 - docs: update root README, English README, and Mooncake README with config usage notes - docs: update formatter examples to demonstrate template-based text rendering - docs: update file sink examples to demonstrate rotation and backup retention +- docs: document file sink reopen and observability counters in README variants - docs: add split sink examples for level-based routing - docs: add `bind(...)` examples for reusable context binding - docs: update root README and English README with async adapter notes and current scope @@ -66,5 +69,6 @@ version 0.3.0 - current config scope is still intentionally constrained to stable built-in sink shapes - formatter templates are intentionally limited to simple token replacement and do not yet include conditional blocks or alignment/padding controls - current file rotation scope is intentionally limited to size-based rename retention and does not yet include time rotation or compression +- file sink observability is currently counter-based and explicit; it does not yet attempt automatic recovery or background self-healing - queue wrapping remains synchronous drain-based delivery, not async runtime scheduling - async logging remains an isolated adapter layer and currently targets `native/llvm` only