diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
index f076036..44349ca 100644
--- a/.github/FUNDING.yml
+++ b/.github/FUNDING.yml
@@ -1,9 +1,9 @@
# These are supported funding model platforms
-github: [n4vrl0s3] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
+github: [sha-wrks]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
-ko_fi: yanshaaa # Replace with a single Ko-fi username
+ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..ececb2d
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,49 @@
+name: CI
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ branches: [main]
+
+env:
+ CARGO_TERM_COLOR: always
+
+jobs:
+ test:
+ name: Test
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Install Rust stable
+ uses: dtolnay/rust-toolchain@stable
+
+ - name: Cache dependencies
+ uses: Swatinem/rust-cache@v2
+
+ - name: Build
+ run: cargo build --verbose
+
+ - name: Run tests
+ run: cargo test --verbose
+
+ lint:
+ name: Lint
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Install Rust stable
+ uses: dtolnay/rust-toolchain@stable
+ with:
+ components: rustfmt, clippy
+
+ - name: Cache dependencies
+ uses: Swatinem/rust-cache@v2
+
+ - name: Check formatting
+ run: cargo fmt --check
+
+ - name: Clippy
+ run: cargo clippy -- -D warnings
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..fc122c0
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,36 @@
+# Rust build output
+/target/
+
+# Cargo lock file (keep for binaries, remove this line if publishing as a library)
+# Cargo.lock
+
+# Editor and IDE
+.vscode/settings.json
+.vscode/launch.json
+.vscode/tasks.json
+.idea/
+*.iml
+
+# OS generated
+.DS_Store
+.DS_Store?
+._*
+.Spotlight-V8
+.Trashes
+ehthumbs.db
+Thumbs.db
+desktop.ini
+
+# Environment and secrets
+.env
+.env.local
+.env.*.local
+*.pem
+*.key
+
+# Logs
+*.log
+!tests/fixtures/*.log
+
+# Benchmark results
+criterion/
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
index 2e19b90..ff493aa 100644
--- a/CODE_OF_CONDUCT.md
+++ b/CODE_OF_CONDUCT.md
@@ -60,8 +60,7 @@ representative at an online or offline event.
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
-n4vrl0s3@gmail.com.
-All complaints will be reviewed and investigated promptly and fairly.
+yansha@yansha.dev. All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
@@ -126,3 +125,4 @@ enforcement ladder](https://github.com/mozilla/diversity).
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.
+
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..2cb5b2d
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,53 @@
+# Contribution Guide
+
+Thank you for your interest in contributing to this project! Your contributions can help make this project better. Here are some guidelines to help you get started.
+
+## How to Contribute
+
+1. Fork this repository:
+ - Click the `Fork` button at the top of this repository page.
+2. Clone the forked repository to your local machine.
+ ```bash
+ git clone https://github.com/username/Log-Rift.git
+ ```
+ - Replace `username` with your GitHub username.
+3. Create a new branch
+ - Create a new branch for the feature or fix you want to add.
+ ```
+ cd Log-Rift
+ git checkout -b your-branch-name
+ ```
+ - Use a descriptive branch name that reflects the feature or fix you are working on.
+4. Make the necessary changes:
+ - Add or modify your code.
+5. Commit your changes:
+ - Make sure to write a clear and descriptive commit message.
+ ```
+ git add .
+ git commit -m "Brief description of the changes you made"
+ ```
+6. Push to your repository:
+ - Push your branch to your GitHub repository.
+ ```
+ git push origin your-branch-name
+ ```
+7. Create a Pull Request (PR):
+ - Go to the original repository page and create a pull request from your branch.
+ - Provide a clear description of what you added or fixed in your pull request.
+
+## Coding Guidelines
+
+- Follow a consistent coding standard: Ensure your code is consistent with the existing code style in this project.
+- Write clear documentation: Add necessary comments and documentation to help others understand your code.
+- Write tests: If possible, add tests for the features or fixes you are adding.
+
+## Reporting Issues
+
+If you find any bugs or have suggestions for improvements, please create a new issue on the Issues page.
+
+## Communication
+
+If you want to discuss something related to your contribution or the project in general, feel free to reach out to us via [Discussions](https://github.com/sha-wrks/Log-Rift/discussions).
+
+
+Thank you for your contribution!
diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644
index 0000000..90186cd
--- /dev/null
+++ b/Cargo.lock
@@ -0,0 +1,1306 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "android_system_properties"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "anes"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
+
+[[package]]
+name = "anstream"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "is_terminal_polyfill",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
+
+[[package]]
+name = "anstyle-parse"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
+dependencies = [
+ "windows-sys",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
+dependencies = [
+ "anstyle",
+ "once_cell_polyfill",
+ "windows-sys",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.102"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
+
+[[package]]
+name = "autocfg"
+version = "1.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
+
+[[package]]
+name = "bitflags"
+version = "2.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
+
+[[package]]
+name = "bumpalo"
+version = "3.20.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
+
+[[package]]
+name = "cast"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
+
+[[package]]
+name = "cc"
+version = "1.2.62"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98"
+dependencies = [
+ "find-msvc-tools",
+ "shlex",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "chrono"
+version = "0.4.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0"
+dependencies = [
+ "iana-time-zone",
+ "js-sys",
+ "num-traits",
+ "serde",
+ "wasm-bindgen",
+ "windows-link",
+]
+
+[[package]]
+name = "ciborium"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e"
+dependencies = [
+ "ciborium-io",
+ "ciborium-ll",
+ "serde",
+]
+
+[[package]]
+name = "ciborium-io"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757"
+
+[[package]]
+name = "ciborium-ll"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9"
+dependencies = [
+ "ciborium-io",
+ "half",
+]
+
+[[package]]
+name = "clap"
+version = "4.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "clap_lex"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
+
+[[package]]
+name = "colorchoice"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
+
+[[package]]
+name = "criterion"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f"
+dependencies = [
+ "anes",
+ "cast",
+ "ciborium",
+ "clap",
+ "criterion-plot",
+ "is-terminal",
+ "itertools",
+ "num-traits",
+ "once_cell",
+ "oorandom",
+ "plotters",
+ "rayon",
+ "regex",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "tinytemplate",
+ "walkdir",
+]
+
+[[package]]
+name = "criterion-plot"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1"
+dependencies = [
+ "cast",
+ "itertools",
+]
+
+[[package]]
+name = "crossbeam-deque"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
+dependencies = [
+ "crossbeam-epoch",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-epoch"
+version = "0.9.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
+
+[[package]]
+name = "crunchy"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
+
+[[package]]
+name = "csv"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938"
+dependencies = [
+ "csv-core",
+ "itoa",
+ "ryu",
+ "serde_core",
+]
+
+[[package]]
+name = "csv-core"
+version = "0.1.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "dirs-next"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1"
+dependencies = [
+ "cfg-if",
+ "dirs-sys-next",
+]
+
+[[package]]
+name = "dirs-sys-next"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d"
+dependencies = [
+ "libc",
+ "redox_users",
+ "winapi",
+]
+
+[[package]]
+name = "either"
+version = "1.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e"
+
+[[package]]
+name = "encode_unicode"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
+
+[[package]]
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
+name = "errno"
+version = "0.3.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
+dependencies = [
+ "libc",
+ "windows-sys",
+]
+
+[[package]]
+name = "fastrand"
+version = "2.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
+
+[[package]]
+name = "find-msvc-tools"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
+
+[[package]]
+name = "foldhash"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
+
+[[package]]
+name = "futures-core"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
+
+[[package]]
+name = "futures-task"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
+
+[[package]]
+name = "futures-util"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "pin-project-lite",
+ "slab",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "r-efi",
+ "wasip2",
+ "wasip3",
+]
+
+[[package]]
+name = "half"
+version = "2.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b"
+dependencies = [
+ "cfg-if",
+ "crunchy",
+ "zerocopy",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.15.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
+dependencies = [
+ "foldhash",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.17.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "hermit-abi"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
+
+[[package]]
+name = "iana-time-zone"
+version = "0.1.65"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
+dependencies = [
+ "android_system_properties",
+ "core-foundation-sys",
+ "iana-time-zone-haiku",
+ "js-sys",
+ "log",
+ "wasm-bindgen",
+ "windows-core",
+]
+
+[[package]]
+name = "iana-time-zone-haiku"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "id-arena"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
+
+[[package]]
+name = "indexmap"
+version = "2.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
+dependencies = [
+ "equivalent",
+ "hashbrown 0.17.1",
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "is-terminal"
+version = "0.4.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46"
+dependencies = [
+ "hermit-abi",
+ "libc",
+ "windows-sys",
+]
+
+[[package]]
+name = "is_terminal_polyfill"
+version = "1.70.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
+
+[[package]]
+name = "itertools"
+version = "0.10.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
+
+[[package]]
+name = "js-sys"
+version = "0.3.99"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11"
+dependencies = [
+ "cfg-if",
+ "futures-util",
+ "once_cell",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+
+[[package]]
+name = "leb128fmt"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
+
+[[package]]
+name = "libc"
+version = "0.2.186"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
+
+[[package]]
+name = "libredox"
+version = "0.1.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
+
+[[package]]
+name = "log"
+version = "0.4.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
+
+[[package]]
+name = "logagg"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "chrono",
+ "clap",
+ "criterion",
+ "prettytable-rs",
+ "rayon",
+ "regex",
+ "serde",
+ "serde_json",
+ "tempfile",
+ "thiserror",
+]
+
+[[package]]
+name = "memchr"
+version = "2.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
+
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
+
+[[package]]
+name = "once_cell_polyfill"
+version = "1.70.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
+
+[[package]]
+name = "oorandom"
+version = "11.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e"
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
+
+[[package]]
+name = "plotters"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747"
+dependencies = [
+ "num-traits",
+ "plotters-backend",
+ "plotters-svg",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "plotters-backend"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a"
+
+[[package]]
+name = "plotters-svg"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670"
+dependencies = [
+ "plotters-backend",
+]
+
+[[package]]
+name = "prettyplease"
+version = "0.2.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
+dependencies = [
+ "proc-macro2",
+ "syn",
+]
+
+[[package]]
+name = "prettytable-rs"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eea25e07510aa6ab6547308ebe3c036016d162b8da920dbb079e3ba8acf3d95a"
+dependencies = [
+ "csv",
+ "encode_unicode",
+ "is-terminal",
+ "lazy_static",
+ "term",
+ "unicode-width",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "r-efi"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
+
+[[package]]
+name = "rayon"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d"
+dependencies = [
+ "either",
+ "rayon-core",
+]
+
+[[package]]
+name = "rayon-core"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
+dependencies = [
+ "crossbeam-deque",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "redox_users"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
+dependencies = [
+ "getrandom 0.2.17",
+ "libredox",
+ "thiserror",
+]
+
+[[package]]
+name = "regex"
+version = "1.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
+
+[[package]]
+name = "rustix"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
+dependencies = [
+ "bitflags",
+ "errno",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
+
+[[package]]
+name = "ryu"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
+
+[[package]]
+name = "same-file"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "semver"
+version = "1.0.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
+
+[[package]]
+name = "serde"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.150"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
+dependencies = [
+ "itoa",
+ "memchr",
+ "serde",
+ "serde_core",
+ "zmij",
+]
+
+[[package]]
+name = "shlex"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
+
+[[package]]
+name = "slab"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
+
+[[package]]
+name = "strsim"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
+[[package]]
+name = "syn"
+version = "2.0.117"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "tempfile"
+version = "3.27.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
+dependencies = [
+ "fastrand",
+ "getrandom 0.4.2",
+ "once_cell",
+ "rustix",
+ "windows-sys",
+]
+
+[[package]]
+name = "term"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f"
+dependencies = [
+ "dirs-next",
+ "rustversion",
+ "winapi",
+]
+
+[[package]]
+name = "thiserror"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tinytemplate"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc"
+dependencies = [
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+
+[[package]]
+name = "unicode-width"
+version = "0.1.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
+
+[[package]]
+name = "unicode-xid"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
+
+[[package]]
+name = "utf8parse"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+
+[[package]]
+name = "walkdir"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
+dependencies = [
+ "same-file",
+ "winapi-util",
+]
+
+[[package]]
+name = "wasi"
+version = "0.11.1+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+
+[[package]]
+name = "wasip2"
+version = "1.0.3+wasi-0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6"
+dependencies = [
+ "wit-bindgen 0.57.1",
+]
+
+[[package]]
+name = "wasip3"
+version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
+dependencies = [
+ "wit-bindgen 0.51.0",
+]
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.122"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "rustversion",
+ "wasm-bindgen-macro",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.122"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.122"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e"
+dependencies = [
+ "bumpalo",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.122"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "wasm-encoder"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
+dependencies = [
+ "leb128fmt",
+ "wasmparser",
+]
+
+[[package]]
+name = "wasm-metadata"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
+dependencies = [
+ "anyhow",
+ "indexmap",
+ "wasm-encoder",
+ "wasmparser",
+]
+
+[[package]]
+name = "wasmparser"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
+dependencies = [
+ "bitflags",
+ "hashbrown 0.15.5",
+ "indexmap",
+ "semver",
+]
+
+[[package]]
+name = "web-sys"
+version = "0.3.99"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d621441cfc37b84979402712047321980c178f299193a3589d05b99e8763436"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-util"
+version = "0.1.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
+dependencies = [
+ "windows-sys",
+]
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "windows-core"
+version = "0.62.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
+dependencies = [
+ "windows-implement",
+ "windows-interface",
+ "windows-link",
+ "windows-result",
+ "windows-strings",
+]
+
+[[package]]
+name = "windows-implement"
+version = "0.60.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "windows-interface"
+version = "0.59.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "windows-link"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+
+[[package]]
+name = "windows-result"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-strings"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "wit-bindgen"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
+dependencies = [
+ "wit-bindgen-rust-macro",
+]
+
+[[package]]
+name = "wit-bindgen"
+version = "0.57.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
+
+[[package]]
+name = "wit-bindgen-core"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
+dependencies = [
+ "anyhow",
+ "heck",
+ "wit-parser",
+]
+
+[[package]]
+name = "wit-bindgen-rust"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
+dependencies = [
+ "anyhow",
+ "heck",
+ "indexmap",
+ "prettyplease",
+ "syn",
+ "wasm-metadata",
+ "wit-bindgen-core",
+ "wit-component",
+]
+
+[[package]]
+name = "wit-bindgen-rust-macro"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
+dependencies = [
+ "anyhow",
+ "prettyplease",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wit-bindgen-core",
+ "wit-bindgen-rust",
+]
+
+[[package]]
+name = "wit-component"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
+dependencies = [
+ "anyhow",
+ "bitflags",
+ "indexmap",
+ "log",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "wasm-encoder",
+ "wasm-metadata",
+ "wasmparser",
+ "wit-parser",
+]
+
+[[package]]
+name = "wit-parser"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
+dependencies = [
+ "anyhow",
+ "id-arena",
+ "indexmap",
+ "log",
+ "semver",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "unicode-xid",
+ "wasmparser",
+]
+
+[[package]]
+name = "zerocopy"
+version = "0.8.48"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9"
+dependencies = [
+ "zerocopy-derive",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.8.48"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "zmij"
+version = "1.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..bada14a
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,34 @@
+[package]
+name = "logagg"
+version = "0.1.0"
+edition = "2021"
+description = "A fast, flexible command-line log analyzer"
+authors = ["sha-wrks"]
+license = "MIT"
+
+[[bin]]
+name = "logagg"
+path = "src/main.rs"
+
+[lib]
+name = "logagg"
+path = "src/lib.rs"
+
+[dependencies]
+chrono = { version = "0.4", features = ["serde"] }
+regex = "1.10"
+serde = { version = "1.0", features = ["derive"] }
+serde_json = "1.0"
+clap = { version = "4.4", features = ["derive"] }
+prettytable-rs = "0.10"
+rayon = "1.7"
+anyhow = "1.0"
+thiserror = "1.0"
+
+[dev-dependencies]
+criterion = { version = "0.5", features = ["html_reports"] }
+tempfile = "3.8"
+
+[[bench]]
+name = "parser_bench"
+harness = false
diff --git a/LICENSE b/LICENSE
index df5a900..410d926 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
MIT License
-Copyright (c) 2025 Yansha
+Copyright (c) 2026 Yansha
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md
deleted file mode 100644
index dc5bc19..0000000
--- a/PULL_REQUEST_TEMPLATE.md
+++ /dev/null
@@ -1,16 +0,0 @@
-# Description
-
-Please describe the changes you have made.
-
-## Type of Change
-
-- [ ] Bug fix
-- [ ] New feature
-- [ ] Breaking change
-- [ ] Documentation update
-
-## Checklist
-
-- [ ] I have read the [CONTRIBUTING.md](CONTRIBUTING.md)
-- [ ] Tests have been added to verify that the changes are correct
-- [ ] All local tests pass (including code style checks)
diff --git a/README.md b/README.md
index aeddeec..9efe7d1 100644
--- a/README.md
+++ b/README.md
@@ -1,97 +1,174 @@
-# Sequential and Binary Search
+
+
+# Log-Rift
-This repository aims to provide a comprehensive starting point for understanding and implementing two fundamental search algorithms: Sequential Search and Binary Search. These search algorithms are implemented in Python and serve as a great introduction to search techniques for beginners and intermediate programmers.
+A fast, flexible command-line log analyzer written in Rust.
-
+[](https://github.com/sha-wrks/Log-Rift/actions/workflows/ci.yml)
+[](LICENSE)
+[](https://www.rust-lang.org)
+[](CONTRIBUTING.md)
-## Purpose of This Repository
+
-The purpose of this repository is to help users understand and implement two basic search algorithms in Python. It includes detailed explanations, code examples, and usage instructions for both Sequential Search and Binary Search.
+Log-Rift parses logs from multiple formats, filters by level, source, message pattern, or time range, and outputs results as a table, JSON, or CSV.
-
+## Features
-## Demonstration
+- Multi-format parsing: JSON, logfmt/key-value, and common text log formats (ISO timestamps, bracketed, syslog, plain prefix)
+- Filter by minimum log level, source substring, message pattern, or time range
+- Parallel file loading via Rayon
+- Output as pretty table, JSON, or CSV
+- Zero runtime dependencies in the output binary
-Here is a quick demo of how the search algorithms work:
+## Installation
-```python
-# Sequential Search Example
-def sequential_search(arr, target):
- for i in range(len(arr)):
- if arr[i] == target:
- return i
- return -1
+**Prerequisites:** Rust 1.70 or later. Install from [rustup.rs](https://rustup.rs).
-# Binary Search Example
-def binary_search(arr, target):
- low = 0
- high = len(arr) - 1
- while low <= high:
- mid = (low + high) // 2
- if arr[mid] == target:
- return mid
- elif arr[mid] < target:
- low = mid + 1
- else:
- high = mid - 1
- return -1
+```bash
+git clone https://github.com/sha-wrks/Log-Rift.git
+cd Log-Rift
+cargo build --release
```
-
+The compiled binary will be at `target/release/logagg.exe` (Windows) or `target/release/logagg` (Linux/macOS).
-## Features
+Optionally, install to your PATH:
-- Implementation of Sequential Search in Python
-- Implementation of Binary Search in Python
-- Example usage of both search algorithms
-- Detailed comments and explanations
+```bash
+cargo install --path .
+```
-
+## Usage
-## Technologies Used
+```
+logagg [OPTIONS] [FILES...]
+```
-- Python
+If no files are provided, logagg reads from stdin.
-
+### Options
-## Project Setup
+| Flag | Description |
+|---|---|
+| `-l, --level ` | Minimum log level: trace, debug, info, warn, error, fatal |
+| `-s, --source ` | Filter by source name (substring match, case-insensitive) |
+| `-m, --message ` | Filter messages containing pattern (case-insensitive) |
+| `--from ` | Include entries on or after this datetime (RFC3339) |
+| `--to ` | Include entries on or before this datetime (RFC3339) |
+| `-o, --output ` | Output format: table (default), json, csv |
+| `--stats` | Print summary statistics instead of log entries |
+| `-h, --help` | Print help |
+| `-V, --version` | Print version |
-To set up the project locally, follow these steps:
+### Examples
-1. **Clone the repository:**
- ```bash
- git clone https://github.com/n4vrl0s3/Sequential-Search-and-Binary-Search.git
- ```
-2. **Navigate to the project directory:**
- ```bash
- cd Sequential-Search-and-Binary-Search
- ```
+Analyze a log file and show only errors:
-
+```bash
+logagg app.log --level error
+```
-## Steps to Run
+Filter by source and output as JSON:
-To run the Python scripts, use the following commands:
+```bash
+logagg app.log --source database --output json
+```
-1. **Run the Sequential Search script:**
- ```bash
- python program_v2.py
- ```
-2. **Run the Binary Search script:**
- ```bash
- python program.py
- ```
+Show summary statistics for a time range:
-
+```bash
+logagg app.log --from 2024-01-15T09:00:00Z --to 2024-01-15T17:00:00Z --stats
+```
-## License
+Pipe from another command:
-This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
+```bash
+kubectl logs my-pod | logagg --level warn --output csv
+```
-
+Analyze multiple files:
-
+```bash
+logagg logs/*.log --level error --source api
+```
+
+## Supported Log Formats
+
+**JSON**
+```
+{"timestamp":"2024-01-15T12:00:00Z","level":"error","source":"app","message":"Connection failed"}
+```
+
+**logfmt / key-value**
+```
+ts=2024-01-15T12:00:00Z level=error source=app msg="Connection failed" latency=500ms
+```
+
+**ISO timestamp text**
+```
+2024-01-15T12:00:00Z ERROR [app] Connection failed
+```
+
+**Bracketed**
+```
+[2024-01-15 12:00:00] [ERROR] Connection failed
+```
+
+**Simple prefix**
+```
+ERROR: Connection failed
+```
+
+Log format is auto-detected per line, so mixed-format files are supported.
+
+## Project Structure
+
+```
+src/
+ main.rs CLI entry point (clap)
+ lib.rs Core types: LogEntry, LogLevel
+ parser/
+ mod.rs LogParser trait and auto-detection
+ json.rs JSON log parser
+ regex.rs Regex-based text log parser
+ structured.rs logfmt / key-value parser
+ filter/
+ engine.rs Filter struct and match logic
+ analyzer/
+ mod.rs LogAnalyzer builder (load, filter, analyze)
+ aggregator.rs Aggregation and statistics
+ output/
+ table.rs Pretty table output (prettytable-rs)
+ json.rs JSON output (serde_json)
+ csv.rs CSV output
+tests/
+ integration_tests.rs End-to-end tests
+ fixtures/ Sample log files for testing
+benches/
+ parser_bench.rs Criterion benchmarks
+```
+
+## Development
+
+```bash
+# Run tests
+cargo test
+
+# Run benchmarks
+cargo bench
+
+# Lint
+cargo clippy
+
+# Format
+cargo fmt
+```
+
+## Contributing
+
+Contributions are welcome. Please read [CONTRIBUTING.md](CONTRIBUTING.md) before submitting a pull request.
+
+## License
+
+This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
diff --git a/SECURITY.md b/SECURITY.md
index c5a2428..2896ff7 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -10,7 +10,7 @@ If you discover a security vulnerability within this repository, please follow t
2. **Send an email to the security team.**
- - Please report the vulnerability to our security team via email at [n4vrl0s3@gmail.com]. Include as much detail as possible about the vulnerability and any potential fixes.
+ - Please report the vulnerability to our security team via email at [yansha@yansha.dev]. Include as much detail as possible about the vulnerability and any potential fixes.
3. **Provide detailed information.**
@@ -34,4 +34,5 @@ We will issue security updates for supported versions of our project as soon as
By following this Security Policy, you help us maintain a secure and reliable project for everyone. Thank you for your cooperation and contribution to the security of our project.
-If you have any questions or need further assistance, please contact us at [n4vrl0s3@gmail.com].
+If you have any questions or need further assistance, please contact us at [yansha@yansha.dev].
+
diff --git a/benches/parser_bench.rs b/benches/parser_bench.rs
new file mode 100644
index 0000000..c631cce
--- /dev/null
+++ b/benches/parser_bench.rs
@@ -0,0 +1,78 @@
+use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion};
+use logagg::parser::{JsonLogParser, RegexLogParser, StructuredLogParser};
+use logagg::LogParser;
+
+fn benchmark_json_parser(c: &mut Criterion) {
+ let parser = JsonLogParser;
+ let log_line = r#"{"timestamp":"2024-01-15T12:00:00Z","level":"ERROR","source":"app","message":"Connection timeout","request_id":"abc123","latency_ms":5000}"#;
+
+ c.bench_function("json_parse_single_line", |b| {
+ b.iter(|| parser.parse(black_box(log_line)))
+ });
+}
+
+fn benchmark_regex_parser(c: &mut Criterion) {
+ let parser = RegexLogParser::default();
+ let lines = [
+ "2024-01-15T12:00:00Z ERROR [myapp] Connection timeout after 5000ms",
+ "2024-01-15 12:00:00.000 WARN Service response slow",
+ "[2024-01-15 12:00:00] [INFO] All systems operational",
+ "ERROR: disk full on /dev/sda1",
+ ];
+
+ let mut group = c.benchmark_group("regex_parser");
+ for (i, line) in lines.iter().enumerate() {
+ group.bench_with_input(BenchmarkId::new("format", i), line, |b, l| {
+ b.iter(|| parser.parse(black_box(l)))
+ });
+ }
+ group.finish();
+}
+
+fn benchmark_structured_parser(c: &mut Criterion) {
+ let parser = StructuredLogParser;
+ let log_line =
+ r#"ts=2024-01-15T12:00:00Z level=error source=app msg="Connection timeout" request_id=abc123 latency=5000ms"#;
+
+ c.bench_function("structured_parse_single_line", |b| {
+ b.iter(|| parser.parse(black_box(log_line)))
+ });
+}
+
+fn benchmark_auto_detect(c: &mut Criterion) {
+ use logagg::parser::auto_detect;
+
+ let json_line = r#"{"level":"ERROR","message":"timeout"}"#;
+ let regex_line = "2024-01-15T12:00:00Z ERROR [app] timeout";
+ let struct_line = r#"level=error msg="timeout""#;
+
+ let mut group = c.benchmark_group("auto_detect_and_parse");
+ group.bench_function("json", |b| {
+ b.iter(|| {
+ let p = auto_detect(black_box(json_line));
+ p.parse(black_box(json_line))
+ })
+ });
+ group.bench_function("regex", |b| {
+ b.iter(|| {
+ let p = auto_detect(black_box(regex_line));
+ p.parse(black_box(regex_line))
+ })
+ });
+ group.bench_function("structured", |b| {
+ b.iter(|| {
+ let p = auto_detect(black_box(struct_line));
+ p.parse(black_box(struct_line))
+ })
+ });
+ group.finish();
+}
+
+criterion_group!(
+ benches,
+ benchmark_json_parser,
+ benchmark_regex_parser,
+ benchmark_structured_parser,
+ benchmark_auto_detect,
+);
+criterion_main!(benches);
diff --git a/program.py b/program.py
deleted file mode 100644
index 09d08fa..0000000
--- a/program.py
+++ /dev/null
@@ -1,23 +0,0 @@
-def sequential_search(array, target):
- for i in range(len(array)):
- if array[i] == target:
- return i
- return -1
-
-def binary_search(array, target):
- left, right = 0, len(array) - 1
- while left <= right:
- mid = (left + right) // 2
- if array[mid] == target:
- return mid
- elif array[mid] < target:
- left = mid + 1
- else:
- right = mid - 1
- return -1
-
-# Testing
-array = ['d', 'c', 'g', 'h', 'b', 'a', 'f', 'i', 'e', 'c']
-
-print("Element 'a' found at index", sequential_search(array, 'a'), "using sequential search")
-print("Element 'a' found at index", binary_search(array, 'a'), "using binary search")
\ No newline at end of file
diff --git a/program_v2.py b/program_v2.py
deleted file mode 100644
index c5cc686..0000000
--- a/program_v2.py
+++ /dev/null
@@ -1,43 +0,0 @@
-# Proses Pencarian Sequential Search
-def sequential_search(nama, data):
- for i in range(len(data)):
- if data[i][0] == nama:
- return data[i][1]
- return "Tidak ditemukan"
-
-# Proses Pencarian Binary Search
-def binary_search(nama, data):
- low = 0
- high = len(data) - 1
-
- while low <= high:
- mid = (low + high) // 2
- if data[mid][0] == nama:
- return data[mid][1]
- elif data[mid][0] < nama:
- low = mid + 1
- else:
- high = mid - 1
- return "Tidak ditemukan"
-
-# Contoh Data
-data = [["budi", "LULUS"],
- ["andi", "TIDAK LULUS"],
- ["tini", "LULUS"],
- ["cindi", "LULUS"],
- ["tono", "TIDAK LULUS"],
- ["joko", "LULUS"]]
-
-# Sorting Data (Wajib untuk Binary Search)
-data.sort(key=lambda x: x[0])
-
-# Contoh Pencarian
-nama = "joko"
-
-# Output Pencarian Sequential Search
-result_seq = sequential_search(nama, data)
-print(f"Hasil Pencarian Sequential Search: {result_seq}")
-
-# Output Pencarian Binary Search
-result_bin = binary_search(nama, data)
-print(f"Hasil Pencarian Binary Search: {result_bin}")
diff --git a/src/analyzer/aggregator.rs b/src/analyzer/aggregator.rs
new file mode 100644
index 0000000..b2a0f15
--- /dev/null
+++ b/src/analyzer/aggregator.rs
@@ -0,0 +1,41 @@
+use std::collections::HashMap;
+
+use crate::{LogEntry, LogLevel};
+
+#[derive(Debug, serde::Serialize)]
+pub struct AnalysisResult {
+ pub count: usize,
+ pub entries: Vec,
+ pub level_counts: HashMap,
+ pub source_counts: HashMap,
+ pub error_rate: f64,
+}
+
+pub fn aggregate(entries: Vec) -> AnalysisResult {
+ let count = entries.len();
+ let mut level_counts: HashMap = HashMap::new();
+ let mut source_counts: HashMap = HashMap::new();
+ let mut error_count: usize = 0;
+
+ for entry in &entries {
+ *level_counts.entry(entry.level.to_string()).or_insert(0) += 1;
+ *source_counts.entry(entry.source.clone()).or_insert(0) += 1;
+ if matches!(entry.level, LogLevel::Error | LogLevel::Fatal) {
+ error_count += 1;
+ }
+ }
+
+ let error_rate = if count > 0 {
+ error_count as f64 / count as f64 * 100.0
+ } else {
+ 0.0
+ };
+
+ AnalysisResult {
+ count,
+ entries,
+ level_counts,
+ source_counts,
+ error_rate,
+ }
+}
diff --git a/src/analyzer/mod.rs b/src/analyzer/mod.rs
new file mode 100644
index 0000000..9c1c785
--- /dev/null
+++ b/src/analyzer/mod.rs
@@ -0,0 +1,102 @@
+pub mod aggregator;
+pub use aggregator::AnalysisResult;
+
+use anyhow::Result;
+use chrono::{DateTime, Utc};
+use rayon::prelude::*;
+use std::fs;
+use std::path::Path;
+
+use crate::filter::Filter;
+use crate::{LogEntry, LogLevel};
+use crate::parser;
+
+pub struct LogAnalyzer {
+ entries: Vec,
+ filter: Filter,
+}
+
+impl LogAnalyzer {
+ pub fn load_files + Sync>(paths: &[P]) -> Result {
+ let entries: Vec = paths
+ .par_iter()
+ .flat_map(|path| {
+ let content = match fs::read_to_string(path.as_ref()) {
+ Ok(c) => c,
+ Err(_) => return Vec::new(),
+ };
+ content
+ .lines()
+ .filter(|l| !l.trim().is_empty())
+ .filter_map(|line| {
+ let line = line.trim_end_matches('\r');
+ let p = parser::auto_detect(line);
+ p.parse(line)
+ })
+ .collect::>()
+ })
+ .collect();
+
+ Ok(LogAnalyzer {
+ entries,
+ filter: Filter::default(),
+ })
+ }
+
+ pub fn load_stdin() -> Result {
+ use std::io::{self, BufRead};
+ let stdin = io::stdin();
+ let entries: Vec = stdin
+ .lock()
+ .lines()
+ .filter_map(|l| l.ok())
+ .filter(|l| !l.trim().is_empty())
+ .filter_map(|line| {
+ let line = line.trim_end_matches('\r').to_string();
+ let p = parser::auto_detect(&line);
+ p.parse(&line)
+ })
+ .collect();
+
+ Ok(LogAnalyzer {
+ entries,
+ filter: Filter::default(),
+ })
+ }
+
+ pub fn filter_by_level(mut self, level: LogLevel) -> Self {
+ self.filter.level = Some(level);
+ self
+ }
+
+ pub fn filter_by_source(mut self, source: impl Into) -> Self {
+ self.filter.source = Some(source.into());
+ self
+ }
+
+ pub fn filter_by_message(mut self, pattern: impl Into) -> Self {
+ self.filter.message_contains = Some(pattern.into());
+ self
+ }
+
+ pub fn filter_by_time_range(mut self, from: DateTime, to: DateTime) -> Self {
+ self.filter.from = Some(from);
+ self.filter.to = Some(to);
+ self
+ }
+
+ pub fn analyze(self) -> AnalysisResult {
+ let filter = self.filter;
+ let filtered: Vec = self
+ .entries
+ .into_iter()
+ .filter(|e| filter.matches(e))
+ .collect();
+
+ aggregator::aggregate(filtered)
+ }
+
+ pub fn entry_count(&self) -> usize {
+ self.entries.len()
+ }
+}
diff --git a/src/filter/engine.rs b/src/filter/engine.rs
new file mode 100644
index 0000000..f681659
--- /dev/null
+++ b/src/filter/engine.rs
@@ -0,0 +1,101 @@
+use chrono::{DateTime, Utc};
+
+use crate::{LogEntry, LogLevel};
+
+#[derive(Debug, Clone, Default)]
+pub struct Filter {
+ pub level: Option,
+ pub source: Option,
+ pub message_contains: Option,
+ pub from: Option>,
+ pub to: Option>,
+}
+
+impl Filter {
+ pub fn new() -> Self {
+ Self::default()
+ }
+
+ pub fn matches(&self, entry: &LogEntry) -> bool {
+ if let Some(ref min_level) = self.level {
+ if &entry.level < min_level {
+ return false;
+ }
+ }
+
+ if let Some(ref src) = self.source {
+ if !entry.source.to_lowercase().contains(&src.to_lowercase()) {
+ return false;
+ }
+ }
+
+ if let Some(ref pattern) = self.message_contains {
+ if !entry.message.to_lowercase().contains(&pattern.to_lowercase()) {
+ return false;
+ }
+ }
+
+ if let Some(from) = self.from {
+ match entry.timestamp {
+ Some(ts) if ts >= from => {}
+ _ => return false,
+ }
+ }
+
+ if let Some(to) = self.to {
+ match entry.timestamp {
+ Some(ts) if ts <= to => {}
+ _ => return false,
+ }
+ }
+
+ true
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::LogLevel;
+ use std::collections::HashMap;
+
+ fn make_entry(level: LogLevel, source: &str, message: &str) -> LogEntry {
+ LogEntry {
+ timestamp: None,
+ level,
+ source: source.to_string(),
+ message: message.to_string(),
+ fields: HashMap::new(),
+ }
+ }
+
+ #[test]
+ fn test_level_filter() {
+ let mut f = Filter::new();
+ f.level = Some(LogLevel::Error);
+
+ assert!(!f.matches(&make_entry(LogLevel::Info, "app", "msg")));
+ assert!(!f.matches(&make_entry(LogLevel::Warn, "app", "msg")));
+ assert!(f.matches(&make_entry(LogLevel::Error, "app", "msg")));
+ assert!(f.matches(&make_entry(LogLevel::Fatal, "app", "msg")));
+ }
+
+ #[test]
+ fn test_source_filter() {
+ let mut f = Filter::new();
+ f.source = Some("database".to_string());
+
+ assert!(f.matches(&make_entry(LogLevel::Info, "database", "msg")));
+ assert!(f.matches(&make_entry(LogLevel::Info, "my-database-pool", "msg")));
+ assert!(!f.matches(&make_entry(LogLevel::Info, "webserver", "msg")));
+ }
+
+ #[test]
+ fn test_message_filter() {
+ let mut f = Filter::new();
+ f.message_contains = Some("timeout".to_string());
+
+ assert!(f.matches(&make_entry(LogLevel::Error, "app", "Connection Timeout exceeded")));
+ assert!(!f.matches(&make_entry(LogLevel::Error, "app", "Connection refused")));
+ }
+}
diff --git a/src/filter/mod.rs b/src/filter/mod.rs
new file mode 100644
index 0000000..e58dfd4
--- /dev/null
+++ b/src/filter/mod.rs
@@ -0,0 +1,2 @@
+pub mod engine;
+pub use engine::Filter;
diff --git a/src/lib.rs b/src/lib.rs
new file mode 100644
index 0000000..44694b2
--- /dev/null
+++ b/src/lib.rs
@@ -0,0 +1,60 @@
+pub mod analyzer;
+pub mod filter;
+pub mod output;
+pub mod parser;
+
+use chrono::{DateTime, Utc};
+use std::collections::HashMap;
+use std::fmt;
+use std::str::FromStr;
+
+pub use analyzer::LogAnalyzer;
+pub use parser::LogParser;
+
+#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]
+pub enum LogLevel {
+ Trace,
+ Debug,
+ Info,
+ Warn,
+ Error,
+ Fatal,
+}
+
+impl FromStr for LogLevel {
+ type Err = anyhow::Error;
+
+ fn from_str(s: &str) -> Result {
+ match s.to_uppercase().as_str() {
+ "TRACE" | "TRC" => Ok(LogLevel::Trace),
+ "DEBUG" | "DBG" => Ok(LogLevel::Debug),
+ "INFO" | "INF" => Ok(LogLevel::Info),
+ "WARN" | "WARNING" | "WRN" => Ok(LogLevel::Warn),
+ "ERROR" | "ERR" => Ok(LogLevel::Error),
+ "FATAL" | "CRITICAL" | "CRIT" => Ok(LogLevel::Fatal),
+ _ => Err(anyhow::anyhow!("Unknown log level: {}", s)),
+ }
+ }
+}
+
+impl fmt::Display for LogLevel {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ LogLevel::Trace => write!(f, "TRACE"),
+ LogLevel::Debug => write!(f, "DEBUG"),
+ LogLevel::Info => write!(f, "INFO"),
+ LogLevel::Warn => write!(f, "WARN"),
+ LogLevel::Error => write!(f, "ERROR"),
+ LogLevel::Fatal => write!(f, "FATAL"),
+ }
+ }
+}
+
+#[derive(Debug, Clone, serde::Serialize)]
+pub struct LogEntry {
+ pub timestamp: Option>,
+ pub level: LogLevel,
+ pub source: String,
+ pub message: String,
+ pub fields: HashMap,
+}
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..4daa13c
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,113 @@
+use anyhow::Result;
+use clap::Parser;
+use std::path::PathBuf;
+use std::str::FromStr;
+
+use logagg::analyzer::LogAnalyzer;
+use logagg::output::{self, OutputFormat};
+use logagg::LogLevel;
+
+#[derive(Parser)]
+#[command(
+ name = "logagg",
+ about = "A fast, flexible command-line log analyzer",
+ version
+)]
+struct Cli {
+ /// Log files to analyze (reads from stdin if omitted)
+ files: Vec,
+
+ /// Filter by minimum log level
+ #[arg(short, long, value_name = "LEVEL")]
+ level: Option,
+
+ /// Filter by source (substring match)
+ #[arg(short, long, value_name = "SOURCE")]
+ source: Option,
+
+ /// Filter messages containing pattern (case-insensitive)
+ #[arg(short = 'm', long, value_name = "PATTERN")]
+ message: Option,
+
+ /// Include entries from this datetime (RFC3339, e.g. 2024-01-15T09:00:00Z)
+ #[arg(long, value_name = "DATETIME")]
+ from: Option,
+
+ /// Include entries up to this datetime (RFC3339)
+ #[arg(long, value_name = "DATETIME")]
+ to: Option,
+
+ /// Output format
+ #[arg(short, long, value_name = "FORMAT", default_value = "table")]
+ output: String,
+
+ /// Show summary statistics instead of log entries
+ #[arg(long)]
+ stats: bool,
+}
+
+fn main() -> Result<()> {
+ let cli = Cli::parse();
+
+ // Load entries
+ let analyzer = if cli.files.is_empty() {
+ LogAnalyzer::load_stdin()?
+ } else {
+ LogAnalyzer::load_files(&cli.files)?
+ };
+
+ // Apply filters (builder pattern)
+ let mut analyzer = analyzer;
+
+ if let Some(ref level_str) = cli.level {
+ let level = LogLevel::from_str(level_str)
+ .map_err(|_| anyhow::anyhow!("Invalid level: {}. Use trace/debug/info/warn/error/fatal", level_str))?;
+ analyzer = analyzer.filter_by_level(level);
+ }
+
+ if let Some(ref src) = cli.source {
+ analyzer = analyzer.filter_by_source(src.clone());
+ }
+
+ if let Some(ref pattern) = cli.message {
+ analyzer = analyzer.filter_by_message(pattern.clone());
+ }
+
+ if cli.from.is_some() || cli.to.is_some() {
+ use chrono::{DateTime, Utc};
+ let from = cli
+ .from
+ .as_deref()
+ .map(|s| {
+ s.parse::>()
+ .map_err(|_| anyhow::anyhow!("Invalid --from datetime: {}", s))
+ })
+ .transpose()?
+ .unwrap_or(DateTime::::MIN_UTC);
+
+ let to = cli
+ .to
+ .as_deref()
+ .map(|s| {
+ s.parse::>()
+ .map_err(|_| anyhow::anyhow!("Invalid --to datetime: {}", s))
+ })
+ .transpose()?
+ .unwrap_or(DateTime::::MAX_UTC);
+
+ analyzer = analyzer.filter_by_time_range(from, to);
+ }
+
+ let result = analyzer.analyze();
+
+ // Render output
+ let fmt = OutputFormat::from_str(&cli.output)?;
+
+ if cli.stats {
+ print!("{}", logagg::output::table::render_stats(&result)?);
+ } else {
+ print!("{}", output::render(&result, fmt)?);
+ }
+
+ Ok(())
+}
diff --git a/src/output/csv.rs b/src/output/csv.rs
new file mode 100644
index 0000000..9948e4b
--- /dev/null
+++ b/src/output/csv.rs
@@ -0,0 +1,23 @@
+use anyhow::Result;
+
+use crate::analyzer::AnalysisResult;
+
+pub fn render(result: &AnalysisResult) -> Result {
+ let mut out = String::from("timestamp,level,source,message\n");
+
+ for entry in &result.entries {
+ let ts = entry
+ .timestamp
+ .map(|t| t.to_rfc3339())
+ .unwrap_or_default();
+
+ let msg = entry.message.replace('"', "\"\"");
+
+ out.push_str(&format!(
+ "{},{},{},\"{}\"\n",
+ ts, entry.level, entry.source, msg
+ ));
+ }
+
+ Ok(out)
+}
diff --git a/src/output/json.rs b/src/output/json.rs
new file mode 100644
index 0000000..150dd8c
--- /dev/null
+++ b/src/output/json.rs
@@ -0,0 +1,7 @@
+use anyhow::Result;
+
+use crate::analyzer::AnalysisResult;
+
+pub fn render(result: &AnalysisResult) -> Result {
+ Ok(serde_json::to_string_pretty(result)?)
+}
diff --git a/src/output/mod.rs b/src/output/mod.rs
new file mode 100644
index 0000000..c5a40e5
--- /dev/null
+++ b/src/output/mod.rs
@@ -0,0 +1,34 @@
+pub mod csv;
+pub mod json;
+pub mod table;
+
+use anyhow::Result;
+
+use crate::analyzer::AnalysisResult;
+
+pub enum OutputFormat {
+ Table,
+ Json,
+ Csv,
+}
+
+impl std::str::FromStr for OutputFormat {
+ type Err = anyhow::Error;
+
+ fn from_str(s: &str) -> Result {
+ match s.to_lowercase().as_str() {
+ "table" => Ok(OutputFormat::Table),
+ "json" => Ok(OutputFormat::Json),
+ "csv" => Ok(OutputFormat::Csv),
+ _ => Err(anyhow::anyhow!("Unknown format: {}. Use table, json, or csv.", s)),
+ }
+ }
+}
+
+pub fn render(result: &AnalysisResult, format: OutputFormat) -> Result {
+ match format {
+ OutputFormat::Table => table::render(result),
+ OutputFormat::Json => json::render(result),
+ OutputFormat::Csv => csv::render(result),
+ }
+}
diff --git a/src/output/table.rs b/src/output/table.rs
new file mode 100644
index 0000000..c37871f
--- /dev/null
+++ b/src/output/table.rs
@@ -0,0 +1,60 @@
+use anyhow::Result;
+use prettytable::{Cell, Row, Table};
+
+use crate::analyzer::AnalysisResult;
+
+pub fn render(result: &AnalysisResult) -> Result {
+ let mut table = Table::new();
+
+ table.add_row(Row::new(vec![
+ Cell::new("Timestamp"),
+ Cell::new("Level"),
+ Cell::new("Source"),
+ Cell::new("Message"),
+ ]));
+
+ for entry in &result.entries {
+ let ts = entry
+ .timestamp
+ .map(|t| t.format("%Y-%m-%d %H:%M:%S").to_string())
+ .unwrap_or_else(|| "-".to_string());
+
+ let msg = if entry.message.len() > 80 {
+ format!("{}…", &entry.message[..79])
+ } else {
+ entry.message.clone()
+ };
+
+ table.add_row(Row::new(vec![
+ Cell::new(&ts),
+ Cell::new(&entry.level.to_string()),
+ Cell::new(&entry.source),
+ Cell::new(&msg),
+ ]));
+ }
+
+ Ok(format!("{table}"))
+}
+
+pub fn render_stats(result: &AnalysisResult) -> Result {
+ let mut output = String::new();
+
+ output.push_str(&format!("Total entries : {}\n", result.count));
+ output.push_str(&format!("Error rate : {:.1}%\n\n", result.error_rate));
+
+ output.push_str("By level:\n");
+ let mut levels: Vec<_> = result.level_counts.iter().collect();
+ levels.sort_by(|a, b| b.1.cmp(a.1));
+ for (level, count) in &levels {
+ output.push_str(&format!(" {:8} {}\n", level, count));
+ }
+
+ output.push_str("\nBy source (top 10):\n");
+ let mut sources: Vec<_> = result.source_counts.iter().collect();
+ sources.sort_by(|a, b| b.1.cmp(a.1));
+ for (src, count) in sources.iter().take(10) {
+ output.push_str(&format!(" {:30} {}\n", src, count));
+ }
+
+ Ok(output)
+}
diff --git a/src/parser/json.rs b/src/parser/json.rs
new file mode 100644
index 0000000..475f582
--- /dev/null
+++ b/src/parser/json.rs
@@ -0,0 +1,101 @@
+use chrono::{DateTime, Utc};
+use std::str::FromStr;
+
+use crate::{LogEntry, LogLevel};
+use super::LogParser;
+
+pub struct JsonLogParser;
+
+impl LogParser for JsonLogParser {
+ fn parse(&self, line: &str) -> Option {
+ let json: serde_json::Value = serde_json::from_str(line).ok()?;
+
+ Some(LogEntry {
+ timestamp: json
+ .get("timestamp")
+ .and_then(|v| v.as_str())
+ .and_then(|s| DateTime::parse_from_rfc3339(s).ok())
+ .map(|dt| dt.with_timezone(&Utc)),
+ level: json
+ .get("level")
+ .and_then(|v| v.as_str())
+ .and_then(|s| LogLevel::from_str(s).ok())
+ .unwrap_or(LogLevel::Info),
+ source: json
+ .get("source")
+ .and_then(|v| v.as_str())
+ .unwrap_or("unknown")
+ .to_string(),
+ message: json
+ .get("message")
+ .or_else(|| json.get("msg"))
+ .and_then(|v| v.as_str())
+ .unwrap_or("")
+ .to_string(),
+ fields: json
+ .as_object()
+ .map(|obj| {
+ obj.iter()
+ .filter(|(k, _)| {
+ !["timestamp", "level", "source", "message", "msg"]
+ .contains(&k.as_str())
+ })
+ .map(|(k, v)| (k.clone(), v.to_string()))
+ .collect()
+ })
+ .unwrap_or_default(),
+ })
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_parse_valid_json_log() {
+ let parser = JsonLogParser;
+ let line = r#"{"timestamp":"2024-01-15T12:00:00Z","level":"ERROR","source":"app","message":"Connection failed"}"#;
+
+ let entry = parser.parse(line).expect("Should parse");
+ assert_eq!(entry.level, LogLevel::Error);
+ assert_eq!(entry.message, "Connection failed");
+ assert_eq!(entry.source, "app");
+ }
+
+ #[test]
+ fn test_parse_invalid_json() {
+ let parser = JsonLogParser;
+ assert!(parser.parse("not json").is_none());
+ }
+
+ #[test]
+ fn test_parse_missing_fields() {
+ let parser = JsonLogParser;
+ let line = r#"{"level":"WARN"}"#;
+
+ let entry = parser.parse(line).expect("Should parse with defaults");
+ assert_eq!(entry.message, "");
+ assert_eq!(entry.source, "unknown");
+ assert_eq!(entry.level, LogLevel::Warn);
+ }
+
+ #[test]
+ fn test_parse_msg_alias() {
+ let parser = JsonLogParser;
+ let line = r#"{"level":"INFO","msg":"hello world"}"#;
+
+ let entry = parser.parse(line).expect("Should parse msg alias");
+ assert_eq!(entry.message, "hello world");
+ }
+
+ #[test]
+ fn test_extra_fields_captured() {
+ let parser = JsonLogParser;
+ let line = r#"{"level":"INFO","message":"test","request_id":"abc123","latency_ms":42}"#;
+
+ let entry = parser.parse(line).expect("Should parse");
+ assert!(entry.fields.contains_key("request_id"));
+ assert!(entry.fields.contains_key("latency_ms"));
+ }
+}
diff --git a/src/parser/mod.rs b/src/parser/mod.rs
new file mode 100644
index 0000000..73adea8
--- /dev/null
+++ b/src/parser/mod.rs
@@ -0,0 +1,24 @@
+pub mod json;
+pub mod regex;
+pub mod structured;
+
+pub use json::JsonLogParser;
+pub use regex::RegexLogParser;
+pub use structured::StructuredLogParser;
+
+use crate::LogEntry;
+
+pub trait LogParser: Send + Sync {
+ fn parse(&self, line: &str) -> Option;
+}
+
+pub fn auto_detect(line: &str) -> Box {
+ let trimmed = line.trim();
+ if trimmed.starts_with('{') {
+ Box::new(JsonLogParser)
+ } else if trimmed.contains('=') && !trimmed.contains('[') && !trimmed.starts_with('#') {
+ Box::new(StructuredLogParser)
+ } else {
+ Box::new(RegexLogParser::default())
+ }
+}
diff --git a/src/parser/regex.rs b/src/parser/regex.rs
new file mode 100644
index 0000000..4f8ff9f
--- /dev/null
+++ b/src/parser/regex.rs
@@ -0,0 +1,177 @@
+use chrono::{DateTime, NaiveDateTime, TimeZone, Utc};
+use regex::Regex;
+use std::str::FromStr;
+
+use crate::{LogEntry, LogLevel};
+use super::LogParser;
+
+pub struct RegexLogParser {
+ // 2024-01-15T12:00:00Z ERROR [source] message
+ // 2024-01-15 12:00:00.000 ERROR source: message
+ iso_re: Regex,
+ // [2024-01-15 12:00:00] [ERROR] message
+ bracketed_re: Regex,
+ // Jan 15 12:00:00 host process[pid]: message
+ syslog_re: Regex,
+ // ERROR: message or ERROR message
+ simple_re: Regex,
+}
+
+impl Default for RegexLogParser {
+ fn default() -> Self {
+ Self {
+ iso_re: Regex::new(
+ r"(?i)^(\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(?:[.,]\d+)?(?:Z|[+-]\d{2}:?\d{2})?)\s+(TRACE|DEBUG|INFO|WARN(?:ING)?|ERROR|ERR|FATAL|CRITICAL)\s*(?:\[([^\]]+)\])?\s*(.*)$"
+ ).unwrap(),
+ bracketed_re: Regex::new(
+ r"(?i)^\[(\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}[^\]]*)\]\s+\[?(TRACE|DEBUG|INFO|WARN(?:ING)?|ERROR|FATAL)\]?\s+(.*)$"
+ ).unwrap(),
+ syslog_re: Regex::new(
+ r"^(\w{3}\s+\d{1,2}\s+\d{2}:\d{2}:\d{2})\s+\S+\s+([^\[:\s]+)(?:\[\d+\])?:\s+(.+)$"
+ ).unwrap(),
+ simple_re: Regex::new(
+ r"(?i)^(TRACE|DEBUG|INFO|WARN(?:ING)?|ERROR|ERR|FATAL|CRITICAL)\s*[:\-]?\s+(.+)$"
+ ).unwrap(),
+ }
+ }
+}
+
+impl LogParser for RegexLogParser {
+ fn parse(&self, line: &str) -> Option {
+ let line = line.trim();
+
+ if let Some(caps) = self.iso_re.captures(line) {
+ let ts_str = caps.get(1).map(|m| m.as_str()).unwrap_or("");
+ let level_str = caps.get(2).map(|m| m.as_str()).unwrap_or("INFO");
+ let source = caps.get(3).map(|m| m.as_str()).unwrap_or("unknown").to_string();
+ let message = caps.get(4).map(|m| m.as_str()).unwrap_or("").to_string();
+
+ return Some(LogEntry {
+ timestamp: parse_timestamp(ts_str),
+ level: LogLevel::from_str(level_str).unwrap_or(LogLevel::Info),
+ source,
+ message,
+ fields: Default::default(),
+ });
+ }
+
+ if let Some(caps) = self.bracketed_re.captures(line) {
+ let ts_str = caps.get(1).map(|m| m.as_str()).unwrap_or("");
+ let level_str = caps.get(2).map(|m| m.as_str()).unwrap_or("INFO");
+ let message = caps.get(3).map(|m| m.as_str()).unwrap_or("").to_string();
+
+ return Some(LogEntry {
+ timestamp: parse_timestamp(ts_str),
+ level: LogLevel::from_str(level_str).unwrap_or(LogLevel::Info),
+ source: "unknown".to_string(),
+ message,
+ fields: Default::default(),
+ });
+ }
+
+ if let Some(caps) = self.syslog_re.captures(line) {
+ let source = caps.get(2).map(|m| m.as_str()).unwrap_or("unknown").to_string();
+ let message = caps.get(3).map(|m| m.as_str()).unwrap_or("").to_string();
+
+ return Some(LogEntry {
+ timestamp: None,
+ level: LogLevel::Info,
+ source,
+ message,
+ fields: Default::default(),
+ });
+ }
+
+ if let Some(caps) = self.simple_re.captures(line) {
+ let level_str = caps.get(1).map(|m| m.as_str()).unwrap_or("INFO");
+ let message = caps.get(2).map(|m| m.as_str()).unwrap_or("").to_string();
+
+ return Some(LogEntry {
+ timestamp: None,
+ level: LogLevel::from_str(level_str).unwrap_or(LogLevel::Info),
+ source: "unknown".to_string(),
+ message,
+ fields: Default::default(),
+ });
+ }
+
+ // Fallback: treat entire line as an INFO message
+ if !line.is_empty() {
+ return Some(LogEntry {
+ timestamp: None,
+ level: LogLevel::Info,
+ source: "unknown".to_string(),
+ message: line.to_string(),
+ fields: Default::default(),
+ });
+ }
+
+ None
+ }
+}
+
+fn parse_timestamp(s: &str) -> Option> {
+ // Try RFC3339 / ISO 8601 with timezone
+ if let Ok(dt) = DateTime::parse_from_rfc3339(s) {
+ return Some(dt.with_timezone(&Utc));
+ }
+ // Try without timezone suffix (assume UTC)
+ let formats = [
+ "%Y-%m-%dT%H:%M:%S%.f",
+ "%Y-%m-%dT%H:%M:%S",
+ "%Y-%m-%d %H:%M:%S%.f",
+ "%Y-%m-%d %H:%M:%S",
+ ];
+ for fmt in &formats {
+ if let Ok(ndt) = NaiveDateTime::parse_from_str(s, fmt) {
+ return Some(Utc.from_utc_datetime(&ndt));
+ }
+ }
+ None
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_parse_iso_format() {
+ let parser = RegexLogParser::default();
+ let line = "2024-01-15T12:00:00Z ERROR [myapp] Connection timeout";
+
+ let entry = parser.parse(line).expect("Should parse");
+ assert_eq!(entry.level, LogLevel::Error);
+ assert_eq!(entry.source, "myapp");
+ assert_eq!(entry.message, "Connection timeout");
+ assert!(entry.timestamp.is_some());
+ }
+
+ #[test]
+ fn test_parse_space_separated_timestamp() {
+ let parser = RegexLogParser::default();
+ let line = "2024-01-15 12:00:00.000 WARN Service degraded";
+
+ let entry = parser.parse(line).expect("Should parse");
+ assert_eq!(entry.level, LogLevel::Warn);
+ }
+
+ #[test]
+ fn test_parse_simple_level_prefix() {
+ let parser = RegexLogParser::default();
+ let line = "ERROR: disk full";
+
+ let entry = parser.parse(line).expect("Should parse");
+ assert_eq!(entry.level, LogLevel::Error);
+ assert_eq!(entry.message, "disk full");
+ }
+
+ #[test]
+ fn test_parse_bracketed_format() {
+ let parser = RegexLogParser::default();
+ let line = "[2024-01-15 12:00:00] [FATAL] system crash";
+
+ let entry = parser.parse(line).expect("Should parse");
+ assert_eq!(entry.level, LogLevel::Fatal);
+ assert_eq!(entry.message, "system crash");
+ }
+}
diff --git a/src/parser/structured.rs b/src/parser/structured.rs
new file mode 100644
index 0000000..89e29bb
--- /dev/null
+++ b/src/parser/structured.rs
@@ -0,0 +1,163 @@
+use chrono::{DateTime, Utc};
+use std::collections::HashMap;
+use std::str::FromStr;
+
+use crate::{LogEntry, LogLevel};
+use super::LogParser;
+
+/// Parses logfmt / key=value structured log lines.
+/// Example: ts=2024-01-15T12:00:00Z level=error source=app msg="Connection failed" latency=100ms
+pub struct StructuredLogParser;
+
+impl LogParser for StructuredLogParser {
+ fn parse(&self, line: &str) -> Option {
+ let pairs = parse_kv(line.trim());
+ if pairs.is_empty() {
+ return None;
+ }
+
+ let get = |key: &str| pairs.get(key).map(|s| s.as_str());
+
+ let timestamp = get("ts")
+ .or_else(|| get("timestamp"))
+ .or_else(|| get("time"))
+ .and_then(|s| DateTime::parse_from_rfc3339(s).ok())
+ .map(|dt| dt.with_timezone(&Utc));
+
+ let level = get("level")
+ .or_else(|| get("lvl"))
+ .or_else(|| get("severity"))
+ .and_then(|s| LogLevel::from_str(s).ok())
+ .unwrap_or(LogLevel::Info);
+
+ let source = get("source")
+ .or_else(|| get("service"))
+ .or_else(|| get("app"))
+ .or_else(|| get("logger"))
+ .unwrap_or("unknown")
+ .to_string();
+
+ let message = get("msg")
+ .or_else(|| get("message"))
+ .or_else(|| get("text"))
+ .unwrap_or("")
+ .to_string();
+
+ let reserved = ["ts", "timestamp", "time", "level", "lvl", "severity",
+ "source", "service", "app", "logger", "msg", "message", "text"];
+
+ let fields: HashMap = pairs
+ .into_iter()
+ .filter(|(k, _)| !reserved.contains(&k.as_str()))
+ .collect();
+
+ Some(LogEntry { timestamp, level, source, message, fields })
+ }
+}
+
+fn parse_kv(line: &str) -> HashMap {
+ let mut map = HashMap::new();
+ let b = line.as_bytes();
+ let len = b.len();
+ let mut i = 0;
+
+ while i < len {
+ // Skip whitespace
+ while i < len && b[i].is_ascii_whitespace() {
+ i += 1;
+ }
+ if i >= len {
+ break;
+ }
+
+ // Read key (up to '=' or whitespace)
+ let key_start = i;
+ while i < len && b[i] != b'=' && !b[i].is_ascii_whitespace() {
+ i += 1;
+ }
+ let key = &line[key_start..i];
+ if key.is_empty() || i >= len || b[i] != b'=' {
+ // Token without '=', skip it
+ while i < len && !b[i].is_ascii_whitespace() {
+ i += 1;
+ }
+ continue;
+ }
+ i += 1; // consume '='
+
+ // Read value (quoted or unquoted)
+ let value = if i < len && b[i] == b'"' {
+ i += 1; // consume opening '"'
+ let mut val = String::new();
+ while i < len {
+ if b[i] == b'\\' && i + 1 < len {
+ i += 1;
+ val.push(b[i] as char);
+ i += 1;
+ } else if b[i] == b'"' {
+ i += 1; // consume closing '"'
+ break;
+ } else {
+ val.push(b[i] as char);
+ i += 1;
+ }
+ }
+ val
+ } else {
+ let val_start = i;
+ while i < len && !b[i].is_ascii_whitespace() {
+ i += 1;
+ }
+ line[val_start..i].to_string()
+ };
+
+ map.insert(key.to_string(), value);
+ }
+
+ map
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_parse_basic_kv() {
+ let parser = StructuredLogParser;
+ let line = r#"ts=2024-01-15T12:00:00Z level=error source=app msg="Connection failed""#;
+
+ let entry = parser.parse(line).expect("Should parse");
+ assert_eq!(entry.level, LogLevel::Error);
+ assert_eq!(entry.source, "app");
+ assert_eq!(entry.message, "Connection failed");
+ assert!(entry.timestamp.is_some());
+ }
+
+ #[test]
+ fn test_parse_logfmt_aliases() {
+ let parser = StructuredLogParser;
+ let line = r#"time=2024-01-15T12:00:00Z lvl=warn service=api text="high latency""#;
+
+ let entry = parser.parse(line).expect("Should parse");
+ assert_eq!(entry.level, LogLevel::Warn);
+ assert_eq!(entry.source, "api");
+ assert_eq!(entry.message, "high latency");
+ }
+
+ #[test]
+ fn test_extra_fields_in_fields_map() {
+ let parser = StructuredLogParser;
+ let line = r#"level=info msg=ok request_id=abc123 duration=42ms"#;
+
+ let entry = parser.parse(line).expect("Should parse");
+ assert!(entry.fields.contains_key("request_id"));
+ assert!(entry.fields.contains_key("duration"));
+ }
+
+ #[test]
+ fn test_parse_empty_line() {
+ let parser = StructuredLogParser;
+ assert!(parser.parse("").is_none());
+ assert!(parser.parse(" ").is_none());
+ }
+}
diff --git a/tests/fixtures/json.log b/tests/fixtures/json.log
new file mode 100644
index 0000000..b185cf1
--- /dev/null
+++ b/tests/fixtures/json.log
@@ -0,0 +1,11 @@
+{"timestamp":"2024-01-15T08:00:00Z","level":"INFO","source":"startup","message":"Application starting","version":"1.4.2"}
+{"timestamp":"2024-01-15T08:00:01Z","level":"INFO","source":"config","message":"Configuration loaded","env":"production"}
+{"timestamp":"2024-01-15T08:00:02Z","level":"DEBUG","source":"database","message":"Establishing connection pool","pool_size":10}
+{"timestamp":"2024-01-15T09:15:22Z","level":"INFO","source":"webserver","message":"Request handled","method":"GET","path":"/api/users","status":200,"latency_ms":45}
+{"timestamp":"2024-01-15T09:30:01Z","level":"WARN","source":"database","message":"Slow query detected","query_time_ms":2300,"table":"users"}
+{"timestamp":"2024-01-15T10:00:00Z","level":"ERROR","source":"database","message":"Connection pool exhausted","active":10,"waiting":5}
+{"timestamp":"2024-01-15T10:00:01Z","level":"ERROR","source":"webserver","message":"Request failed","method":"GET","path":"/api/data","status":503}
+{"timestamp":"2024-01-15T10:00:07Z","level":"INFO","source":"database","message":"Connection restored","downtime_ms":7000}
+{"timestamp":"2024-01-15T12:00:00Z","level":"INFO","source":"health","message":"Health check passed","checks":{"db":"ok","cache":"ok"}}
+{"timestamp":"2024-01-15T14:30:20Z","level":"ERROR","source":"memory","message":"Memory critical","usage_pct":95}
+{"timestamp":"2024-01-15T14:30:21Z","level":"FATAL","source":"runtime","message":"Out of memory - process terminating"}
diff --git a/tests/fixtures/large_file.log b/tests/fixtures/large_file.log
new file mode 100644
index 0000000..8cacfee
--- /dev/null
+++ b/tests/fixtures/large_file.log
@@ -0,0 +1,200 @@
+{"timestamp": "2024-01-15T00:00:00Z", "level": "INFO", "source": "cache", "message": "Unexpected state", "req_id": "req-0000"}
+{"timestamp": "2024-01-15T00:03:00Z", "level": "TRACE", "source": "webserver", "message": "Cache hit", "req_id": "req-0001"}
+{"timestamp": "2024-01-15T00:06:00Z", "level": "INFO", "source": "database", "message": "Connection timeout", "req_id": "req-0002"}
+{"timestamp": "2024-01-15T00:09:00Z", "level": "TRACE", "source": "database", "message": "Job started", "req_id": "req-0003"}
+{"timestamp": "2024-01-15T00:12:00Z", "level": "INFO", "source": "scheduler", "message": "Disk I/O error", "req_id": "req-0004"}
+{"timestamp": "2024-01-15T00:15:00Z", "level": "DEBUG", "source": "auth", "message": "Cache miss", "req_id": "req-0005"}
+{"timestamp": "2024-01-15T00:18:00Z", "level": "INFO", "source": "scheduler", "message": "Slow query", "req_id": "req-0006"}
+{"timestamp": "2024-01-15T00:21:00Z", "level": "INFO", "source": "webserver", "message": "Cache hit", "req_id": "req-0007"}
+{"timestamp": "2024-01-15T00:24:00Z", "level": "ERROR", "source": "scheduler", "message": "Cache hit", "req_id": "req-0008"}
+{"timestamp": "2024-01-15T00:27:00Z", "level": "INFO", "source": "scheduler", "message": "Request received", "req_id": "req-0009"}
+{"timestamp": "2024-01-15T00:30:00Z", "level": "FATAL", "source": "cache", "message": "Email queued", "req_id": "req-0010"}
+{"timestamp": "2024-01-15T00:33:00Z", "level": "INFO", "source": "cache", "message": "Cache hit", "req_id": "req-0011"}
+{"timestamp": "2024-01-15T00:36:00Z", "level": "INFO", "source": "scheduler", "message": "Query executed", "req_id": "req-0012"}
+{"timestamp": "2024-01-15T00:39:00Z", "level": "INFO", "source": "webserver", "message": "Query executed", "req_id": "req-0013"}
+{"timestamp": "2024-01-15T00:42:00Z", "level": "FATAL", "source": "webserver", "message": "Cache hit", "req_id": "req-0014"}
+{"timestamp": "2024-01-15T00:45:00Z", "level": "FATAL", "source": "scheduler", "message": "Cache hit", "req_id": "req-0015"}
+{"timestamp": "2024-01-15T00:48:00Z", "level": "TRACE", "source": "cache", "message": "Slow query", "req_id": "req-0016"}
+{"timestamp": "2024-01-15T00:51:00Z", "level": "TRACE", "source": "database", "message": "Disk I/O error", "req_id": "req-0017"}
+{"timestamp": "2024-01-15T00:54:00Z", "level": "TRACE", "source": "scheduler", "message": "Slow query", "req_id": "req-0018"}
+{"timestamp": "2024-01-15T00:57:00Z", "level": "DEBUG", "source": "webserver", "message": "Connection timeout", "req_id": "req-0019"}
+{"timestamp": "2024-01-15T01:00:00Z", "level": "INFO", "source": "auth", "message": "Unexpected state", "req_id": "req-0020"}
+{"timestamp": "2024-01-15T01:03:00Z", "level": "DEBUG", "source": "cache", "message": "Disk I/O error", "req_id": "req-0021"}
+{"timestamp": "2024-01-15T01:06:00Z", "level": "DEBUG", "source": "scheduler", "message": "Request received", "req_id": "req-0022"}
+{"timestamp": "2024-01-15T01:09:00Z", "level": "TRACE", "source": "cache", "message": "Request received", "req_id": "req-0023"}
+{"timestamp": "2024-01-15T01:12:00Z", "level": "ERROR", "source": "database", "message": "Job started", "req_id": "req-0024"}
+{"timestamp": "2024-01-15T01:15:00Z", "level": "INFO", "source": "auth", "message": "Disk I/O error", "req_id": "req-0025"}
+{"timestamp": "2024-01-15T01:18:00Z", "level": "INFO", "source": "webserver", "message": "Disk I/O error", "req_id": "req-0026"}
+{"timestamp": "2024-01-15T01:21:00Z", "level": "FATAL", "source": "mailer", "message": "Unexpected state", "req_id": "req-0027"}
+{"timestamp": "2024-01-15T01:24:00Z", "level": "INFO", "source": "webserver", "message": "Query executed", "req_id": "req-0028"}
+{"timestamp": "2024-01-15T01:27:00Z", "level": "INFO", "source": "auth", "message": "Unexpected state", "req_id": "req-0029"}
+{"timestamp": "2024-01-15T01:30:00Z", "level": "INFO", "source": "cache", "message": "Slow query", "req_id": "req-0030"}
+{"timestamp": "2024-01-15T01:33:00Z", "level": "WARN", "source": "database", "message": "Query executed", "req_id": "req-0031"}
+{"timestamp": "2024-01-15T01:36:00Z", "level": "INFO", "source": "auth", "message": "Connection timeout", "req_id": "req-0032"}
+{"timestamp": "2024-01-15T01:39:00Z", "level": "TRACE", "source": "webserver", "message": "Job started", "req_id": "req-0033"}
+{"timestamp": "2024-01-15T01:42:00Z", "level": "FATAL", "source": "auth", "message": "Cache miss", "req_id": "req-0034"}
+{"timestamp": "2024-01-15T01:45:00Z", "level": "INFO", "source": "webserver", "message": "Email queued", "req_id": "req-0035"}
+{"timestamp": "2024-01-15T01:48:00Z", "level": "WARN", "source": "database", "message": "Cache miss", "req_id": "req-0036"}
+{"timestamp": "2024-01-15T01:51:00Z", "level": "TRACE", "source": "database", "message": "Request received", "req_id": "req-0037"}
+{"timestamp": "2024-01-15T01:54:00Z", "level": "ERROR", "source": "mailer", "message": "Connection timeout", "req_id": "req-0038"}
+{"timestamp": "2024-01-15T01:57:00Z", "level": "FATAL", "source": "mailer", "message": "Job started", "req_id": "req-0039"}
+{"timestamp": "2024-01-15T02:00:00Z", "level": "DEBUG", "source": "mailer", "message": "Email queued", "req_id": "req-0040"}
+{"timestamp": "2024-01-15T02:03:00Z", "level": "DEBUG", "source": "scheduler", "message": "Cache hit", "req_id": "req-0041"}
+{"timestamp": "2024-01-15T02:06:00Z", "level": "TRACE", "source": "cache", "message": "Query executed", "req_id": "req-0042"}
+{"timestamp": "2024-01-15T02:09:00Z", "level": "WARN", "source": "mailer", "message": "Disk I/O error", "req_id": "req-0043"}
+{"timestamp": "2024-01-15T02:12:00Z", "level": "WARN", "source": "database", "message": "Job started", "req_id": "req-0044"}
+{"timestamp": "2024-01-15T02:15:00Z", "level": "ERROR", "source": "auth", "message": "Cache miss", "req_id": "req-0045"}
+{"timestamp": "2024-01-15T02:18:00Z", "level": "INFO", "source": "database", "message": "Email queued", "req_id": "req-0046"}
+{"timestamp": "2024-01-15T02:21:00Z", "level": "INFO", "source": "scheduler", "message": "Email queued", "req_id": "req-0047"}
+{"timestamp": "2024-01-15T02:24:00Z", "level": "INFO", "source": "auth", "message": "Cache miss", "req_id": "req-0048"}
+{"timestamp": "2024-01-15T02:27:00Z", "level": "TRACE", "source": "database", "message": "Slow query", "req_id": "req-0049"}
+{"timestamp": "2024-01-15T02:30:00Z", "level": "INFO", "source": "webserver", "message": "Disk I/O error", "req_id": "req-0050"}
+{"timestamp": "2024-01-15T02:33:00Z", "level": "INFO", "source": "auth", "message": "Token validated", "req_id": "req-0051"}
+{"timestamp": "2024-01-15T02:36:00Z", "level": "TRACE", "source": "cache", "message": "Connection timeout", "req_id": "req-0052"}
+{"timestamp": "2024-01-15T02:39:00Z", "level": "TRACE", "source": "mailer", "message": "Auth failed", "req_id": "req-0053"}
+{"timestamp": "2024-01-15T02:42:00Z", "level": "FATAL", "source": "mailer", "message": "Email queued", "req_id": "req-0054"}
+{"timestamp": "2024-01-15T02:45:00Z", "level": "INFO", "source": "cache", "message": "Job started", "req_id": "req-0055"}
+{"timestamp": "2024-01-15T02:48:00Z", "level": "TRACE", "source": "webserver", "message": "Job started", "req_id": "req-0056"}
+{"timestamp": "2024-01-15T02:51:00Z", "level": "FATAL", "source": "scheduler", "message": "Auth failed", "req_id": "req-0057"}
+{"timestamp": "2024-01-15T02:54:00Z", "level": "DEBUG", "source": "cache", "message": "Query executed", "req_id": "req-0058"}
+{"timestamp": "2024-01-15T02:57:00Z", "level": "ERROR", "source": "cache", "message": "Auth failed", "req_id": "req-0059"}
+{"timestamp": "2024-01-15T03:00:00Z", "level": "INFO", "source": "cache", "message": "Cache miss", "req_id": "req-0060"}
+{"timestamp": "2024-01-15T03:03:00Z", "level": "INFO", "source": "cache", "message": "Cache hit", "req_id": "req-0061"}
+{"timestamp": "2024-01-15T03:06:00Z", "level": "INFO", "source": "database", "message": "Cache hit", "req_id": "req-0062"}
+{"timestamp": "2024-01-15T03:09:00Z", "level": "INFO", "source": "webserver", "message": "Unexpected state", "req_id": "req-0063"}
+{"timestamp": "2024-01-15T03:12:00Z", "level": "ERROR", "source": "webserver", "message": "Cache miss", "req_id": "req-0064"}
+{"timestamp": "2024-01-15T03:15:00Z", "level": "FATAL", "source": "auth", "message": "Request received", "req_id": "req-0065"}
+{"timestamp": "2024-01-15T03:18:00Z", "level": "TRACE", "source": "auth", "message": "Disk I/O error", "req_id": "req-0066"}
+{"timestamp": "2024-01-15T03:21:00Z", "level": "INFO", "source": "mailer", "message": "Email queued", "req_id": "req-0067"}
+{"timestamp": "2024-01-15T03:24:00Z", "level": "INFO", "source": "cache", "message": "Connection timeout", "req_id": "req-0068"}
+{"timestamp": "2024-01-15T03:27:00Z", "level": "FATAL", "source": "database", "message": "Email queued", "req_id": "req-0069"}
+{"timestamp": "2024-01-15T03:30:00Z", "level": "FATAL", "source": "cache", "message": "Slow query", "req_id": "req-0070"}
+{"timestamp": "2024-01-15T03:33:00Z", "level": "INFO", "source": "cache", "message": "Job started", "req_id": "req-0071"}
+{"timestamp": "2024-01-15T03:36:00Z", "level": "INFO", "source": "webserver", "message": "Slow query", "req_id": "req-0072"}
+{"timestamp": "2024-01-15T03:39:00Z", "level": "INFO", "source": "auth", "message": "Connection timeout", "req_id": "req-0073"}
+{"timestamp": "2024-01-15T03:42:00Z", "level": "ERROR", "source": "scheduler", "message": "Query executed", "req_id": "req-0074"}
+{"timestamp": "2024-01-15T03:45:00Z", "level": "TRACE", "source": "webserver", "message": "Request received", "req_id": "req-0075"}
+{"timestamp": "2024-01-15T03:48:00Z", "level": "WARN", "source": "webserver", "message": "Query executed", "req_id": "req-0076"}
+{"timestamp": "2024-01-15T03:51:00Z", "level": "WARN", "source": "cache", "message": "Cache miss", "req_id": "req-0077"}
+{"timestamp": "2024-01-15T03:54:00Z", "level": "INFO", "source": "scheduler", "message": "Cache miss", "req_id": "req-0078"}
+{"timestamp": "2024-01-15T03:57:00Z", "level": "INFO", "source": "scheduler", "message": "Query executed", "req_id": "req-0079"}
+{"timestamp": "2024-01-15T04:00:00Z", "level": "TRACE", "source": "mailer", "message": "Job started", "req_id": "req-0080"}
+{"timestamp": "2024-01-15T04:03:00Z", "level": "INFO", "source": "scheduler", "message": "Cache hit", "req_id": "req-0081"}
+{"timestamp": "2024-01-15T04:06:00Z", "level": "FATAL", "source": "database", "message": "Token validated", "req_id": "req-0082"}
+{"timestamp": "2024-01-15T04:09:00Z", "level": "INFO", "source": "webserver", "message": "Cache miss", "req_id": "req-0083"}
+{"timestamp": "2024-01-15T04:12:00Z", "level": "FATAL", "source": "mailer", "message": "Disk I/O error", "req_id": "req-0084"}
+{"timestamp": "2024-01-15T04:15:00Z", "level": "WARN", "source": "webserver", "message": "Unexpected state", "req_id": "req-0085"}
+{"timestamp": "2024-01-15T04:18:00Z", "level": "INFO", "source": "webserver", "message": "Slow query", "req_id": "req-0086"}
+{"timestamp": "2024-01-15T04:21:00Z", "level": "INFO", "source": "webserver", "message": "Auth failed", "req_id": "req-0087"}
+{"timestamp": "2024-01-15T04:24:00Z", "level": "TRACE", "source": "scheduler", "message": "Cache hit", "req_id": "req-0088"}
+{"timestamp": "2024-01-15T04:27:00Z", "level": "INFO", "source": "cache", "message": "Query executed", "req_id": "req-0089"}
+{"timestamp": "2024-01-15T04:30:00Z", "level": "DEBUG", "source": "mailer", "message": "Token validated", "req_id": "req-0090"}
+{"timestamp": "2024-01-15T04:33:00Z", "level": "INFO", "source": "scheduler", "message": "Disk I/O error", "req_id": "req-0091"}
+{"timestamp": "2024-01-15T04:36:00Z", "level": "WARN", "source": "scheduler", "message": "Job started", "req_id": "req-0092"}
+{"timestamp": "2024-01-15T04:39:00Z", "level": "TRACE", "source": "database", "message": "Cache miss", "req_id": "req-0093"}
+{"timestamp": "2024-01-15T04:42:00Z", "level": "WARN", "source": "database", "message": "Disk I/O error", "req_id": "req-0094"}
+{"timestamp": "2024-01-15T04:45:00Z", "level": "INFO", "source": "scheduler", "message": "Cache hit", "req_id": "req-0095"}
+{"timestamp": "2024-01-15T04:48:00Z", "level": "ERROR", "source": "database", "message": "Cache miss", "req_id": "req-0096"}
+{"timestamp": "2024-01-15T04:51:00Z", "level": "WARN", "source": "webserver", "message": "Job started", "req_id": "req-0097"}
+{"timestamp": "2024-01-15T04:54:00Z", "level": "DEBUG", "source": "scheduler", "message": "Token validated", "req_id": "req-0098"}
+{"timestamp": "2024-01-15T04:57:00Z", "level": "TRACE", "source": "scheduler", "message": "Auth failed", "req_id": "req-0099"}
+{"timestamp": "2024-01-15T05:00:00Z", "level": "INFO", "source": "webserver", "message": "Cache hit", "req_id": "req-0100"}
+{"timestamp": "2024-01-15T05:03:00Z", "level": "INFO", "source": "scheduler", "message": "Disk I/O error", "req_id": "req-0101"}
+{"timestamp": "2024-01-15T05:06:00Z", "level": "INFO", "source": "auth", "message": "Query executed", "req_id": "req-0102"}
+{"timestamp": "2024-01-15T05:09:00Z", "level": "ERROR", "source": "webserver", "message": "Cache miss", "req_id": "req-0103"}
+{"timestamp": "2024-01-15T05:12:00Z", "level": "TRACE", "source": "auth", "message": "Cache hit", "req_id": "req-0104"}
+{"timestamp": "2024-01-15T05:15:00Z", "level": "WARN", "source": "mailer", "message": "Email queued", "req_id": "req-0105"}
+{"timestamp": "2024-01-15T05:18:00Z", "level": "ERROR", "source": "webserver", "message": "Request received", "req_id": "req-0106"}
+{"timestamp": "2024-01-15T05:21:00Z", "level": "DEBUG", "source": "cache", "message": "Cache miss", "req_id": "req-0107"}
+{"timestamp": "2024-01-15T05:24:00Z", "level": "DEBUG", "source": "webserver", "message": "Disk I/O error", "req_id": "req-0108"}
+{"timestamp": "2024-01-15T05:27:00Z", "level": "INFO", "source": "mailer", "message": "Cache hit", "req_id": "req-0109"}
+{"timestamp": "2024-01-15T05:30:00Z", "level": "ERROR", "source": "cache", "message": "Cache miss", "req_id": "req-0110"}
+{"timestamp": "2024-01-15T05:33:00Z", "level": "INFO", "source": "cache", "message": "Slow query", "req_id": "req-0111"}
+{"timestamp": "2024-01-15T05:36:00Z", "level": "FATAL", "source": "auth", "message": "Cache hit", "req_id": "req-0112"}
+{"timestamp": "2024-01-15T05:39:00Z", "level": "ERROR", "source": "cache", "message": "Slow query", "req_id": "req-0113"}
+{"timestamp": "2024-01-15T05:42:00Z", "level": "INFO", "source": "cache", "message": "Cache miss", "req_id": "req-0114"}
+{"timestamp": "2024-01-15T05:45:00Z", "level": "TRACE", "source": "webserver", "message": "Cache miss", "req_id": "req-0115"}
+{"timestamp": "2024-01-15T05:48:00Z", "level": "INFO", "source": "mailer", "message": "Auth failed", "req_id": "req-0116"}
+{"timestamp": "2024-01-15T05:51:00Z", "level": "FATAL", "source": "scheduler", "message": "Connection timeout", "req_id": "req-0117"}
+{"timestamp": "2024-01-15T05:54:00Z", "level": "INFO", "source": "cache", "message": "Cache miss", "req_id": "req-0118"}
+{"timestamp": "2024-01-15T05:57:00Z", "level": "INFO", "source": "cache", "message": "Connection timeout", "req_id": "req-0119"}
+{"timestamp": "2024-01-15T06:00:00Z", "level": "FATAL", "source": "mailer", "message": "Email queued", "req_id": "req-0120"}
+{"timestamp": "2024-01-15T06:03:00Z", "level": "FATAL", "source": "database", "message": "Email queued", "req_id": "req-0121"}
+{"timestamp": "2024-01-15T06:06:00Z", "level": "INFO", "source": "mailer", "message": "Token validated", "req_id": "req-0122"}
+{"timestamp": "2024-01-15T06:09:00Z", "level": "INFO", "source": "mailer", "message": "Disk I/O error", "req_id": "req-0123"}
+{"timestamp": "2024-01-15T06:12:00Z", "level": "TRACE", "source": "scheduler", "message": "Query executed", "req_id": "req-0124"}
+{"timestamp": "2024-01-15T06:15:00Z", "level": "WARN", "source": "auth", "message": "Auth failed", "req_id": "req-0125"}
+{"timestamp": "2024-01-15T06:18:00Z", "level": "WARN", "source": "webserver", "message": "Cache miss", "req_id": "req-0126"}
+{"timestamp": "2024-01-15T06:21:00Z", "level": "INFO", "source": "scheduler", "message": "Query executed", "req_id": "req-0127"}
+{"timestamp": "2024-01-15T06:24:00Z", "level": "INFO", "source": "mailer", "message": "Cache miss", "req_id": "req-0128"}
+{"timestamp": "2024-01-15T06:27:00Z", "level": "DEBUG", "source": "scheduler", "message": "Request received", "req_id": "req-0129"}
+{"timestamp": "2024-01-15T06:30:00Z", "level": "WARN", "source": "cache", "message": "Unexpected state", "req_id": "req-0130"}
+{"timestamp": "2024-01-15T06:33:00Z", "level": "TRACE", "source": "auth", "message": "Connection timeout", "req_id": "req-0131"}
+{"timestamp": "2024-01-15T06:36:00Z", "level": "FATAL", "source": "auth", "message": "Cache miss", "req_id": "req-0132"}
+{"timestamp": "2024-01-15T06:39:00Z", "level": "INFO", "source": "scheduler", "message": "Slow query", "req_id": "req-0133"}
+{"timestamp": "2024-01-15T06:42:00Z", "level": "INFO", "source": "scheduler", "message": "Connection timeout", "req_id": "req-0134"}
+{"timestamp": "2024-01-15T06:45:00Z", "level": "ERROR", "source": "scheduler", "message": "Job started", "req_id": "req-0135"}
+{"timestamp": "2024-01-15T06:48:00Z", "level": "DEBUG", "source": "mailer", "message": "Slow query", "req_id": "req-0136"}
+{"timestamp": "2024-01-15T06:51:00Z", "level": "ERROR", "source": "auth", "message": "Request received", "req_id": "req-0137"}
+{"timestamp": "2024-01-15T06:54:00Z", "level": "DEBUG", "source": "webserver", "message": "Disk I/O error", "req_id": "req-0138"}
+{"timestamp": "2024-01-15T06:57:00Z", "level": "FATAL", "source": "database", "message": "Unexpected state", "req_id": "req-0139"}
+{"timestamp": "2024-01-15T07:00:00Z", "level": "ERROR", "source": "database", "message": "Token validated", "req_id": "req-0140"}
+{"timestamp": "2024-01-15T07:03:00Z", "level": "FATAL", "source": "scheduler", "message": "Cache hit", "req_id": "req-0141"}
+{"timestamp": "2024-01-15T07:06:00Z", "level": "WARN", "source": "webserver", "message": "Cache miss", "req_id": "req-0142"}
+{"timestamp": "2024-01-15T07:09:00Z", "level": "DEBUG", "source": "cache", "message": "Cache hit", "req_id": "req-0143"}
+{"timestamp": "2024-01-15T07:12:00Z", "level": "INFO", "source": "auth", "message": "Unexpected state", "req_id": "req-0144"}
+{"timestamp": "2024-01-15T07:15:00Z", "level": "INFO", "source": "scheduler", "message": "Connection timeout", "req_id": "req-0145"}
+{"timestamp": "2024-01-15T07:18:00Z", "level": "DEBUG", "source": "auth", "message": "Request received", "req_id": "req-0146"}
+{"timestamp": "2024-01-15T07:21:00Z", "level": "DEBUG", "source": "cache", "message": "Unexpected state", "req_id": "req-0147"}
+{"timestamp": "2024-01-15T07:24:00Z", "level": "INFO", "source": "mailer", "message": "Auth failed", "req_id": "req-0148"}
+{"timestamp": "2024-01-15T07:27:00Z", "level": "FATAL", "source": "webserver", "message": "Cache hit", "req_id": "req-0149"}
+{"timestamp": "2024-01-15T07:30:00Z", "level": "DEBUG", "source": "webserver", "message": "Request received", "req_id": "req-0150"}
+{"timestamp": "2024-01-15T07:33:00Z", "level": "FATAL", "source": "auth", "message": "Cache hit", "req_id": "req-0151"}
+{"timestamp": "2024-01-15T07:36:00Z", "level": "ERROR", "source": "auth", "message": "Disk I/O error", "req_id": "req-0152"}
+{"timestamp": "2024-01-15T07:39:00Z", "level": "TRACE", "source": "webserver", "message": "Request received", "req_id": "req-0153"}
+{"timestamp": "2024-01-15T07:42:00Z", "level": "WARN", "source": "database", "message": "Unexpected state", "req_id": "req-0154"}
+{"timestamp": "2024-01-15T07:45:00Z", "level": "INFO", "source": "webserver", "message": "Email queued", "req_id": "req-0155"}
+{"timestamp": "2024-01-15T07:48:00Z", "level": "DEBUG", "source": "database", "message": "Unexpected state", "req_id": "req-0156"}
+{"timestamp": "2024-01-15T07:51:00Z", "level": "DEBUG", "source": "scheduler", "message": "Request received", "req_id": "req-0157"}
+{"timestamp": "2024-01-15T07:54:00Z", "level": "FATAL", "source": "cache", "message": "Request received", "req_id": "req-0158"}
+{"timestamp": "2024-01-15T07:57:00Z", "level": "TRACE", "source": "cache", "message": "Cache hit", "req_id": "req-0159"}
+{"timestamp": "2024-01-15T08:00:00Z", "level": "FATAL", "source": "auth", "message": "Request received", "req_id": "req-0160"}
+{"timestamp": "2024-01-15T08:03:00Z", "level": "INFO", "source": "scheduler", "message": "Auth failed", "req_id": "req-0161"}
+{"timestamp": "2024-01-15T08:06:00Z", "level": "DEBUG", "source": "auth", "message": "Email queued", "req_id": "req-0162"}
+{"timestamp": "2024-01-15T08:09:00Z", "level": "INFO", "source": "mailer", "message": "Auth failed", "req_id": "req-0163"}
+{"timestamp": "2024-01-15T08:12:00Z", "level": "INFO", "source": "auth", "message": "Connection timeout", "req_id": "req-0164"}
+{"timestamp": "2024-01-15T08:15:00Z", "level": "WARN", "source": "database", "message": "Unexpected state", "req_id": "req-0165"}
+{"timestamp": "2024-01-15T08:18:00Z", "level": "ERROR", "source": "cache", "message": "Auth failed", "req_id": "req-0166"}
+{"timestamp": "2024-01-15T08:21:00Z", "level": "INFO", "source": "scheduler", "message": "Request received", "req_id": "req-0167"}
+{"timestamp": "2024-01-15T08:24:00Z", "level": "FATAL", "source": "auth", "message": "Email queued", "req_id": "req-0168"}
+{"timestamp": "2024-01-15T08:27:00Z", "level": "INFO", "source": "mailer", "message": "Unexpected state", "req_id": "req-0169"}
+{"timestamp": "2024-01-15T08:30:00Z", "level": "INFO", "source": "mailer", "message": "Unexpected state", "req_id": "req-0170"}
+{"timestamp": "2024-01-15T08:33:00Z", "level": "ERROR", "source": "webserver", "message": "Disk I/O error", "req_id": "req-0171"}
+{"timestamp": "2024-01-15T08:36:00Z", "level": "FATAL", "source": "webserver", "message": "Auth failed", "req_id": "req-0172"}
+{"timestamp": "2024-01-15T08:39:00Z", "level": "INFO", "source": "scheduler", "message": "Slow query", "req_id": "req-0173"}
+{"timestamp": "2024-01-15T08:42:00Z", "level": "INFO", "source": "mailer", "message": "Job started", "req_id": "req-0174"}
+{"timestamp": "2024-01-15T08:45:00Z", "level": "FATAL", "source": "mailer", "message": "Cache hit", "req_id": "req-0175"}
+{"timestamp": "2024-01-15T08:48:00Z", "level": "INFO", "source": "cache", "message": "Auth failed", "req_id": "req-0176"}
+{"timestamp": "2024-01-15T08:51:00Z", "level": "TRACE", "source": "webserver", "message": "Query executed", "req_id": "req-0177"}
+{"timestamp": "2024-01-15T08:54:00Z", "level": "INFO", "source": "database", "message": "Auth failed", "req_id": "req-0178"}
+{"timestamp": "2024-01-15T08:57:00Z", "level": "INFO", "source": "mailer", "message": "Connection timeout", "req_id": "req-0179"}
+{"timestamp": "2024-01-15T09:00:00Z", "level": "INFO", "source": "cache", "message": "Cache miss", "req_id": "req-0180"}
+{"timestamp": "2024-01-15T09:03:00Z", "level": "ERROR", "source": "database", "message": "Token validated", "req_id": "req-0181"}
+{"timestamp": "2024-01-15T09:06:00Z", "level": "INFO", "source": "database", "message": "Job started", "req_id": "req-0182"}
+{"timestamp": "2024-01-15T09:09:00Z", "level": "INFO", "source": "auth", "message": "Cache hit", "req_id": "req-0183"}
+{"timestamp": "2024-01-15T09:12:00Z", "level": "FATAL", "source": "cache", "message": "Unexpected state", "req_id": "req-0184"}
+{"timestamp": "2024-01-15T09:15:00Z", "level": "WARN", "source": "mailer", "message": "Cache hit", "req_id": "req-0185"}
+{"timestamp": "2024-01-15T09:18:00Z", "level": "INFO", "source": "database", "message": "Token validated", "req_id": "req-0186"}
+{"timestamp": "2024-01-15T09:21:00Z", "level": "ERROR", "source": "database", "message": "Token validated", "req_id": "req-0187"}
+{"timestamp": "2024-01-15T09:24:00Z", "level": "TRACE", "source": "database", "message": "Unexpected state", "req_id": "req-0188"}
+{"timestamp": "2024-01-15T09:27:00Z", "level": "ERROR", "source": "mailer", "message": "Request received", "req_id": "req-0189"}
+{"timestamp": "2024-01-15T09:30:00Z", "level": "INFO", "source": "database", "message": "Query executed", "req_id": "req-0190"}
+{"timestamp": "2024-01-15T09:33:00Z", "level": "ERROR", "source": "cache", "message": "Cache hit", "req_id": "req-0191"}
+{"timestamp": "2024-01-15T09:36:00Z", "level": "TRACE", "source": "cache", "message": "Unexpected state", "req_id": "req-0192"}
+{"timestamp": "2024-01-15T09:39:00Z", "level": "DEBUG", "source": "cache", "message": "Job started", "req_id": "req-0193"}
+{"timestamp": "2024-01-15T09:42:00Z", "level": "INFO", "source": "mailer", "message": "Query executed", "req_id": "req-0194"}
+{"timestamp": "2024-01-15T09:45:00Z", "level": "INFO", "source": "scheduler", "message": "Cache miss", "req_id": "req-0195"}
+{"timestamp": "2024-01-15T09:48:00Z", "level": "FATAL", "source": "scheduler", "message": "Cache miss", "req_id": "req-0196"}
+{"timestamp": "2024-01-15T09:51:00Z", "level": "INFO", "source": "auth", "message": "Connection timeout", "req_id": "req-0197"}
+{"timestamp": "2024-01-15T09:54:00Z", "level": "ERROR", "source": "database", "message": "Auth failed", "req_id": "req-0198"}
+{"timestamp": "2024-01-15T09:57:00Z", "level": "DEBUG", "source": "cache", "message": "Slow query", "req_id": "req-0199"}
diff --git a/tests/fixtures/sample.log b/tests/fixtures/sample.log
new file mode 100644
index 0000000..387ad96
--- /dev/null
+++ b/tests/fixtures/sample.log
@@ -0,0 +1,18 @@
+2024-01-15T08:00:00Z INFO [startup] Application starting
+2024-01-15T08:00:01Z INFO [config] Loaded configuration from /etc/app/config.yaml
+2024-01-15T08:00:02Z DEBUG [database] Connecting to postgres://localhost:5432/app
+2024-01-15T08:00:03Z INFO [database] Connection pool initialized (size=10)
+2024-01-15T08:00:04Z INFO [webserver] Listening on 0.0.0.0:8080
+2024-01-15T09:15:22Z INFO [webserver] GET /api/users 200 45ms
+2024-01-15T09:15:23Z INFO [webserver] POST /api/login 200 120ms
+2024-01-15T09:30:01Z WARN [database] Slow query detected (query_time=2300ms)
+2024-01-15T10:00:00Z ERROR [database] Connection pool exhausted (active=10, waiting=5)
+2024-01-15T10:00:01Z ERROR [webserver] GET /api/data 503 timeout
+2024-01-15T10:00:05Z WARN [database] Retrying connection...
+2024-01-15T10:00:07Z INFO [database] Connection restored
+2024-01-15T11:00:00Z DEBUG [cache] Cache hit ratio: 0.87
+2024-01-15T12:00:00Z INFO [health] Health check passed
+2024-01-15T14:30:00Z WARN [memory] Memory usage at 78%
+2024-01-15T14:30:10Z WARN [memory] Memory usage at 85%
+2024-01-15T14:30:20Z ERROR [memory] Memory usage at 95%, triggering GC
+2024-01-15T14:30:21Z FATAL [runtime] Out of memory - process terminating
diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs
new file mode 100644
index 0000000..e967ffc
--- /dev/null
+++ b/tests/integration_tests.rs
@@ -0,0 +1,124 @@
+use logagg::{LogAnalyzer, LogLevel};
+use std::io::Write;
+use std::path::PathBuf;
+use tempfile::NamedTempFile;
+
+#[test]
+fn test_analyze_multiple_files() {
+ let mut file1 = NamedTempFile::new().unwrap();
+ writeln!(file1, r#"{{"level":"ERROR","message":"Error 1"}}"#).unwrap();
+ writeln!(file1, r#"{{"level":"INFO","message":"Info 1"}}"#).unwrap();
+
+ let mut file2 = NamedTempFile::new().unwrap();
+ writeln!(file2, r#"{{"level":"ERROR","message":"Error 2"}}"#).unwrap();
+
+ let paths = vec![
+ PathBuf::from(file1.path()),
+ PathBuf::from(file2.path()),
+ ];
+
+ let analyzer = LogAnalyzer::load_files(&paths).unwrap();
+ let result = analyzer.filter_by_level(LogLevel::Error).analyze();
+
+ assert_eq!(result.count, 2);
+}
+
+#[test]
+fn test_filter_by_time_range() {
+ let mut file = NamedTempFile::new().unwrap();
+ writeln!(file, r#"{{"timestamp":"2024-01-15T10:00:00Z","level":"ERROR"}}"#).unwrap();
+ writeln!(file, r#"{{"timestamp":"2024-01-15T12:00:00Z","level":"ERROR"}}"#).unwrap();
+ writeln!(file, r#"{{"timestamp":"2024-01-15T14:00:00Z","level":"ERROR"}}"#).unwrap();
+
+ let analyzer = LogAnalyzer::load_files(&[PathBuf::from(file.path())]).unwrap();
+
+ let from = "2024-01-15T09:00:00Z".parse().unwrap();
+ let to = "2024-01-15T13:00:00Z".parse().unwrap();
+
+ let result = analyzer.filter_by_time_range(from, to).analyze();
+ assert_eq!(result.count, 2);
+}
+
+#[test]
+fn test_filter_by_source() {
+ let mut file = NamedTempFile::new().unwrap();
+ writeln!(file, r#"{{"level":"ERROR","source":"database","message":"deadlock"}}"#).unwrap();
+ writeln!(file, r#"{{"level":"ERROR","source":"webserver","message":"500"}}"#).unwrap();
+ writeln!(file, r#"{{"level":"ERROR","source":"database","message":"timeout"}}"#).unwrap();
+
+ let analyzer = LogAnalyzer::load_files(&[PathBuf::from(file.path())]).unwrap();
+ let result = analyzer.filter_by_source("database").analyze();
+
+ assert_eq!(result.count, 2);
+}
+
+#[test]
+fn test_filter_by_message_pattern() {
+ let mut file = NamedTempFile::new().unwrap();
+ writeln!(file, r#"{{"level":"ERROR","message":"Connection timeout"}}"#).unwrap();
+ writeln!(file, r#"{{"level":"ERROR","message":"Disk full"}}"#).unwrap();
+ writeln!(file, r#"{{"level":"WARN","message":"High memory: timeout risk"}}"#).unwrap();
+
+ let analyzer = LogAnalyzer::load_files(&[PathBuf::from(file.path())]).unwrap();
+ let result = analyzer.filter_by_message("timeout").analyze();
+
+ assert_eq!(result.count, 2);
+}
+
+#[test]
+fn test_empty_file() {
+ let file = NamedTempFile::new().unwrap();
+ let analyzer = LogAnalyzer::load_files(&[PathBuf::from(file.path())]).unwrap();
+ let result = analyzer.analyze();
+ assert_eq!(result.count, 0);
+ assert_eq!(result.error_rate, 0.0);
+}
+
+#[test]
+fn test_error_rate_calculation() {
+ let mut file = NamedTempFile::new().unwrap();
+ writeln!(file, r#"{{"level":"INFO","message":"startup"}}"#).unwrap();
+ writeln!(file, r#"{{"level":"INFO","message":"running"}}"#).unwrap();
+ writeln!(file, r#"{{"level":"ERROR","message":"crash"}}"#).unwrap();
+ writeln!(file, r#"{{"level":"FATAL","message":"panic"}}"#).unwrap();
+
+ let analyzer = LogAnalyzer::load_files(&[PathBuf::from(file.path())]).unwrap();
+ let result = analyzer.analyze();
+
+ assert_eq!(result.count, 4);
+ assert!((result.error_rate - 50.0).abs() < f64::EPSILON);
+}
+
+#[test]
+fn test_structured_log_parsing() {
+ let mut file = NamedTempFile::new().unwrap();
+ writeln!(
+ file,
+ r#"ts=2024-01-15T12:00:00Z level=error source=db msg="deadlock detected""#
+ )
+ .unwrap();
+ writeln!(
+ file,
+ r#"ts=2024-01-15T12:01:00Z level=info source=api msg="request ok""#
+ )
+ .unwrap();
+
+ let analyzer = LogAnalyzer::load_files(&[PathBuf::from(file.path())]).unwrap();
+ let result = analyzer.filter_by_level(LogLevel::Error).analyze();
+
+ assert_eq!(result.count, 1);
+}
+
+#[test]
+fn test_regex_log_parsing() {
+ let mut file = NamedTempFile::new().unwrap();
+ writeln!(file, "2024-01-15T12:00:00Z ERROR [myapp] Connection refused").unwrap();
+ writeln!(file, "2024-01-15T12:00:01Z INFO [myapp] Retrying...").unwrap();
+ writeln!(file, "2024-01-15T12:00:02Z WARN [myapp] Slow response").unwrap();
+
+ let analyzer = LogAnalyzer::load_files(&[PathBuf::from(file.path())]).unwrap();
+ let result = analyzer.analyze();
+
+ assert_eq!(result.count, 3);
+ assert_eq!(*result.level_counts.get("ERROR").unwrap_or(&0), 1);
+}