From 6e3cc165b6eb1bc71084e99f083f44e468e3156a Mon Sep 17 00:00:00 2001 From: Nanaloveyuki Date: Sun, 5 Jul 2026 21:31:52 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Add=20Int64=20file=20rotation=20pat?= =?UTF-8?q?h=20and=20JSON=20roundtrip?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/BitLogger_test.mbt | 207 ++++++++++++++++++++++++++++++ src/BitLogger_wbtest.mbt | 11 ++ src/file_backend_native.mbt | 4 +- src/file_backend_stub.mbt | 4 +- src/presets.mbt | 26 ++++ src/presets_test.mbt | 22 ++++ src/sinks_file.mbt | 25 +++- src/utils/config.mbt | 43 ++++++- src/utils/file_backend_native.mbt | 10 +- src/utils/file_backend_stub.mbt | 4 +- src/utils/moon.pkg | 1 + src/utils/sink_models.mbt | 24 ++++ src/utils/stub.c | 10 +- 13 files changed, 368 insertions(+), 23 deletions(-) diff --git a/src/BitLogger_test.mbt b/src/BitLogger_test.mbt index 9aaa119..026c574 100644 --- a/src/BitLogger_test.mbt +++ b/src/BitLogger_test.mbt @@ -147,6 +147,23 @@ test "logger config parser reads file rotation options" { Some(rotation) => { inspect(rotation.max_bytes, content="128") inspect(rotation.max_backups, content="3") + inspect(rotation.native_wide_max_bytes is None, content="true") + } + None => inspect(false, content="true") + } +} + +///| +test "logger config parser prefers max_bytes_i64 when present" { + let config = parse_logger_config_text( + "{\"sink\":{\"kind\":\"file\",\"path\":\"bitlogger.log\",\"rotation\":{\"max_bytes\":128,\"max_bytes_i64\":\"4294967296\",\"max_backups\":3}}}", + ) + inspect(config.sink.path, content="bitlogger.log") + match config.sink.rotation { + Some(rotation) => { + inspect(rotation.max_bytes, content="2147483647") + inspect(rotation.max_backups, content="3") + inspect(rotation.native_wide_max_bytes.unwrap(), content="4294967296") } None => inspect(false, content="true") } @@ -376,6 +393,30 @@ test "logger config stringify roundtrips file rotation fields" { Some(rotation) => { inspect(rotation.max_bytes, content="256") inspect(rotation.max_backups, content="2") + inspect(rotation.native_wide_max_bytes is None, content="true") + } + None => inspect(false, content="true") + } +} + +///| +test "logger config stringify roundtrips file rotation i64 metadata" { + let text = stringify_logger_config( + LoggerConfig::new( + sink=SinkConfig::new( + kind=SinkKind::File, + path="bitlogger.log", + rotation=Some(file_rotation_i64(4294967296L, max_backups=2)), + ), + ), + ) + let config = parse_logger_config_text(text) + inspect(config.sink.path, content="bitlogger.log") + match config.sink.rotation { + Some(rotation) => { + inspect(rotation.max_bytes, content="2147483647") + inspect(rotation.max_backups, content="2") + inspect(rotation.native_wide_max_bytes.unwrap(), content="4294967296") } None => inspect(false, content="true") } @@ -425,6 +466,14 @@ 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}}", ) + inspect( + @json_parser.stringify( + file_rotation_config_to_json( + file_rotation_i64(4294967296L, max_backups=2), + ), + ), + content="{\"max_bytes\":2147483647,\"max_backups\":2,\"max_bytes_i64\":\"4294967296\"}", + ) } ///| @@ -559,6 +608,24 @@ test "config json helpers export stable structured shapes" { rotation_obj.get("max_backups").unwrap().as_number().unwrap().to_int(), content="2", ) + inspect(rotation_obj.get("max_bytes_i64") is None, content="true") + + let wide_rotation_json = file_rotation_config_to_json( + file_rotation_i64(4294967296L, max_backups=3), + ) + let wide_rotation_obj = wide_rotation_json.as_object().unwrap() + inspect( + wide_rotation_obj.get("max_bytes").unwrap().as_number().unwrap().to_int(), + content="2147483647", + ) + inspect( + wide_rotation_obj.get("max_backups").unwrap().as_number().unwrap().to_int(), + content="3", + ) + inspect( + wide_rotation_obj.get("max_bytes_i64").unwrap().as_string().unwrap(), + content="4294967296", + ) let logger_json = logger_config_to_json( LoggerConfig::new( @@ -1614,6 +1681,16 @@ test "file state json helpers stringify stable snapshots" { rotation_obj.get("max_backups").unwrap().as_number().unwrap().to_int(), content="2", ) + inspect(rotation_obj.get("max_bytes_i64") is None, content="true") + + let wide_rotation_json = file_rotation_config_to_json( + file_rotation_i64(4294967296L, max_backups=2), + ) + let wide_rotation_obj = wide_rotation_json.as_object().unwrap() + inspect( + wide_rotation_obj.get("max_bytes_i64").unwrap().as_string().unwrap(), + content="4294967296", + ) let plain = file_sink_state_to_json( FileSinkState::new( @@ -1675,6 +1752,48 @@ test "file state json helpers stringify stable snapshots" { plain_rotation_obj.get("max_backups").unwrap().as_number().unwrap().to_int(), content="2", ) + + let wide = file_sink_state_to_json( + FileSinkState::new( + "wide.log", + available=true, + append=true, + auto_flush=false, + rotation=Some(file_rotation_i64(4294967296L, max_backups=3)), + open_failures=0, + write_failures=0, + flush_failures=0, + rotation_failures=0, + ), + ) + let wide_obj = wide.as_object().unwrap() + let wide_state_rotation_obj = wide_obj + .get("rotation") + .unwrap() + .as_object() + .unwrap() + inspect( + wide_state_rotation_obj + .get("max_bytes") + .unwrap() + .as_number() + .unwrap() + .to_int(), + content="2147483647", + ) + inspect( + wide_state_rotation_obj + .get("max_backups") + .unwrap() + .as_number() + .unwrap() + .to_int(), + content="3", + ) + inspect( + wide_state_rotation_obj.get("max_bytes_i64").unwrap().as_string().unwrap(), + content="4294967296", + ) inspect( stringify_file_sink_state( FileSinkState::new( @@ -1759,6 +1878,58 @@ test "runtime file state json helpers stringify queue snapshots" { 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}", ) + + let wide_runtime_json = runtime_file_state_to_json( + RuntimeFileState::new( + FileSinkState::new( + "wide-queue.log", + available=true, + append=true, + auto_flush=true, + rotation=Some(file_rotation_i64(4294967296L, max_backups=2)), + open_failures=0, + write_failures=0, + flush_failures=0, + rotation_failures=0, + ), + queued=false, + pending_count=0, + dropped_count=0, + ), + ) + let wide_runtime_obj = wide_runtime_json.as_object().unwrap() + let wide_runtime_file_obj = wide_runtime_obj + .get("file") + .unwrap() + .as_object() + .unwrap() + let wide_runtime_rotation_obj = wide_runtime_file_obj + .get("rotation") + .unwrap() + .as_object() + .unwrap() + inspect( + wide_runtime_rotation_obj + .get("max_bytes") + .unwrap() + .as_number() + .unwrap() + .to_int(), + content="2147483647", + ) + inspect( + wide_runtime_rotation_obj + .get("max_backups") + .unwrap() + .as_number() + .unwrap() + .to_int(), + content="2", + ) + inspect( + wide_runtime_rotation_obj.get("max_bytes_i64").unwrap().as_string().unwrap(), + content="4294967296", + ) } ///| @@ -1798,6 +1969,42 @@ test "file sink policy json helpers stringify stable policies" { .to_int(), content="3", ) + + let wide_policy_json = file_sink_policy_to_json( + FileSinkPolicy::new( + append=true, + auto_flush=false, + rotation=Some(file_rotation_i64(4294967296L, max_backups=4)), + ), + ) + let wide_policy_obj = wide_policy_json.as_object().unwrap() + let wide_policy_rotation_obj = wide_policy_obj + .get("rotation") + .unwrap() + .as_object() + .unwrap() + inspect( + wide_policy_rotation_obj + .get("max_bytes") + .unwrap() + .as_number() + .unwrap() + .to_int(), + content="2147483647", + ) + inspect( + wide_policy_rotation_obj + .get("max_backups") + .unwrap() + .as_number() + .unwrap() + .to_int(), + content="4", + ) + inspect( + wide_policy_rotation_obj.get("max_bytes_i64").unwrap().as_string().unwrap(), + content="4294967296", + ) inspect( stringify_file_sink_policy( FileSinkPolicy::new(append=true, auto_flush=false, rotation=None), diff --git a/src/BitLogger_wbtest.mbt b/src/BitLogger_wbtest.mbt index 5e8004b..f6af4a2 100644 --- a/src/BitLogger_wbtest.mbt +++ b/src/BitLogger_wbtest.mbt @@ -576,6 +576,17 @@ test "file sink rotation config normalizes invalid inputs" { } } +///| +test "file rotation i64 preserves wide threshold metadata" { + let rotation = file_rotation_i64(4294967296L, max_backups=2) + inspect(rotation.max_bytes, content="2147483647") + inspect(rotation.max_backups, content="2") + match rotation.native_wide_max_bytes { + Some(value) => inspect(value, content="4294967296") + None => inspect(false, content="true") + } +} + ///| test "file sink setters update auto flush and rotation state" { let sink = file_sink("bitlogger-setters.log") diff --git a/src/file_backend_native.mbt b/src/file_backend_native.mbt index 49bfd39..a57aa26 100644 --- a/src/file_backend_native.mbt +++ b/src/file_backend_native.mbt @@ -22,8 +22,8 @@ fn close_file_handle_internal(handle : FileHandle) -> Bool { } ///| -fn file_size_internal(handle : FileHandle) -> Int { - @utils.file_size_internal(handle) +fn file_size_i64_internal(handle : FileHandle) -> Int64 { + @utils.file_size_i64_internal(handle) } ///| diff --git a/src/file_backend_stub.mbt b/src/file_backend_stub.mbt index 49bfd39..a57aa26 100644 --- a/src/file_backend_stub.mbt +++ b/src/file_backend_stub.mbt @@ -22,8 +22,8 @@ fn close_file_handle_internal(handle : FileHandle) -> Bool { } ///| -fn file_size_internal(handle : FileHandle) -> Int { - @utils.file_size_internal(handle) +fn file_size_i64_internal(handle : FileHandle) -> Int64 { + @utils.file_size_i64_internal(handle) } ///| diff --git a/src/presets.mbt b/src/presets.mbt index d8cd28c..94696e3 100644 --- a/src/presets.mbt +++ b/src/presets.mbt @@ -110,3 +110,29 @@ pub fn with_file_rotation( _ => config } } + +///| +pub fn with_file_rotation_i64( + config : LoggerConfig, + max_bytes : Int64, + max_backups? : Int = 1, +) -> LoggerConfig { + match config.sink.kind { + SinkKind::File => + LoggerConfig::new( + min_level=config.min_level, + target=config.target, + timestamp=config.timestamp, + sink=SinkConfig::new( + kind=config.sink.kind, + path=config.sink.path, + append=config.sink.append, + auto_flush=config.sink.auto_flush, + rotation=Some(file_rotation_i64(max_bytes, max_backups~)), + text_formatter=config.sink.text_formatter, + ), + queue=config.queue, + ) + _ => config + } +} diff --git a/src/presets_test.mbt b/src/presets_test.mbt index e00c4ae..956586e 100644 --- a/src/presets_test.mbt +++ b/src/presets_test.mbt @@ -160,6 +160,28 @@ test "queue helper preserves file rotation when applied after rotation" { } } +///| +test "file rotation i64 helper preserves file preset shape" { + let config = with_file_rotation_i64( + with_queue(file("service.log", append=false), max_pending=8), + 4294967296L, + max_backups=2, + ) + inspect(config.sink.path, content="service.log") + inspect(config.sink.append, content="false") + match config.sink.rotation { + Some(rotation) => { + inspect(rotation.max_bytes, content="2147483647") + inspect(rotation.max_backups, content="2") + match rotation.native_wide_max_bytes { + Some(value) => inspect(value, content="4294967296") + None => inspect(false, content="true") + } + } + None => inspect(false, content="true") + } +} + ///| test "file rotation helper leaves non-file presets unchanged" { let console_config = with_file_rotation( diff --git a/src/sinks_file.mbt b/src/sinks_file.mbt index 19bda74..260de4e 100644 --- a/src/sinks_file.mbt +++ b/src/sinks_file.mbt @@ -29,6 +29,14 @@ pub fn file_rotation(max_bytes : Int, max_backups? : Int = 1) -> FileRotation { @utils.file_rotation(max_bytes, max_backups~) } +///| +pub fn file_rotation_i64( + max_bytes : Int64, + max_backups? : Int = 1, +) -> FileRotation { + @utils.file_rotation_i64(max_bytes, max_backups~) +} + ///| pub fn native_files_supported() -> Bool { native_files_supported_internal() @@ -216,11 +224,21 @@ fn policy_rotation_equals_internal( match (left, right) { (None, None) => true (Some(a), Some(b)) => - a.max_bytes == b.max_bytes && a.max_backups == b.max_backups + a.max_bytes == b.max_bytes && + a.max_backups == b.max_backups && + a.native_wide_max_bytes == b.native_wide_max_bytes _ => false } } +///| +fn rotation_max_bytes_internal(rotation : FileRotation) -> Int64 { + match rotation.native_wide_max_bytes { + Some(value) => value + None => rotation.max_bytes.to_int64() + } +} + ///| pub fn FileSink::state(self : FileSink) -> FileSinkState { FileSinkState::new( @@ -316,8 +334,9 @@ fn rotate_if_needed_internal(sink : FileSink, next_line_bytes : Int) -> Bool { match sink.handle.val { None => false Some(handle) => { - let size = file_size_internal(handle) - if size + next_line_bytes <= rotation.max_bytes { + let size = file_size_i64_internal(handle) + let next_line = next_line_bytes.to_int64() + if size + next_line <= rotation_max_bytes_internal(rotation) { true } else { let rotated = rotate_file_sink_internal(sink, rotation) diff --git a/src/utils/config.mbt b/src/utils/config.mbt index 78b972c..b21efd4 100644 --- a/src/utils/config.mbt +++ b/src/utils/config.mbt @@ -245,6 +245,30 @@ fn get_int( } } +///| +fn get_optional_int64_string( + obj : Map[String, @json_parser.JsonValue], + key : String, +) -> Int64? raise ConfigError { + match obj.get(key) { + None => None + Some(value) => + match value.as_string() { + Some(text) => + Some( + @string.parse_int64(text) catch { + _ => + raise ConfigError::InvalidConfig( + "Expected int64 string at key " + key, + ) + }, + ) + None => + raise ConfigError::InvalidConfig("Expected string at key " + key) + } + } +} + ///| fn parse_level(name : String) -> @core.Level raise ConfigError { match name.to_upper() { @@ -403,10 +427,11 @@ fn parse_file_rotation_config( value : @json_parser.JsonValue, ) -> FileRotation raise ConfigError { let obj = expect_object(value, "sink.rotation") - file_rotation( - get_int(obj, "max_bytes", default=1), - max_backups=get_int(obj, "max_backups", default=1), - ) + let max_backups = get_int(obj, "max_backups", default=1) + match get_optional_int64_string(obj, "max_bytes_i64") { + Some(max_bytes_i64) => file_rotation_i64(max_bytes_i64, max_backups~) + None => file_rotation(get_int(obj, "max_bytes", default=1), max_backups~) + } } ///| @@ -570,12 +595,18 @@ pub fn stringify_text_formatter_config( pub fn file_rotation_config_to_json( config : FileRotation, ) -> @json_parser.JsonValue { - @json_parser.JsonValue::Object({ + let obj : Map[String, @json_parser.JsonValue] = { "max_bytes": @json_parser.JsonValue::Number(config.max_bytes.to_double()), "max_backups": @json_parser.JsonValue::Number( config.max_backups.to_double(), ), - }) + } + match config.native_wide_max_bytes { + Some(value) => + obj["max_bytes_i64"] = @json_parser.JsonValue::String(value.to_string()) + None => () + } + @json_parser.JsonValue::Object(obj) } ///| diff --git a/src/utils/file_backend_native.mbt b/src/utils/file_backend_native.mbt index e302fc5..bbf79a8 100644 --- a/src/utils/file_backend_native.mbt +++ b/src/utils/file_backend_native.mbt @@ -66,7 +66,7 @@ extern "C" fn file_seek_ffi( ) -> Int = "bitlogger_file_seek" ///| -extern "C" fn file_tell_ffi(handle : NativeFileHandle) -> Int = "bitlogger_file_tell" +extern "C" fn file_tell_i64_ffi(handle : NativeFileHandle) -> Int64 = "bitlogger_file_tell_i64" ///| #borrow(from_path, to_path) @@ -114,11 +114,11 @@ pub fn close_file_handle_internal(handle : FileHandle) -> Bool { } ///| -pub fn file_size_internal(handle : FileHandle) -> Int { +pub fn file_size_i64_internal(handle : FileHandle) -> Int64 { ignore(file_seek_ffi(handle.raw, 0, 2)) - let size = file_tell_ffi(handle.raw) - if size < 0 { - 0 + let size = file_tell_i64_ffi(handle.raw) + if size < 0L { + 0L } else { size } diff --git a/src/utils/file_backend_stub.mbt b/src/utils/file_backend_stub.mbt index ed24aee..0733a57 100644 --- a/src/utils/file_backend_stub.mbt +++ b/src/utils/file_backend_stub.mbt @@ -35,9 +35,9 @@ pub fn close_file_handle_internal(handle : FileHandle) -> Bool { } ///| -pub fn file_size_internal(handle : FileHandle) -> Int { +pub fn file_size_i64_internal(handle : FileHandle) -> Int64 { ignore(handle) - 0 + 0L } ///| diff --git a/src/utils/moon.pkg b/src/utils/moon.pkg index c8d344f..30f3652 100644 --- a/src/utils/moon.pkg +++ b/src/utils/moon.pkg @@ -4,6 +4,7 @@ import { "moonbitlang/core/env", "moonbitlang/core/json", "moonbitlang/core/ref", + "moonbitlang/core/string", } options( diff --git a/src/utils/sink_models.mbt b/src/utils/sink_models.mbt index 7dad8df..91fc46c 100644 --- a/src/utils/sink_models.mbt +++ b/src/utils/sink_models.mbt @@ -2,6 +2,7 @@ pub struct FileRotation { max_bytes : Int max_backups : Int + native_wide_max_bytes : Int64? } ///| @@ -71,6 +72,29 @@ pub fn file_rotation(max_bytes : Int, max_backups? : Int = 1) -> FileRotation { } else { max_backups }, + native_wide_max_bytes: None, + } +} + +///| +pub fn file_rotation_i64( + max_bytes : Int64, + max_backups? : Int = 1, +) -> FileRotation { + let normalized = if max_bytes <= 0L { 1L } else { max_bytes } + let clamped_max_bytes = if normalized > 2147483647L { + 2147483647 + } else { + normalized.to_int() + } + { + max_bytes: clamped_max_bytes, + max_backups: if max_backups <= 0 { + 1 + } else { + max_backups + }, + native_wide_max_bytes: Some(normalized), } } diff --git a/src/utils/stub.c b/src/utils/stub.c index c794bfd..df33982 100644 --- a/src/utils/stub.c +++ b/src/utils/stub.c @@ -37,9 +37,13 @@ int32_t bitlogger_file_seek(void *handle, int32_t offset, int32_t origin) { return fseek((FILE *)handle, offset, origin); } -int32_t bitlogger_file_tell(void *handle) { - long position = ftell((FILE *)handle); - return position < 0 ? -1 : (int32_t)position; +int64_t bitlogger_file_tell_i64(void *handle) { +#if defined(_WIN32) + __int64 position = _ftelli64((FILE *)handle); +#else + long long position = ftello((FILE *)handle); +#endif + return position < 0 ? -1 : (int64_t)position; } int32_t bitlogger_file_rename(const char *from_path, const char *to_path) {