diff --git a/Cargo.lock b/Cargo.lock index 135d7bd946..5fc0853d90 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -103,7 +103,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" dependencies = [ "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -220,7 +220,7 @@ dependencies = [ "actix-router", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -657,7 +657,7 @@ checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", "synstructure", ] @@ -669,7 +669,7 @@ checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -763,7 +763,7 @@ dependencies = [ "async-trait", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", "tokio", ] @@ -821,7 +821,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -838,7 +838,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -987,7 +987,7 @@ checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -1043,9 +1043,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" -version = "1.8.1" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e050f626429857a27ddccb31e0aca21356bfa709c04041aefddac081a8f068a" +checksum = "7d809780667f4410e7c41b07f52439b94d2bdf8528eeedc287fa38d3b7f95d82" [[package]] name = "bdd" @@ -1335,7 +1335,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -1358,7 +1358,7 @@ dependencies = [ "proc-macro-crate 3.4.0", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -1586,7 +1586,7 @@ checksum = "5ec2398273c047c67d69794a924b1a2a5c14a5fab6bcbe8b24e86a0df9328e5e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -1615,9 +1615,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.53" +version = "4.5.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" +checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394" dependencies = [ "clap_builder", "clap_derive", @@ -1625,9 +1625,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.53" +version = "4.5.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" +checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00" dependencies = [ "anstream", "anstyle", @@ -1638,9 +1638,9 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.5.63" +version = "4.5.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90ef1fcbbf16b486489d0df91725ccc653c07115dd61f46363162535b74c6bc3" +checksum = "4c0da80818b2d95eca9aa614a30783e42f62bf5fdfee24e68cfb960b071ba8d1" dependencies = [ "clap", ] @@ -1654,7 +1654,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -1825,7 +1825,7 @@ dependencies = [ "proc-macro-crate 3.4.0", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -2311,7 +2311,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "syn 2.0.111", + "syn 2.0.113", "synthez", ] @@ -2353,7 +2353,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -2457,7 +2457,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -2471,7 +2471,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -2484,7 +2484,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -2495,7 +2495,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core 0.20.11", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -2506,7 +2506,7 @@ checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ "darling_core 0.21.3", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -2517,7 +2517,7 @@ checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ "darling_core 0.23.0", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -2617,7 +2617,7 @@ checksum = "2cdc8d50f426189eef89dac62fabfa0abb27d5cc008f25bf4156a0203325becc" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -2628,7 +2628,7 @@ checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -2649,7 +2649,7 @@ dependencies = [ "darling 0.20.11", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -2659,7 +2659,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" dependencies = [ "derive_builder_core", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -2681,7 +2681,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.111", + "syn 2.0.113", "unicode-xid", ] @@ -2755,7 +2755,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -2784,7 +2784,7 @@ checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -2988,7 +2988,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -3057,7 +3057,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -3170,9 +3170,9 @@ dependencies = [ [[package]] name = "ferroid" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce161062fb044bd629c2393590efd47cab8d0241faf15704ffb0d47b7b4e4a35" +checksum = "bb330bbd4cb7a5b9f559427f06f98a4f853a137c8298f3bd3f8ca57663e21986" dependencies = [ "portable-atomic", "rand 0.9.2", @@ -3218,9 +3218,9 @@ dependencies = [ [[package]] name = "file-operation" -version = "0.8.8" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34664772b984b86dbccb14ab56722778cca458b38f6f324db3331141ef441e05" +checksum = "273da42359c6082af66506c15599d72f1b3f20e00fcc335dd9cbeed4b23c0f4e" dependencies = [ "tokio", ] @@ -3354,6 +3354,16 @@ dependencies = [ "tokio", ] +[[package]] +name = "fs2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "fs_extra" version = "1.3.0" @@ -3455,7 +3465,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -3579,7 +3589,7 @@ dependencies = [ "quote", "serde", "serde_json", - "syn 2.0.111", + "syn 2.0.113", "textwrap", "thiserror 2.0.17", "typed-builder 0.23.2", @@ -3803,7 +3813,7 @@ dependencies = [ "proc-macro-crate 1.3.1", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -3902,9 +3912,9 @@ dependencies = [ [[package]] name = "handlebars" -version = "6.3.2" +version = "6.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "759e2d5aea3287cb1190c8ec394f42866cb5bf74fcbf213f354e3c856ea26098" +checksum = "9b3f9296c208515b87bd915a2f5d1163d4b3f863ba83337d7713cf478055948e" dependencies = [ "derive_builder", "log", @@ -4209,7 +4219,7 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", - "webpki-roots 1.0.4", + "webpki-roots 1.0.5", ] [[package]] @@ -4502,7 +4512,7 @@ dependencies = [ "tracing", "trait-variant", "tungstenite", - "webpki-roots 1.0.4", + "webpki-roots 1.0.5", ] [[package]] @@ -4936,7 +4946,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "699c1b6d335e63d0ba5c1e1c7f647371ce989c3bcbe1f7ed2b85fa56e3bd1a21" dependencies = [ "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -5148,7 +5158,7 @@ checksum = "b787bebb543f8969132630c51fd0afab173a86c6abae56ff3b9e5e3e3f9f6e58" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -5387,9 +5397,9 @@ checksum = "2c4a545a15244c7d945065b5d392b2d2d7f21526fba56ce51467b06ed445e8f7" [[package]] name = "libc" -version = "0.2.178" +version = "0.2.179" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" +checksum = "c5a2d376baa530d1238d133232d15e239abad80d05838b4b59354e5268af431f" [[package]] name = "libdbus-sys" @@ -5593,7 +5603,7 @@ dependencies = [ "quote", "regex-syntax", "rustc_version", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -5731,7 +5741,7 @@ checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -5810,7 +5820,7 @@ dependencies = [ "cfg-if", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -6135,9 +6145,9 @@ dependencies = [ [[package]] name = "octocrab" -version = "0.49.4" +version = "0.49.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "add74bcdaf5b0a0c3edb0f8c0a952d7a52d9e9a76ac9c690a3e477c4e8c343b9" +checksum = "89f6f72d7084a80bf261bb6b6f83bd633323d5633d5ec7988c6c95b20448b2b5" dependencies = [ "arc-swap", "async-trait", @@ -6253,7 +6263,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -6542,7 +6552,7 @@ dependencies = [ "regex", "regex-syntax", "structmeta", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -6615,7 +6625,7 @@ dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -6672,9 +6682,9 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pest" -version = "2.8.4" +version = "2.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbcfd20a6d4eeba40179f05735784ad32bdaef05ce8e8af05f180d45bb3e7e22" +checksum = "2c9eb05c21a464ea704b53158d358a31e6425db2f63a1a7312268b05fe2b75f7" dependencies = [ "memchr", "ucd-trie", @@ -6682,9 +6692,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.8.4" +version = "2.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51f72981ade67b1ca6adc26ec221be9f463f2b5839c7508998daa17c23d94d7f" +checksum = "68f9dbced329c441fa79d80472764b1a2c7e57123553b8519b36663a2fb234ed" dependencies = [ "pest", "pest_generator", @@ -6692,22 +6702,22 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.8.4" +version = "2.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dee9efd8cdb50d719a80088b76f81aec7c41ed6d522ee750178f83883d271625" +checksum = "3bb96d5051a78f44f43c8f712d8e810adb0ebf923fc9ed2655a7f66f63ba8ee5" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] name = "pest_meta" -version = "2.8.4" +version = "2.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf1d70880e76bdc13ba52eafa6239ce793d85c8e43896507e43dd8984ff05b82" +checksum = "602113b5b5e8621770cfd490cfd90b9f84ab29bd2b0e49ad83eb6d186cef2365" dependencies = [ "pest", "sha2", @@ -6730,7 +6740,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -6910,7 +6920,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -6982,7 +6992,7 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", "version_check", "yansi", ] @@ -7007,7 +7017,7 @@ checksum = "9adf1691c04c0a5ff46ff8f262b58beb07b0dbb61f96f9f54f6cbd82106ed87f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -7030,7 +7040,7 @@ dependencies = [ "itertools 0.14.0", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -7377,7 +7387,7 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -7492,7 +7502,7 @@ dependencies = [ "wasm-bindgen-futures", "wasm-streams", "web-sys", - "webpki-roots 1.0.4", + "webpki-roots 1.0.5", ] [[package]] @@ -7663,7 +7673,7 @@ dependencies = [ "proc-macro2", "quote", "serde_json", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -7695,6 +7705,15 @@ dependencies = [ "byteorder", ] +[[package]] +name = "rolling-file" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8395b4f860856b740f20a296ea2cd4d823e81a2658cf05ef61be22916026a906" +dependencies = [ + "chrono", +] + [[package]] name = "route-recognizer" version = "0.3.1" @@ -7741,7 +7760,7 @@ dependencies = [ "proc-macro2", "quote", "rust-embed-utils", - "syn 2.0.111", + "syn 2.0.113", "walkdir", ] @@ -7978,7 +7997,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -8013,7 +8032,7 @@ checksum = "22f968c5ea23d555e670b449c1c5e7b2fc399fdaec1d304a17cd48e288abc107" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -8145,7 +8164,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -8156,7 +8175,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -8191,7 +8210,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -8252,7 +8271,7 @@ dependencies = [ "darling 0.21.3", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -8270,11 +8289,12 @@ dependencies = [ [[package]] name = "serial_test" -version = "3.2.0" +version = "3.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b258109f244e1d6891bf1053a55d63a5cd4f8f4c30cf9a1280989f80e7a1fa9" +checksum = "0d0b343e184fc3b7bb44dff0705fffcf4b3756ba6aff420dddd8b24ca145e555" dependencies = [ - "futures", + "futures-executor", + "futures-util", "log", "once_cell", "parking_lot", @@ -8284,13 +8304,13 @@ dependencies = [ [[package]] name = "serial_test_derive" -version = "3.2.0" +version = "3.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d69265a08751de7844521fd15003ae0a888e035773ba05695c5c759a6f89eef" +checksum = "6f50427f258fb77356e4cd4aa0e87e2bd2c66dbcee41dc405282cae2bfc26c83" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -8325,6 +8345,7 @@ dependencies = [ "figlet-rs", "figment", "flume 0.12.0", + "fs2", "futures", "hash32 1.0.0", "human-repr", @@ -8348,6 +8369,7 @@ dependencies = [ "reqwest", "ringbuffer", "rmp-serde", + "rolling-file", "rust-embed", "rustls", "rustls-pemfile", @@ -8508,7 +8530,7 @@ checksum = "0eb01866308440fc64d6c44d9e86c5cc17adfe33c4d6eed55da9145044d0ffc1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -8535,7 +8557,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -8653,7 +8675,7 @@ dependencies = [ "quote", "sqlx-core", "sqlx-macros-core", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -8676,7 +8698,7 @@ dependencies = [ "sqlx-mysql", "sqlx-postgres", "sqlx-sqlite", - "syn 2.0.111", + "syn 2.0.113", "tokio", "url", ] @@ -8819,7 +8841,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", "toml 0.8.23", ] @@ -8849,7 +8871,7 @@ dependencies = [ "proc-macro2", "quote", "structmeta-derive", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -8860,7 +8882,7 @@ checksum = "152a0b65a590ff6c3da95cabe2353ee04e6167c896b28e3b14478c2636c922fc" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -8888,7 +8910,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -8900,7 +8922,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -8922,9 +8944,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.111" +version = "2.0.113" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +checksum = "678faa00651c9eb72dd2020cbdf275d92eccb2400d568e419efdd64838145cb4" dependencies = [ "proc-macro2", "quote", @@ -8948,7 +8970,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -8957,7 +8979,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d8a928f38f1bc873f28e0d2ba8298ad65374a6ac2241dabd297271531a736cd" dependencies = [ - "syn 2.0.111", + "syn 2.0.113", "synthez-codegen", "synthez-core", ] @@ -8968,7 +8990,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fb83b8df4238e11746984dfb3819b155cd270de0e25847f45abad56b3671047" dependencies = [ - "syn 2.0.111", + "syn 2.0.113", "synthez-core", ] @@ -8981,7 +9003,7 @@ dependencies = [ "proc-macro2", "quote", "sealed", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -9081,7 +9103,7 @@ dependencies = [ "cfg-if", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -9092,7 +9114,7 @@ checksum = "5c89e72a01ed4c579669add59014b9a524d609c0c88c6a585ce37485879f6ffb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", "test-case-core", ] @@ -9172,7 +9194,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -9183,7 +9205,7 @@ checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -9286,9 +9308,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.48.0" +version = "1.49.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" dependencies = [ "bytes", "libc", @@ -9309,7 +9331,7 @@ checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -9324,9 +9346,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.17" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" dependencies = [ "futures-core", "pin-project-lite", @@ -9351,9 +9373,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.17" +version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" dependencies = [ "bytes", "futures-core", @@ -9623,7 +9645,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -9692,7 +9714,7 @@ checksum = "70977707304198400eb4835a78f6a9f928bf41bba420deb8fdb175cd965d77a7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -9764,7 +9786,7 @@ checksum = "f9534daa9fd3ed0bd911d462a37f172228077e7abf18c18a5f67199d959205f8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -9775,7 +9797,7 @@ checksum = "3c36781cc0e46a83726d9879608e4cf6c2505237e263a8eb8c24502989cfdb28" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -9786,7 +9808,7 @@ checksum = "076a02dc54dd46795c2e9c8282ed40bcfb1e22747e955de9389a1de28190fb26" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -9919,7 +9941,7 @@ dependencies = [ "rustls-pki-types", "ureq-proto", "utf-8", - "webpki-roots 1.0.4", + "webpki-roots 1.0.5", ] [[package]] @@ -10169,7 +10191,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", "wasm-bindgen-shared", ] @@ -10232,9 +10254,9 @@ dependencies = [ [[package]] name = "webpki-root-certs" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee3e3b5f5e80bc89f30ce8d0343bf4e5f12341c51f3e26cbeecbc7c85443e85b" +checksum = "36a29fc0408b113f68cf32637857ab740edfafdf460c326cd2afaa2d84cc05dc" dependencies = [ "rustls-pki-types", ] @@ -10245,14 +10267,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ - "webpki-roots 1.0.4", + "webpki-roots 1.0.5", ] [[package]] name = "webpki-roots" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e" +checksum = "12bed680863276c63889429bfd6cab3b99943659923822de1c8a39c49e4d722c" dependencies = [ "rustls-pki-types", ] @@ -10393,7 +10415,7 @@ checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -10404,7 +10426,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -10415,7 +10437,7 @@ checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -10426,7 +10448,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -10921,7 +10943,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -10951,7 +10973,7 @@ checksum = "9e87a3ce33434ab66a700edbaf2cc8a417d9b89f00a6fd8216fd6ac83b0e7b1c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -10973,7 +10995,7 @@ checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", "synstructure", ] @@ -10994,7 +11016,7 @@ checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -11014,7 +11036,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", "synstructure", ] @@ -11035,7 +11057,7 @@ checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -11068,7 +11090,7 @@ checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -11107,9 +11129,9 @@ checksum = "40990edd51aae2c2b6907af74ffb635029d5788228222c4bb811e9351c0caad3" [[package]] name = "zmij" -version = "1.0.3" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9747e91771f56fd7893e1164abd78febd14a670ceec257caad15e051de35f06" +checksum = "30e0d8dffbae3d840f64bda38e28391faef673a7b5a6017840f2a106c8145868" [[package]] name = "zopfli" diff --git a/DEPENDENCIES.md b/DEPENDENCIES.md index 1468dcbb0d..a3c4e0e03e 100644 --- a/DEPENDENCIES.md +++ b/DEPENDENCIES.md @@ -84,7 +84,7 @@ backon: 1.6.0, "Apache-2.0", base16ct: 0.2.0, "Apache-2.0 OR MIT", base64: 0.21.7, "Apache-2.0 OR MIT", base64: 0.22.1, "Apache-2.0 OR MIT", -base64ct: 1.8.1, "Apache-2.0 OR MIT", +base64ct: 1.8.2, "Apache-2.0 OR MIT", bdd: 0.0.1, "Apache-2.0", beef: 0.5.2, "Apache-2.0 OR MIT", bench-dashboard-frontend: 0.4.1, "Apache-2.0", @@ -136,9 +136,9 @@ charming: 0.6.0, "Apache-2.0 OR MIT", charming_macros: 0.1.0, "Apache-2.0 OR MIT", chrono: 0.4.42, "Apache-2.0 OR MIT", cipher: 0.4.4, "Apache-2.0 OR MIT", -clap: 4.5.53, "Apache-2.0 OR MIT", -clap_builder: 4.5.53, "Apache-2.0 OR MIT", -clap_complete: 4.5.63, "Apache-2.0 OR MIT", +clap: 4.5.54, "Apache-2.0 OR MIT", +clap_builder: 4.5.54, "Apache-2.0 OR MIT", +clap_complete: 4.5.64, "Apache-2.0 OR MIT", clap_derive: 4.5.49, "Apache-2.0 OR MIT", clap_lex: 0.7.6, "Apache-2.0 OR MIT", clock: 0.1.0, "N/A", @@ -280,12 +280,12 @@ extension-traits: 1.0.1, "Apache-2.0 OR MIT OR Zlib", fast-async-mutex: 0.6.7, "Apache-2.0 OR MIT", fastbloom: 0.14.0, "Apache-2.0 OR MIT", fastrand: 2.3.0, "Apache-2.0 OR MIT", -ferroid: 0.8.8, "Apache-2.0 OR MIT", +ferroid: 0.8.9, "Apache-2.0 OR MIT", ff: 0.13.1, "Apache-2.0 OR MIT", fiat-crypto: 0.2.9, "Apache-2.0 OR BSD-1-Clause OR MIT", figlet-rs: 0.1.5, "Apache-2.0", figment: 0.10.19, "Apache-2.0 OR MIT", -file-operation: 0.8.8, "MIT", +file-operation: 0.8.10, "MIT", filetime: 0.2.26, "Apache-2.0 OR MIT", find-msvc-tools: 0.1.6, "Apache-2.0 OR MIT", flatbuffers: 25.12.19, "Apache-2.0", @@ -301,6 +301,7 @@ foreign-types-shared: 0.1.1, "Apache-2.0 OR MIT", form_urlencoded: 1.2.2, "Apache-2.0 OR MIT", fragile: 2.0.1, "Apache-2.0", fs-err: 3.2.2, "Apache-2.0 OR MIT", +fs2: 0.4.3, "Apache-2.0 OR MIT", fs_extra: 1.3.0, "MIT", fsevent-sys: 4.1.0, "MIT", funty: 2.0.0, "MIT", @@ -345,7 +346,7 @@ h2: 0.3.27, "MIT", h2: 0.4.12, "MIT", half: 2.7.1, "Apache-2.0 OR MIT", halfbrown: 0.4.0, "Apache-2.0 OR MIT", -handlebars: 6.3.2, "MIT", +handlebars: 6.4.0, "MIT", hash32: 0.2.1, "Apache-2.0 OR MIT", hash32: 1.0.0, "Apache-2.0 OR MIT", hashbrown: 0.12.3, "Apache-2.0 OR MIT", @@ -457,7 +458,7 @@ lexical-util: 1.0.7, "Apache-2.0 OR MIT", lexical-write-float: 1.0.6, "Apache-2.0 OR MIT", lexical-write-integer: 1.0.6, "Apache-2.0 OR MIT", libbz2-rs-sys: 0.2.2, "bzip2-1.0.6", -libc: 0.2.178, "Apache-2.0 OR MIT", +libc: 0.2.179, "Apache-2.0 OR MIT", libdbus-sys: 0.2.7, "Apache-2.0 OR MIT", libflate: 2.2.1, "MIT", libflate_lz77: 2.2.0, "MIT", @@ -536,7 +537,7 @@ objc2: 0.6.3, "MIT", objc2-core-foundation: 0.3.2, "Apache-2.0 OR MIT OR Zlib", objc2-encode: 4.1.0, "MIT", objc2-io-kit: 0.3.2, "Apache-2.0 OR MIT OR Zlib", -octocrab: 0.49.4, "Apache-2.0 OR MIT", +octocrab: 0.49.5, "Apache-2.0 OR MIT", oid-registry: 0.8.1, "Apache-2.0 OR MIT", once_cell: 1.21.3, "Apache-2.0 OR MIT", once_cell_polyfill: 1.70.2, "Apache-2.0 OR MIT", @@ -582,10 +583,10 @@ peg-runtime: 0.6.3, "MIT", pem: 3.0.6, "MIT", pem-rfc7468: 0.7.0, "Apache-2.0 OR MIT", percent-encoding: 2.3.2, "Apache-2.0 OR MIT", -pest: 2.8.4, "Apache-2.0 OR MIT", -pest_derive: 2.8.4, "Apache-2.0 OR MIT", -pest_generator: 2.8.4, "Apache-2.0 OR MIT", -pest_meta: 2.8.4, "Apache-2.0 OR MIT", +pest: 2.8.5, "Apache-2.0 OR MIT", +pest_derive: 2.8.5, "Apache-2.0 OR MIT", +pest_generator: 2.8.5, "Apache-2.0 OR MIT", +pest_meta: 2.8.5, "Apache-2.0 OR MIT", pin-project: 1.1.10, "Apache-2.0 OR MIT", pin-project-internal: 1.1.10, "Apache-2.0 OR MIT", pin-project-lite: 0.2.16, "Apache-2.0 OR MIT", @@ -673,6 +674,7 @@ rmcp-macros: 0.12.0, "MIT", rmp: 0.8.15, "MIT", rmp-serde: 1.3.1, "MIT", roaring: 0.10.12, "Apache-2.0 OR MIT", +rolling-file: 0.2.0, "Apache-2.0 OR MIT", route-recognizer: 0.3.1, "MIT", rsa: 0.9.9, "Apache-2.0 OR MIT", rust-embed: 8.9.0, "MIT", @@ -727,8 +729,8 @@ serde_urlencoded: 0.7.1, "Apache-2.0 OR MIT", serde_with: 3.16.1, "Apache-2.0 OR MIT", serde_with_macros: 3.16.1, "Apache-2.0 OR MIT", serde_yaml_ng: 0.10.0, "MIT", -serial_test: 3.2.0, "MIT", -serial_test_derive: 3.2.0, "MIT", +serial_test: 3.3.1, "MIT", +serial_test_derive: 3.3.1, "MIT", server: 0.6.1-edge.4, "Apache-2.0", sha1: 0.10.6, "Apache-2.0 OR MIT", sha2: 0.10.9, "Apache-2.0 OR MIT", @@ -774,7 +776,7 @@ strum_macros: 0.26.4, "MIT", strum_macros: 0.27.2, "MIT", subtle: 2.6.1, "BSD-3-Clause", syn: 1.0.109, "Apache-2.0 OR MIT", -syn: 2.0.111, "Apache-2.0 OR MIT", +syn: 2.0.113, "Apache-2.0 OR MIT", sync_wrapper: 1.0.2, "Apache-2.0", synstructure: 0.13.2, "MIT", synthez: 0.4.0, "BlueOak-1.0.0", @@ -808,12 +810,12 @@ tiny-keccak: 2.0.2, "CC0-1.0", tinystr: 0.8.2, "Unicode-3.0", tinyvec: 1.10.0, "Apache-2.0 OR MIT OR Zlib", tinyvec_macros: 0.1.1, "Apache-2.0 OR MIT OR Zlib", -tokio: 1.48.0, "MIT", +tokio: 1.49.0, "MIT", tokio-macros: 2.6.0, "MIT", tokio-rustls: 0.26.4, "Apache-2.0 OR MIT", -tokio-stream: 0.1.17, "MIT", +tokio-stream: 0.1.18, "MIT", tokio-tungstenite: 0.28.0, "MIT", -tokio-util: 0.7.17, "MIT", +tokio-util: 0.7.18, "MIT", tokise: 0.2.0, "Apache-2.0 OR MIT", toml: 0.8.23, "Apache-2.0 OR MIT", toml: 0.9.10+spec-1.1.0, "Apache-2.0 OR MIT", @@ -899,9 +901,9 @@ wasm-streams: 0.4.2, "Apache-2.0 OR MIT", wasmtimer: 0.4.3, "MIT", web-sys: 0.3.83, "Apache-2.0 OR MIT", web-time: 1.1.0, "Apache-2.0 OR MIT", -webpki-root-certs: 1.0.4, "CDLA-Permissive-2.0", +webpki-root-certs: 1.0.5, "CDLA-Permissive-2.0", webpki-roots: 0.26.11, "CDLA-Permissive-2.0", -webpki-roots: 1.0.4, "CDLA-Permissive-2.0", +webpki-roots: 1.0.5, "CDLA-Permissive-2.0", whoami: 1.6.1, "Apache-2.0 OR BSL-1.0 OR MIT", widestring: 1.2.1, "Apache-2.0 OR MIT", winapi: 0.3.9, "Apache-2.0 OR MIT", @@ -994,7 +996,7 @@ zerovec: 0.11.5, "Unicode-3.0", zerovec-derive: 0.11.2, "Unicode-3.0", zip: 7.0.0, "MIT", zlib-rs: 0.5.5, "Zlib", -zmij: 1.0.3, "MIT", +zmij: 1.0.10, "MIT", zopfli: 0.8.3, "Apache-2.0", zstd: 0.13.3, "MIT", zstd-safe: 7.2.4, "Apache-2.0 OR MIT", diff --git a/core/common/src/utils/duration.rs b/core/common/src/utils/duration.rs index cb256808ad..aa6c796a7e 100644 --- a/core/common/src/utils/duration.rs +++ b/core/common/src/utils/duration.rs @@ -29,6 +29,35 @@ use std::{ pub const SEC_IN_MICRO: u64 = 1_000_000; +/// A struct for representing time durations with various utility functions. +/// +/// This struct wraps `std::time::Duration` and uses the `humantime` crate for parsing and formatting +/// human-readable duration strings. It also implements serialization and deserialization via the `serde` crate. +/// +/// # Example +/// +/// ``` +/// use iggy_common::IggyDuration; +/// use std::str::FromStr; +/// +/// let duration = IggyDuration::from(3661_000_000_u64); // 3661 seconds in microseconds +/// assert_eq!(3661, duration.as_secs()); +/// assert_eq!("1h 1m 1s", duration.as_human_time_string()); +/// assert_eq!("1h 1m 1s", format!("{}", duration)); +/// +/// let duration = IggyDuration::from(0_u64); +/// assert_eq!(0, duration.as_secs()); +/// assert_eq!("0s", duration.as_human_time_string()); +/// assert_eq!("0s", format!("{}", duration)); +/// +/// let duration = IggyDuration::from_str("1h 1m 1s").unwrap(); +/// assert_eq!(3661, duration.as_secs()); +/// assert_eq!("1h 1m 1s", duration.as_human_time_string()); +/// assert_eq!("1h 1m 1s", format!("{}", duration)); +/// +/// let duration = IggyDuration::from_str("unlimited").unwrap(); +/// assert_eq!(0, duration.as_secs()); +/// ``` #[derive(Debug, Clone, Copy, Eq, PartialEq)] pub struct IggyDuration { duration: Duration, diff --git a/core/configs/server.toml b/core/configs/server.toml index cec71ddc74..da86c79e25 100644 --- a/core/configs/server.toml +++ b/core/configs/server.toml @@ -363,8 +363,17 @@ level = "info" # When enabled, logs are stored in {system.path}/{system.logging.path} (default: local_data/logs). file_enabled = true -# Maximum size of the log files before rotation. -max_size = "512 MB" +# Maximum size of a single log file before rotation occurs. +# When a log file reaches this size, it will be rotated (closed and a new file created). +# This setting works together with max_total_size to control log storage. +max_file_size = "512 MB" + +# Maximum total size of all log files combined. +# When this size is reached, oldest files will be deleted. +max_total_size = "4 GB" + +# Time interval for checking log file size and rotation. +rotation_check_interval = "1 h" # Time to retain log files before deletion. retention = "7 days" diff --git a/core/integration/tests/server/scenarios/log_rotation_scenario.rs b/core/integration/tests/server/scenarios/log_rotation_scenario.rs new file mode 100644 index 0000000000..eec1de2f81 --- /dev/null +++ b/core/integration/tests/server/scenarios/log_rotation_scenario.rs @@ -0,0 +1,240 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +use iggy::prelude::*; +use iggy_common::{ + CompressionAlgorithm, Identifier, IggyByteSize, IggyDuration, IggyExpiry, MaxTopicSize, +}; +use integration::test_server::{ClientFactory, login_root}; +use std::path::Path; +use std::time::Duration; +use tokio::fs; +use tokio::time::{sleep, timeout}; + +pub const MAX_SINGLE_LOG_SIZE: IggyByteSize = IggyByteSize::new(1_000_000); // 1MB +pub const MAX_TOTAL_LOG_SIZE: IggyByteSize = IggyByteSize::new(3_000_000); // 3MB +pub const LOG_ROTATION_CHECK_INTERVAL: IggyDuration = IggyDuration::ONE_SECOND; +const OPERATION_TIMEOUT_SECS: u64 = 10; +const OPERATION_LOOP_COUNT: usize = 2000; +const FROM_BYTES_TO_KB: u64 = 1000; +const IGGY_LOG_BASE_NAME: &str = "iggy-server.log"; + +pub async fn run(client_factory: &dyn ClientFactory, log_dir: &str) { + let log_path = Path::new(log_dir); + assert!( + log_path.exists() && log_path.is_dir(), + "failed::no_such_directory => {log_dir}" + ); + + let client = init_valid_client(client_factory).await; + assert!( + client.is_ok(), + "failed::client_initialize => {}", + client.unwrap_err() + ); + + let generator_result = generate_enough_logs(client.as_ref().unwrap()).await; + assert!( + generator_result.is_ok(), + "failed::generate_logs => {}", + generator_result.unwrap_err() + ); + + nocapture_observer(log_path).await; + sleep(LOG_ROTATION_CHECK_INTERVAL.get_duration()).await; + let rotation_result = validate_log_rotation_rules(log_path).await; + assert!( + rotation_result.is_ok(), + "failed::rotation_check => {}", + rotation_result.unwrap_err() + ); + nocapture_observer(log_path).await; +} + +async fn init_valid_client(client_factory: &dyn ClientFactory) -> Result { + let operation_timeout = IggyDuration::new(Duration::from_secs(OPERATION_TIMEOUT_SECS)); + let client_wrapper = timeout( + operation_timeout.get_duration(), + client_factory.create_client(), + ) + .await + .map_err(|_| "ClientWrapper creation timed out")?; + + timeout(operation_timeout.get_duration(), client_wrapper.connect()) + .await + .map_err(|_| "Client connection timed out")? + .map_err(|e| format!("Client connection failed: {e:?}"))?; + + let client = IggyClient::create(client_wrapper, None, None); + timeout(operation_timeout.get_duration(), login_root(&client)) + .await + .map_err(|e| format!("Root user login timed out: {e:?}"))?; + + Ok(client) +} + +async fn generate_enough_logs(client: &IggyClient) -> Result<(), String> { + for i in 0..OPERATION_LOOP_COUNT { + let stream_name = format!("stream_log_rotation_{i}"); + let topic_name = format!("topic_log_rotation_{i}"); + + client + .create_stream(&stream_name) + .await + .map_err(|e| format!("Failed to create {stream_name}: {e}"))?; + + let stream_identifier = Identifier::named(&stream_name) + .map_err(|e| format!("Failed to create stream label {e}"))?; + client + .create_topic( + &stream_identifier, + &topic_name, + 1, + CompressionAlgorithm::default(), + None, + IggyExpiry::NeverExpire, + MaxTopicSize::Unlimited, + ) + .await + .map_err(|e| format!("Failed to create topic {topic_name}: {e}"))?; + + client + .delete_stream(&stream_identifier) + .await + .map_err(|e| format!("Failed to remove stream {stream_name}: {e}"))?; + } + + Ok(()) +} + +async fn validate_log_rotation_rules(log_dir: &Path) -> Result<(), String> { + let mut dir_entries = fs::read_dir(log_dir).await.map_err(|e| { + format!( + "Failed to read log directory '{log_dir}': {e}", + log_dir = log_dir.display() + ) + })?; + + let mut valid_log_files = Vec::new(); + while let Some(entry) = dir_entries.next_entry().await.map_err(|e| { + format!( + "Failed to read next entry in log directory '{log_dir}': {e}", + log_dir = log_dir.display() + ) + })? { + let file_path = entry.path(); + + if !file_path.is_file() { + continue; + } + + let file_name = match file_path.file_name().and_then(|name| name.to_str()) { + Some(name) => name, + None => continue, + }; + + if is_valid_iggy_log_file(file_name) { + valid_log_files.push(file_path); + } + } + + if valid_log_files.is_empty() { + return Err(format!( + "No valid Iggy log files found in directory '{}'. Expected files matching '{}' (original) or '{}.' (archived).", + log_dir.display(), + IGGY_LOG_BASE_NAME, + IGGY_LOG_BASE_NAME + )); + } + + let mut total_log_size = IggyByteSize::new(0); + let max_single_kb = MAX_SINGLE_LOG_SIZE.as_bytes_u64() / FROM_BYTES_TO_KB; + let max_total_kb = MAX_TOTAL_LOG_SIZE.as_bytes_u64() / FROM_BYTES_TO_KB; + + for log_file in valid_log_files { + let file_metadata = fs::metadata(&log_file).await.map_err(|e| { + format!( + "Failed to get metadata for file '{}': {}", + log_file.display(), + e + ) + })?; + + let file_size_bytes = file_metadata.len(); + + // The delay in log writing in Iggy mainly depends on the processing speed + // of background threads and the operating system's I/O scheduling, which + // means that the actual size of written logs may be slightly larger than + // expected. So there ignores tiny minor overflow by comparing integer KB + // values instead of exact bytes. + let current_single_kb = file_size_bytes / FROM_BYTES_TO_KB; + if current_single_kb > max_single_kb { + return Err(format!( + "Single log file exceeds maximum allowed size: '{}'", + log_file.display() + )); + } + + total_log_size += IggyByteSize::new(file_size_bytes); + } + + let current_total_kb = total_log_size.as_bytes_u64() / FROM_BYTES_TO_KB; + if current_total_kb > max_total_kb { + return Err(format!( + "Total log size exceeds maximum allowed size: '{}'", + log_dir.display() + )); + } + + Ok(()) +} + +fn is_valid_iggy_log_file(file_name: &str) -> bool { + if file_name == IGGY_LOG_BASE_NAME { + return true; + } + + let archive_log_prefix = format!("{}.", IGGY_LOG_BASE_NAME); + if file_name.starts_with(&archive_log_prefix) { + let numeric_suffix = &file_name[archive_log_prefix.len()..]; + return !numeric_suffix.is_empty() && numeric_suffix.chars().all(|c| c.is_ascii_digit()); + } + false +} + +/// Solely for manual && direct observation of file status to reduce debugging overhead. +async fn nocapture_observer(log_path: &Path) -> () { + println!( + "\n{:>4} Size <-> Path && server::specific::log_rotation_should_launch::nocapture_observer", + "" + ); + let mut dir_entries = fs::read_dir(log_path).await.unwrap(); + while let Some(entry) = dir_entries.next_entry().await.unwrap() { + let file_path = entry.path(); + if file_path.is_file() { + let meta = fs::metadata(&file_path).await.unwrap(); + println!( + "{:>6} KB <-> {:<50}", + meta.len() / FROM_BYTES_TO_KB, + file_path.display() + ); + } + } + println!(); +} diff --git a/core/integration/tests/server/scenarios/mod.rs b/core/integration/tests/server/scenarios/mod.rs index 2c2fbab3a8..3befd8bd37 100644 --- a/core/integration/tests/server/scenarios/mod.rs +++ b/core/integration/tests/server/scenarios/mod.rs @@ -28,6 +28,7 @@ pub mod consumer_timestamp_polling_scenario; pub mod create_message_payload; pub mod delete_segments_scenario; pub mod encryption_scenario; +pub mod log_rotation_scenario; pub mod message_headers_scenario; pub mod message_size_scenario; pub mod read_during_persistence_scenario; diff --git a/core/integration/tests/server/specific.rs b/core/integration/tests/server/specific.rs index 205d6b5dff..b630d8c1ad 100644 --- a/core/integration/tests/server/specific.rs +++ b/core/integration/tests/server/specific.rs @@ -1,4 +1,5 @@ -/* Licensed to the Apache Software Foundation (ASF) under one +/* + * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file @@ -17,7 +18,8 @@ */ use crate::server::scenarios::{ - delete_segments_scenario, message_size_scenario, tcp_tls_scenario, websocket_tls_scenario, + delete_segments_scenario, log_rotation_scenario, message_size_scenario, tcp_tls_scenario, + websocket_tls_scenario, }; use iggy::prelude::*; use integration::{ @@ -25,6 +27,7 @@ use integration::{ test_server::{IpAddrKind, TestServer}, test_tls_utils::generate_test_certificates, }; +use log_rotation_scenario::{LOG_ROTATION_CHECK_INTERVAL, MAX_SINGLE_LOG_SIZE, MAX_TOTAL_LOG_SIZE}; use serial_test::parallel; use std::collections::HashMap; @@ -125,11 +128,46 @@ async fn tcp_tls_self_signed_scenario_should_be_valid() { .await .expect("Failed to connect TLS client with self-signed cert"); - let client = iggy::clients::client::IggyClient::create(ClientWrapper::Iggy(client), None, None); + let client = IggyClient::create(ClientWrapper::Iggy(client), None, None); tcp_tls_scenario::run(&client).await; } +#[tokio::test] +#[parallel] +async fn log_rotation_should_launch() { + let mut extra_envs = HashMap::new(); + extra_envs.insert( + "IGGY_SYSTEM_LOGGING_MAX_FILE_SIZE".to_string(), + format!("{}", MAX_SINGLE_LOG_SIZE), + ); + extra_envs.insert( + "IGGY_SYSTEM_LOGGING_MAX_TOTAL_SIZE".to_string(), + format!("{}", MAX_TOTAL_LOG_SIZE), + ); + extra_envs.insert( + "IGGY_SYSTEM_LOGGING_ROTATION_CHECK_INTERVAL".to_string(), + format!("{}", LOG_ROTATION_CHECK_INTERVAL), + ); + + let mut test_server = TestServer::new(Some(extra_envs), true, None, IpAddrKind::V4); + test_server.start(); + + let server_addr = test_server.get_raw_tcp_addr().unwrap(); + let client_factory = TcpClientFactory { + server_addr, + ..Default::default() + }; + + // Give the server some time to start and for log rotation thread to be initialized + tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; + + let log_path = format!("{}/logs", test_server.get_local_data_path()); + + test_server.assert_running(); + log_rotation_scenario::run(&client_factory, &log_path).await; +} + #[tokio::test] #[parallel] async fn websocket_tls_scenario_should_be_valid() { diff --git a/core/server/Cargo.toml b/core/server/Cargo.toml index fd72144be4..eb3ca1c595 100644 --- a/core/server/Cargo.toml +++ b/core/server/Cargo.toml @@ -66,6 +66,7 @@ error_set = { workspace = true } figlet-rs = { workspace = true } figment = { workspace = true } flume = { workspace = true } +fs2 = "0.4.3" futures = { workspace = true } hash32 = "1.0.0" human-repr = { workspace = true } @@ -101,6 +102,7 @@ rand = { workspace = true } reqwest = { workspace = true, features = ["rustls-tls-no-provider"] } ringbuffer = "0.16.0" rmp-serde = { workspace = true } +rolling-file = "0.2.0" rust-embed = { version = "8.9.0", optional = true } rustls = { workspace = true } rustls-pemfile = "2.2.0" diff --git a/core/server/src/configs/defaults.rs b/core/server/src/configs/defaults.rs index f6d9d83e5d..eebdb42439 100644 --- a/core/server/src/configs/defaults.rs +++ b/core/server/src/configs/defaults.rs @@ -402,7 +402,14 @@ impl Default for LoggingConfig { path: SERVER_CONFIG.system.logging.path.parse().unwrap(), level: SERVER_CONFIG.system.logging.level.parse().unwrap(), file_enabled: SERVER_CONFIG.system.logging.file_enabled, - max_size: SERVER_CONFIG.system.logging.max_size.parse().unwrap(), + max_file_size: SERVER_CONFIG.system.logging.max_file_size.parse().unwrap(), + max_total_size: SERVER_CONFIG.system.logging.max_total_size.parse().unwrap(), + rotation_check_interval: SERVER_CONFIG + .system + .logging + .rotation_check_interval + .parse() + .unwrap(), retention: SERVER_CONFIG.system.logging.retention.parse().unwrap(), sysinfo_print_interval: SERVER_CONFIG .system diff --git a/core/server/src/configs/displays.rs b/core/server/src/configs/displays.rs index 8973b356af..515011c255 100644 --- a/core/server/src/configs/displays.rs +++ b/core/server/src/configs/displays.rs @@ -170,7 +170,7 @@ impl Display for ServerConfig { } impl Display for MessageSaverConfig { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!( f, "{{ enabled: {}, enforce_fsync: {}, interval: {} }}", @@ -249,11 +249,13 @@ impl Display for LoggingConfig { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!( f, - "{{ path: {}, level: {}, file_enabled: {}, max_size: {}, retention: {} }}", + "{{ path: {}, level: {}, file_enabled: {}, max_file_size: {}, max_total_size: {}, rotation_check_interval: {}, retention: {} }}", self.path, self.level, self.file_enabled, - self.max_size.as_human_string_with_zero_as_unlimited(), + self.max_file_size.as_human_string_with_zero_as_unlimited(), + self.max_total_size.as_human_string_with_zero_as_unlimited(), + self.rotation_check_interval, self.retention ) } diff --git a/core/server/src/configs/system.rs b/core/server/src/configs/system.rs index c1b089a795..d9b19160ae 100644 --- a/core/server/src/configs/system.rs +++ b/core/server/src/configs/system.rs @@ -16,13 +16,12 @@ * under the License. */ +use super::cache_indexes::CacheIndexesConfig; +use super::sharding::ShardingConfig; use crate::configs::server::MemoryPoolConfig; use crate::slab::partitions; use crate::slab::streams; use crate::slab::topics; - -use super::cache_indexes::CacheIndexesConfig; -use super::sharding::ShardingConfig; use iggy_common::IggyByteSize; use iggy_common::IggyError; use iggy_common::IggyExpiry; @@ -87,7 +86,10 @@ pub struct LoggingConfig { pub path: String, pub level: String, pub file_enabled: bool, - pub max_size: IggyByteSize, + pub max_file_size: IggyByteSize, + pub max_total_size: IggyByteSize, + #[serde_as(as = "DisplayFromStr")] + pub rotation_check_interval: IggyDuration, #[serde_as(as = "DisplayFromStr")] pub retention: IggyDuration, #[serde_as(as = "DisplayFromStr")] diff --git a/core/server/src/configs/validators.rs b/core/server/src/configs/validators.rs index af12a4102a..e800214c5c 100644 --- a/core/server/src/configs/validators.rs +++ b/core/server/src/configs/validators.rs @@ -22,7 +22,7 @@ use super::server::{ DataMaintenanceConfig, MessageSaverConfig, MessagesMaintenanceConfig, TelemetryConfig, }; use super::sharding::{CpuAllocation, ShardingConfig}; -use super::system::{CompressionConfig, PartitionConfig}; +use super::system::{CompressionConfig, LoggingConfig, PartitionConfig}; use crate::configs::COMPONENT; use crate::configs::server::{MemoryPoolConfig, PersonalAccessTokenConfig, ServerConfig}; use crate::configs::sharding::NumaTopology; @@ -85,6 +85,13 @@ impl Validatable for ServerConfig { format!("{COMPONENT} (error: {e}) - failed to validate cluster config") })?; + self.system + .logging + .validate() + .error(|e: &iggy_common::ConfigurationError| { + format!("{COMPONENT} (error: {e}) - failed to validate logging config") + })?; + let topic_size = match self.system.topic.max_size { MaxTopicSize::Custom(size) => Ok(size.as_bytes_u64()), MaxTopicSize::Unlimited => Ok(u64::MAX), @@ -255,6 +262,41 @@ impl Validatable for PersonalAccessTokenConfig { } } +impl Validatable for LoggingConfig { + fn validate(&self) -> Result<(), ConfigurationError> { + if self.level.is_empty() { + error!("system.logging.level is supposed be configured"); + return Err(ConfigurationError::InvalidConfigurationValue); + } + + if self.retention.as_secs() < 1 { + error!( + "Configured system.logging.retention {} is less than minimum 1 second", + self.retention + ); + return Err(ConfigurationError::InvalidConfigurationValue); + } + + if self.rotation_check_interval.as_secs() < 1 { + error!( + "Configured system.logging.rotation_check_interval {} is less than minimum 1 second", + self.rotation_check_interval + ); + return Err(ConfigurationError::InvalidConfigurationValue); + } + + if self.max_total_size.as_bytes_u64() < self.max_file_size.as_bytes_u64() { + error!( + "Configured system.logging.max_total_size {} is less than system.logging.max_file_size {}", + self.max_total_size, self.max_file_size + ); + return Err(ConfigurationError::InvalidConfigurationValue); + } + + Ok(()) + } +} + impl Validatable for MemoryPoolConfig { fn validate(&self) -> Result<(), ConfigurationError> { if self.enabled && self.size == 0 { diff --git a/core/server/src/log/logger.rs b/core/server/src/log/logger.rs index 4c0e0691eb..4da4ec3114 100644 --- a/core/server/src/log/logger.rs +++ b/core/server/src/log/logger.rs @@ -1,4 +1,5 @@ -/* Licensed to the Apache Software Foundation (ASF) under one +/* + * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file @@ -21,6 +22,7 @@ use crate::configs::server::{TelemetryConfig, TelemetryTransport}; use crate::configs::system::LoggingConfig; use crate::log::runtime::CompioRuntime; use crate::server_error::LogError; +use iggy_common::IggyDuration; use opentelemetry::KeyValue; use opentelemetry::global; use opentelemetry::trace::TracerProvider; @@ -30,10 +32,14 @@ use opentelemetry_sdk::Resource; use opentelemetry_sdk::logs::log_processor_with_async_runtime; use opentelemetry_sdk::propagation::TraceContextPropagator; use opentelemetry_sdk::trace::span_processor_with_async_runtime; +use rolling_file::{BasicRollingFileAppender, RollingConditionBasic}; +use std::fs; use std::io::{self, Write}; use std::path::PathBuf; +use std::sync::atomic::AtomicBool; use std::sync::{Arc, Mutex}; -use tracing::{info, trace}; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; +use tracing::{debug, error, info, trace, warn}; use tracing_appender::non_blocking::WorkerGuard; use tracing_opentelemetry::OpenTelemetryLayer; use tracing_subscriber::field::{RecordFields, VisitOutput}; @@ -47,15 +53,16 @@ use tracing_subscriber::{ }; const IGGY_LOG_FILE_PREFIX: &str = "iggy-server.log"; +const MIN_DISK_SPACE_BYTES: u64 = 10 * 1024 * 1024; // Writer that does nothing struct NullWriter; impl Write for NullWriter { - fn write(&mut self, buf: &[u8]) -> std::io::Result { + fn write(&mut self, buf: &[u8]) -> io::Result { Ok(buf.len()) } - fn flush(&mut self) -> std::io::Result<()> { + fn flush(&mut self) -> io::Result<()> { Ok(()) } } @@ -63,13 +70,13 @@ impl Write for NullWriter { // Wrapper around Arc>> to implement Write struct VecStringWriter(Arc>>); impl Write for VecStringWriter { - fn write(&mut self, buf: &[u8]) -> std::io::Result { + fn write(&mut self, buf: &[u8]) -> io::Result { let mut lock = self.0.lock().unwrap(); lock.push(String::from_utf8_lossy(buf).into_owned()); Ok(buf.len()) } - fn flush(&mut self) -> std::io::Result<()> { + fn flush(&mut self) -> io::Result<()> { // Just nop, we don't need to flush anything Ok(()) } @@ -128,6 +135,9 @@ pub struct Logging { otel_traces_reload_handle: Option>, early_logs_buffer: Arc>>, + rotation_should_stop: Arc, + rotation_thread: Option>, + rotation_stop_sender: Arc>>>, } impl Logging { @@ -140,7 +150,10 @@ impl Logging { env_filter_reload_handle: None, otel_logs_reload_handle: None, otel_traces_reload_handle: None, + rotation_thread: None, + rotation_stop_sender: Arc::new(Mutex::new(None)), early_logs_buffer: Arc::new(Mutex::new(vec![])), + rotation_should_stop: Arc::new(AtomicBool::new(false)), } } @@ -213,7 +226,7 @@ impl Logging { // Use the rolling appender to avoid having a huge log file. // Make sure logs are dumped to the file during graceful shutdown. - trace!("Logging config: {}", config); + trace!("Logging config: {config}"); // Reload EnvFilter with config level if RUST_LOG is not set. // Config level supports EnvFilter syntax (e.g., "warn,server=debug,iggy=trace"). @@ -232,7 +245,7 @@ impl Logging { }; // Initialize non-blocking stdout layer - let (non_blocking_stdout, stdout_guard) = tracing_appender::non_blocking(std::io::stdout()); + let (non_blocking_stdout, stdout_guard) = tracing_appender::non_blocking(io::stdout()); let stdout_layer = fmt::Layer::default() .with_ansi(true) .event_format(Self::get_log_format()) @@ -252,9 +265,47 @@ impl Logging { let logs_path = if config.file_enabled { let base_directory = PathBuf::from(base_directory); let logs_subdirectory = PathBuf::from(config.path.clone()); - let logs_path = base_directory.join(logs_subdirectory.clone()); - let file_appender = - tracing_appender::rolling::hourly(logs_path.clone(), IGGY_LOG_FILE_PREFIX); + let logs_subdirectory = logs_subdirectory + .canonicalize() + .unwrap_or(logs_subdirectory); + let logs_path = base_directory.join(logs_subdirectory); + + if let Err(e) = fs::create_dir_all(&logs_path) { + warn!("Failed to create logs directory {logs_path:?}: {e}"); + return Err(LogError::FileReloadFailure); + } + + // Check available disk space (at least 10MB) + let min_disk_space: u64 = MIN_DISK_SPACE_BYTES; + if let Ok(available_space) = fs2::available_space(&logs_path) { + if available_space < min_disk_space { + warn!( + "Low disk space for logs. Available: {available_space} bytes, Recommended: {min_disk_space} bytes" + ); + } + } else { + warn!("Failed to check available disk space for logs directory: {logs_path:?}"); + } + + let max_files = Self::calculate_max_files( + config.max_total_size.as_bytes_u64(), + config.max_file_size.as_bytes_u64(), + ); + + let condition = RollingConditionBasic::new() + .max_size(config.max_file_size.as_bytes_u64()) + .hourly(); + + let file_appender = BasicRollingFileAppender::new( + logs_path.join(IGGY_LOG_FILE_PREFIX), + condition, + max_files, + ) + .map_err(|e| { + error!("Failed to create file appender: {e}"); + LogError::FileReloadFailure + })?; + let (mut non_blocking_file, file_guard) = tracing_appender::non_blocking(file_appender); self.dump_to_file(&mut non_blocking_file); @@ -284,9 +335,11 @@ impl Logging { self.init_telemetry(telemetry_config)?; } + self.rotation_thread = self.install_log_rotation_handler(config, logs_path.as_ref()); + if let Some(logs_path) = logs_path { info!( - "Logging initialized, logs will be stored at: {logs_path:?}. Logs will be rotated hourly. Log filter: {log_filter}." + "Logging initialized, logs will be stored at: {logs_path:?}. Logs will be rotated based on size. Log filter: {log_filter}." ); } else { info!("Logging initialized (file output disabled). Log filter: {log_filter}."); @@ -387,8 +440,8 @@ impl Logging { .expect("Failed to modify telemetry traces layer"); info!( - "Telemetry initialized with service name: {}", - telemetry_config.service_name + "Telemetry initialized with service name: {config_service_name}", + config_service_name = telemetry_config.service_name ); Ok(()) } @@ -397,10 +450,6 @@ impl Logging { Format::default().with_thread_names(true) } - fn _install_log_rotation_handler(&self) { - todo!("Implement log rotation handler based on size and retention time"); - } - fn print_build_info() { if option_env!("IGGY_CI_BUILD") == Some("true") { let hash = option_env!("VERGEN_GIT_SHA").unwrap_or("unknown"); @@ -408,8 +457,7 @@ impl Logging { let rust_version = option_env!("VERGEN_RUSTC_SEMVER").unwrap_or("unknown"); let target = option_env!("VERGEN_CARGO_TARGET_TRIPLE").unwrap_or("unknown"); info!( - "Version: {VERSION}, hash: {}, built at: {} using rust version: {} for target: {}", - hash, built_at, rust_version, target + "Version: {VERSION}, hash: {hash}, built at: {built_at} using rust version: {rust_version} for target: {target}" ); } else { info!( @@ -417,6 +465,266 @@ impl Logging { ) } } + + fn calculate_max_files(max_total_size_bytes: u64, max_file_size_bytes: u64) -> usize { + if max_file_size_bytes == 0 { + return 10; + } + + let max_files = max_total_size_bytes / max_file_size_bytes; + max_files.clamp(1, 1000) as usize + } + + fn install_log_rotation_handler( + &self, + config: &LoggingConfig, + logs_path: Option<&PathBuf>, + ) -> Option> { + let logs_path = logs_path?; + let path = logs_path.to_path_buf(); + let max_total_size_bytes = config.max_total_size.as_bytes_u64(); + let max_file_size_bytes = config.max_file_size.as_bytes_u64(); + let check_interval = config.rotation_check_interval; + let retention = config.retention; + let should_stop = Arc::clone(&self.rotation_should_stop); + + let (tx, rx) = std::sync::mpsc::channel::<()>(); + *self.rotation_stop_sender.lock().unwrap() = Some(tx.clone()); + + let handle = std::thread::Builder::new() + .name("log-rotation".to_string()) + .spawn(move || { + Self::run_log_rotation_loop( + path, + retention, + max_total_size_bytes, + max_file_size_bytes, + check_interval, + should_stop, + rx, + ) + }) + .expect("Failed to spawn log rotation thread"); + + Some(handle) + } + + fn run_log_rotation_loop( + path: PathBuf, + retention: IggyDuration, + max_total_size_bytes: u64, + max_file_size_bytes: u64, + check_interval: IggyDuration, + should_stop: Arc, + rx: std::sync::mpsc::Receiver<()>, + ) { + loop { + if should_stop.load(std::sync::atomic::Ordering::Relaxed) { + debug!("Log rotation thread detected stop flag, exiting"); + break; + } + + match rx.recv_timeout(check_interval.get_duration()) { + Ok(_) => { + debug!("Log rotation thread received channel stop signal, exiting"); + break; + } + Err(std::sync::mpsc::RecvTimeoutError::Timeout) => { + Self::cleanup_log_files( + &path, + retention, + max_total_size_bytes, + max_file_size_bytes, + ); + } + Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => { + warn!("Log rotation channel disconnected, exiting thread"); + break; + } + } + } + + debug!("Log rotation thread exited gracefully"); + } + + fn read_log_files(logs_path: &PathBuf) -> Vec<(fs::DirEntry, SystemTime, Duration, u64)> { + let entries = match fs::read_dir(logs_path) { + Ok(entries) => entries, + Err(e) => { + warn!("Failed to read log directory {logs_path:?}: {e}"); + return Vec::new(); + } + }; + + let mut file_entries = Vec::new(); + + for entry in entries.flatten() { + if let Some(file_name) = entry.file_name().to_str() { + if file_name == IGGY_LOG_FILE_PREFIX { + continue; + } + if !file_name.starts_with(IGGY_LOG_FILE_PREFIX) { + continue; + } + } else { + continue; + } + + let metadata = match entry.metadata() { + Ok(metadata) => metadata, + Err(e) => { + warn!( + "Failed to get metadata for {entry_path:?}: {e}", + entry_path = entry.path() + ); + continue; + } + }; + + if !metadata.is_file() { + continue; + } + + let modified = match metadata.modified() { + Ok(modified) => modified, + Err(e) => { + warn!( + "Failed to get modification time for {entry_path:?}: {e}", + entry_path = entry.path() + ); + continue; + } + }; + + let elapsed = match modified.duration_since(UNIX_EPOCH) { + Ok(elapsed) => elapsed, + Err(e) => { + warn!( + "Failed to calculate elapsed time for {entry_path:?}: {e}", + entry_path = entry.path() + ); + continue; + } + }; + + let file_size = metadata.len(); + file_entries.push((entry, modified, elapsed, file_size)); + } + + file_entries + } + + fn cleanup_log_files( + logs_path: &PathBuf, + retention: IggyDuration, + max_total_size_bytes: u64, + max_file_size_bytes: u64, + ) { + debug!( + "Starting log cleanup for directory: {logs_path:?}, retention: {retention:?}, max_total_size: {max_total_size_bytes} bytes, max_single_file_size: {max_file_size_bytes} bytes" + ); + + let mut file_entries = Self::read_log_files(logs_path); + debug!( + "Processed {file_entries_len} log files from directory: {logs_path:?}", + file_entries_len = file_entries.len(), + ); + + let mut removed_files_count = 0; + let cutoff = if !retention.is_zero() { + match SystemTime::now().duration_since(UNIX_EPOCH) { + Ok(now) => Some(now - retention.get_duration()), + Err(e) => { + warn!("Failed to get current time: {e}"); + return; + } + } + } else { + None + }; + + let mut expired_file_indices = Vec::new(); + for (idx, tuple) in file_entries.iter().enumerate() { + let entry = &tuple.0; + let elapsed = &tuple.2; + + let mut need_remove = false; + if let Some(cutoff) = &cutoff + && *elapsed < *cutoff + { + need_remove = true; + debug!( + "Mark old log file for remove: {entry_path:?}", + entry_path = entry.path() + ); + } + + if need_remove { + expired_file_indices.push(idx); + } + } + + for &idx in expired_file_indices.iter().rev() { + let entry = &file_entries[idx]; + if fs::remove_file(entry.0.path()).is_ok() { + debug!( + "Removed log file: {entry_path:?}", + entry_path = entry.0.path() + ); + removed_files_count += 1; + file_entries.remove(idx); + } else { + warn!( + "Failed to remove log file {entry_path:?}", + entry_path = entry.0.path() + ); + } + } + + let skip_size_check = max_total_size_bytes == 0 && max_file_size_bytes == 0; + if !skip_size_check && max_total_size_bytes > 0 { + let total_size: u64 = file_entries.iter().map(|(_, _, _, size)| *size).sum(); + + if total_size > max_total_size_bytes { + file_entries.sort_unstable_by_key(|(_, mtime, _, _)| *mtime); + + let mut remaining_size = total_size; + let mut to_remove = Vec::new(); + + for (idx, (_entry, _, _, fsize)) in file_entries.iter().enumerate() { + if remaining_size <= max_total_size_bytes { + break; + } + to_remove.push((idx, *fsize)); + remaining_size = remaining_size.saturating_sub(*fsize); + } + + for (idx, fsize) in to_remove.iter().rev() { + let entry = &file_entries[*idx]; + if fs::remove_file(entry.0.path()).is_ok() { + debug!( + "Removed log file (size control): {:?} freed {:.2} MiB", + entry.0.path(), + *fsize as f64 / 1_048_576.0 + ); + removed_files_count += 1; + file_entries.remove(*idx); + } else { + warn!( + "Failed to remove log file for size control: {:?}", + entry.0.path() + ); + } + } + } + } + + if removed_files_count > 0 { + info!( + "Completed log cleanup for directory: {logs_path:?}. Removed {removed_files_count} files." + ); + } + } } impl Default for Logging { @@ -425,6 +733,29 @@ impl Default for Logging { } } +impl Drop for Logging { + fn drop(&mut self) { + self.rotation_should_stop + .store(true, std::sync::atomic::Ordering::Relaxed); + debug!("Set rotation_should_stop to true for log rotation thread"); + + if let Ok(sender_guard) = self.rotation_stop_sender.lock() + && let Some(ref sender) = *sender_guard + { + let _ = sender.send(()).map_err(|e| { + warn!("Failed to send stop signal to log rotation thread: {e}"); + }); + } + + if let Some(handle) = self.rotation_thread.take() { + match handle.join() { + Ok(_) => debug!("Log rotation thread joined successfully"), + Err(e) => warn!("Failed to join log rotation thread: {e:?}"), + } + } + } +} + // This is a workaround for a bug with `with_ansi` setting in tracing // Bug thread: https://github.com/tokio-rs/tracing/issues/3116 struct NoAnsiFields {} @@ -440,3 +771,79 @@ impl<'writer> FormatFields<'writer> for NoAnsiFields { a.finish() } } + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::TempDir; + + #[test] + fn test_log_directory_creation() { + let temp_dir = TempDir::new().expect("Failed to create temporary directory"); + let base_path = temp_dir.path().to_str().unwrap().to_string(); + let log_subdir = "test_logs".to_string(); + + let log_path = PathBuf::from(&base_path).join(&log_subdir); + assert!(!log_path.exists()); + fs::create_dir_all(&log_path).expect("Failed to create log directory"); + assert!(log_path.exists()); + } + + #[test] + fn test_disk_space_check() { + let temp_dir = TempDir::new().expect("Failed to create temporary directory"); + let log_path = temp_dir.path(); + let result = fs2::available_space(log_path); + assert!(result.is_ok()); + + let available_space = result.unwrap(); + assert!(available_space > 0); + } + + #[test] + fn test_calculate_max_files() { + assert_eq!(Logging::calculate_max_files(0, 100), 1); + assert_eq!(Logging::calculate_max_files(100, 0), 10); + assert_eq!(Logging::calculate_max_files(1000, 100), 10); + assert_eq!(Logging::calculate_max_files(500, 100), 5); + assert_eq!(Logging::calculate_max_files(2000, 100), 20); + assert_eq!(Logging::calculate_max_files(1000000, 1), 1000); + assert_eq!(Logging::calculate_max_files(50, 100), 1); + } + + #[test] + fn test_calculate_max_files_with_values() { + let total_size = 10 * 1024 * 1024 * 1024; // 10 GB + let file_size = 512 * 1024 * 1024; // 512 MB + assert_eq!(Logging::calculate_max_files(total_size, file_size), 20); + + let total_size = 5 * 1024 * 1024 * 1024; // 5 GB + let file_size = 256 * 1024 * 1024; // 256 MB + assert_eq!(Logging::calculate_max_files(total_size, file_size), 20); + + let total_size = 1024 * 1024 * 1024; // 1 GB + let file_size = 100 * 1024 * 1024; // 100 MB + assert_eq!(Logging::calculate_max_files(total_size, file_size), 10); + } + + #[test] + fn test_cleanup_log_files_functions() { + use std::time::Duration; + let temp_dir = TempDir::new().expect("Failed to create temporary directory"); + let log_path = temp_dir.path().to_path_buf(); + Logging::cleanup_log_files( + &log_path, + IggyDuration::new(Duration::from_secs(3600)), + 2048 * 1024, + 512 * 1024, + ); + } + + #[test] + fn test_logging_creation() { + let logging = Logging::new(); + assert!(logging.stdout_guard.is_none()); + assert!(logging.file_guard.is_none()); + assert!(logging.env_filter_reload_handle.is_none()); + } +} diff --git a/foreign/cpp/tests/e2e/server.toml b/foreign/cpp/tests/e2e/server.toml index de67367aad..1206e9a447 100644 --- a/foreign/cpp/tests/e2e/server.toml +++ b/foreign/cpp/tests/e2e/server.toml @@ -270,8 +270,17 @@ path = "logs" # Level of logging detail. Options: "debug", "info", "warn", "error". level = "info" -# Maximum size of the log files before rotation. -max_size = "512 MB" +# Maximum size of a single log file before rotation occurs. +# When a log file reaches this size, it will be rotated (closed and a new file created). +# This setting works together with max_total_size to control log storage. +max_file_size = "512 MB" + +# Maximum total size of all log files combined. +# When this size is reached, oldest files will be deleted. +max_total_size = "4 GB" + +# Time interval for checking log file size and rotation. +rotation_check_interval = "1 h" # Time to retain log files before deletion. retention = "7 days"