diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index 3ab895f..e541386 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -28,7 +28,7 @@ jobs: - name: Setup Rust uses: actions-rs/toolchain@v1 with: - toolchain: nightly + toolchain: stable override: true components: rustfmt, clippy @@ -49,15 +49,3 @@ jobs: with: command: test args: --workspace --no-fail-fast - env: - CARGO_INCREMENTAL: '0' - RUSTFLAGS: '-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off -Cpanic=abort -Zpanic_abort_tests' - RUSTDOCFLAGS: '-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off -Cpanic=abort -Zpanic_abort_tests' - - - name: Generate coverage - id: coverage - uses: actions-rs/grcov@v0.1 - - - uses: codecov/codecov-action@v3 - with: - files: ${{ steps.coverage.outputs.report }} \ No newline at end of file diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 348c20d..0056f54 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -25,7 +25,7 @@ jobs: - name: Setup Rust uses: actions-rs/toolchain@v1 with: - toolchain: nightly + toolchain: stable override: true components: rustfmt, clippy @@ -46,15 +46,3 @@ jobs: with: command: test args: --workspace --no-fail-fast - env: - CARGO_INCREMENTAL: '0' - RUSTFLAGS: '-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off -Cpanic=abort -Zpanic_abort_tests' - RUSTDOCFLAGS: '-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off -Cpanic=abort -Zpanic_abort_tests' - - - name: Generate coverage - id: coverage - uses: actions-rs/grcov@v0.1 - - - uses: codecov/codecov-action@v3 - with: - files: ${{ steps.coverage.outputs.report }} diff --git a/.idea/discord.xml b/.idea/discord.xml new file mode 100644 index 0000000..66c4b97 --- /dev/null +++ b/.idea/discord.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ef5f323..07076a9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,16 +8,17 @@ Bear in mind to keep your contributions under the [Code of Conduct](./.github/CO ## Bug report, ideas, and suggestion -The [issues](https://github.com/teknologi-umum/polarite/issues) page is a great way to communicate to us. -Other than that, we have a [Telegram group](https://t.me/teknologi_umum_v2) that you can discuss your ideas into. +The [issues](https://github.com/teknologi-umum/polarite/issues) page is a great way to communicate to us. +Other than that, we have a [Telegram group](https://t.me/teknologi_umum_v2) that you can discuss your ideas into. If you're not an Indonesian speaker, it's 100% fine to talk in English there. -Please make sure that the issue you're creating is in as much detail as possible. Poor communication might lead to a big mistake, we're trying to avoid that. +Please make sure that the issue you're creating is in as much detail as possible. Poor communication might lead to a big +mistake, we're trying to avoid that. ## Pull request -**A big heads up before you're writing a breaking change code or a new feature: Please open up an -[issue](https://github.com/teknologi-umum/polarite/issues) regarding what you're working on, or just talk in the +**A big heads up before you're writing a breaking change code or a new feature: Please open up an +[issue](https://github.com/teknologi-umum/polarite/issues) regarding what you're working on, or just talk in the [Telegram group](https://t.me/teknologi_umum_v2).** ### Prerequisites @@ -29,12 +30,13 @@ You will need a few things to get things working: ### Getting Started -1. [Fork](https://help.github.com/articles/fork-a-repo/) this repository to your own Github account and [clone](https://help.github.com/articles/cloning-a-repository/) it to your local machine. +1. [Fork](https://help.github.com/articles/fork-a-repo/) this repository to your own Github account + and [clone](https://help.github.com/articles/cloning-a-repository/) it to your local machine. 2. Run `cargo update` to install the dependencies needed. 3. Run `cargo run` to start the development application. 4. Have fun! -You are encouraged to use [Conventional Commit](https://www.conventionalcommits.org/en/v1.0.0-beta.2/) +You are encouraged to use [Conventional Commit](https://www.conventionalcommits.org/en/v1.0.0-beta.2/) for your commit message. But it's not really compulsory. ### Testing your change diff --git a/Cargo.lock b/Cargo.lock index 9946c0e..380d558 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,15 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "aho-corasick" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c378d78423fdad8089616f827526ee33c19f2fddbd5de1629152c9593ba4783" +dependencies = [ + "memchr", +] + [[package]] name = "anstream" version = "0.3.2" @@ -83,7 +92,7 @@ checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.16", ] [[package]] @@ -156,12 +165,6 @@ dependencies = [ "rustc-demangle", ] -[[package]] -name = "base64" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" - [[package]] name = "base64" version = "0.21.0" @@ -240,7 +243,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.16", ] [[package]] @@ -340,6 +343,18 @@ dependencies = [ "libc", ] +[[package]] +name = "fastping-rs" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bed7f653ac8d713246f65291b25523c37d91e47056b26161cd6ed0e11f3815ff" +dependencies = [ + "log", + "pnet", + "pnet_macros_support", + "rand", +] + [[package]] name = "fastrand" version = "1.9.0" @@ -447,7 +462,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.16", ] [[package]] @@ -507,6 +522,12 @@ version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad0a93d233ebf96623465aad4046a8d3aa4da22d4f4beba5388838c8a434bbb4" +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + [[package]] name = "h2" version = "0.3.19" @@ -687,6 +708,15 @@ version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "12b6ee2129af8d4fb011108c73d99a1b83a85977f23b82460c0ae2e25bb4b57f" +[[package]] +name = "ipnetwork" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4088d739b183546b239688ddbc79891831df421773df95e236daf7867866d355" +dependencies = [ + "serde", +] + [[package]] name = "is-terminal" version = "0.4.7" @@ -873,7 +903,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.16", ] [[package]] @@ -964,7 +994,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn", + "syn 2.0.16", ] [[package]] @@ -995,7 +1025,7 @@ checksum = "39407670928234ebc5e6e580247dd567ad73a3578460c5990f9503df207e8f07" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.16", ] [[package]] @@ -1016,6 +1046,94 @@ version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" +[[package]] +name = "pnet" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b6d2a0409666964722368ef5fb74b9f93fac11c18bef3308693c16c6733f103" +dependencies = [ + "ipnetwork", + "pnet_base", + "pnet_datalink", + "pnet_packet", + "pnet_sys", + "pnet_transport", +] + +[[package]] +name = "pnet_base" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25488cd551a753dcaaa6fffc9f69a7610a412dd8954425bf7ffad5f7d1156fb8" + +[[package]] +name = "pnet_datalink" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4d1f8ab1ef6c914cf51dc5dfe0be64088ea5f3b08bbf5a31abc70356d271198" +dependencies = [ + "ipnetwork", + "libc", + "pnet_base", + "pnet_sys", + "winapi", +] + +[[package]] +name = "pnet_macros" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30490e0852e58402b8fae0d39897b08a24f493023a4d6cf56b2e30f31ed57548" +dependencies = [ + "proc-macro2", + "quote", + "regex", + "syn 1.0.109", +] + +[[package]] +name = "pnet_macros_support" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4714e10f30cab023005adce048f2d30dd4ac4f093662abf2220855655ef8f90" +dependencies = [ + "pnet_base", +] + +[[package]] +name = "pnet_packet" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8588067671d03c9f4254b2e66fecb4d8b93b5d3e703195b84f311cd137e32130" +dependencies = [ + "glob", + "pnet_base", + "pnet_macros", + "pnet_macros_support", +] + +[[package]] +name = "pnet_sys" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9a3f32b0df45515befd19eed04616f6b56a488da92afc61164ef455e955f07f" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "pnet_transport" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "932b2916d693bcc5fa18443dc99142e0a6fd31a6ce75a511868f7174c17e2bce" +dependencies = [ + "libc", + "pnet_base", + "pnet_packet", + "pnet_sys", +] + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -1094,6 +1212,8 @@ version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81ca098a9821bd52d6b24fd8b10bd081f47d39c22778cafaa75a2857a62c6390" dependencies = [ + "aho-corasick", + "memchr", "regex-syntax", ] @@ -1109,7 +1229,7 @@ version = "0.11.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cde824a14b7c14f85caff81225f411faacc04a2013f41670f41443742b1c1c55" dependencies = [ - "base64 0.21.0", + "base64", "bytes", "encoding_rs", "futures-core", @@ -1151,6 +1271,7 @@ dependencies = [ "roselite-request", "roselite-server", "sentry", + "sentry-tracing", "tokio", ] @@ -1177,9 +1298,12 @@ name = "roselite-request" version = "0.1.0" dependencies = [ "anyhow", + "fastping-rs", "reqwest", "roselite-common", "roselite-config", + "sentry", + "sentry-tracing", "tokio", ] @@ -1284,9 +1408,9 @@ checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" [[package]] name = "sentry" -version = "0.31.3" +version = "0.31.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de31c6e03322af2175d3c850c5b5e11efcadc01948cd1fb7b5ad0a7c7b6c7ff2" +checksum = "2e95efd0cefa32028cdb9766c96de71d96671072f9fb494dc9fb84c0ef93e52b" dependencies = [ "httpdate", "native-tls", @@ -1304,9 +1428,9 @@ dependencies = [ [[package]] name = "sentry-anyhow" -version = "0.31.3" +version = "0.31.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4df1501f58a7821af9d3bd11435d1c0c74121a6e6931f7615592fdfa85a5fb84" +checksum = "27c30fb3ec036e3210a51d102890e734be62951d876e626205db8412a2a57c1a" dependencies = [ "anyhow", "sentry-backtrace", @@ -1315,9 +1439,9 @@ dependencies = [ [[package]] name = "sentry-backtrace" -version = "0.31.3" +version = "0.31.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "264e3ad27da3d1ad81b499dbcceae0a50e0e6ffc4b65b93f47d5180d46827644" +checksum = "6ac2bac6f310c4c4c4bb094d1541d32ae497f8c5c23405e85492cefdfe0971a9" dependencies = [ "backtrace", "once_cell", @@ -1327,9 +1451,9 @@ dependencies = [ [[package]] name = "sentry-contexts" -version = "0.31.3" +version = "0.31.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7144590f7950647e4df5bd95f234c3aa29124729c54bd2457e1224d701d1a91c" +checksum = "6c3e17295cecdbacf66c5bd38d6e1147e09e1e9d824d2d5341f76638eda02a3a" dependencies = [ "hostname", "libc", @@ -1341,9 +1465,9 @@ dependencies = [ [[package]] name = "sentry-core" -version = "0.31.3" +version = "0.31.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35614ecf115f55d93583baa02a85cb63acb6567cf91b17690d1147bac1739ca4" +checksum = "8339474f587f36cb110fa1ed1b64229eea6d47b0b886375579297b7e47aeb055" dependencies = [ "once_cell", "rand", @@ -1354,9 +1478,9 @@ dependencies = [ [[package]] name = "sentry-debug-images" -version = "0.31.3" +version = "0.31.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53c4288a1b255e6ff55f111d2e14a48d369da76e86fae15a00ee26a371d82ad4" +checksum = "1c11e7d2b809b06497a18a2e60f513206462ae2db27081dfb7be9ade1f329cc8" dependencies = [ "findshlibs", "once_cell", @@ -1365,9 +1489,9 @@ dependencies = [ [[package]] name = "sentry-panic" -version = "0.31.3" +version = "0.31.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a941028a24baf0a5a994d8a39670cecc72a61971bb0155f771537447a46211a" +checksum = "875b69f506da75bd664029eafb05f8934297d2990192896d17325f066bd665b7" dependencies = [ "sentry-backtrace", "sentry-core", @@ -1375,9 +1499,9 @@ dependencies = [ [[package]] name = "sentry-tracing" -version = "0.31.3" +version = "0.31.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eec56ebafd7cfc1175bccdf277be582ccc3308b8c353dca5831261a967a6e28c" +checksum = "89feead9bdd116f8035e89567651340fc382db29240b6c55ef412078b08d1aa3" dependencies = [ "sentry-backtrace", "sentry-core", @@ -1387,9 +1511,9 @@ dependencies = [ [[package]] name = "sentry-types" -version = "0.31.3" +version = "0.31.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c56f616602a3b282bf4b4e8e5b4d10bcf9412a987df91c592b95a1f6ef1ee43" +checksum = "99dc599bd6646884fc403d593cdcb9816dd67c50cff3271c01ff123617908dcd" dependencies = [ "debugid", "getrandom", @@ -1419,7 +1543,7 @@ checksum = "8c805777e3930c8883389c602315a24224bcc738b63905ef87cd1420353ea93e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.16", ] [[package]] @@ -1527,6 +1651,17 @@ 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.16" @@ -1574,7 +1709,7 @@ checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.16", ] [[package]] @@ -1646,7 +1781,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.16", ] [[package]] @@ -1822,11 +1957,11 @@ checksum = "1865806a559042e51ab5414598446a5871b561d21b6764f2eabb0dd481d880a6" [[package]] name = "ureq" -version = "2.6.2" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "338b31dd1314f68f3aabf3ed57ab922df95ffcd902476ca7ba3c4ce7b908c46d" +checksum = "0b11c96ac7ee530603dcdf68ed1557050f374ce55a5a07193ebf8cbc9f8927e9" dependencies = [ - "base64 0.13.1", + "base64", "log", "native-tls", "once_cell", @@ -1916,7 +2051,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn", + "syn 2.0.16", "wasm-bindgen-shared", ] @@ -1950,7 +2085,7 @@ checksum = "e128beba882dd1eb6200e1dc92ae6c5dbaa4311aa7bb211ca035779e5efc39f8" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.16", "wasm-bindgen-backend", "wasm-bindgen-shared", ] diff --git a/Dockerfile b/Dockerfile index 340f821..cb379fe 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,7 @@ FROM rust:1.69.0-bullseye AS builder WORKDIR /app +ENV DEBIAN_FRONTEND=noninteractive +RUN apt-get update && apt-get install -y pkg-config libudev-dev perl libssl-dev COPY . . RUN cargo build --release diff --git a/roselite-config/src/lib.rs b/roselite-config/src/lib.rs index 8c2d330..2e7e00f 100644 --- a/roselite-config/src/lib.rs +++ b/roselite-config/src/lib.rs @@ -1,32 +1,54 @@ use std::collections::BTreeMap; use std::fs::File; use std::io::Read; +use std::str::FromStr; use anyhow::{Error, Result}; use serde::Deserialize; +#[derive(Debug, Deserialize, Clone, PartialEq)] +pub enum MonitorType { + HTTP, + ICMP, +} + +impl FromStr for MonitorType { + type Err = (); + + fn from_str(s: &str) -> std::result::Result { + match s { + "HTTP" => Ok(MonitorType::HTTP), + "ICMP" => Ok(MonitorType::ICMP), + _ => Err(()), + } + } +} + /// Monitor defines a single monitor configuration -#[derive(Deserialize, Clone)] +#[derive(Deserialize, Clone, Debug)] pub struct Monitor { + pub monitor_type: MonitorType, pub push_url: String, - pub monitor_url: String, + pub monitor_target: String, #[serde(skip_serializing_if = "Option::is_none")] pub request_headers: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub skip_tls_verify: Option, } -#[derive(Deserialize, Clone)] +#[derive(Deserialize, Clone, Debug)] pub struct ErrorReporting { pub sentry_dsn: String, } -#[derive(Deserialize, Clone)] +#[derive(Deserialize, Clone, Debug)] pub struct ServerConfig { pub listen_address: String, pub upstream_kuma: Option, } /// Configuration sets a global configuration for the application. -#[derive(Deserialize, Clone)] +#[derive(Deserialize, Clone, Debug)] pub struct Configuration { pub error_reporting: Option, pub server: Option, @@ -77,23 +99,27 @@ impl Configuration { #[cfg(test)] mod tests { - use crate::Configuration; - use anyhow::Result; use std::env::temp_dir; use std::fs::File; use std::io::Write; + use anyhow::Result; + + use crate::{Configuration, MonitorType}; + #[test] fn parse_toml_configuration() { let configuration = r#"[[monitors]] +monitor_type = "HTTP" push_url = "https://your-uptime-kuma.com/api/push/Eq15E23yc3" -monitor_url = "https://github.com/healthz""#; +monitor_target = "https://github.com/healthz""#; let parsed_configuration = Configuration::from_toml(configuration); assert_eq!(parsed_configuration.monitors.len(), 1); if let Some(first_monitor) = parsed_configuration.monitors.first() { - assert_eq!(first_monitor.monitor_url, "https://github.com/healthz"); + assert_eq!(first_monitor.monitor_type, MonitorType::HTTP); + assert_eq!(first_monitor.monitor_target, "https://github.com/healthz"); assert_eq!( first_monitor.push_url, "https://your-uptime-kuma.com/api/push/Eq15E23yc3" @@ -104,14 +130,16 @@ monitor_url = "https://github.com/healthz""#; #[test] fn parse_yaml_configuration() { let configuration = r#"monitors: - - push_url: "https://your-uptime-kuma.com/api/push/Eq15E23yc3" - monitor_url: "https://github.com/healthz""#; + - monitor_type: "HTTP" + push_url: "https://your-uptime-kuma.com/api/push/Eq15E23yc3" + monitor_target: "https://github.com/healthz""#; let parsed_configuration = Configuration::from_yaml(configuration); assert_eq!(parsed_configuration.monitors.len(), 1); if let Some(first_monitor) = parsed_configuration.monitors.first() { - assert_eq!(first_monitor.monitor_url, "https://github.com/healthz"); + assert_eq!(first_monitor.monitor_type, MonitorType::HTTP); + assert_eq!(first_monitor.monitor_target, "https://github.com/healthz"); assert_eq!( first_monitor.push_url, "https://your-uptime-kuma.com/api/push/Eq15E23yc3" @@ -124,8 +152,9 @@ monitor_url = "https://github.com/healthz""#; let configuration = r#"{ "monitors": [ { + "monitor_type": "HTTP", "push_url": "https://your-uptime-kuma.com/api/push/Eq15E23yc3", - "monitor_url": "https://github.com/healthz" + "monitor_target": "https://github.com/healthz" } ] }"#; @@ -134,7 +163,8 @@ monitor_url = "https://github.com/healthz""#; assert_eq!(parsed_configuration.monitors.len(), 1); if let Some(first_monitor) = parsed_configuration.monitors.first() { - assert_eq!(first_monitor.monitor_url, "https://github.com/healthz"); + assert_eq!(first_monitor.monitor_type, MonitorType::HTTP); + assert_eq!(first_monitor.monitor_target, "https://github.com/healthz"); assert_eq!( first_monitor.push_url, "https://your-uptime-kuma.com/api/push/Eq15E23yc3" @@ -155,8 +185,10 @@ sentry_dsn = "https://sentry.io" listen_address = "127.0.0.1:8321" [[monitors]] +monitor_type = "ICMP" push_url = "https://your-uptime-kuma.com/api/push/Eq15E23yc3" -monitor_url = "https://github.com/healthz""#; +monitor_target = "https://github.com/healthz" +"#; let _ = file.write(configuration.as_bytes())?; file.flush()?; @@ -178,7 +210,8 @@ monitor_url = "https://github.com/healthz""#; assert_eq!(parsed_configuration.monitors.len(), 1); if let Some(first_monitor) = parsed_configuration.monitors.first() { - assert_eq!(first_monitor.monitor_url, "https://github.com/healthz"); + assert_eq!(first_monitor.monitor_type, MonitorType::ICMP); + assert_eq!(first_monitor.monitor_target, "https://github.com/healthz"); assert_eq!( first_monitor.push_url, "https://your-uptime-kuma.com/api/push/Eq15E23yc3" diff --git a/roselite-request/Cargo.toml b/roselite-request/Cargo.toml index 037aa48..0a6d49e 100644 --- a/roselite-request/Cargo.toml +++ b/roselite-request/Cargo.toml @@ -9,5 +9,8 @@ edition = "2021" reqwest = { version = "0.11" } tokio = { version = "1", features = ["full"] } anyhow = "1.0.71" +fastping-rs = "0.2" +sentry = { version = "0.31", features = ["default", "anyhow"] } +sentry-tracing = { version = "0.31" } roselite-config = { path = "../roselite-config" } roselite-common = { path = "../roselite-common" } \ No newline at end of file diff --git a/roselite-request/src/bonk_caller.rs b/roselite-request/src/bonk_caller.rs new file mode 100644 index 0000000..b2e9248 --- /dev/null +++ b/roselite-request/src/bonk_caller.rs @@ -0,0 +1,33 @@ +use anyhow::Result; +use roselite_common::heartbeat::{Heartbeat, HeartbeatStatus}; +use roselite_config::Monitor; + +use crate::RequestCaller; + +#[derive(Clone)] +/// BonKCaller is a mock or empty struct that's implement +/// RequestCaller. Do not use this as a normal caller transport +/// unless you know what you're doing. +pub struct BonkCaller {} + +impl BonkCaller { + pub fn new() -> Self { + BonkCaller {} + } +} + +impl Default for BonkCaller { + fn default() -> Self { + Self::new() + } +} + +impl RequestCaller for BonkCaller { + fn call(&self, _monitor: Monitor) -> Result { + Ok(Heartbeat { + msg: "OK".to_string(), + status: HeartbeatStatus::Up, + ping: 0, + }) + } +} diff --git a/roselite-request/src/http_caller.rs b/roselite-request/src/http_caller.rs new file mode 100644 index 0000000..a648c57 --- /dev/null +++ b/roselite-request/src/http_caller.rs @@ -0,0 +1,79 @@ +use std::time::Duration; + +use anyhow::Result; +use reqwest::blocking::Client; +use reqwest::{Method, StatusCode}; +use tokio::time::Instant; + +use roselite_common::heartbeat::{Heartbeat, HeartbeatStatus}; +use roselite_config::Monitor; + +use crate::RequestCaller; + +#[derive(Clone)] +pub struct HttpCaller { + client: Client, +} + +impl HttpCaller { + pub fn new() -> Self { + HttpCaller { + client: Client::builder() + .user_agent("Roselite/1.0") + .build() + .unwrap(), + } + } +} + +impl Default for HttpCaller { + fn default() -> Self { + Self::new() + } +} + +impl RequestCaller for HttpCaller { + fn call(&self, monitor: Monitor) -> Result { + // Retrieve the currently running span + let parent_span = sentry::configure_scope(|scope| scope.get_span()); + + let span: sentry::TransactionOrSpan = match &parent_span { + Some(parent) => parent + .start_child("http_caller.call", "Call target HTTP request") + .into(), + None => { + let ctx = + sentry::TransactionContext::new("Call target HTTP request", "http_caller.call"); + sentry::start_transaction(ctx).into() + } + }; + + let current_instant = Instant::now(); + let response = self + .client + .request(Method::GET, monitor.monitor_target.as_str()) + .timeout(Duration::from_secs(30)) + .send()?; + + let elapsed: Duration = current_instant.elapsed(); + + let status_code: StatusCode = response.status(); + let mut ok = true; + // everything from 2xx-3xx is considered ok + if status_code >= StatusCode::BAD_REQUEST { + ok = false; + } + + span.finish(); + + Ok(Heartbeat { + msg: "OK".to_string(), + status: if ok { + HeartbeatStatus::Up + } else { + HeartbeatStatus::Down + }, + ping: elapsed.as_millis(), + }) + } +} diff --git a/roselite-request/src/icmp_caller.rs b/roselite-request/src/icmp_caller.rs new file mode 100644 index 0000000..9c94daa --- /dev/null +++ b/roselite-request/src/icmp_caller.rs @@ -0,0 +1,82 @@ +use std::time::Duration; + +use anyhow::{Error, Result}; +use fastping_rs::PingResult::{Idle, Receive}; +use fastping_rs::Pinger; + +use roselite_common::heartbeat::{Heartbeat, HeartbeatStatus}; +use roselite_config::Monitor; + +use crate::RequestCaller; + +#[derive(Debug, Clone)] +pub struct IcmpCaller {} + +impl IcmpCaller { + pub fn new() -> Self { + IcmpCaller {} + } +} + +impl Default for IcmpCaller { + fn default() -> Self { + Self::new() + } +} + +impl RequestCaller for IcmpCaller { + fn call(&self, monitor: Monitor) -> Result { + // Retrieve the currently running span + let parent_span = sentry::configure_scope(|scope| scope.get_span()); + + let span: sentry::TransactionOrSpan = match &parent_span { + Some(parent) => parent + .start_child("icmp_caller.call", "Call target ICMP request") + .into(), + None => { + let ctx = + sentry::TransactionContext::new("Call target ICMP request", "icmp_caller.call"); + sentry::start_transaction(ctx).into() + } + }; + + let (pinger, results) = Pinger::new(None, None).unwrap(); + + pinger.add_ipaddr(monitor.monitor_target.as_str()); + pinger.run_pinger(); + + let mut ok = true; + let mut ping_duration = Duration::from_secs(0); + for _ in 0..4 { + match results.recv() { + Ok(result) => match result { + Idle { addr: _addr } => { + ok = false; + } + Receive { addr: _addr, rtt } => { + ok = true; + ping_duration = rtt; + } + }, + Err(err) => { + span.finish(); + return Err(Error::from(err)); + } + } + } + + pinger.stop_pinger(); + + span.finish(); + + Ok(Heartbeat { + msg: "OK".to_string(), + status: if ok { + HeartbeatStatus::Up + } else { + HeartbeatStatus::Down + }, + ping: ping_duration.as_millis(), + }) + } +} diff --git a/roselite-request/src/lib.rs b/roselite-request/src/lib.rs index e9c14c4..049d8c3 100644 --- a/roselite-request/src/lib.rs +++ b/roselite-request/src/lib.rs @@ -2,89 +2,142 @@ use std::time::Duration; use anyhow::Result; use reqwest::{Client, Method, StatusCode, Url}; -use tokio::time::Instant; -use roselite_common::heartbeat::{Heartbeat, HeartbeatStatus}; -use roselite_config::Monitor; +use roselite_common::heartbeat::Heartbeat; +use roselite_config::{Monitor, MonitorType}; -pub async fn call_monitor_endpoint(monitor: Monitor) -> Result { - let monitor_client = Client::builder().user_agent("Roselite/1.0").build()?; +use crate::bonk_caller::BonkCaller; +use crate::http_caller::HttpCaller; +use crate::icmp_caller::IcmpCaller; - let current_instant = Instant::now(); - let response = monitor_client - .request(Method::GET, monitor.monitor_url.as_str()) - .timeout(Duration::from_secs(30)) - .send() - .await?; +mod bonk_caller; +pub mod http_caller; +pub mod icmp_caller; - let elapsed: Duration = current_instant.elapsed(); +pub trait RequestCaller: Send + Sync { + fn call(&self, monitor: Monitor) -> Result; +} - let status_code: StatusCode = response.status(); - let mut ok = true; - // everything from 2xx-3xx is considered ok - if status_code >= StatusCode::BAD_REQUEST { - ok = false; - } +pub struct RoseliteRequest { + http_caller: Box, + icmp_caller: Box, +} + +unsafe impl Send for RoseliteRequest {} - Ok(Heartbeat { - msg: "OK".to_string(), - status: if ok { - HeartbeatStatus::Up - } else { - HeartbeatStatus::Down - }, - ping: elapsed.as_millis(), - }) +unsafe impl Sync for RoseliteRequest {} + +impl Default for RoseliteRequest { + fn default() -> Self { + RoseliteRequest { + http_caller: Box::new(BonkCaller::new()), + icmp_caller: Box::new(BonkCaller::new()), + } + } } -pub async fn call_kuma_endpoint(upstream_url: String, heartbeat: Heartbeat) -> Result<()> { - let push_client = Client::builder().user_agent("Roselite/1.0").build()?; - - let mut push_url = Url::parse(upstream_url.as_str())?; - push_url - .query_pairs_mut() - .append_pair("msg", heartbeat.msg.as_str()) - .append_pair("status", heartbeat.status.to_string().as_str()) - .append_pair("ping", heartbeat.ping.to_string().as_str()); - - match push_client - .request(Method::GET, push_url) - .timeout(Duration::from_secs(60)) - .send() - .await - { - Ok(response) => { - if response.status() >= StatusCode::BAD_REQUEST { - println!( - "Received response status of {} during sending event to remote push url", - response.status() +impl RoseliteRequest { + pub fn new(http_caller: Box, icmp_caller: Box) -> Self { + RoseliteRequest { + http_caller, + icmp_caller, + } + } + + pub async fn call_kuma_endpoint( + &self, + upstream_url: String, + heartbeat: Heartbeat, + ) -> Result<()> { + // Retrieve the currently running span + let parent_span = sentry::configure_scope(|scope| scope.get_span()); + + let span: sentry::TransactionOrSpan = match &parent_span { + Some(parent) => parent + .start_child( + "request.call_kuma_endpoint", + "Call upstream Uptime Kuma endpoint", + ) + .into(), + None => { + let ctx = sentry::TransactionContext::new( + "Call upstream Uptime Kuma endpoint", + "request.call_kuma_endpoint", ); - if let Ok(body) = response.text().await { - println!("Response body: {}", body) + sentry::start_transaction(ctx).into() + } + }; + + let push_client = Client::builder().user_agent("Roselite/1.0").build()?; + + let mut push_url = Url::parse(upstream_url.as_str())?; + push_url + .query_pairs_mut() + .append_pair("msg", heartbeat.msg.as_str()) + .append_pair("status", heartbeat.status.to_string().as_str()) + .append_pair("ping", heartbeat.ping.to_string().as_str()); + + match push_client + .request(Method::GET, push_url) + .timeout(Duration::from_secs(60)) + .send() + .await + { + Ok(response) => { + if response.status() >= StatusCode::BAD_REQUEST { + println!( + "Received response status of {} during sending event to remote push url", + response.status() + ); + if let Ok(body) = response.text().await { + println!("Response body: {}", body) + } + return Ok(()); } - return Ok(()); + println!("Successfully sent an event to remote push url"); + Ok(()) } - println!("Successfully sent an event to remote push url"); - Ok(()) - } - Err(err) => { - println!( - "An error occurred during sending event to remote push url: {}", - err - ); - Err(err) - } - }?; + Err(err) => { + println!( + "An error occurred during sending event to remote push url: {}", + err + ); + span.clone().finish(); + Err(err) + } + }?; - Ok(()) -} + span.finish(); + Ok(()) + } -/// It calls the monitor endpoint to create a heartbeat that will be sent to the -/// push endpoint. -pub async fn perform_task(monitor: Monitor) -> Result { - let heartbeat = call_monitor_endpoint(monitor.clone()).await?; + /// It calls the monitor endpoint to create a heartbeat that will be sent to the + /// push endpoint. + pub async fn perform_task(&self, monitor: Monitor) -> Result { + // Retrieve the currently running span + let parent_span = sentry::configure_scope(|scope| scope.get_span()); + + let span: sentry::TransactionOrSpan = match &parent_span { + Some(parent) => parent + .start_child("request.perform_task", "Perform request task") + .into(), + None => { + let ctx = + sentry::TransactionContext::new("Perform request task", "request.perform_task"); + sentry::start_transaction(ctx).into() + } + }; - call_kuma_endpoint(monitor.clone().push_url, heartbeat.clone()).await?; + let heartbeat = match monitor.monitor_type { + MonitorType::HTTP => self.http_caller.call(monitor.clone())?, + MonitorType::ICMP => self.icmp_caller.call(monitor.clone())?, + }; - Ok(heartbeat) + self.call_kuma_endpoint(monitor.clone().push_url, heartbeat.clone()) + .await?; + + span.finish(); + + Ok(heartbeat) + } } diff --git a/roselite-server/src/handlers/remote_write.rs b/roselite-server/src/handlers/remote_write.rs index 9c94bf0..6abb5fa 100644 --- a/roselite-server/src/handlers/remote_write.rs +++ b/roselite-server/src/handlers/remote_write.rs @@ -1,32 +1,39 @@ -use crate::config::DynServerConfig; +use std::sync::Arc; + use anyhow::Result; use axum::extract::{Path, Query, State}; use axum::http::StatusCode; +use axum::response::IntoResponse; use axum::Json; use reqwest::Url; -use roselite_common::heartbeat::Heartbeat; -use roselite_request::call_kuma_endpoint; use sentry::integrations::anyhow::capture_anyhow; use sentry::{capture_message, Level}; use serde::{Deserialize, Serialize}; +use roselite_common::heartbeat::Heartbeat; +use roselite_request::RoseliteRequest; + +use crate::config::{ServerConfig, ServerOptions}; + #[derive(Clone, Serialize, Deserialize)] pub struct RemoteWriteResponse { pub ok: bool, } pub async fn remote_write( - State(server_config): State, Path(id): Path, Query(params): Query, -) -> (StatusCode, Json) { + State(server_config): State>, +) -> impl IntoResponse { + let request = Arc::new(RoseliteRequest::default()); let upstream_kuma = server_config.get_upstream_kuma(); if let Some(upstream_url) = upstream_kuma { match convert_to_upstream(upstream_url, id) { - Ok(push_url) => match call_kuma_endpoint(push_url, params).await { + Ok(push_url) => match request.call_kuma_endpoint(push_url, params).await { Ok(_) => (StatusCode::OK, Json(RemoteWriteResponse { ok: true })), Err(error) => { capture_anyhow(&error); + ( StatusCode::INTERNAL_SERVER_ERROR, Json(RemoteWriteResponse { ok: false }), diff --git a/roselite-server/src/lib.rs b/roselite-server/src/lib.rs index fa58de7..8d5bf6f 100644 --- a/roselite-server/src/lib.rs +++ b/roselite-server/src/lib.rs @@ -1,8 +1,10 @@ -use crate::config::ServerConfig; -use crate::routes::register_routes; +use std::net::SocketAddr; + use anyhow::Result; use axum::Server; -use std::net::SocketAddr; + +use crate::config::ServerConfig; +use crate::routes::register_routes; pub mod config; mod handlers; diff --git a/roselite-server/src/routes.rs b/roselite-server/src/routes.rs index 97cb392..67cd10f 100644 --- a/roselite-server/src/routes.rs +++ b/roselite-server/src/routes.rs @@ -1,14 +1,16 @@ -use crate::config::{DynServerConfig, ServerConfig}; +use std::sync::Arc; + +use axum::routing::get; +use axum::Router; + +use crate::config::ServerConfig; use crate::handlers::ping::ping; use crate::handlers::remote_write::remote_write; -use axum::routing::{get, post}; -use axum::Router; -use std::sync::Arc; pub fn register_routes(config: ServerConfig) -> Router { - let server_config = Arc::new(config) as DynServerConfig; + let server_config = Arc::new(config); Router::new() .route("/ping", get(ping)) - .route("/api/push/:id", post(remote_write)) + .route("/api/push/:id", get(remote_write).post(remote_write)) .with_state(server_config) } diff --git a/roselite/Cargo.toml b/roselite/Cargo.toml index 0959c92..f123c00 100644 --- a/roselite/Cargo.toml +++ b/roselite/Cargo.toml @@ -11,6 +11,7 @@ futures = "0.3.28" anyhow = "1.0.71" clap = { version = "4.3", features = ["derive"] } sentry = { version = "0.31", features = ["default", "anyhow"] } +sentry-tracing = { version = "0.31" } roselite-request = { path = "../roselite-request" } roselite-config = { path = "../roselite-config" } roselite-server = { path = "../roselite-server" } \ No newline at end of file diff --git a/roselite/src/main.rs b/roselite/src/main.rs index 1d60ef7..cd77f67 100644 --- a/roselite/src/main.rs +++ b/roselite/src/main.rs @@ -1,15 +1,18 @@ -mod cli; -mod monitor; +use std::{env, process}; + use anyhow::Result; use futures::future; -use std::{env, process}; use tokio::{signal, spawn}; -use crate::cli::cli; -use crate::monitor::configure_monitors; use roselite_config::Configuration; use roselite_server::config::ServerConfig; +use crate::cli::cli; +use crate::monitor::configure_monitors; + +mod cli; +mod monitor; + #[tokio::main] async fn main() -> Result<()> { let matches = cli().get_matches(); @@ -32,7 +35,26 @@ async fn main() -> Result<()> { Some(error_reporting) => error_reporting.sentry_dsn, None => String::new(), }); - let _guard = sentry::init(sentry_dsn); + let _guard = sentry::init(( + sentry_dsn, + sentry::ClientOptions { + environment: Some( + env::var("ENVIRONMENT") + .unwrap_or("production".to_string()) + .into(), + ), + sample_rate: env::var("SENTRY_SAMPLE_RATE") + .unwrap_or("1.0".to_string()) + .parse::() + .unwrap_or(1.0), + traces_sample_rate: env::var("SENTRY_TRACES_SAMPLE_RATE") + .unwrap_or("0.2".to_string()) + .parse::() + .unwrap_or(0.2), + attach_stacktrace: true, + ..Default::default() + }, + )); let mut handles = vec![]; @@ -71,6 +93,7 @@ async fn main() -> Result<()> { process::exit(0); } Err(err) => { + sentry::capture_error(&err); eprintln!("Unable to listen for shutdown signal: {}", err); } } diff --git a/roselite/src/monitor.rs b/roselite/src/monitor.rs index 25386d0..579dd74 100644 --- a/roselite/src/monitor.rs +++ b/roselite/src/monitor.rs @@ -1,32 +1,55 @@ -use roselite_config::Monitor; -use roselite_request::perform_task; use std::time::Duration; + +use sentry::integrations::anyhow::capture_anyhow; use tokio::spawn; use tokio::task::JoinHandle; use tokio::time::{sleep, Instant}; +use roselite_config::Monitor; +use roselite_request::http_caller::HttpCaller; +use roselite_request::icmp_caller::IcmpCaller; +use roselite_request::RoseliteRequest; + pub fn configure_monitors(monitors: Vec) -> Vec> { let mut handles: Vec> = vec![]; + // Build dependency for http_caller and icmp_caller + let http_caller = Box::new(HttpCaller::new()); + let icmp_caller = Box::new(IcmpCaller::new()); + + // Start the monitors for monitor in monitors { - println!("Starting monitor for {}", monitor.monitor_url); + println!("Starting monitor for {}", monitor.monitor_target); + let http_copy = http_caller.clone(); + let icmp_copy = icmp_caller.clone(); handles.push(spawn(async move { - let cloned_monitor: Monitor = monitor.clone(); - loop { + let tx_ctx = sentry::TransactionContext::new( + "Start Roselite monitor", + "roselite.start_monitor", + ); + let transaction = sentry::start_transaction(tx_ctx); + + // Bind the transaction / span to the scope: + sentry::configure_scope(|scope| scope.set_span(Some(transaction.clone().into()))); + + let request = RoseliteRequest::new(http_copy.clone(), icmp_copy.clone()); let current_time = Instant::now(); - if let Err(err) = perform_task(monitor.clone()).await { + if let Err(err) = request.perform_task(monitor.clone()).await { // Do nothing of this error + capture_anyhow(&err); eprintln!("Unexpected error during performing task: {}", err); } + transaction.finish(); + let elapsed = current_time.elapsed(); if 60 - elapsed.as_secs() > 0 { let sleeping_duration = Duration::from_secs(60 - elapsed.as_secs()); println!( "Monitor for {0} will be sleeping for {1} seconds", - cloned_monitor.monitor_url, + monitor.monitor_target, sleeping_duration.as_secs() ); sleep(sleeping_duration).await;