diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d3aad5a --- /dev/null +++ b/.gitignore @@ -0,0 +1,35 @@ +# Docker volumes data +*data/ +*.volume + +# Rust build artifacts +/target/ +*/target/ +**/target/ +**/*.rs.bk + +# Logs +*.log +/logs/ + +# Environment variables +.env +.env.* +!.env.example + +# IDE - Common +.vscode/ +.idea/ +*.iml +.DS_Store + +# ClickHouse specific +preprocessed_configs/ +clickhouse-server.err.log +clickhouse-server.log + +# Temporary files +*~ +*.swp +*.swo +/tmp/ \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..cdee352 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,3012 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "Inflector" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" +dependencies = [ + "lazy_static", + "regex", +] + +[[package]] +name = "actix" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de7fa236829ba0841304542f7614c42b80fca007455315c45c785ccfa873a85b" +dependencies = [ + "actix-macros", + "actix-rt", + "actix_derive", + "bitflags", + "bytes", + "crossbeam-channel", + "futures-core", + "futures-sink", + "futures-task", + "futures-util", + "log", + "once_cell", + "parking_lot", + "pin-project-lite", + "smallvec", + "tokio", + "tokio-util", +] + +[[package]] +name = "actix-codec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" +dependencies = [ + "bitflags", + "bytes", + "futures-core", + "futures-sink", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "actix-http" +version = "3.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa882656b67966045e4152c634051e70346939fced7117d5f0b52146a7c74c9" +dependencies = [ + "actix-codec", + "actix-rt", + "actix-service", + "actix-utils", + "base64 0.22.1", + "bitflags", + "brotli", + "bytes", + "bytestring", + "derive_more", + "encoding_rs", + "flate2", + "foldhash", + "futures-core", + "h2", + "http 0.2.12", + "httparse", + "httpdate", + "itoa", + "language-tags", + "local-channel", + "mime", + "percent-encoding", + "pin-project-lite", + "rand 0.9.0", + "sha1", + "smallvec", + "tokio", + "tokio-util", + "tracing", + "zstd", +] + +[[package]] +name = "actix-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" +dependencies = [ + "quote", + "syn 2.0.100", +] + +[[package]] +name = "actix-router" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13d324164c51f63867b57e73ba5936ea151b8a41a1d23d1031eeb9f70d0236f8" +dependencies = [ + "bytestring", + "cfg-if", + "http 0.2.12", + "regex", + "regex-lite", + "serde", + "tracing", +] + +[[package]] +name = "actix-rt" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24eda4e2a6e042aa4e55ac438a2ae052d3b5da0ecf83d7411e1a368946925208" +dependencies = [ + "futures-core", + "tokio", +] + +[[package]] +name = "actix-server" +version = "2.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6398974fd4284f4768af07965701efbbb5fdc0616bff20cade1bb14b77675e24" +dependencies = [ + "actix-rt", + "actix-service", + "actix-utils", + "futures-core", + "futures-util", + "mio", + "socket2", + "tokio", + "tracing", +] + +[[package]] +name = "actix-service" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e46f36bf0e5af44bdc4bdb36fbbd421aa98c79a9bce724e1edeb3894e10dc7f" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "actix-utils" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8" +dependencies = [ + "local-waker", + "pin-project-lite", +] + +[[package]] +name = "actix-web" +version = "4.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2e3b15b3dc6c6ed996e4032389e9849d4ab002b1e92fbfe85b5f307d1479b4d" +dependencies = [ + "actix-codec", + "actix-http", + "actix-macros", + "actix-router", + "actix-rt", + "actix-server", + "actix-service", + "actix-utils", + "actix-web-codegen", + "bytes", + "bytestring", + "cfg-if", + "cookie", + "derive_more", + "encoding_rs", + "foldhash", + "futures-core", + "futures-util", + "impl-more", + "itoa", + "language-tags", + "log", + "mime", + "once_cell", + "pin-project-lite", + "regex", + "regex-lite", + "serde", + "serde_json", + "serde_urlencoded", + "smallvec", + "socket2", + "time", + "tracing", + "url", +] + +[[package]] +name = "actix-web-actors" +version = "4.3.1+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98c5300b38fd004fe7d2a964f9a90813fdbe8a81fed500587e78b1b71c6f980" +dependencies = [ + "actix", + "actix-codec", + "actix-http", + "actix-web", + "bytes", + "bytestring", + "futures-core", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "actix-web-codegen" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f591380e2e68490b5dfaf1dd1aa0ebe78d84ba7067078512b4ea6e4492d622b8" +dependencies = [ + "actix-router", + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "actix_derive" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6ac1e58cded18cb28ddc17143c4dea5345b3ad575e14f32f66e4054a56eb271" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + +[[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.97" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" + +[[package]] +name = "ascii_utils" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71938f30533e4d95a6d17aa530939da3842c2ab6f4f84b9dae68447e4129f74a" + +[[package]] +name = "async-channel" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" +dependencies = [ + "concurrent-queue", + "event-listener", + "futures-core", +] + +[[package]] +name = "async-graphql" +version = "4.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9ed522678d412d77effe47b3c82314ac36952a35e6e852093dd48287c421f80" +dependencies = [ + "async-graphql-derive", + "async-graphql-parser", + "async-graphql-value", + "async-stream", + "async-trait", + "base64 0.13.1", + "bytes", + "fast_chemail", + "fnv", + "futures-util", + "http 0.2.12", + "indexmap 1.9.3", + "mime", + "multer", + "num-traits", + "once_cell", + "pin-project-lite", + "regex", + "serde", + "serde_json", + "serde_urlencoded", + "static_assertions", + "tempfile", + "thiserror 1.0.69", +] + +[[package]] +name = "async-graphql-actix-web" +version = "4.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c23479439e8a3819a937b88f6e8ec8e2185abebdc46a9b6d726e92518bbf858f" +dependencies = [ + "actix", + "actix-http", + "actix-web", + "actix-web-actors", + "async-channel", + "async-graphql", + "futures-channel", + "futures-util", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "async-graphql-derive" +version = "4.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c121a894495d7d3fc3d4e15e0a9843e422e4d1d9e3c514d8062a1c94b35b005d" +dependencies = [ + "Inflector", + "async-graphql-parser", + "darling", + "proc-macro-crate 1.3.1", + "proc-macro2", + "quote", + "syn 1.0.109", + "thiserror 1.0.69", +] + +[[package]] +name = "async-graphql-parser" +version = "4.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b6c386f398145c6180206c1869c2279f5a3d45db5be4e0266148c6ac5c6ad68" +dependencies = [ + "async-graphql-value", + "pest", + "serde", + "serde_json", +] + +[[package]] +name = "async-graphql-value" +version = "4.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a941b499fead4a3fb5392cabf42446566d18c86313f69f2deab69560394d65f" +dependencies = [ + "bytes", + "indexmap 1.9.3", + "serde", + "serde_json", +] + +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "async-trait" +version = "0.1.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "axum" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de45108900e1f9b9242f7f2e254aa3e2c029c921c258fe9e6b4217eeebd54288" +dependencies = [ + "axum-core", + "bytes", + "form_urlencoded", + "futures-util", + "http 1.3.1", + "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 1.3.1", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "backtrace" +version = "0.3.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets", +] + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" + +[[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 = "brotli" +version = "7.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc97b8f16f944bba54f0433f07e30be199b6dc2bd25937444bbad560bcea29bd" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "4.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74fa05ad7d803d413eb8380983b092cbbaf9a85f151b871360e7b00cd7060b37" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bstr" +version = "1.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "531a9155a481e2ee699d4f98f43c0ca4ff8ee1bfd55c31e9e98fb29d2b176fe0" +dependencies = [ + "memchr", +] + +[[package]] +name = "bumpalo" +version = "3.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +dependencies = [ + "serde", +] + +[[package]] +name = "bytestring" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e465647ae23b2823b0753f50decb2d5a86d2bb2cac04788fafd1f80e45378e5f" +dependencies = [ + "bytes", +] + +[[package]] +name = "cc" +version = "1.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fcb57c740ae1daf453ae85f16e37396f672b039e00d9d866e07ddb24e328e3a" +dependencies = [ + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "cityhash-rs" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93a719913643003b84bd13022b4b7e703c09342cd03b679c4641c7d2e50dc34d" + +[[package]] +name = "clickhouse" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9894248c4c5a4402f76a56c273836a0c32547ec8a68166aedee7e01b7b8d102" +dependencies = [ + "bstr", + "bytes", + "cityhash-rs", + "clickhouse-derive", + "futures", + "futures-channel", + "http-body-util", + "hyper", + "hyper-util", + "lz4_flex", + "replace_with", + "sealed", + "serde", + "static_assertions", + "thiserror 1.0.69", + "tokio", + "url", + "uuid", +] + +[[package]] +name = "clickhouse-derive" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d70f3e2893f7d3e017eeacdc9a708fbc29a10488e3ebca21f9df6a5d2b616dbb" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.100", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "cookie" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + +[[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 = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "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.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 1.0.109", +] + +[[package]] +name = "darling_macro" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" +dependencies = [ + "darling_core", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "deranged" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28cfac68e08048ae1883171632c2aef3ebc555621ae56fbccce1cbf22dd7f058" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "derive_more" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", + "unicode-xid", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[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.100", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "fast_chemail" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "495a39d30d624c2caabe6312bfead73e7717692b44e0b32df168c275a2e8e9e4" +dependencies = [ + "ascii_utils", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + +[[package]] +name = "flate2" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11faaf5a5236997af9848be0bef4db95824b1d534ebc64d0f0c6cf3e67bd38dc" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[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 = "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.100", +] + +[[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.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "h2" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.12", + "indexmap 2.8.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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[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 1.3.1", +] + +[[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 1.3.1", + "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", + "http 1.3.1", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497bbc33a26fdd4af9ed9c70d63f61cf56a938375fbb32df34db9b1cd6d643f2" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http 1.3.1", + "http-body", + "hyper", + "libc", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.62" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2fd658b06e56721792c5df4475705b6cda790e9298d19d2f8af083457bcd127" +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 = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "impl-more" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2" + +[[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.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058" +dependencies = [ + "equivalent", + "hashbrown 0.15.2", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "jobserver" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +dependencies = [ + "libc", +] + +[[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 = "language-tags" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" + +[[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.171" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" + +[[package]] +name = "libz-sys" +version = "1.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b70e7a7df205e92a1a4cd9aaae7898dac0aa555503cc0a649494d0d60e7651d" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe7db12097d22ec582439daf8618b8fdd1a7bef6270e9af3b1ebcd30893cf413" + +[[package]] +name = "litemap" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" + +[[package]] +name = "local-channel" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6cbc85e69b8df4b8bb8b89ec634e7189099cea8927a276b7384ce5488e53ec8" +dependencies = [ + "futures-core", + "futures-sink", + "local-waker", +] + +[[package]] +name = "local-waker" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d873d7c67ce09b42110d801813efbc9364414e356be9935700d368351657487" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "lz4_flex" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75761162ae2b0e580d7e7c390558127e5f01b4194debd6221fd8c207fc80e3f5" + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[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.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +dependencies = [ + "libc", + "log", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.52.0", +] + +[[package]] +name = "multer" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01acbdc23469fd8fe07ab135923371d5f5a422fbf9c522158677c8eb15bc51c2" +dependencies = [ + "bytes", + "encoding_rs", + "futures-util", + "http 0.2.12", + "httparse", + "log", + "memchr", + "mime", + "spin", + "version_check", +] + +[[package]] +name = "multimap" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "defc4c55412d89136f966bbb339008b474350e5e6e78d2714439c386b3137a03" + +[[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-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "num_enum" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179" +dependencies = [ + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" +dependencies = [ + "proc-macro-crate 3.3.0", + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d75b0bedcc4fe52caa0e03d9f1151a323e4aa5e2d78ba3580400cd3c9e2bc4bc" + +[[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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pest" +version = "2.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b7cafe60d6cf8e62e1b9b2ea516a089c008945bb5a275416789e7db0bc199dc" +dependencies = [ + "memchr", + "thiserror 2.0.12", + "ucd-trie", +] + +[[package]] +name = "petgraph" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" +dependencies = [ + "fixedbitset", + "indexmap 2.8.0", +] + +[[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 = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[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 = "prettyplease" +version = "0.2.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5316f57387668042f561aae71480de936257848f9c43ce528e311d89a07cadeb" +dependencies = [ + "proc-macro2", + "syn 2.0.100", +] + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" +dependencies = [ + "toml_edit 0.22.24", +] + +[[package]] +name = "proc-macro2" +version = "1.0.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "prost" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-build" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf" +dependencies = [ + "heck", + "itertools", + "log", + "multimap", + "once_cell", + "petgraph", + "prettyplease", + "prost", + "prost-types", + "regex", + "syn 2.0.100", + "tempfile", +] + +[[package]] +name = "prost-derive" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "prost-types" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16" +dependencies = [ + "prost", +] + +[[package]] +name = "qos_graphql_api" +version = "0.1.0" +dependencies = [ + "actix-web", + "anyhow", + "async-graphql", + "async-graphql-actix-web", + "axum", + "bs58", + "chrono", + "clickhouse", + "futures", + "hex", + "once_cell", + "regex", + "serde", + "serde_json", + "tokio", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "qos_kafka_producer" +version = "0.1.0" +dependencies = [ + "chrono", + "hex", + "num_cpus", + "once_cell", + "prost", + "prost-build", + "rand 0.8.5", + "rdkafka", + "tokio", + "uuid", +] + +[[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.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", + "zerocopy", +] + +[[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.15", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.2", +] + +[[package]] +name = "rdkafka" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1de127f294f2dba488ed46760b129d5ecbeabbd337ccbf3739cb29d50db2161c" +dependencies = [ + "futures", + "libc", + "log", + "rdkafka-sys", + "serde", + "serde_derive", + "serde_json", + "slab", + "tokio", +] + +[[package]] +name = "rdkafka-sys" +version = "4.8.0+2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced38182dc436b3d9df0c77976f37a67134df26b050df1f0006688e46fc4c8be" +dependencies = [ + "libc", + "libz-sys", + "num_enum", + "pkg-config", +] + +[[package]] +name = "redox_syscall" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata 0.4.9", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-lite" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a" + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "replace_with" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a8614ee435691de62bcffcf4a66d91b3594bf1428a5722e79103249a095690" + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustix" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e56a18552996ac8d29ecc3b190b4fdbb2d91ca4ec396de7bbffaf43f3d637e96" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustversion" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sealed" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22f968c5ea23d555e670b449c1c5e7b2fc399fdaec1d304a17cd48e288abc107" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[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_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "serde_json" +version = "1.0.140" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +dependencies = [ + "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 = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" + +[[package]] +name = "socket2" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[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.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" +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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "tempfile" +version = "3.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" +dependencies = [ + "fastrand", + "getrandom 0.3.2", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl 2.0.12", +] + +[[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.100", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[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 = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +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.44.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f382da615b842244d4b8738c82ed1275e6c5dd90c459a30941cd07080b06c91a" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.52.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.100", +] + +[[package]] +name = "tokio-util" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b9590b93e6fcc1739458317cccd391ad3955e2bde8913edf6f95f9e65a8f034" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.8.0", + "toml_datetime", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.22.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" +dependencies = [ + "indexmap 2.8.0", + "toml_datetime", + "winnow 0.7.4", +] + +[[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.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "tracing-core" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +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-serde" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" +dependencies = [ + "serde", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "serde", + "serde_json", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", + "tracing-serde", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[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 = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[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.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9" +dependencies = [ + "getrandom 0.3.2", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[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.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[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.100", + "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.100", + "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 = "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.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-link" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[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.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[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.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[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.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e97b544156e9bebe1a0ffbc03484fc1ffe3100cbce3ffb17eac35f7cdd7ab36" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags", +] + +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + +[[package]] +name = "yoke" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2586fea28e186957ef732a5f8b3be2da217d65c5969d4b1e17f973ebbe876879" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a996a8f63c5c4448cd959ac1bab0aaa3306ccfd060472f85943ee0750f0169be" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[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.100", + "synstructure", +] + +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.15+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..08da581 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,7 @@ +[workspace] +members = [ + "oracle/test-producer", + "oracle/graphql-api", +] + +resolver = "2" diff --git a/README.md b/README.md index ed33bd9..baf1bc3 100644 --- a/README.md +++ b/README.md @@ -1 +1,348 @@ -# qos-oracle-v2 \ No newline at end of file +# QoS Oracle V2 + +## Overview + +QoS Oracle V2 is a data pipeline and reporting system for The Graph Network quality metrics. It collects, processes, and exposes quality-of-service data from Gateway nodes, providing valuable insights into indexer performance and network health. + +## Architecture & Data Flow + +The system processes data in the following sequence: + +1. **Gateway:** External Gateway nodes (deployed separately) produce Protobuf-encoded QoS messages containing details about client queries and indexer interactions. +2. **Redpanda/Kafka (`gateway_qos_topic`):** These messages are published to a central Kafka topic, typically named `gateway_qos_topic`. In production, this is usually an existing, managed Kafka cluster (like Redpanda). +3. **ClickHouse (`kafka_qos_data` -> MVs -> Aggregates):** The ClickHouse server consumes messages from the `gateway_qos_topic` via a Kafka Engine table (`kafka_qos_data`). Materialized Views (MVs) trigger automatically to process these raw messages, calculating aggregates and storing them efficiently in AggregatingMergeTree tables (e.g., `agg_deployment_5min`, `agg_indexer_5min`, etc.) at a 5-minute granularity. +4. **GraphQL API:** The GraphQL API service queries the aggregated tables in ClickHouse (using the final Views like `deployment_aggregations_hourly`) to serve requests for QoS metrics over different time intervals (5min, hourly, daily). +5. **Monitoring (Prometheus & Grafana):** + * Prometheus (typically an existing instance) scrapes metrics directly from the ClickHouse server's `/metrics` endpoint (port 9363 by default). + * Grafana (typically an existing instance) queries Prometheus for ClickHouse operational metrics and can also query the ClickHouse aggregated tables directly for QoS metric dashboards. + +```mermaid +graph LR + A[Gateway] -- Protobuf Messages --> B(Redpanda/Kafka Topic: gateway_qos_topic); + B -- Consumed by --> C{ClickHouse Server}; + C -- Kafka Engine Table --> D[MV: mv_agg_..._5min]; + D -- Aggregates & Inserts --> E[AggregatingMergeTree: agg_..._5min]; + F[GraphQL API] -- Queries --> E; + G[Prometheus] -- Scrapes /metrics --> C; + H[Grafana] -- Queries --> G; + H -- Queries --> E; + + subgraph "QoS Oracle V2 Components (To Deploy)" + C + F + end + + subgraph "External/Existing Infrastructure" + A + B + G + H + end +``` + +## Features + +- High-throughput data ingestion (via Kafka/Redpanda) +- Efficient storage with automatic TTL for raw data (managed by ClickHouse) +- Pre-aggregated metrics in 5-minute, hourly, and daily intervals +- Flexible GraphQL API for querying aggregated data with filtering, sorting, and time range selection +- Docker-based deployment for easy local development setup +- Configurable via environment variables for production deployments + +## Getting Started (Local Development) + +This section describes how to run the *entire* stack locally using Docker Compose, including dependencies like Redpanda and a test data producer. **This is intended for development and testing purposes only.** For production deployment guidance, see the "Production Deployment (Kubernetes/Helm)" section. + +### Prerequisites + +- Docker and Docker Compose (v2 recommended) +- Git +- 4+ CPU cores recommended +- 16+ GB RAM recommended +- 250+ GB SSD storage recommended + +### Installation + +1. Clone the repository: + ```bash + git clone https://github.com/graphops/qos-oracle.git # Or your repo URL + cd qos-oracle + ``` + +2. Start the services (Development profile includes API, DB, Kafka, and Test Producer): + ```bash + # Ensure you are in the root qos-oracle directory + docker compose -f oracle/docker-compose.yml --profile dev up --build -d + ``` + * Use `--profile deps` to start only ClickHouse and Redpanda. + * Use `--profile all` to start everything including monitoring tools. + +3. Verify the installation: + ```bash + # Check API health + curl http://localhost:8000/health + # Check running containers + docker compose -f oracle/docker-compose.yml ps + ``` + +4. Access the GraphQL endpoint at `http://localhost:8000/graphql`. + +## Production Deployment (Kubernetes/Helm) + +This section provides guidance for deploying the QoS Oracle V2 components into a production Kubernetes environment, likely managed via Helm, assuming existing infrastructure for Kafka, Prometheus, and Grafana. + +### Components to Deploy + +From the `oracle/docker-compose.yml` definition, you will need to create Kubernetes resources (Deployments, Services, ConfigMaps, etc.) for: + +1. **`clickhouse-server`:** The core data processing and storage engine. +2. **`graphql-api`:** The service exposing the aggregated data. + +### Components NOT to Deploy (Assuming Existing Infrastructure) + +You will likely **not** deploy the following components if they already exist in your cluster: + +* `redpanda`: Use your existing Kafka/Redpanda cluster. +* `test-producer`: This is only for development; real data comes from Gateways. +* `prometheus`: Integrate with your existing Prometheus instance. +* `grafana`: Integrate with your existing Grafana instance. +* `redpanda-console`: Use existing tools or deploy if needed separately. + +### Configuration (Environment Variables) + +Configure the deployable components using environment variables, typically managed via Helm `values.yaml` or Kubernetes manifests. + +**1. `clickhouse-server` Configuration:** + +* **Image:** `clickhouse/clickhouse-server:25.3` (or the version specified in `oracle/docker-compose.yml`) +* **Initialization:** The standard ClickHouse Docker image executes `.sh`, `.sql`, and `.sql.gz` files found in `/docker-entrypoint-initdb.d/` upon initial startup. For production: + * You need to provide the necessary SQL schema definitions (defined in `oracle/clickhouse/tables.sql`) to create the `kafka_qos_data` table, Materialized Views (e.g., `mv_agg_deployment_5min`), and AggregatingMergeTree tables (e.g., `agg_deployment_5min`). + * Mount these SQL definition files into the `/docker-entrypoint-initdb.d/` directory within the container. This is typically done using a Kubernetes ConfigMap mounted as volume(s). + * **Crucially:** The `CREATE TABLE kafka_qos_data` statement within your mounted SQL file(s) must use the correct production values for `kafka_broker_list`, `kafka_topic_list`, `kafka_group_name`, and `kafka_schema`. You will likely need to template these values into the ConfigMap using Helm or a similar tool, rather than hardcoding them. +* **Volume Mounts:** + * Mount your SQL initialization file(s) (e.g., from a ConfigMap) to `/docker-entrypoint-initdb.d/`. (This is the tables.sql file) + * Mount `oracle/clickhouse/schema.proto` to `/var/lib/clickhouse/user_files/schema.proto`. (The `kafka_schema` setting in your SQL should reference the relative path: `'schema.proto:qos.ClientQueryProtobuf'`). + * Mount necessary ClickHouse config XMLs (`settings.xml`, `users.xml`) to `/etc/clickhouse-server/config.d/` and `/etc/clickhouse-server/users.d/` respectively. Ensure `users.xml` defines the user/password needed by the GraphQL API. +* **Ports:** + * `8123` (HTTP): For queries, health checks. + * `9000` (Native): For GraphQL API connection. + * `9363` (Metrics): For Prometheus scraping. +* **Persistence:** Configure persistent storage for `/var/lib/clickhouse`. + +**2. `graphql-api` Configuration:** + +* **Image:** Build the image using the Dockerfile at `oracle/graphql-api/Dockerfile`. +* **Required Environment Variables:** + * `CLICKHOUSE_URL`: The endpoint for connecting to the ClickHouse native protocol (Ensure user/pass match ClickHouse config). +* **Ports:** + * `8000` (API): Or the value set in the `PORT` env var. + +### Monitoring Integration + +**1. Prometheus Scrape Configuration:** + +Add the following job to your Prometheus configuration to scrape metrics from the deployed ClickHouse service: + +``` +scrape_configs: + - job_name: 'clickhouse-qos-oracle' + metrics_path: /metrics + static_configs: + - targets: [':9363'] # Replace with your K8s service name and metrics port + # Optional: Add labels specific to your environment + # relabel_configs: + # - source_labels: [__address__] + # target_label: instance + # replacement: '' # e.g., qos-oracle-clickhouse-prod +``` + +**2. Grafana Dashboard:** + +* The Grafana dashboard definition used in development is located in `oracle/grafana/dashboards/`. +* Export this dashboard (if running locally) or retrieve the JSON file from the repository. +* Import this JSON file into your production Grafana instance via the UI ("+" -> "Import"). +* **Important:** After importing, you may need to edit the dashboard's settings to ensure the "ClickHouse" data source points to your production ClickHouse instance (if the data source name differs from the one used in development) and the "Prometheus" data source points to your production Prometheus. + +## Usage + +### GraphQL API + +The GraphQL API provides access to aggregated QoS metrics via three main queries: `deploymentAggregations`, `indexerAggregations`, and `allocationAggregations`. + +**Endpoint:** `http://:/graphql` (Use the Kubernetes service endpoint in production) + +**Key Arguments:** + +- `interval: AggregationInterval` (Optional: `FiveMinutes`, `Hourly`, `Daily`. Default: `Hourly`) +- `timeRange: TimeRangeInput` (Optional: Defaults to last 24h. Requires `from` or `to` if provided) + * `from: String` (Optional: RFC3339 or Unix timestamp string, inclusive) + * `to: String` (Optional: RFC3339 or Unix timestamp string, exclusive) +- `filter: AggregationFilterInput` (Optional: Filter by IDs) + * `gatewayId: String` + * `subgraph: String` (Deployment ID) + * `indexer: String` (Hex Address) + * `allocation: String` (Hex Address) +- `limit: Int` (Optional: Default 1000, Max 10000) +- `sort: [QuerySpecific]SortInput` (Optional: Sort by field and direction) + +**Example Queries:** + +1. **Get Hourly Deployment Aggregations (Default - Last 24 hours):** + + * **cURL:** + ```bash + curl -X POST http://localhost:8000/graphql \ + -H "Content-Type: application/json" \ + -d '{ + "query": "{ deploymentAggregations { timeBucket subgraph gatewayId queryCount avgResponseTimeMs } }" + }' + ``` + * **GraphQL:** + ```graphql + { + deploymentAggregations { + timeBucket + subgraph + gatewayId + queryCount + avgResponseTimeMs + } + } + ``` + +2. **Get Daily Indexer Aggregations for the Last 7 Days:** + + * **cURL:** + ```bash + curl -X POST http://localhost:8000/graphql \ + -H "Content-Type: application/json" \ + -d '{ + "query": "{ indexerAggregations( interval: Daily, timeRange: { from: \"$(date -v-7d +%s)\" } ) { timeBucket indexer gatewayId queryCount avgFeeGrt avgSecondsBehind } }" + }' + ``` + * **GraphQL:** + ```graphql + { + indexerAggregations( + interval: Daily, + timeRange: { from: "PAST_7_DAYS_TIMESTAMP" } # Replace with actual timestamp + ) { + timeBucket + indexer + gatewayId + queryCount + avgFeeGrt + avgSecondsBehind + } + } + ``` + +3. **Get Top 50 Allocations by Query Count (5-Minute Interval, Since Specific Time):** + + * **cURL:** + ```bash + curl -X POST http://localhost:8000/graphql \ + -H "Content-Type: application/json" \ + -d '{ + "query": "{ allocationAggregations( interval: FiveMinutes, timeRange: { from: \"1698300000\" }, sort: { field: QueryCount, direction: Desc }, limit: 50 ) { timeBucket subgraph indexer gatewayId queryCount avgSecondsBehind } }" + }' + ``` + * **GraphQL:** + ```graphql + { + allocationAggregations( + interval: FiveMinutes, + timeRange: { from: "1698300000" }, + sort: { field: QueryCount, direction: Desc }, + limit: 50 + ) { + timeBucket + subgraph + indexer + gatewayId + queryCount + avgSecondsBehind + } + } + ``` + +4. **Get Hourly Deployment Aggregations Up To a Specific Time for a Gateway:** + + * **cURL:** + ```bash + curl -X POST http://localhost:8000/graphql \ + -H "Content-Type: application/json" \ + -d '{ + "query": "query GatewayUntil($gwId: String!) { deploymentAggregations( timeRange: { to: \"2023-10-27T12:00:00Z\" }, filter: { gatewayId: $gwId } ) { timeBucket subgraph gatewayId queryCount avgResponseTimeMs } }", + "variables": { "gwId": "gateway-mainnet-1" } + }' + ``` + * **GraphQL (with variables):** + ```graphql + query GatewayUntil($gwId: String!) { + deploymentAggregations( + timeRange: { to: "2023-10-27T12:00:00Z" }, + filter: { gatewayId: $gwId } + ) { + timeBucket + subgraph + gatewayId + queryCount + avgResponseTimeMs + } + } + ``` + *Variables:* + ```json + { + "gwId": "gateway-mainnet-1" + } + ``` + +## Development + +This section describes the local development environment setup. + +### Project Structure + +- `oracle/clickhouse/` - ClickHouse configuration, schema (`schema.proto`), init template (`init-db.sql.template`), and entrypoint (`entrypoint.sh`). +- `oracle/graphql-api/` - GraphQL API service (Rust) +- `oracle/test-producer/` - **Development only:** Test data generator (Rust) +- `oracle/docker-compose.yml` - **Development only:** Docker Compose configuration +- `oracle/prometheus/` - **Development only:** Prometheus configuration +- `oracle/grafana/` - **Development only:** Grafana provisioning and dashboard definitions + +### Running in Development Mode + +``` +# From the root qos-oracle directory +docker compose -f oracle/docker-compose.yml --profile dev up +``` + +*(Add `-d` to run detached)* + +This will start the core services (`clickhouse-server`, `graphql-api`), dependencies (`redpanda`), and the `test-producer` using the configuration in `oracle/docker-compose.yml`. + +### Local Monitoring (Development Only) + +When running with `--profile all` or `--profile deps`: + +- **GraphQL API Health:** `http://localhost:8000/health` +- **GraphQL API Endpoint:** `http://localhost:8000/graphql` +- **ClickHouse HTTP Interface:** `http://localhost:8123` (Credentials: `graphql`/`graphql_password` by default, see `oracle/docker-compose.yml` `x-clickhouse-env`) +- **Redpanda Kafka API:** `localhost:9092` +- **Prometheus UI:** `http://localhost:9090` +- **Grafana UI:** `http://localhost:3000` +- **Redpanda Console:** `http://localhost:8080` + +## License + +[Add license information here] + +## Contributing + +[Add contribution guidelines here] + +## Acknowledgements + +This project is maintained by [GraphOps](https://graphops.xyz) and is part of The Graph Network ecosystem. diff --git a/oracle/README.md b/oracle/README.md deleted file mode 100644 index f80149b..0000000 --- a/oracle/README.md +++ /dev/null @@ -1 +0,0 @@ -# Qos Oracle V2 \ No newline at end of file diff --git a/oracle/clickhouse/allow_schema_path.xml b/oracle/clickhouse/allow_schema_path.xml new file mode 100644 index 0000000..daf5aa6 --- /dev/null +++ b/oracle/clickhouse/allow_schema_path.xml @@ -0,0 +1,11 @@ + + /var/lib/clickhouse/ + /var/lib/clickhouse/tmp/ + /var/lib/clickhouse/user_files/ + /etc/clickhouse-server/ + + + 0 + + + \ No newline at end of file diff --git a/oracle/clickhouse/schema.proto b/oracle/clickhouse/schema.proto new file mode 100644 index 0000000..d3451cc --- /dev/null +++ b/oracle/clickhouse/schema.proto @@ -0,0 +1,32 @@ +syntax = "proto3"; + +package qos; + +message ClientQueryProtobuf { + string gateway_id = 1; + bytes receipt_signer = 2; + string query_id = 3; + string api_key = 4; + string result = 5; + uint32 response_time_ms = 6; + uint32 request_bytes = 7; + optional uint32 response_bytes = 8; + double total_fees_usd = 9; + repeated IndexerQueryProtobuf indexer_queries = 10; + string user_id = 11; + optional string subgraph = 12; +} + +message IndexerQueryProtobuf { + bytes indexer = 1; + bytes deployment = 2; + bytes allocation = 3; + string indexed_chain = 4; + string url = 5; + double fee_grt = 6; + uint32 response_time_ms = 7; + uint32 seconds_behind = 8; + string result = 9; + string indexer_errors = 10; + uint64 blocks_behind = 11; +} diff --git a/oracle/clickhouse/settings.xml b/oracle/clickhouse/settings.xml new file mode 100644 index 0000000..3d968cb --- /dev/null +++ b/oracle/clickhouse/settings.xml @@ -0,0 +1,27 @@ + + 0.0.0.0 + 8123 + 9000 + + /metrics + 9363 + true + true + true + true + + + warning + true + + + + + + + + + + + + diff --git a/oracle/clickhouse/tables.sql b/oracle/clickhouse/tables.sql new file mode 100644 index 0000000..b47f4e3 --- /dev/null +++ b/oracle/clickhouse/tables.sql @@ -0,0 +1,785 @@ +-- ============================================================ +-- RAW DATA INGESTION (Kafka -> qos_data) +-- ============================================================ + +-- Kafka Engine Table (Queue) - Remains the same +CREATE TABLE IF NOT EXISTS kafka_qos_data +( + gateway_id String, + receipt_signer String, + query_id String, + api_key String, + user_id String, + subgraph Nullable(String), + result String, + response_time_ms UInt32, + request_bytes UInt32, + response_bytes UInt32, + total_fees_usd Float64, + indexer_queries Nested( + indexer String, + deployment String, + allocation String, + indexed_chain String, + url String, + fee_grt Float64, + response_time_ms UInt32, + seconds_behind UInt32, + result String, + indexer_errors String, + blocks_behind UInt64 + ) +) ENGINE = Kafka +SETTINGS + kafka_broker_list = 'redpanda:9092', + kafka_topic_list = 'gateway_qos_topic', + kafka_group_name = 'clickhouse_qos_consumer', + kafka_format = 'ProtobufSingle', + kafka_schema = 'schema.proto:qos.ClientQueryProtobuf', + kafka_thread_per_consumer = 1, + kafka_flush_interval_ms = 2500; + +-- ============================================================ +-- 5-MINUTE LEVEL AGGREGATION TABLES & MATERIALIZED VIEWS +-- ============================================================ +-- We store aggregate state at the 5-minute level. +-- Coarser granularities (hourly, daily) are calculated +-- on the fly in the final views using -Merge functions. +-- Materialized Views read directly from kafka_qos_data. +-- ============================================================ + +-- ---------------------------------------- +-- Deployment Level Aggregations (5-Minute State) +-- ---------------------------------------- +-- Renamed table from agg_deployment_1min to agg_deployment_5min +CREATE TABLE IF NOT EXISTS agg_deployment_5min +( + time_bucket DateTime, -- 5-Minute level bucket + subgraph String, + gateway_id String, + -- Aggregate state columns + query_count AggregateFunction(count), + success_count AggregateFunction(countIf, UInt8), + failure_count AggregateFunction(countIf, UInt8), + total_fees_usd AggregateFunction(sum, Float64), + avg_response_time_ms AggregateFunction(avg, UInt32), + max_response_time_ms AggregateFunction(max, UInt32), + quantiles_response_time_ms AggregateFunction(quantiles(0.90, 0.99), UInt32), + stddev_response_time_ms AggregateFunction(stddevSamp, UInt32), + avg_fee_usd AggregateFunction(avg, Float64), + max_fee_usd AggregateFunction(max, Float64), + quantiles_fee_usd AggregateFunction(quantiles(0.90, 0.99), Float64), + stddev_fee_usd AggregateFunction(stddevSamp, Float64) +) +ENGINE = AggregatingMergeTree +PARTITION BY toYYYYMM(time_bucket) +ORDER BY (time_bucket, subgraph, gateway_id); + +-- Renamed MV from mv_agg_deployment_1min to mv_agg_deployment_5min +-- Changed TO clause to point to agg_deployment_5min +-- Changed time bucketing to toStartOfFiveMinute +CREATE MATERIALIZED VIEW IF NOT EXISTS mv_agg_deployment_5min TO agg_deployment_5min AS +SELECT + toStartOfFiveMinute(now()) AS time_bucket, -- Use 5-minute bucket + kq.subgraph, + kq.gateway_id, + -- Calculate aggregate states directly from Kafka data + countState() AS query_count, + countIfState(kq.result = 'success') AS success_count, + countIfState(kq.result != 'success') AS failure_count, + sumState(kq.total_fees_usd) AS total_fees_usd, + avgState(kq.response_time_ms) AS avg_response_time_ms, + maxState(kq.response_time_ms) AS max_response_time_ms, + quantilesState(0.90, 0.99)(kq.response_time_ms) AS quantiles_response_time_ms, + stddevSampState(kq.response_time_ms) AS stddev_response_time_ms, + avgState(kq.total_fees_usd) AS avg_fee_usd, + maxState(kq.total_fees_usd) AS max_fee_usd, + quantilesState(0.90, 0.99)(kq.total_fees_usd) AS quantiles_fee_usd, + stddevSampState(kq.total_fees_usd) AS stddev_fee_usd +FROM kafka_qos_data AS kq -- Read directly from Kafka table +WHERE kq.subgraph IS NOT NULL +GROUP BY time_bucket, kq.subgraph, kq.gateway_id; + + +-- ---------------------------------------- +-- Allocation Level Aggregations (5-Minute State) +-- ---------------------------------------- +-- Renamed table from agg_allocation_1min to agg_allocation_5min +CREATE TABLE IF NOT EXISTS agg_allocation_5min +( + time_bucket DateTime, -- 5-Minute level bucket + subgraph String, + indexer String, -- HEX encoded + gateway_id String, + -- Aggregate state columns + query_count AggregateFunction(count), + success_count AggregateFunction(countIf, UInt8), + failure_count AggregateFunction(countIf, UInt8), + total_fee_grt AggregateFunction(sum, Float64), + avg_indexer_response_time_ms AggregateFunction(avg, UInt32), + max_indexer_response_time_ms AggregateFunction(max, UInt32), + quantiles_indexer_response_time_ms AggregateFunction(quantiles(0.90, 0.99), UInt32), + stddev_indexer_response_time_ms AggregateFunction(stddevSamp, UInt32), + avg_fee_grt AggregateFunction(avg, Float64), + max_fee_grt AggregateFunction(max, Float64), + quantiles_fee_grt AggregateFunction(quantiles(0.90, 0.99), Float64), + stddev_fee_grt AggregateFunction(stddevSamp, Float64), + avg_seconds_behind AggregateFunction(avg, UInt32), + max_seconds_behind AggregateFunction(max, UInt32), + quantiles_seconds_behind AggregateFunction(quantiles(0.90, 0.99), UInt32), + stddev_seconds_behind AggregateFunction(stddevSamp, UInt32), + avg_blocks_behind AggregateFunction(avg, UInt64), + max_blocks_behind AggregateFunction(max, UInt64), + quantiles_blocks_behind AggregateFunction(quantiles(0.90, 0.99), UInt64), + stddev_blocks_behind AggregateFunction(stddevSamp, UInt64) +) +ENGINE = AggregatingMergeTree +PARTITION BY toYYYYMM(time_bucket) +ORDER BY (time_bucket, subgraph, indexer, gateway_id); -- Grouping keys + +-- Renamed MV from mv_agg_allocation_1min to mv_agg_allocation_5min +-- Changed TO clause to point to agg_allocation_5min +-- Changed time bucketing to toStartOfFiveMinute +CREATE MATERIALIZED VIEW IF NOT EXISTS mv_agg_allocation_5min TO agg_allocation_5min AS +SELECT + toStartOfFiveMinute(now()) AS time_bucket, -- Use 5-minute bucket + kq.subgraph, + HEX(iq.indexer) AS indexer, -- Apply HEX encoding here + kq.gateway_id, + -- Calculate aggregate states directly from Kafka data and nested indexer_queries + countState() AS query_count, + countIfState(iq.result = 'success') AS success_count, + countIfState(iq.result != 'success') AS failure_count, + sumState(iq.fee_grt) AS total_fee_grt, + avgState(iq.response_time_ms) AS avg_indexer_response_time_ms, + maxState(iq.response_time_ms) AS max_indexer_response_time_ms, + quantilesState(0.90, 0.99)(iq.response_time_ms) AS quantiles_indexer_response_time_ms, + stddevSampState(iq.response_time_ms) AS stddev_indexer_response_time_ms, + avgState(iq.fee_grt) AS avg_fee_grt, + maxState(iq.fee_grt) AS max_fee_grt, + quantilesState(0.90, 0.99)(iq.fee_grt) AS quantiles_fee_grt, + stddevSampState(iq.fee_grt) AS stddev_fee_grt, + avgState(iq.seconds_behind) AS avg_seconds_behind, + maxState(iq.seconds_behind) AS max_seconds_behind, + quantilesState(0.90, 0.99)(iq.seconds_behind) AS quantiles_seconds_behind, + stddevSampState(iq.seconds_behind) AS stddev_seconds_behind, + avgState(iq.blocks_behind) AS avg_blocks_behind, + maxState(iq.blocks_behind) AS max_blocks_behind, + quantilesState(0.90, 0.99)(iq.blocks_behind) AS quantiles_blocks_behind, + stddevSampState(iq.blocks_behind) AS stddev_blocks_behind +FROM kafka_qos_data AS kq -- Read directly from Kafka table +ARRAY JOIN indexer_queries AS iq -- Join with nested data +WHERE kq.subgraph IS NOT NULL AND length(iq.indexer) > 0 -- Ensure subgraph and indexer are present +GROUP BY time_bucket, kq.subgraph, indexer, kq.gateway_id; -- Group by HEX encoded indexer + + +-- ---------------------------------------- +-- Indexer Level Aggregations (5-Minute State) +-- ---------------------------------------- +-- Renamed table from agg_indexer_1min to agg_indexer_5min +CREATE TABLE IF NOT EXISTS agg_indexer_5min +( + time_bucket DateTime, -- 5-Minute level bucket + indexer String, -- HEX encoded + gateway_id String, + -- Aggregate state columns + query_count AggregateFunction(count), + success_count AggregateFunction(countIf, UInt8), + failure_count AggregateFunction(countIf, UInt8), + total_fee_grt AggregateFunction(sum, Float64), + avg_indexer_response_time_ms AggregateFunction(avg, UInt32), + max_indexer_response_time_ms AggregateFunction(max, UInt32), + quantiles_indexer_response_time_ms AggregateFunction(quantiles(0.90, 0.99), UInt32), + stddev_indexer_response_time_ms AggregateFunction(stddevSamp, UInt32), + avg_fee_grt AggregateFunction(avg, Float64), + max_fee_grt AggregateFunction(max, Float64), + quantiles_fee_grt AggregateFunction(quantiles(0.90, 0.99), Float64), + stddev_fee_grt AggregateFunction(stddevSamp, Float64), + avg_seconds_behind AggregateFunction(avg, UInt32), + max_seconds_behind AggregateFunction(max, UInt32), + quantiles_seconds_behind AggregateFunction(quantiles(0.90, 0.99), UInt32), + stddev_seconds_behind AggregateFunction(stddevSamp, UInt32), + avg_blocks_behind AggregateFunction(avg, UInt64), + max_blocks_behind AggregateFunction(max, UInt64), + quantiles_blocks_behind AggregateFunction(quantiles(0.90, 0.99), UInt64), + stddev_blocks_behind AggregateFunction(stddevSamp, UInt64) +) +ENGINE = AggregatingMergeTree +PARTITION BY toYYYYMM(time_bucket) +ORDER BY (time_bucket, indexer, gateway_id); -- Grouping keys + +-- Renamed MV from mv_agg_indexer_1min to mv_agg_indexer_5min +-- Changed TO clause to point to agg_indexer_5min +-- Changed time bucketing to toStartOfFiveMinute +CREATE MATERIALIZED VIEW IF NOT EXISTS mv_agg_indexer_5min TO agg_indexer_5min AS +SELECT + toStartOfFiveMinute(now()) AS time_bucket, -- Use 5-minute bucket + HEX(iq.indexer) AS indexer, -- Apply HEX encoding here + kq.gateway_id, + -- Calculate aggregate states directly from Kafka data + countState() AS query_count, + countIfState(iq.result = 'success') AS success_count, + countIfState(iq.result != 'success') AS failure_count, + sumState(iq.fee_grt) AS total_fee_grt, + avgState(iq.response_time_ms) AS avg_indexer_response_time_ms, + maxState(iq.response_time_ms) AS max_indexer_response_time_ms, + quantilesState(0.90, 0.99)(iq.response_time_ms) AS quantiles_indexer_response_time_ms, + stddevSampState(iq.response_time_ms) AS stddev_indexer_response_time_ms, + avgState(iq.fee_grt) AS avg_fee_grt, + maxState(iq.fee_grt) AS max_fee_grt, + quantilesState(0.90, 0.99)(iq.fee_grt) AS quantiles_fee_grt, + stddevSampState(iq.fee_grt) AS stddev_fee_grt, + avgState(iq.seconds_behind) AS avg_seconds_behind, + maxState(iq.seconds_behind) AS max_seconds_behind, + quantilesState(0.90, 0.99)(iq.seconds_behind) AS quantiles_seconds_behind, + stddevSampState(iq.seconds_behind) AS stddev_seconds_behind, + avgState(iq.blocks_behind) AS avg_blocks_behind, + maxState(iq.blocks_behind) AS max_blocks_behind, + quantilesState(0.90, 0.99)(iq.blocks_behind) AS quantiles_blocks_behind, + stddevSampState(iq.blocks_behind) AS stddev_blocks_behind +FROM kafka_qos_data AS kq -- Read directly from Kafka table +ARRAY JOIN indexer_queries AS iq +WHERE length(iq.indexer) > 0 -- Ensure indexer is present +GROUP BY time_bucket, indexer, kq.gateway_id; -- Group by HEX encoded indexer + + +-- ============================================================ +-- FINAL AGGREGATION VIEWS (for GraphQL API) +-- ============================================================ +-- These views read from the 5-minute aggregate state tables +-- and compute the final results for the desired time granularity +-- (5min, hourly, daily) on the fly using -Merge functions. +-- ============================================================ + +-- ---------------------------------------- +-- Deployment Views (5min, Hourly, Daily) +-- ---------------------------------------- + +-- View for Final 5-Minute Deployment Aggregations +-- Updated FROM clause to read from agg_deployment_5min +CREATE VIEW IF NOT EXISTS view_agg_deployment_5min AS +WITH AggregatedValues AS ( + SELECT + time_bucket AS final_time_bucket, -- Already at 5-minute level + subgraph, + gateway_id, + -- Merge the 5-minute states (often just one state per group here) + countMerge(query_count) AS final_query_count, + countIfMerge(success_count) AS final_success_count, + countIfMerge(failure_count) AS final_failure_count, + sumMerge(total_fees_usd) AS final_total_fees_usd, + avgMerge(avg_response_time_ms) AS final_avg_response_time_ms, + maxMerge(max_response_time_ms) AS final_max_response_time_ms, + quantilesMerge(0.90, 0.99)(quantiles_response_time_ms) AS final_quantiles_response_time_ms, + stddevSampMerge(stddev_response_time_ms) AS final_stddev_response_time_ms, + avgMerge(avg_fee_usd) AS final_avg_fee_usd, + maxMerge(max_fee_usd) AS final_max_fee_usd, + quantilesMerge(0.90, 0.99)(quantiles_fee_usd) AS final_quantiles_fee_usd, + stddevSampMerge(stddev_fee_usd) AS final_stddev_fee_usd + FROM agg_deployment_5min -- Read from the 5-minute state table + GROUP BY final_time_bucket, subgraph, gateway_id -- Group by 5-minute bucket and dimensions +) +SELECT + final_time_bucket AS time_bucket, + subgraph, + gateway_id, + final_query_count AS query_count, + final_success_count AS success_count, + final_failure_count AS failure_count, + final_avg_response_time_ms AS avg_response_time_ms, + final_max_response_time_ms AS max_response_time_ms, + final_quantiles_response_time_ms[1] AS p90_response_time_ms, + final_quantiles_response_time_ms[2] AS p99_response_time_ms, + final_stddev_response_time_ms AS stddev_response_time_ms, + final_total_fees_usd AS total_fees_usd, + final_avg_fee_usd AS avg_fee_usd, + final_max_fee_usd AS max_fee_usd, + final_quantiles_fee_usd[1] AS p90_fee_usd, + final_quantiles_fee_usd[2] AS p99_fee_usd, + final_stddev_fee_usd AS stddev_fee_usd, + if(final_query_count = 0, 0.0, final_success_count / final_query_count) AS success_proportion +FROM AggregatedValues; + +-- View for Final Hourly Deployment Aggregations +-- Updated FROM clause to read from agg_deployment_5min +CREATE VIEW IF NOT EXISTS view_agg_deployment_hourly AS +WITH AggregatedValues AS ( + SELECT + toStartOfHour(time_bucket) AS final_time_bucket, -- Group 5min buckets into hourly + subgraph, + gateway_id, + -- Merge the 5-minute states + countMerge(query_count) AS final_query_count, + countIfMerge(success_count) AS final_success_count, + countIfMerge(failure_count) AS final_failure_count, + sumMerge(total_fees_usd) AS final_total_fees_usd, + avgMerge(avg_response_time_ms) AS final_avg_response_time_ms, + maxMerge(max_response_time_ms) AS final_max_response_time_ms, + quantilesMerge(0.90, 0.99)(quantiles_response_time_ms) AS final_quantiles_response_time_ms, + stddevSampMerge(stddev_response_time_ms) AS final_stddev_response_time_ms, + avgMerge(avg_fee_usd) AS final_avg_fee_usd, + maxMerge(max_fee_usd) AS final_max_fee_usd, + quantilesMerge(0.90, 0.99)(quantiles_fee_usd) AS final_quantiles_fee_usd, + stddevSampMerge(stddev_fee_usd) AS final_stddev_fee_usd + FROM agg_deployment_5min -- Read from the 5-minute state table + GROUP BY final_time_bucket, subgraph, gateway_id -- Group by hourly bucket and dimensions +) +SELECT + final_time_bucket AS time_bucket, + subgraph, + gateway_id, + final_query_count AS query_count, + final_success_count AS success_count, + final_failure_count AS failure_count, + final_avg_response_time_ms AS avg_response_time_ms, + final_max_response_time_ms AS max_response_time_ms, + final_quantiles_response_time_ms[1] AS p90_response_time_ms, + final_quantiles_response_time_ms[2] AS p99_response_time_ms, + final_stddev_response_time_ms AS stddev_response_time_ms, + final_total_fees_usd AS total_fees_usd, + final_avg_fee_usd AS avg_fee_usd, + final_max_fee_usd AS max_fee_usd, + final_quantiles_fee_usd[1] AS p90_fee_usd, + final_quantiles_fee_usd[2] AS p99_fee_usd, + final_stddev_fee_usd AS stddev_fee_usd, + if(final_query_count = 0, 0.0, final_success_count / final_query_count) AS success_proportion +FROM AggregatedValues; + +-- View for Final Daily Deployment Aggregations +-- Updated FROM clause to read from agg_deployment_5min +CREATE VIEW IF NOT EXISTS view_agg_deployment_daily AS +WITH AggregatedValues AS ( + SELECT + toStartOfDay(time_bucket) AS final_time_bucket, -- Group 5min buckets into daily + subgraph, + gateway_id, + -- Merge the 5-minute states + countMerge(query_count) AS final_query_count, + countIfMerge(success_count) AS final_success_count, + countIfMerge(failure_count) AS final_failure_count, + sumMerge(total_fees_usd) AS final_total_fees_usd, + avgMerge(avg_response_time_ms) AS final_avg_response_time_ms, + maxMerge(max_response_time_ms) AS final_max_response_time_ms, + quantilesMerge(0.90, 0.99)(quantiles_response_time_ms) AS final_quantiles_response_time_ms, + stddevSampMerge(stddev_response_time_ms) AS final_stddev_response_time_ms, + avgMerge(avg_fee_usd) AS final_avg_fee_usd, + maxMerge(max_fee_usd) AS final_max_fee_usd, + quantilesMerge(0.90, 0.99)(quantiles_fee_usd) AS final_quantiles_fee_usd, + stddevSampMerge(stddev_fee_usd) AS final_stddev_fee_usd + FROM agg_deployment_5min -- Read from the 5-minute state table + GROUP BY final_time_bucket, subgraph, gateway_id -- Group by daily bucket and dimensions +) +SELECT + final_time_bucket AS time_bucket, + subgraph, + gateway_id, + final_query_count AS query_count, + final_success_count AS success_count, + final_failure_count AS failure_count, + final_avg_response_time_ms AS avg_response_time_ms, + final_max_response_time_ms AS max_response_time_ms, + final_quantiles_response_time_ms[1] AS p90_response_time_ms, + final_quantiles_response_time_ms[2] AS p99_response_time_ms, + final_stddev_response_time_ms AS stddev_response_time_ms, + final_total_fees_usd AS total_fees_usd, + final_avg_fee_usd AS avg_fee_usd, + final_max_fee_usd AS max_fee_usd, + final_quantiles_fee_usd[1] AS p90_fee_usd, + final_quantiles_fee_usd[2] AS p99_fee_usd, + final_stddev_fee_usd AS stddev_fee_usd, + if(final_query_count = 0, 0.0, final_success_count / final_query_count) AS success_proportion +FROM AggregatedValues; + + +-- ---------------------------------------- +-- Allocation Views (5min, Hourly, Daily) +-- ---------------------------------------- + +-- View for Final 5-Minute Allocation Aggregations +-- Updated FROM clause to read from agg_allocation_5min +CREATE VIEW IF NOT EXISTS view_agg_allocation_5min AS +WITH AggregatedValues AS ( + SELECT + time_bucket AS final_time_bucket, -- Already at 5-minute level + subgraph, + indexer, + gateway_id, + -- Merge the 5-minute states + countMerge(query_count) AS final_query_count, + countIfMerge(success_count) AS final_success_count, + countIfMerge(failure_count) AS final_failure_count, + sumMerge(total_fee_grt) AS final_total_fee_grt, + avgMerge(avg_indexer_response_time_ms) AS final_avg_indexer_response_time_ms, + maxMerge(max_indexer_response_time_ms) AS final_max_indexer_response_time_ms, + quantilesMerge(0.90, 0.99)(quantiles_indexer_response_time_ms) AS final_quantiles_indexer_response_time_ms, + stddevSampMerge(stddev_indexer_response_time_ms) AS final_stddev_indexer_response_time_ms, + avgMerge(avg_fee_grt) AS final_avg_fee_grt, + maxMerge(max_fee_grt) AS final_max_fee_grt, + quantilesMerge(0.90, 0.99)(quantiles_fee_grt) AS final_quantiles_fee_grt, + stddevSampMerge(stddev_fee_grt) AS final_stddev_fee_grt, + avgMerge(avg_seconds_behind) AS final_avg_seconds_behind, + maxMerge(max_seconds_behind) AS final_max_seconds_behind, + quantilesMerge(0.90, 0.99)(quantiles_seconds_behind) AS final_quantiles_seconds_behind, + stddevSampMerge(stddev_seconds_behind) AS final_stddev_seconds_behind, + avgMerge(avg_blocks_behind) AS final_avg_blocks_behind, + maxMerge(max_blocks_behind) AS final_max_blocks_behind, + quantilesMerge(0.90, 0.99)(quantiles_blocks_behind) AS final_quantiles_blocks_behind, + stddevSampMerge(stddev_blocks_behind) AS final_stddev_blocks_behind + FROM agg_allocation_5min -- Read from the 5-minute state table + GROUP BY final_time_bucket, subgraph, indexer, gateway_id -- Group by 5-minute bucket and dimensions +) +SELECT + final_time_bucket AS time_bucket, + subgraph, + indexer, + gateway_id, + final_query_count AS query_count, + final_success_count AS success_count, + final_failure_count AS failure_count, + final_avg_indexer_response_time_ms AS avg_indexer_response_time_ms, + final_max_indexer_response_time_ms AS max_indexer_response_time_ms, + final_quantiles_indexer_response_time_ms[1] AS p90_indexer_response_time_ms, + final_quantiles_indexer_response_time_ms[2] AS p99_indexer_response_time_ms, + final_stddev_indexer_response_time_ms AS stddev_indexer_response_time_ms, + final_total_fee_grt AS total_fee_grt, + final_avg_fee_grt AS avg_fee_grt, + final_max_fee_grt AS max_fee_grt, + final_quantiles_fee_grt[1] AS p90_fee_grt, + final_quantiles_fee_grt[2] AS p99_fee_grt, + final_stddev_fee_grt AS stddev_fee_grt, + final_avg_seconds_behind AS avg_seconds_behind, + final_max_seconds_behind AS max_seconds_behind, + final_quantiles_seconds_behind[1] AS p90_seconds_behind, + final_quantiles_seconds_behind[2] AS p99_seconds_behind, + final_stddev_seconds_behind AS stddev_seconds_behind, + final_avg_blocks_behind AS avg_blocks_behind, + final_max_blocks_behind AS max_blocks_behind, + final_quantiles_blocks_behind[1] AS p90_blocks_behind, + final_quantiles_blocks_behind[2] AS p99_blocks_behind, + final_stddev_blocks_behind AS stddev_blocks_behind, + if(final_query_count = 0, 0.0, final_success_count / final_query_count) AS success_proportion +FROM AggregatedValues; + +-- View for Final Hourly Allocation Aggregations +-- Updated FROM clause to read from agg_allocation_5min +CREATE VIEW IF NOT EXISTS view_agg_allocation_hourly AS +WITH AggregatedValues AS ( + SELECT + toStartOfHour(time_bucket) AS final_time_bucket, -- Group 5min buckets into hourly + subgraph, + indexer, + gateway_id, + -- Merge the 5-minute states + countMerge(query_count) AS final_query_count, + countIfMerge(success_count) AS final_success_count, + countIfMerge(failure_count) AS final_failure_count, + sumMerge(total_fee_grt) AS final_total_fee_grt, + avgMerge(avg_indexer_response_time_ms) AS final_avg_indexer_response_time_ms, + maxMerge(max_indexer_response_time_ms) AS final_max_indexer_response_time_ms, + quantilesMerge(0.90, 0.99)(quantiles_indexer_response_time_ms) AS final_quantiles_indexer_response_time_ms, + stddevSampMerge(stddev_indexer_response_time_ms) AS final_stddev_indexer_response_time_ms, + avgMerge(avg_fee_grt) AS final_avg_fee_grt, + maxMerge(max_fee_grt) AS final_max_fee_grt, + quantilesMerge(0.90, 0.99)(quantiles_fee_grt) AS final_quantiles_fee_grt, + stddevSampMerge(stddev_fee_grt) AS final_stddev_fee_grt, + avgMerge(avg_seconds_behind) AS final_avg_seconds_behind, + maxMerge(max_seconds_behind) AS final_max_seconds_behind, + quantilesMerge(0.90, 0.99)(quantiles_seconds_behind) AS final_quantiles_seconds_behind, + stddevSampMerge(stddev_seconds_behind) AS final_stddev_seconds_behind, + avgMerge(avg_blocks_behind) AS final_avg_blocks_behind, + maxMerge(max_blocks_behind) AS final_max_blocks_behind, + quantilesMerge(0.90, 0.99)(quantiles_blocks_behind) AS final_quantiles_blocks_behind, + stddevSampMerge(stddev_blocks_behind) AS final_stddev_blocks_behind + FROM agg_allocation_5min -- Read from the 5-minute state table + GROUP BY final_time_bucket, subgraph, indexer, gateway_id -- Group by hourly bucket and dimensions +) +SELECT + final_time_bucket AS time_bucket, + subgraph, + indexer, + gateway_id, + final_query_count AS query_count, + final_success_count AS success_count, + final_failure_count AS failure_count, + final_avg_indexer_response_time_ms AS avg_indexer_response_time_ms, + final_max_indexer_response_time_ms AS max_indexer_response_time_ms, + final_quantiles_indexer_response_time_ms[1] AS p90_indexer_response_time_ms, + final_quantiles_indexer_response_time_ms[2] AS p99_indexer_response_time_ms, + final_stddev_indexer_response_time_ms AS stddev_indexer_response_time_ms, + final_total_fee_grt AS total_fee_grt, + final_avg_fee_grt AS avg_fee_grt, + final_max_fee_grt AS max_fee_grt, + final_quantiles_fee_grt[1] AS p90_fee_grt, + final_quantiles_fee_grt[2] AS p99_fee_grt, + final_stddev_fee_grt AS stddev_fee_grt, + final_avg_seconds_behind AS avg_seconds_behind, + final_max_seconds_behind AS max_seconds_behind, + final_quantiles_seconds_behind[1] AS p90_seconds_behind, + final_quantiles_seconds_behind[2] AS p99_seconds_behind, + final_stddev_seconds_behind AS stddev_seconds_behind, + final_avg_blocks_behind AS avg_blocks_behind, + final_max_blocks_behind AS max_blocks_behind, + final_quantiles_blocks_behind[1] AS p90_blocks_behind, + final_quantiles_blocks_behind[2] AS p99_blocks_behind, + final_stddev_blocks_behind AS stddev_blocks_behind, + if(final_query_count = 0, 0.0, final_success_count / final_query_count) AS success_proportion +FROM AggregatedValues; + +-- View for Final Daily Allocation Aggregations +-- Updated FROM clause to read from agg_allocation_5min +CREATE VIEW IF NOT EXISTS view_agg_allocation_daily AS +WITH AggregatedValues AS ( + SELECT + toStartOfDay(time_bucket) AS final_time_bucket, -- Group 5min buckets into daily + subgraph, + indexer, + gateway_id, + -- Merge the 5-minute states + countMerge(query_count) AS final_query_count, + countIfMerge(success_count) AS final_success_count, + countIfMerge(failure_count) AS final_failure_count, + sumMerge(total_fee_grt) AS final_total_fee_grt, + avgMerge(avg_indexer_response_time_ms) AS final_avg_indexer_response_time_ms, + maxMerge(max_indexer_response_time_ms) AS final_max_indexer_response_time_ms, + quantilesMerge(0.90, 0.99)(quantiles_indexer_response_time_ms) AS final_quantiles_indexer_response_time_ms, + stddevSampMerge(stddev_indexer_response_time_ms) AS final_stddev_indexer_response_time_ms, + avgMerge(avg_fee_grt) AS final_avg_fee_grt, + maxMerge(max_fee_grt) AS final_max_fee_grt, + quantilesMerge(0.90, 0.99)(quantiles_fee_grt) AS final_quantiles_fee_grt, + stddevSampMerge(stddev_fee_grt) AS final_stddev_fee_grt, + avgMerge(avg_seconds_behind) AS final_avg_seconds_behind, + maxMerge(max_seconds_behind) AS final_max_seconds_behind, + quantilesMerge(0.90, 0.99)(quantiles_seconds_behind) AS final_quantiles_seconds_behind, + stddevSampMerge(stddev_seconds_behind) AS final_stddev_seconds_behind, + avgMerge(avg_blocks_behind) AS final_avg_blocks_behind, + maxMerge(max_blocks_behind) AS final_max_blocks_behind, + quantilesMerge(0.90, 0.99)(quantiles_blocks_behind) AS final_quantiles_blocks_behind, + stddevSampMerge(stddev_blocks_behind) AS final_stddev_blocks_behind + FROM agg_allocation_5min -- Read from the 5-minute state table + GROUP BY final_time_bucket, subgraph, indexer, gateway_id -- Group by daily bucket and dimensions +) +SELECT + final_time_bucket AS time_bucket, + subgraph, + indexer, + gateway_id, + final_query_count AS query_count, + final_success_count AS success_count, + final_failure_count AS failure_count, + final_avg_indexer_response_time_ms AS avg_indexer_response_time_ms, + final_max_indexer_response_time_ms AS max_indexer_response_time_ms, + final_quantiles_indexer_response_time_ms[1] AS p90_indexer_response_time_ms, + final_quantiles_indexer_response_time_ms[2] AS p99_indexer_response_time_ms, + final_stddev_indexer_response_time_ms AS stddev_indexer_response_time_ms, + final_total_fee_grt AS total_fee_grt, + final_avg_fee_grt AS avg_fee_grt, + final_max_fee_grt AS max_fee_grt, + final_quantiles_fee_grt[1] AS p90_fee_grt, + final_quantiles_fee_grt[2] AS p99_fee_grt, + final_stddev_fee_grt AS stddev_fee_grt, + final_avg_seconds_behind AS avg_seconds_behind, + final_max_seconds_behind AS max_seconds_behind, + final_quantiles_seconds_behind[1] AS p90_seconds_behind, + final_quantiles_seconds_behind[2] AS p99_seconds_behind, + final_stddev_seconds_behind AS stddev_seconds_behind, + final_avg_blocks_behind AS avg_blocks_behind, + final_max_blocks_behind AS max_blocks_behind, + final_quantiles_blocks_behind[1] AS p90_blocks_behind, + final_quantiles_blocks_behind[2] AS p99_blocks_behind, + final_stddev_blocks_behind AS stddev_blocks_behind, + if(final_query_count = 0, 0.0, final_success_count / final_query_count) AS success_proportion +FROM AggregatedValues; + + +-- ---------------------------------------- +-- Indexer Views (5min, Hourly, Daily) +-- ---------------------------------------- + +-- View for Final 5-Minute Indexer Aggregations +-- Updated FROM clause to read from agg_indexer_5min +CREATE VIEW IF NOT EXISTS view_agg_indexer_5min AS +WITH AggregatedValues AS ( + SELECT + time_bucket AS final_time_bucket, -- Already at 5-minute level + indexer, + gateway_id, + -- Merge the 5-minute states + countMerge(query_count) AS final_query_count, + countIfMerge(success_count) AS final_success_count, + countIfMerge(failure_count) AS final_failure_count, + sumMerge(total_fee_grt) AS final_total_fee_grt, + avgMerge(avg_indexer_response_time_ms) AS final_avg_indexer_response_time_ms, + maxMerge(max_indexer_response_time_ms) AS final_max_indexer_response_time_ms, + quantilesMerge(0.90, 0.99)(quantiles_indexer_response_time_ms) AS final_quantiles_indexer_response_time_ms, + stddevSampMerge(stddev_indexer_response_time_ms) AS final_stddev_indexer_response_time_ms, + avgMerge(avg_fee_grt) AS final_avg_fee_grt, + maxMerge(max_fee_grt) AS final_max_fee_grt, + quantilesMerge(0.90, 0.99)(quantiles_fee_grt) AS final_quantiles_fee_grt, + stddevSampMerge(stddev_fee_grt) AS final_stddev_fee_grt, + avgMerge(avg_seconds_behind) AS final_avg_seconds_behind, + maxMerge(max_seconds_behind) AS final_max_seconds_behind, + quantilesMerge(0.90, 0.99)(quantiles_seconds_behind) AS final_quantiles_seconds_behind, + stddevSampMerge(stddev_seconds_behind) AS final_stddev_seconds_behind, + avgMerge(avg_blocks_behind) AS final_avg_blocks_behind, + maxMerge(max_blocks_behind) AS final_max_blocks_behind, + quantilesMerge(0.90, 0.99)(quantiles_blocks_behind) AS final_quantiles_blocks_behind, + stddevSampMerge(stddev_blocks_behind) AS final_stddev_blocks_behind + FROM agg_indexer_5min -- Read from the 5-minute state table + GROUP BY final_time_bucket, indexer, gateway_id -- Group by 5-minute bucket and dimensions +) +SELECT + final_time_bucket AS time_bucket, + indexer, + gateway_id, + final_query_count AS query_count, + final_success_count AS success_count, + final_failure_count AS failure_count, + final_avg_indexer_response_time_ms AS avg_indexer_response_time_ms, + final_max_indexer_response_time_ms AS max_indexer_response_time_ms, + final_quantiles_indexer_response_time_ms[1] AS p90_indexer_response_time_ms, + final_quantiles_indexer_response_time_ms[2] AS p99_indexer_response_time_ms, + final_stddev_indexer_response_time_ms AS stddev_indexer_response_time_ms, + final_total_fee_grt AS total_fee_grt, + final_avg_fee_grt AS avg_fee_grt, + final_max_fee_grt AS max_fee_grt, + final_quantiles_fee_grt[1] AS p90_fee_grt, + final_quantiles_fee_grt[2] AS p99_fee_grt, + final_stddev_fee_grt AS stddev_fee_grt, + final_avg_seconds_behind AS avg_seconds_behind, + final_max_seconds_behind AS max_seconds_behind, + final_quantiles_seconds_behind[1] AS p90_seconds_behind, + final_quantiles_seconds_behind[2] AS p99_seconds_behind, + final_stddev_seconds_behind AS stddev_seconds_behind, + final_avg_blocks_behind AS avg_blocks_behind, + final_max_blocks_behind AS max_blocks_behind, + final_quantiles_blocks_behind[1] AS p90_blocks_behind, + final_quantiles_blocks_behind[2] AS p99_blocks_behind, + final_stddev_blocks_behind AS stddev_blocks_behind, + if(final_query_count = 0, 0.0, final_success_count / final_query_count) AS success_proportion +FROM AggregatedValues; + +-- View for Final Hourly Indexer Aggregations +-- Updated FROM clause to read from agg_indexer_5min +CREATE VIEW IF NOT EXISTS view_agg_indexer_hourly AS +WITH AggregatedValues AS ( + SELECT + toStartOfHour(time_bucket) AS final_time_bucket, -- Group 5min buckets into hourly + indexer, + gateway_id, + -- Merge the 5-minute states + countMerge(query_count) AS final_query_count, + countIfMerge(success_count) AS final_success_count, + countIfMerge(failure_count) AS final_failure_count, + sumMerge(total_fee_grt) AS final_total_fee_grt, + avgMerge(avg_indexer_response_time_ms) AS final_avg_indexer_response_time_ms, + maxMerge(max_indexer_response_time_ms) AS final_max_indexer_response_time_ms, + quantilesMerge(0.90, 0.99)(quantiles_indexer_response_time_ms) AS final_quantiles_indexer_response_time_ms, + stddevSampMerge(stddev_indexer_response_time_ms) AS final_stddev_indexer_response_time_ms, + avgMerge(avg_fee_grt) AS final_avg_fee_grt, + maxMerge(max_fee_grt) AS final_max_fee_grt, + quantilesMerge(0.90, 0.99)(quantiles_fee_grt) AS final_quantiles_fee_grt, + stddevSampMerge(stddev_fee_grt) AS final_stddev_fee_grt, + avgMerge(avg_seconds_behind) AS final_avg_seconds_behind, + maxMerge(max_seconds_behind) AS final_max_seconds_behind, + quantilesMerge(0.90, 0.99)(quantiles_seconds_behind) AS final_quantiles_seconds_behind, + stddevSampMerge(stddev_seconds_behind) AS final_stddev_seconds_behind, + avgMerge(avg_blocks_behind) AS final_avg_blocks_behind, + maxMerge(max_blocks_behind) AS final_max_blocks_behind, + quantilesMerge(0.90, 0.99)(quantiles_blocks_behind) AS final_quantiles_blocks_behind, + stddevSampMerge(stddev_blocks_behind) AS final_stddev_blocks_behind + FROM agg_indexer_5min -- Read from the 5-minute state table + GROUP BY final_time_bucket, indexer, gateway_id -- Group by hourly bucket and dimensions +) +SELECT + final_time_bucket AS time_bucket, + indexer, + gateway_id, + final_query_count AS query_count, + final_success_count AS success_count, + final_failure_count AS failure_count, + final_avg_indexer_response_time_ms AS avg_indexer_response_time_ms, + final_max_indexer_response_time_ms AS max_indexer_response_time_ms, + final_quantiles_indexer_response_time_ms[1] AS p90_indexer_response_time_ms, + final_quantiles_indexer_response_time_ms[2] AS p99_indexer_response_time_ms, + final_stddev_indexer_response_time_ms AS stddev_indexer_response_time_ms, + final_total_fee_grt AS total_fee_grt, + final_avg_fee_grt AS avg_fee_grt, + final_max_fee_grt AS max_fee_grt, + final_quantiles_fee_grt[1] AS p90_fee_grt, + final_quantiles_fee_grt[2] AS p99_fee_grt, + final_stddev_fee_grt AS stddev_fee_grt, + final_avg_seconds_behind AS avg_seconds_behind, + final_max_seconds_behind AS max_seconds_behind, + final_quantiles_seconds_behind[1] AS p90_seconds_behind, + final_quantiles_seconds_behind[2] AS p99_seconds_behind, + final_stddev_seconds_behind AS stddev_seconds_behind, + final_avg_blocks_behind AS avg_blocks_behind, + final_max_blocks_behind AS max_blocks_behind, + final_quantiles_blocks_behind[1] AS p90_blocks_behind, + final_quantiles_blocks_behind[2] AS p99_blocks_behind, + final_stddev_blocks_behind AS stddev_blocks_behind, + if(final_query_count = 0, 0.0, final_success_count / final_query_count) AS success_proportion +FROM AggregatedValues; + +-- View for Final Daily Indexer Aggregations +-- Updated FROM clause to read from agg_indexer_5min +CREATE VIEW IF NOT EXISTS view_agg_indexer_daily AS +WITH AggregatedValues AS ( + SELECT + toStartOfDay(time_bucket) AS final_time_bucket, -- Group 5min buckets into daily + indexer, + gateway_id, + -- Merge the 5-minute states + countMerge(query_count) AS final_query_count, + countIfMerge(success_count) AS final_success_count, + countIfMerge(failure_count) AS final_failure_count, + sumMerge(total_fee_grt) AS final_total_fee_grt, + avgMerge(avg_indexer_response_time_ms) AS final_avg_indexer_response_time_ms, + maxMerge(max_indexer_response_time_ms) AS final_max_indexer_response_time_ms, + quantilesMerge(0.90, 0.99)(quantiles_indexer_response_time_ms) AS final_quantiles_indexer_response_time_ms, + stddevSampMerge(stddev_indexer_response_time_ms) AS final_stddev_indexer_response_time_ms, + avgMerge(avg_fee_grt) AS final_avg_fee_grt, + maxMerge(max_fee_grt) AS final_max_fee_grt, + quantilesMerge(0.90, 0.99)(quantiles_fee_grt) AS final_quantiles_fee_grt, + stddevSampMerge(stddev_fee_grt) AS final_stddev_fee_grt, + avgMerge(avg_seconds_behind) AS final_avg_seconds_behind, + maxMerge(max_seconds_behind) AS final_max_seconds_behind, + quantilesMerge(0.90, 0.99)(quantiles_seconds_behind) AS final_quantiles_seconds_behind, + stddevSampMerge(stddev_seconds_behind) AS final_stddev_seconds_behind, + avgMerge(avg_blocks_behind) AS final_avg_blocks_behind, + maxMerge(max_blocks_behind) AS final_max_blocks_behind, + quantilesMerge(0.90, 0.99)(quantiles_blocks_behind) AS final_quantiles_blocks_behind, + stddevSampMerge(stddev_blocks_behind) AS final_stddev_blocks_behind + FROM agg_indexer_5min -- Read from the 5-minute state table + GROUP BY final_time_bucket, indexer, gateway_id -- Group by daily bucket and dimensions +) +SELECT + final_time_bucket AS time_bucket, + indexer, + gateway_id, + final_query_count AS query_count, + final_success_count AS success_count, + final_failure_count AS failure_count, + final_avg_indexer_response_time_ms AS avg_indexer_response_time_ms, + final_max_indexer_response_time_ms AS max_indexer_response_time_ms, + final_quantiles_indexer_response_time_ms[1] AS p90_indexer_response_time_ms, + final_quantiles_indexer_response_time_ms[2] AS p99_indexer_response_time_ms, + final_stddev_indexer_response_time_ms AS stddev_indexer_response_time_ms, + final_total_fee_grt AS total_fee_grt, + final_avg_fee_grt AS avg_fee_grt, + final_max_fee_grt AS max_fee_grt, + final_quantiles_fee_grt[1] AS p90_fee_grt, + final_quantiles_fee_grt[2] AS p99_fee_grt, + final_stddev_fee_grt AS stddev_fee_grt, + final_avg_seconds_behind AS avg_seconds_behind, + final_max_seconds_behind AS max_seconds_behind, + final_quantiles_seconds_behind[1] AS p90_seconds_behind, + final_quantiles_seconds_behind[2] AS p99_seconds_behind, + final_stddev_seconds_behind AS stddev_seconds_behind, + final_avg_blocks_behind AS avg_blocks_behind, + final_max_blocks_behind AS max_blocks_behind, + final_quantiles_blocks_behind[1] AS p90_blocks_behind, + final_quantiles_blocks_behind[2] AS p99_blocks_behind, + final_stddev_blocks_behind AS stddev_blocks_behind, + if(final_query_count = 0, 0.0, final_success_count / final_query_count) AS success_proportion +FROM AggregatedValues; diff --git a/oracle/clickhouse/users.xml b/oracle/clickhouse/users.xml new file mode 100644 index 0000000..033e5ba --- /dev/null +++ b/oracle/clickhouse/users.xml @@ -0,0 +1,44 @@ + + + + + + ::/0 + + default + default + 1 + + + + graphql_password + + ::/0 + + default + default + + + + + + 10000000000 + 0 + 0 + 0 + + + + + + + 3600 + 0 + 0 + 0 + 0 + 0 + + + + \ No newline at end of file diff --git a/oracle/docker-compose.yml b/oracle/docker-compose.yml new file mode 100644 index 0000000..f83a626 --- /dev/null +++ b/oracle/docker-compose.yml @@ -0,0 +1,198 @@ +# QoS Oracle - Data pipeline for The Graph Network quality metrics +# Common configuration blocks +x-default-healthcheck: &default-healthcheck + interval: 10s + timeout: 5s + retries: 3 + +x-clickhouse-env: &clickhouse-env + CLICKHOUSE_DB: default + CLICKHOUSE_USER: graphql + CLICKHOUSE_PASSWORD: graphql_password + +# Services +services: + # Core dependencies + clickhouse-server: + image: clickhouse/clickhouse-server:25.3 + profiles: [all, deps, dev] + expose: + - "8123" # HTTP interface + - "9000" # Native protocol + - "9363" # Prometheus + ports: + - "8123:8123" # HTTP: queries/monitoring + - "9000:9000" # Native: client connections + - "9363:9363" # Prometheus + volumes: + - ./clickhouse/schema.proto:/etc/clickhouse-server/schema.proto:ro + - ./clickhouse/tables.sql:/docker-entrypoint-initdb.d/tables.sql:ro + - ./clickhouse/allow_schema_path.xml:/etc/clickhouse-server/config.d/allow_schema_path.xml:ro + - ./clickhouse/settings.xml:/etc/clickhouse-server/config.d/settings.xml:ro + - ./clickhouse/users.xml:/etc/clickhouse-server/users.d/users.xml:ro + - clickhouse_data:/var/lib/clickhouse + - clickhouse_logs:/var/log/clickhouse-server + ulimits: + nofile: + soft: 262144 + hard: 262144 + healthcheck: + <<: *default-healthcheck + test: ["CMD", "wget", "--spider", "-q", "http://localhost:8123/ping"] + + redpanda: + image: redpandadata/redpanda:latest + profiles: [all, deps, dev] + expose: + - "9092" # Kafka API + - "8082" # Pandaproxy HTTP + - "9644" # Prometheus metrics + ports: + - "9092:9092" # Kafka: producer/consumer + - "9644:9644" + command: + - redpanda + - start + - --smp=1 + - --memory=1G + - --reserve-memory=0M + - --overprovisioned + - --node-id=0 + - --check=false + - --pandaproxy-addr=0.0.0.0:8082 + - --advertise-pandaproxy-addr=redpanda:8082 + - --kafka-addr=0.0.0.0:9092 + - --advertise-kafka-addr=redpanda:9092 + - --rpc-addr=0.0.0.0:33145 + - --advertise-rpc-addr=redpanda:33145 + volumes: + - redpanda_data:/var/lib/redpanda/data:delegated # data: container writes + healthcheck: + <<: *default-healthcheck + test: ["CMD-SHELL", "rpk cluster health | grep -q 'Healthy:.*true'"] + start_period: 30s # Redpanda needs more time to initialize + environment: + REDPANDA_RPC_SERVER_LISTEN_ADDR: 0.0.0.0 + REDPANDA_KAFKA_ADDRESS: 0.0.0.0:9092 + + # Application services + graphql-api: + build: + context: ./graphql-api + profiles: [all, dev] + expose: + - "8000" # GraphQL API + ports: + - "8000:8000" # API: external access + depends_on: + clickhouse-server: + condition: service_healthy + environment: + <<: *clickhouse-env + CLICKHOUSE_URL: http://clickhouse-server:8123 + healthcheck: + <<: *default-healthcheck + test: ["CMD-SHELL", "curl -f http://localhost:8000/health || exit 1"] + + # Test/development tools + test-producer: + build: + context: . + dockerfile: ./test-producer/Dockerfile + profiles: [all, dev] + depends_on: + redpanda: + condition: service_healthy + clickhouse-server: + condition: service_healthy + volumes: + - ./clickhouse/schema.proto:/app/clickhouse/schema.proto:ro + environment: + KAFKA_BROKER: redpanda:9092 + MESSAGES_PER_SECOND: 50000 + restart: on-failure + healthcheck: + <<: *default-healthcheck + test: ["CMD-SHELL", "ps aux | grep -v grep | grep -q qos_kafka_producer || exit 1"] + + redpanda-console: + profiles: [all, dev] + image: redpandadata/console:latest + container_name: redpanda-console + depends_on: + - redpanda + ports: + - "8080:8080" + environment: + KAFKA_BROKERS: redpanda:9092 + PROTOBUF_ENABLED: "true" + volumes: + - ./clickhouse/schema.proto:/etc/redpanda/protobuf/schema.proto:ro + healthcheck: + test: wget --no-verbose --tries=1 --spider http://localhost:8080 || exit 1 + interval: 10s + timeout: 5s + retries: 5 + start_period: 10s + + # Observability: Prometheus + Grafana + prometheus: + profiles: [all, deps] + image: prom/prometheus:latest + container_name: prometheus + depends_on: + - redpanda + volumes: + - ./prometheus:/etc/prometheus:ro + # This volume will store TSDB data so metrics persist across restarts + - prometheus_data:/prometheus + command: + - '--config.file=/etc/prometheus/prometheus.yml' + - '--storage.tsdb.path=/prometheus' + - '--storage.tsdb.retention.time=7d' # Example retention: 7 days + - '--web.enable-lifecycle' # If you want to allow config reload via HTTP + expose: + - "9090" + ports: + - '9090:9090' + healthcheck: + <<: *default-healthcheck + test: ["CMD-SHELL", "wget --spider -q http://localhost:9090/-/healthy || exit 1"] + start_period: 10s + + grafana: + profiles: [all, deps] + image: grafana/grafana:latest + container_name: grafana + depends_on: + - prometheus + environment: + GF_PATHS_PROVISIONING: /etc/grafana/provisioning + GF_AUTH_ANONYMOUS_ENABLED: true + GF_AUTH_ANONYMOUS_ORG_ROLE: Admin + GF_AUTH_BASIC_ENABLED: false + expose: + - "3000" + ports: + - "3000:3000" + healthcheck: + <<: *default-healthcheck + test: ["CMD-SHELL", "curl -f http://localhost:3000/api/health || exit 1"] + start_period: 10s + volumes: + - ./grafana/provisioning/:/etc/grafana/provisioning/ + - ./grafana/dashboards/:/var/lib/grafana/dashboards/ + +# Persistent volumes +volumes: + clickhouse_data: + driver: local + clickhouse_logs: + redpanda_data: + driver: local + prometheus_data: + driver: local + grafana_data: + driver: local + grafana_provisioning: + driver: local diff --git a/oracle/grafana/dashboards/qos_oracle_dashboard.json b/oracle/grafana/dashboards/qos_oracle_dashboard.json new file mode 100644 index 0000000..30d944a --- /dev/null +++ b/oracle/grafana/dashboards/qos_oracle_dashboard.json @@ -0,0 +1,1901 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 1, + "links": [], + "panels": [ + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 24, + "panels": [], + "title": "Redpanda Overview", + "type": "row" + }, + { + "datasource": "Prometheus", + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 0, + "y": 1 + }, + "id": 1, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "expr": "vectorized_application_uptime", + "legendFormat": "Instance: {{instance}}", + "refId": "A" + } + ], + "title": "Redpanda Uptime (seconds)", + "type": "stat" + }, + { + "datasource": "Prometheus", + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 4, + "y": 1 + }, + "id": 2, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "expr": "vectorized_application_build", + "legendFormat": "Build: {{version}}", + "refId": "A" + } + ], + "title": "Redpanda Build Info", + "type": "stat" + }, + { + "datasource": "Prometheus", + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 8, + "y": 1 + }, + "id": 3, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "expr": "vectorized_application_fips_mode", + "legendFormat": "{{instance}} - FIPS", + "refId": "A" + } + ], + "title": "FIPS Mode", + "type": "stat" + }, + { + "datasource": "Prometheus", + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 12, + "y": 1 + }, + "id": 4, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "expr": "vectorized_cluster_partition_under_replicated_replicas", + "legendFormat": "URP", + "refId": "A" + } + ], + "title": "Under-Replicated Partitions", + "type": "stat" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 5 + }, + "id": 25, + "panels": [], + "title": "Redpanda Traffic & Throughput", + "type": "row" + }, + { + "datasource": "Prometheus", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ops" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 6 + }, + "id": 5, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "expr": "rate(vectorized_cluster_partition_records_produced[5m])", + "legendFormat": "Produced", + "refId": "A" + }, + { + "expr": "rate(vectorized_cluster_partition_records_fetched[5m])", + "legendFormat": "Fetched", + "refId": "B" + } + ], + "title": "Records Produced / Fetched per Sec", + "type": "timeseries" + }, + { + "datasource": "Prometheus", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "Bps" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 6 + }, + "id": 6, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "expr": "rate(vectorized_cluster_partition_bytes_produced_total[5m])", + "legendFormat": "Bytes Produced", + "refId": "A" + }, + { + "expr": "rate(vectorized_cluster_partition_bytes_fetched_total[5m])", + "legendFormat": "Bytes Fetched", + "refId": "B" + } + ], + "title": "Bytes Produced / Fetched per Sec", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 14 + }, + "id": 26, + "panels": [], + "title": "Redpanda Latencies & Connection Health", + "type": "row" + }, + { + "datasource": "Prometheus", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "µs" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 0, + "y": 15 + }, + "id": 7, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "expr": "rate(vectorized_kafka_handler_latency_microseconds_count[5m])", + "legendFormat": "Handler Latency", + "refId": "A" + } + ], + "title": "Kafka Handler Latency (µs)", + "type": "timeseries" + }, + { + "datasource": "Prometheus", + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 8, + "x": 8, + "y": 15 + }, + "id": 8, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "expr": "vectorized_kafka_rpc_active_connections", + "legendFormat": "{{instance}}", + "refId": "A" + } + ], + "title": "Active Kafka Connections", + "type": "stat" + }, + { + "datasource": "Prometheus", + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 8, + "x": 16, + "y": 15 + }, + "id": 9, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "expr": "vectorized_kafka_rpc_connections_rejected", + "legendFormat": "Rejected", + "refId": "A" + } + ], + "title": "Rejected Kafka Connections", + "type": "stat" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 23 + }, + "id": 27, + "panels": [], + "title": "Redpanda Resource Usage", + "type": "row" + }, + { + "datasource": "Prometheus", + "description": "CPU busy time in ms; using irate for approximate CPU usage.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ms" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 24 + }, + "id": 10, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "expr": "irate(vectorized_reactor_cpu_busy_ms[5m])", + "legendFormat": "{{instance}}", + "refId": "A" + } + ], + "title": "CPU Busy ms/s (approx)", + "type": "timeseries" + }, + { + "datasource": "Prometheus", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "decimals": 2, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 24 + }, + "id": 11, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "expr": "vectorized_memory_allocated_memory", + "legendFormat": "Allocated", + "refId": "A" + }, + { + "expr": "vectorized_memory_available_memory", + "legendFormat": "Available", + "refId": "B" + } + ], + "title": "Memory Used / Available", + "type": "timeseries" + }, + { + "datasource": "Prometheus", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "decimals": 2, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 32 + }, + "id": 12, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "expr": "vectorized_space_management_disk_usage_bytes", + "legendFormat": "Used Bytes", + "refId": "A" + }, + { + "expr": "vectorized_space_management_target_disk_size_bytes", + "legendFormat": "Target Disk Size", + "refId": "B" + } + ], + "title": "Disk Usage", + "type": "timeseries" + }, + { + "datasource": "Prometheus", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ms" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 32 + }, + "id": 13, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "expr": "irate(vectorized_io_queue_delay[5m])", + "legendFormat": "I/O Delay (ms)", + "refId": "A" + }, + { + "expr": "irate(vectorized_io_queue_consumption[5m])", + "legendFormat": "I/O Consumption (ms)", + "refId": "B" + } + ], + "title": "I/O Queue Delays & Consumption", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 40 + }, + "id": 28, + "panels": [], + "title": "ClickHouse Overview", + "type": "row" + }, + { + "datasource": "Prometheus", + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 8, + "x": 0, + "y": 41 + }, + "id": 14, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "expr": "ClickHouseProfileEvents_Query", + "legendFormat": "All Queries", + "refId": "A" + } + ], + "title": "Total Queries", + "type": "stat" + }, + { + "datasource": "Prometheus", + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 8, + "x": 8, + "y": 41 + }, + "id": 15, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "expr": "ClickHouseProfileEvents_SelectQuery", + "legendFormat": "Select", + "refId": "A" + } + ], + "title": "Select Queries (Total)", + "type": "stat" + }, + { + "datasource": "Prometheus", + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 8, + "x": 16, + "y": 41 + }, + "id": 16, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "expr": "ClickHouseProfileEvents_InsertQuery", + "legendFormat": "Insert", + "refId": "A" + } + ], + "title": "Insert Queries (Total)", + "type": "stat" + }, + { + "datasource": "Prometheus", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ops" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 45 + }, + "id": 17, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "expr": "rate(ClickHouseProfileEvents_Query[5m])", + "legendFormat": "Queries/s", + "refId": "A" + } + ], + "title": "Queries per Second", + "type": "timeseries" + }, + { + "datasource": "Prometheus", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "µs" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 45 + }, + "id": 18, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "expr": "rate(ClickHouseProfileEvents_QueryTimeMicroseconds[5m])", + "legendFormat": "Query Time µs/s", + "refId": "A" + } + ], + "title": "Query Time (µs) Rate", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 53 + }, + "id": 29, + "panels": [], + "title": "ClickHouse Inserts & Merges", + "type": "row" + }, + { + "datasource": "Prometheus", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ops" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 0, + "y": 54 + }, + "id": 19, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "expr": "rate(ClickHouseProfileEvents_InsertedRows[5m])", + "legendFormat": "Rows/s", + "refId": "A" + } + ], + "title": "Inserted Rows/s", + "type": "timeseries" + }, + { + "datasource": "Prometheus", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ops" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 8, + "y": 54 + }, + "id": 20, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "expr": "rate(ClickHouseProfileEvents_MergedRows[5m])", + "legendFormat": "Merged/s", + "refId": "A" + } + ], + "title": "Merged Rows/s", + "type": "timeseries" + }, + { + "datasource": "Prometheus", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ms" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 16, + "y": 54 + }, + "id": 21, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "expr": "rate(ClickHouseProfileEvents_MergesTimeMilliseconds[5m])", + "legendFormat": "Merges Time ms/s", + "refId": "A" + } + ], + "title": "Merges Time (ms/s)", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 62 + }, + "id": 30, + "panels": [], + "title": "ClickHouse Resource Usage", + "type": "row" + }, + { + "datasource": "Prometheus", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "decimals": 2, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 63 + }, + "id": 22, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "expr": "ClickHouseMetrics_MemoryTracking", + "legendFormat": "Memory In Use", + "refId": "A" + } + ], + "title": "ClickHouse Memory Tracking (bytes)", + "type": "timeseries" + }, + { + "datasource": "Prometheus", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "decimals": 2, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 63 + }, + "id": 23, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "expr": "ClickHouseAsyncMetrics_TotalBytesOfMergeTreeTables", + "legendFormat": "Total Bytes of MergeTree", + "refId": "A" + }, + { + "expr": "ClickHouseAsyncMetrics_DiskAvailable_default", + "legendFormat": "Disk Available (default)", + "refId": "B" + } + ], + "title": "MergeTree Disk Usage / Available", + "type": "timeseries" + } + ], + "preload": false, + "schemaVersion": 41, + "tags": [ + "qos_oracle" + ], + "templating": { + "list": [] + }, + "timepicker": {}, + "timezone": "", + "title": "QoS Oracle: ClickHouse & Redpanda Overview", + "uid": "qos-oracle-clickhouse-redpanda", + "version": 8 +} diff --git a/oracle/grafana/provisioning/dashboards/dashboard.yaml b/oracle/grafana/provisioning/dashboards/dashboard.yaml new file mode 100644 index 0000000..8f9f56f --- /dev/null +++ b/oracle/grafana/provisioning/dashboards/dashboard.yaml @@ -0,0 +1,10 @@ +apiVersion: 1 +providers: + - name: 'Local Dashboards' + orgId: 1 + folder: '' + type: file + disableDeletion: false + updateIntervalSeconds: 10 + options: + path: /var/lib/grafana/dashboards diff --git a/oracle/grafana/provisioning/datasources/datasource.yaml b/oracle/grafana/provisioning/datasources/datasource.yaml new file mode 100644 index 0000000..0eddf26 --- /dev/null +++ b/oracle/grafana/provisioning/datasources/datasource.yaml @@ -0,0 +1,7 @@ +apiVersion: 1 +datasources: + - name: Prometheus + type: prometheus + access: proxy + url: http://prometheus:9090 + isDefault: true diff --git a/oracle/graphql-api/Cargo.toml b/oracle/graphql-api/Cargo.toml new file mode 100644 index 0000000..476a7d8 --- /dev/null +++ b/oracle/graphql-api/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "qos_graphql_api" +version = "0.1.0" +edition = "2021" + +[dependencies] +actix-web = "4" +async-graphql = "4" +async-graphql-actix-web = "4" +clickhouse = { version = "0.13.2", features = ["uuid"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +tokio = { version = "1", features = ["full"] } +futures = "0.3" +bs58 = "0.5.0" +hex = "0.4.3" +chrono = "0.4" +anyhow = "1.0.79" +tracing = { version = "0.1.40", features = ["attributes"] } +tracing-subscriber = { version = "0.3.18", features = ["env-filter", "json"] } +axum = "0.8.1" +regex = "1" +once_cell = "1" \ No newline at end of file diff --git a/oracle/graphql-api/Dockerfile b/oracle/graphql-api/Dockerfile new file mode 100644 index 0000000..c7a7755 --- /dev/null +++ b/oracle/graphql-api/Dockerfile @@ -0,0 +1,129 @@ +# ----------------------------------------------------------------------------- +# 1. Global Build Arguments & Metadata +# ----------------------------------------------------------------------------- +ARG RUST_VERSION=1.84 +ARG DEBIAN_VERSION=bookworm +ARG CHEF_IMAGE=lukemathwalker/cargo-chef:latest-rust-${RUST_VERSION}-slim-${DEBIAN_VERSION} +ARG RUNTIME_BASE=gcr.io/distroless/cc-debian12:nonroot + +ARG TINI_VERSION=v0.19.0 +ARG TINI_SHA256=c5b0666b4cb676901f90dfcb37106783c5fe2077b04590973b885950611b30ee + +ARG BUILD_VERSION="0.1.0" +ARG VCS_REF="development" +ARG BUILD_DATE="2023-11-02" + +# ----------------------------------------------------------------------------- +# 2. Cargo Chef Stage - Prepare Dependency Recipe +# ----------------------------------------------------------------------------- +FROM ${CHEF_IMAGE} AS chef +WORKDIR /app +COPY . . +RUN cargo chef prepare --recipe-path recipe.json + +# ----------------------------------------------------------------------------- +# 3. Builder Stage - Compile Rust with Cached Dependencies +# ----------------------------------------------------------------------------- +FROM ${CHEF_IMAGE} AS builder + +ARG CARGO_BUILD_JOBS="default" +ARG CARGO_PROFILE=release +ENV RUSTFLAGS="-C opt-level=3 -C codegen-units=1" +# Explicitly set C++ compiler path +ENV CXX=/usr/bin/g++ +ENV CC=/usr/bin/gcc + +# Install build dependencies with proper C++ compiler setup +RUN apt-get update && apt-get install -y --no-install-recommends \ + pkg-config \ + libssl-dev \ + protobuf-compiler \ + g++ \ + build-essential \ + cmake \ + make \ + python3 \ + libsasl2-dev \ + zlib1g-dev \ + binutils \ + ca-certificates \ + && rm -rf /var/lib/apt/lists/* \ + && ln -sf /usr/bin/g++ /usr/bin/c++ \ + && ls -la /usr/bin/g++ /usr/bin/c++ \ + && g++ --version + +WORKDIR /app + +# Cook dependencies separately +COPY --from=chef /app/recipe.json recipe.json +RUN CXX=/usr/bin/g++ CC=/usr/bin/gcc cargo chef cook --profile ${CARGO_PROFILE} --recipe-path recipe.json + +# Build the application +COPY . . +RUN cargo build --profile ${CARGO_PROFILE} --jobs "${CARGO_BUILD_JOBS}" \ + && ls -la target/${CARGO_PROFILE}/ \ + && strip target/${CARGO_PROFILE}/qos_graphql_api \ + && cp target/${CARGO_PROFILE}/qos_graphql_api /app/graphql_api + +# ----------------------------------------------------------------------------- +# 4. Preparation Stage (Tini + Directory Setup) +# ----------------------------------------------------------------------------- +FROM debian:${DEBIAN_VERSION}-slim AS preparation + +ARG TINI_VERSION +ARG TINI_SHA256 + +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates curl \ + && rm -rf /var/lib/apt/lists/* + +# Download Tini and verify its checksum +RUN curl -fsSL -o /tini \ + https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini-static-amd64 \ + && echo "${TINI_SHA256} /tini" | sha256sum --check \ + && chmod +x /tini + +# Create log directory (owned by non-root UID 65532) +RUN mkdir -p /var/log/graphql_api \ + && chown 65532:65532 /var/log/graphql_api + +# ----------------------------------------------------------------------------- +# 5. Final Stage - Minimal Distroless Image +# ----------------------------------------------------------------------------- +FROM ${RUNTIME_BASE} + +COPY --from=preparation /tini /usr/local/bin/tini +COPY --from=builder /app/graphql_api /usr/local/bin/graphql_api +COPY --from=preparation /var/log/graphql_api /var/log/graphql_api + +ENV TZ=UTC \ + LANG=C.UTF-8 \ + SSL_CERT_DIR=/etc/ssl/certs \ + RUST_BACKTRACE=1 + +WORKDIR / +STOPSIGNAL SIGTERM + +# Healthcheck for API +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD ["/usr/local/bin/graphql_api", "--health-check"] || exit 1 + +# Expose GraphQL port +EXPOSE 8000 + +# Distroless:nonroot automatically runs as UID 65532 +USER nonroot + +ENTRYPOINT ["/usr/local/bin/tini", "--", "/usr/local/bin/graphql_api"] + +# ----------------------------------------------------------------------------- +# 6. OCI Labels for Metadata +# ----------------------------------------------------------------------------- +LABEL org.opencontainers.image.version="${BUILD_VERSION}" \ + org.opencontainers.image.revision="${VCS_REF}" \ + org.opencontainers.image.created="${BUILD_DATE}" \ + org.opencontainers.image.source="https://github.com/graphops/qos-oracle" \ + org.opencontainers.image.description="The Graph QoS Oracle GraphQL API" \ + org.opencontainers.image.vendor="GraphOps" \ + org.opencontainers.image.title="QoS GraphQL API" \ + maintainer="GraphOps " diff --git a/oracle/graphql-api/src/main.rs b/oracle/graphql-api/src/main.rs new file mode 100644 index 0000000..54f17bf --- /dev/null +++ b/oracle/graphql-api/src/main.rs @@ -0,0 +1,1390 @@ +use actix_web::{web, App, HttpResponse, HttpServer, Result}; +use async_graphql::{ + Context, EmptyMutation, EmptySubscription, Enum, InputObject, Object, Schema, SimpleObject, +}; +use async_graphql_actix_web::{GraphQLRequest, GraphQLResponse}; +use chrono::{DateTime, TimeZone, Utc}; +use clickhouse::{Client, Row}; +use once_cell::sync::Lazy; +use regex::Regex; +use serde::Deserialize; +use std::env; +use tokio::signal::unix::{signal, SignalKind}; + +// --- Input Validation Regexes --- +// Define allowed characters/patterns. Adjust these based on actual expected formats. +static GATEWAY_ID_REGEX: Lazy = Lazy::new(|| Regex::new(r"^[a-zA-Z0-9_-]+$").unwrap()); +// Allow alphanumeric, hyphens, slashes (e.g., for ENS names/subgraph names) +static SUBGRAPH_REGEX: Lazy = Lazy::new(|| Regex::new(r"^[a-zA-Z0-9_/-]+$").unwrap()); +// Allow hex characters, optionally prefixed with 0x +static INDEXER_REGEX: Lazy = Lazy::new(|| Regex::new(r"^(0x)?[a-fA-F0-9]+$").unwrap()); + +// --- ClickHouse String Escaping --- +/// Escapes a string for safe inclusion within single quotes in a ClickHouse SQL query. +/// Replaces backslashes (`\`) with `\\` and single quotes (`'`) with `\'`. +fn escape_clickhouse_string_literal(input: &str) -> String { + input.replace('\\', r"\\").replace('\'', r"\'") +} + +// --- Time Parsing Helper --- +/// Parses a string as either a Unix timestamp (seconds) or an RFC3339 datetime. +fn parse_datetime_input(input: &str) -> Result, String> { + // Try parsing as Unix timestamp (integer seconds) first + if let Ok(ts) = input.parse::() { + match Utc.timestamp_opt(ts, 0).single() { + Some(dt) => Ok(dt), + None => Err(format!("Invalid Unix timestamp value: {}", ts)), + } + } else { + // Fallback to parsing as RFC3339 + DateTime::parse_from_rfc3339(input) + .map(|dt| dt.with_timezone(&Utc)) + .map_err(|e| { + format!( + "Invalid format: Expected Unix timestamp or RFC3339. Parse error: {}", + e + ) + }) + } +} + +// --- Structs for Aggregated Data --- + +#[derive(Enum, Copy, Clone, Eq, PartialEq, Debug)] +enum AggregationInterval { + FiveMinutes, + Hourly, + Daily, +} + +impl AggregationInterval { + fn table_suffix(&self) -> &'static str { + match self { + AggregationInterval::FiveMinutes => "5min", + AggregationInterval::Hourly => "hourly", + AggregationInterval::Daily => "daily", + } + } +} + +// --- Deployment Aggregation Structs --- + +#[derive(Row, Deserialize, Debug, Clone, SimpleObject)] +struct DeploymentAggregationRow { + time_bucket: u32, + subgraph: String, + gateway_id: String, + query_count: u64, + success_count: u64, + failure_count: u64, + avg_response_time_ms: f64, + max_response_time_ms: u32, + p90_response_time_ms: f64, + p99_response_time_ms: f64, + stddev_response_time_ms: f64, + total_fees_usd: f64, + avg_fee_usd: f64, + max_fee_usd: f64, + p90_fee_usd: f64, + p99_fee_usd: f64, + stddev_fee_usd: f64, + success_proportion: f64, +} + +#[derive(SimpleObject, Debug, Clone)] +struct DeploymentAggregationOutput { + time_bucket: String, + subgraph: String, + gateway_id: String, + query_count: u64, + success_count: u64, + failure_count: u64, + // Latency + #[graphql(name = "avgResponseTimeMs")] + avg_response_time_ms: Option, + #[graphql(name = "maxResponseTimeMs")] + max_response_time_ms: Option, + #[graphql(name = "p90ResponseTimeMs")] + p90_response_time_ms: Option, + #[graphql(name = "p99ResponseTimeMs")] + p99_response_time_ms: Option, + #[graphql(name = "stddevResponseTimeMs")] + stddev_response_time_ms: Option, + // Fees + #[graphql(name = "totalFeesUsd")] + total_fees_usd: Option, + #[graphql(name = "avgFeeUsd")] + avg_fee_usd: Option, + #[graphql(name = "maxFeeUsd")] + max_fee_usd: Option, + #[graphql(name = "p90FeeUsd")] + p90_fee_usd: Option, + #[graphql(name = "p99FeeUsd")] + p99_fee_usd: Option, + #[graphql(name = "stddevFeeUsd")] + stddev_fee_usd: Option, + // Success + #[graphql(name = "successProportion")] + success_proportion: Option, +} + +// --- Indexer Aggregation Structs (Updated) --- + +#[derive(Row, Deserialize, Debug, Clone, SimpleObject)] +struct IndexerAggregationRow { + time_bucket: u32, + indexer: String, + gateway_id: String, + query_count: u64, + success_count: u64, + failure_count: u64, + avg_indexer_response_time_ms: f64, + max_indexer_response_time_ms: u32, + p90_indexer_response_time_ms: f64, + p99_indexer_response_time_ms: f64, + stddev_indexer_response_time_ms: f64, + total_fee_grt: f64, + avg_fee_grt: f64, + max_fee_grt: f64, + p90_fee_grt: f64, + p99_fee_grt: f64, + stddev_fee_grt: f64, + avg_seconds_behind: f64, + max_seconds_behind: u32, + p90_seconds_behind: f64, + p99_seconds_behind: f64, + stddev_seconds_behind: f64, + avg_blocks_behind: f64, + max_blocks_behind: u64, + p90_blocks_behind: f64, + p99_blocks_behind: f64, + stddev_blocks_behind: f64, + success_proportion: f64, +} + +#[derive(SimpleObject, Debug, Clone)] +#[graphql(name = "IndexerAggregation")] // Keep existing name if desired +struct IndexerAggregationOutput { + time_bucket: String, + indexer: String, + gateway_id: String, + query_count: u64, + success_count: u64, + failure_count: u64, + // Latency + #[graphql(name = "avgIndexerResponseTimeMs")] + avg_indexer_response_time_ms: Option, + #[graphql(name = "maxIndexerResponseTimeMs")] + max_indexer_response_time_ms: Option, + #[graphql(name = "p90IndexerResponseTimeMs")] + p90_indexer_response_time_ms: Option, + #[graphql(name = "p99IndexerResponseTimeMs")] + p99_indexer_response_time_ms: Option, + #[graphql(name = "stddevIndexerResponseTimeMs")] + stddev_indexer_response_time_ms: Option, + // Fees + #[graphql(name = "totalFeeGrt")] + total_fee_grt: Option, + #[graphql(name = "avgFeeGrt")] + avg_fee_grt: Option, + #[graphql(name = "maxFeeGrt")] + max_fee_grt: Option, + #[graphql(name = "p90FeeGrt")] + p90_fee_grt: Option, + #[graphql(name = "p99FeeGrt")] + p99_fee_grt: Option, + #[graphql(name = "stddevFeeGrt")] + stddev_fee_grt: Option, + // Behindness + #[graphql(name = "avgSecondsBehind")] + avg_seconds_behind: Option, + #[graphql(name = "maxSecondsBehind")] + max_seconds_behind: Option, + #[graphql(name = "p90SecondsBehind")] + p90_seconds_behind: Option, + #[graphql(name = "p99SecondsBehind")] + p99_seconds_behind: Option, + #[graphql(name = "stddevSecondsBehind")] + stddev_seconds_behind: Option, + #[graphql(name = "avgBlocksBehind")] + avg_blocks_behind: Option, + #[graphql(name = "maxBlocksBehind")] + max_blocks_behind: Option, + #[graphql(name = "p90BlocksBehind")] + p90_blocks_behind: Option, + #[graphql(name = "p99BlocksBehind")] + p99_blocks_behind: Option, + #[graphql(name = "stddevBlocksBehind")] + stddev_blocks_behind: Option, + // Success + #[graphql(name = "successProportion")] + success_proportion: Option, +} + +// --- Allocation Aggregation Structs --- + +#[derive(Row, Deserialize, Debug, Clone, SimpleObject)] +struct AllocationAggregationRow { + time_bucket: u32, + subgraph: String, + indexer: String, + gateway_id: String, + query_count: u64, + success_count: u64, + failure_count: u64, + avg_indexer_response_time_ms: f64, + max_indexer_response_time_ms: u32, + p90_indexer_response_time_ms: f64, + p99_indexer_response_time_ms: f64, + stddev_indexer_response_time_ms: f64, + total_fee_grt: f64, + avg_fee_grt: f64, + max_fee_grt: f64, + p90_fee_grt: f64, + p99_fee_grt: f64, + stddev_fee_grt: f64, + avg_seconds_behind: f64, + max_seconds_behind: u32, + p90_seconds_behind: f64, + p99_seconds_behind: f64, + stddev_seconds_behind: f64, + avg_blocks_behind: f64, + max_blocks_behind: u64, + p90_blocks_behind: f64, + p99_blocks_behind: f64, + stddev_blocks_behind: f64, + success_proportion: f64, +} + +#[derive(SimpleObject, Debug, Clone)] +struct AllocationAggregationOutput { + time_bucket: String, + subgraph: String, + indexer: String, + gateway_id: String, + query_count: u64, + success_count: u64, + failure_count: u64, + // Latency + #[graphql(name = "avgIndexerResponseTimeMs")] + avg_indexer_response_time_ms: Option, + #[graphql(name = "maxIndexerResponseTimeMs")] + max_indexer_response_time_ms: Option, + #[graphql(name = "p90IndexerResponseTimeMs")] + p90_indexer_response_time_ms: Option, + #[graphql(name = "p99IndexerResponseTimeMs")] + p99_indexer_response_time_ms: Option, + #[graphql(name = "stddevIndexerResponseTimeMs")] + stddev_indexer_response_time_ms: Option, + // Fees + #[graphql(name = "totalFeeGrt")] + total_fee_grt: Option, + #[graphql(name = "avgFeeGrt")] + avg_fee_grt: Option, + #[graphql(name = "maxFeeGrt")] + max_fee_grt: Option, + #[graphql(name = "p90FeeGrt")] + p90_fee_grt: Option, + #[graphql(name = "p99FeeGrt")] + p99_fee_grt: Option, + #[graphql(name = "stddevFeeGrt")] + stddev_fee_grt: Option, + // Behindness + #[graphql(name = "avgSecondsBehind")] + avg_seconds_behind: Option, + #[graphql(name = "maxSecondsBehind")] + max_seconds_behind: Option, + #[graphql(name = "p90SecondsBehind")] + p90_seconds_behind: Option, + #[graphql(name = "p99SecondsBehind")] + p99_seconds_behind: Option, + #[graphql(name = "stddevSecondsBehind")] + stddev_seconds_behind: Option, + #[graphql(name = "avgBlocksBehind")] + avg_blocks_behind: Option, + #[graphql(name = "maxBlocksBehind")] + max_blocks_behind: Option, + #[graphql(name = "p90BlocksBehind")] + p90_blocks_behind: Option, + #[graphql(name = "p99BlocksBehind")] + p99_blocks_behind: Option, + #[graphql(name = "stddevBlocksBehind")] + stddev_blocks_behind: Option, + // Success + #[graphql(name = "successProportion")] + success_proportion: Option, +} + +// --- Input Objects for Filtering --- + +#[derive(InputObject, Debug, Clone)] +struct TimeRangeInput { + from: Option, + to: Option, +} + +#[derive(InputObject, Debug, Clone)] +struct AggregationFilterInput { + /// Optional: Filter by gateway ID + gateway_id: Option, + /// Optional: Filter by subgraph ID (deployment ID) + subgraph: Option, + /// Optional: Filter by indexer ID (HEX encoded) + indexer: Option, + /// Optional: Filter by allocation ID (HEX encoded) + allocation: Option, +} + +// --- Enums and Structs for Sorting --- + +#[derive(Enum, Copy, Clone, Eq, PartialEq, Debug)] +enum SortDirection { + Asc, + Desc, +} + +// Define which fields can be sorted for Deployments +#[derive(Enum, Copy, Clone, Eq, PartialEq, Debug)] +enum DeploymentSortField { + TimeBucket, + Subgraph, + GatewayId, + QueryCount, + SuccessCount, + FailureCount, + AvgResponseTimeMs, + MaxResponseTimeMs, + P90ResponseTimeMs, + P99ResponseTimeMs, + TotalFeesUsd, + AvgFeeUsd, + MaxFeeUsd, + P90FeeUsd, + P99FeeUsd, + SuccessProportion, +} + +impl DeploymentSortField { + // Helper to get the corresponding ClickHouse column name + fn column_name(&self) -> &'static str { + match self { + DeploymentSortField::TimeBucket => "time_bucket", + DeploymentSortField::Subgraph => "subgraph", + DeploymentSortField::GatewayId => "gateway_id", + DeploymentSortField::QueryCount => "query_count", + DeploymentSortField::SuccessCount => "success_count", + DeploymentSortField::FailureCount => "failure_count", + DeploymentSortField::AvgResponseTimeMs => "avg_response_time_ms", + DeploymentSortField::MaxResponseTimeMs => "max_response_time_ms", + DeploymentSortField::P90ResponseTimeMs => "p90_response_time_ms", + DeploymentSortField::P99ResponseTimeMs => "p99_response_time_ms", + DeploymentSortField::TotalFeesUsd => "total_fees_usd", + DeploymentSortField::AvgFeeUsd => "avg_fee_usd", + DeploymentSortField::MaxFeeUsd => "max_fee_usd", + DeploymentSortField::P90FeeUsd => "p90_fee_usd", + DeploymentSortField::P99FeeUsd => "p99_fee_usd", + DeploymentSortField::SuccessProportion => "success_proportion", + } + } +} + +// Define which fields can be sorted for Indexers +#[derive(Enum, Copy, Clone, Eq, PartialEq, Debug)] +enum IndexerSortField { + TimeBucket, + Indexer, + GatewayId, + QueryCount, + SuccessCount, + FailureCount, + AvgIndexerResponseTimeMs, + MaxIndexerResponseTimeMs, + P90IndexerResponseTimeMs, + P99IndexerResponseTimeMs, + TotalFeeGrt, + AvgFeeGrt, + MaxFeeGrt, + P90FeeGrt, + P99FeeGrt, + AvgSecondsBehind, + MaxSecondsBehind, + P90SecondsBehind, + P99SecondsBehind, + AvgBlocksBehind, + MaxBlocksBehind, + P90BlocksBehind, + P99BlocksBehind, + SuccessProportion, +} + +impl IndexerSortField { + // Helper to get the corresponding ClickHouse column name + fn column_name(&self) -> &'static str { + match self { + IndexerSortField::TimeBucket => "time_bucket", + IndexerSortField::Indexer => "indexer", + IndexerSortField::GatewayId => "gateway_id", + IndexerSortField::QueryCount => "query_count", + IndexerSortField::SuccessCount => "success_count", + IndexerSortField::FailureCount => "failure_count", + IndexerSortField::AvgIndexerResponseTimeMs => "avg_indexer_response_time_ms", + IndexerSortField::MaxIndexerResponseTimeMs => "max_indexer_response_time_ms", + IndexerSortField::P90IndexerResponseTimeMs => "p90_indexer_response_time_ms", + IndexerSortField::P99IndexerResponseTimeMs => "p99_indexer_response_time_ms", + IndexerSortField::TotalFeeGrt => "total_fee_grt", + IndexerSortField::AvgFeeGrt => "avg_fee_grt", + IndexerSortField::MaxFeeGrt => "max_fee_grt", + IndexerSortField::P90FeeGrt => "p90_fee_grt", + IndexerSortField::P99FeeGrt => "p99_fee_grt", + IndexerSortField::AvgSecondsBehind => "avg_seconds_behind", + IndexerSortField::MaxSecondsBehind => "max_seconds_behind", + IndexerSortField::P90SecondsBehind => "p90_seconds_behind", + IndexerSortField::P99SecondsBehind => "p99_seconds_behind", + IndexerSortField::AvgBlocksBehind => "avg_blocks_behind", + IndexerSortField::MaxBlocksBehind => "max_blocks_behind", + IndexerSortField::P90BlocksBehind => "p90_blocks_behind", + IndexerSortField::P99BlocksBehind => "p99_blocks_behind", + IndexerSortField::SuccessProportion => "success_proportion", + } + } +} + +// Define which fields can be sorted for Allocations +#[derive(Enum, Copy, Clone, Eq, PartialEq, Debug)] +enum AllocationSortField { + TimeBucket, + Subgraph, + Indexer, + GatewayId, + QueryCount, + SuccessCount, + FailureCount, + AvgIndexerResponseTimeMs, + MaxIndexerResponseTimeMs, + P90IndexerResponseTimeMs, + P99IndexerResponseTimeMs, + TotalFeeGrt, + AvgFeeGrt, + MaxFeeGrt, + P90FeeGrt, + P99FeeGrt, + AvgSecondsBehind, + MaxSecondsBehind, + P90SecondsBehind, + P99SecondsBehind, + AvgBlocksBehind, + MaxBlocksBehind, + P90BlocksBehind, + P99BlocksBehind, + SuccessProportion, +} + +impl AllocationSortField { + // Helper to get the corresponding ClickHouse column name + fn column_name(&self) -> &'static str { + match self { + AllocationSortField::TimeBucket => "time_bucket", + AllocationSortField::Subgraph => "subgraph", + AllocationSortField::Indexer => "indexer", + AllocationSortField::GatewayId => "gateway_id", + AllocationSortField::QueryCount => "query_count", + AllocationSortField::SuccessCount => "success_count", + AllocationSortField::FailureCount => "failure_count", + AllocationSortField::AvgIndexerResponseTimeMs => "avg_indexer_response_time_ms", + AllocationSortField::MaxIndexerResponseTimeMs => "max_indexer_response_time_ms", + AllocationSortField::P90IndexerResponseTimeMs => "p90_indexer_response_time_ms", + AllocationSortField::P99IndexerResponseTimeMs => "p99_indexer_response_time_ms", + AllocationSortField::TotalFeeGrt => "total_fee_grt", + AllocationSortField::AvgFeeGrt => "avg_fee_grt", + AllocationSortField::MaxFeeGrt => "max_fee_grt", + AllocationSortField::P90FeeGrt => "p90_fee_grt", + AllocationSortField::P99FeeGrt => "p99_fee_grt", + AllocationSortField::AvgSecondsBehind => "avg_seconds_behind", + AllocationSortField::MaxSecondsBehind => "max_seconds_behind", + AllocationSortField::P90SecondsBehind => "p90_seconds_behind", + AllocationSortField::P99SecondsBehind => "p99_seconds_behind", + AllocationSortField::AvgBlocksBehind => "avg_blocks_behind", + AllocationSortField::MaxBlocksBehind => "max_blocks_behind", + AllocationSortField::P90BlocksBehind => "p90_blocks_behind", + AllocationSortField::P99BlocksBehind => "p99_blocks_behind", + AllocationSortField::SuccessProportion => "success_proportion", + } + } +} + +// Input object for specifying sorting - Generic enough for all types? +// Let's make specific ones for type safety in the resolver signature. + +#[derive(InputObject, Debug)] +struct DeploymentSortInput { + field: DeploymentSortField, + direction: Option, // Default to Desc +} + +#[derive(InputObject, Debug)] +struct IndexerSortInput { + field: IndexerSortField, + direction: Option, // Default to Desc +} + +#[derive(InputObject, Debug)] +struct AllocationSortInput { + field: AllocationSortField, + direction: Option, // Default to Desc +} + +// --- GraphQL Query Root --- + +struct QueryRoot; + +#[Object] +impl QueryRoot { + /// Query for Deployment level aggregations + async fn deployment_aggregations( + &self, + _ctx: &Context<'_>, + #[graphql(desc = "Aggregation interval (default: Hourly)")] interval: Option< + AggregationInterval, + >, + #[graphql( + desc = "Optional time range filter. If omitted, defaults to the last 24 hours. \ + If provided, requires at least 'from' or 'to'." + )] + time_range: Option, + #[graphql(desc = "Optional filters for the query")] filter: Option, + #[graphql(desc = "Maximum number of records to return (default 1000, max 10000)")] + limit: Option, + #[graphql(desc = "Optional sorting (default: time_bucket DESC)")] sort: Option< + DeploymentSortInput, + >, + ) -> Result, String> { + let client = get_clickhouse_client()?; + let actual_interval = interval.unwrap_or(AggregationInterval::Hourly); + let view_name = format!("view_agg_deployment_{}", actual_interval.table_suffix()); + + // --- Build WHERE clause --- + let mut conditions = Vec::new(); + + // --- Handle Time Range --- + match time_range { + // Case 1: time_range argument is completely omitted - use default + None => { + let now = Utc::now(); + let default_from = now - chrono::Duration::hours(24); + conditions.push(format!( + "time_bucket >= toDateTime({})", + default_from.timestamp() + )); + conditions.push(format!("time_bucket < toDateTime({})", now.timestamp())); + } + // Case 2: time_range argument is provided + Some(tr) => { + let parsed_from = tr.from.as_deref().map(parse_datetime_input).transpose()?; + let parsed_to = tr.to.as_deref().map(parse_datetime_input).transpose()?; + + match (parsed_from, parsed_to) { + (None, None) => { + // If time_range was provided but both fields are null/missing + return Err( + "Time range filter requires at least 'from' or 'to' to be specified." + .to_string(), + ); + } + (Some(from_dt), None) => { + conditions.push(format!( + "time_bucket >= toDateTime({})", + from_dt.timestamp() + )); + } + (None, Some(to_dt)) => { + conditions.push(format!("time_bucket < toDateTime({})", to_dt.timestamp())); + } + (Some(from_dt), Some(to_dt)) => { + if from_dt >= to_dt { + return Err("'from' time must be earlier than 'to' time.".to_string()); + } + conditions.push(format!( + "time_bucket >= toDateTime({})", + from_dt.timestamp() + )); + conditions.push(format!("time_bucket < toDateTime({})", to_dt.timestamp())); + } + } + } + } + // --- End Handle Time Range --- + + // --- Handle Other Filters (gateway_id, subgraph) --- + if let Some(f) = filter { + if let Some(gw) = &f.gateway_id { + // --- Validate and Escape gateway_id --- + if !GATEWAY_ID_REGEX.is_match(gw) { + return Err("Invalid format for gateway_id filter".to_string()); + } + let escaped_gw = escape_clickhouse_string_literal(gw); + conditions.push(format!("gateway_id = '{}'", escaped_gw)); + // --- End Validation --- + } + if let Some(sg) = &f.subgraph { + // --- Validate and Escape subgraph --- + if !SUBGRAPH_REGEX.is_match(sg) { + return Err("Invalid format for subgraph filter".to_string()); + } + let escaped_sg = escape_clickhouse_string_literal(sg); + conditions.push(format!("subgraph = '{}'", escaped_sg)); + // --- End Validation --- + } + // No indexer filter applicable here + } + // --- End Handle Other Filters --- + + // Should always have at least one time condition now + let where_clause = format!("WHERE {}", conditions.join(" AND ")); + let query_limit = limit.unwrap_or(1000).clamp(1, 10000); + + // --- Build ORDER BY Clause --- + let order_by_clause = match sort { + Some(s) => { + let direction = match s.direction.unwrap_or(SortDirection::Desc) { + SortDirection::Asc => "ASC", + SortDirection::Desc => "DESC", + }; + format!("ORDER BY {} {}", s.field.column_name(), direction) + } + None => "ORDER BY time_bucket DESC, subgraph ASC, gateway_id ASC".to_string(), + }; + + // --- Build and Execute Query --- + let query = format!( + "SELECT time_bucket, subgraph, gateway_id, \ + query_count, success_count, failure_count, \ + avg_response_time_ms, max_response_time_ms, p90_response_time_ms, p99_response_time_ms, stddev_response_time_ms, \ + total_fees_usd, avg_fee_usd, max_fee_usd, p90_fee_usd, p99_fee_usd, stddev_fee_usd, \ + success_proportion \ + FROM {} {} \ + {} \ + LIMIT {}", + view_name, where_clause, order_by_clause, query_limit + ); + + println!("Executing query: {}", query); + + let rows = client + .query(&query) + .fetch_all::() + .await + .map_err(|e| format!("Database query failed: {}", e))?; + + // Map results (logic remains the same) + Ok(rows + .into_iter() + .map(|row| DeploymentAggregationOutput { + time_bucket: Utc + .timestamp_opt(row.time_bucket as i64, 0) + .single() + .map_or_else(|| "Invalid Timestamp".to_string(), |dt| dt.to_rfc3339()), + subgraph: row.subgraph, + gateway_id: row.gateway_id, + query_count: row.query_count, + success_count: row.success_count, + failure_count: row.failure_count, + avg_response_time_ms: if row.avg_response_time_ms.is_finite() { + Some(row.avg_response_time_ms) + } else { + None + }, + max_response_time_ms: Some(row.max_response_time_ms), + p90_response_time_ms: if row.p90_response_time_ms.is_finite() { + Some(row.p90_response_time_ms) + } else { + None + }, + p99_response_time_ms: if row.p99_response_time_ms.is_finite() { + Some(row.p99_response_time_ms) + } else { + None + }, + stddev_response_time_ms: if row.stddev_response_time_ms.is_finite() { + Some(row.stddev_response_time_ms) + } else { + None + }, + total_fees_usd: if row.total_fees_usd.is_finite() { + Some(row.total_fees_usd) + } else { + None + }, + avg_fee_usd: if row.avg_fee_usd.is_finite() { + Some(row.avg_fee_usd) + } else { + None + }, + max_fee_usd: if row.max_fee_usd.is_finite() { + Some(row.max_fee_usd) + } else { + None + }, + p90_fee_usd: if row.p90_fee_usd.is_finite() { + Some(row.p90_fee_usd) + } else { + None + }, + p99_fee_usd: if row.p99_fee_usd.is_finite() { + Some(row.p99_fee_usd) + } else { + None + }, + stddev_fee_usd: if row.stddev_fee_usd.is_finite() { + Some(row.stddev_fee_usd) + } else { + None + }, + success_proportion: if row.success_proportion.is_finite() { + Some(row.success_proportion) + } else { + None + }, + }) + .collect()) + } + + /// Query for Indexer level aggregations + async fn indexer_aggregations( + &self, + _ctx: &Context<'_>, + #[graphql(desc = "Aggregation interval (default: Hourly)")] interval: Option< + AggregationInterval, + >, + #[graphql( + desc = "Optional time range filter. If omitted, defaults to the last 24 hours. \ + If provided, requires at least 'from' or 'to'." + )] + time_range: Option, + #[graphql(desc = "Optional filters for the query")] filter: Option, + #[graphql(desc = "Maximum number of records to return (default 1000, max 10000)")] + limit: Option, + #[graphql(desc = "Optional sorting (default: time_bucket DESC)")] sort: Option< + IndexerSortInput, + >, + ) -> Result, String> { + let client = get_clickhouse_client()?; + let actual_interval = interval.unwrap_or(AggregationInterval::Hourly); + let view_name = format!("view_agg_indexer_{}", actual_interval.table_suffix()); + + // --- Build WHERE clause --- + let mut conditions = Vec::new(); + + // --- Handle Time Range (Apply same logic as deployment_aggregations) --- + match time_range { + None => { + let now = Utc::now(); + let default_from = now - chrono::Duration::hours(24); + conditions.push(format!( + "time_bucket >= toDateTime({})", + default_from.timestamp() + )); + conditions.push(format!("time_bucket < toDateTime({})", now.timestamp())); + } + Some(tr) => { + let parsed_from = tr.from.as_deref().map(parse_datetime_input).transpose()?; + let parsed_to = tr.to.as_deref().map(parse_datetime_input).transpose()?; + + match (parsed_from, parsed_to) { + (None, None) => { + return Err( + "Time range filter requires at least 'from' or 'to' to be specified." + .to_string(), + ); + } + (Some(from_dt), None) => { + conditions.push(format!( + "time_bucket >= toDateTime({})", + from_dt.timestamp() + )); + } + (None, Some(to_dt)) => { + conditions.push(format!("time_bucket < toDateTime({})", to_dt.timestamp())); + } + (Some(from_dt), Some(to_dt)) => { + if from_dt >= to_dt { + return Err("'from' time must be earlier than 'to' time.".to_string()); + } + conditions.push(format!( + "time_bucket >= toDateTime({})", + from_dt.timestamp() + )); + conditions.push(format!("time_bucket < toDateTime({})", to_dt.timestamp())); + } + } + } + } + // --- End Handle Time Range --- + + // --- Handle Other Filters (gateway_id, indexer) --- + if let Some(f) = filter { + if let Some(gw) = &f.gateway_id { + // --- Validate and Escape gateway_id --- + if !GATEWAY_ID_REGEX.is_match(gw) { + return Err("Invalid format for gateway_id filter".to_string()); + } + let escaped_gw = escape_clickhouse_string_literal(gw); + conditions.push(format!("gateway_id = '{}'", escaped_gw)); + // --- End Validation --- + } + if let Some(ix) = &f.indexer { + // --- Validate and Escape indexer --- + if !INDEXER_REGEX.is_match(ix) { + return Err("Invalid format for indexer filter".to_string()); + } + let escaped_ix = escape_clickhouse_string_literal(ix); + conditions.push(format!("indexer = '{}'", escaped_ix)); + // --- End Validation --- + } + // No subgraph filter applicable here + } + // --- End Handle Other Filters --- + + let where_clause = format!("WHERE {}", conditions.join(" AND ")); + let query_limit = limit.unwrap_or(1000).clamp(1, 10000); + + // --- Build ORDER BY Clause (Using IndexerSortField) --- + let order_by_clause = match sort { + Some(s) => { + let direction = match s.direction.unwrap_or(SortDirection::Desc) { + SortDirection::Asc => "ASC", + SortDirection::Desc => "DESC", + }; + format!("ORDER BY {} {}", s.field.column_name(), direction) + } + None => "ORDER BY time_bucket DESC, indexer ASC, gateway_id ASC".to_string(), + }; + + // --- Build and Execute Query --- + let query = format!( + "SELECT time_bucket, indexer, gateway_id, \ + query_count, success_count, failure_count, \ + avg_indexer_response_time_ms, max_indexer_response_time_ms, \ + p90_indexer_response_time_ms, p99_indexer_response_time_ms, \ + stddev_indexer_response_time_ms, \ + total_fee_grt, avg_fee_grt, max_fee_grt, p90_fee_grt, p99_fee_grt, stddev_fee_grt, \ + avg_seconds_behind, max_seconds_behind, p90_seconds_behind, p99_seconds_behind, stddev_seconds_behind, \ + avg_blocks_behind, max_blocks_behind, p90_blocks_behind, p99_blocks_behind, stddev_blocks_behind, \ + success_proportion \ + FROM {} {} \ + {} \ + LIMIT {}", + view_name, where_clause, order_by_clause, query_limit + ); + + println!("Executing query: {}", query); + + let rows = client + .query(&query) + .fetch_all::() + .await + .map_err(|e| format!("Database query failed: {}", e))?; + + // Map results (logic remains the same) + Ok(rows + .into_iter() + .map(|row| IndexerAggregationOutput { + time_bucket: Utc + .timestamp_opt(row.time_bucket as i64, 0) + .single() + .map_or_else(|| "Invalid Timestamp".to_string(), |dt| dt.to_rfc3339()), + indexer: row.indexer, + gateway_id: row.gateway_id, + query_count: row.query_count, + success_count: row.success_count, + failure_count: row.failure_count, + avg_indexer_response_time_ms: if row.avg_indexer_response_time_ms.is_finite() { + Some(row.avg_indexer_response_time_ms) + } else { + None + }, + max_indexer_response_time_ms: Some(row.max_indexer_response_time_ms), + p90_indexer_response_time_ms: if row.p90_indexer_response_time_ms.is_finite() { + Some(row.p90_indexer_response_time_ms) + } else { + None + }, + p99_indexer_response_time_ms: if row.p99_indexer_response_time_ms.is_finite() { + Some(row.p99_indexer_response_time_ms) + } else { + None + }, + stddev_indexer_response_time_ms: if row.stddev_indexer_response_time_ms.is_finite() + { + Some(row.stddev_indexer_response_time_ms) + } else { + None + }, + total_fee_grt: if row.total_fee_grt.is_finite() { + Some(row.total_fee_grt) + } else { + None + }, + avg_fee_grt: if row.avg_fee_grt.is_finite() { + Some(row.avg_fee_grt) + } else { + None + }, + max_fee_grt: if row.max_fee_grt.is_finite() { + Some(row.max_fee_grt) + } else { + None + }, + p90_fee_grt: if row.p90_fee_grt.is_finite() { + Some(row.p90_fee_grt) + } else { + None + }, + p99_fee_grt: if row.p99_fee_grt.is_finite() { + Some(row.p99_fee_grt) + } else { + None + }, + stddev_fee_grt: if row.stddev_fee_grt.is_finite() { + Some(row.stddev_fee_grt) + } else { + None + }, + avg_seconds_behind: if row.avg_seconds_behind.is_finite() { + Some(row.avg_seconds_behind) + } else { + None + }, + max_seconds_behind: Some(row.max_seconds_behind), + p90_seconds_behind: if row.p90_seconds_behind.is_finite() { + Some(row.p90_seconds_behind) + } else { + None + }, + p99_seconds_behind: if row.p99_seconds_behind.is_finite() { + Some(row.p99_seconds_behind) + } else { + None + }, + stddev_seconds_behind: if row.stddev_seconds_behind.is_finite() { + Some(row.stddev_seconds_behind) + } else { + None + }, + avg_blocks_behind: if row.avg_blocks_behind.is_finite() { + Some(row.avg_blocks_behind) + } else { + None + }, + max_blocks_behind: Some(row.max_blocks_behind), + p90_blocks_behind: if row.p90_blocks_behind.is_finite() { + Some(row.p90_blocks_behind) + } else { + None + }, + p99_blocks_behind: if row.p99_blocks_behind.is_finite() { + Some(row.p99_blocks_behind) + } else { + None + }, + stddev_blocks_behind: if row.stddev_blocks_behind.is_finite() { + Some(row.stddev_blocks_behind) + } else { + None + }, + success_proportion: if row.success_proportion.is_finite() { + Some(row.success_proportion) + } else { + None + }, + }) + .collect()) + } + + /// Query for Allocation level aggregations (grouped by subgraph and indexer) + async fn allocation_aggregations( + &self, + _ctx: &Context<'_>, + #[graphql(desc = "Aggregation interval (default: Hourly)")] interval: Option< + AggregationInterval, + >, + #[graphql( + desc = "Optional time range filter. If omitted, defaults to the last 24 hours. \ + If provided, requires at least 'from' or 'to'." + )] + time_range: Option, + #[graphql(desc = "Optional filters for the query")] filter: Option, + #[graphql(desc = "Maximum number of records to return (default 1000, max 10000)")] + limit: Option, + #[graphql(desc = "Optional sorting (default: time_bucket DESC)")] sort: Option< + AllocationSortInput, + >, + ) -> Result, String> { + let client = get_clickhouse_client()?; + let actual_interval = interval.unwrap_or(AggregationInterval::Hourly); + let view_name = format!("view_agg_allocation_{}", actual_interval.table_suffix()); + + // --- Build WHERE clause --- + let mut conditions = Vec::new(); + + // --- Handle Time Range (Apply same logic as deployment_aggregations) --- + match time_range { + None => { + let now = Utc::now(); + let default_from = now - chrono::Duration::hours(24); + conditions.push(format!( + "time_bucket >= toDateTime({})", + default_from.timestamp() + )); + conditions.push(format!("time_bucket < toDateTime({})", now.timestamp())); + } + Some(tr) => { + let parsed_from = tr.from.as_deref().map(parse_datetime_input).transpose()?; + let parsed_to = tr.to.as_deref().map(parse_datetime_input).transpose()?; + + match (parsed_from, parsed_to) { + (None, None) => { + return Err( + "Time range filter requires at least 'from' or 'to' to be specified." + .to_string(), + ); + } + (Some(from_dt), None) => { + conditions.push(format!( + "time_bucket >= toDateTime({})", + from_dt.timestamp() + )); + } + (None, Some(to_dt)) => { + conditions.push(format!("time_bucket < toDateTime({})", to_dt.timestamp())); + } + (Some(from_dt), Some(to_dt)) => { + if from_dt >= to_dt { + return Err("'from' time must be earlier than 'to' time.".to_string()); + } + conditions.push(format!( + "time_bucket >= toDateTime({})", + from_dt.timestamp() + )); + conditions.push(format!("time_bucket < toDateTime({})", to_dt.timestamp())); + } + } + } + } + // --- End Handle Time Range --- + + // --- Handle Other Filters (gateway_id, subgraph, indexer) --- + if let Some(f) = filter { + if let Some(gw) = &f.gateway_id { + // --- Validate and Escape gateway_id --- + if !GATEWAY_ID_REGEX.is_match(gw) { + return Err("Invalid format for gateway_id filter".to_string()); + } + let escaped_gw = escape_clickhouse_string_literal(gw); + conditions.push(format!("gateway_id = '{}'", escaped_gw)); + // --- End Validation --- + } + if let Some(sg) = &f.subgraph { + // --- Validate and Escape subgraph --- + if !SUBGRAPH_REGEX.is_match(sg) { + return Err("Invalid format for subgraph filter".to_string()); + } + let escaped_sg = escape_clickhouse_string_literal(sg); + conditions.push(format!("subgraph = '{}'", escaped_sg)); + // --- End Validation --- + } + if let Some(ix) = &f.indexer { + // --- Validate and Escape indexer --- + if !INDEXER_REGEX.is_match(ix) { + return Err("Invalid format for indexer filter".to_string()); + } + let escaped_ix = escape_clickhouse_string_literal(ix); + conditions.push(format!("indexer = '{}'", escaped_ix)); + // --- End Validation --- + } + } + // --- End Handle Other Filters --- + + let where_clause = format!("WHERE {}", conditions.join(" AND ")); + let query_limit = limit.unwrap_or(1000).clamp(1, 10000); + + // --- Build ORDER BY Clause (Using AllocationSortField) --- + let order_by_clause = match sort { + Some(s) => { + let direction = match s.direction.unwrap_or(SortDirection::Desc) { + SortDirection::Asc => "ASC", + SortDirection::Desc => "DESC", + }; + format!("ORDER BY {} {}", s.field.column_name(), direction) + } + None => { + "ORDER BY time_bucket DESC, subgraph ASC, indexer ASC, gateway_id ASC".to_string() + } + }; + + // --- Build and Execute Query --- + let query = format!( + "SELECT time_bucket, subgraph, indexer, gateway_id, \ + query_count, success_count, failure_count, \ + avg_indexer_response_time_ms, max_indexer_response_time_ms, \ + p90_indexer_response_time_ms, p99_indexer_response_time_ms, \ + stddev_indexer_response_time_ms, \ + total_fee_grt, avg_fee_grt, max_fee_grt, p90_fee_grt, p99_fee_grt, stddev_fee_grt, \ + avg_seconds_behind, max_seconds_behind, p90_seconds_behind, p99_seconds_behind, stddev_seconds_behind, \ + avg_blocks_behind, max_blocks_behind, p90_blocks_behind, p99_blocks_behind, stddev_blocks_behind, \ + success_proportion \ + FROM {} {} \ + {} \ + LIMIT {}", + view_name, where_clause, order_by_clause, query_limit + ); + + println!("Executing query: {}", query); + + let rows = client + .query(&query) + .fetch_all::() + .await + .map_err(|e| format!("Database query failed: {}", e))?; + + // Map results (logic remains the same) + Ok(rows + .into_iter() + .map(|row| AllocationAggregationOutput { + time_bucket: Utc + .timestamp_opt(row.time_bucket as i64, 0) + .single() + .map_or_else(|| "Invalid Timestamp".to_string(), |dt| dt.to_rfc3339()), + subgraph: row.subgraph, + indexer: row.indexer, + gateway_id: row.gateway_id, + query_count: row.query_count, + success_count: row.success_count, + failure_count: row.failure_count, + avg_indexer_response_time_ms: if row.avg_indexer_response_time_ms.is_finite() { + Some(row.avg_indexer_response_time_ms) + } else { + None + }, + max_indexer_response_time_ms: Some(row.max_indexer_response_time_ms), + p90_indexer_response_time_ms: if row.p90_indexer_response_time_ms.is_finite() { + Some(row.p90_indexer_response_time_ms) + } else { + None + }, + p99_indexer_response_time_ms: if row.p99_indexer_response_time_ms.is_finite() { + Some(row.p99_indexer_response_time_ms) + } else { + None + }, + stddev_indexer_response_time_ms: if row.stddev_indexer_response_time_ms.is_finite() + { + Some(row.stddev_indexer_response_time_ms) + } else { + None + }, + total_fee_grt: if row.total_fee_grt.is_finite() { + Some(row.total_fee_grt) + } else { + None + }, + avg_fee_grt: if row.avg_fee_grt.is_finite() { + Some(row.avg_fee_grt) + } else { + None + }, + max_fee_grt: if row.max_fee_grt.is_finite() { + Some(row.max_fee_grt) + } else { + None + }, + p90_fee_grt: if row.p90_fee_grt.is_finite() { + Some(row.p90_fee_grt) + } else { + None + }, + p99_fee_grt: if row.p99_fee_grt.is_finite() { + Some(row.p99_fee_grt) + } else { + None + }, + stddev_fee_grt: if row.stddev_fee_grt.is_finite() { + Some(row.stddev_fee_grt) + } else { + None + }, + avg_seconds_behind: if row.avg_seconds_behind.is_finite() { + Some(row.avg_seconds_behind) + } else { + None + }, + max_seconds_behind: Some(row.max_seconds_behind), + p90_seconds_behind: if row.p90_seconds_behind.is_finite() { + Some(row.p90_seconds_behind) + } else { + None + }, + p99_seconds_behind: if row.p99_seconds_behind.is_finite() { + Some(row.p99_seconds_behind) + } else { + None + }, + stddev_seconds_behind: if row.stddev_seconds_behind.is_finite() { + Some(row.stddev_seconds_behind) + } else { + None + }, + avg_blocks_behind: if row.avg_blocks_behind.is_finite() { + Some(row.avg_blocks_behind) + } else { + None + }, + max_blocks_behind: Some(row.max_blocks_behind), + p90_blocks_behind: if row.p90_blocks_behind.is_finite() { + Some(row.p90_blocks_behind) + } else { + None + }, + p99_blocks_behind: if row.p99_blocks_behind.is_finite() { + Some(row.p99_blocks_behind) + } else { + None + }, + stddev_blocks_behind: if row.stddev_blocks_behind.is_finite() { + Some(row.stddev_blocks_behind) + } else { + None + }, + success_proportion: if row.success_proportion.is_finite() { + Some(row.success_proportion) + } else { + None + }, + }) + .collect()) + } +} + +// Helper function to create ClickHouse client +// Returns the configured client builder. Connection happens on first query. +fn get_clickhouse_client() -> Result { + Ok(Client::default() + .with_url(env::var("CLICKHOUSE_URL").unwrap_or_else(|_| "http://localhost:8123".into())) + .with_database(env::var("CLICKHOUSE_DB").unwrap_or_else(|_| "default".into())) + .with_user(env::var("CLICKHOUSE_USER").unwrap_or_else(|_| "graphql".into())) + .with_password( + env::var("CLICKHOUSE_PASSWORD").unwrap_or_else(|_| "graphql_password".into()), + )) + // Remove .try_into() and .map_err() +} + +// GraphQL handlers +async fn graphql_handler( + schema: web::Data>, + req: GraphQLRequest, +) -> GraphQLResponse { + schema.execute(req.into_inner()).await.into() +} + +async fn graphql_playground() -> Result { + Ok(HttpResponse::Ok() + .content_type("text/html; charset=utf-8") + .body(async_graphql::http::playground_source( + async_graphql::http::GraphQLPlaygroundConfig::new("/graphql"), + ))) +} + +// Health check endpoint +async fn health_check() -> HttpResponse { + // Basic check: Can we create a client builder? + let client_result = get_clickhouse_client(); + if client_result.is_err() { + // Log the actual error if needed + eprintln!( + "Health check failed (client config): {:?}", + client_result.err() + ); + return HttpResponse::InternalServerError().body("ClickHouse client configuration error"); + } + let client = client_result.unwrap(); // Safe unwrap after check above + + // More advanced check: Can we execute a simple query? + match client.query("SELECT 1").execute().await { + // Use execute() for simple queries + Ok(_) => HttpResponse::Ok().body("OK"), + Err(e) => { + eprintln!("Health check failed (query execution): {}", e); // Log the error + HttpResponse::ServiceUnavailable().body(format!("ClickHouse connection error: {}", e)) + } + } +} + +#[actix_web::main] +async fn main() -> std::io::Result<()> { + // Initialize logging (optional but recommended) + // You can use a simple logger like env_logger or tracing + // env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); + // Or using tracing if you prefer (ensure tracing/tracing-subscriber are deps) + tracing_subscriber::fmt() + .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) + .init(); + + // Initialize environment variables (e.g., from .env file if needed) + // dotenv::dotenv().ok(); + + let schema = Schema::build(QueryRoot, EmptyMutation, EmptySubscription).finish(); + + tracing::info!("Starting GraphQL API server..."); // Use tracing/log + + // --- Server Initialization --- + // Create the server instance but don't await .run() immediately + let server = HttpServer::new(move || { + App::new() + .app_data(web::Data::new(schema.clone())) + .route("/graphql", web::post().to(graphql_handler)) + .route("/", web::get().to(graphql_playground)) + .route("/health", web::get().to(health_check)) // Keep health check + }) + .bind("0.0.0.0:8000")? + .run(); // .run() returns a Server instance + // --- End Server Initialization --- + + // --- Graceful Shutdown Logic --- + // Get a handle to the server instance + let server_handle = server.handle(); + + // Spawn a separate task to listen for termination signals + tokio::spawn(async move { + let mut sigint = signal(SignalKind::interrupt()).expect("Failed to install SIGINT handler"); + let mut sigterm = + signal(SignalKind::terminate()).expect("Failed to install SIGTERM handler"); + + // Wait for either SIGINT or SIGTERM + tokio::select! { + _ = sigint.recv() => { + tracing::info!("SIGINT received, initiating graceful shutdown..."); + }, + _ = sigterm.recv() => { + tracing::info!("SIGTERM received, initiating graceful shutdown..."); + }, + }; + + // Initiate graceful shutdown using the server handle. + // stop(true) sends the stop signal gracefully. + // We await it to ensure the signal is processed. + server_handle.stop(true).await; + tracing::info!("Shutdown signal sent to Actix server."); + }); + // --- End Graceful Shutdown Logic --- + + tracing::info!("GraphQL server running at http://0.0.0.0:8000"); + tracing::info!("GraphQL playground available at http://localhost:8000"); // Updated log + + // Wait for the server to stop. + // This will block until the server is shut down, either normally + // or via the signal handler calling server_handle.stop(). + server.await?; + + tracing::info!("GraphQL server has stopped gracefully."); + + Ok(()) +} diff --git a/oracle/prometheus/prometheus.yml b/oracle/prometheus/prometheus.yml new file mode 100644 index 0000000..c973769 --- /dev/null +++ b/oracle/prometheus/prometheus.yml @@ -0,0 +1,11 @@ +global: + scrape_interval: 60s + +scrape_configs: + - job_name: 'redpanda' + static_configs: + - targets: ['redpanda:9644'] + + - job_name: 'clickhouse' + static_configs: + - targets: ['clickhouse-server:9363'] diff --git a/oracle/test-producer/Cargo.toml b/oracle/test-producer/Cargo.toml new file mode 100644 index 0000000..1698bd3 --- /dev/null +++ b/oracle/test-producer/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "qos_kafka_producer" +version = "0.1.0" +edition = "2021" +resolver = "2" + +[dependencies] +tokio = { version = "1", features = ["full"] } +rdkafka = { version = "0.28", features = ["tokio"] } +prost = "0.13.1" +uuid = { version = "1", features = ["v4"] } +rand = "0.8" +chrono = "0.4" +hex = "0.4" +once_cell = "1.19" +num_cpus = "1" + +[build-dependencies] +prost-build = "0.13.1" \ No newline at end of file diff --git a/oracle/test-producer/Dockerfile b/oracle/test-producer/Dockerfile new file mode 100644 index 0000000..0997030 --- /dev/null +++ b/oracle/test-producer/Dockerfile @@ -0,0 +1,145 @@ +# ----------------------------------------------------------------------------- +# 1. Global Build Arguments & Metadata +# ----------------------------------------------------------------------------- +ARG RUST_VERSION=1.84 +ARG DEBIAN_VERSION=bookworm +ARG CHEF_IMAGE=lukemathwalker/cargo-chef:latest-rust-${RUST_VERSION}-slim-${DEBIAN_VERSION} +ARG RUNTIME_BASE=debian:${DEBIAN_VERSION}-slim + +ARG TINI_VERSION=v0.19.0 +ARG TINI_SHA256=c5b0666b4cb676901f90dfcb37106783c5fe2077b04590973b885950611b30ee + +ARG BUILD_VERSION="0.1.0" +ARG VCS_REF="development" +ARG BUILD_DATE="2023-11-02" + +# ----------------------------------------------------------------------------- +# 2. Cargo Chef Stage - Prepare Dependency Recipe +# ----------------------------------------------------------------------------- +FROM ${CHEF_IMAGE} AS chef +WORKDIR /app +# Copy the test-producer code, not the entire context +COPY ./test-producer . +# Generate the Cargo.lock file first +RUN cargo update +RUN cargo chef prepare --recipe-path recipe.json + +# ----------------------------------------------------------------------------- +# 3. Builder Stage - Compile Rust with Cached Dependencies +# ----------------------------------------------------------------------------- +FROM ${CHEF_IMAGE} AS builder + +ARG CARGO_BUILD_JOBS="default" +ARG CARGO_PROFILE=release +ENV RUSTFLAGS="-C opt-level=3 -C codegen-units=1" +ENV CXX=/usr/bin/g++ +ENV CC=/usr/bin/gcc + +# Install build dependencies +RUN apt-get update && apt-get install -y --no-install-recommends \ + pkg-config \ + libssl-dev \ + protobuf-compiler \ + g++ \ + build-essential \ + cmake \ + make \ + python3 \ + libsasl2-dev \ + zlib1g-dev \ + binutils \ + ca-certificates \ + && rm -rf /var/lib/apt/lists/* \ + && ln -sf /usr/bin/g++ /usr/bin/c++ + +WORKDIR /app + +# Create the proper directory structure +RUN mkdir -p /app/clickhouse + +# Copy the actual schema.proto file from the project root +COPY ./clickhouse/schema.proto /app/clickhouse/schema.proto + +# Modify build.rs to use the local path +COPY ./test-producer/build.rs /app/build.rs.original +RUN sed 's|"../clickhouse/schema.proto"|"./clickhouse/schema.proto"|g' /app/build.rs.original > /app/build.rs.new + +# Cook dependencies separately +COPY --from=chef /app/recipe.json recipe.json +COPY --from=chef /app/Cargo.lock Cargo.lock +RUN CXX=/usr/bin/g++ CC=/usr/bin/gcc cargo chef cook --profile ${CARGO_PROFILE} --recipe-path recipe.json + +# Build the application +COPY ./test-producer . +# Replace the build.rs with our modified version +RUN cp /app/build.rs.new /app/build.rs + +# Verify the proto file exists before building +RUN ls -la /app/clickhouse/ && \ + cat /app/build.rs && \ + cargo build --profile ${CARGO_PROFILE} --jobs "${CARGO_BUILD_JOBS}" \ + && strip target/${CARGO_PROFILE}/qos_kafka_producer \ + && cp target/${CARGO_PROFILE}/qos_kafka_producer /app/qos_kafka_producer + +# ----------------------------------------------------------------------------- +# 4. Preparation Stage (Tini + Directory Setup) +# ----------------------------------------------------------------------------- +FROM debian:${DEBIAN_VERSION}-slim AS preparation + +ARG TINI_VERSION +ARG TINI_SHA256 + +# Install required packages and clean up +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates curl zlib1g \ + && rm -rf /var/lib/apt/lists/* + +# Fetch and verify tini +RUN curl -fsSL -o /tini \ + https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini-static-amd64 \ + && echo "${TINI_SHA256} /tini" | sha256sum -c - \ + && chmod +x /tini + +# Create logging directory +RUN mkdir -p /var/log/qos_producer \ + && chown 65532:65532 /var/log/qos_producer + +# ----------------------------------------------------------------------------- +# 5. Final Stage - Minimal Runtime Image +# ----------------------------------------------------------------------------- +FROM ${RUNTIME_BASE} + +COPY --from=preparation /tini /usr/local/bin/tini +COPY --from=preparation /var/log/qos_producer /var/log/qos_producer +COPY --from=builder /app/qos_kafka_producer /usr/local/bin/qos_kafka_producer + +# Install required runtime dependencies +RUN apt-get update && apt-get install -y --no-install-recommends \ + zlib1g ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +ENV TZ=UTC \ + LANG=C.UTF-8 + +WORKDIR / +STOPSIGNAL SIGTERM + +# Set non-root user (same as distroless) +RUN groupadd --gid 65532 nonroot && \ + useradd --uid 65532 --gid 65532 -s /sbin/nologin nonroot + +USER nonroot + +ENTRYPOINT ["/usr/local/bin/tini", "--", "/usr/local/bin/qos_kafka_producer"] + +# ----------------------------------------------------------------------------- +# 6. OCI Labels for Metadata +# ----------------------------------------------------------------------------- +LABEL org.opencontainers.image.version="${BUILD_VERSION}" \ + org.opencontainers.image.revision="${VCS_REF}" \ + org.opencontainers.image.created="${BUILD_DATE}" \ + org.opencontainers.image.source="https://github.com/graphops/qos-oracle" \ + org.opencontainers.image.description="The Graph QoS Oracle Kafka Producer" \ + org.opencontainers.image.vendor="GraphOps" \ + org.opencontainers.image.title="QoS Kafka Producer" \ + maintainer="GraphOps " \ No newline at end of file diff --git a/oracle/test-producer/build.rs b/oracle/test-producer/build.rs new file mode 100644 index 0000000..2eae0bb --- /dev/null +++ b/oracle/test-producer/build.rs @@ -0,0 +1,8 @@ +fn main() { + // Specify the path to the schema.proto file + let proto_file = "./clickhouse/schema.proto"; + + println!("cargo:rerun-if-changed={}", proto_file); + + prost_build::compile_protos(&[proto_file], &["."]).unwrap(); +} diff --git a/oracle/test-producer/src/main.rs b/oracle/test-producer/src/main.rs new file mode 100644 index 0000000..502c3a0 --- /dev/null +++ b/oracle/test-producer/src/main.rs @@ -0,0 +1,615 @@ +use std::env; +use std::time::Duration; + +use hex; +use prost::Message; +use rand::Rng; +use rdkafka::config::ClientConfig; +use rdkafka::producer::{FutureProducer, FutureRecord}; +use rdkafka::util::get_rdkafka_version; +use tokio::time; +use once_cell::sync::Lazy; +use rand::seq::SliceRandom; +use num_cpus; + +// Mock types to match the gateway's dependencies +mod mock { + #[derive(Clone, Copy, PartialEq, Eq, Hash)] + pub struct DeploymentId(pub [u8; 32]); + + #[derive(Clone, Copy, PartialEq, Eq, Hash)] + pub struct IndexerId(pub [u8; 20]); + + #[derive(Clone)] + pub struct SubgraphId(pub String); + + #[derive(Clone, Copy, PartialEq, Eq, Hash)] + pub struct Address(pub [u8; 20]); + + impl std::ops::Deref for IndexerId { + type Target = Address; + fn deref(&self) -> &Self::Target { + unsafe { std::mem::transmute(self) } + } + } + + impl DeploymentId { + pub fn to_vec(&self) -> Vec { + self.0.to_vec() + } + } + + impl IndexerId { + pub fn to_vec(&self) -> Vec { + self.0.to_vec() + } + } + + impl SubgraphId { + pub fn to_string(&self) -> String { + self.0.clone() + } + } + + impl Address { + pub fn to_vec(&self) -> Vec { + self.0.to_vec() + } + } + + // Mock error types + pub enum Error { + QueryFailed(String), + } + + impl ToString for Error { + fn to_string(&self) -> String { + match self { + Error::QueryFailed(reason) => format!("Query failed: {}", reason), + } + } + } + + pub enum IndexerError { + NetworkError(String), + Timeout, + } + + impl ToString for IndexerError { + fn to_string(&self) -> String { + match self { + IndexerError::NetworkError(err) => format!("Network error: {}", err), + IndexerError::Timeout => "Timeout".to_string(), + } + } + } + + // Mock Receipt + pub struct Receipt { + allocation: Address, + value: u64, + } + + impl Receipt { + pub fn new(allocation: Address, value: u64) -> Self { + Self { allocation, value } + } + + pub fn allocation(&self) -> Address { + self.allocation + } + + pub fn value(&self) -> u64 { + self.value + } + } + + // Simplified IndexerResponse without attestation + pub struct IndexerResponse { + pub errors: Vec, + } +} + +use mock::*; + +// Protobuf definitions - exact copies from the gateway code +#[derive(prost::Message)] +pub struct ClientQueryProtobuf { + #[prost(string, tag = "1")] + pub gateway_id: String, + // 20 bytes + #[prost(bytes, tag = "2")] + pub receipt_signer: Vec, + #[prost(string, tag = "3")] + pub query_id: String, + #[prost(string, tag = "4")] + pub api_key: String, + #[prost(string, tag = "5")] + pub result: String, + #[prost(uint32, tag = "6")] + pub response_time_ms: u32, + #[prost(uint32, tag = "7")] + pub request_bytes: u32, + #[prost(uint32, optional, tag = "8")] + pub response_bytes: Option, + #[prost(double, tag = "9")] + pub total_fees_usd: f64, + #[prost(message, repeated, tag = "10")] + pub indexer_queries: Vec, + #[prost(string, tag = "11")] + pub user_id: String, + #[prost(string, optional, tag = "12")] + pub subgraph: Option, +} + +#[derive(prost::Message)] +pub struct IndexerQueryProtobuf { + /// 20 bytes + #[prost(bytes, tag = "1")] + pub indexer: Vec, + /// 32 bytes + #[prost(bytes, tag = "2")] + pub deployment: Vec, + /// 20 bytes + #[prost(bytes, tag = "3")] + pub allocation: Vec, + #[prost(string, tag = "4")] + pub indexed_chain: String, + #[prost(string, tag = "5")] + pub url: String, + #[prost(double, tag = "6")] + pub fee_grt: f64, + #[prost(uint32, tag = "7")] + pub response_time_ms: u32, + #[prost(uint32, tag = "8")] + pub seconds_behind: u32, + #[prost(string, tag = "9")] + pub result: String, + #[prost(string, tag = "10")] + pub indexer_errors: String, + #[prost(uint64, tag = "11")] + pub blocks_behind: u64, +} + +// Simplified structures to match the gateway's domain model without attestation +pub struct ClientRequest { + pub id: String, + pub response_time_ms: u16, + pub result: Result<(), Error>, + pub api_key: String, + pub user: String, + pub subgraph: Option, + pub grt_per_usd: f64, // Using f64 instead of NotNan for simplicity + pub indexer_requests: Vec, + pub request_bytes: u32, + pub response_bytes: Option, +} + +pub struct IndexerRequest { + pub indexer: IndexerId, + pub deployment: DeploymentId, + pub url: String, + pub receipt: Receipt, + pub subgraph_chain: String, + pub result: Result, + pub response_time_ms: u16, + pub seconds_behind: u32, + pub blocks_behind: u64, +} + +// Helper functions to generate random data +fn random_bytes(len: usize) -> Vec { + let mut rng = rand::thread_rng(); + let mut bytes = vec![0u8; len]; + rng.fill(&mut bytes[..]); + bytes +} + +// Generate a random string that's guaranteed to be valid UTF-8 +fn random_utf8_string(prefix: &str, len: usize) -> String { + use rand::{thread_rng, Rng}; + + let suffix: String = thread_rng() + .sample_iter(&rand::distributions::Alphanumeric) + .take(len) + .map(char::from) + .collect(); + + format!("{}-{}", prefix, suffix) +} + +// Generate random hex string (guaranteed valid UTF-8) from random bytes +fn random_hex_string(len: usize) -> String { + hex::encode(random_bytes(len)) +} + +// --- Define Pool Sizes --- +const POOL_SIZE: usize = 30; // Number of unique IDs for each type + +// --- Define Static ID Pools (Place this before the random_* functions) --- + +static INDEXER_ID_POOL: Lazy> = Lazy::new(|| { + (0..POOL_SIZE) + .map(|_| { + let mut bytes = [0u8; 20]; + rand::thread_rng().fill(&mut bytes); + IndexerId(bytes) + }) + .collect() +}); + +static DEPLOYMENT_ID_POOL: Lazy> = Lazy::new(|| { + (0..POOL_SIZE) + .map(|_| { + let mut bytes = [0u8; 32]; + rand::thread_rng().fill(&mut bytes); + DeploymentId(bytes) + }) + .collect() +}); + +// Pool for Addresses (used by random_address and tap_signer) +static ADDRESS_POOL: Lazy> = Lazy::new(|| { + (0..POOL_SIZE) + .map(|_| { + let mut bytes = [0u8; 20]; + rand::thread_rng().fill(&mut bytes); + Address(bytes) + }) + .collect() +}); + +// Pool for Subgraph IDs (derived from Deployment pool for consistency) +static SUBGRAPH_ID_POOL: Lazy> = Lazy::new(|| { + DEPLOYMENT_ID_POOL + .iter() + .map(|d| SubgraphId(format!("Qm{}", hex::encode(d.0)))) + .collect() +}); + +// Pools for String-based IDs +static GATEWAY_ID_POOL: Lazy> = Lazy::new(|| { + (0..POOL_SIZE) + .map(|i| format!("gateway-pool-{}", i)) + .collect() +}); + +static API_KEY_POOL: Lazy> = Lazy::new(|| { + (0..POOL_SIZE) + .map(|i| random_utf8_string(&format!("api-key-pool-{}", i), 10)) + .collect() +}); + +static USER_POOL: Lazy> = Lazy::new(|| { + (0..POOL_SIZE) + .map(|i| random_utf8_string(&format!("user-pool-{}", i), 10)) + .collect() +}); + +// --- Update the random ID generation functions --- + +// Replace the existing random_address function (lines 223-227) +fn random_address() -> Address { + // Choose a random Address from the pre-generated pool + *ADDRESS_POOL.choose(&mut rand::thread_rng()).unwrap() +} + +// Replace the existing random_deployment_id function (lines 229-233) +fn random_deployment_id() -> DeploymentId { + // Choose a random DeploymentId from the pre-generated pool + *DEPLOYMENT_ID_POOL.choose(&mut rand::thread_rng()).unwrap() +} + +// Replace the existing random_indexer_id function (lines 235-239) +fn random_indexer_id() -> IndexerId { + // Choose a random IndexerId from the pre-generated pool + *INDEXER_ID_POOL.choose(&mut rand::thread_rng()).unwrap() +} + +// --- Add/Update functions for String-based IDs --- + +// Add this function to select from the Subgraph ID pool +fn random_subgraph_id() -> SubgraphId { + SUBGRAPH_ID_POOL.choose(&mut rand::thread_rng()).unwrap().clone() +} + +// Add this function to select from the Gateway ID pool +fn random_gateway_id() -> String { + GATEWAY_ID_POOL.choose(&mut rand::thread_rng()).unwrap().clone() +} + +// Add this function to select from the API Key pool +fn random_api_key() -> String { + API_KEY_POOL.choose(&mut rand::thread_rng()).unwrap().clone() +} + +// Add this function to select from the User pool +fn random_user() -> String { + USER_POOL.choose(&mut rand::thread_rng()).unwrap().clone() +} + +// --- Update generate_random_client_request (lines 241-314) --- +// Modify calls inside this function to use the new pool-based random functions + +fn generate_random_client_request() -> ClientRequest { + let mut rng = rand::thread_rng(); + + let num_indexers = rng.gen_range(1..5); + let mut indexer_requests = Vec::with_capacity(num_indexers); + + // --- Key Change: Pick IDs *once* per ClientRequest where appropriate --- + let deployment_id = random_deployment_id(); // Pick one deployment from pool + let subgraph_id = random_subgraph_id(); // Pick one subgraph from pool (could also derive from deployment) + let api_key = random_api_key(); // Pick one api key from pool + let user = random_user(); // Pick one user from pool + + let subgraph_chain = if rng.gen_bool(0.8) { "mainnet".to_string() } else { "goerli".to_string() }; + + for _ in 0..num_indexers { + let success = rng.gen_bool(0.9); + + // --- Use pool-based functions for indexer-specific IDs --- + let indexer_id = random_indexer_id(); + let allocation_id = random_address(); // Allocation can vary per indexer request + + let result = if success { + Ok(IndexerResponse { errors: vec![] }) + } else { + Err(IndexerError::NetworkError( + "Error processing request".to_string(), + )) + }; + + indexer_requests.push(IndexerRequest { + indexer: indexer_id, // From pool + deployment: deployment_id, // Use the *same* deployment for all indexers in this request + url: format!( // Use consistent subgraph ID for URL + "https://api.thegraph.com/subgraphs/id/{}", + subgraph_id.0 // Access the inner String + ), + receipt: Receipt::new( + allocation_id, // From pool + rng.gen_range(100_000_000_000_000..1_000_000_000_000_000), + ), + subgraph_chain: subgraph_chain.clone(), + result, + response_time_ms: rng.gen_range(50..2000) as u16, + seconds_behind: rng.gen_range(0..100), + blocks_behind: rng.gen_range(0..50), + }); + } + + let success = rng.gen_bool(0.95); + + ClientRequest { + id: random_hex_string(16), // Keep request ID unique + response_time_ms: rng.gen_range(50..5000) as u16, + result: if success { + Ok(()) + } else { + Err(Error::QueryFailed("Query validation error".to_string())) + }, + api_key: api_key, // Use the chosen API key + user: user, // Use the chosen user + subgraph: if rng.gen_bool(0.9) { Some(subgraph_id) } else { None }, // Use the chosen subgraph ID + grt_per_usd: rng.gen_range(0.05..0.2), + indexer_requests, + request_bytes: rng.gen_range(100..5000), + response_bytes: if rng.gen_bool(0.95) { + Some(rng.gen_range(200..10000)) + } else { + None + }, + } +} + +// Function that matches the logic in gateway's report function but without attestation +fn encode_client_request( + client_request: ClientRequest, + // tap_signer: Address, // Remove this parameter + // graph_env: String, // Remove this parameter +) -> Vec { + // --- Key Change: Select gateway and signer from pools here --- + let gateway_id = random_gateway_id(); // Select from pool + let tap_signer = random_address(); // Select from pool + + let indexer_queries = client_request + .indexer_requests + .iter() + .map(|indexer_request| IndexerQueryProtobuf { + indexer: indexer_request.indexer.to_vec(), + deployment: indexer_request.deployment.to_vec(), + allocation: indexer_request.receipt.allocation().to_vec(), + indexed_chain: indexer_request.subgraph_chain.clone(), + url: indexer_request.url.clone(), + fee_grt: indexer_request.receipt.value() as f64 * 1e-18, + response_time_ms: indexer_request.response_time_ms as u32, + seconds_behind: indexer_request.seconds_behind, + result: indexer_request + .result + .as_ref() + .map(|_| "success".to_string()) + .unwrap_or_else(|err| err.to_string()), + indexer_errors: indexer_request + .result + .as_ref() + .map(|r| { + r.errors + .iter() + .map(|err| err.as_str()) + .collect::>() + .join("; ") + }) + .unwrap_or_default(), + blocks_behind: indexer_request.blocks_behind, + }) + .collect(); + + let total_fees_grt: f64 = client_request + .indexer_requests + .iter() + .map(|i| i.receipt.value() as f64 * 1e-18) + .sum(); + let total_fees_usd: f64 = total_fees_grt / client_request.grt_per_usd; + + let client_query_msg = ClientQueryProtobuf { + gateway_id, + receipt_signer: tap_signer.to_vec(), + query_id: client_request.id, + api_key: client_request.api_key, + user_id: client_request.user, + subgraph: client_request.subgraph.map(|s| s.to_string()), + result: client_request + .result + .map(|()| "success".to_string()) + .unwrap_or_else(|err| err.to_string()), + response_time_ms: client_request.response_time_ms as u32, + request_bytes: client_request.request_bytes, + response_bytes: client_request.response_bytes, + total_fees_usd, + indexer_queries, + }; + + // Encode to Protobuf + let mut buf = Vec::new(); + client_query_msg.encode(&mut buf).unwrap(); + + // Return the raw protobuf message without length prefix + buf +} + +// --- NEW: Asynchronous function to run in each producer task --- +async fn produce_messages(producer: FutureProducer, topic: String, task_id: u32, delay_micros: u64) { + let mut counter: u64 = 0; + println!("[Task {}] Starting producer loop", task_id); + + loop { + // --- Use existing message generation logic --- + let client_request = generate_random_client_request(); + let payload = encode_client_request(client_request); + // --- End Use existing message generation logic --- + + // Use a simple counter or a field from the request for the key + // to help distribute messages across partitions. + // Using modulo is okay for basic distribution. + let key = format!("key-{}", counter % 100); + + // Send asynchronously - *DO NOT* await the future here in the loop! + let send_future = producer.send_result( + FutureRecord::to(&topic) + .payload(&payload) + .key(&key), + ); + + // Check for immediate queuing errors (e.g., queue full). + if let Err((e, _)) = send_future { + eprintln!("[Task {}] Failed to queue message (queue full?): {:?}. Pausing...", task_id, e); + // Pause briefly if the producer queue is full to avoid overwhelming it. + time::sleep(Duration::from_millis(100)).await; + // Optionally: break or implement more robust backoff + } + + counter += 1; + + // Apply throttling delay if configured + if delay_micros > 0 { + time::sleep(Duration::from_micros(delay_micros)).await; + } else { + // Yield control occasionally if the loop is extremely tight (no throttling) + if counter % 1000 == 0 { + tokio::task::yield_now().await; + } + } + } +} + +// --- NEW: Updated main function using Tokio --- +#[tokio::main] +async fn main() { + // Print librdkafka version + let (version_n, version_s) = get_rdkafka_version(); + println!("rd_kafka_version: 0x{:08x}, {}", version_n, version_s); + + // --- Configuration --- + // Read target total messages per second (informational, not used for delay) + let total_mps: u64 = env::var("MESSAGES_PER_SECOND") + .unwrap_or_else(|_| "10000".to_string()) // Default to 10k + .parse() + .expect("MESSAGES_PER_SECOND must be a valid number"); + + // Determine number of producer tasks (use logical cores) + let num_tasks = num_cpus::get().max(1); + println!("Target MPS: {}, Spawning {} producer tasks", total_mps, num_tasks); + + // Calculate delay per task to achieve target MPS + // Each task needs to wait `num_tasks` times longer than the overall target interval + let delay_per_task_micros = if total_mps > 0 { + (1_000_000 * num_tasks as u64) / total_mps + } else { + 0 // No delay if target MPS is 0 (run as fast as possible) + }; + if delay_per_task_micros > 0 { + println!( + "[Config] Throttling enabled: Each task will pause for {} microseconds.", + delay_per_task_micros + ); + } else { + println!("[Config] Throttling disabled (MESSAGES_PER_SECOND=0 or not set appropriately). Running at max speed."); + } + + let broker = env::var("KAFKA_BROKER").unwrap_or_else(|_| "redpanda:9092".to_string()); + let topic = "gateway_qos_topic".to_string(); // Ensure this matches ClickHouse + + println!("Connecting to Kafka broker at {}", broker); + println!("Waiting 5 seconds for Redpanda to initialize..."); + time::sleep(Duration::from_secs(5)).await; + + // --- Producer Setup --- + // Configure the Kafka producer client + let producer: FutureProducer = ClientConfig::new() + .set("bootstrap.servers", &broker) + .set("message.timeout.ms", "30000") // Max time librdkafka tries to send one message + .set("security.protocol", "PLAINTEXT") + // .set("debug", "all") // Disable verbose debugging for performance testing + .set("enable.idempotence", "false") // Disable for max throughput test (less overhead) + .set("retries", "5") // Retries for recoverable send failures + .set("retry.backoff.ms", "100") // Backoff between retries (reduced for faster recovery) + .set("socket.timeout.ms", "10000") + .set("socket.keepalive.enable", "true") + // --- Batching Configuration (Crucial for Throughput) --- + .set("linger.ms", "5") // Wait up to 5ms to gather messages into a batch + .set("batch.size", "131072") // Batch size in bytes (e.g., 128 KB). Tune based on avg message size. + // --- Buffering Configuration (Increase if producer blocks/errors often) --- + .set("queue.buffering.max.messages", "500000") // Max messages in internal producer queue + .set("queue.buffering.max.ms", "1000") // Max time msgs wait in queue before send() blocks/errors + // --- Optional: Compression (Reduces network bandwidth, increases CPU) --- + // .set("compression.codec", "snappy") // or lz4, gzip, zstd + .create() + .expect("Producer creation error"); + + // --- Spawn Producer Tasks --- + let mut tasks = vec![]; + println!("Spawning {} producer tasks...", num_tasks); + for i in 0..num_tasks { + let producer_clone = producer.clone(); // Clone producer for each task + let topic_clone = topic.clone(); + tasks.push(tokio::spawn(produce_messages( + producer_clone, + topic_clone, + i as u32, + delay_per_task_micros, // Pass the calculated delay + ))); + } + + // --- Keep Main Task Alive --- + println!( + "Producer tasks running. Sending to topic '{}'. Press Ctrl+C to stop.", + topic + ); + // Wait for all tasks to complete (they run infinitely, so this waits forever unless they error out) + for task in tasks { + if let Err(e) = task.await { + eprintln!("Producer task failed: {:?}", e); + } + } +} diff --git a/subgraph/README.md b/subgraph/README.md deleted file mode 100644 index 297d806..0000000 --- a/subgraph/README.md +++ /dev/null @@ -1 +0,0 @@ -# QoS Oracle V2 subgraph \ No newline at end of file