diff --git a/.gitignore b/.gitignore index 90fcc0c..28b0d22 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,35 @@ -/target -*.env +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib +bin/ + +# Test binary, built with `go test -c` +*.test + +# Code coverage profiles and other test artifacts +*.out +coverage.* +*.coverprofile +profile.cov + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work +go.work.sum + +# env file +.env + +# Editor/IDE +# .idea/ +# .vscode/ + +.DS_Store \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock deleted file mode 100644 index aa8457e..0000000 --- a/Cargo.lock +++ /dev/null @@ -1,2693 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "addr2line" -version = "0.24.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" - -[[package]] -name = "ahash" -version = "0.8.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" -dependencies = [ - "cfg-if", - "getrandom 0.3.3", - "once_cell", - "version_check", - "zerocopy", -] - -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - -[[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 = "anyhow" -version = "1.0.99" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" - -[[package]] -name = "async-trait" -version = "0.1.89" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "atomic-waker" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" - -[[package]] -name = "autocfg" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" - -[[package]] -name = "axum" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "021e862c184ae977658b36c4500f7feac3221ca5da43e3f25bd04ab6c79a29b5" -dependencies = [ - "axum-core", - "bytes", - "form_urlencoded", - "futures-util", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-util", - "itoa", - "matchit", - "memchr", - "mime", - "percent-encoding", - "pin-project-lite", - "rustversion", - "serde", - "serde_json", - "serde_path_to_error", - "serde_urlencoded", - "sync_wrapper", - "tokio", - "tower", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "axum-core" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68464cd0412f486726fb3373129ef5d2993f90c34bc2bc1c1e9943b2f4fc7ca6" -dependencies = [ - "bytes", - "futures-core", - "http", - "http-body", - "http-body-util", - "mime", - "pin-project-lite", - "rustversion", - "sync_wrapper", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "backtrace" -version = "0.3.75" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" -dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-targets 0.52.6", -] - -[[package]] -name = "base64" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" - -[[package]] -name = "base64" -version = "0.21.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" - -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitflags" -version = "2.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a65b545ab31d687cff52899d4890855fec459eb6afe0da6417b8a18da87aa29" - -[[package]] -name = "bitvec" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" -dependencies = [ - "funty", - "radium", - "tap", - "wyz", -] - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "bson" -version = "2.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969a9ba84b0ff843813e7249eed1678d9b6607ce5a3b8f0a47af3fcf7978e6e" -dependencies = [ - "ahash", - "base64 0.22.1", - "bitvec", - "getrandom 0.2.16", - "getrandom 0.3.3", - "hex", - "indexmap 2.10.0", - "js-sys", - "once_cell", - "rand 0.9.2", - "serde", - "serde_bytes", - "serde_json", - "time", - "uuid", -] - -[[package]] -name = "bue-worker" -version = "0.1.0" -dependencies = [ - "anyhow", - "axum", - "base64 0.22.1", - "bson", - "chrono", - "dotenvy", - "futures", - "hyper", - "mongodb", - "redis", - "serde", - "serde_json", - "tokio", - "tracing", - "tracing-subscriber", -] - -[[package]] -name = "bumpalo" -version = "3.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" - -[[package]] -name = "bytes" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" - -[[package]] -name = "cc" -version = "1.2.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ee0f8803222ba5a7e2777dd72ca451868909b1ac410621b676adf07280e9b5f" -dependencies = [ - "shlex", -] - -[[package]] -name = "cfg-if" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" - -[[package]] -name = "chrono" -version = "0.4.41" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" -dependencies = [ - "android-tzdata", - "iana-time-zone", - "js-sys", - "num-traits", - "serde", - "wasm-bindgen", - "windows-link", -] - -[[package]] -name = "combine" -version = "4.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" -dependencies = [ - "bytes", - "futures-core", - "memchr", - "pin-project-lite", - "tokio", - "tokio-util", -] - -[[package]] -name = "const-random" -version = "0.1.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" -dependencies = [ - "const-random-macro", -] - -[[package]] -name = "const-random-macro" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" -dependencies = [ - "getrandom 0.2.16", - "once_cell", - "tiny-keccak", -] - -[[package]] -name = "convert_case" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - -[[package]] -name = "crunchy" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" - -[[package]] -name = "crypto-common" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" -dependencies = [ - "generic-array", - "typenum", -] - -[[package]] -name = "darling" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" -dependencies = [ - "darling_core", - "darling_macro", -] - -[[package]] -name = "darling_core" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn 2.0.106", -] - -[[package]] -name = "darling_macro" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" -dependencies = [ - "darling_core", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "data-encoding" -version = "2.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" - -[[package]] -name = "deranged" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" -dependencies = [ - "powerfmt", - "serde", -] - -[[package]] -name = "derive-syn-parse" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d65d7ce8132b7c0e54497a4d9a55a1c2a0912a0d786cf894472ba818fba45762" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "derive-where" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef941ded77d15ca19b40374869ac6000af1c9f2a4c0f3d4c70926287e6364a8f" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "derive_more" -version = "0.99.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" -dependencies = [ - "convert_case", - "proc-macro2", - "quote", - "rustc_version", - "syn 2.0.106", -] - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "crypto-common", - "subtle", -] - -[[package]] -name = "displaydoc" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "dotenvy" -version = "0.15.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" - -[[package]] -name = "dyn-clone" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" - -[[package]] -name = "enum-as-inner" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "form_urlencoded" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "funty" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" - -[[package]] -name = "futures" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-core" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" - -[[package]] -name = "futures-executor" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" - -[[package]] -name = "futures-macro" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "futures-sink" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" - -[[package]] -name = "futures-task" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" - -[[package]] -name = "futures-util" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "pin-utils", - "slab", -] - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - -[[package]] -name = "getrandom" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "wasi 0.11.1+wasi-snapshot-preview1", - "wasm-bindgen", -] - -[[package]] -name = "getrandom" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "r-efi", - "wasi 0.14.2+wasi-0.2.4", - "wasm-bindgen", -] - -[[package]] -name = "gimli" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" - -[[package]] -name = "h2" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" -dependencies = [ - "atomic-waker", - "bytes", - "fnv", - "futures-core", - "futures-sink", - "http", - "indexmap 2.10.0", - "slab", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" - -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "hickory-proto" -version = "0.24.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92652067c9ce6f66ce53cc38d1169daa36e6e7eb7dd3b63b5103bd9d97117248" -dependencies = [ - "async-trait", - "cfg-if", - "data-encoding", - "enum-as-inner", - "futures-channel", - "futures-io", - "futures-util", - "idna", - "ipnet", - "once_cell", - "rand 0.8.5", - "thiserror", - "tinyvec", - "tokio", - "tracing", - "url", -] - -[[package]] -name = "hickory-resolver" -version = "0.24.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbb117a1ca520e111743ab2f6688eddee69db4e0ea242545a604dce8a66fd22e" -dependencies = [ - "cfg-if", - "futures-util", - "hickory-proto", - "ipconfig", - "lru-cache", - "once_cell", - "parking_lot", - "rand 0.8.5", - "resolv-conf", - "smallvec", - "thiserror", - "tokio", - "tracing", -] - -[[package]] -name = "hmac" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = [ - "digest", -] - -[[package]] -name = "http" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - -[[package]] -name = "http-body" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" -dependencies = [ - "bytes", - "http", -] - -[[package]] -name = "http-body-util" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" -dependencies = [ - "bytes", - "futures-core", - "http", - "http-body", - "pin-project-lite", -] - -[[package]] -name = "httparse" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" - -[[package]] -name = "httpdate" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" - -[[package]] -name = "hyper" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" -dependencies = [ - "bytes", - "futures-channel", - "futures-util", - "h2", - "http", - "http-body", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "smallvec", - "tokio", - "want", -] - -[[package]] -name = "hyper-util" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" -dependencies = [ - "bytes", - "futures-core", - "http", - "http-body", - "hyper", - "pin-project-lite", - "tokio", - "tower-service", -] - -[[package]] -name = "iana-time-zone" -version = "0.1.63" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" -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 = "icu_collections" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" -dependencies = [ - "displaydoc", - "potential_utf", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locale_core" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_normalizer" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" -dependencies = [ - "displaydoc", - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" - -[[package]] -name = "icu_properties" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" -dependencies = [ - "displaydoc", - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "potential_utf", - "zerotrie", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" - -[[package]] -name = "icu_provider" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" -dependencies = [ - "displaydoc", - "icu_locale_core", - "stable_deref_trait", - "tinystr", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", -] - -[[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - -[[package]] -name = "idna" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" -dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] - -[[package]] -name = "idna_adapter" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" -dependencies = [ - "icu_normalizer", - "icu_properties", -] - -[[package]] -name = "indexmap" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" -dependencies = [ - "autocfg", - "hashbrown 0.12.3", - "serde", -] - -[[package]] -name = "indexmap" -version = "2.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" -dependencies = [ - "equivalent", - "hashbrown 0.15.5", - "serde", -] - -[[package]] -name = "io-uring" -version = "0.7.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4" -dependencies = [ - "bitflags 2.9.2", - "cfg-if", - "libc", -] - -[[package]] -name = "ipconfig" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" -dependencies = [ - "socket2 0.5.10", - "widestring", - "windows-sys 0.48.0", - "winreg", -] - -[[package]] -name = "ipnet" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" - -[[package]] -name = "itoa" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" - -[[package]] -name = "js-sys" -version = "0.3.77" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" -dependencies = [ - "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 = "libc" -version = "0.2.175" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" - -[[package]] -name = "linked-hash-map" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" - -[[package]] -name = "litemap" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" - -[[package]] -name = "lock_api" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" -dependencies = [ - "autocfg", - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" - -[[package]] -name = "lru-cache" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c" -dependencies = [ - "linked-hash-map", -] - -[[package]] -name = "macro_magic" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc33f9f0351468d26fbc53d9ce00a096c8522ecb42f19b50f34f2c422f76d21d" -dependencies = [ - "macro_magic_core", - "macro_magic_macros", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "macro_magic_core" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1687dc887e42f352865a393acae7cf79d98fab6351cde1f58e9e057da89bf150" -dependencies = [ - "const-random", - "derive-syn-parse", - "macro_magic_core_macros", - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "macro_magic_core_macros" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b02abfe41815b5bd98dbd4260173db2c116dda171dc0fe7838cb206333b83308" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "macro_magic_macros" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73ea28ee64b88876bf45277ed9a5817c1817df061a74f2b988971a12570e5869" -dependencies = [ - "macro_magic_core", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "matchit" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" - -[[package]] -name = "md-5" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" -dependencies = [ - "cfg-if", - "digest", -] - -[[package]] -name = "memchr" -version = "2.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" - -[[package]] -name = "mime" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" - -[[package]] -name = "miniz_oxide" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" -dependencies = [ - "adler2", -] - -[[package]] -name = "mio" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" -dependencies = [ - "libc", - "wasi 0.11.1+wasi-snapshot-preview1", - "windows-sys 0.59.0", -] - -[[package]] -name = "mongodb" -version = "3.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0f8c69f13acf07eae386a2974f48ffd9187ea2aba8defbea9aa34e7e272c5f3" -dependencies = [ - "async-trait", - "base64 0.13.1", - "bitflags 1.3.2", - "bson", - "chrono", - "derive-where", - "derive_more", - "futures-core", - "futures-executor", - "futures-io", - "futures-util", - "hex", - "hickory-proto", - "hickory-resolver", - "hmac", - "macro_magic", - "md-5", - "mongodb-internal-macros", - "once_cell", - "pbkdf2", - "percent-encoding", - "rand 0.8.5", - "rustc_version_runtime", - "rustls", - "rustls-pemfile", - "serde", - "serde_bytes", - "serde_with", - "sha1", - "sha2", - "socket2 0.5.10", - "stringprep", - "strsim", - "take_mut", - "thiserror", - "tokio", - "tokio-rustls", - "tokio-util", - "typed-builder", - "uuid", - "webpki-roots", -] - -[[package]] -name = "mongodb-internal-macros" -version = "3.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9202de265a3a8bbb43f9fe56db27c93137d4f9fb04c093f47e9c7de0c61ac7d" -dependencies = [ - "macro_magic", - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "nu-ansi-term" -version = "0.46.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" -dependencies = [ - "overload", - "winapi", -] - -[[package]] -name = "num-bigint" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" -dependencies = [ - "num-integer", - "num-traits", -] - -[[package]] -name = "num-conv" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" - -[[package]] -name = "num-integer" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - -[[package]] -name = "object" -version = "0.36.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" -dependencies = [ - "memchr", -] - -[[package]] -name = "once_cell" -version = "1.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" - -[[package]] -name = "overload" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" - -[[package]] -name = "parking_lot" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-targets 0.52.6", -] - -[[package]] -name = "pbkdf2" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" -dependencies = [ - "digest", -] - -[[package]] -name = "percent-encoding" -version = "2.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" - -[[package]] -name = "pin-project-lite" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "potential_utf" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" -dependencies = [ - "zerovec", -] - -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - -[[package]] -name = "proc-macro2" -version = "1.0.101" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - -[[package]] -name = "radium" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" - -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", -] - -[[package]] -name = "rand" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" -dependencies = [ - "rand_chacha 0.9.0", - "rand_core 0.9.3", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", -] - -[[package]] -name = "rand_chacha" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" -dependencies = [ - "ppv-lite86", - "rand_core 0.9.3", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.16", -] - -[[package]] -name = "rand_core" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" -dependencies = [ - "getrandom 0.3.3", -] - -[[package]] -name = "redis" -version = "0.32.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd3650deebc68526b304898b192fa4102a4ef0b9ada24da096559cb60e0eef8" -dependencies = [ - "bytes", - "cfg-if", - "combine", - "futures-util", - "itoa", - "num-bigint", - "percent-encoding", - "pin-project-lite", - "ryu", - "sha1_smol", - "socket2 0.6.0", - "tokio", - "tokio-util", - "url", -] - -[[package]] -name = "redox_syscall" -version = "0.5.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" -dependencies = [ - "bitflags 2.9.2", -] - -[[package]] -name = "ref-cast" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a0ae411dbe946a674d89546582cea4ba2bb8defac896622d6496f14c23ba5cf" -dependencies = [ - "ref-cast-impl", -] - -[[package]] -name = "ref-cast-impl" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "resolv-conf" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95325155c684b1c89f7765e30bc1c42e4a6da51ca513615660cb8a62ef9a88e3" - -[[package]] -name = "ring" -version = "0.17.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" -dependencies = [ - "cc", - "cfg-if", - "getrandom 0.2.16", - "libc", - "untrusted", - "windows-sys 0.52.0", -] - -[[package]] -name = "rustc-demangle" -version = "0.1.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" - -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - -[[package]] -name = "rustc_version_runtime" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dd18cd2bae1820af0b6ad5e54f4a51d0f3fcc53b05f845675074efcc7af071d" -dependencies = [ - "rustc_version", - "semver", -] - -[[package]] -name = "rustls" -version = "0.21.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" -dependencies = [ - "log", - "ring", - "rustls-webpki", - "sct", -] - -[[package]] -name = "rustls-pemfile" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" -dependencies = [ - "base64 0.21.7", -] - -[[package]] -name = "rustls-webpki" -version = "0.101.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" -dependencies = [ - "ring", - "untrusted", -] - -[[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.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" - -[[package]] -name = "schemars" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" -dependencies = [ - "dyn-clone", - "ref-cast", - "serde", - "serde_json", -] - -[[package]] -name = "schemars" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0" -dependencies = [ - "dyn-clone", - "ref-cast", - "serde", - "serde_json", -] - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "sct" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" -dependencies = [ - "ring", - "untrusted", -] - -[[package]] -name = "semver" -version = "1.0.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" - -[[package]] -name = "serde" -version = "1.0.219" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_bytes" -version = "0.11.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8437fd221bde2d4ca316d61b90e337e9e702b3820b87d63caa9ba6c02bd06d96" -dependencies = [ - "serde", -] - -[[package]] -name = "serde_derive" -version = "1.0.219" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "serde_json" -version = "1.0.142" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7" -dependencies = [ - "indexmap 2.10.0", - "itoa", - "memchr", - "ryu", - "serde", -] - -[[package]] -name = "serde_path_to_error" -version = "0.1.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59fab13f937fa393d08645bf3a84bdfe86e296747b506ada67bb15f10f218b2a" -dependencies = [ - "itoa", - "serde", -] - -[[package]] -name = "serde_urlencoded" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "serde_with" -version = "3.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2c45cd61fefa9db6f254525d46e392b852e0e61d9a1fd36e5bd183450a556d5" -dependencies = [ - "base64 0.22.1", - "chrono", - "hex", - "indexmap 1.9.3", - "indexmap 2.10.0", - "schemars 0.9.0", - "schemars 1.0.4", - "serde", - "serde_derive", - "serde_json", - "serde_with_macros", - "time", -] - -[[package]] -name = "serde_with_macros" -version = "3.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de90945e6565ce0d9a25098082ed4ee4002e047cb59892c318d66821e14bb30f" -dependencies = [ - "darling", - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "sha1" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "sha1_smol" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" - -[[package]] -name = "sha2" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "sharded-slab" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" -dependencies = [ - "lazy_static", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "signal-hook-registry" -version = "1.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" -dependencies = [ - "libc", -] - -[[package]] -name = "slab" -version = "0.4.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" - -[[package]] -name = "smallvec" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" - -[[package]] -name = "socket2" -version = "0.5.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - -[[package]] -name = "socket2" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" -dependencies = [ - "libc", - "windows-sys 0.59.0", -] - -[[package]] -name = "stable_deref_trait" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" - -[[package]] -name = "stringprep" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" -dependencies = [ - "unicode-bidi", - "unicode-normalization", - "unicode-properties", -] - -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - -[[package]] -name = "subtle" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "2.0.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "sync_wrapper" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" - -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "take_mut" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f764005d11ee5f36500a149ace24e00e3da98b0158b3e2d53a7495660d3f4d60" - -[[package]] -name = "tap" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" - -[[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 2.0.106", -] - -[[package]] -name = "thread_local" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "time" -version = "0.3.41" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" -dependencies = [ - "deranged", - "itoa", - "num-conv", - "powerfmt", - "serde", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" - -[[package]] -name = "time-macros" -version = "0.2.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" -dependencies = [ - "num-conv", - "time-core", -] - -[[package]] -name = "tiny-keccak" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" -dependencies = [ - "crunchy", -] - -[[package]] -name = "tinystr" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" -dependencies = [ - "displaydoc", - "zerovec", -] - -[[package]] -name = "tinyvec" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - -[[package]] -name = "tokio" -version = "1.47.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" -dependencies = [ - "backtrace", - "bytes", - "io-uring", - "libc", - "mio", - "parking_lot", - "pin-project-lite", - "signal-hook-registry", - "slab", - "socket2 0.6.0", - "tokio-macros", - "windows-sys 0.59.0", -] - -[[package]] -name = "tokio-macros" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "tokio-rustls" -version = "0.24.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" -dependencies = [ - "rustls", - "tokio", -] - -[[package]] -name = "tokio-util" -version = "0.7.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" -dependencies = [ - "bytes", - "futures-core", - "futures-io", - "futures-sink", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "tower" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" -dependencies = [ - "futures-core", - "futures-util", - "pin-project-lite", - "sync_wrapper", - "tokio", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "tower-layer" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" - -[[package]] -name = "tower-service" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" - -[[package]] -name = "tracing" -version = "0.1.41" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" -dependencies = [ - "log", - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "tracing-core" -version = "0.1.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" -dependencies = [ - "once_cell", - "valuable", -] - -[[package]] -name = "tracing-log" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" -dependencies = [ - "log", - "once_cell", - "tracing-core", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" -dependencies = [ - "nu-ansi-term", - "sharded-slab", - "smallvec", - "thread_local", - "tracing-core", - "tracing-log", -] - -[[package]] -name = "try-lock" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" - -[[package]] -name = "typed-builder" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89851716b67b937e393b3daa8423e67ddfc4bbbf1654bcf05488e95e0828db0c" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "typenum" -version = "1.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" - -[[package]] -name = "unicode-bidi" -version = "0.3.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" - -[[package]] -name = "unicode-ident" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" - -[[package]] -name = "unicode-normalization" -version = "0.1.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" -dependencies = [ - "tinyvec", -] - -[[package]] -name = "unicode-properties" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" - -[[package]] -name = "untrusted" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" - -[[package]] -name = "url" -version = "2.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", -] - -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - -[[package]] -name = "uuid" -version = "1.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f33196643e165781c20a5ead5582283a7dacbb87855d867fbc2df3f81eddc1be" -dependencies = [ - "getrandom 0.3.3", - "js-sys", - "serde", - "wasm-bindgen", -] - -[[package]] -name = "valuable" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "want" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" -dependencies = [ - "try-lock", -] - -[[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" - -[[package]] -name = "wasi" -version = "0.14.2+wasi-0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" -dependencies = [ - "wit-bindgen-rt", -] - -[[package]] -name = "wasm-bindgen" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn 2.0.106", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", - "wasm-bindgen-backend", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "webpki-roots" -version = "0.25.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" - -[[package]] -name = "widestring" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd7cf3379ca1aac9eea11fba24fd7e315d621f8dfe35c8d7d2be8b793726e07d" - -[[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-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.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link", - "windows-result", - "windows-strings", -] - -[[package]] -name = "windows-implement" -version = "0.60.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "windows-interface" -version = "0.59.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "windows-link" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" - -[[package]] -name = "windows-result" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-strings" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets 0.48.5", -] - -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-targets" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "winreg" -version = "0.50.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" -dependencies = [ - "cfg-if", - "windows-sys 0.48.0", -] - -[[package]] -name = "wit-bindgen-rt" -version = "0.39.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" -dependencies = [ - "bitflags 2.9.2", -] - -[[package]] -name = "writeable" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" - -[[package]] -name = "wyz" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" -dependencies = [ - "tap", -] - -[[package]] -name = "yoke" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" -dependencies = [ - "serde", - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", - "synstructure", -] - -[[package]] -name = "zerocopy" -version = "0.8.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "zerofrom" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", - "synstructure", -] - -[[package]] -name = "zerotrie" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", -] - -[[package]] -name = "zerovec" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] diff --git a/Cargo.toml b/Cargo.toml deleted file mode 100644 index d68db28..0000000 --- a/Cargo.toml +++ /dev/null @@ -1,28 +0,0 @@ -[package] -name = "bue-worker" -version = "0.1.0" -edition = "2024" - -[dependencies] -anyhow = "1.0.99" -dotenvy = "0.15.7" -hyper = { version = "1.6.0", features = ["full"] } -axum = "0.8.4" -mongodb = "3.2.4" -redis = { version = "0.32.5", features = ["tokio-comp"] } -serde = { version = "1.0.219", features = ["derive"] } -serde_json = "1.0.142" -tokio = { version = "1.47.1", features = ["full"] } -tracing = "0.1.41" -tracing-subscriber = "0.3.19" -bson = "2.15.0" -futures = "0.3.31" -base64 = "0.22.1" -chrono = "0.4.41" - -[profile.release] -opt-level = 3 -lto = "fat" -codegen-units = 1 -panic = "abort" -strip = true diff --git a/Dockerfile b/Dockerfile index 1836dab..0394f86 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,30 +1,25 @@ -# Builder stage -FROM rust:1.85-slim as builder +FROM golang:1.23-alpine AS builder -# Install musl cross toolchain and build deps -RUN apt-get update && apt-get install -y \ - musl-tools \ - build-essential \ - pkg-config \ - libssl-dev \ - && rm -rf /var/lib/apt/lists/* +RUN apk add --no-cache git ca-certificates tzdata -# Add musl target -RUN rustup target add x86_64-unknown-linux-musl +WORKDIR /app + +COPY go.mod go.sum ./ +RUN go mod download -WORKDIR /usr/src/app COPY . . -# Build statically linked binary -RUN cargo build --release --target x86_64-unknown-linux-musl +RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -ldflags '-extldflags "-static"' -o worker ./cmd/worker + +FROM alpine:latest + +RUN apk --no-cache add ca-certificates tzdata -# Final minimal runtime image -FROM debian:bullseye-slim -WORKDIR /usr/src/app +WORKDIR /app -RUN apt-get update && apt-get install -y ca-certificates \ - && rm -rf /var/lib/apt/lists/* +COPY --from=builder /app/worker . -COPY --from=builder /usr/src/app/target/x86_64-unknown-linux-musl/release/bue-worker . +RUN addgroup -S appgroup && adduser -S appuser -G appgroup +USER appuser -CMD ["./bue-worker"] +CMD ["./worker"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..588fe67 --- /dev/null +++ b/Makefile @@ -0,0 +1,57 @@ +.PHONY: build run test clean docker-build docker-run install + +# Build the application +build: + go build -o bin/worker ./cmd/worker + +# Run the application +run: + go run ./cmd/worker + +# Run tests +test: + go test -v ./... + +# Run tests with coverage +test-coverage: + go test -v -coverprofile=coverage.out ./... + go tool cover -html=coverage.out -o coverage.html + +# Clean build artifacts +clean: + rm -rf bin/ + rm -f coverage.out coverage.html + +# Install dependencies +install: + go mod download + go mod tidy + +# Run linter +lint: + golangci-lint run + +# Format code +fmt: + go fmt ./... + +# Build Docker image +docker-build: + docker build -t bue-worker . + +# Run with Docker Compose +docker-up: + docker-compose up -d + +# Stop Docker Compose +docker-down: + docker-compose down + +# View logs +docker-logs: + docker-compose logs -f worker + +# Run locally with .env +dev: + @if [ ! -f .env ]; then echo "Error: .env file not found"; exit 1; fi + go run ./cmd/worker diff --git a/README.md b/README.md new file mode 100644 index 0000000..7116229 --- /dev/null +++ b/README.md @@ -0,0 +1,256 @@ +# Bue Worker + +A high-performance Go worker service that fetches events from Redis queues and bulk writes them to PostgreSQL with intelligent batching and error handling. + +## Features + +- 🚀 **High Performance**: Concurrent processing of multiple event queues +- 📊 **Adaptive Tuning**: Dynamically adjusts batch sizes and intervals based on queue length +- 🔄 **Dead Letter Queue**: Automatic retry mechanism for failed events +- 🛡️ **Graceful Shutdown**: Ensures all pending events are flushed before shutdown +- 📝 **JSON Logging**: Structured logging with zerolog +- 🐳 **Docker Ready**: Includes optimized multi-stage Dockerfile + +## Architecture + +The worker processes three types of Discord events: + +- **Event Logs** (`logs:events`) - General Discord events +- **Guardian Logs** (`logs:guardian`) - Moderation/guardian actions +- **Join Logs** (`logs:join`) - User join/leave events + +Events are: + +1. Fetched from Redis lists in batches +2. Base64-decoded and parsed from JSON +3. Bulk inserted into PostgreSQL tables +4. Failed events are automatically moved to a DLQ for retry + +## Prerequisites + +- Go 1.23+ +- PostgreSQL 13+ +- Redis 6+ + +## Installation + +```bash +# Clone the repository +git clone https://github.com/bueapp/worker.git +cd worker + +# Install dependencies +go mod download + +# Build +go build -o worker ./cmd/worker +``` + +## Configuration + +Configure via environment variables (or `.env` file): + +```bash +# Required +DATABASE_URL=postgresql://user:pass@localhost:5432/dbname +REDIS_URL=redis://default:pass@localhost:6379 + +# Optional +LOG_LEVEL=info # debug, info, warn, error (default: info) +BATCH_SIZE=1000 # Base batch size (default: 1000) +FLUSH_INTERVAL=5 # Flush interval in seconds (default: 5) +``` + +## Usage + +### Run Locally + +```bash +# With environment variables +export DATABASE_URL="postgresql://..." +export REDIS_URL="redis://..." +./worker + +# Or with .env file +./worker +``` + +### Run with Docker + +```bash +# Build image +docker build -t bue-worker . + +# Run container +docker run --env-file .env bue-worker +``` + +### Docker Compose + +```yaml +version: "3.8" + +services: + worker: + build: . + environment: + DATABASE_URL: postgresql://postgres:password@postgres:5432/discord + REDIS_URL: redis://redis:6379 + LOG_LEVEL: info + depends_on: + - postgres + - redis + restart: unless-stopped + + postgres: + image: postgres:16-alpine + environment: + POSTGRES_PASSWORD: password + POSTGRES_DB: discord + volumes: + - postgres-data:/var/lib/postgresql/data + + redis: + image: redis:7-alpine + volumes: + - redis-data:/data + +volumes: + postgres-data: + redis-data: +``` + +## Database Schema + +The worker automatically creates the following tables: + +### Events Table + +```sql +CREATE TABLE events ( + id TEXT PRIMARY KEY, + guild_id TEXT NOT NULL, + channel_id TEXT, + user_id TEXT, + event_type TEXT NOT NULL, + logged_at TIMESTAMPTZ NOT NULL, + data JSONB, + created_at TIMESTAMPTZ DEFAULT NOW() +); +``` + +### Guardian Logs Table + +```sql +CREATE TABLE guardian_logs ( + id TEXT PRIMARY KEY, + guild_id TEXT NOT NULL, + user_id TEXT, + action TEXT NOT NULL, + created_at TIMESTAMPTZ NOT NULL, + data JSONB, + inserted_at TIMESTAMPTZ DEFAULT NOW() +); +``` + +### Join Logs Table + +```sql +CREATE TABLE join_logs ( + id TEXT PRIMARY KEY, + guild_id TEXT NOT NULL, + user_id TEXT NOT NULL, + event_type TEXT NOT NULL, + created_at TIMESTAMPTZ NOT NULL, + data JSONB, + inserted_at TIMESTAMPTZ DEFAULT NOW() +); +``` + +## Adaptive Tuning + +The worker automatically adjusts performance based on queue depth: + +| Queue Length | Batch Size Multiplier | Interval Multiplier | +| ------------ | --------------------- | ------------------- | +| > 100,000 | 4x | 0.5x (faster) | +| > 10,000 | 2x | 0.5x (faster) | +| > 1,000 | 1x | 1x (normal) | +| < 1,000 | 0.5x | 2x (slower) | + +This ensures efficient processing during high load and resource conservation during low load. + +## Dead Letter Queue (DLQ) + +Failed events are automatically: + +1. Base64-encoded and pushed to `logs:failed` Redis list +2. Periodically retried by the DLQ reprocessor (every 5 seconds) +3. Logged with error details + +## Monitoring + +The worker provides structured JSON logs suitable for log aggregation systems: + +```json +{ + "level": "info", + "time": 1700000000, + "count": 1000, + "batch_size": 1000, + "interval": 5000000000, + "message": "✅ Flushed event logs" +} +``` + +## Graceful Shutdown + +When receiving SIGTERM or SIGINT: + +1. Stops accepting new work +2. Flushes all remaining events from Redis +3. Waits up to 30 seconds for completion +4. Closes all connections cleanly + +## Development + +### Project Structure + +``` +. +├── cmd/ +│ └── worker/ # Main application +│ └── main.go +├── internal/ +│ ├── config/ # Configuration +│ ├── database/ # PostgreSQL operations +│ ├── dlq/ # Dead letter queue handler +│ ├── flush/ # Core flush logic +│ ├── models/ # Data models +│ ├── redis/ # Redis client +│ └── tuning/ # Adaptive tuning +├── Dockerfile +├── go.mod +├── go.sum +└── README.md +``` + +### Running Tests + +```bash +go test ./... +``` + +### Linting + +```bash +golangci-lint run +``` + +## License + +MIT License - see [LICENSE](LICENSE) file for details + +## Contributing + +Contributions are welcome! Please feel free to submit a Pull Request. diff --git a/cmd/worker/main.go b/cmd/worker/main.go new file mode 100644 index 0000000..1ca678e --- /dev/null +++ b/cmd/worker/main.go @@ -0,0 +1,260 @@ +package main + +import ( + "context" + "os" + "os/signal" + "sync" + "syscall" + "time" + + "github.com/bueapp/worker/internal/config" + "github.com/bueapp/worker/internal/database" + "github.com/bueapp/worker/internal/dlq" + "github.com/bueapp/worker/internal/flush" + "github.com/bueapp/worker/internal/redis" + "github.com/bueapp/worker/internal/tuning" + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" +) + +func main() { + // Load configuration + cfg := config.Load() + + // Setup logger + setupLogger(cfg.LogLevel) + + ctx := context.Background() + + // Connect to Redis + redisClient, err := redis.New(cfg.RedisURL) + if err != nil { + log.Fatal().Err(err).Msg("Failed to connect to Redis") + } + defer redisClient.Close() + + // Connect to PostgreSQL + db, err := database.New(ctx, cfg.DatabaseURL) + if err != nil { + log.Fatal().Err(err).Msg("Failed to connect to database") + } + defer db.Close() + + log.Info(). + Int("batch_size", cfg.BatchSize). + Dur("flush_interval", cfg.FlushInterval). + Msg("🚀 Log worker started") + + // Start DLQ reprocessor in background + dlqCtx, dlqCancel := context.WithCancel(ctx) + defer dlqCancel() + + go dlq.Reprocessor(dlqCtx, redisClient, db, cfg.BatchSize) + + // Setup graceful shutdown + shutdownCtx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) + defer stop() + + // Start worker loop + var wg sync.WaitGroup + wg.Add(1) + + go func() { + defer wg.Done() + workerLoop(shutdownCtx, redisClient, db, cfg) + }() + + // Wait for shutdown signal + <-shutdownCtx.Done() + log.Info().Msg("🛑 Shutdown signal received, flushing remaining logs...") + + // Flush remaining logs + flushCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + flushRemaining(flushCtx, redisClient, db, cfg.BatchSize) + + // Wait for worker to finish + wg.Wait() + + log.Info().Msg("✅ Shutdown complete, exiting cleanly") +} + +func workerLoop(ctx context.Context, redisClient *redis.Client, db *database.DB, cfg *config.Config) { + ticker := time.NewTicker(cfg.FlushInterval) + defer ticker.Stop() + + // Create stream adapter + streamAdapter := &flush.StreamAdapter{Client: redisClient} + + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + // Discover all guild event streams (pattern: guilds:*:events) + eventStreams, err := redisClient.Scan(ctx, "guilds:*:events") + if err != nil { + log.Error().Err(err).Msg("Failed to scan for event streams") + continue + } + + // Also check for guardian and join streams + guardianStreams, _ := redisClient.Scan(ctx, "guilds:*:guardian") + joinStreams, _ := redisClient.Scan(ctx, "guilds:*:join") + + totalStreams := len(eventStreams) + len(guardianStreams) + len(joinStreams) + + // Calculate total queue size across all streams + var totalQueue int64 + for _, stream := range eventStreams { + if length, err := redisClient.XLen(ctx, stream); err == nil { + totalQueue += length + } + } + + // Adjust tuning based on total queue + batchSize, interval := tuning.AdjustTuning(int(totalQueue), cfg.BatchSize, cfg.FlushInterval) + ticker.Reset(interval) + + if totalStreams == 0 { + continue + } + + log.Debug(). + Int("event_streams", len(eventStreams)). + Int("guardian_streams", len(guardianStreams)). + Int("join_streams", len(joinStreams)). + Int64("total_queue", totalQueue). + Msg("Processing streams") + + // Process event streams + var wg sync.WaitGroup + + for _, stream := range eventStreams { + wg.Add(1) + go func(streamKey string) { + defer wg.Done() + count, err := flush.FlushStreamLogs( + ctx, + streamAdapter, + streamKey, + batchSize, + db.BulkInsertEvents, + ) + if err != nil { + log.Error().Err(err).Str("stream", streamKey).Msg("❌ Error flushing events") + } else if count > 0 { + log.Info(). + Int("count", count). + Str("stream", streamKey). + Msg("✅ Flushed event logs") + } + }(stream) + } + + // Process guardian streams + for _, stream := range guardianStreams { + wg.Add(1) + go func(streamKey string) { + defer wg.Done() + count, err := flush.FlushStreamLogs( + ctx, + streamAdapter, + streamKey, + batchSize, + db.BulkInsertGuardianLogs, + ) + if err != nil { + log.Error().Err(err).Str("stream", streamKey).Msg("❌ Error flushing guardian logs") + } else if count > 0 { + log.Info(). + Int("count", count). + Str("stream", streamKey). + Msg("✅ Flushed guardian logs") + } + }(stream) + } + + // Process join streams + for _, stream := range joinStreams { + wg.Add(1) + go func(streamKey string) { + defer wg.Done() + count, err := flush.FlushStreamLogs( + ctx, + streamAdapter, + streamKey, + batchSize, + db.BulkInsertJoinLogs, + ) + if err != nil { + log.Error().Err(err).Str("stream", streamKey).Msg("❌ Error flushing join logs") + } else if count > 0 { + log.Info(). + Int("count", count). + Str("stream", streamKey). + Msg("✅ Flushed join logs") + } + }(stream) + } + + wg.Wait() + } + } +} + +func flushRemaining(ctx context.Context, redisClient *redis.Client, db *database.DB, batchSize int) { + streamAdapter := &flush.StreamAdapter{Client: redisClient} + + // Discover all streams + eventStreams, _ := redisClient.Scan(ctx, "guilds:*:events") + guardianStreams, _ := redisClient.Scan(ctx, "guilds:*:guardian") + joinStreams, _ := redisClient.Scan(ctx, "guilds:*:join") + + // Flush all event streams + for _, stream := range eventStreams { + count, _ := flush.FlushStreamLogs(ctx, streamAdapter, stream, batchSize, db.BulkInsertEvents) + if count > 0 { + log.Info().Int("count", count).Str("stream", stream).Msg("Flushed event logs on shutdown") + } + } + + // Flush all guardian streams + for _, stream := range guardianStreams { + count, _ := flush.FlushStreamLogs(ctx, streamAdapter, stream, batchSize, db.BulkInsertGuardianLogs) + if count > 0 { + log.Info().Int("count", count).Str("stream", stream).Msg("Flushed guardian logs on shutdown") + } + } + + // Flush all join streams + for _, stream := range joinStreams { + count, _ := flush.FlushStreamLogs(ctx, streamAdapter, stream, batchSize, db.BulkInsertJoinLogs) + if count > 0 { + log.Info().Int("count", count).Str("stream", stream).Msg("Flushed join logs on shutdown") + } + } +} + +func setupLogger(level string) { + // Use JSON output for production + zerolog.TimeFieldFormat = zerolog.TimeFormatUnix + + // Set log level + switch level { + case "debug": + zerolog.SetGlobalLevel(zerolog.DebugLevel) + case "info": + zerolog.SetGlobalLevel(zerolog.InfoLevel) + case "warn": + zerolog.SetGlobalLevel(zerolog.WarnLevel) + case "error": + zerolog.SetGlobalLevel(zerolog.ErrorLevel) + default: + zerolog.SetGlobalLevel(zerolog.InfoLevel) + } + + log.Logger = zerolog.New(os.Stdout).With().Timestamp().Logger() +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..bd9c4b6 --- /dev/null +++ b/go.mod @@ -0,0 +1,27 @@ +module github.com/bueapp/worker + +go 1.25.3 + +require github.com/stretchr/testify v1.11.1 + +require ( + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/pgx/v5 v5.7.6 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/joho/godotenv v1.5.1 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/redis/go-redis/v9 v9.17.0 // indirect + github.com/rs/zerolog v1.34.0 // indirect + github.com/stretchr/objx v0.5.2 // indirect + golang.org/x/crypto v0.37.0 // indirect + golang.org/x/sync v0.13.0 // indirect + golang.org/x/sys v0.32.0 // indirect + golang.org/x/text v0.24.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..28b2e2e --- /dev/null +++ b/go.sum @@ -0,0 +1,54 @@ +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.7.6 h1:rWQc5FwZSPX58r1OQmkuaNicxdmExaEz5A2DO2hUuTk= +github.com/jackc/pgx/v5 v5.7.6/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/redis/go-redis/v9 v9.17.0 h1:K6E+ZlYN95KSMmZeEQPbU/c++wfmEvfFB17yEAq/VhM= +github.com/redis/go-redis/v9 v9.17.0/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370= +github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= +github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= +github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= +golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= +golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= +golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= +golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= +golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..2b49304 --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,47 @@ +package config + +import ( + "os" + "strconv" + "time" + + "github.com/joho/godotenv" +) + +type Config struct { + RedisURL string + DatabaseURL string + LogLevel string + FlushInterval time.Duration + BatchSize int +} + +// Load reads configuration from environment variables +func Load() *Config { + // Load .env file if it exists + _ = godotenv.Load() + + return &Config{ + RedisURL: getEnv("REDIS_URL", "redis://localhost:6379"), + DatabaseURL: getEnv("DATABASE_URL", "postgresql://postgres:postgres@localhost:5432/postgres"), + LogLevel: getEnv("LOG_LEVEL", "info"), + FlushInterval: time.Duration(getEnvInt("FLUSH_INTERVAL", 5)) * time.Second, + BatchSize: getEnvInt("BATCH_SIZE", 1000), + } +} + +func getEnv(key, defaultValue string) string { + if value := os.Getenv(key); value != "" { + return value + } + return defaultValue +} + +func getEnvInt(key string, defaultValue int) int { + if value := os.Getenv(key); value != "" { + if intVal, err := strconv.Atoi(value); err == nil { + return intVal + } + } + return defaultValue +} diff --git a/internal/config/config_test.go b/internal/config/config_test.go new file mode 100644 index 0000000..e61d8b3 --- /dev/null +++ b/internal/config/config_test.go @@ -0,0 +1,84 @@ +package config + +import ( + "os" + "testing" + "time" +) + +func TestLoad(t *testing.T) { + // Clear env vars + os.Clearenv() + + cfg := Load() + + if cfg.RedisURL != "redis://localhost:6379" { + t.Errorf("expected default RedisURL, got %s", cfg.RedisURL) + } + if cfg.DatabaseURL != "postgresql://postgres:postgres@localhost:5432/postgres" { + t.Errorf("expected default DatabaseURL, got %s", cfg.DatabaseURL) + } + if cfg.LogLevel != "info" { + t.Errorf("expected default LogLevel, got %s", cfg.LogLevel) + } + if cfg.FlushInterval != 5*time.Second { + t.Errorf("expected 5s FlushInterval, got %v", cfg.FlushInterval) + } + if cfg.BatchSize != 1000 { + t.Errorf("expected 1000 BatchSize, got %d", cfg.BatchSize) + } +} + +func TestLoadWithEnvVars(t *testing.T) { + os.Setenv("REDIS_URL", "redis://custom:6380") + os.Setenv("DATABASE_URL", "postgresql://custom") + os.Setenv("LOG_LEVEL", "debug") + os.Setenv("FLUSH_INTERVAL", "10") + os.Setenv("BATCH_SIZE", "500") + defer os.Clearenv() + + cfg := Load() + + if cfg.RedisURL != "redis://custom:6380" { + t.Errorf("expected custom RedisURL, got %s", cfg.RedisURL) + } + if cfg.DatabaseURL != "postgresql://custom" { + t.Errorf("expected custom DatabaseURL, got %s", cfg.DatabaseURL) + } + if cfg.LogLevel != "debug" { + t.Errorf("expected debug LogLevel, got %s", cfg.LogLevel) + } + if cfg.FlushInterval != 10*time.Second { + t.Errorf("expected 10s FlushInterval, got %v", cfg.FlushInterval) + } + if cfg.BatchSize != 500 { + t.Errorf("expected 500 BatchSize, got %d", cfg.BatchSize) + } +} + +func TestGetEnv(t *testing.T) { + os.Setenv("TEST_KEY", "test_value") + defer os.Unsetenv("TEST_KEY") + + if getEnv("TEST_KEY", "default") != "test_value" { + t.Error("expected test_value") + } + if getEnv("NONEXISTENT", "default") != "default" { + t.Error("expected default") + } +} + +func TestGetEnvInt(t *testing.T) { + os.Setenv("TEST_INT", "42") + defer os.Unsetenv("TEST_INT") + + if getEnvInt("TEST_INT", 10) != 42 { + t.Error("expected 42") + } + if getEnvInt("NONEXISTENT", 10) != 10 { + t.Error("expected 10") + } + if getEnvInt("INVALID_INT", 10) != 10 { + t.Error("expected 10 for invalid int") + } +} diff --git a/internal/database/database.go b/internal/database/database.go new file mode 100644 index 0000000..c4268b9 --- /dev/null +++ b/internal/database/database.go @@ -0,0 +1,237 @@ +package database + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgxpool" + "github.com/rs/zerolog/log" +) + +type DB struct { + pool *pgxpool.Pool +} + +// New creates a new database connection pool +func New(ctx context.Context, databaseURL string) (*DB, error) { + config, err := pgxpool.ParseConfig(databaseURL) + if err != nil { + return nil, fmt.Errorf("parse database URL: %w", err) + } + + // Configure connection to prefer simple protocol and disable TLS if not in URL + // This handles servers that don't support TLS + if config.ConnConfig.TLSConfig == nil { + config.ConnConfig.TLSConfig = nil // Disable TLS + } + + pool, err := pgxpool.NewWithConfig(ctx, config) + if err != nil { + return nil, fmt.Errorf("create connection pool: %w", err) + } + + // Test connection + if err := pool.Ping(ctx); err != nil { + return nil, fmt.Errorf("ping database: %w", err) + } + + db := &DB{pool: pool} + + // Verify connection (tables managed by Drizzle) + if err := db.verifyConnection(ctx); err != nil { + return nil, fmt.Errorf("verify connection: %w", err) + } + + return db, nil +} + +// verifyConnection verifies the database connection is working +// Tables are managed by Drizzle migrations, not this worker +func (db *DB) verifyConnection(ctx context.Context) error { + // Just verify we can query the database + var result int + err := db.pool.QueryRow(ctx, "SELECT 1").Scan(&result) + if err != nil { + return fmt.Errorf("verify connection: %w", err) + } + return nil +} + +// BulkInsertEvents inserts multiple event logs in a single transaction +// Matches the Drizzle schema with flattened fields +func (db *DB) BulkInsertEvents(ctx context.Context, logs []map[string]interface{}) (int, error) { + if len(logs) == 0 { + return 0, nil + } + + batch := &pgx.Batch{} + for _, logEntry := range logs { + // Convert JSONB fields + oldValueJSON, _ := json.Marshal(normalizeValue(logEntry["oldValue"])) + newValueJSON, _ := json.Marshal(normalizeValue(logEntry["newValue"])) + + // Handle "undefined" strings as NULL + batch.Queue( + `INSERT INTO events ( + guild_id, action, action_thread_id, logged_at, + actor_id, channel_id, target_id, message_id, + reason, duration, old_value, new_value, url + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)`, + logEntry["guildId"], + logEntry["action"], + normalizeString(logEntry["actionThreadId"]), + logEntry["loggedAt"], + normalizeString(logEntry["actorId"]), + normalizeString(logEntry["channelId"]), + normalizeString(logEntry["targetId"]), + normalizeString(logEntry["messageId"]), + normalizeString(logEntry["reason"]), + normalizeInt(logEntry["duration"]), + oldValueJSON, + newValueJSON, + normalizeString(logEntry["url"]), + ) + } + + results := db.pool.SendBatch(ctx, batch) + defer results.Close() + + inserted := 0 + for i := 0; i < len(logs); i++ { + _, err := results.Exec() + if err != nil { + log.Warn().Err(err).Interface("log", logs[i]).Msg("Failed to insert event log") + } else { + inserted++ + } + } + + return inserted, nil +} + +// BulkInsertGuardianLogs inserts multiple guardian logs in a single transaction +// Matches the Drizzle schema +func (db *DB) BulkInsertGuardianLogs(ctx context.Context, logs []map[string]interface{}) (int, error) { + if len(logs) == 0 { + return 0, nil + } + + batch := &pgx.Batch{} + for _, logEntry := range logs { + metadataJSON, _ := json.Marshal(normalizeValue(logEntry["metadata"])) + + batch.Queue( + `INSERT INTO guardian_logs ( + guild_id, action, performed_by, target, reason, metadata + ) VALUES ($1, $2, $3, $4, $5, $6)`, + logEntry["guildId"], + logEntry["action"], + logEntry["performedBy"], + normalizeString(logEntry["target"]), + normalizeString(logEntry["reason"]), + metadataJSON, + ) + } + + results := db.pool.SendBatch(ctx, batch) + defer results.Close() + + inserted := 0 + for i := 0; i < len(logs); i++ { + _, err := results.Exec() + if err != nil { + log.Warn().Err(err).Interface("log", logs[i]).Msg("Failed to insert guardian log") + } else { + inserted++ + } + } + + return inserted, nil +} + +// BulkInsertJoinLogs inserts multiple join logs in a single transaction +// Matches the Drizzle schema +func (db *DB) BulkInsertJoinLogs(ctx context.Context, logs []map[string]interface{}) (int, error) { + if len(logs) == 0 { + return 0, nil + } + + batch := &pgx.Batch{} + for _, logEntry := range logs { + batch.Queue( + `INSERT INTO join_logs ( + guild_id, user_id, action + ) VALUES ($1, $2, $3)`, + logEntry["guildId"], + logEntry["userId"], + logEntry["action"], + ) + } + + results := db.pool.SendBatch(ctx, batch) + defer results.Close() + + inserted := 0 + for i := 0; i < len(logs); i++ { + _, err := results.Exec() + if err != nil { + log.Warn().Err(err).Interface("log", logs[i]).Msg("Failed to insert join log") + } else { + inserted++ + } + } + + return inserted, nil +} + +// normalizeString handles "undefined" strings from JavaScript as NULL +func normalizeString(v interface{}) *string { + if v == nil { + return nil + } + s, ok := v.(string) + if !ok { + return nil + } + if s == "undefined" || s == "null" || s == "" { + return nil + } + return &s +} + +// normalizeInt handles "undefined" or nil values for integers +func normalizeInt(v interface{}) *int { + if v == nil { + return nil + } + switch val := v.(type) { + case float64: + i := int(val) + return &i + case int: + return &val + case string: + if val == "undefined" || val == "null" || val == "" { + return nil + } + } + return nil +} + +// normalizeValue handles "undefined" values for JSONB fields +func normalizeValue(v interface{}) interface{} { + if v == nil { + return nil + } + if s, ok := v.(string); ok && (s == "undefined" || s == "null") { + return nil + } + return v +} + +// Close closes the database connection pool +func (db *DB) Close() { + db.pool.Close() +} diff --git a/internal/database/database_test.go b/internal/database/database_test.go new file mode 100644 index 0000000..d3a4a8b --- /dev/null +++ b/internal/database/database_test.go @@ -0,0 +1,159 @@ +package database + +import ( + "context" + "testing" + "time" +) + +func TestBulkInsertEvents(t *testing.T) { + tests := []struct { + name string + logs []map[string]interface{} + wantErr bool + expected int + }{ + { + name: "empty logs", + logs: []map[string]interface{}{}, + wantErr: false, + expected: 0, + }, + { + name: "single event", + logs: []map[string]interface{}{ + { + "guildId": "guild-123", + "action": "MessageDelete", + "loggedAt": time.Now(), + "actorId": "actor-456", + "channelId": "channel-789", + "targetId": "target-111", + "messageId": "msg-222", + "reason": "undefined", + "duration": "undefined", + "oldValue": "undefined", + "newValue": "undefined", + "url": "undefined", + }, + }, + wantErr: false, + expected: 1, + }, + { + name: "multiple events", + logs: []map[string]interface{}{ + { + "guildId": "guild-123", + "action": "MessageDelete", + "loggedAt": time.Now(), + "actorId": "actor-456", + "channelId": "channel-789", + }, + { + "guildId": "guild-123", + "action": "MessageUpdate", + "loggedAt": time.Now(), + "actorId": "actor-456", + "channelId": "channel-789", + "oldValue": map[string]string{"content": "old"}, + "newValue": map[string]string{"content": "new"}, + }, + }, + wantErr: false, + expected: 2, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.name == "empty logs" { + db := &DB{} + got, err := db.BulkInsertEvents(context.Background(), tt.logs) + if (err != nil) != tt.wantErr { + t.Errorf("BulkInsertEvents() error = %v, wantErr %v", err, tt.wantErr) + } + if got != tt.expected { + t.Errorf("BulkInsertEvents() got = %v, want %v", got, tt.expected) + } + } + }) + } +} + +func TestBulkInsertGuardianLogs(t *testing.T) { + tests := []struct { + name string + logs []map[string]interface{} + expected int + }{ + { + name: "empty logs", + logs: []map[string]interface{}{}, + expected: 0, + }, + { + name: "single guardian log", + logs: []map[string]interface{}{ + { + "guildId": "guild-123", + "action": "ban", + "performedBy": "mod-789", + "target": "user-456", + "reason": "spam", + "metadata": map[string]string{"extra": "data"}, + }, + }, + expected: 1, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.name == "empty logs" { + db := &DB{} + got, _ := db.BulkInsertGuardianLogs(context.Background(), tt.logs) + if got != tt.expected { + t.Errorf("BulkInsertGuardianLogs() got = %v, want %v", got, tt.expected) + } + } + }) + } +} + +func TestBulkInsertJoinLogs(t *testing.T) { + tests := []struct { + name string + logs []map[string]interface{} + expected int + }{ + { + name: "empty logs", + logs: []map[string]interface{}{}, + expected: 0, + }, + { + name: "single join log", + logs: []map[string]interface{}{ + { + "guildId": "guild-123", + "userId": "user-789", + "action": "join", + }, + }, + expected: 1, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.name == "empty logs" { + db := &DB{} + got, _ := db.BulkInsertJoinLogs(context.Background(), tt.logs) + if got != tt.expected { + t.Errorf("BulkInsertJoinLogs() got = %v, want %v", got, tt.expected) + } + } + }) + } +} diff --git a/internal/dlq/dlq.go b/internal/dlq/dlq.go new file mode 100644 index 0000000..b024d34 --- /dev/null +++ b/internal/dlq/dlq.go @@ -0,0 +1,104 @@ +package dlq + +import ( + "context" + "encoding/base64" + "encoding/json" + "time" + + "github.com/rs/zerolog/log" +) + +// RedisClient interface for Redis operations +type RedisClient interface { + LRange(ctx context.Context, key string, start, stop int64) ([]string, error) + LTrim(ctx context.Context, key string, start, stop int64) error + LPush(ctx context.Context, key string, values ...interface{}) error +} + +// Database interface for database operations +type Database interface { + BulkInsertEvents(ctx context.Context, logs []map[string]interface{}) (int, error) +} + +// Reprocessor continuously processes failed logs from the DLQ +func Reprocessor( + ctx context.Context, + redisClient RedisClient, + db Database, + batchSize int, +) { + ticker := time.NewTicker(5 * time.Second) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + if err := processFailedLogs(ctx, redisClient, db, batchSize); err != nil { + log.Error().Err(err).Msg("DLQ reprocessing error") + } + } + } +} + +func processFailedLogs( + ctx context.Context, + redisClient RedisClient, + db Database, + batchSize int, +) error { + // Get failed logs + logs, err := redisClient.LRange(ctx, "logs:failed", 0, int64(batchSize-1)) + if err != nil { + return err + } + + if len(logs) == 0 { + return nil + } + + // Remove from DLQ + if err := redisClient.LTrim(ctx, "logs:failed", int64(len(logs)), -1); err != nil { + return err + } + + // Parse logs + parsedLogs := make([]map[string]interface{}, 0, len(logs)) + for _, logStr := range logs { + decoded, err := base64.StdEncoding.DecodeString(logStr) + if err != nil { + log.Warn().Err(err).Msg("Failed to decode DLQ log") + continue + } + + var logData map[string]interface{} + if err := json.Unmarshal(decoded, &logData); err != nil { + log.Warn().Err(err).Msg("Failed to unmarshal DLQ log") + continue + } + + parsedLogs = append(parsedLogs, logData) + } + + if len(parsedLogs) == 0 { + return nil + } + + // Try to insert into events table (you might want to route to the correct table) + count, err := db.BulkInsertEvents(ctx, parsedLogs) + if err != nil { + log.Error().Err(err).Msg("Failed to reprocess DLQ logs, pushing back") + // Push back to DLQ + for _, logData := range parsedLogs { + jsonBytes, _ := json.Marshal(logData) + encoded := base64.StdEncoding.EncodeToString(jsonBytes) + _ = redisClient.LPush(ctx, "logs:failed", encoded) + } + return err + } + + log.Info().Int("count", count).Msg("✅ Reprocessed logs from DLQ") + return nil +} diff --git a/internal/dlq/dlq_test.go b/internal/dlq/dlq_test.go new file mode 100644 index 0000000..6570b72 --- /dev/null +++ b/internal/dlq/dlq_test.go @@ -0,0 +1,109 @@ +package dlq + +import ( + "context" + "encoding/base64" + "encoding/json" + "errors" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +type MockRedisClient struct { + mock.Mock +} + +func (m *MockRedisClient) LRange(ctx context.Context, key string, start, stop int64) ([]string, error) { + args := m.Called(ctx, key, start, stop) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).([]string), args.Error(1) +} + +func (m *MockRedisClient) LTrim(ctx context.Context, key string, start, stop int64) error { + args := m.Called(ctx, key, start, stop) + return args.Error(0) +} + +func (m *MockRedisClient) LPush(ctx context.Context, key string, values ...interface{}) error { + args := m.Called(ctx, key, values) + return args.Error(0) +} + +type MockDB struct { + mock.Mock +} + +func (m *MockDB) BulkInsertEvents(ctx context.Context, logs []map[string]interface{}) (int, error) { + args := m.Called(ctx, logs) + return args.Int(0), args.Error(1) +} + +func TestProcessFailedLogs_Success(t *testing.T) { + ctx := context.Background() + mockRedis := new(MockRedisClient) + mockDB := new(MockDB) + + logData := map[string]interface{}{"message": "test", "level": "info"} + jsonBytes, _ := json.Marshal(logData) + encoded := base64.StdEncoding.EncodeToString(jsonBytes) + + mockRedis.On("LRange", ctx, "logs:failed", int64(0), int64(9)).Return([]string{encoded}, nil) + mockRedis.On("LTrim", ctx, "logs:failed", int64(1), int64(-1)).Return(nil) + mockDB.On("BulkInsertEvents", ctx, mock.MatchedBy(func(logs []map[string]interface{}) bool { + return len(logs) == 1 + })).Return(1, nil) + + err := processFailedLogs(ctx, mockRedis, mockDB, 10) + assert.NoError(t, err) + mockRedis.AssertExpectations(t) + mockDB.AssertExpectations(t) +} + +func TestProcessFailedLogs_EmptyQueue(t *testing.T) { + ctx := context.Background() + mockRedis := new(MockRedisClient) + mockDB := new(MockDB) + + mockRedis.On("LRange", ctx, "logs:failed", int64(0), int64(9)).Return([]string{}, nil) + + err := processFailedLogs(ctx, mockRedis, mockDB, 10) + assert.NoError(t, err) + mockDB.AssertNotCalled(t, "BulkInsertEvents") +} + +func TestProcessFailedLogs_DecodeError(t *testing.T) { + ctx := context.Background() + mockRedis := new(MockRedisClient) + mockDB := new(MockDB) + + mockRedis.On("LRange", ctx, "logs:failed", int64(0), int64(9)).Return([]string{"invalid_base64!@#"}, nil) + mockRedis.On("LTrim", ctx, "logs:failed", int64(1), int64(-1)).Return(nil) + + err := processFailedLogs(ctx, mockRedis, mockDB, 10) + assert.NoError(t, err) + mockDB.AssertNotCalled(t, "BulkInsertEvents") + mockRedis.AssertExpectations(t) +} + +func TestProcessFailedLogs_InsertFailure(t *testing.T) { + ctx := context.Background() + mockRedis := new(MockRedisClient) + mockDB := new(MockDB) + + logData := map[string]interface{}{"message": "test"} + jsonBytes, _ := json.Marshal(logData) + encoded := base64.StdEncoding.EncodeToString(jsonBytes) + + mockRedis.On("LRange", ctx, "logs:failed", int64(0), int64(9)).Return([]string{encoded}, nil) + mockRedis.On("LTrim", ctx, "logs:failed", int64(1), int64(-1)).Return(nil) + mockDB.On("BulkInsertEvents", ctx, mock.Anything).Return(0, errors.New("db error")) + mockRedis.On("LPush", ctx, "logs:failed", mock.Anything).Return(nil) + + err := processFailedLogs(ctx, mockRedis, mockDB, 10) + assert.Error(t, err) + mockRedis.AssertExpectations(t) +} diff --git a/internal/flush/flush.go b/internal/flush/flush.go new file mode 100644 index 0000000..8c6ea32 --- /dev/null +++ b/internal/flush/flush.go @@ -0,0 +1,219 @@ +package flush + +import ( + "context" + "encoding/base64" + "encoding/json" + "fmt" + "time" + + "github.com/bueapp/worker/internal/redis" + "github.com/rs/zerolog/log" +) + +// RedisClient interface for Redis List operations +type RedisClient interface { + LRange(ctx context.Context, key string, start, stop int64) ([]string, error) + LTrim(ctx context.Context, key string, start, stop int64) error + LPush(ctx context.Context, key string, values ...interface{}) error +} + +// StreamEntry represents a Redis Stream entry +type StreamEntry struct { + ID string + Values map[string]interface{} +} + +// RedisStreamClient interface for Redis Stream operations +type RedisStreamClient interface { + XRange(ctx context.Context, stream, start, stop string, count int64) ([]StreamEntry, error) + XDel(ctx context.Context, stream string, ids ...string) error + XAdd(ctx context.Context, stream string, values map[string]interface{}) (string, error) + XLen(ctx context.Context, stream string) (int64, error) +} + +// StreamAdapter adapts redis.Client to RedisStreamClient interface +type StreamAdapter struct { + Client *redis.Client +} + +func (a *StreamAdapter) XRange(ctx context.Context, stream, start, stop string, count int64) ([]StreamEntry, error) { + entries, err := a.Client.XRange(ctx, stream, start, stop, count) + if err != nil { + return nil, err + } + result := make([]StreamEntry, len(entries)) + for i, e := range entries { + result[i] = StreamEntry{ID: e.ID, Values: e.Values} + } + return result, nil +} + +func (a *StreamAdapter) XDel(ctx context.Context, stream string, ids ...string) error { + return a.Client.XDel(ctx, stream, ids...) +} + +func (a *StreamAdapter) XAdd(ctx context.Context, stream string, values map[string]interface{}) (string, error) { + return a.Client.XAdd(ctx, stream, values) +} + +func (a *StreamAdapter) XLen(ctx context.Context, stream string) (int64, error) { + return a.Client.XLen(ctx, stream) +} + +func (a *StreamAdapter) Scan(ctx context.Context, pattern string) ([]string, error) { + return a.Client.Scan(ctx, pattern) +} + +// FlushStreamLogs fetches logs from Redis Stream and inserts them into PostgreSQL +func FlushStreamLogs( + ctx context.Context, + redisClient RedisStreamClient, + streamKey string, + batchSize int, + insertFunc func(context.Context, []map[string]interface{}) (int, error), +) (int, error) { + // Read entries from stream (from beginning) + entries, err := redisClient.XRange(ctx, streamKey, "-", "+", int64(batchSize)) + if err != nil { + return 0, fmt.Errorf("xrange: %w", err) + } + + if len(entries) == 0 { + return 0, nil + } + + // Convert stream entries to maps and collect IDs + parsedLogs := make([]map[string]interface{}, 0, len(entries)) + entryIDs := make([]string, 0, len(entries)) + + for _, entry := range entries { + logData := make(map[string]interface{}) + + for k, v := range entry.Values { + // Handle the value - Redis streams store everything as strings + strVal, ok := v.(string) + if !ok { + logData[k] = v + continue + } + + // Try to parse special values + switch k { + case "loggedAt", "createdAt": + // Parse ISO timestamp + if t, err := time.Parse(time.RFC3339, strVal); err == nil { + logData[k] = t + } else if t, err := time.Parse(time.RFC3339Nano, strVal); err == nil { + logData[k] = t + } else { + logData[k] = strVal + } + case "oldValue", "newValue", "metadata": + // Try to parse as JSON + var jsonVal interface{} + if err := json.Unmarshal([]byte(strVal), &jsonVal); err == nil { + logData[k] = jsonVal + } else { + logData[k] = strVal + } + case "duration": + // Keep as string, will be normalized in DB layer + logData[k] = strVal + default: + logData[k] = strVal + } + } + + parsedLogs = append(parsedLogs, logData) + entryIDs = append(entryIDs, entry.ID) + } + + if len(parsedLogs) == 0 { + return 0, nil + } + + // Insert into database + count, err := insertFunc(ctx, parsedLogs) + if err != nil { + // On failure, push to DLQ stream + for _, logData := range parsedLogs { + logData["_error"] = err.Error() + logData["_originalStream"] = streamKey + if _, addErr := redisClient.XAdd(ctx, "logs:failed", logData); addErr != nil { + log.Error().Err(addErr).Msg("Failed to push to DLQ stream") + } + } + return 0, fmt.Errorf("insert: %w", err) + } + + // Delete processed entries from stream + if err := redisClient.XDel(ctx, streamKey, entryIDs...); err != nil { + log.Warn().Err(err).Str("stream", streamKey).Msg("Failed to delete processed entries") + } + + return count, nil +} + +// FlushLogs fetches logs from Redis List and inserts them into PostgreSQL (legacy) +func FlushLogs( + ctx context.Context, + redisClient RedisClient, + db interface{}, + redisKey string, + batchSize int, + insertFunc func(context.Context, []map[string]interface{}) (int, error), +) (int, error) { + // Get logs from Redis + logs, err := redisClient.LRange(ctx, redisKey, 0, int64(batchSize-1)) + if err != nil { + return 0, fmt.Errorf("lrange: %w", err) + } + + if len(logs) == 0 { + return 0, nil + } + + // Remove fetched logs from Redis + if err := redisClient.LTrim(ctx, redisKey, int64(len(logs)), -1); err != nil { + return 0, fmt.Errorf("ltrim: %w", err) + } + + // Parse logs + parsedLogs := make([]map[string]interface{}, 0, len(logs)) + for _, logStr := range logs { + decoded, err := base64.StdEncoding.DecodeString(logStr) + if err != nil { + log.Warn().Err(err).Str("key", redisKey).Msg("Failed to decode base64") + continue + } + + var logData map[string]interface{} + if err := json.Unmarshal(decoded, &logData); err != nil { + log.Warn().Err(err).Str("key", redisKey).Msg("Failed to unmarshal JSON") + continue + } + + parsedLogs = append(parsedLogs, logData) + } + + if len(parsedLogs) == 0 { + return 0, nil + } + + // Insert into database + count, err := insertFunc(ctx, parsedLogs) + if err != nil { + // On failure, push to DLQ + for _, logData := range parsedLogs { + jsonBytes, _ := json.Marshal(logData) + encoded := base64.StdEncoding.EncodeToString(jsonBytes) + if pushErr := redisClient.LPush(ctx, "logs:failed", encoded); pushErr != nil { + log.Error().Err(pushErr).Msg("Failed to push to DLQ") + } + } + return 0, fmt.Errorf("insert: %w", err) + } + + return count, nil +} diff --git a/internal/flush/flush_test.go b/internal/flush/flush_test.go new file mode 100644 index 0000000..dcf4dcd --- /dev/null +++ b/internal/flush/flush_test.go @@ -0,0 +1,123 @@ +package flush + +import ( + "context" + "encoding/base64" + "encoding/json" + "errors" + "testing" +) + +type mockRedisClient struct { + lrangeFunc func(ctx context.Context, key string, start, stop int64) ([]string, error) + ltrimFunc func(ctx context.Context, key string, start, stop int64) error + lpushFunc func(ctx context.Context, key string, values ...interface{}) error +} + +func (m *mockRedisClient) LRange(ctx context.Context, key string, start, stop int64) ([]string, error) { + return m.lrangeFunc(ctx, key, start, stop) +} + +func (m *mockRedisClient) LTrim(ctx context.Context, key string, start, stop int64) error { + return m.ltrimFunc(ctx, key, start, stop) +} + +func (m *mockRedisClient) LPush(ctx context.Context, key string, values ...interface{}) error { + return m.lpushFunc(ctx, key, values...) +} + +func TestFlushLogsSuccess(t *testing.T) { + ctx := context.Background() + logData := map[string]interface{}{"message": "test", "level": "info"} + jsonBytes, _ := json.Marshal(logData) + encoded := base64.StdEncoding.EncodeToString(jsonBytes) + + redis := &mockRedisClient{ + lrangeFunc: func(ctx context.Context, key string, start, stop int64) ([]string, error) { + return []string{encoded}, nil + }, + ltrimFunc: func(ctx context.Context, key string, start, stop int64) error { + return nil + }, + lpushFunc: func(ctx context.Context, key string, values ...interface{}) error { + return nil + }, + } + + insertFunc := func(ctx context.Context, logs []map[string]interface{}) (int, error) { + return len(logs), nil + } + + count, err := FlushLogs(ctx, redis, nil, "logs", 10, insertFunc) + if err != nil || count != 1 { + t.Errorf("expected count=1 and no error, got count=%d and err=%v", count, err) + } +} + +func TestFlushLogsEmptyRedis(t *testing.T) { + ctx := context.Background() + redis := &mockRedisClient{ + lrangeFunc: func(ctx context.Context, key string, start, stop int64) ([]string, error) { + return []string{}, nil + }, + ltrimFunc: func(ctx context.Context, key string, start, stop int64) error { + return nil + }, + lpushFunc: func(ctx context.Context, key string, values ...interface{}) error { + return nil + }, + } + + count, err := FlushLogs(ctx, redis, nil, "logs", 10, nil) + if err != nil || count != 0 { + t.Errorf("expected count=0 and no error, got count=%d and err=%v", count, err) + } +} + +func TestFlushLogsLRangeError(t *testing.T) { + ctx := context.Background() + redis := &mockRedisClient{ + lrangeFunc: func(ctx context.Context, key string, start, stop int64) ([]string, error) { + return nil, errors.New("redis error") + }, + ltrimFunc: func(ctx context.Context, key string, start, stop int64) error { + return nil + }, + lpushFunc: func(ctx context.Context, key string, values ...interface{}) error { + return nil + }, + } + + _, err := FlushLogs(ctx, redis, nil, "logs", 10, nil) + if err == nil { + t.Error("expected error from LRange, got nil") + } +} + +func TestFlushLogsInsertError(t *testing.T) { + ctx := context.Background() + logData := map[string]interface{}{"message": "test"} + jsonBytes, _ := json.Marshal(logData) + encoded := base64.StdEncoding.EncodeToString(jsonBytes) + + redis := &mockRedisClient{ + lrangeFunc: func(ctx context.Context, key string, start, stop int64) ([]string, error) { + return []string{encoded}, nil + }, + ltrimFunc: func(ctx context.Context, key string, start, stop int64) error { + return nil + }, + lpushFunc: func(ctx context.Context, key string, values ...interface{}) error { + return nil + }, + } + + insertFunc := func(ctx context.Context, logs []map[string]interface{}) (int, error) { + return 0, errors.New("insert failed") + } + + _, err := FlushLogs(ctx, redis, nil, "logs", 10, insertFunc) + if err == nil { + t.Error("expected error from insert, got nil") + } +} diff --git a/internal/models/models.go b/internal/models/models.go new file mode 100644 index 0000000..360e436 --- /dev/null +++ b/internal/models/models.go @@ -0,0 +1,42 @@ +package models + +import "time" + +// Event represents a Discord event log entry (matches Drizzle schema) +type Event struct { + ID string `json:"id,omitempty"` + GuildID string `json:"guildId"` + Action string `json:"action"` + ActionThreadID *string `json:"actionThreadId,omitempty"` + LoggedAt time.Time `json:"loggedAt"` + ActorID *string `json:"actorId,omitempty"` + ChannelID *string `json:"channelId,omitempty"` + TargetID *string `json:"targetId,omitempty"` + MessageID *string `json:"messageId,omitempty"` + Reason *string `json:"reason,omitempty"` + Duration *int `json:"duration,omitempty"` + OldValue interface{} `json:"oldValue,omitempty"` + NewValue interface{} `json:"newValue,omitempty"` + URL *string `json:"url,omitempty"` +} + +// GuardianLog represents a guardian action log (matches Drizzle schema) +type GuardianLog struct { + ID string `json:"id,omitempty"` + GuildID string `json:"guildId"` + Action string `json:"action"` + PerformedBy string `json:"performedBy"` + Target *string `json:"target,omitempty"` + Reason *string `json:"reason,omitempty"` + Metadata interface{} `json:"metadata,omitempty"` + CreatedAt time.Time `json:"createdAt"` +} + +// JoinLog represents a user join/leave log (matches Drizzle schema) +type JoinLog struct { + ID string `json:"id,omitempty"` + GuildID string `json:"guildId"` + UserID string `json:"userId"` + Action string `json:"action"` // 'join' | 'leave' + CreatedAt time.Time `json:"createdAt"` +} diff --git a/internal/models/models_test.go b/internal/models/models_test.go new file mode 100644 index 0000000..3286544 --- /dev/null +++ b/internal/models/models_test.go @@ -0,0 +1,92 @@ +package models + +import ( + "encoding/json" + "testing" + "time" +) + +func TestEventJSON(t *testing.T) { + now := time.Now() + actorID := "user123" + channelID := "channel123" + targetID := "target456" + messageID := "msg789" + + event := Event{ + GuildID: "guild123", + Action: "MessageDelete", + LoggedAt: now, + ActorID: &actorID, + ChannelID: &channelID, + TargetID: &targetID, + MessageID: &messageID, + } + + data, err := json.Marshal(event) + if err != nil { + t.Fatalf("Failed to marshal Event: %v", err) + } + + var event2 Event + if err := json.Unmarshal(data, &event2); err != nil { + t.Fatalf("Failed to unmarshal Event: %v", err) + } + + if event2.GuildID != event.GuildID || event2.Action != event.Action { + t.Errorf("Event mismatch after JSON round-trip") + } +} + +func TestGuardianLogJSON(t *testing.T) { + now := time.Now() + target := "user456" + reason := "spam" + + gl := GuardianLog{ + GuildID: "guild123", + Action: "kick", + PerformedBy: "mod123", + Target: &target, + Reason: &reason, + CreatedAt: now, + } + + data, err := json.Marshal(gl) + if err != nil { + t.Fatalf("Failed to marshal GuardianLog: %v", err) + } + + var gl2 GuardianLog + if err := json.Unmarshal(data, &gl2); err != nil { + t.Fatalf("Failed to unmarshal GuardianLog: %v", err) + } + + if gl2.Action != gl.Action || gl2.PerformedBy != gl.PerformedBy { + t.Errorf("GuardianLog mismatch after JSON round-trip") + } +} + +func TestJoinLogJSON(t *testing.T) { + now := time.Now() + jl := JoinLog{ + GuildID: "guild123", + UserID: "user123", + Action: "join", + CreatedAt: now, + } + + data, err := json.Marshal(jl) + if err != nil { + t.Fatalf("Failed to marshal JoinLog: %v", err) + } + + var jl2 JoinLog + if err := json.Unmarshal(data, &jl2); err != nil { + t.Fatalf("Failed to unmarshal JoinLog: %v", err) + } + + if jl2.UserID != jl.UserID || jl2.Action != jl.Action { + t.Errorf("JoinLog mismatch after JSON round-trip") + } +} diff --git a/internal/redis/redis.go b/internal/redis/redis.go new file mode 100644 index 0000000..2c4b9bf --- /dev/null +++ b/internal/redis/redis.go @@ -0,0 +1,120 @@ +package redis + +import ( + "context" + "fmt" + + "github.com/redis/go-redis/v9" +) + +type Client struct { + client *redis.Client +} + +// StreamEntry represents a Redis Stream entry +type StreamEntry struct { + ID string + Values map[string]interface{} +} + +// New creates a new Redis client +func New(redisURL string) (*Client, error) { + opts, err := redis.ParseURL(redisURL) + if err != nil { + return nil, fmt.Errorf("parse Redis URL: %w", err) + } + + client := redis.NewClient(opts) + + // Test connection + ctx := context.Background() + if err := client.Ping(ctx).Err(); err != nil { + return nil, fmt.Errorf("ping Redis: %w", err) + } + + return &Client{client: client}, nil +} + +// XLen returns the length of a stream +func (c *Client) XLen(ctx context.Context, stream string) (int64, error) { + return c.client.XLen(ctx, stream).Result() +} + +// XRange reads entries from a stream +func (c *Client) XRange(ctx context.Context, stream, start, stop string, count int64) ([]StreamEntry, error) { + result, err := c.client.XRangeN(ctx, stream, start, stop, count).Result() + if err != nil { + return nil, err + } + + entries := make([]StreamEntry, len(result)) + for i, msg := range result { + entries[i] = StreamEntry{ + ID: msg.ID, + Values: msg.Values, + } + } + return entries, nil +} + +// XDel deletes entries from a stream by ID +func (c *Client) XDel(ctx context.Context, stream string, ids ...string) error { + return c.client.XDel(ctx, stream, ids...).Err() +} + +// XAdd adds an entry to a stream +func (c *Client) XAdd(ctx context.Context, stream string, values map[string]interface{}) (string, error) { + return c.client.XAdd(ctx, &redis.XAddArgs{ + Stream: stream, + Values: values, + }).Result() +} + +// LLen returns the length of a list +func (c *Client) LLen(ctx context.Context, key string) (int64, error) { + return c.client.LLen(ctx, key).Result() +} + +// LRange returns a range of elements from a list +func (c *Client) LRange(ctx context.Context, key string, start, stop int64) ([]string, error) { + return c.client.LRange(ctx, key, start, stop).Result() +} + +// LTrim trims a list to the specified range +func (c *Client) LTrim(ctx context.Context, key string, start, stop int64) error { + return c.client.LTrim(ctx, key, start, stop).Err() +} + +// LPush pushes one or more values to the head of a list +func (c *Client) LPush(ctx context.Context, key string, values ...interface{}) error { + return c.client.LPush(ctx, key, values...).Err() +} + +// Close closes the Redis connection +func (c *Client) Close() error { + return c.client.Close() +} + +// Keys returns all keys matching a pattern +func (c *Client) Keys(ctx context.Context, pattern string) ([]string, error) { + return c.client.Keys(ctx, pattern).Result() +} + +// Scan iterates over keys matching a pattern (more efficient for large datasets) +func (c *Client) Scan(ctx context.Context, pattern string) ([]string, error) { + var keys []string + var cursor uint64 + for { + var batch []string + var err error + batch, cursor, err = c.client.Scan(ctx, cursor, pattern, 100).Result() + if err != nil { + return nil, err + } + keys = append(keys, batch...) + if cursor == 0 { + break + } + } + return keys, nil +} diff --git a/internal/tuning/tuning.go b/internal/tuning/tuning.go new file mode 100644 index 0000000..4dc0bdb --- /dev/null +++ b/internal/tuning/tuning.go @@ -0,0 +1,17 @@ +package tuning + +import "time" + +// AdjustTuning dynamically adjusts batch size and interval based on queue length +func AdjustTuning(queueLen int, baseBatch int, baseInterval time.Duration) (int, time.Duration) { + switch { + case queueLen > 100_000: + return baseBatch * 4, baseInterval / 2 + case queueLen > 10_000: + return baseBatch * 2, baseInterval / 2 + case queueLen > 1000: + return baseBatch, baseInterval + default: + return baseBatch / 2, baseInterval * 2 + } +} diff --git a/internal/tuning/tuning_test.go b/internal/tuning/tuning_test.go new file mode 100644 index 0000000..f3ec7da --- /dev/null +++ b/internal/tuning/tuning_test.go @@ -0,0 +1,60 @@ +package tuning + +import ( + "testing" + "time" +) + +func TestAdjustTuning(t *testing.T) { + tests := []struct { + name string + queueLen int + baseBatch int + baseInterval time.Duration + wantBatch int + wantInterval time.Duration + }{ + { + name: "high queue load", + queueLen: 150_000, + baseBatch: 10, + baseInterval: 100 * time.Millisecond, + wantBatch: 40, + wantInterval: 50 * time.Millisecond, + }, + { + name: "medium-high queue load", + queueLen: 50_000, + baseBatch: 10, + baseInterval: 100 * time.Millisecond, + wantBatch: 20, + wantInterval: 50 * time.Millisecond, + }, + { + name: "medium queue load", + queueLen: 5_000, + baseBatch: 10, + baseInterval: 100 * time.Millisecond, + wantBatch: 10, + wantInterval: 100 * time.Millisecond, + }, + { + name: "low queue load", + queueLen: 100, + baseBatch: 10, + baseInterval: 100 * time.Millisecond, + wantBatch: 5, + wantInterval: 200 * time.Millisecond, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotBatch, gotInterval := AdjustTuning(tt.queueLen, tt.baseBatch, tt.baseInterval) + if gotBatch != tt.wantBatch || gotInterval != tt.wantInterval { + t.Errorf("AdjustTuning(%d, %d, %v) = (%d, %v), want (%d, %v)", + tt.queueLen, tt.baseBatch, tt.baseInterval, gotBatch, gotInterval, tt.wantBatch, tt.wantInterval) + } + }) + } +} diff --git a/src/config.rs b/src/config.rs deleted file mode 100644 index c7c36a3..0000000 --- a/src/config.rs +++ /dev/null @@ -1,31 +0,0 @@ -use std::env; - -#[derive(Debug, Clone)] -pub struct Config { - pub redis_url: String, - pub mongo_url: String, - pub mongo_db: String, - pub flush_interval: u64, - pub batch_size: usize, -} - -impl Config { - pub fn from_env() -> Self { - dotenvy::dotenv().ok(); - - Self { - redis_url: env::var("REDIS_URL").unwrap_or_else(|_| "redis://127.0.0.1/".to_string()), - mongo_url: env::var("MONGO_URL") - .unwrap_or_else(|_| "mongodb://127.0.0.1:27017".to_string()), - mongo_db: env::var("MONGO_DB").unwrap_or_else(|_| "discord_logs".to_string()), - flush_interval: env::var("FLUSH_INTERVAL") - .unwrap_or_else(|_| "5".to_string()) - .parse() - .unwrap_or(5), - batch_size: env::var("BATCH_SIZE") - .unwrap_or_else(|_| "1000".to_string()) - .parse() - .unwrap_or(1000), - } - } -} diff --git a/src/dlq.rs b/src/dlq.rs deleted file mode 100644 index 3c2d8f1..0000000 --- a/src/dlq.rs +++ /dev/null @@ -1,69 +0,0 @@ -use anyhow::Result; -use base64::Engine; -use base64::engine::general_purpose::STANDARD as BASE64; -use bson::{Document, from_slice}; -use mongodb::Collection; -use redis::aio::MultiplexedConnection; -use tokio::time::{Duration, sleep}; -use tracing::{error, info}; - -pub async fn dlq_reprocessor( - mut redis_conn: MultiplexedConnection, - collection: Collection, - batch_size: usize, -) -> Result<()> { - loop { - let logs: Vec = redis::cmd("LRANGE") - .arg("logs:failed") - .arg(0) - .arg((batch_size - 1) as isize) - .query_async(&mut redis_conn) - .await?; - - if logs.is_empty() { - sleep(Duration::from_secs(5)).await; - continue; - } - - redis::cmd("LTRIM") - .arg("logs:failed") - .arg(logs.len()) - .arg(-1) - .query_async::<()>(&mut redis_conn) - .await?; - - let mut parsed_logs: Vec = Vec::new(); - - for s in logs { - if let Ok(bytes) = BASE64.decode(s) { - if let Ok(doc) = from_slice::(&bytes) { - parsed_logs.push(doc); - } - } - } - - if parsed_logs.is_empty() { - continue; - } - - match collection.insert_many(parsed_logs.clone()).await { - Ok(_) => { - info!(count = parsed_logs.len(), "✅ Reprocessed logs from DLQ"); - } - Err(e) => { - error!(error=?e, "❌ Failed to reprocess logs, pushing back to DLQ"); - for doc in parsed_logs { - if let Ok(bytes) = bson::to_vec(&doc) { - let base64 = BASE64.encode(&bytes); - let _ = redis::cmd("LPUSH") - .arg("logs:failed") - .arg(base64) - .query_async::<()>(&mut redis_conn) - .await; - } - } - } - } - } -} - diff --git a/src/flush.rs b/src/flush.rs deleted file mode 100644 index 43c3898..0000000 --- a/src/flush.rs +++ /dev/null @@ -1,64 +0,0 @@ -use base64::Engine; -use base64::engine::general_purpose::STANDARD as BASE64; -use bson::Document; -use mongodb::Collection; -use redis::aio::MultiplexedConnection; - -pub async fn flush_logs( - redis_conn: &mut MultiplexedConnection, - collection: &Collection, - redis_key: &str, - batch_size: usize, -) -> Result { - let logs: Vec = redis::cmd("LRANGE") - .arg(redis_key) - .arg(0) - .arg((batch_size - 1) as isize) - .query_async(redis_conn) - .await?; - - if logs.is_empty() { - return Ok(0); - } - - redis::cmd("LTRIM") - .arg(redis_key) - .arg(logs.len()) - .arg(-1) - .query_async::<()>(redis_conn) - .await?; - - let parsed_logs: Vec = logs - .into_iter() - .filter_map(|s| { - BASE64 - .decode(s) - .ok() - .and_then(|bytes| bson::from_slice::(&bytes).ok()) - }) - .collect(); - - if parsed_logs.is_empty() { - return Ok(0); - } - - let insert_result = collection.insert_many(parsed_logs.clone()).await; - - if let Err(e) = insert_result { - let mut dlq_conn = redis_conn.clone(); - for doc in parsed_logs { - if let Ok(bytes) = bson::to_vec(&doc) { - let base64 = BASE64.encode(&bytes); - let _ = redis::cmd("LPUSH") - .arg("logs:failed") - .arg(base64) - .query_async::<()>(&mut dlq_conn) - .await; - } - } - tracing::error!(error=?e, "❌ Failed to insert logs, sent to DLQ"); - return Err(anyhow::anyhow!(e)); - } - - Ok(parsed_logs.len()) -} diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index afeadb3..0000000 --- a/src/main.rs +++ /dev/null @@ -1,186 +0,0 @@ -mod config; -mod dlq; -mod flush; -mod models; -mod ttl_index; -mod tuning; - -use anyhow::Result; -use bson::Document; -use config::Config; -use flush::flush_logs; -use models::{EventLog, GuardianLog}; -use mongodb::{Client, Collection, options::ClientOptions}; -use redis::aio::MultiplexedConnection; -use tokio::time::Duration; -use tracing::{error, info}; - -use crate::{dlq::dlq_reprocessor, models::JoinLog, tuning::adjust_tuning}; - -#[tokio::main] -async fn main() -> Result<()> { - tracing_subscriber::fmt::init(); - - let cfg = Config::from_env(); - - // Redis - let redis_client = redis::Client::open(cfg.redis_url.clone())?; - let redis_conn: MultiplexedConnection = redis_client.get_multiplexed_async_connection().await?; - - // Mongo - let mongo_client = Client::with_options(ClientOptions::parse(&cfg.mongo_url).await?)?; - let db = mongo_client.database(&cfg.mongo_db); - - let events_collection: Collection = db.collection("events"); - let guardian_collection: Collection = db.collection("guardianLogs"); - let join_collection: Collection = db.collection("joinLogs"); - - // ttl indexing - ttl_index::ensure_ttl_indexes(&events_collection, &guardian_collection, &join_collection) - .await?; - - info!( - "🚀 Log worker started with batch={} interval={}s", - cfg.batch_size, cfg.flush_interval - ); - - let dlq_conn = redis_conn.clone(); - let dlq_collection = events_collection.clone(); // or a dedicated "failed" collection - tokio::spawn(async move { - if let Err(e) = dlq_reprocessor(dlq_conn, dlq_collection, cfg.batch_size).await { - error!("❌ DLQ reprocessor stopped: {:?}", e); - } - }); - - tokio::select! { - _ = worker_loop(redis_conn.clone(), events_collection.clone(), guardian_collection.clone(),join_collection.clone(), cfg.clone()) => {}, - _ = shutdown_signal(redis_conn.clone(), events_collection.clone(), guardian_collection.clone(),join_collection.clone(), cfg.clone()) => {}, - } - - Ok(()) -} - -async fn worker_loop( - redis_conn: MultiplexedConnection, - events_collection: mongodb::Collection, - guardian_collection: mongodb::Collection, - join_collection: mongodb::Collection, - cfg: Config, -) { - loop { - let mut conn1 = redis_conn.clone(); - let mut conn2 = redis_conn.clone(); - let mut conn3 = redis_conn.clone(); - let mut conn4 = redis_conn.clone(); - - let mut cmd_events = redis::cmd("LLEN"); - cmd_events.arg("logs:events"); - - let mut cmd_guardian = redis::cmd("LLEN"); - cmd_guardian.arg("logs:guardian"); - - let mut cmd_join = redis::cmd("LLEN"); - cmd_join.arg("logs:join"); - - let (events_len, guardian_len, join_len) = tokio::join!( - cmd_events.query_async::(&mut conn3), - cmd_guardian.query_async::(&mut conn4), - cmd_join.query_async::(&mut conn2) - ); - - let total_queue = - events_len.unwrap_or(0) + guardian_len.unwrap_or(0) + join_len.unwrap_or(0); - - let (batch_size, interval) = adjust_tuning(total_queue, cfg.batch_size, cfg.flush_interval); - - let (events_res, guardian_res, join_res) = tokio::join!( - flush_logs(&mut conn1, &events_collection, "logs:events", batch_size), - flush_logs( - &mut conn2, - &guardian_collection, - "logs:guardian", - batch_size - ), - flush_logs(&mut conn3, &join_collection, "logs:join", batch_size) - ); - - match events_res { - Ok(count) if count > 0 => { - tracing::info!(count, batch_size, interval, "✅ Flushed event logs") - } - Ok(_) => {} - Err(e) => tracing::error!(error=?e, "❌ Error flushing events"), - } - - match guardian_res { - Ok(count) if count > 0 => { - tracing::info!(count, batch_size, interval, "✅ Flushed guardian logs") - } - Ok(_) => {} - Err(e) => tracing::error!(error=?e, "❌ Error flushing guardian logs"), - } - - match join_res { - Ok(count) if count > 0 => { - tracing::info!(count, batch_size, interval, "✅ Flushed join logs") - } - Ok(_) => {} - Err(e) => tracing::error!(error=?e, "❌ Error flushing join logs"), - } - - tokio::time::sleep(Duration::from_secs(interval)).await; - } -} - -async fn shutdown_signal( - mut redis_conn: MultiplexedConnection, - events_collection: mongodb::Collection, - guardian_collection: mongodb::Collection, - join_collection: mongodb::Collection, - cfg: Config, -) { - tokio::signal::ctrl_c() - .await - .expect("Failed to listen for shutdown signal"); - info!("🛑 Shutdown signal received, flushing remaining logs..."); - - if let Ok(count) = flush_logs( - &mut redis_conn, - &events_collection, - "logs:events", - cfg.batch_size, - ) - .await - { - if count > 0 { - info!("Flushed {} event logs on shutdown", count); - } - } - if let Ok(count) = flush_logs( - &mut redis_conn, - &guardian_collection, - "logs:guardian", - cfg.batch_size, - ) - .await - { - if count > 0 { - info!("Flushed {} guardian logs on shutdown", count); - } - } - - if let Ok(count) = flush_logs( - &mut redis_conn, - &join_collection, - "logs:join", - cfg.batch_size, - ) - .await - { - if count > 0 { - info!("Flushed {} join logs on shutdown", count); - } - } - - info!("✅ Shutdown complete, exiting cleanly"); -} diff --git a/src/models.rs b/src/models.rs deleted file mode 100644 index e3fc644..0000000 --- a/src/models.rs +++ /dev/null @@ -1,4 +0,0 @@ -use bson::Document; -pub type GuardianLog = Document; -pub type EventLog = Document; -pub type JoinLog = Document; diff --git a/src/ttl_index.rs b/src/ttl_index.rs deleted file mode 100644 index 32da722..0000000 --- a/src/ttl_index.rs +++ /dev/null @@ -1,44 +0,0 @@ -use bson::doc; -use mongodb::IndexModel; -use mongodb::options::IndexOptions; - -use crate::models::{EventLog, GuardianLog, JoinLog}; - -pub async fn ensure_ttl_indexes( - events_collection: &mongodb::Collection, - guardian_collection: &mongodb::Collection, - join_collection: &mongodb::Collection, -) -> mongodb::error::Result<()> { - let events_index = IndexModel::builder() - .keys(doc! { "loggedAt": 1 }) - .options( - IndexOptions::builder() - .expire_after(Some(std::time::Duration::from_secs(60 * 60 * 24 * 14))) - .build(), - ) - .build(); - events_collection.create_index(events_index).await?; - - let guardian_index = IndexModel::builder() - .keys(doc! { "createdAt": 1 }) - .options( - IndexOptions::builder() - .expire_after(Some(std::time::Duration::from_secs(60 * 60 * 24 * 14))) - .build(), - ) - .build(); - guardian_collection.create_index(guardian_index).await?; - - let join_index = IndexModel::builder() - .keys(doc! { "createdAt": 1 }) - .options( - IndexOptions::builder() - .expire_after(Some(std::time::Duration::from_secs(60 * 60 * 24 * 3))) - .build(), - ) - .build(); - - join_collection.create_index(join_index).await?; - - Ok(()) -} diff --git a/src/tuning.rs b/src/tuning.rs deleted file mode 100644 index 8c347f7..0000000 --- a/src/tuning.rs +++ /dev/null @@ -1,11 +0,0 @@ -pub fn adjust_tuning(queue_len: usize, base_batch: usize, base_interval: u64) -> (usize, u64) { - if queue_len > 100_000 { - (base_batch * 4, base_interval / 2) - } else if queue_len > 10_000 { - (base_batch * 2, base_interval / 2) - } else if queue_len > 1000 { - (base_batch, base_interval) - } else { - (base_batch / 2, base_interval * 2) - } -}