├── .dockerignore ├── .github ├── FUNDING.yml └── workflows │ ├── build.yml │ └── test.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── Dockerfile ├── LICENSE.md ├── PACKAGING.md ├── PROTOCOL.md ├── README.md ├── config.cfg ├── debian ├── changelog ├── compat ├── control ├── copyright ├── rules ├── source │ └── format ├── vigil.install ├── vigil.postinst └── vigil.service ├── dev ├── designs │ └── status_page.sketch └── workspaces │ ├── manager.paw │ └── reporter.paw ├── res └── assets │ ├── fonts │ └── open_sans │ │ ├── open_sans_bold.woff │ │ ├── open_sans_bold.woff2 │ │ ├── open_sans_light.woff │ │ ├── open_sans_light.woff2 │ │ ├── open_sans_regular.woff │ │ ├── open_sans_regular.woff2 │ │ ├── open_sans_semibold.woff │ │ └── open_sans_semibold.woff2 │ ├── images │ └── badges │ │ ├── color-dead-default.svg │ │ ├── color-healthy-default.svg │ │ ├── color-sick-default.svg │ │ ├── icon-dead-default.svg │ │ ├── icon-dead-large.svg │ │ ├── icon-healthy-default.svg │ │ ├── icon-healthy-large.svg │ │ ├── icon-sick-default.svg │ │ └── icon-sick-large.svg │ ├── javascripts │ └── index.js │ ├── public │ └── robots.txt │ ├── stylesheets │ ├── common.css │ └── index.css │ └── templates │ └── index.tera ├── scripts ├── build_packages.sh ├── release_binaries.sh └── sign_binaries.sh └── src ├── aggregator ├── manager.rs └── mod.rs ├── config ├── config.rs ├── defaults.rs ├── logger.rs ├── mod.rs ├── reader.rs └── regex.rs ├── main.rs ├── notifier ├── email.rs ├── generic.rs ├── gotify.rs ├── matrix.rs ├── mod.rs ├── pushover.rs ├── slack.rs ├── telegram.rs ├── twilio.rs ├── webex.rs ├── webhook.rs ├── xmpp.rs └── zulip.rs ├── prober ├── manager.rs ├── mod.rs ├── mode.rs ├── replica.rs ├── report.rs ├── states.rs └── status.rs └── responder ├── announcements.rs ├── context.rs ├── manager.rs ├── mod.rs ├── payload.rs └── routes.rs /.dockerignore: -------------------------------------------------------------------------------- 1 | target/* 2 | .cargo 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: valeriansaliou 4 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | tags: 4 | - "v*.*.*" 5 | 6 | name: Build and Release 7 | 8 | jobs: 9 | build-releases: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v2 15 | 16 | - name: Cache build artifacts 17 | id: cache-cargo 18 | uses: actions/cache@v4 19 | with: 20 | path: | 21 | ~/.cargo/bin 22 | ~/.cargo/registry 23 | ~/.cargo/git 24 | target 25 | key: build-${{ runner.os }}-cargo-any 26 | 27 | - name: Install Rust toolchain 28 | uses: actions-rs/toolchain@v1 29 | with: 30 | toolchain: stable 31 | components: rustfmt 32 | override: true 33 | 34 | - name: Install cross-compilation tools (if needed) 35 | run: which cross >/dev/null || cargo install cross 36 | 37 | - name: Verify versions 38 | run: rustc --version && rustup --version && cargo --version && cross --version 39 | 40 | - name: Get current tag 41 | id: current_tag 42 | uses: WyriHaximus/github-action-get-previous-tag@v1 43 | 44 | - name: Release package 45 | run: cargo publish --no-verify --token ${CRATES_TOKEN} 46 | env: 47 | CRATES_TOKEN: ${{ secrets.CRATES_TOKEN }} 48 | 49 | - name: Release binaries 50 | run: ./scripts/release_binaries.sh --version=${{ steps.current_tag.outputs.tag }} 51 | 52 | - name: Release new version 53 | uses: softprops/action-gh-release@v1 54 | with: 55 | tag_name: ${{ steps.current_tag.outputs.tag }} 56 | name: Vigil ${{ steps.current_tag.outputs.tag }} 57 | body: "⚠️ Changelog not yet provided." 58 | files: ./${{ steps.current_tag.outputs.tag }}-*.tar.gz 59 | env: 60 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 61 | 62 | build-packages: 63 | needs: build-releases 64 | runs-on: ubuntu-latest 65 | 66 | steps: 67 | - name: Checkout code 68 | uses: actions/checkout@v2 69 | 70 | - name: Build packages 71 | run: ./scripts/build_packages.sh 72 | 73 | - name: Push packages to Packagecloud 74 | uses: faucetsdn/action-packagecloud-upload-debian-packages@v1 75 | with: 76 | path: ./packages 77 | repo: ${{ secrets.PACKAGECLOUD_REPO }} 78 | token: ${{ secrets.PACKAGECLOUD_TOKEN }} 79 | 80 | build-docker: 81 | runs-on: ubuntu-latest 82 | 83 | steps: 84 | - name: Checkout code 85 | uses: actions/checkout@v2 86 | 87 | - name: Acquire Docker image metadata 88 | id: metadata 89 | uses: docker/metadata-action@v4 90 | with: 91 | images: valeriansaliou/vigil 92 | 93 | - name: Login to Docker Hub 94 | uses: docker/login-action@v2 95 | with: 96 | username: ${{ secrets.DOCKERHUB_USERNAME }} 97 | password: ${{ secrets.DOCKERHUB_TOKEN }} 98 | 99 | - name: Build and push Docker image 100 | uses: docker/build-push-action@v3 101 | with: 102 | context: . 103 | tags: ${{ steps.metadata.outputs.tags }} 104 | labels: ${{ steps.metadata.outputs.labels }} 105 | push: true 106 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | 3 | name: Test and Build 4 | 5 | jobs: 6 | test: 7 | strategy: 8 | matrix: 9 | os: [ubuntu-latest, macos-latest, windows-latest] 10 | rust-toolchain: [stable] 11 | fail-fast: false 12 | 13 | runs-on: ${{ matrix.os }} 14 | 15 | steps: 16 | - name: Checkout code 17 | uses: actions/checkout@v2 18 | 19 | - name: Cache build artifacts 20 | id: cache-cargo 21 | uses: actions/cache@v4 22 | with: 23 | path: | 24 | ~/.cargo/registry 25 | ~/.cargo/git 26 | target 27 | key: test-${{ runner.os }}-cargo-${{ matrix.rust-toolchain }} 28 | 29 | - name: Install Rust toolchain 30 | uses: actions-rs/toolchain@v1 31 | with: 32 | toolchain: ${{ matrix.rust-toolchain }} 33 | components: rustfmt 34 | override: true 35 | 36 | - name: Install build headers 37 | run: sudo apt-get install libstrophe-dev 38 | if: ${{ matrix.os == 'ubuntu-latest' }} 39 | 40 | - name: Verify versions 41 | run: rustc --version && rustup --version && cargo --version 42 | 43 | - name: Build code (default features) 44 | run: cargo build 45 | if: ${{ matrix.os != 'ubuntu-latest' }} 46 | 47 | - name: Build code (all features) 48 | run: cargo build --all-features 49 | if: ${{ matrix.os == 'ubuntu-latest' }} 50 | 51 | - name: Test code 52 | run: cargo test 53 | 54 | - name: Check code style 55 | run: cargo fmt -- --check 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/* 2 | .DS_Store 3 | *~ 4 | *# 5 | .cargo 6 | 7 | build/ 8 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "vigil-server" 3 | version = "1.27.0" 4 | description = "Microservices Status Page. Monitors a distributed infrastructure and sends alerts (Slack, SMS, etc.)." 5 | readme = "README.md" 6 | license = "MPL-2.0" 7 | edition = "2021" 8 | homepage = "https://github.com/valeriansaliou/vigil" 9 | repository = "https://github.com/valeriansaliou/vigil.git" 10 | keywords = ["microservices", "infrastructure", "status", "monitor", "slack"] 11 | categories = ["web-programming"] 12 | authors = ["Valerian Saliou "] 13 | exclude = ["dev/*"] 14 | 15 | [[bin]] 16 | name = "vigil" 17 | path = "src/main.rs" 18 | doc = false 19 | 20 | [dependencies] 21 | log = { version = "0.4", features = ["std"] } 22 | clap = { version = "4.3", features = ["std", "cargo"] } 23 | lazy_static = "1.4" 24 | time = { version = "0.3", features = ["formatting"] } 25 | serde = "1.0" 26 | serde_derive = "1.0" 27 | toml = "0.7" 28 | envsubst = "0.2" 29 | uuid = { version = "1.1", features = ["v4", "fast-rng"], default-features = false } 30 | regex = "1.6" 31 | url = { version = "2.2", default-features = false } 32 | url_serde = { version = "0.2", default-features = false } 33 | http = "0.2" 34 | http-serde = "1.1" 35 | indexmap = { version = "1.9", features = ["serde-1"] } 36 | actix-web = "4.3" 37 | actix-files = "0.6" 38 | actix-web-httpauth = "0.8" 39 | tera = { version = "1.19", default-features = false } 40 | native-tls = { version = "0.2", features = ["vendored"] } 41 | openssl-probe = "0.1" 42 | reqwest = { version = "0.11", features = ["native-tls-vendored", "gzip", "blocking", "json"], default-features = false } 43 | ping = "0.4" 44 | run_script = "0.10" 45 | lettre = { version = "0.10", features = ["smtp-transport", "native-tls", "hostname", "builder"], default-features = false, optional = true } 46 | libstrophe = { version = "0.17", optional = true } 47 | 48 | [features] 49 | default = ["notifier-email", "notifier-twilio", "notifier-slack", "notifier-zulip", "notifier-telegram", "notifier-pushover", "notifier-gotify", "notifier-matrix", "notifier-webex", "notifier-webhook"] 50 | notifier-email = ["lettre"] 51 | notifier-twilio = [] 52 | notifier-slack = [] 53 | notifier-zulip = [] 54 | notifier-telegram = [] 55 | notifier-pushover = [] 56 | notifier-gotify = [] 57 | notifier-matrix = [] 58 | notifier-webex = [] 59 | notifier-webhook = [] 60 | notifier-xmpp = ["libstrophe"] 61 | 62 | [profile.dev] 63 | opt-level = 0 64 | debug = true 65 | debug-assertions = true 66 | 67 | [profile.release] 68 | opt-level = "s" 69 | lto = true 70 | debug = false 71 | debug-assertions = false 72 | panic = "abort" 73 | strip = true 74 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rustlang/rust:nightly-buster AS build 2 | 3 | RUN apt-get update 4 | RUN apt-get install -y musl-tools 5 | 6 | RUN rustup --version 7 | RUN rustup target add x86_64-unknown-linux-musl 8 | 9 | RUN rustc --version && \ 10 | rustup --version && \ 11 | cargo --version 12 | 13 | WORKDIR /app 14 | COPY . /app 15 | RUN cargo clean && cargo build --release --target x86_64-unknown-linux-musl 16 | RUN strip ./target/x86_64-unknown-linux-musl/release/vigil 17 | 18 | FROM alpine:latest 19 | 20 | WORKDIR /usr/src/vigil 21 | 22 | COPY ./res/assets/ ./res/assets/ 23 | COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt 24 | COPY --from=build /app/target/x86_64-unknown-linux-musl/release/vigil /usr/local/bin/vigil 25 | 26 | CMD [ "vigil", "-c", "/etc/vigil.cfg" ] 27 | 28 | EXPOSE 8080 29 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | 6. Disclaimer of Warranty 262 | ------------------------- 263 | 264 | > Covered Software is provided under this License on an "as is" 265 | > basis, without warranty of any kind, either expressed, implied, or 266 | > statutory, including, without limitation, warranties that the 267 | > Covered Software is free of defects, merchantable, fit for a 268 | > particular purpose or non-infringing. The entire risk as to the 269 | > quality and performance of the Covered Software is with You. 270 | > Should any Covered Software prove defective in any respect, You 271 | > (not any Contributor) assume the cost of any necessary servicing, 272 | > repair, or correction. This disclaimer of warranty constitutes an 273 | > essential part of this License. No use of any Covered Software is 274 | > authorized under this License except under this disclaimer. 275 | 276 | 277 | 7. Limitation of Liability 278 | -------------------------- 279 | 280 | > Under no circumstances and under no legal theory, whether tort 281 | > (including negligence), contract, or otherwise, shall any 282 | > Contributor, or anyone who distributes Covered Software as 283 | > permitted above, be liable to You for any direct, indirect, 284 | > special, incidental, or consequential damages of any character 285 | > including, without limitation, damages for lost profits, loss of 286 | > goodwill, work stoppage, computer failure or malfunction, or any 287 | > and all other commercial damages or losses, even if such party 288 | > shall have been informed of the possibility of such damages. This 289 | > limitation of liability shall not apply to liability for death or 290 | > personal injury resulting from such party's negligence to the 291 | > extent applicable law prohibits such limitation. Some 292 | > jurisdictions do not allow the exclusion or limitation of 293 | > incidental or consequential damages, so this exclusion and 294 | > limitation may not apply to You. 295 | 296 | 8. Litigation 297 | ------------- 298 | 299 | Any litigation relating to this License may be brought only in the 300 | courts of a jurisdiction where the defendant maintains its principal 301 | place of business and such litigation shall be governed by laws of that 302 | jurisdiction, without reference to its conflict-of-law provisions. 303 | Nothing in this Section shall prevent a party's ability to bring 304 | cross-claims or counter-claims. 305 | 306 | 9. Miscellaneous 307 | ---------------- 308 | 309 | This License represents the complete agreement concerning the subject 310 | matter hereof. If any provision of this License is held to be 311 | unenforceable, such provision shall be reformed only to the extent 312 | necessary to make it enforceable. Any law or regulation which provides 313 | that the language of a contract shall be construed against the drafter 314 | shall not be used to construe this License against a Contributor. 315 | 316 | 10. Versions of the License 317 | --------------------------- 318 | 319 | 10.1. New Versions 320 | 321 | Mozilla Foundation is the license steward. Except as provided in Section 322 | 10.3, no one other than the license steward has the right to modify or 323 | publish new versions of this License. Each version will be given a 324 | distinguishing version number. 325 | 326 | 10.2. Effect of New Versions 327 | 328 | You may distribute the Covered Software under the terms of the version 329 | of the License under which You originally received the Covered Software, 330 | or under the terms of any subsequent version published by the license 331 | steward. 332 | 333 | 10.3. Modified Versions 334 | 335 | If you create software not governed by this License, and you want to 336 | create a new license for such software, you may create and use a 337 | modified version of this License if you rename the license and remove 338 | any references to the name of the license steward (except to note that 339 | such modified license differs from this License). 340 | 341 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 342 | Licenses 343 | 344 | If You choose to distribute Source Code Form that is Incompatible With 345 | Secondary Licenses under the terms of this version of the License, the 346 | notice described in Exhibit B of this License must be attached. 347 | 348 | Exhibit A - Source Code Form License Notice 349 | ------------------------------------------- 350 | 351 | This Source Code Form is subject to the terms of the Mozilla Public 352 | License, v. 2.0. If a copy of the MPL was not distributed with this 353 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 354 | 355 | If it is not possible or desirable to put the notice in a particular 356 | file, then You may include the notice in a location (such as a LICENSE 357 | file in a relevant directory) where a recipient would be likely to look 358 | for such a notice. 359 | 360 | You may add additional accurate notices of copyright ownership. 361 | 362 | Exhibit B - "Incompatible With Secondary Licenses" Notice 363 | --------------------------------------------------------- 364 | 365 | This Source Code Form is "Incompatible With Secondary Licenses", as 366 | defined by the Mozilla Public License, v. 2.0. 367 | -------------------------------------------------------------------------------- /PACKAGING.md: -------------------------------------------------------------------------------- 1 | Packaging 2 | ========= 3 | 4 | This file contains quick reminders and notes on how to package Vigil. 5 | 6 | We consider here the packaging flow of Vigil version `1.0.0` for Linux. 7 | 8 | 1. **How to bump Vigil version before a release:** 9 | 1. Bump version in `Cargo.toml` to `1.0.0` 10 | 2. Execute `cargo update` to bump `Cargo.lock` 11 | 3. Bump Debian package version in `debian/rules` to `1.0.0` 12 | 13 | 2. **How to build Vigil, package it and release it on Crates, GitHub, Docker Hub and Packagecloud (multiple architectures):** 14 | 1. Tag the latest Git commit corresponding to the release with tag `v1.0.0`, and push the tag 15 | 2. Wait for all release jobs to complete on the [actions](https://github.com/valeriansaliou/vigil/actions) page on GitHub 16 | 3. Download all release archives, and sign them locally using: `./scripts/sign_binaries.sh --version=1.0.0` 17 | 4. Publish a changelog and upload all the built archives, as well as their signatures on the [releases](https://github.com/valeriansaliou/vigil/releases) page on GitHub 18 | -------------------------------------------------------------------------------- /PROTOCOL.md: -------------------------------------------------------------------------------- 1 | Vigil Protocol 2 | ============== 3 | 4 | # Vigil Reporter HTTP API 5 | 6 | ## 1️⃣ Report a replica 7 | 8 | **Endpoint URL:** 9 | 10 | `HTTP POST https://status.example.com/reporter///` 11 | 12 | Where: 13 | 14 | * `node_id`: The parent node of the reporting replica 15 | * `probe_id`: The parent probe of the node 16 | 17 | **Request headers:** 18 | 19 | * Add an `Authorization` header with a `Basic` authentication where the password is your configured `reporter_token`. 20 | * Set the `Content-Type` to `application/json; charset=utf-8`, and ensure you submit the request data as UTF-8. 21 | 22 | **Request data:** 23 | 24 | Adjust the request data to your replica context and send it as `HTTP POST`: 25 | 26 | ```json 27 | { 28 | "replica": "", 29 | "interval": 30, 30 | 31 | "load": { 32 | "cpu": 0.30, 33 | "ram": 0.80 34 | } 35 | } 36 | ``` 37 | 38 | Where: 39 | 40 | * `replica`: The replica unique identifier (eg. the server LAN IP) 41 | * `interval`: The push interval (in seconds) 42 | * `load.cpu`: The general CPU load, from `0.00` to `1.00` (can be more than `1.00` if the CPU is overloaded) 43 | * `load.ram`: The general RAM load, from `0.00` to `1.00` 44 | 45 | ## 2️⃣ Flush a replica 46 | 47 | **Endpoint URL:** 48 | 49 | `HTTP DELETE https://status.example.com/reporter////` 50 | 51 | Where: 52 | 53 | * `node_id`: The parent node of the reporting replica 54 | * `probe_id`: The parent probe of the node 55 | * `replica_id`: The replica unique identifier (eg. the server LAN IP) 56 | 57 | **Request headers:** 58 | 59 | * Add an `Authorization` header with a `Basic` authentication where the password is your configured `reporter_token`. 60 | 61 | --- 62 | 63 | # Vigil Manager HTTP API 64 | 65 | ## 1️⃣ List published announcements 66 | 67 | **Endpoint URL:** 68 | 69 | `HTTP GET https://status.example.com/manager/announcements/` 70 | 71 | **Request headers:** 72 | 73 | * Add an `Authorization` header with a `Basic` authentication where the password is your configured `manager_token`. 74 | 75 | ## 2️⃣ Insert a new announcement 76 | 77 | **Endpoint URL:** 78 | 79 | `HTTP POST https://status.example.com/manager/announcement/` 80 | 81 | **Request headers:** 82 | 83 | * Add an `Authorization` header with a `Basic` authentication where the password is your configured `manager_token`. 84 | * Set the `Content-Type` to `application/json; charset=utf-8`, and ensure you submit the request data as UTF-8. 85 | 86 | **Request data:** 87 | 88 | Adjust the request data to your announcement and send it as `HTTP POST`: 89 | 90 | ```json 91 | { 92 | "title": "", 93 | "text": "<text>" 94 | } 95 | ``` 96 | 97 | Where: 98 | 99 | * `title`: The title for the announcement 100 | * `text`: The description text for the announcement (can be multi-line) 101 | 102 | ## 3️⃣ Retract a published announcement 103 | 104 | **Endpoint URL:** 105 | 106 | `HTTP DELETE https://status.example.com/manager/announcement/<announcement_id>/` 107 | 108 | Where: 109 | 110 | * `announcement_id`: The announcement identifier to be removed 111 | 112 | **Request headers:** 113 | 114 | * Add an `Authorization` header with a `Basic` authentication where the password is your configured `manager_token`. 115 | 116 | ## 4️⃣ List prober alerts 117 | 118 | **Endpoint URL:** 119 | 120 | `HTTP GET https://status.example.com/manager/prober/alerts/` 121 | 122 | **Request headers:** 123 | 124 | * Add an `Authorization` header with a `Basic` authentication where the password is your configured `manager_token`. 125 | 126 | ## 5️⃣ Resolve ignore rules for prober alerts 127 | 128 | **Endpoint URL:** 129 | 130 | `HTTP GET https://status.example.com/manager/prober/alerts/ignored/` 131 | 132 | **Request headers:** 133 | 134 | * Add an `Authorization` header with a `Basic` authentication where the password is your configured `manager_token`. 135 | 136 | ## 6️⃣ Update ignore rules for prober alerts 137 | 138 | **Endpoint URL:** 139 | 140 | `HTTP PUT https://status.example.com/manager/prober/alerts/ignored/` 141 | 142 | **Request headers:** 143 | 144 | * Add an `Authorization` header with a `Basic` authentication where the password is your configured `manager_token`. 145 | * Set the `Content-Type` to `application/json; charset=utf-8`, and ensure you submit the request data as UTF-8. 146 | 147 | **Request data:** 148 | 149 | Adjust the request data to your announcement and send it as `HTTP PUT`: 150 | 151 | ```json 152 | { 153 | "reminders_seconds": 600 154 | } 155 | ``` 156 | 157 | Where: 158 | 159 | * `reminders_seconds`: The number of seconds during which downtime reminders should not be sent anymore (skipped) 160 | -------------------------------------------------------------------------------- /config.cfg: -------------------------------------------------------------------------------- 1 | # Vigil 2 | # Microservices Status Page 3 | # Configuration file 4 | # Example: https://github.com/valeriansaliou/vigil/blob/master/config.cfg 5 | 6 | 7 | [server] 8 | 9 | log_level = "debug" 10 | inet = "[::1]:8080" 11 | workers = 4 12 | 13 | manager_token = "REPLACE_THIS_WITH_A_VERY_SECRET_KEY" 14 | reporter_token = "REPLACE_THIS_WITH_A_SECRET_KEY" 15 | 16 | [assets] 17 | 18 | path = "./res/assets/" 19 | 20 | [branding] 21 | 22 | page_title = "Crisp Status" 23 | page_url = "https://status.crisp.chat/" 24 | company_name = "Crisp IM SAS" 25 | icon_color = "#1972F5" 26 | icon_url = "https://valeriansaliou.github.io/vigil/images/crisp-icon.png" 27 | logo_color = "#1972F5" 28 | logo_url = "https://valeriansaliou.github.io/vigil/images/crisp-logo.svg" 29 | website_url = "https://crisp.chat/" 30 | support_url = "mailto:support@crisp.chat" 31 | custom_html = "" 32 | 33 | [metrics] 34 | 35 | poll_interval = 120 36 | poll_retry = 2 37 | 38 | poll_http_status_healthy_above = 200 39 | poll_http_status_healthy_below = 400 40 | 41 | poll_delay_dead = 10 42 | poll_delay_sick = 5 43 | 44 | poll_parallelism = 4 45 | 46 | push_delay_dead = 20 47 | 48 | push_system_cpu_sick_above = 0.90 49 | push_system_ram_sick_above = 0.90 50 | 51 | script_interval = 300 52 | 53 | script_parallelism = 2 54 | 55 | local_delay_dead = 40 56 | 57 | [plugins] 58 | 59 | [plugins.rabbitmq] 60 | 61 | api_url = "http://127.0.0.1:15672" 62 | auth_username = "rabbitmq-administrator" 63 | auth_password = "RABBITMQ_ADMIN_PASSWORD" 64 | virtualhost = "crisp" 65 | 66 | queue_ready_healthy_below = 500 67 | queue_nack_healthy_below = 100 68 | queue_ready_dead_above = 20000 69 | queue_nack_dead_above = 5000 70 | queue_loaded_retry_delay = 500 71 | 72 | [notify] 73 | 74 | startup_notification = true 75 | reminder_interval = 300 76 | reminder_backoff_function = "linear" 77 | reminder_backoff_limit = 3 78 | 79 | [notify.email] 80 | 81 | from = "status@crisp.chat" 82 | to = "status@crisp.chat" 83 | 84 | smtp_host = "localhost" 85 | smtp_port = 587 86 | smtp_username = "user-access" 87 | smtp_password = "user-password" 88 | smtp_encrypt = false 89 | 90 | [notify.twilio] 91 | 92 | to = [ 93 | "+336xxxxxxx", 94 | "+337xxxxxxx" 95 | ] 96 | 97 | service_sid = "service-sid" 98 | account_sid = "account-sid" 99 | auth_token = "auth-token" 100 | 101 | reminders_only = true 102 | 103 | [notify.slack] 104 | 105 | hook_url = "https://hooks.slack.com/services/xxxx" 106 | mention_channel = true 107 | 108 | [notify.zulip] 109 | 110 | bot_email = "bot-name@domain.zulipchat.com" 111 | bot_api_key = "xxxx" 112 | channel = "vigil" 113 | api_url = "https://domain.zulipchat.com/api/v1/" 114 | 115 | [notify.telegram] 116 | 117 | bot_token = "xxxxxxxxxx:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" 118 | chat_id = "xxxxxxxxx" 119 | 120 | [notify.pushover] 121 | 122 | app_token = "xxxx" 123 | user_keys = ["xxxx"] 124 | 125 | [notify.gotify] 126 | 127 | app_url = "https://push.gotify.net" 128 | app_token = "xxxx" 129 | 130 | [notify.xmpp] 131 | 132 | from = "vigil@valeriansaliou.name" 133 | to = "valerian@valeriansaliou.name" 134 | 135 | xmpp_password = "xmpp-password" 136 | 137 | [notify.matrix] 138 | 139 | homeserver_url = "https://matrix.org" 140 | access_token = "xxxx" 141 | room_id = "!abc123:matrix.org" 142 | 143 | [notify.webex] 144 | 145 | endpoint_url = "https://webexapis.com/v1/messages" 146 | token = "xxxxx" 147 | room_id = "yyyyy" 148 | 149 | [notify.webhook] 150 | 151 | hook_url = "https://domain.com/webhooks/xxxx" 152 | 153 | [probe] 154 | 155 | [[probe.service]] 156 | 157 | id = "web" 158 | label = "Web nodes" 159 | 160 | [[probe.service.node]] 161 | 162 | id = "router" 163 | label = "Core main router" 164 | mode = "poll" 165 | 166 | replicas = [ 167 | "icmp://edge-1.pool.net.crisp.chat", 168 | "icmp://edge-2.pool.net.crisp.chat" 169 | ] 170 | 171 | [[probe.service.node]] 172 | 173 | id = "load-balancer" 174 | label = "Core main load balancer" 175 | mode = "poll" 176 | 177 | replicas = [ 178 | "tcp://edge-1.pool.net.crisp.chat:80", 179 | "tcp://edge-2.pool.net.crisp.chat:80", 180 | "tcp://edge-3.pool.net.crisp.chat:80" 181 | ] 182 | 183 | [[probe.service.node]] 184 | 185 | id = "help" 186 | label = "Core help load balancer" 187 | mode = "poll" 188 | replicas = ["tcp://help-1.pool.net.crisp.chat:80"] 189 | 190 | [[probe.service.node]] 191 | 192 | id = "api" 193 | label = "Access to API service" 194 | mode = "poll" 195 | replicas = ["https://api.crisp.chat/v1/_system/health"] 196 | 197 | [[probe.service.node]] 198 | 199 | id = "status" 200 | label = "Access to status page" 201 | mode = "poll" 202 | replicas = ["https://status.crisp.chat/robots.txt"] 203 | http_body_healthy_match = "User-agent:.*" 204 | 205 | [[probe.service]] 206 | 207 | id = "relay" 208 | label = "Relay nodes" 209 | 210 | [[probe.service.node]] 211 | 212 | id = "socket-client" 213 | label = "Visitor realtime sockets" 214 | mode = "push" 215 | reveal_replica_name = true 216 | rabbitmq_queue = "client" 217 | rabbitmq_queue_nack_healthy_below = 100 218 | rabbitmq_queue_nack_dead_above = 1000 219 | 220 | [[probe.service]] 221 | 222 | id = "internal" 223 | label = "Internal nodes" 224 | 225 | [[probe.service.node]] 226 | 227 | id = "gateway" 228 | label = "Private gateway" 229 | mode = "local" 230 | 231 | [[probe.service.node]] 232 | 233 | id = "capacity" 234 | label = "Network capacity" 235 | mode = "local" 236 | 237 | [[probe.service]] 238 | 239 | id = "plugin" 240 | label = "Plugin nodes" 241 | 242 | [[probe.service.node]] 243 | 244 | id = "plugin-health" 245 | label = "Plugins health" 246 | mode = "script" 247 | link_url = "https://status.plugins.crisp.chat/" 248 | link_label = "See status details" 249 | 250 | scripts = [ 251 | ''' 252 | status=$(curl --silent --connect-timeout 3 https://status.plugins.crisp.chat/status/text) 253 | 254 | if [ -z "$status" ]; then 255 | exit 2 256 | fi 257 | 258 | if [ "$status" = "healthy" ]; then 259 | exit 0 260 | fi 261 | 262 | exit 1 263 | ''' 264 | ] 265 | -------------------------------------------------------------------------------- /debian/changelog: -------------------------------------------------------------------------------- 1 | vigil (0.0.0-1) UNRELEASED; urgency=medium 2 | 3 | * Initial release. 4 | 5 | -- Valerian Saliou <valerian@valeriansaliou.name> Tue, 31 Aug 2023 12:00:00 +0000 6 | -------------------------------------------------------------------------------- /debian/compat: -------------------------------------------------------------------------------- 1 | 10 2 | -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: vigil 2 | Section: net 3 | Priority: ext 4 | Maintainer: Valerian Saliou <valerian@valeriansaliou.name> 5 | Standards-Version: 3.9.4 6 | Build-Depends: wget, ca-certificates 7 | Homepage: https://github.com/valeriansaliou/vigil 8 | 9 | Package: vigil 10 | Architecture: any 11 | Depends: adduser 12 | Provides: vigil 13 | Description: Microservices Status Page. Monitors a distributed infrastructure and sends alerts. 14 | -------------------------------------------------------------------------------- /debian/copyright: -------------------------------------------------------------------------------- 1 | Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ 2 | Upstream-Name: vigil 3 | Upstream-Contact: Valerian Saliou <valerian@valeriansaliou.name> 4 | Source: https://github.com/valeriansaliou/vigil 5 | 6 | Files: * 7 | Copyright: 2023 Valerian Saliou 8 | License: MPL-2 9 | 10 | License: MPL-2 11 | This Source Code Form is subject to the terms of the Mozilla Public License, 12 | v. 2.0. If a copy of the MPL was not distributed with this file, 13 | You can obtain one at http://mozilla.org/MPL/2.0/. 14 | -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | 3 | DISTRIBUTION = $(shell lsb_release -sr) 4 | VERSION = 1.27.0 5 | PACKAGEVERSION = $(VERSION)-0~$(DISTRIBUTION)0 6 | URL = https://github.com/valeriansaliou/vigil/releases/download/v$(VERSION)/ 7 | 8 | %: 9 | dh $@ --with systemd 10 | 11 | override_dh_auto_clean: 12 | override_dh_auto_test: 13 | override_dh_auto_build: 14 | override_dh_auto_install: 15 | $(eval ENV_ARCH := $(shell dpkg --print-architecture)) 16 | $(eval ENV_ISA := $(shell if [ "$(ENV_ARCH)" = "amd64" ]; then echo "x86_64"; else echo "$(ENV_ARCH)"; fi)) 17 | $(eval ENV_TARBALL := v$(VERSION)-$(ENV_ISA).tar.gz) 18 | 19 | echo "Architecture: $(ENV_ARCH)" 20 | echo "Instruction Set: $(ENV_ISA)" 21 | echo "Target: $(URL)$(ENV_TARBALL)" 22 | 23 | wget -N --progress=dot:mega $(URL)$(ENV_TARBALL) 24 | tar -xf $(ENV_TARBALL) 25 | strip vigil/vigil 26 | mv vigil/config.cfg vigil/vigil.cfg 27 | mv vigil/res/assets/ vigil/assets/ 28 | rm -r vigil/res/ 29 | sed -i 's/path = ".\/res\/assets\/"/path = "\/etc\/vigil\/assets\/"/g' vigil/vigil.cfg 30 | 31 | override_dh_gencontrol: 32 | dh_gencontrol -- -v$(PACKAGEVERSION) 33 | -------------------------------------------------------------------------------- /debian/source/format: -------------------------------------------------------------------------------- 1 | 3.0 (quilt) 2 | -------------------------------------------------------------------------------- /debian/vigil.install: -------------------------------------------------------------------------------- 1 | vigil/vigil usr/bin/ 2 | vigil/vigil.cfg etc/vigil/ 3 | vigil/assets/ etc/vigil/ 4 | -------------------------------------------------------------------------------- /debian/vigil.postinst: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | case "$1" in 6 | configure) 7 | adduser --system --disabled-password --disabled-login --home /var/empty \ 8 | --no-create-home --quiet --group vigil 9 | ;; 10 | esac 11 | 12 | #DEBHELPER# 13 | 14 | exit 0 15 | -------------------------------------------------------------------------------- /debian/vigil.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Microservices Status Page 3 | After=network.target 4 | 5 | [Service] 6 | Type=simple 7 | User=vigil 8 | Group=vigil 9 | ExecStartPre=/sbin/setcap 'cap_net_raw+ep' /bin/vigil 10 | ExecStart=/usr/bin/vigil -c /etc/vigil/vigil.cfg 11 | Restart=on-failure 12 | PermissionsStartOnly=true 13 | LimitNOFILE=infinity 14 | 15 | [Install] 16 | WantedBy=multi-user.target 17 | -------------------------------------------------------------------------------- /dev/designs/status_page.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valeriansaliou/vigil/fa655c250a917cf93583095264cd0333811b6a89/dev/designs/status_page.sketch -------------------------------------------------------------------------------- /dev/workspaces/manager.paw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valeriansaliou/vigil/fa655c250a917cf93583095264cd0333811b6a89/dev/workspaces/manager.paw -------------------------------------------------------------------------------- /dev/workspaces/reporter.paw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valeriansaliou/vigil/fa655c250a917cf93583095264cd0333811b6a89/dev/workspaces/reporter.paw -------------------------------------------------------------------------------- /res/assets/fonts/open_sans/open_sans_bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valeriansaliou/vigil/fa655c250a917cf93583095264cd0333811b6a89/res/assets/fonts/open_sans/open_sans_bold.woff -------------------------------------------------------------------------------- /res/assets/fonts/open_sans/open_sans_bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valeriansaliou/vigil/fa655c250a917cf93583095264cd0333811b6a89/res/assets/fonts/open_sans/open_sans_bold.woff2 -------------------------------------------------------------------------------- /res/assets/fonts/open_sans/open_sans_light.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valeriansaliou/vigil/fa655c250a917cf93583095264cd0333811b6a89/res/assets/fonts/open_sans/open_sans_light.woff -------------------------------------------------------------------------------- /res/assets/fonts/open_sans/open_sans_light.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valeriansaliou/vigil/fa655c250a917cf93583095264cd0333811b6a89/res/assets/fonts/open_sans/open_sans_light.woff2 -------------------------------------------------------------------------------- /res/assets/fonts/open_sans/open_sans_regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valeriansaliou/vigil/fa655c250a917cf93583095264cd0333811b6a89/res/assets/fonts/open_sans/open_sans_regular.woff -------------------------------------------------------------------------------- /res/assets/fonts/open_sans/open_sans_regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valeriansaliou/vigil/fa655c250a917cf93583095264cd0333811b6a89/res/assets/fonts/open_sans/open_sans_regular.woff2 -------------------------------------------------------------------------------- /res/assets/fonts/open_sans/open_sans_semibold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valeriansaliou/vigil/fa655c250a917cf93583095264cd0333811b6a89/res/assets/fonts/open_sans/open_sans_semibold.woff -------------------------------------------------------------------------------- /res/assets/fonts/open_sans/open_sans_semibold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valeriansaliou/vigil/fa655c250a917cf93583095264cd0333811b6a89/res/assets/fonts/open_sans/open_sans_semibold.woff2 -------------------------------------------------------------------------------- /res/assets/images/badges/color-dead-default.svg: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8"?> 2 | <svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> 3 | <defs></defs> 4 | <g id="Color" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" transform="translate(-45.000000, -81.000000)"> 5 | <rect id="Dead" fill="#E10000" x="45" y="81" width="20" height="20"></rect> 6 | </g> 7 | </svg> 8 | -------------------------------------------------------------------------------- /res/assets/images/badges/color-healthy-default.svg: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8"?> 2 | <svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> 3 | <defs></defs> 4 | <g id="Color" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" transform="translate(-45.000000, -41.000000)"> 5 | <rect id="Healthy" fill="#0EB033" x="45" y="41" width="20" height="20"></rect> 6 | </g> 7 | </svg> 8 | -------------------------------------------------------------------------------- /res/assets/images/badges/color-sick-default.svg: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8"?> 2 | <svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> 3 | <defs></defs> 4 | <g id="Color" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" transform="translate(-85.000000, -41.000000)"> 5 | <rect id="Sick" fill="#F18000" x="85" y="41" width="20" height="20"></rect> 6 | </g> 7 | </svg> 8 | -------------------------------------------------------------------------------- /res/assets/images/badges/icon-dead-default.svg: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8"?> 2 | <svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> 3 | <defs></defs> 4 | <g id="Status-Page" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" transform="translate(-286.000000, -979.000000)"> 5 | <g id="Content" transform="translate(190.000000, 163.000000)"> 6 | <g id="Node-Boxes" transform="translate(0.000000, 154.000000)"> 7 | <g id="Box-Wrap" transform="translate(0.000000, 638.000000)"> 8 | <g id="Box"> 9 | <g id="Title" transform="translate(96.000000, 24.000000)"> 10 | <g id="Icon"> 11 | <rect id="Box" fill="#E10000" x="0" y="0" width="20" height="20" rx="2"></rect> 12 | <g id="Checkmark" transform="translate(6.000000, 6.000000)" stroke="#FFFFFF" stroke-width="2.5"> 13 | <path d="M8,0 L0.028978103,7.9710219" id="Line"></path> 14 | <path d="M8,0 L0.028978103,7.9710219" id="Line" transform="translate(4.014489, 3.985511) scale(-1, 1) translate(-4.014489, -3.985511) "></path> 15 | </g> 16 | </g> 17 | </g> 18 | </g> 19 | </g> 20 | </g> 21 | </g> 22 | </g> 23 | </svg> 24 | -------------------------------------------------------------------------------- /res/assets/images/badges/icon-dead-large.svg: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8"?> 2 | <svg width="42px" height="42px" viewBox="0 0 42 42" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> 3 | <defs></defs> 4 | <g id="Status-Page" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" transform="translate(-286.000000, -187.000000)"> 5 | <g id="Content" transform="translate(190.000000, 163.000000)"> 6 | <g id="Box-Wrap"> 7 | <g id="Box-(Dead)"> 8 | <g id="Inner" transform="translate(96.000000, 20.000000)"> 9 | <g id="Icon" transform="translate(0.000000, 4.000000)"> 10 | <rect id="Box" fill="#E10000" x="0" y="0" width="42" height="42" rx="2"></rect> 11 | <g id="Checkmark" transform="translate(12.600000, 12.600000)" stroke="#FFFFFF" stroke-width="4"> 12 | <path d="M16.8,0 L0.0608540163,16.739146" id="Line"></path> 13 | <path d="M16.8,0 L0.0608540163,16.739146" id="Line" transform="translate(8.430427, 8.369573) scale(-1, 1) translate(-8.430427, -8.369573) "></path> 14 | </g> 15 | </g> 16 | </g> 17 | </g> 18 | </g> 19 | </g> 20 | </g> 21 | </svg> 22 | -------------------------------------------------------------------------------- /res/assets/images/badges/icon-healthy-default.svg: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8"?> 2 | <svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> 3 | <defs></defs> 4 | <g id="Status-Page" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" transform="translate(-286.000000, -341.000000)"> 5 | <g id="Content" transform="translate(190.000000, 163.000000)"> 6 | <g id="Node-Boxes" transform="translate(0.000000, 154.000000)"> 7 | <g id="Box-Wrap"> 8 | <g id="Box"> 9 | <g id="Title" transform="translate(96.000000, 24.000000)"> 10 | <g id="Icon"> 11 | <rect id="Box" fill="#0EB033" x="0" y="0" width="20" height="20" rx="2"></rect> 12 | <g id="Checkmark" transform="translate(5.250000, 6.500000)" stroke="#FFFFFF" stroke-linecap="square" stroke-width="2.5"> 13 | <path d="M0,4 L3,7" id="Line"></path> 14 | <path d="M9.5,0.5 L3,7" id="Line"></path> 15 | </g> 16 | </g> 17 | </g> 18 | </g> 19 | </g> 20 | </g> 21 | </g> 22 | </g> 23 | </svg> 24 | -------------------------------------------------------------------------------- /res/assets/images/badges/icon-healthy-large.svg: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8"?> 2 | <svg width="42px" height="42px" viewBox="0 0 42 42" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> 3 | <defs></defs> 4 | <g id="Status-Page" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" transform="translate(-286.000000, -187.000000)"> 5 | <g id="Content" transform="translate(190.000000, 163.000000)"> 6 | <g id="Box-Wrap"> 7 | <g id="Box-(Healthy)"> 8 | <g id="Inner" transform="translate(96.000000, 20.000000)"> 9 | <g id="Icon" transform="translate(0.000000, 4.000000)"> 10 | <rect id="Box" fill="#0EB033" x="0" y="0" width="42" height="42" rx="2"></rect> 11 | <g id="Checkmark" transform="translate(11.025000, 13.650000)" stroke="#FFFFFF" stroke-linecap="square" stroke-width="4"> 12 | <path d="M0,8.4 L6.3,14.7" id="Line"></path> 13 | <path d="M19.95,1.05 L6.3,14.7" id="Line"></path> 14 | </g> 15 | </g> 16 | </g> 17 | </g> 18 | </g> 19 | </g> 20 | </g> 21 | </svg> 22 | -------------------------------------------------------------------------------- /res/assets/images/badges/icon-sick-default.svg: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8"?> 2 | <svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> 3 | <defs></defs> 4 | <g id="Status-Page" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" transform="translate(-286.000000, -747.000000)"> 5 | <g id="Content" transform="translate(190.000000, 163.000000)"> 6 | <g id="Node-Boxes" transform="translate(0.000000, 154.000000)"> 7 | <g id="Box-Wrap" transform="translate(0.000000, 406.000000)"> 8 | <g id="Box"> 9 | <g id="Title" transform="translate(96.000000, 24.000000)"> 10 | <g id="Icon"> 11 | <rect id="Box" fill="#F18000" x="0" y="0" width="20" height="20" rx="2"></rect> 12 | <g id="Checkmark" transform="translate(7.500000, 3.500000)" fill="#FFFFFF"> 13 | <path d="M3.82653809,8.140625 L1.24841309,8.140625 L0.849975586,0.75 L4.22497559,0.75 L3.82653809,8.140625 Z M0.881225586,10.875 C0.881225586,10.4010393 1.02184918,10.0325534 1.30310059,9.76953125 C1.58435199,9.5065091 1.99320207,9.375 2.52966309,9.375 C3.06091574,9.375 3.46325547,9.5065091 3.73669434,9.76953125 C4.0101332,10.0325534 4.14685059,10.4010393 4.14685059,10.875 C4.14685059,11.3437523 4.00492492,11.7096341 3.72106934,11.9726562 C3.43721375,12.2356784 3.0400823,12.3671875 2.52966309,12.3671875 C2.01403551,12.3671875 1.61039371,12.2356784 1.31872559,11.9726562 C1.02705746,11.7096341 0.881225586,11.3437523 0.881225586,10.875 Z" id="Mark"></path> 14 | </g> 15 | </g> 16 | </g> 17 | </g> 18 | </g> 19 | </g> 20 | </g> 21 | </g> 22 | </svg> 23 | -------------------------------------------------------------------------------- /res/assets/images/badges/icon-sick-large.svg: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8"?> 2 | <svg width="42px" height="42px" viewBox="0 0 42 42" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> 3 | <defs></defs> 4 | <g id="Status-Page" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" transform="translate(-286.000000, -187.000000)"> 5 | <g id="Content" transform="translate(190.000000, 163.000000)"> 6 | <g id="Box-Wrap"> 7 | <g id="Box-(Sick)"> 8 | <g id="Inner" transform="translate(96.000000, 20.000000)"> 9 | <g id="Icon" transform="translate(0.000000, 4.000000)"> 10 | <rect id="Box" fill="#F18000" x="0" y="0" width="42" height="42" rx="2"></rect> 11 | <g id="Checkmark" transform="translate(15.750000, 7.350000)" fill="#FFFFFF"> 12 | <path d="M8.03572998,17.0953125 L2.62166748,17.0953125 L1.78494873,1.575 L8.87244873,1.575 L8.03572998,17.0953125 Z M1.85057373,22.8375 C1.85057373,21.8421825 2.14588328,21.0683621 2.73651123,20.5160156 C3.32713918,19.9636691 4.18572435,19.6875 5.31229248,19.6875 C6.42792306,19.6875 7.27283648,19.9636691 7.84705811,20.5160156 C8.42127973,21.0683621 8.70838623,21.8421825 8.70838623,22.8375 C8.70838623,23.8218799 8.41034234,24.5902316 7.81424561,25.1425781 C7.21814888,25.6949246 6.38417284,25.9710937 5.31229248,25.9710937 C4.22947457,25.9710937 3.38182679,25.6949246 2.76932373,25.1425781 C2.15682067,24.5902316 1.85057373,23.8218799 1.85057373,22.8375 Z" id="Mark"></path> 13 | </g> 14 | </g> 15 | </g> 16 | </g> 17 | </g> 18 | </g> 19 | </g> 20 | </svg> 21 | -------------------------------------------------------------------------------- /res/assets/javascripts/index.js: -------------------------------------------------------------------------------- 1 | var IndexManager = (function() { 2 | return { 3 | _REFRESH_TIMEOUT : 5000, 4 | _REFRESH_INTERVAL : 20000, 5 | 6 | _SELECTOR_ASIDE : null, 7 | _SELECTOR_MAIN : null, 8 | 9 | bind : function() { 10 | IndexManager._SELECTOR_ASIDE = ( 11 | (document.getElementsByTagName("aside") || [])[0] 12 | ); 13 | IndexManager._SELECTOR_MAIN = ( 14 | (document.getElementsByTagName("main") || [])[0] 15 | ); 16 | 17 | if (IndexManager._SELECTOR_ASIDE && IndexManager._SELECTOR_MAIN) { 18 | IndexManager.__schedule_refresh(); 19 | } 20 | }, 21 | 22 | __schedule_refresh : function() { 23 | setTimeout(function() { 24 | IndexManager.__load( 25 | "/status/text/", "text", 26 | 27 | IndexManager.__handle_status_text_done_from_request, 28 | IndexManager.__handle_status_text_error 29 | ); 30 | }, IndexManager._REFRESH_INTERVAL); 31 | }, 32 | 33 | __handle_status_text_done_from_request : function(request) { 34 | IndexManager.__handle_status_text_done( 35 | request.responseText || window.STATUS_GENERAL 36 | ); 37 | }, 38 | 39 | __handle_status_text_done : function(status) { 40 | if (status !== window.STATUS_GENERAL) { 41 | window.STATUS_GENERAL = status; 42 | 43 | IndexManager.__load( 44 | "/", "document", 45 | 46 | IndexManager.__handle_base_done, 47 | IndexManager.__handle_base_error 48 | ); 49 | } else { 50 | IndexManager.__schedule_refresh(); 51 | } 52 | }, 53 | 54 | __handle_status_text_error : function() { 55 | IndexManager.__handle_status_text_done( 56 | window.STATUS_GENERAL 57 | ); 58 | }, 59 | 60 | __handle_base_done : function(request) { 61 | if (request && request.response && request.response.body) { 62 | var aside_sel = ( 63 | request.response.body.getElementsByTagName("aside") || [] 64 | )[0]; 65 | var main_sel = ( 66 | request.response.body.getElementsByTagName("main") || [] 67 | )[0]; 68 | 69 | if (aside_sel && main_sel) { 70 | IndexManager._SELECTOR_ASIDE.parentNode.replaceChild( 71 | aside_sel, IndexManager._SELECTOR_ASIDE 72 | ); 73 | IndexManager._SELECTOR_MAIN.parentNode.replaceChild( 74 | main_sel, IndexManager._SELECTOR_MAIN 75 | ); 76 | 77 | IndexManager._SELECTOR_ASIDE = aside_sel; 78 | IndexManager._SELECTOR_MAIN = main_sel; 79 | } 80 | } 81 | 82 | IndexManager.__schedule_refresh(); 83 | }, 84 | 85 | __handle_base_error : function() { 86 | IndexManager.__handle_base_done(null); 87 | }, 88 | 89 | __load : function(path, type, fn_handle_done, fn_handle_error) { 90 | var request = new XMLHttpRequest(); 91 | 92 | request.open("GET", path, true); 93 | 94 | request.responseType = type; 95 | request.timeout = IndexManager._REFRESH_TIMEOUT; 96 | 97 | request.onreadystatechange = function() { 98 | // Request finished. 99 | if (request.readyState === 4) { 100 | if (request.status === 200) { 101 | if (typeof fn_handle_done === "function") { 102 | fn_handle_done(request); 103 | } 104 | } else { 105 | if (typeof fn_handle_error === "function") { 106 | fn_handle_error(request); 107 | } 108 | } 109 | } 110 | }; 111 | 112 | request.send(); 113 | } 114 | }; 115 | })(); 116 | 117 | 118 | window.onload = function() { 119 | IndexManager.bind(); 120 | }; 121 | -------------------------------------------------------------------------------- /res/assets/public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /res/assets/stylesheets/common.css: -------------------------------------------------------------------------------- 1 | @charset "UTF-8"; 2 | 3 | @font-face { 4 | font-family: "Vigil Open Sans Light"; 5 | src: url("/assets/fonts/open_sans/open_sans_light.woff2") format("woff2"), url("/assets/fonts/open_sans/open_sans_light.woff") format("woff"); 6 | font-weight: 100; 7 | font-style: normal; 8 | } 9 | 10 | @font-face { 11 | font-family: "Vigil Open Sans Regular"; 12 | src: url("/assets/fonts/open_sans/open_sans_regular.woff2") format("woff2"), url("/assets/fonts/open_sans/open_sans_regular.woff") format("woff"); 13 | font-weight: 400; 14 | font-style: normal; 15 | } 16 | 17 | @font-face { 18 | font-family: "Vigil Open Sans Semibold"; 19 | src: url("/assets/fonts/open_sans/open_sans_semibold.woff2") format("woff2"), url("/assets/fonts/open_sans/open_sans_semibold.woff") format("woff"); 20 | font-weight: 600; 21 | font-style: normal; 22 | } 23 | 24 | @font-face { 25 | font-family: "Vigil Open Sans Bold"; 26 | src: url("/assets/fonts/open_sans/open_sans_bold.woff2") format("woff2"), url("/assets/fonts/open_sans/open_sans_bold.woff") format("woff"); 27 | font-weight: 700; 28 | font-style: normal; 29 | } 30 | 31 | .font-sans-light { 32 | font-family: "Vigil Open Sans Light", sans-serif; 33 | } 34 | 35 | .font-sans-regular { 36 | font-family: "Vigil Open Sans Regular", sans-serif; 37 | } 38 | 39 | .font-sans-semibold { 40 | font-family: "Vigil Open Sans Semibold", sans-serif; 41 | } 42 | 43 | .font-sans-bold { 44 | font-family: "Vigil Open Sans Bold", sans-serif; 45 | } 46 | 47 | body { 48 | font-family: "Vigil Open Sans Regular", sans-serif; 49 | font-size: 14px; 50 | background: #F7F8FA !important; 51 | color: rgba(0, 0, 0, 0.95); 52 | } 53 | 54 | * { 55 | font-weight: normal !important; 56 | margin: 0; 57 | padding: 0; 58 | } 59 | 60 | a, 61 | button { 62 | outline: 0 none !important; 63 | } 64 | 65 | a { 66 | text-decoration: none; 67 | cursor: pointer; 68 | } 69 | 70 | a:hover { 71 | cursor: pointer; 72 | } 73 | 74 | .clear { 75 | display: block !important; 76 | clear: both !important; 77 | } 78 | 79 | .wrapper { 80 | width: 980px; 81 | max-width: 980px; 82 | margin: 0 auto; 83 | display: block; 84 | } 85 | 86 | .badge { 87 | background-size: contain; 88 | background-repeat: no-repeat; 89 | background-position: center; 90 | display: inline-block; 91 | } 92 | 93 | .badge.badge-default { 94 | width: 20px; 95 | height: 20px; 96 | } 97 | 98 | .badge.badge-large { 99 | width: 42px; 100 | height: 42px; 101 | } 102 | 103 | .has-tooltip { 104 | cursor: crosshair; 105 | position: relative; 106 | } 107 | 108 | .has-tooltip .tooltip { 109 | line-height: 19px; 110 | text-align: center; 111 | cursor: default; 112 | pointer-events: none; 113 | width: 500px; 114 | margin-left: -250px; 115 | margin-top: 6px; 116 | display: none; 117 | position: absolute; 118 | top: 100%; 119 | left: 50%; 120 | z-index: 100; 121 | } 122 | 123 | .has-tooltip .tooltip:after { 124 | content: ""; 125 | border-bottom: 4px solid rgba(0, 0, 0, 0.94); 126 | border-left: 4px solid transparent; 127 | border-right: 4px solid transparent; 128 | height: 0; 129 | width: 0; 130 | margin-left: -4px; 131 | position: absolute; 132 | left: 50%; 133 | top: -4px; 134 | } 135 | 136 | .has-tooltip .tooltip .tooltip-value { 137 | background-color: rgba(0, 0, 0, 0.94); 138 | font-size: 11px; 139 | line-height: 16px; 140 | text-align: center; 141 | text-shadow: none; 142 | text-transform: initial; 143 | display: inline-block; 144 | left: 50%; 145 | border-radius: 1px; 146 | } 147 | 148 | .has-tooltip .tooltip .tooltip-value .tooltip-value-text { 149 | padding: 6px 20px 7px; 150 | display: block; 151 | } 152 | 153 | .has-tooltip .tooltip .tooltip-value .tooltip-value-text .tooltip-main, 154 | .has-tooltip .tooltip .tooltip-value .tooltip-value-text .tooltip-label { 155 | display: block; 156 | } 157 | 158 | .has-tooltip .tooltip .tooltip-value .tooltip-value-text .tooltip-main { 159 | color: #FFFFFF; 160 | } 161 | 162 | .has-tooltip .tooltip .tooltip-value .tooltip-value-text .tooltip-label { 163 | color: rgba(255, 255, 255, 0.75); 164 | margin-top: 1px; 165 | } 166 | 167 | .has-tooltip .tooltip .tooltip-value .tooltip-value-details { 168 | border-top: 1px solid rgba(255, 255, 255, 0.2); 169 | padding: 7px 20px 9px; 170 | display: block; 171 | } 172 | 173 | .has-tooltip .tooltip .tooltip-value .tooltip-value-details .tooltip-detail:after { 174 | content: ""; 175 | background-color: rgba(255, 255, 255, 0.4); 176 | vertical-align: sub; 177 | width: 1px; 178 | height: 13px; 179 | margin: 0 8px; 180 | display: inline-block; 181 | } 182 | 183 | .has-tooltip .tooltip .tooltip-value .tooltip-value-details .tooltip-detail:last-child:after { 184 | display: none; 185 | } 186 | 187 | .has-tooltip .tooltip .tooltip-value .tooltip-value-details .tooltip-detail-label { 188 | text-decoration: underline; 189 | } 190 | 191 | .has-tooltip:hover .tooltip { 192 | display: block; 193 | pointer-events: auto; 194 | } 195 | 196 | .badge.badge-status-healthy.badge-default { 197 | background-image: url("/assets/images/badges/icon-healthy-default.svg"); 198 | } 199 | 200 | .badge.badge-status-healthy.badge-large { 201 | background-image: url("/assets/images/badges/icon-healthy-large.svg"); 202 | } 203 | 204 | .status-healthy-background, 205 | .status-healthy-background-before:before { 206 | background-color: #0EB033; 207 | } 208 | 209 | .status-healthy-color { 210 | color: #0EB033; 211 | } 212 | 213 | .status-healthy-background-subtle { 214 | background-color: rgba(14, 176, 51, 0.10); 215 | } 216 | 217 | .status-healthy-border-subtle { 218 | border-color: rgba(14, 176, 51, 0.17); 219 | } 220 | 221 | .badge-status-sick.badge-default { 222 | background-image: url("/assets/images/badges/icon-sick-default.svg"); 223 | } 224 | 225 | .badge-status-sick.badge-large { 226 | background-image: url("/assets/images/badges/icon-sick-large.svg"); 227 | } 228 | 229 | .status-sick-background, 230 | .status-sick-background-before:before { 231 | background-color: #F18000; 232 | } 233 | 234 | .status-sick-color { 235 | color: #F18000; 236 | } 237 | 238 | .status-sick-background-subtle { 239 | background-color: rgba(241, 128, 0, 0.10); 240 | } 241 | 242 | .status-sick-border-subtle { 243 | border-color: rgba(241, 128, 0, 0.17); 244 | } 245 | 246 | .badge-status-dead.badge-default { 247 | background-image: url("/assets/images/badges/icon-dead-default.svg"); 248 | } 249 | 250 | .badge-status-dead.badge-large { 251 | background-image: url("/assets/images/badges/icon-dead-large.svg"); 252 | } 253 | 254 | .status-dead-background, 255 | .status-dead-background-before:before { 256 | background-color: #E10000; 257 | } 258 | 259 | .status-dead-color { 260 | color: #E10000; 261 | } 262 | 263 | .status-dead-background-subtle { 264 | background-color: rgba(225, 0, 0, 0.08); 265 | } 266 | 267 | .status-dead-border-subtle { 268 | border-color: rgba(225, 0, 0, 0.10); 269 | } 270 | 271 | .alert-announce-background, 272 | .alert-announce-background-before:before { 273 | background-color: #1972F5; 274 | } 275 | 276 | @media screen and (max-width: 1020px) { 277 | .wrapper { 278 | width: calc(100% - 40px); 279 | max-width: calc(100% - 40px); 280 | } 281 | } 282 | 283 | @media screen and (max-width: 480px) { 284 | .wrapper { 285 | width: calc(100% - 18px); 286 | max-width: calc(100% - 18px); 287 | } 288 | } 289 | -------------------------------------------------------------------------------- /res/assets/stylesheets/index.css: -------------------------------------------------------------------------------- 1 | @charset "UTF-8"; 2 | 3 | header { 4 | background: #FFFFFF; 5 | height: 62px; 6 | overflow: hidden; 7 | position: relative; 8 | z-index: 1; 9 | box-shadow: 0 2px 3px 0 rgba(0, 0, 0, 0.03); 10 | } 11 | 12 | header .header-inner { 13 | padding: 0 10px; 14 | } 15 | 16 | header .logo { 17 | margin-top: 17px; 18 | float: left; 19 | } 20 | 21 | header .logo img, 22 | header .logo .logo-label { 23 | vertical-align: middle; 24 | display: inline-block; 25 | } 26 | 27 | header .logo img { 28 | height: 26px; 29 | max-width: 160px; 30 | } 31 | 32 | header .logo .logo-label { 33 | color: rgba(0, 0, 0, 0.80); 34 | border-left: 1px solid rgba(0, 0, 0, 0.08); 35 | font-size: 15px; 36 | text-transform: lowercase; 37 | line-height: 24px; 38 | letter-spacing: -0.15px; 39 | margin-top: -2px; 40 | margin-left: 9px; 41 | padding-left: 12px; 42 | } 43 | 44 | header nav { 45 | margin-top: 13px; 46 | float: right; 47 | } 48 | 49 | header nav ul { 50 | display: block; 51 | } 52 | 53 | header nav ul li { 54 | list-style-type: none; 55 | display: inline-block; 56 | } 57 | 58 | header nav ul li a { 59 | font-size: 11.5px; 60 | line-height: 32px; 61 | height: 33px; 62 | margin-left: 8px; 63 | display: inline-block; 64 | border-radius: 2px; 65 | } 66 | 67 | header nav ul li a.nav-support { 68 | color: #FFFFFF; 69 | background-color: rgba(0, 0, 0, 0.85); 70 | letter-spacing: 0.10px; 71 | padding: 0 17px; 72 | box-shadow: 0 2px 3px 0 rgba(0, 0, 0, 0.04); 73 | transition: all 0.15s ease 0.10s; 74 | } 75 | 76 | header nav ul li a.nav-support:active { 77 | box-shadow: 0 1px 1px 0 rgba(0, 0, 0, 0.04); 78 | transform: translateY(1px); 79 | } 80 | 81 | header nav ul li a.nav-website { 82 | color: #000000; 83 | background-color: #FFFFFF; 84 | text-decoration: underline; 85 | padding: 0 15px; 86 | } 87 | 88 | aside { 89 | color: #FFFFFF; 90 | padding: 16px 0 19px; 91 | text-align: center;; 92 | overflow: hidden; 93 | display: flex; 94 | } 95 | 96 | aside .wrapper { 97 | padding: 0 8px; 98 | } 99 | 100 | aside h1, 101 | aside h4, 102 | aside .separator { 103 | vertical-align: middle; 104 | display: inline-block; 105 | } 106 | 107 | aside h1, 108 | aside h4 { 109 | line-height: 21px; 110 | flex: 1; 111 | } 112 | 113 | aside h1 { 114 | font-size: 15px; 115 | text-align: right; 116 | } 117 | 118 | aside h4 { 119 | font-size: 14px; 120 | text-align: left; 121 | opacity: 0.85; 122 | } 123 | 124 | aside .separator { 125 | background-color: rgba(255, 255, 255, 0.25); 126 | width: 1px; 127 | height: 21px; 128 | margin: 0 22px; 129 | } 130 | 131 | main { 132 | padding: 38px 0 42px; 133 | } 134 | 135 | main section { 136 | background: #FFFFFF; 137 | padding: 0 92px; 138 | position: relative; 139 | box-shadow: 0 2px 3px 0 rgba(15, 31, 64, 0.12); 140 | border-radius: 2px; 141 | } 142 | 143 | main section.general:before, 144 | main section.announcement:before { 145 | content: ""; 146 | width: 4px; 147 | position: absolute; 148 | left: 0; 149 | top: 0; 150 | bottom: 0; 151 | border-top-left-radius: 2px; 152 | border-bottom-left-radius: 2px; 153 | } 154 | 155 | main section.general .general-inner, 156 | main section.announcement .announcement-inner { 157 | letter-spacing: -0.10px; 158 | hyphens: auto; 159 | word-wrap: break-word; 160 | word-break: break-word; 161 | padding: 0 0 1px 24px; 162 | flex: 1; 163 | } 164 | 165 | main section.general .general-inner h2, 166 | main section.announcement .announcement-inner h4 { 167 | font-size: 15.5px; 168 | } 169 | 170 | main section.general .general-inner p, 171 | main section.announcement .announcement-inner p { 172 | font-size: 13px; 173 | line-height: 18px; 174 | } 175 | 176 | main section.general .general-icon .badge { 177 | width: 42px; 178 | height: 42px; 179 | } 180 | 181 | main section.general { 182 | margin-bottom: 30px; 183 | padding-top: 22px; 184 | padding-bottom: 24px; 185 | display: flex; 186 | position: relative; 187 | } 188 | 189 | main section.general.announcement-preceding { 190 | margin-bottom: 0; 191 | z-index: 1; 192 | box-shadow: 0 1px 3px 0 rgba(15, 31, 64, 0.08); 193 | } 194 | 195 | main section.general .general-icon { 196 | padding-top: 2px; 197 | padding-right: 36px; 198 | } 199 | 200 | main section.general .general-inner { 201 | border-left: 1px solid rgba(0, 0, 0, 0.08); 202 | } 203 | 204 | main section.general .general-inner h2 { 205 | margin-bottom: 11px; 206 | } 207 | 208 | main section.general .general-inner p, 209 | main section.general .general-inner p a { 210 | color: rgba(0, 0, 0, 0.65); 211 | } 212 | 213 | main section.general .general-inner p { 214 | margin-top: 3px; 215 | } 216 | 217 | main section.general .general-inner p a { 218 | text-decoration: underline; 219 | } 220 | 221 | main section.announcement { 222 | margin-top: -5px; 223 | padding-top: 26px; 224 | padding-bottom: 21px; 225 | display: flex; 226 | position: relative; 227 | z-index: 0; 228 | } 229 | 230 | main section.announcement .announcement-inner h4 { 231 | margin-top: -1px; 232 | margin-bottom: 9px; 233 | } 234 | 235 | main section.announcement .announcement-inner time { 236 | color: rgba(0, 0, 0, 0.6); 237 | font-size: 12.5px; 238 | line-height: 16px; 239 | margin-top: 14px; 240 | display: block; 241 | } 242 | 243 | main section.announcement .announcement-badge { 244 | color: #FFFFFF; 245 | font-size: 12.5px; 246 | line-height: 29px; 247 | padding: 0 14px 1px; 248 | display: inline-block; 249 | border-radius: 2px; 250 | } 251 | 252 | main section.announcement .announcement-unit { 253 | margin-bottom: 24px; 254 | } 255 | 256 | main section.announcement .announcement-unit:last-child { 257 | margin-bottom: 0; 258 | } 259 | 260 | main section.probe { 261 | margin-top: 24px; 262 | padding-top: 22px; 263 | padding-bottom: 32px; 264 | } 265 | 266 | main section.probe .title .badge, 267 | main section.probe .title h3 { 268 | vertical-align: middle; 269 | display: inline-block; 270 | } 271 | 272 | main section.probe .title .badge { 273 | margin-right: 8px; 274 | } 275 | 276 | main section.probe .title h3 { 277 | font-size: 15px; 278 | line-height: 20px; 279 | } 280 | 281 | main section.probe .title h3 a { 282 | color: inherit; 283 | } 284 | 285 | main section.probe .title h3 a:hover { 286 | text-decoration: underline; 287 | text-decoration-color: rgba(0, 0, 0, 0.2); 288 | cursor: alias; 289 | } 290 | 291 | main section.probe ul { 292 | margin-top: 20px; 293 | display: block; 294 | } 295 | 296 | main section.probe ul li { 297 | list-style-type: none; 298 | margin-top: 9px; 299 | display: flex; 300 | position: relative; 301 | border-radius: 2px; 302 | } 303 | 304 | main section.probe ul li:before { 305 | content: ""; 306 | width: 3px; 307 | position: absolute; 308 | left: 0; 309 | top: 0; 310 | bottom: 0; 311 | border-top-left-radius: 2px; 312 | border-bottom-left-radius: 2px; 313 | } 314 | 315 | main section.probe ul li label { 316 | color: rgba(0, 0, 0, 0.85); 317 | border-width: 0 1px 0 0; 318 | border-style: solid; 319 | font-size: 13px; 320 | letter-spacing: -0.10px; 321 | line-height: 16px; 322 | hyphens: auto; 323 | word-wrap: break-word; 324 | word-break: break-word; 325 | padding: 16px 14px 18px 32px; 326 | flex: 0.35; 327 | } 328 | 329 | main section.probe ul li .node { 330 | background-color: #F7F8FA; 331 | padding: 9px 14px 8px 24px; 332 | flex: 0.65; 333 | } 334 | 335 | main section.probe ul li .node .replica { 336 | color: #FFFFFF; 337 | font-size: 12px; 338 | text-align: center; 339 | line-height: 23px; 340 | height: 23px; 341 | margin: 4px 2px; 342 | padding: 0 8px; 343 | display: inline-block; 344 | border-radius: 2px; 345 | } 346 | 347 | main section.probe ul li .node .replica:last-of-type { 348 | margin-right: 8px; 349 | } 350 | 351 | main section.probe ul li .node .link { 352 | color: #000000; 353 | font-size: 12px; 354 | line-height: 16px; 355 | text-decoration: underline; 356 | margin-top: 7px; 357 | display: inline-block; 358 | } 359 | 360 | footer { 361 | text-align: center; 362 | letter-spacing: -0.05px; 363 | padding-bottom: 36px; 364 | } 365 | 366 | footer p, 367 | footer p a { 368 | color: rgba(0, 0, 0, 0.6); 369 | } 370 | 371 | footer p { 372 | font-size: 11.5px; 373 | line-height: 17px; 374 | margin-top: 2px; 375 | } 376 | 377 | footer p a { 378 | text-decoration: underline; 379 | } 380 | 381 | @media screen and (max-width: 1020px) { 382 | main section { 383 | padding-left: 54px; 384 | padding-right: 54px; 385 | } 386 | } 387 | 388 | @media screen and (max-width: 720px) { 389 | main section.probe ul li label, 390 | main section.probe ul li .node { 391 | flex: 0.5; 392 | } 393 | } 394 | 395 | @media screen and (max-width: 640px) { 396 | header nav ul li a.nav-website { 397 | display: none; 398 | } 399 | 400 | aside h1 { 401 | text-align: center; 402 | } 403 | 404 | aside h4, 405 | aside .separator { 406 | display: none; 407 | } 408 | 409 | main section { 410 | padding-left: 40px; 411 | padding-right: 40px; 412 | } 413 | 414 | main section.general .general-icon { 415 | display: none; 416 | } 417 | 418 | main section.general .general-inner { 419 | border-left: 0 none; 420 | padding-left: 0; 421 | } 422 | 423 | main section.general .general-inner h2, 424 | main section.general .general-inner p { 425 | text-align: justify; 426 | } 427 | 428 | main section.general .general-inner p { 429 | margin-top: 9px; 430 | } 431 | 432 | main section.announcement { 433 | flex-direction: column; 434 | } 435 | 436 | main section.announcement .announcement-badge { 437 | text-align: center; 438 | display: block; 439 | } 440 | 441 | main section.announcement .announcement-inner { 442 | margin-top: 18px; 443 | padding-left: 0; 444 | } 445 | } 446 | 447 | @media screen and (max-width: 480px) { 448 | header { 449 | text-align: center; 450 | } 451 | 452 | header .logo { 453 | display: inline-block; 454 | float: none; 455 | } 456 | 457 | header nav { 458 | display: none; 459 | } 460 | 461 | main { 462 | padding-top: 28px; 463 | padding-bottom: 32px; 464 | } 465 | 466 | main section { 467 | padding-left: 20px; 468 | padding-right: 20px; 469 | } 470 | 471 | main section.general { 472 | padding-top: 20px; 473 | padding-bottom: 22px; 474 | } 475 | 476 | main section.probe { 477 | padding-top: 22px; 478 | padding-bottom: 28px; 479 | } 480 | 481 | main section.probe ul li label { 482 | padding-left: 20px; 483 | padding-right: 10px; 484 | } 485 | 486 | main section.probe ul li .node { 487 | padding-left: 18px; 488 | padding-right: 14px; 489 | } 490 | 491 | main section.announcement { 492 | padding-top: 25px; 493 | padding-bottom: 18px; 494 | } 495 | 496 | footer { 497 | padding-bottom: 28px; 498 | } 499 | } 500 | -------------------------------------------------------------------------------- /res/assets/templates/index.tera: -------------------------------------------------------------------------------- 1 | <!DOCTYPE html> 2 | 3 | <html lang="en" dir="ltr"> 4 | <head> 5 | <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> 6 | <meta name="viewport" content="width=device-width, initial-scale=1"> 7 | 8 | <link rel="icon" href="{{ config.icon_url | escape }}" type="{{ config.icon_mime | escape }}"> 9 | <link rel="apple-touch-icon" href="{{ config.icon_url | escape }}" type="{{ config.icon_mime | escape }}"> 10 | 11 | <meta name="msapplication-TileColor" content="{{ config.icon_color | escape }}"> 12 | <meta name="msapplication-TileImage" content="{{ config.icon_url | escape }}"> 13 | <meta property="og:image" content="{{ config.icon_url | escape }}"> 14 | 15 | <title>{{ config.page_title | escape }} 16 | 17 | 18 | 19 | 20 | 21 | 22 | 25 | 26 | {% if config.custom_html %} 27 | {{ config.custom_html | safe }} 28 | {% endif %} 29 | 30 | 31 | 32 |
33 |
34 |
35 | 40 | 41 | 52 | 53 |
54 |
55 |
56 |
57 | 58 | 81 | 82 |
83 |
84 |
85 |
86 |
87 | 88 |
89 |

90 | {% if states.status == "dead" %} 91 | Looks like some services are not working. 92 | {% elif states.status == "sick" %} 93 | Looks like services are under high load. 94 | {% else %} 95 | Looks like everything is operating normally. 96 | {% endif %} 97 |

98 | 99 |

This status page automatically monitors our systems and alerts if something is not working as expected.

100 | 101 |

102 | {% if states.status == "dead" %} 103 | Our team has been notified of the issue. If the outage persists, 104 | {% elif states.status == "sick" %} 105 | Services are slower than usual at the moment. If you are encountering an issue, 106 | {% else %} 107 | All systems are healthy at the moment. If you are encountering an issue, 108 | {% endif %} 109 | 110 | please contact our support. 111 |

112 |
113 |
114 | 115 | {% if announcements | length > 0 %} 116 |
117 |
118 | Announcement 119 |
120 | 121 |
122 | {% for announcement in announcements | reverse %} 123 |
124 |

{{ announcement.title | escape }}

125 |

{{ announcement.text | escape | linebreaksbr }}

126 | 127 | {% if announcement.date %} 128 | 129 | {% endif %} 130 |
131 | {% endfor %} 132 |
133 |
134 | {% endif %} 135 | 136 | {% for _, probe in states.probes %} 137 |
138 |
139 |
140 | 141 |

142 | {{ probe.label | escape }} 143 |

144 |
145 | 146 |
    147 | {% for _, node in probe.nodes %} 148 |
  • 149 | 150 | 151 |
    152 | {% for replica_id, replica in node.replicas %} 153 | 154 | {{ loop.index }} 155 | 156 | 157 | 158 | 159 | 160 | {% if replica.status == "dead" %} 161 | This replica reports as dead. 162 | {% elif replica.status == "sick" %} 163 | This replica reports as sick. 164 | {% else %} 165 | This replica reports as healthy. 166 | {% endif %} 167 | 168 | 169 | 170 | {% if node.mode == "local" %} 171 | Checked via a local probe (internal). 172 | {% elif node.mode == "script" %} 173 | Checked via a script probe. 174 | {% elif node.mode == "push" %} 175 | Checked via a push probe (Reporter). 176 | {% else %} 177 | Checked via a poll probe (HTTP, TCP or ICMP). 178 | {% endif %} 179 | 180 | 181 | 182 | {% if node.reveal_replica_name %} 183 | {{ replica_id }} 184 | {% endif %} 185 | 186 | {% if replica.metrics.system or replica.metrics.latency or replica.metrics.latency == 0 or replica.metrics.rabbitmq %} 187 | 188 | {% if replica.metrics.system %} 189 | 190 | Load: {{ replica.metrics.system.cpu }}% 191 | 192 | 193 | 194 | Memory: {{ replica.metrics.system.ram }}% 195 | 196 | {% endif %} 197 | 198 | {% if replica.metrics.latency or replica.metrics.latency == 0 %} 199 | 200 | Latency: {{ replica.metrics.latency }}ms 201 | 202 | {% endif %} 203 | 204 | {% if replica.metrics.rabbitmq %} 205 | 206 | Queue: {{ replica.metrics.rabbitmq.queue_ready }}R {{ replica.metrics.rabbitmq.queue_nack }}N 207 | 208 | {% endif %} 209 | 210 | {% endif %} 211 | 212 | 213 | 214 | {% endfor %} 215 | 216 | {% if node.link_url %} 217 | 218 | {% if node.link_label %} 219 | {{ node.link_label }} 220 | {% else %} 221 | {{ node.link_url }} 222 | {% endif %} 223 | 224 | {% endif %} 225 |
    226 |
  • 227 | {% endfor %} 228 |
229 |
230 | {% endfor %} 231 |
232 | 233 |
234 |

© {{ environment.year }} {{ config.company_name | escape }}

235 |

This status page is powered by Vigil.

236 |
237 | 238 | 239 | -------------------------------------------------------------------------------- /scripts/build_packages.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ## 4 | # Vigil 5 | # 6 | # Microservices Status Page 7 | # Copyright: 2023, Valerian Saliou 8 | # License: Mozilla Public License v2.0 (MPL v2.0) 9 | ## 10 | 11 | # Define build pipeline 12 | function build_for_target { 13 | OS="$2" DIST="$3" ARCH="$1" ./packpack/packpack 14 | release_result=$? 15 | 16 | if [ $release_result -eq 0 ]; then 17 | mkdir -p "./packages/$2_$3/" 18 | mv ./build/*$4 "./packages/$2_$3/" 19 | 20 | echo "Result: Packaged architecture: $1 for OS: $2:$3 (*$4)" 21 | fi 22 | 23 | return $release_result 24 | } 25 | 26 | # Run release tasks 27 | ABSPATH=$(cd "$(dirname "$0")"; pwd) 28 | BASE_DIR="$ABSPATH/../" 29 | 30 | rc=0 31 | 32 | pushd "$BASE_DIR" > /dev/null 33 | echo "Executing packages build steps for Vigil..." 34 | 35 | # Initialize `packpack` 36 | rm -rf ./packpack && \ 37 | git clone https://github.com/packpack/packpack.git packpack 38 | rc=$? 39 | 40 | # Proceed build for each target? 41 | if [ $rc -eq 0 ]; then 42 | build_for_target "x86_64" "debian" "buster" ".deb" && \ 43 | build_for_target "x86_64" "debian" "bullseye" ".deb" && \ 44 | build_for_target "x86_64" "debian" "bookworm" ".deb" 45 | rc=$? 46 | fi 47 | 48 | # Cleanup environment 49 | rm -rf ./build ./packpack 50 | 51 | if [ $rc -eq 0 ]; then 52 | echo "Success: Done executing packages build steps for Vigil" 53 | else 54 | echo "Error: Failed executing packages build steps for Vigil" 55 | fi 56 | popd > /dev/null 57 | 58 | exit $rc 59 | -------------------------------------------------------------------------------- /scripts/release_binaries.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ## 4 | # Vigil 5 | # 6 | # Microservices Status Page 7 | # Copyright: 2020, Valerian Saliou 8 | # License: Mozilla Public License v2.0 (MPL v2.0) 9 | ## 10 | 11 | # Read arguments 12 | while [ "$1" != "" ]; do 13 | argument_key=`echo $1 | awk -F= '{print $1}'` 14 | argument_value=`echo $1 | awk -F= '{print $2}'` 15 | 16 | case $argument_key in 17 | -v | --version) 18 | # Notice: strip any leading 'v' to the version number 19 | VIGIL_VERSION="${argument_value/v}" 20 | ;; 21 | *) 22 | echo "Unknown argument received: '$argument_key'" 23 | exit 1 24 | ;; 25 | esac 26 | 27 | shift 28 | done 29 | 30 | # Ensure release version is provided 31 | if [ -z "$VIGIL_VERSION" ]; then 32 | echo "No Vigil release version was provided, please provide it using '--version'" 33 | 34 | exit 1 35 | fi 36 | 37 | # Define release pipeline 38 | function release_for_architecture { 39 | final_tar="v$VIGIL_VERSION-$1.tar.gz" 40 | 41 | rm -rf ./vigil/ && \ 42 | cross build --target "$2" --release && \ 43 | mkdir ./vigil && \ 44 | cp -p "target/$2/release/vigil" ./vigil/ && \ 45 | cp -r ./config.cfg ./res vigil/ && \ 46 | tar --owner=0 --group=0 -czvf "$final_tar" ./vigil && \ 47 | rm -r ./vigil/ 48 | release_result=$? 49 | 50 | if [ $release_result -eq 0 ]; then 51 | echo "Result: Packed architecture: $1 to file: $final_tar" 52 | fi 53 | 54 | return $release_result 55 | } 56 | 57 | # Run release tasks 58 | ABSPATH=$(cd "$(dirname "$0")"; pwd) 59 | BASE_DIR="$ABSPATH/../" 60 | 61 | rc=0 62 | 63 | pushd "$BASE_DIR" > /dev/null 64 | echo "Executing release steps for Vigil v$VIGIL_VERSION..." 65 | 66 | release_for_architecture "x86_64" "x86_64-unknown-linux-musl" && \ 67 | release_for_architecture "armv7" "armv7-unknown-linux-musleabihf" 68 | rc=$? 69 | 70 | if [ $rc -eq 0 ]; then 71 | echo "Success: Done executing release steps for Vigil v$VIGIL_VERSION" 72 | else 73 | echo "Error: Failed executing release steps for Vigil v$VIGIL_VERSION" 74 | fi 75 | popd > /dev/null 76 | 77 | exit $rc 78 | -------------------------------------------------------------------------------- /scripts/sign_binaries.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ## 4 | # Vigil 5 | # 6 | # Microservices Status Page 7 | # Copyright: 2022, Valerian Saliou 8 | # License: Mozilla Public License v2.0 (MPL v2.0) 9 | ## 10 | 11 | # Read arguments 12 | while [ "$1" != "" ]; do 13 | argument_key=`echo $1 | awk -F= '{print $1}'` 14 | argument_value=`echo $1 | awk -F= '{print $2}'` 15 | 16 | case $argument_key in 17 | -v | --version) 18 | # Notice: strip any leading 'v' to the version number 19 | VIGIL_VERSION="${argument_value/v}" 20 | ;; 21 | *) 22 | echo "Unknown argument received: '$argument_key'" 23 | exit 1 24 | ;; 25 | esac 26 | 27 | shift 28 | done 29 | 30 | # Ensure release version is provided 31 | if [ -z "$VIGIL_VERSION" ]; then 32 | echo "No Vigil release version was provided, please provide it using '--version'" 33 | 34 | exit 1 35 | fi 36 | 37 | # Define sign pipeline 38 | function sign_for_architecture { 39 | final_tar="v$VIGIL_VERSION-$1.tar.gz" 40 | gpg_signer="valerian@valeriansaliou.name" 41 | 42 | gpg -u "$gpg_signer" --armor --detach-sign "$final_tar" 43 | sign_result=$? 44 | 45 | if [ $sign_result -eq 0 ]; then 46 | echo "Result: Signed architecture: $1 for file: $final_tar" 47 | fi 48 | 49 | return $sign_result 50 | } 51 | 52 | # Run sign tasks 53 | ABSPATH=$(cd "$(dirname "$0")"; pwd) 54 | BASE_DIR="$ABSPATH/../" 55 | 56 | rc=0 57 | 58 | pushd "$BASE_DIR" > /dev/null 59 | echo "Executing sign steps for Vigil v$VIGIL_VERSION..." 60 | 61 | sign_for_architecture "x86_64" && \ 62 | sign_for_architecture "armv7" 63 | rc=$? 64 | 65 | if [ $rc -eq 0 ]; then 66 | echo "Success: Done executing sign steps for Vigil v$VIGIL_VERSION" 67 | else 68 | echo "Error: Failed executing sign steps for Vigil v$VIGIL_VERSION" 69 | fi 70 | popd > /dev/null 71 | 72 | exit $rc 73 | -------------------------------------------------------------------------------- /src/aggregator/mod.rs: -------------------------------------------------------------------------------- 1 | // Vigil 2 | // 3 | // Microservices Status Page 4 | // Copyright: 2018, Valerian Saliou 5 | // License: Mozilla Public License v2.0 (MPL v2.0) 6 | 7 | pub mod manager; 8 | -------------------------------------------------------------------------------- /src/config/config.rs: -------------------------------------------------------------------------------- 1 | // Vigil 2 | // 3 | // Microservices Status Page 4 | // Copyright: 2018, Valerian Saliou 5 | // License: Mozilla Public License v2.0 (MPL v2.0) 6 | 7 | use std::net::SocketAddr; 8 | use std::path::PathBuf; 9 | 10 | use url_serde::SerdeUrl; 11 | 12 | use super::defaults; 13 | use super::regex::Regex; 14 | use crate::prober::mode::Mode; 15 | 16 | #[derive(Deserialize)] 17 | pub struct Config { 18 | pub server: ConfigServer, 19 | pub assets: ConfigAssets, 20 | pub branding: ConfigBranding, 21 | pub metrics: ConfigMetrics, 22 | pub plugins: Option, 23 | pub notify: Option, 24 | pub probe: ConfigProbe, 25 | } 26 | 27 | #[derive(Deserialize)] 28 | pub struct ConfigServer { 29 | #[serde(default = "defaults::server_log_level")] 30 | pub log_level: String, 31 | 32 | #[serde(default = "defaults::server_inet")] 33 | pub inet: SocketAddr, 34 | 35 | #[serde(default = "defaults::server_workers")] 36 | pub workers: usize, 37 | 38 | pub manager_token: String, 39 | pub reporter_token: String, 40 | } 41 | 42 | #[derive(Deserialize)] 43 | pub struct ConfigAssets { 44 | #[serde(default = "defaults::assets_path")] 45 | pub path: PathBuf, 46 | } 47 | 48 | #[derive(Deserialize)] 49 | pub struct ConfigBranding { 50 | #[serde(default = "defaults::branding_page_title")] 51 | pub page_title: String, 52 | 53 | pub page_url: SerdeUrl, 54 | pub company_name: String, 55 | pub icon_color: String, 56 | pub icon_url: SerdeUrl, 57 | pub logo_color: String, 58 | pub logo_url: SerdeUrl, 59 | pub website_url: SerdeUrl, 60 | pub support_url: SerdeUrl, 61 | pub custom_html: Option, 62 | } 63 | 64 | #[derive(Deserialize)] 65 | pub struct ConfigMetrics { 66 | #[serde(default = "defaults::metrics_poll_interval")] 67 | pub poll_interval: u64, 68 | 69 | #[serde(default = "defaults::metrics_poll_retry")] 70 | pub poll_retry: u64, 71 | 72 | #[serde(default = "defaults::metrics_poll_http_status_healthy_above")] 73 | pub poll_http_status_healthy_above: u16, 74 | 75 | #[serde(default = "defaults::metrics_poll_http_status_healthy_below")] 76 | pub poll_http_status_healthy_below: u16, 77 | 78 | #[serde(default = "defaults::metrics_poll_delay_dead")] 79 | pub poll_delay_dead: u64, 80 | 81 | #[serde(default = "defaults::metrics_poll_delay_sick")] 82 | pub poll_delay_sick: u64, 83 | 84 | #[serde(default = "defaults::metrics_poll_parallelism")] 85 | pub poll_parallelism: u16, 86 | 87 | #[serde(default = "defaults::metrics_push_delay_dead")] 88 | pub push_delay_dead: u64, 89 | 90 | #[serde(default = "defaults::metrics_push_system_cpu_sick_above")] 91 | pub push_system_cpu_sick_above: f32, 92 | 93 | #[serde(default = "defaults::metrics_push_system_ram_sick_above")] 94 | pub push_system_ram_sick_above: f32, 95 | 96 | #[serde(default = "defaults::metrics_script_interval")] 97 | pub script_interval: u64, 98 | 99 | #[serde(default = "defaults::script_parallelism")] 100 | pub script_parallelism: u16, 101 | 102 | #[serde(default = "defaults::metrics_local_delay_dead")] 103 | pub local_delay_dead: u64, 104 | } 105 | 106 | #[derive(Deserialize)] 107 | pub struct ConfigNotify { 108 | #[serde(default = "defaults::notify_startup_notification")] 109 | pub startup_notification: bool, 110 | 111 | pub reminder_interval: Option, 112 | 113 | #[serde(default = "defaults::notify_reminder_backoff_function")] 114 | pub reminder_backoff_function: ConfigNotifyReminderBackoffFunction, 115 | 116 | #[serde(default = "defaults::notify_reminder_backoff_limit")] 117 | pub reminder_backoff_limit: u16, 118 | 119 | #[cfg(feature = "notifier-email")] 120 | pub email: Option, 121 | 122 | #[cfg(feature = "notifier-twilio")] 123 | pub twilio: Option, 124 | 125 | #[cfg(feature = "notifier-slack")] 126 | pub slack: Option, 127 | 128 | #[cfg(feature = "notifier-zulip")] 129 | pub zulip: Option, 130 | 131 | #[cfg(feature = "notifier-telegram")] 132 | pub telegram: Option, 133 | 134 | #[cfg(feature = "notifier-pushover")] 135 | pub pushover: Option, 136 | 137 | #[cfg(feature = "notifier-gotify")] 138 | pub gotify: Option, 139 | 140 | #[cfg(feature = "notifier-xmpp")] 141 | pub xmpp: Option, 142 | 143 | #[cfg(feature = "notifier-matrix")] 144 | pub matrix: Option, 145 | 146 | #[cfg(feature = "notifier-webex")] 147 | pub webex: Option, 148 | 149 | #[cfg(feature = "notifier-webhook")] 150 | pub webhook: Option, 151 | } 152 | 153 | #[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)] 154 | pub enum ConfigNotifyReminderBackoffFunction { 155 | #[serde(rename = "none")] 156 | None = 0, 157 | 158 | #[serde(rename = "linear")] 159 | Linear = 1, 160 | 161 | #[serde(rename = "square")] 162 | Square = 2, 163 | 164 | #[serde(rename = "cubic")] 165 | Cubic = 3, 166 | } 167 | 168 | #[derive(Deserialize)] 169 | pub struct ConfigPlugins { 170 | pub rabbitmq: Option, 171 | } 172 | 173 | #[derive(Deserialize)] 174 | pub struct ConfigPluginsRabbitMQ { 175 | pub api_url: SerdeUrl, 176 | pub auth_username: String, 177 | pub auth_password: String, 178 | pub virtualhost: String, 179 | pub queue_ready_healthy_below: u32, 180 | pub queue_nack_healthy_below: u32, 181 | pub queue_ready_dead_above: u32, 182 | pub queue_nack_dead_above: u32, 183 | pub queue_loaded_retry_delay: Option, 184 | } 185 | 186 | #[cfg(feature = "notifier-email")] 187 | #[derive(Deserialize)] 188 | pub struct ConfigNotifyEmail { 189 | pub to: String, 190 | pub from: String, 191 | 192 | #[serde(default = "defaults::notify_email_smtp_host")] 193 | pub smtp_host: String, 194 | 195 | #[serde(default = "defaults::notify_email_smtp_port")] 196 | pub smtp_port: u16, 197 | 198 | pub smtp_username: Option, 199 | pub smtp_password: Option, 200 | 201 | #[serde(default = "defaults::notify_email_smtp_encrypt")] 202 | pub smtp_encrypt: bool, 203 | 204 | #[serde(default = "defaults::notify_generic_reminders_only")] 205 | pub reminders_only: bool, 206 | } 207 | 208 | #[cfg(feature = "notifier-twilio")] 209 | #[derive(Deserialize)] 210 | pub struct ConfigNotifyTwilio { 211 | pub to: Vec, 212 | pub service_sid: String, 213 | pub account_sid: String, 214 | pub auth_token: String, 215 | 216 | #[serde(default = "defaults::notify_generic_reminders_only")] 217 | pub reminders_only: bool, 218 | } 219 | 220 | #[cfg(feature = "notifier-slack")] 221 | #[derive(Deserialize)] 222 | pub struct ConfigNotifySlack { 223 | pub hook_url: SerdeUrl, 224 | 225 | #[serde(default = "defaults::notify_slack_mention_channel")] 226 | pub mention_channel: bool, 227 | 228 | #[serde(default = "defaults::notify_generic_reminders_only")] 229 | pub reminders_only: bool, 230 | } 231 | 232 | #[cfg(feature = "notifier-zulip")] 233 | #[derive(Deserialize)] 234 | pub struct ConfigNotifyZulip { 235 | pub bot_email: String, 236 | pub bot_api_key: String, 237 | pub channel: String, 238 | pub api_url: SerdeUrl, 239 | 240 | #[serde(default = "defaults::notify_generic_reminders_only")] 241 | pub reminders_only: bool, 242 | } 243 | 244 | #[cfg(feature = "notifier-telegram")] 245 | #[derive(Deserialize)] 246 | pub struct ConfigNotifyTelegram { 247 | pub bot_token: String, 248 | pub chat_id: String, 249 | 250 | #[serde(default = "defaults::notify_generic_reminders_only")] 251 | pub reminders_only: bool, 252 | } 253 | 254 | #[cfg(feature = "notifier-pushover")] 255 | #[derive(Deserialize)] 256 | pub struct ConfigNotifyPushover { 257 | pub app_token: String, 258 | pub user_keys: Vec, 259 | 260 | #[serde(default = "defaults::notify_generic_reminders_only")] 261 | pub reminders_only: bool, 262 | } 263 | 264 | #[cfg(feature = "notifier-gotify")] 265 | #[derive(Deserialize)] 266 | pub struct ConfigNotifyGotify { 267 | pub app_url: SerdeUrl, 268 | pub app_token: String, 269 | 270 | #[serde(default = "defaults::notify_generic_reminders_only")] 271 | pub reminders_only: bool, 272 | } 273 | 274 | #[cfg(feature = "notifier-xmpp")] 275 | #[derive(Deserialize)] 276 | pub struct ConfigNotifyXMPP { 277 | pub to: String, 278 | pub from: String, 279 | pub xmpp_password: String, 280 | 281 | #[serde(default = "defaults::notify_generic_reminders_only")] 282 | pub reminders_only: bool, 283 | } 284 | 285 | #[cfg(feature = "notifier-matrix")] 286 | #[derive(Deserialize)] 287 | pub struct ConfigNotifyMatrix { 288 | pub homeserver_url: SerdeUrl, 289 | pub access_token: String, 290 | pub room_id: String, 291 | 292 | #[serde(default = "defaults::notify_generic_reminders_only")] 293 | pub reminders_only: bool, 294 | } 295 | 296 | #[cfg(feature = "notifier-webex")] 297 | #[derive(Deserialize)] 298 | pub struct ConfigNotifyWebEx { 299 | pub endpoint_url: SerdeUrl, 300 | pub token: String, 301 | pub room_id: String, 302 | 303 | #[serde(default = "defaults::notify_generic_reminders_only")] 304 | pub reminders_only: bool, 305 | } 306 | 307 | #[cfg(feature = "notifier-webhook")] 308 | #[derive(Deserialize)] 309 | pub struct ConfigNotifyWebHook { 310 | pub hook_url: SerdeUrl, 311 | } 312 | 313 | #[derive(Deserialize)] 314 | pub struct ConfigProbe { 315 | pub service: Vec, 316 | } 317 | 318 | #[derive(Deserialize)] 319 | pub struct ConfigProbeService { 320 | pub id: String, 321 | pub label: String, 322 | pub node: Vec, 323 | } 324 | 325 | #[derive(Deserialize)] 326 | pub struct ConfigProbeServiceNode { 327 | pub id: String, 328 | pub label: String, 329 | pub mode: Mode, 330 | pub replicas: Option>, 331 | pub scripts: Option>, 332 | 333 | #[serde(default)] 334 | #[serde(with = "http_serde::header_map")] 335 | pub http_headers: http::HeaderMap, 336 | 337 | pub http_method: Option, 338 | pub http_body: Option, 339 | pub http_body_healthy_match: Option, 340 | 341 | #[serde(default = "defaults::probe_service_node_reveal_replica_name")] 342 | pub reveal_replica_name: bool, 343 | 344 | pub link_url: Option, 345 | pub link_label: Option, 346 | 347 | pub rabbitmq_queue: Option, 348 | pub rabbitmq_queue_nack_healthy_below: Option, 349 | pub rabbitmq_queue_nack_dead_above: Option, 350 | } 351 | 352 | #[derive(Serialize, Deserialize, Debug, Clone)] 353 | pub enum ConfigProbeServiceNodeHTTPMethod { 354 | #[serde(rename = "HEAD")] 355 | Head, 356 | 357 | #[serde(rename = "GET")] 358 | Get, 359 | 360 | #[serde(rename = "POST")] 361 | Post, 362 | 363 | #[serde(rename = "PUT")] 364 | Put, 365 | 366 | #[serde(rename = "PATCH")] 367 | Patch, 368 | } 369 | -------------------------------------------------------------------------------- /src/config/defaults.rs: -------------------------------------------------------------------------------- 1 | // Vigil 2 | // 3 | // Microservices Status Page 4 | // Copyright: 2018, Valerian Saliou 5 | // License: Mozilla Public License v2.0 (MPL v2.0) 6 | 7 | use std::net::SocketAddr; 8 | use std::path::PathBuf; 9 | 10 | use super::config::ConfigNotifyReminderBackoffFunction; 11 | 12 | pub fn server_log_level() -> String { 13 | "error".to_string() 14 | } 15 | 16 | pub fn server_inet() -> SocketAddr { 17 | "[::1]:8080".parse().unwrap() 18 | } 19 | 20 | pub fn server_workers() -> usize { 21 | 4 22 | } 23 | 24 | pub fn assets_path() -> PathBuf { 25 | PathBuf::from("./res/assets/") 26 | } 27 | 28 | pub fn branding_page_title() -> String { 29 | "Status Page".to_string() 30 | } 31 | 32 | pub fn metrics_poll_interval() -> u64 { 33 | 120 34 | } 35 | 36 | pub fn metrics_poll_retry() -> u64 { 37 | 2 38 | } 39 | 40 | pub fn metrics_poll_http_status_healthy_above() -> u16 { 41 | 200 42 | } 43 | 44 | pub fn metrics_poll_http_status_healthy_below() -> u16 { 45 | 400 46 | } 47 | 48 | pub fn metrics_poll_delay_dead() -> u64 { 49 | 10 50 | } 51 | 52 | pub fn metrics_poll_delay_sick() -> u64 { 53 | 5 54 | } 55 | 56 | pub fn metrics_poll_parallelism() -> u16 { 57 | 4 58 | } 59 | 60 | pub fn metrics_push_delay_dead() -> u64 { 61 | 20 62 | } 63 | 64 | pub fn metrics_push_system_cpu_sick_above() -> f32 { 65 | 0.99 66 | } 67 | 68 | pub fn metrics_push_system_ram_sick_above() -> f32 { 69 | 0.99 70 | } 71 | 72 | pub fn metrics_script_interval() -> u64 { 73 | 300 74 | } 75 | 76 | pub fn script_parallelism() -> u16 { 77 | 2 78 | } 79 | 80 | pub fn metrics_local_delay_dead() -> u64 { 81 | 40 82 | } 83 | 84 | pub fn notify_startup_notification() -> bool { 85 | true 86 | } 87 | 88 | pub fn notify_reminder_backoff_function() -> ConfigNotifyReminderBackoffFunction { 89 | ConfigNotifyReminderBackoffFunction::None 90 | } 91 | 92 | pub fn notify_reminder_backoff_limit() -> u16 { 93 | 3 94 | } 95 | 96 | #[cfg(feature = "notifier-email")] 97 | pub fn notify_email_smtp_host() -> String { 98 | "localhost".to_string() 99 | } 100 | 101 | #[cfg(feature = "notifier-email")] 102 | pub fn notify_email_smtp_port() -> u16 { 103 | 587 104 | } 105 | 106 | #[cfg(feature = "notifier-email")] 107 | pub fn notify_email_smtp_encrypt() -> bool { 108 | true 109 | } 110 | 111 | #[cfg(feature = "notifier-slack")] 112 | pub fn notify_slack_mention_channel() -> bool { 113 | false 114 | } 115 | 116 | pub fn notify_generic_reminders_only() -> bool { 117 | false 118 | } 119 | 120 | pub fn probe_service_node_reveal_replica_name() -> bool { 121 | false 122 | } 123 | -------------------------------------------------------------------------------- /src/config/logger.rs: -------------------------------------------------------------------------------- 1 | // Vigil 2 | // 3 | // Microservices Status Page 4 | // Copyright: 2018, Valerian Saliou 5 | // License: Mozilla Public License v2.0 (MPL v2.0) 6 | 7 | use log; 8 | use log::{Level, LevelFilter, Metadata, Record, SetLoggerError}; 9 | 10 | pub struct ConfigLogger; 11 | 12 | impl log::Log for ConfigLogger { 13 | fn enabled(&self, metadata: &Metadata) -> bool { 14 | metadata.level() <= Level::Debug 15 | } 16 | 17 | fn log(&self, record: &Record) { 18 | if self.enabled(record.metadata()) { 19 | println!("({}) - {}", record.level(), record.args()); 20 | } 21 | } 22 | 23 | fn flush(&self) {} 24 | } 25 | 26 | impl ConfigLogger { 27 | pub fn init(level: LevelFilter) -> Result<(), SetLoggerError> { 28 | log::set_max_level(level); 29 | log::set_boxed_logger(Box::new(ConfigLogger)) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/config/mod.rs: -------------------------------------------------------------------------------- 1 | // Vigil 2 | // 3 | // Microservices Status Page 4 | // Copyright: 2018, Valerian Saliou 5 | // License: Mozilla Public License v2.0 (MPL v2.0) 6 | 7 | mod defaults; 8 | 9 | pub mod config; 10 | pub mod logger; 11 | pub mod reader; 12 | pub mod regex; 13 | -------------------------------------------------------------------------------- /src/config/reader.rs: -------------------------------------------------------------------------------- 1 | // Vigil 2 | // 3 | // Microservices Status Page 4 | // Copyright: 2018, Valerian Saliou 5 | // License: Mozilla Public License v2.0 (MPL v2.0) 6 | 7 | use envsubst::substitute; 8 | use std::{ 9 | collections::{hash_set::HashSet, HashMap}, 10 | env, fs, 11 | }; 12 | 13 | use toml; 14 | 15 | use super::config::*; 16 | use crate::APP_ARGS; 17 | 18 | pub struct ConfigReader; 19 | 20 | impl ConfigReader { 21 | pub fn make() -> Config { 22 | debug!("reading config file: {}", &APP_ARGS.config); 23 | 24 | // Read configuration 25 | let mut conf = fs::read_to_string(&APP_ARGS.config).expect("cannot find config file"); 26 | 27 | debug!("read config file: {}", &APP_ARGS.config); 28 | 29 | // Replace environment variables 30 | let environment = env::vars().collect::>(); 31 | 32 | conf = substitute(&conf, &environment).expect("cannot substitute environment variables"); 33 | 34 | // Parse configuration 35 | let config = toml::from_str(&conf).expect("syntax error in config file"); 36 | 37 | // Validate configuration 38 | Self::validate(&config); 39 | 40 | config 41 | } 42 | 43 | fn validate(config: &Config) { 44 | // Validate all identifiers 45 | Self::validate_identifiers(config) 46 | } 47 | 48 | fn validate_identifiers(config: &Config) { 49 | // Scan for service identifier duplicates 50 | let mut service_identifiers = HashSet::new(); 51 | 52 | for service in config.probe.service.iter() { 53 | // Service identifier was already previously inserted? (caught a duplicate) 54 | if service_identifiers.insert(&service.id) == false { 55 | panic!( 56 | "configuration has duplicate service identifier: {}", 57 | service.id 58 | ) 59 | } 60 | 61 | // Scan for node identifier duplicates 62 | let mut node_identifiers = HashSet::new(); 63 | 64 | for node in service.node.iter() { 65 | // Node identifier was already previously inserted? (caught a duplicate) 66 | if node_identifiers.insert(&node.id) == false { 67 | panic!( 68 | "configuration has duplicate node identifier: {} in service: {}", 69 | node.id, service.id 70 | ) 71 | } 72 | } 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/config/regex.rs: -------------------------------------------------------------------------------- 1 | // Vigil 2 | // 3 | // Microservices Status Page 4 | // Copyright: 2018, Valerian Saliou 5 | // License: Mozilla Public License v2.0 (MPL v2.0) 6 | 7 | use std::fmt; 8 | use std::ops::Deref; 9 | 10 | use regex; 11 | use serde::de::{Error, Visitor}; 12 | use serde::{Deserialize, Deserializer, Serialize, Serializer}; 13 | 14 | #[derive(Clone, Debug)] 15 | pub struct Regex(regex::Regex); 16 | 17 | impl Deref for Regex { 18 | type Target = regex::Regex; 19 | 20 | fn deref(&self) -> ®ex::Regex { 21 | &self.0 22 | } 23 | } 24 | 25 | impl<'de> Deserialize<'de> for Regex { 26 | fn deserialize(de: D) -> Result 27 | where 28 | D: Deserializer<'de>, 29 | { 30 | struct RegexVisitor; 31 | 32 | impl<'de> Visitor<'de> for RegexVisitor { 33 | type Value = Regex; 34 | 35 | fn expecting(&self, format: &mut fmt::Formatter) -> fmt::Result { 36 | format.write_str("a regular expression pattern") 37 | } 38 | 39 | fn visit_str(self, value: &str) -> Result { 40 | regex::Regex::new(value) 41 | .map(Regex) 42 | .map_err(|err| E::custom(err.to_string())) 43 | } 44 | } 45 | 46 | de.deserialize_str(RegexVisitor) 47 | } 48 | } 49 | 50 | impl Serialize for Regex { 51 | fn serialize(&self, serializer: S) -> Result 52 | where 53 | S: Serializer, 54 | { 55 | // Notice: ignore Regex serialization here, as it is not used in templates (which \ 56 | // serialization is used for in Vigil). 57 | serializer.serialize_none() 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | // Vigil 2 | // 3 | // Microservices Status Page 4 | // Copyright: 2018, Valerian Saliou 5 | // License: Mozilla Public License v2.0 (MPL v2.0) 6 | 7 | #[macro_use] 8 | extern crate log; 9 | #[macro_use] 10 | extern crate lazy_static; 11 | #[macro_use] 12 | extern crate serde_derive; 13 | 14 | mod aggregator; 15 | mod config; 16 | mod notifier; 17 | mod prober; 18 | mod responder; 19 | 20 | use std::ops::Deref; 21 | use std::str::FromStr; 22 | use std::thread; 23 | use std::time::Duration; 24 | 25 | use clap::{Arg, Command}; 26 | use log::LevelFilter; 27 | 28 | use crate::aggregator::manager::run as run_aggregator; 29 | use crate::config::config::Config; 30 | use crate::config::logger::ConfigLogger; 31 | use crate::config::reader::ConfigReader; 32 | use crate::prober::manager::{ 33 | initialize_store as initialize_store_prober, run_poll as run_poll_prober, 34 | run_script as run_script_prober, 35 | }; 36 | use crate::responder::manager::run as run_responder; 37 | 38 | struct AppArgs { 39 | config: String, 40 | } 41 | 42 | pub static THREAD_NAME_PROBER_POLL: &'static str = "vigil-prober-poll"; 43 | pub static THREAD_NAME_PROBER_SCRIPT: &'static str = "vigil-prober-script"; 44 | pub static THREAD_NAME_AGGREGATOR: &'static str = "vigil-aggregator"; 45 | pub static THREAD_NAME_RESPONDER: &'static str = "vigil-responder"; 46 | 47 | macro_rules! gen_spawn_managed { 48 | ($name:expr, $method:ident, $thread_name:ident, $managed_fn:ident) => { 49 | fn $method() { 50 | debug!("spawn managed thread: {}", $name); 51 | 52 | let worker = thread::Builder::new() 53 | .name($thread_name.to_string()) 54 | .spawn($managed_fn); 55 | 56 | // Block on worker thread (join it) 57 | let has_error = if let Ok(worker_thread) = worker { 58 | worker_thread.join().is_err() 59 | } else { 60 | true 61 | }; 62 | 63 | // Worker thread crashed? 64 | if has_error == true { 65 | error!("managed thread crashed ({}), setting it up again", $name); 66 | 67 | // Prevents thread start loop floods 68 | thread::sleep(Duration::from_secs(1)); 69 | 70 | $method(); 71 | } 72 | } 73 | }; 74 | } 75 | 76 | lazy_static! { 77 | static ref APP_ARGS: AppArgs = make_app_args(); 78 | static ref APP_CONF: Config = ConfigReader::make(); 79 | } 80 | 81 | gen_spawn_managed!( 82 | "prober-poll", 83 | spawn_poll_prober, 84 | THREAD_NAME_PROBER_POLL, 85 | run_poll_prober 86 | ); 87 | gen_spawn_managed!( 88 | "prober-script", 89 | spawn_script_prober, 90 | THREAD_NAME_PROBER_SCRIPT, 91 | run_script_prober 92 | ); 93 | gen_spawn_managed!( 94 | "aggregator", 95 | spawn_aggregator, 96 | THREAD_NAME_AGGREGATOR, 97 | run_aggregator 98 | ); 99 | 100 | gen_spawn_managed!( 101 | "responder", 102 | spawn_responder, 103 | THREAD_NAME_RESPONDER, 104 | run_responder 105 | ); 106 | 107 | fn make_app_args() -> AppArgs { 108 | let matches = Command::new(clap::crate_name!()) 109 | .version(clap::crate_version!()) 110 | .author(clap::crate_authors!()) 111 | .about(clap::crate_description!()) 112 | .arg( 113 | Arg::new("config") 114 | .short('c') 115 | .long("config") 116 | .help("Path to configuration file") 117 | .default_value("./config.cfg"), 118 | ) 119 | .get_matches(); 120 | 121 | // Generate owned app arguments 122 | AppArgs { 123 | config: matches 124 | .get_one::("config") 125 | .expect("invalid config value") 126 | .to_owned(), 127 | } 128 | } 129 | 130 | fn ensure_states() { 131 | // Ensure all statics are valid (a `deref` is enough to lazily initialize them) 132 | let (_, _) = (APP_ARGS.deref(), APP_CONF.deref()); 133 | 134 | // Ensure assets path exists 135 | assert_eq!( 136 | APP_CONF.assets.path.exists(), 137 | true, 138 | "assets directory not found: {:?}", 139 | APP_CONF.assets.path 140 | ); 141 | } 142 | 143 | fn main() { 144 | // Ensure OpenSSL root chain is found on current environment 145 | openssl_probe::init_ssl_cert_env_vars(); 146 | 147 | // Initialize shared logger 148 | let _logger = ConfigLogger::init( 149 | LevelFilter::from_str(&APP_CONF.server.log_level).expect("invalid log level"), 150 | ); 151 | 152 | info!("starting up"); 153 | 154 | // Ensure all states are bound 155 | ensure_states(); 156 | 157 | // Initialize prober store 158 | initialize_store_prober(); 159 | 160 | // Spawn probes (background thread) 161 | thread::spawn(spawn_poll_prober); 162 | thread::spawn(spawn_script_prober); 163 | 164 | // Spawn aggregator (background thread) 165 | thread::spawn(spawn_aggregator); 166 | 167 | // Spawn Web responder (foreground thread) 168 | spawn_responder(); 169 | 170 | error!("could not start"); 171 | } 172 | -------------------------------------------------------------------------------- /src/notifier/email.rs: -------------------------------------------------------------------------------- 1 | // Vigil 2 | // 3 | // Microservices Status Page 4 | // Copyright: 2018, Valerian Saliou 5 | // License: Mozilla Public License v2.0 (MPL v2.0) 6 | 7 | use std::time::Duration; 8 | 9 | use lettre::message::{Mailbox, Message}; 10 | use lettre::transport::smtp::authentication::Credentials; 11 | use lettre::transport::smtp::client::{Tls, TlsParameters}; 12 | use lettre::transport::smtp::{Error as SmtpError, SmtpTransport}; 13 | use lettre::{Address, Transport}; 14 | 15 | use super::generic::{GenericNotifier, Notification, DISPATCH_TIMEOUT_SECONDS}; 16 | use crate::config::config::ConfigNotify; 17 | use crate::APP_CONF; 18 | 19 | pub struct EmailNotifier; 20 | 21 | impl GenericNotifier for EmailNotifier { 22 | fn attempt(notify: &ConfigNotify, notification: &Notification) -> Result<(), bool> { 23 | if let Some(ref email_config) = notify.email { 24 | let nodes_label = notification.replicas.join(", "); 25 | 26 | // Build up the message text 27 | let mut message = String::new(); 28 | 29 | if notification.startup == true { 30 | message.push_str(&format!( 31 | "Status startup alert from: {}\n", 32 | APP_CONF.branding.page_title 33 | )); 34 | } else if notification.changed == true { 35 | message.push_str(&format!( 36 | "Status change report from: {}\n", 37 | APP_CONF.branding.page_title 38 | )); 39 | } else { 40 | message.push_str(&format!( 41 | "Status unchanged reminder from: {}\n", 42 | APP_CONF.branding.page_title 43 | )); 44 | } 45 | 46 | message.push_str("\n--\n"); 47 | message.push_str(&format!("Status: {:?}\n", notification.status)); 48 | message.push_str(&format!("Nodes: {}\n", &nodes_label)); 49 | message.push_str(&format!("Time: {}\n", ¬ification.time)); 50 | message.push_str(&format!("URL: {}", APP_CONF.branding.page_url.as_str())); 51 | 52 | message.push_str("\n--\n"); 53 | message.push_str("\n"); 54 | message.push_str("To unsubscribe, please edit your status page configuration."); 55 | 56 | debug!("will send email notification with message: {}", &message); 57 | 58 | // Build up the email 59 | let email_message = Message::builder() 60 | .to(Mailbox::new( 61 | None, 62 | email_config.to.parse::
().or(Err(true))?, 63 | )) 64 | .from(Mailbox::new( 65 | Some(APP_CONF.branding.page_title.to_owned()), 66 | email_config.from.parse::
().or(Err(true))?, 67 | )) 68 | .subject(if nodes_label.is_empty() { 69 | notification.status.as_str().to_uppercase() 70 | } else { 71 | format!( 72 | "{} | {}", 73 | notification.status.as_str().to_uppercase(), 74 | &nodes_label 75 | ) 76 | }) 77 | .body(message) 78 | .or(Err(true))?; 79 | 80 | // Create the transport if not present 81 | let transport = match acquire_transport( 82 | &email_config.smtp_host, 83 | email_config.smtp_port, 84 | email_config.smtp_username.to_owned(), 85 | email_config.smtp_password.to_owned(), 86 | email_config.smtp_encrypt, 87 | ) { 88 | Ok(email_config) => email_config, 89 | Err(err) => { 90 | error!("failed to build email transport: {err}"); 91 | 92 | return Err(true); 93 | } 94 | }; 95 | 96 | // Deliver the message 97 | if let Err(err) = transport.send(&email_message) { 98 | error!("failed to send email: {err}"); 99 | 100 | return Err(true); 101 | } 102 | 103 | return Ok(()); 104 | } 105 | 106 | Err(false) 107 | } 108 | 109 | fn can_notify(notify: &ConfigNotify, notification: &Notification) -> bool { 110 | if let Some(ref email_config) = notify.email { 111 | notification.expected(email_config.reminders_only) 112 | } else { 113 | false 114 | } 115 | } 116 | 117 | fn name() -> &'static str { 118 | "email" 119 | } 120 | } 121 | 122 | fn acquire_transport( 123 | smtp_host: &str, 124 | smtp_port: u16, 125 | smtp_username: Option, 126 | smtp_password: Option, 127 | smtp_encrypt: bool, 128 | ) -> Result { 129 | // Acquire credentials (if any) 130 | let credentials = if let (Some(smtp_username_value), Some(smtp_password_value)) = 131 | (smtp_username, smtp_password) 132 | { 133 | Some(Credentials::new( 134 | smtp_username_value.to_owned(), 135 | smtp_password_value.to_owned(), 136 | )) 137 | } else { 138 | None 139 | }; 140 | 141 | // Acquire TLS wrapper (may fail) 142 | let tls_wrapper = match TlsParameters::new(smtp_host.into()) { 143 | Ok(parameters) if smtp_encrypt => Tls::Required(parameters), 144 | Ok(parameters) => Tls::Opportunistic(parameters), 145 | Err(e) => return Err(e), 146 | }; 147 | 148 | // Build transport 149 | let mut mailer = SmtpTransport::builder_dangerous(smtp_host) 150 | .port(smtp_port) 151 | .tls(tls_wrapper) 152 | .timeout(Some(Duration::from_secs(DISPATCH_TIMEOUT_SECONDS))); 153 | 154 | if let Some(credentials_value) = credentials { 155 | mailer = mailer.credentials(credentials_value); 156 | } 157 | 158 | Ok(mailer.build()) 159 | } 160 | -------------------------------------------------------------------------------- /src/notifier/generic.rs: -------------------------------------------------------------------------------- 1 | // Vigil 2 | // 3 | // Microservices Status Page 4 | // Copyright: 2018, Valerian Saliou 5 | // License: Mozilla Public License v2.0 (MPL v2.0) 6 | 7 | use std::thread; 8 | use std::time::Duration; 9 | 10 | use crate::config::config::ConfigNotify; 11 | use crate::prober::status::Status; 12 | 13 | const DISPATCH_TRY_WAIT_SECONDS: u64 = 2; 14 | const DISPATCH_TRY_ATTEMPT_TIMES: u8 = 3; 15 | pub const DISPATCH_TIMEOUT_SECONDS: u64 = 10; 16 | 17 | pub struct Notification<'a> { 18 | pub status: &'a Status, 19 | pub time: String, 20 | pub replicas: Vec<&'a str>, 21 | pub changed: bool, 22 | pub startup: bool, 23 | } 24 | 25 | pub trait GenericNotifier { 26 | fn attempt(notify: &ConfigNotify, notification: &Notification) -> Result<(), bool>; 27 | fn can_notify(notify: &ConfigNotify, notification: &Notification) -> bool; 28 | fn name() -> &'static str; 29 | } 30 | 31 | impl<'a> Notification<'a> { 32 | pub fn dispatch( 33 | notify: &ConfigNotify, 34 | notification: &Notification, 35 | ) -> Result<(), bool> { 36 | if N::can_notify(notify, notification) == true { 37 | info!( 38 | "dispatch {} notification for status: {:?} and replicas: {:?}", 39 | N::name(), 40 | notification.status, 41 | notification.replicas 42 | ); 43 | 44 | for try_index in 1..(DISPATCH_TRY_ATTEMPT_TIMES + 1) { 45 | debug!( 46 | "dispatch {} notification attempt: #{}", 47 | N::name(), 48 | try_index 49 | ); 50 | 51 | // Hold on for next try 52 | if try_index > 1 { 53 | thread::sleep(Duration::from_secs(DISPATCH_TRY_WAIT_SECONDS)) 54 | } 55 | 56 | // Attempt notification dispatch 57 | if N::attempt(notify, notification).is_ok() == true { 58 | debug!("dispatched notification to provider: {}", N::name()); 59 | 60 | return Ok(()); 61 | } 62 | } 63 | 64 | error!("failed dispatching notification to provider: {}", N::name()); 65 | 66 | return Err(true); 67 | } 68 | 69 | debug!("did not dispatch notification to provider: {}", N::name()); 70 | 71 | Err(false) 72 | } 73 | 74 | pub fn expected(&self, reminders_only: bool) -> bool { 75 | // Notification may not be expected if status has changed, but we only want to receive \ 76 | // reminders on this specific notifier channel. 77 | if reminders_only == false || (reminders_only == true && self.changed == false) { 78 | true 79 | } else { 80 | false 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/notifier/gotify.rs: -------------------------------------------------------------------------------- 1 | // Vigil 2 | // 3 | // Microservices Status Page 4 | // Copyright: 2019, Valerian Saliou 5 | // Copyright: 2020, Rachel Chen 6 | // License: Mozilla Public License v2.0 (MPL v2.0) 7 | 8 | use std::collections::HashMap; 9 | use std::time::Duration; 10 | 11 | use reqwest::blocking::Client; 12 | 13 | use super::generic::{GenericNotifier, Notification, DISPATCH_TIMEOUT_SECONDS}; 14 | use crate::config::config::ConfigNotify; 15 | use crate::APP_CONF; 16 | 17 | lazy_static! { 18 | static ref GOTIFY_HTTP_CLIENT: Client = Client::builder() 19 | .timeout(Duration::from_secs(DISPATCH_TIMEOUT_SECONDS)) 20 | .gzip(true) 21 | .build() 22 | .unwrap(); 23 | } 24 | 25 | pub struct GotifyNotifier; 26 | 27 | impl GenericNotifier for GotifyNotifier { 28 | fn attempt(notify: &ConfigNotify, notification: &Notification) -> Result<(), bool> { 29 | if let Some(ref gotify) = notify.gotify { 30 | // Build up the message text 31 | let mut message = String::new(); 32 | 33 | if notification.startup == true { 34 | message.push_str("This is a startup alert.\n\n"); 35 | } else if notification.changed == false { 36 | message.push_str("This is a reminder.\n\n"); 37 | } 38 | 39 | message.push_str(&format!( 40 | "Status: {}\n", 41 | notification.status.as_str().to_uppercase() 42 | )); 43 | message.push_str(&format!("Nodes:\n{}\n", ¬ification.replicas.join("\n"))); 44 | message.push_str(&format!("Time: {}", ¬ification.time)); 45 | 46 | debug!("will send Gotify notification with message: {}", &message); 47 | 48 | // Generate URL 49 | // See: https://gotify.net/docs/pushmsg 50 | let url = format!( 51 | "{}message?token={}", 52 | gotify.app_url.as_str(), 53 | gotify.app_token 54 | ); 55 | 56 | // Build message parameters 57 | let mut params: HashMap<&str, &str> = HashMap::new(); 58 | 59 | params.insert("title", &APP_CONF.branding.page_title); 60 | params.insert("message", &message); 61 | 62 | if notification.changed == false { 63 | params.insert("priority", "10"); 64 | } 65 | 66 | // Submit message to Gotify 67 | let response = GOTIFY_HTTP_CLIENT.post(&url).form(¶ms).send(); 68 | 69 | if let Ok(response_inner) = response { 70 | if response_inner.status().is_success() != true { 71 | return Err(true); 72 | } 73 | } else { 74 | return Err(true); 75 | } 76 | 77 | return Ok(()); 78 | } 79 | 80 | Err(false) 81 | } 82 | 83 | fn can_notify(notify: &ConfigNotify, notification: &Notification) -> bool { 84 | if let Some(ref gotify_config) = notify.gotify { 85 | notification.expected(gotify_config.reminders_only) 86 | } else { 87 | false 88 | } 89 | } 90 | 91 | fn name() -> &'static str { 92 | "gotify" 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/notifier/matrix.rs: -------------------------------------------------------------------------------- 1 | // Vigil 2 | // 3 | // Microservices Status Page 4 | // Copyright: 2021, Valerian Saliou 5 | // Copyright: 2021, Enrico Risa https://github.com/wolf4ood 6 | // License: Mozilla Public License v2.0 (MPL v2.0) 7 | 8 | use std::collections::HashMap; 9 | use std::time::Duration; 10 | 11 | use reqwest::blocking::Client; 12 | 13 | use super::generic::{GenericNotifier, Notification, DISPATCH_TIMEOUT_SECONDS}; 14 | use crate::config::config::ConfigNotify; 15 | use crate::APP_CONF; 16 | 17 | lazy_static! { 18 | static ref MATRIX_HTTP_CLIENT: Client = Client::builder() 19 | .timeout(Duration::from_secs(DISPATCH_TIMEOUT_SECONDS)) 20 | .gzip(true) 21 | .build() 22 | .unwrap(); 23 | static ref MATRIX_FORMATTERS: Vec String> = vec![ 24 | format_status, 25 | format_replicas, 26 | format_status_page, 27 | format_time 28 | ]; 29 | } 30 | 31 | static MATRIX_MESSAGE_BODY: &'static str = "You received a Vigil alert."; 32 | static MATRIX_MESSAGE_TYPE: &'static str = "m.text"; 33 | static MATRIX_MESSAGE_FORMAT: &'static str = "org.matrix.custom.html"; 34 | 35 | pub struct MatrixNotifier; 36 | 37 | impl GenericNotifier for MatrixNotifier { 38 | fn attempt(notify: &ConfigNotify, notification: &Notification) -> Result<(), bool> { 39 | if let Some(ref matrix) = notify.matrix { 40 | // Build up the message text 41 | let message = format_message(notification); 42 | 43 | debug!("will send Matrix notification with message: {}", &message); 44 | 45 | // Generate URL 46 | // See: https://matrix.org/docs/guides/client-server-api#sending-messages 47 | let url = format!( 48 | "{}_matrix/client/r0/rooms/{}/send/m.room.message?access_token={}", 49 | matrix.homeserver_url.as_str(), 50 | matrix.room_id.as_str(), 51 | matrix.access_token.as_str() 52 | ); 53 | 54 | // Build message parameters 55 | let mut params: HashMap<&str, &str> = HashMap::new(); 56 | 57 | params.insert("body", MATRIX_MESSAGE_BODY); 58 | params.insert("msgtype", MATRIX_MESSAGE_TYPE); 59 | params.insert("format", MATRIX_MESSAGE_FORMAT); 60 | params.insert("formatted_body", &message); 61 | 62 | // Submit message to Matrix 63 | let response = MATRIX_HTTP_CLIENT.post(&url).json(¶ms).send(); 64 | 65 | if let Ok(response_inner) = response { 66 | if response_inner.status().is_success() != true { 67 | return Err(true); 68 | } 69 | } else { 70 | return Err(true); 71 | } 72 | 73 | return Ok(()); 74 | } 75 | 76 | Err(false) 77 | } 78 | 79 | fn can_notify(notify: &ConfigNotify, notification: &Notification) -> bool { 80 | if let Some(ref matrix_config) = notify.matrix { 81 | notification.expected(matrix_config.reminders_only) 82 | } else { 83 | false 84 | } 85 | } 86 | 87 | fn name() -> &'static str { 88 | "matrix" 89 | } 90 | } 91 | 92 | fn format_status(notification: &Notification) -> String { 93 | let msg = if notification.startup == true { 94 | "Status started up, as" 95 | } else if notification.changed == true { 96 | "Status changed to" 97 | } else { 98 | "Status is still" 99 | }; 100 | 101 | format!( 102 | "

{} {}: {}.

", 103 | notification.status.as_icon(), 104 | msg, 105 | notification.status.as_str().to_uppercase() 106 | ) 107 | } 108 | 109 | fn format_replicas(notification: &Notification) -> String { 110 | let replicas = notification 111 | .replicas 112 | .iter() 113 | .map(|replica| replica.split(":").take(2).collect::>().join(":")) 114 | .fold(HashMap::new(), |mut replicas_count, replica| { 115 | *replicas_count.entry(replica).or_insert(0) += 1; 116 | replicas_count 117 | }) 118 | .iter() 119 | .map(|(service_and_node, count)| { 120 | format!( 121 | "
  • {}: {} {}
  • ", 122 | service_and_node, 123 | count, 124 | notification.status.as_str() 125 | ) 126 | }) 127 | .collect::>(); 128 | 129 | if replicas.is_empty() { 130 | "".to_string() 131 | } else { 132 | format!("
      {}
    ", replicas.join("")) 133 | } 134 | } 135 | 136 | fn format_status_page(_: &Notification) -> String { 137 | format!( 138 | "

    Status page: {}

    ", 139 | APP_CONF.branding.page_url.as_str() 140 | ) 141 | } 142 | 143 | fn format_time(notification: &Notification) -> String { 144 | format!("

    Time: {}

    ", notification.time) 145 | } 146 | 147 | fn format_message(notification: &Notification) -> String { 148 | MATRIX_FORMATTERS 149 | .iter() 150 | .fold(String::new(), |mut accumulator, formatter| { 151 | accumulator.push_str(formatter(notification).as_str()); 152 | accumulator 153 | }) 154 | } 155 | -------------------------------------------------------------------------------- /src/notifier/mod.rs: -------------------------------------------------------------------------------- 1 | // Vigil 2 | // 3 | // Microservices Status Page 4 | // Copyright: 2018, Valerian Saliou 5 | // License: Mozilla Public License v2.0 (MPL v2.0) 6 | 7 | pub mod generic; 8 | 9 | #[cfg(feature = "notifier-email")] 10 | pub mod email; 11 | 12 | #[cfg(feature = "notifier-twilio")] 13 | pub mod twilio; 14 | 15 | #[cfg(feature = "notifier-slack")] 16 | pub mod slack; 17 | 18 | #[cfg(feature = "notifier-zulip")] 19 | pub mod zulip; 20 | 21 | #[cfg(feature = "notifier-telegram")] 22 | pub mod telegram; 23 | 24 | #[cfg(feature = "notifier-pushover")] 25 | pub mod pushover; 26 | 27 | #[cfg(feature = "notifier-gotify")] 28 | pub mod gotify; 29 | 30 | #[cfg(feature = "notifier-xmpp")] 31 | pub mod xmpp; 32 | 33 | #[cfg(feature = "notifier-matrix")] 34 | pub mod matrix; 35 | 36 | #[cfg(feature = "notifier-webex")] 37 | pub mod webex; 38 | 39 | #[cfg(feature = "notifier-webhook")] 40 | pub mod webhook; 41 | -------------------------------------------------------------------------------- /src/notifier/pushover.rs: -------------------------------------------------------------------------------- 1 | // Vigil 2 | // 3 | // Microservices Status Page 4 | // Copyright: 2019, Valerian Saliou 5 | // License: Mozilla Public License v2.0 (MPL v2.0) 6 | 7 | use std::collections::HashMap; 8 | use std::time::Duration; 9 | 10 | use reqwest::blocking::Client; 11 | 12 | use super::generic::{GenericNotifier, Notification, DISPATCH_TIMEOUT_SECONDS}; 13 | use crate::config::config::ConfigNotify; 14 | use crate::prober::status::Status; 15 | use crate::APP_CONF; 16 | 17 | lazy_static! { 18 | static ref PUSHOVER_HTTP_CLIENT: Client = Client::builder() 19 | .timeout(Duration::from_secs(DISPATCH_TIMEOUT_SECONDS)) 20 | .gzip(true) 21 | .build() 22 | .unwrap(); 23 | } 24 | 25 | static PUSHOVER_API_URL: &'static str = "https://api.pushover.net/1/messages.json"; 26 | 27 | pub struct PushoverNotifier; 28 | 29 | impl GenericNotifier for PushoverNotifier { 30 | fn attempt(notify: &ConfigNotify, notification: &Notification) -> Result<(), bool> { 31 | if let Some(ref pushover) = notify.pushover { 32 | // Build up the message text 33 | let mut message = String::new(); 34 | 35 | if notification.startup == true { 36 | message.push_str("This is a startup alert.\n\n"); 37 | } else if notification.changed == false { 38 | message.push_str("This is a reminder.\n\n"); 39 | } 40 | 41 | message.push_str(&format!( 42 | "Status: {}\n", 43 | status_to_color(¬ification.status), 44 | notification.status.as_str().to_uppercase() 45 | )); 46 | message.push_str(&format!( 47 | "Nodes: {}\n", 48 | ¬ification.replicas.join(", ") 49 | )); 50 | message.push_str(&format!("Time: {}", ¬ification.time)); 51 | 52 | debug!("will send Pushover notification with message: {}", &message); 53 | 54 | let mut has_sub_delivery_failure = false; 55 | 56 | for user_key in &pushover.user_keys { 57 | // Build form parameters 58 | let mut params: HashMap<&str, &str> = HashMap::new(); 59 | 60 | // Append authorization values 61 | params.insert("token", &pushover.app_token); 62 | params.insert("user", user_key); 63 | 64 | // Append title & message 65 | params.insert("title", &APP_CONF.branding.page_title); 66 | params.insert("message", &message); 67 | params.insert("html", "1"); 68 | 69 | // Append target URL 70 | let url_title = format!("Details on {}", APP_CONF.branding.page_title); 71 | 72 | params.insert("url_title", &url_title); 73 | params.insert("url", APP_CONF.branding.page_url.as_str()); 74 | 75 | // Mark as high-priority? (reminder) 76 | if notification.changed == false { 77 | params.insert("priority", "1"); 78 | } 79 | 80 | // Submit message to Pushover 81 | let response = PUSHOVER_HTTP_CLIENT 82 | .post(PUSHOVER_API_URL) 83 | .form(¶ms) 84 | .send(); 85 | 86 | // Check for any failure 87 | if let Ok(response_inner) = response { 88 | if response_inner.status().is_success() != true { 89 | has_sub_delivery_failure = true; 90 | } 91 | } else { 92 | has_sub_delivery_failure = true; 93 | } 94 | } 95 | 96 | if has_sub_delivery_failure == true { 97 | return Err(true); 98 | } 99 | 100 | return Ok(()); 101 | } 102 | 103 | Err(false) 104 | } 105 | 106 | fn can_notify(notify: &ConfigNotify, notification: &Notification) -> bool { 107 | if let Some(ref pushover_config) = notify.pushover { 108 | notification.expected(pushover_config.reminders_only) 109 | } else { 110 | false 111 | } 112 | } 113 | 114 | fn name() -> &'static str { 115 | "pushover" 116 | } 117 | } 118 | 119 | fn status_to_color(status: &Status) -> &'static str { 120 | match status { 121 | &Status::Healthy => "#54A158", 122 | &Status::Sick => "#D5A048", 123 | &Status::Dead => "#C4291C", 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/notifier/slack.rs: -------------------------------------------------------------------------------- 1 | // Vigil 2 | // 3 | // Microservices Status Page 4 | // Copyright: 2018, Valerian Saliou 5 | // License: Mozilla Public License v2.0 (MPL v2.0) 6 | 7 | use std::time::Duration; 8 | 9 | use reqwest::blocking::Client; 10 | 11 | use super::generic::{GenericNotifier, Notification, DISPATCH_TIMEOUT_SECONDS}; 12 | use crate::config::config::ConfigNotify; 13 | use crate::prober::status::Status; 14 | use crate::APP_CONF; 15 | 16 | lazy_static! { 17 | static ref SLACK_HTTP_CLIENT: Client = Client::builder() 18 | .timeout(Duration::from_secs(DISPATCH_TIMEOUT_SECONDS)) 19 | .gzip(true) 20 | .build() 21 | .unwrap(); 22 | } 23 | 24 | pub struct SlackNotifier; 25 | 26 | #[derive(Serialize)] 27 | struct SlackPayload<'a> { 28 | text: String, 29 | attachments: Vec>, 30 | } 31 | 32 | #[derive(Serialize)] 33 | struct SlackPayloadAttachment<'a> { 34 | fallback: String, 35 | color: &'a str, 36 | fields: Vec>, 37 | } 38 | 39 | #[derive(Serialize)] 40 | struct SlackPayloadAttachmentField<'a> { 41 | title: &'a str, 42 | value: &'a str, 43 | short: bool, 44 | } 45 | 46 | impl GenericNotifier for SlackNotifier { 47 | fn attempt(notify: &ConfigNotify, notification: &Notification) -> Result<(), bool> { 48 | if let Some(ref slack) = notify.slack { 49 | let status_label = format!("{:?}", notification.status); 50 | let mut nodes_label = String::new(); 51 | 52 | // Build message 53 | let message_text = if notification.startup == true { 54 | format!("Status started up, as: *{}*.", notification.status.as_str()) 55 | } else if notification.changed == true { 56 | format!("Status changed to: *{}*.", notification.status.as_str()) 57 | } else { 58 | format!("Status is still: *{}*.", notification.status.as_str()) 59 | }; 60 | 61 | let payload_text = if slack.mention_channel == true { 62 | format!(" {}", &message_text) 63 | } else { 64 | message_text.to_owned() 65 | }; 66 | 67 | // Build paylaod 68 | let mut payload = SlackPayload { 69 | text: payload_text, 70 | attachments: Vec::new(), 71 | }; 72 | 73 | let mut attachment = SlackPayloadAttachment { 74 | fallback: message_text, 75 | color: status_to_color(¬ification.status), 76 | fields: Vec::new(), 77 | }; 78 | 79 | // Append attachment fields 80 | if notification.replicas.len() > 0 { 81 | nodes_label.push_str(¬ification.replicas.join(", ")); 82 | 83 | let nodes_label_titled = format!(" Nodes: *{}*.", nodes_label); 84 | 85 | payload.text.push_str(&nodes_label_titled); 86 | attachment.fallback.push_str(&nodes_label_titled); 87 | 88 | attachment.fields.push(SlackPayloadAttachmentField { 89 | title: "Nodes", 90 | value: &nodes_label, 91 | short: false, 92 | }); 93 | } 94 | 95 | attachment.fields.push(SlackPayloadAttachmentField { 96 | title: "Status", 97 | value: &status_label, 98 | short: true, 99 | }); 100 | 101 | attachment.fields.push(SlackPayloadAttachmentField { 102 | title: "Time", 103 | value: ¬ification.time, 104 | short: true, 105 | }); 106 | 107 | attachment.fields.push(SlackPayloadAttachmentField { 108 | title: "Monitor Page", 109 | value: APP_CONF.branding.page_url.as_str(), 110 | short: false, 111 | }); 112 | 113 | // Append attachment 114 | payload.attachments.push(attachment); 115 | 116 | // Submit payload to Slack 117 | let response = SLACK_HTTP_CLIENT 118 | .post(slack.hook_url.as_str()) 119 | .json(&payload) 120 | .send(); 121 | 122 | if let Ok(response_inner) = response { 123 | if response_inner.status().is_success() == true { 124 | return Ok(()); 125 | } 126 | } 127 | 128 | return Err(true); 129 | } 130 | 131 | Err(false) 132 | } 133 | 134 | fn can_notify(notify: &ConfigNotify, notification: &Notification) -> bool { 135 | if let Some(ref slack_config) = notify.slack { 136 | notification.expected(slack_config.reminders_only) 137 | } else { 138 | false 139 | } 140 | } 141 | 142 | fn name() -> &'static str { 143 | "slack" 144 | } 145 | } 146 | 147 | fn status_to_color(status: &Status) -> &'static str { 148 | match status { 149 | &Status::Healthy => "good", 150 | &Status::Sick => "warning", 151 | &Status::Dead => "danger", 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/notifier/telegram.rs: -------------------------------------------------------------------------------- 1 | // Vigil 2 | // 3 | // Microservices Status Page 4 | // Copyright: 2019, Valerian Saliou 5 | // License: Mozilla Public License v2.0 (MPL v2.0) 6 | 7 | use std::collections::HashMap; 8 | use std::time::Duration; 9 | 10 | use reqwest::blocking::Client; 11 | 12 | use super::generic::{GenericNotifier, Notification, DISPATCH_TIMEOUT_SECONDS}; 13 | use crate::config::config::ConfigNotify; 14 | use crate::APP_CONF; 15 | 16 | lazy_static! { 17 | static ref TELEGRAM_HTTP_CLIENT: Client = Client::builder() 18 | .timeout(Duration::from_secs(DISPATCH_TIMEOUT_SECONDS)) 19 | .gzip(true) 20 | .build() 21 | .unwrap(); 22 | } 23 | 24 | static TELEGRAM_API_BASE_URL: &'static str = "https://api.telegram.org"; 25 | 26 | pub struct TelegramNotifier; 27 | 28 | #[derive(Serialize)] 29 | struct TelegramPayload<'a> { 30 | chat_id: TelegramChatID<'a>, 31 | text: String, 32 | parse_mode: &'static str, 33 | disable_web_page_preview: bool, 34 | } 35 | 36 | #[derive(Serialize)] 37 | #[serde(untagged)] 38 | enum TelegramChatID<'a> { 39 | Group(&'a str), 40 | User(u64), 41 | } 42 | 43 | impl GenericNotifier for TelegramNotifier { 44 | fn attempt(notify: &ConfigNotify, notification: &Notification) -> Result<(), bool> { 45 | if let Some(ref telegram) = notify.telegram { 46 | // Build message 47 | let mut message = if notification.startup == true { 48 | format!( 49 | "{} Status started up, as: *{}*.\n", 50 | notification.status.as_icon(), 51 | notification.status.as_str().to_uppercase() 52 | ) 53 | } else if notification.changed == true { 54 | format!( 55 | "{} Status changed to: *{}*.\n", 56 | notification.status.as_icon(), 57 | notification.status.as_str().to_uppercase() 58 | ) 59 | } else { 60 | format!( 61 | "{} Status is still: *{}*.\n", 62 | notification.status.as_icon(), 63 | notification.status.as_str().to_uppercase() 64 | ) 65 | }; 66 | 67 | let mut replicas_count: HashMap = HashMap::new(); 68 | 69 | for replica in notification.replicas.iter() { 70 | let service_and_node = replica.split(":").take(2).collect::>().join(":"); 71 | *replicas_count.entry(service_and_node).or_insert(0) += 1; 72 | } 73 | 74 | let nodes_count_list_text = replicas_count 75 | .iter() 76 | .map(|(service_and_node, count)| { 77 | format!( 78 | "- `{}`: {} {}", 79 | service_and_node, 80 | count, 81 | notification.status.as_str() 82 | ) 83 | }) 84 | .collect::>() 85 | .join("\n"); 86 | 87 | message.push_str(&nodes_count_list_text); 88 | message.push_str(&format!("\nLink: {}", APP_CONF.branding.page_url.as_str())); 89 | 90 | debug!("will send Telegram notification with message: {}", &message); 91 | 92 | // Generate Telegram chat identifier 93 | let chat_id = match &telegram.chat_id.parse::() { 94 | Ok(user_chat_id) => TelegramChatID::User(*user_chat_id), 95 | Err(_) => TelegramChatID::Group(&telegram.chat_id.as_str()), 96 | }; 97 | 98 | // Build payload 99 | let payload = TelegramPayload { 100 | chat_id: chat_id, 101 | text: message, 102 | parse_mode: "markdown", 103 | disable_web_page_preview: true, 104 | }; 105 | 106 | // Generate target API URL 107 | let url = format!( 108 | "{}/bot{}/sendMessage", 109 | TELEGRAM_API_BASE_URL, telegram.bot_token 110 | ); 111 | 112 | // Submit message to Telegram 113 | let response = TELEGRAM_HTTP_CLIENT 114 | .post(url.as_str()) 115 | .json(&payload) 116 | .send(); 117 | 118 | // Check for any failure 119 | if let Ok(response_inner) = response { 120 | if response_inner.status().is_success() == true { 121 | return Ok(()); 122 | } 123 | } 124 | 125 | return Err(true); 126 | } 127 | 128 | Err(false) 129 | } 130 | 131 | fn can_notify(notify: &ConfigNotify, notification: &Notification) -> bool { 132 | if let Some(ref telegram_config) = notify.telegram { 133 | notification.expected(telegram_config.reminders_only) 134 | } else { 135 | false 136 | } 137 | } 138 | 139 | fn name() -> &'static str { 140 | "telegram" 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/notifier/twilio.rs: -------------------------------------------------------------------------------- 1 | // Vigil 2 | // 3 | // Microservices Status Page 4 | // Copyright: 2018, Valerian Saliou 5 | // License: Mozilla Public License v2.0 (MPL v2.0) 6 | 7 | use std::collections::HashMap; 8 | use std::time::Duration; 9 | 10 | use reqwest::blocking::Client; 11 | 12 | use super::generic::{GenericNotifier, Notification, DISPATCH_TIMEOUT_SECONDS}; 13 | use crate::config::config::ConfigNotify; 14 | use crate::APP_CONF; 15 | 16 | static TEXT_MESSAGE_TRUNCATED_INDICATOR: &'static str = "[..]"; 17 | 18 | const TEXT_MESSAGE_MAXIMUM_LENGTH: usize = 1000; 19 | 20 | lazy_static! { 21 | static ref TWILIO_HTTP_CLIENT: Client = Client::builder() 22 | .timeout(Duration::from_secs(DISPATCH_TIMEOUT_SECONDS)) 23 | .gzip(true) 24 | .build() 25 | .unwrap(); 26 | } 27 | 28 | pub struct TwilioNotifier; 29 | 30 | impl GenericNotifier for TwilioNotifier { 31 | fn attempt(notify: &ConfigNotify, notification: &Notification) -> Result<(), bool> { 32 | if let Some(ref twilio) = notify.twilio { 33 | // Build up the message text 34 | let mut message = String::new(); 35 | 36 | if notification.startup == true { 37 | message.push_str("Startup alert for: "); 38 | } else if notification.changed == false { 39 | message.push_str("Reminder for: "); 40 | } 41 | 42 | message.push_str(&format!("{}\n", APP_CONF.branding.page_title)); 43 | message.push_str("\n"); 44 | message.push_str(&format!("Status: {:?}\n", notification.status)); 45 | message.push_str(&format!("Nodes: {}\n", ¬ification.replicas.join(", "))); 46 | message.push_str(&format!("Time: {}\n", ¬ification.time)); 47 | 48 | // Trim down message to a maximum length? (most SMS receivers and networks support \ 49 | // up to 1600 characters by re-building message segments) 50 | if message.len() > TEXT_MESSAGE_MAXIMUM_LENGTH { 51 | debug!( 52 | "message for Twilio notification is too long, trimming to length: {}", 53 | TEXT_MESSAGE_MAXIMUM_LENGTH 54 | ); 55 | 56 | message 57 | .truncate(TEXT_MESSAGE_MAXIMUM_LENGTH - TEXT_MESSAGE_TRUNCATED_INDICATOR.len()); 58 | 59 | message.push_str(TEXT_MESSAGE_TRUNCATED_INDICATOR); 60 | } 61 | 62 | debug!("will send Twilio notification with message: {}", &message); 63 | 64 | let mut has_sub_delivery_failure = false; 65 | 66 | for to_number in &twilio.to { 67 | // Build form parameters 68 | let mut params = HashMap::new(); 69 | 70 | params.insert("MessagingServiceSid", &twilio.service_sid); 71 | params.insert("To", to_number); 72 | params.insert("Body", &message); 73 | 74 | // Submit message to Twilio 75 | let response = TWILIO_HTTP_CLIENT 76 | .post(&generate_api_url(&twilio.account_sid)) 77 | .basic_auth( 78 | twilio.account_sid.as_str(), 79 | Some(twilio.auth_token.as_str()), 80 | ) 81 | .form(¶ms) 82 | .send(); 83 | 84 | // Check for any failure 85 | if let Ok(response_inner) = response { 86 | if response_inner.status().is_success() != true { 87 | has_sub_delivery_failure = true; 88 | } 89 | } else { 90 | has_sub_delivery_failure = true; 91 | } 92 | } 93 | 94 | if has_sub_delivery_failure == true { 95 | return Err(true); 96 | } 97 | 98 | return Ok(()); 99 | } 100 | 101 | Err(false) 102 | } 103 | 104 | fn can_notify(notify: &ConfigNotify, notification: &Notification) -> bool { 105 | if let Some(ref twilio_config) = notify.twilio { 106 | notification.expected(twilio_config.reminders_only) 107 | } else { 108 | false 109 | } 110 | } 111 | 112 | fn name() -> &'static str { 113 | "twilio" 114 | } 115 | } 116 | 117 | fn generate_api_url(account_sid: &str) -> String { 118 | format!( 119 | "https://api.twilio.com/2010-04-01/Accounts/{}/Messages.json", 120 | account_sid 121 | ) 122 | } 123 | -------------------------------------------------------------------------------- /src/notifier/webex.rs: -------------------------------------------------------------------------------- 1 | // Vigil 2 | // 3 | // Microservices Status Page 4 | // Copyright: 2022, Valerian Saliou 5 | // Copyright: 2022, Timmy O'Tool https://github.com/TimmyOtool 6 | // License: Mozilla Public License v2.0 (MPL v2.0) 7 | 8 | use std::time::Duration; 9 | 10 | use reqwest::blocking::Client; 11 | 12 | use super::generic::{GenericNotifier, Notification, DISPATCH_TIMEOUT_SECONDS}; 13 | use crate::config::config::ConfigNotify; 14 | use crate::APP_CONF; 15 | 16 | lazy_static! { 17 | static ref WEBEX_HTTP_CLIENT: Client = Client::builder() 18 | .timeout(Duration::from_secs(DISPATCH_TIMEOUT_SECONDS)) 19 | .gzip(true) 20 | .build() 21 | .unwrap(); 22 | } 23 | 24 | pub struct WebExNotifier; 25 | 26 | #[derive(Serialize)] 27 | struct WebExPayload<'a> { 28 | #[serde(rename = "roomId")] 29 | room_id: &'a str, 30 | text: &'a str, 31 | } 32 | 33 | impl GenericNotifier for WebExNotifier { 34 | fn attempt(notify: &ConfigNotify, notification: &Notification) -> Result<(), bool> { 35 | if let Some(ref webex) = notify.webex { 36 | let nodes_label = notification.replicas.join(", "); 37 | 38 | // Build up the message text 39 | let mut message = String::new(); 40 | 41 | if notification.startup == true { 42 | message.push_str(&format!( 43 | "Status startup alert from: {}\n", 44 | APP_CONF.branding.page_title 45 | )); 46 | } else if notification.changed == true { 47 | message.push_str(&format!( 48 | "Status change report from: {}\n", 49 | APP_CONF.branding.page_title 50 | )); 51 | } else { 52 | message.push_str(&format!( 53 | "Status unchanged reminder from: {}\n", 54 | APP_CONF.branding.page_title 55 | )); 56 | } 57 | 58 | message.push_str(&format!("Status: {:?}\n", notification.status)); 59 | message.push_str(&format!("Nodes: {}\n", &nodes_label)); 60 | message.push_str(&format!("Time: {}\n", ¬ification.time)); 61 | message.push_str(&format!("URL: {}", APP_CONF.branding.page_url.as_str())); 62 | 63 | // Build paylaod 64 | let payload = WebExPayload { 65 | room_id: webex.room_id.as_str(), 66 | text: &message, 67 | }; 68 | 69 | // Submit payload to Webex 70 | let response = WEBEX_HTTP_CLIENT 71 | .post(webex.endpoint_url.as_str()) 72 | .header("Authorization", "Bearer ".to_owned() + webex.token.as_str()) 73 | .json(&payload) 74 | .send(); 75 | 76 | if let Ok(response_inner) = response { 77 | if response_inner.status().is_success() == true { 78 | return Ok(()); 79 | } 80 | } 81 | 82 | return Err(true); 83 | } 84 | 85 | Err(false) 86 | } 87 | 88 | fn can_notify(notify: &ConfigNotify, notification: &Notification) -> bool { 89 | if let Some(ref webex_config) = notify.webex { 90 | notification.expected(webex_config.reminders_only) 91 | } else { 92 | false 93 | } 94 | } 95 | 96 | fn name() -> &'static str { 97 | "webex" 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/notifier/webhook.rs: -------------------------------------------------------------------------------- 1 | // Vigil 2 | // 3 | // Microservices Status Page 4 | // Copyright: 2019, Valerian Saliou 5 | // License: Mozilla Public License v2.0 (MPL v2.0) 6 | 7 | use std::time::Duration; 8 | 9 | use reqwest::blocking::Client; 10 | 11 | use super::generic::{GenericNotifier, Notification, DISPATCH_TIMEOUT_SECONDS}; 12 | use crate::config::config::ConfigNotify; 13 | use crate::prober::status::Status; 14 | use crate::APP_CONF; 15 | 16 | lazy_static! { 17 | static ref WEBHOOK_HTTP_CLIENT: Client = Client::builder() 18 | .timeout(Duration::from_secs(DISPATCH_TIMEOUT_SECONDS)) 19 | .gzip(true) 20 | .build() 21 | .unwrap(); 22 | } 23 | 24 | pub struct WebHookNotifier; 25 | 26 | #[derive(Serialize)] 27 | struct WebHookPayload<'a> { 28 | #[serde(rename = "type")] 29 | _type: WebHookPayloadType, 30 | 31 | status: &'a Status, 32 | time: &'a str, 33 | replicas: &'a [&'a str], 34 | page: WebHookPayloadPage<'a>, 35 | } 36 | 37 | #[derive(Serialize)] 38 | pub enum WebHookPayloadType { 39 | #[serde(rename = "startup")] 40 | Startup, 41 | 42 | #[serde(rename = "changed")] 43 | Changed, 44 | 45 | #[serde(rename = "reminder")] 46 | Reminder, 47 | } 48 | 49 | #[derive(Serialize)] 50 | struct WebHookPayloadPage<'a> { 51 | title: &'a str, 52 | url: &'a str, 53 | } 54 | 55 | impl GenericNotifier for WebHookNotifier { 56 | fn attempt(notify: &ConfigNotify, notification: &Notification) -> Result<(), bool> { 57 | if let Some(ref webhook) = notify.webhook { 58 | // Acquire hook type 59 | let hook_type = if notification.startup == true { 60 | WebHookPayloadType::Startup 61 | } else if notification.changed == true { 62 | WebHookPayloadType::Changed 63 | } else { 64 | WebHookPayloadType::Reminder 65 | }; 66 | 67 | // Build paylaod 68 | let payload = WebHookPayload { 69 | _type: hook_type, 70 | status: notification.status, 71 | time: notification.time.as_str(), 72 | replicas: ¬ification.replicas, 73 | page: WebHookPayloadPage { 74 | title: APP_CONF.branding.page_title.as_str(), 75 | url: APP_CONF.branding.page_url.as_str(), 76 | }, 77 | }; 78 | 79 | // Submit payload to Web Hooks 80 | let response = WEBHOOK_HTTP_CLIENT 81 | .post(webhook.hook_url.as_str()) 82 | .json(&payload) 83 | .send(); 84 | 85 | if let Ok(response_inner) = response { 86 | if response_inner.status().is_success() == true { 87 | return Ok(()); 88 | } 89 | } 90 | 91 | return Err(true); 92 | } 93 | 94 | Err(false) 95 | } 96 | 97 | fn can_notify(notify: &ConfigNotify, _: &Notification) -> bool { 98 | notify.webhook.is_some() 99 | } 100 | 101 | fn name() -> &'static str { 102 | "webhook" 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/notifier/xmpp.rs: -------------------------------------------------------------------------------- 1 | // Vigil 2 | // 3 | // Microservices Status Page 4 | // Copyright: 2018, Valerian Saliou 5 | // License: Mozilla Public License v2.0 (MPL v2.0) 6 | 7 | use std::sync::RwLock; 8 | use std::time::{Duration, SystemTime}; 9 | 10 | use libstrophe::{Connection, ConnectionEvent, Context, Stanza}; 11 | 12 | use super::generic::{GenericNotifier, Notification, DISPATCH_TIMEOUT_SECONDS}; 13 | use crate::config::config::ConfigNotify; 14 | use crate::APP_CONF; 15 | 16 | pub struct XMPPNotifier; 17 | 18 | impl GenericNotifier for XMPPNotifier { 19 | fn attempt(notify: &ConfigNotify, notification: &Notification) -> Result<(), bool> { 20 | if let Some(ref xmpp) = notify.xmpp { 21 | let is_sent = RwLock::new(false); 22 | 23 | // Build up the message text 24 | let mut message = String::new(); 25 | 26 | if notification.startup == true { 27 | message.push_str("Startup alert for: "); 28 | } else if notification.changed == false { 29 | message.push_str("Reminder for: "); 30 | } 31 | 32 | message.push_str(&format!("{}\n", APP_CONF.branding.page_title)); 33 | message.push_str("\n"); 34 | message.push_str(&format!("Status: {:?}\n", notification.status)); 35 | message.push_str(&format!("Nodes: {}\n", ¬ification.replicas.join(", "))); 36 | message.push_str(&format!("Time: {}\n", ¬ification.time)); 37 | message.push_str(&format!("URL: {}", APP_CONF.branding.page_url.as_str())); 38 | 39 | debug!("will send XMPP notification with message: {}", &message); 40 | 41 | // Configure connection handler 42 | let fn_handle = 43 | |context: &Context, connection: &mut Connection, event: ConnectionEvent| { 44 | match event { 45 | ConnectionEvent::Connect => { 46 | debug!("connected to XMPP account: {}", &xmpp.from); 47 | 48 | // Acquire UNIX time (used to stamp the message w/ an unique identifier) 49 | let now_timestamp = if let Ok(unix_time) = 50 | SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) 51 | { 52 | unix_time.as_secs() 53 | } else { 54 | 0 55 | }; 56 | 57 | // Send status message 58 | let mut message_stanza = Stanza::new_message( 59 | Some("chat"), 60 | Some(&format!("vigil-{}", now_timestamp)), 61 | Some(&xmpp.to), 62 | ); 63 | 64 | if message_stanza.set_body(&message).is_ok() == true { 65 | connection.send(&message_stanza); 66 | 67 | { 68 | let mut is_sent_value = is_sent.write().unwrap(); 69 | 70 | *is_sent_value = true; 71 | } 72 | } 73 | 74 | // Disconnect immediately 75 | connection.disconnect(); 76 | } 77 | ConnectionEvent::Disconnect(err) => { 78 | if let Some(err) = err { 79 | error!( 80 | "connection failure to XMPP account: {} ({:?})", 81 | &xmpp.from, err 82 | ); 83 | } else { 84 | debug!("disconnected from XMPP account: {}", &xmpp.from); 85 | } 86 | 87 | context.stop(); 88 | } 89 | _ => {} 90 | } 91 | }; 92 | 93 | // Configure XMPP connection 94 | let context = Context::new_with_default_logger(); 95 | let mut connection = Connection::new(context); 96 | 97 | connection.set_jid(&xmpp.from); 98 | connection.set_pass(&xmpp.xmpp_password); 99 | 100 | connection.set_keepalive( 101 | Duration::from_secs(DISPATCH_TIMEOUT_SECONDS), 102 | Duration::from_secs(DISPATCH_TIMEOUT_SECONDS / 2), 103 | ); 104 | 105 | // Connect to XMPP server 106 | if let Ok(connection_context) = connection.connect_client(None, None, &fn_handle) { 107 | // Enter context 108 | connection_context.run(); 109 | 110 | if *is_sent.read().unwrap() == true { 111 | return Ok(()); 112 | } 113 | } 114 | 115 | return Err(true); 116 | } 117 | 118 | Err(false) 119 | } 120 | 121 | fn can_notify(notify: &ConfigNotify, notification: &Notification) -> bool { 122 | if let Some(ref xmpp_config) = notify.xmpp { 123 | notification.expected(xmpp_config.reminders_only) 124 | } else { 125 | false 126 | } 127 | } 128 | 129 | fn name() -> &'static str { 130 | "xmpp" 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/notifier/zulip.rs: -------------------------------------------------------------------------------- 1 | // Vigil 2 | // 3 | // Microservices Status Page 4 | // Copyright: 2021, Valerian Saliou 5 | // Copyright: 2021, Bastien Orivel 6 | // License: Mozilla Public License v2.0 (MPL v2.0) 7 | 8 | use std::time::Duration; 9 | 10 | use reqwest::blocking::Client; 11 | 12 | use super::generic::{GenericNotifier, Notification, DISPATCH_TIMEOUT_SECONDS}; 13 | use crate::config::config::ConfigNotify; 14 | use crate::prober::status::Status; 15 | use crate::APP_CONF; 16 | 17 | lazy_static! { 18 | static ref ZULIP_HTTP_CLIENT: Client = Client::builder() 19 | .timeout(Duration::from_secs(DISPATCH_TIMEOUT_SECONDS)) 20 | .gzip(true) 21 | .build() 22 | .unwrap(); 23 | } 24 | 25 | pub struct ZulipNotifier; 26 | 27 | #[derive(Serialize)] 28 | struct ZulipPayload<'a> { 29 | #[serde(rename(serialize = "type"))] 30 | type_: &'a str, 31 | to: &'a str, 32 | topic: &'a str, 33 | content: &'a str, 34 | } 35 | 36 | impl GenericNotifier for ZulipNotifier { 37 | fn attempt(notify: &ConfigNotify, notification: &Notification) -> Result<(), bool> { 38 | if let Some(ref zulip) = notify.zulip { 39 | let status_label = format!("{:?}", notification.status); 40 | 41 | let status_text = match notification.status { 42 | Status::Dead => " *dead* :boom:", 43 | Status::Healthy => " *healthy* :check_mark:", 44 | Status::Sick => " *sick* :sick:", 45 | }; 46 | 47 | // Build message 48 | let mut message_text = if notification.startup == true { 49 | format!("Status started up, as: {}.", status_text) 50 | } else if notification.changed { 51 | format!("Status changed to: {}.", status_text) 52 | } else { 53 | format!("Status is still: {}.", status_text) 54 | }; 55 | 56 | if notification.replicas.len() > 0 { 57 | let nodes_label = notification.replicas.join(", "); 58 | let nodes_label_titled = format!("\n **Nodes**: *{}*.", nodes_label); 59 | 60 | message_text.push_str(&nodes_label_titled); 61 | } 62 | 63 | message_text.push_str(&format!("\n **Status**: {}", &status_label)); 64 | message_text.push_str(&format!("\n **Time**: {}", ¬ification.time)); 65 | message_text.push_str(&format!( 66 | "\n **Page**: {}", 67 | &APP_CONF.branding.page_url.as_str() 68 | )); 69 | 70 | // Submit payload to Zulip 71 | let payload = ZulipPayload { 72 | type_: "stream", 73 | to: &zulip.channel, 74 | topic: "Vigil status", 75 | content: &message_text, 76 | }; 77 | 78 | let response = ZULIP_HTTP_CLIENT 79 | .post(zulip.api_url.join("messages").unwrap().as_str()) 80 | .basic_auth(zulip.bot_email.clone(), Some(zulip.bot_api_key.clone())) 81 | .form(&payload) 82 | .send(); 83 | 84 | if let Ok(response_inner) = response { 85 | if response_inner.status().is_success() == true { 86 | return Ok(()); 87 | } else { 88 | warn!( 89 | "could not submit data to zulip: {:?}", 90 | response_inner.text() 91 | ); 92 | } 93 | } 94 | 95 | return Err(true); 96 | } 97 | 98 | Err(false) 99 | } 100 | 101 | fn can_notify(notify: &ConfigNotify, notification: &Notification) -> bool { 102 | if let Some(ref zulip_config) = notify.zulip { 103 | notification.expected(zulip_config.reminders_only) 104 | } else { 105 | false 106 | } 107 | } 108 | 109 | fn name() -> &'static str { 110 | "zulip" 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/prober/mod.rs: -------------------------------------------------------------------------------- 1 | // Vigil 2 | // 3 | // Microservices Status Page 4 | // Copyright: 2018, Valerian Saliou 5 | // License: Mozilla Public License v2.0 (MPL v2.0) 6 | 7 | mod replica; 8 | 9 | pub mod manager; 10 | pub mod mode; 11 | pub mod report; 12 | pub mod states; 13 | pub mod status; 14 | -------------------------------------------------------------------------------- /src/prober/mode.rs: -------------------------------------------------------------------------------- 1 | // Vigil 2 | // 3 | // Microservices Status Page 4 | // Copyright: 2018, Valerian Saliou 5 | // License: Mozilla Public License v2.0 (MPL v2.0) 6 | 7 | #[derive(Serialize, Deserialize, Clone, PartialEq, Debug)] 8 | pub enum Mode { 9 | #[serde(rename = "poll")] 10 | Poll, 11 | 12 | #[serde(rename = "push")] 13 | Push, 14 | 15 | #[serde(rename = "script")] 16 | Script, 17 | 18 | #[serde(rename = "local")] 19 | Local, 20 | } 21 | -------------------------------------------------------------------------------- /src/prober/replica.rs: -------------------------------------------------------------------------------- 1 | // Vigil 2 | // 3 | // Microservices Status Page 4 | // Copyright: 2018, Valerian Saliou 5 | // License: Mozilla Public License v2.0 (MPL v2.0) 6 | 7 | use url::{Host, Url}; 8 | 9 | #[derive(Serialize, Debug, Clone)] 10 | pub enum ReplicaURL { 11 | ICMP(String), 12 | TCP(String, u16), 13 | HTTP(String), 14 | HTTPS(String), 15 | } 16 | 17 | impl ReplicaURL { 18 | pub fn parse_from(raw_url: &str) -> Result { 19 | match Url::parse(raw_url) { 20 | Ok(url) => match url.scheme() { 21 | "icmp" => match (url.host(), url.port(), url.path_segments()) { 22 | (Some(host), None, None) => Ok(ReplicaURL::ICMP(Self::host_string(host))), 23 | _ => Err(()), 24 | }, 25 | "tcp" => match (url.host(), url.port(), url.path_segments()) { 26 | (Some(host), Some(port), None) => { 27 | Ok(ReplicaURL::TCP(Self::host_string(host), port)) 28 | } 29 | _ => Err(()), 30 | }, 31 | "http" => Ok(ReplicaURL::HTTP(url.into())), 32 | "https" => Ok(ReplicaURL::HTTPS(url.into())), 33 | _ => Err(()), 34 | }, 35 | _ => Err(()), 36 | } 37 | } 38 | 39 | fn host_string(host: Host<&str>) -> String { 40 | // Convert internal host value into string. This is especially useful for IPv6 addresses, \ 41 | // which we need returned in '::1' format; as they would otherwise be returned in \ 42 | // '[::1]' format using built-in top-level 'to_string()' method on the 'Host' trait. The \ 43 | // underlying address parser does not accept IPv6 addresses formatted as '[::1]', so \ 44 | // this seemingly overkill processing is obviously needed. 45 | match host { 46 | Host::Domain(domain_value) => domain_value.to_string(), 47 | Host::Ipv4(ipv4_value) => ipv4_value.to_string(), 48 | Host::Ipv6(ipv6_value) => ipv6_value.to_string(), 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/prober/report.rs: -------------------------------------------------------------------------------- 1 | // Vigil 2 | // 3 | // Microservices Status Page 4 | // Copyright: 2018, Valerian Saliou 5 | // License: Mozilla Public License v2.0 (MPL v2.0) 6 | 7 | use std::time::{Duration, SystemTime}; 8 | 9 | use super::states::{ 10 | ServiceStatesProbeNodeRabbitMQ, ServiceStatesProbeNodeReplica, 11 | ServiceStatesProbeNodeReplicaLoad, ServiceStatesProbeNodeReplicaLoadQueue, 12 | ServiceStatesProbeNodeReplicaMetrics, ServiceStatesProbeNodeReplicaMetricsSystem, 13 | ServiceStatesProbeNodeReplicaReport, 14 | }; 15 | use crate::prober::manager::STORE as PROBER_STORE; 16 | use crate::prober::mode::Mode; 17 | use crate::prober::status::Status; 18 | 19 | pub enum HandleLoadError { 20 | InvalidLoad, 21 | WrongMode, 22 | NotFound, 23 | } 24 | 25 | pub enum HandleHealthError { 26 | WrongMode, 27 | NotFound, 28 | } 29 | 30 | pub enum HandleFlushError { 31 | WrongMode, 32 | NotFound, 33 | } 34 | 35 | pub fn handle_load( 36 | probe_id: &str, 37 | node_id: &str, 38 | replica_id: &str, 39 | interval: u64, 40 | load_cpu: f32, 41 | load_ram: f32, 42 | ) -> Result, HandleLoadError> { 43 | debug!( 44 | "load report handle: {}:{}:{}", 45 | probe_id, node_id, replica_id 46 | ); 47 | 48 | // Validate loads 49 | if load_cpu < 0.00 || load_ram < 0.00 { 50 | return Err(HandleLoadError::InvalidLoad); 51 | } 52 | 53 | let mut store = PROBER_STORE.write().unwrap(); 54 | 55 | if let Some(ref mut probe) = store.states.probes.get_mut(probe_id) { 56 | if let Some(ref mut node) = probe.nodes.get_mut(node_id) { 57 | // Mode isnt push? Dont accept report 58 | if node.mode != Mode::Push { 59 | return Err(HandleLoadError::WrongMode); 60 | } 61 | 62 | // Acquire previous replica status + previous queue load status (follow-up values) 63 | let (status, mut metrics, mut load_queue); 64 | 65 | load_queue = ServiceStatesProbeNodeReplicaLoadQueue::default(); 66 | 67 | if let Some(ref replica) = node.replicas.get(replica_id) { 68 | status = replica.status.to_owned(); 69 | metrics = replica.metrics.to_owned(); 70 | 71 | if let Some(ref replica_load) = replica.load { 72 | load_queue = replica_load.queue.clone(); 73 | } 74 | } else { 75 | status = Status::Healthy; 76 | metrics = ServiceStatesProbeNodeReplicaMetrics::default(); 77 | } 78 | 79 | // Assign new system metrics 80 | metrics.system = Some(ServiceStatesProbeNodeReplicaMetricsSystem { 81 | cpu: (load_cpu * 100.0).round() as u16, 82 | ram: (load_ram * 100.0).round() as u16, 83 | }); 84 | 85 | // Bump stored replica 86 | node.replicas.insert( 87 | replica_id.to_string(), 88 | ServiceStatesProbeNodeReplica { 89 | status: status, 90 | url: None, 91 | script: None, 92 | metrics: metrics, 93 | load: Some(ServiceStatesProbeNodeReplicaLoad { 94 | cpu: load_cpu, 95 | ram: load_ram, 96 | queue: load_queue, 97 | }), 98 | report: Some(ServiceStatesProbeNodeReplicaReport { 99 | time: SystemTime::now(), 100 | interval: Duration::from_secs(interval), 101 | }), 102 | }, 103 | ); 104 | 105 | return Ok(node.rabbitmq.clone()); 106 | } 107 | } 108 | 109 | warn!( 110 | "load report could not be stored: {}:{}:{}", 111 | probe_id, node_id, replica_id 112 | ); 113 | 114 | Err(HandleLoadError::NotFound) 115 | } 116 | 117 | pub fn handle_health( 118 | probe_id: &str, 119 | node_id: &str, 120 | replica_id: &str, 121 | interval: u64, 122 | health: &Status, 123 | ) -> Result<(), HandleHealthError> { 124 | debug!( 125 | "health report handle: {}:{}:{}", 126 | probe_id, node_id, replica_id 127 | ); 128 | 129 | let mut store = PROBER_STORE.write().unwrap(); 130 | 131 | if let Some(ref mut probe) = store.states.probes.get_mut(probe_id) { 132 | if let Some(ref mut node) = probe.nodes.get_mut(node_id) { 133 | // Mode isnt local? Dont accept report 134 | if node.mode != Mode::Local { 135 | return Err(HandleHealthError::WrongMode); 136 | } 137 | 138 | // Bump stored replica 139 | node.replicas.insert( 140 | replica_id.to_string(), 141 | ServiceStatesProbeNodeReplica { 142 | status: health.to_owned(), 143 | url: None, 144 | script: None, 145 | metrics: ServiceStatesProbeNodeReplicaMetrics::default(), 146 | load: None, 147 | report: Some(ServiceStatesProbeNodeReplicaReport { 148 | time: SystemTime::now(), 149 | interval: Duration::from_secs(interval), 150 | }), 151 | }, 152 | ); 153 | 154 | return Ok(()); 155 | } 156 | } 157 | 158 | warn!( 159 | "health report could not be stored: {}:{}:{}", 160 | probe_id, node_id, replica_id 161 | ); 162 | 163 | Err(HandleHealthError::NotFound) 164 | } 165 | 166 | pub fn handle_flush( 167 | probe_id: &str, 168 | node_id: &str, 169 | replica_id: &str, 170 | ) -> Result<(), HandleFlushError> { 171 | debug!( 172 | "flush report handle: {}:{}:{}", 173 | probe_id, node_id, replica_id 174 | ); 175 | 176 | let mut store = PROBER_STORE.write().unwrap(); 177 | 178 | if let Some(ref mut probe) = store.states.probes.get_mut(probe_id) { 179 | if let Some(ref mut node) = probe.nodes.get_mut(node_id) { 180 | // Mode isnt push or local? Dont accept report 181 | if node.mode != Mode::Push && node.mode != Mode::Local { 182 | return Err(HandleFlushError::WrongMode); 183 | } 184 | 185 | return if node.replicas.shift_remove(replica_id).is_none() { 186 | Err(HandleFlushError::NotFound) 187 | } else { 188 | Ok(()) 189 | }; 190 | } 191 | } 192 | 193 | warn!( 194 | "load report could not be flushed: {}:{}:{}", 195 | probe_id, node_id, replica_id 196 | ); 197 | 198 | Err(HandleFlushError::NotFound) 199 | } 200 | -------------------------------------------------------------------------------- /src/prober/states.rs: -------------------------------------------------------------------------------- 1 | // Vigil 2 | // 3 | // Microservices Status Page 4 | // Copyright: 2018, Valerian Saliou 5 | // License: Mozilla Public License v2.0 (MPL v2.0) 6 | 7 | use std::time::{Duration, SystemTime}; 8 | 9 | use indexmap::IndexMap; 10 | 11 | use super::mode::Mode; 12 | use super::replica::ReplicaURL; 13 | use super::status::Status; 14 | use crate::config::{config::ConfigProbeServiceNodeHTTPMethod, regex::Regex}; 15 | 16 | #[derive(Serialize)] 17 | pub struct ServiceStates { 18 | pub status: Status, 19 | pub date: Option, 20 | pub probes: IndexMap, 21 | pub notifier: ServiceStatesNotifier, 22 | } 23 | 24 | #[derive(Serialize)] 25 | pub struct ServiceStatesProbe { 26 | pub id: String, 27 | pub label: String, 28 | pub status: Status, 29 | pub nodes: IndexMap, 30 | } 31 | 32 | #[derive(Serialize)] 33 | pub struct ServiceStatesProbeNode { 34 | pub status: Status, 35 | pub label: String, 36 | pub mode: Mode, 37 | pub replicas: IndexMap, 38 | #[serde(default)] 39 | #[serde(with = "http_serde::header_map")] 40 | pub http_headers: http::HeaderMap, 41 | pub http_method: Option, 42 | pub http_body: Option, 43 | pub http_body_healthy_match: Option, 44 | pub reveal_replica_name: bool, 45 | pub link_url: Option, 46 | pub link_label: Option, 47 | pub rabbitmq: Option, 48 | } 49 | 50 | #[derive(Serialize)] 51 | pub struct ServiceStatesProbeNodeReplica { 52 | pub status: Status, 53 | pub url: Option, 54 | pub script: Option, 55 | pub metrics: ServiceStatesProbeNodeReplicaMetrics, 56 | pub load: Option, 57 | pub report: Option, 58 | } 59 | 60 | #[derive(Serialize, Clone)] 61 | pub struct ServiceStatesProbeNodeRabbitMQ { 62 | pub queue: String, 63 | pub queue_nack_healthy_below: Option, 64 | pub queue_nack_dead_above: Option, 65 | } 66 | 67 | #[derive(Serialize, Clone, Default)] 68 | pub struct ServiceStatesProbeNodeReplicaMetrics { 69 | pub latency: Option, 70 | pub system: Option, 71 | pub rabbitmq: Option, 72 | } 73 | 74 | #[derive(Serialize, Clone)] 75 | pub struct ServiceStatesProbeNodeReplicaMetricsSystem { 76 | pub cpu: u16, 77 | pub ram: u16, 78 | } 79 | 80 | #[derive(Serialize, Clone, Default)] 81 | pub struct ServiceStatesProbeNodeReplicaMetricsRabbitMQ { 82 | pub queue_ready: u32, 83 | pub queue_nack: u32, 84 | } 85 | 86 | #[derive(Serialize)] 87 | pub struct ServiceStatesProbeNodeReplicaLoad { 88 | pub cpu: f32, 89 | pub ram: f32, 90 | pub queue: ServiceStatesProbeNodeReplicaLoadQueue, 91 | } 92 | 93 | #[derive(Serialize, Clone, Default)] 94 | pub struct ServiceStatesProbeNodeReplicaLoadQueue { 95 | pub loaded: bool, 96 | pub stalled: bool, 97 | } 98 | 99 | #[derive(Serialize)] 100 | pub struct ServiceStatesProbeNodeReplicaReport { 101 | pub time: SystemTime, 102 | pub interval: Duration, 103 | } 104 | 105 | #[derive(Serialize)] 106 | pub struct ServiceStatesNotifier { 107 | pub reminder_backoff_counter: u16, 108 | pub reminder_ignore_until: Option, 109 | } 110 | -------------------------------------------------------------------------------- /src/prober/status.rs: -------------------------------------------------------------------------------- 1 | // Vigil 2 | // 3 | // Microservices Status Page 4 | // Copyright: 2018, Valerian Saliou 5 | // License: Mozilla Public License v2.0 (MPL v2.0) 6 | 7 | #[derive(Serialize, Deserialize, Clone, PartialEq, Debug)] 8 | pub enum Status { 9 | #[serde(rename = "healthy")] 10 | Healthy, 11 | 12 | #[serde(rename = "sick")] 13 | Sick, 14 | 15 | #[serde(rename = "dead")] 16 | Dead, 17 | } 18 | 19 | impl Status { 20 | pub fn as_str(&self) -> &'static str { 21 | match self { 22 | &Status::Healthy => "healthy", 23 | &Status::Sick => "sick", 24 | &Status::Dead => "dead", 25 | } 26 | } 27 | 28 | pub fn as_icon(&self) -> &'static str { 29 | match self { 30 | &Status::Dead => "\u{274c}", 31 | &Status::Sick => "\u{26a0}", 32 | &Status::Healthy => "\u{2705}", 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/responder/announcements.rs: -------------------------------------------------------------------------------- 1 | // Vigil 2 | // 3 | // Microservices Status Page 4 | // Copyright: 2022, Valerian Saliou 5 | // License: Mozilla Public License v2.0 (MPL v2.0) 6 | 7 | use std::sync::Arc; 8 | use std::sync::RwLock; 9 | use time; 10 | use time::format_description::FormatItem; 11 | 12 | lazy_static! { 13 | pub static ref STORE: Arc> = Arc::new(RwLock::new(Store { 14 | announcements: Vec::new(), 15 | })); 16 | pub static ref DATE_NOW_FORMATTER: Vec> = time::format_description::parse( 17 | "[day padding:none] [month repr:short] [year], \ 18 | [hour]:[minute]:[second] UTC[offset_hour sign:mandatory]:[offset_minute]" 19 | ) 20 | .expect("invalid time format"); 21 | } 22 | 23 | pub struct Store { 24 | pub announcements: Vec, 25 | } 26 | 27 | #[derive(Serialize)] 28 | pub struct Announcement { 29 | pub id: String, 30 | pub title: String, 31 | pub text: String, 32 | pub date: Option, 33 | } 34 | -------------------------------------------------------------------------------- /src/responder/context.rs: -------------------------------------------------------------------------------- 1 | // Vigil 2 | // 3 | // Microservices Status Page 4 | // Copyright: 2018, Valerian Saliou 5 | // License: Mozilla Public License v2.0 (MPL v2.0) 6 | 7 | use time; 8 | 9 | use url_serde::SerdeUrl; 10 | 11 | use super::announcements::Announcement; 12 | use crate::prober::states::ServiceStates; 13 | use crate::APP_CONF; 14 | 15 | const LOGO_EXTENSION_SPLIT_SPAN: usize = 4; 16 | 17 | lazy_static! { 18 | pub static ref INDEX_CONFIG: IndexContextConfig = IndexContextConfig { 19 | runtime_version: env!("CARGO_PKG_VERSION").to_string(), 20 | page_title: APP_CONF.branding.page_title.to_owned(), 21 | company_name: APP_CONF.branding.company_name.to_owned(), 22 | icon_color: APP_CONF.branding.icon_color.to_owned(), 23 | icon_url: APP_CONF.branding.icon_url.to_owned(), 24 | icon_mime: ImageMime::guess_from(APP_CONF.branding.icon_url.as_str()), 25 | logo_color: APP_CONF.branding.logo_color.to_owned(), 26 | logo_url: APP_CONF.branding.logo_url.to_owned(), 27 | website_url: APP_CONF.branding.website_url.to_owned(), 28 | support_url: APP_CONF.branding.support_url.to_owned(), 29 | custom_html: APP_CONF.branding.custom_html.to_owned(), 30 | }; 31 | pub static ref INDEX_ENVIRONMENT: IndexContextEnvironment = IndexContextEnvironment::default(); 32 | } 33 | 34 | #[derive(Serialize)] 35 | pub enum ImageMime { 36 | #[serde(rename = "image/png")] 37 | ImagePNG, 38 | 39 | #[serde(rename = "image/jpeg")] 40 | ImageJPEG, 41 | 42 | #[serde(rename = "image/gif")] 43 | ImageGIF, 44 | 45 | #[serde(rename = "image/svg")] 46 | ImageSVG, 47 | } 48 | 49 | impl ImageMime { 50 | fn guess_from(logo_url: &str) -> ImageMime { 51 | if logo_url.len() > LOGO_EXTENSION_SPLIT_SPAN { 52 | let (_, logo_url_extension) = 53 | logo_url.split_at(logo_url.len() - LOGO_EXTENSION_SPLIT_SPAN); 54 | 55 | match logo_url_extension { 56 | ".svg" => ImageMime::ImageSVG, 57 | ".jpg" => ImageMime::ImageJPEG, 58 | ".gif" => ImageMime::ImageGIF, 59 | _ => ImageMime::ImagePNG, 60 | } 61 | } else { 62 | ImageMime::ImagePNG 63 | } 64 | } 65 | } 66 | 67 | impl Default for IndexContextEnvironment { 68 | fn default() -> Self { 69 | IndexContextEnvironment { 70 | year: time::OffsetDateTime::now_utc().year() as u16, 71 | } 72 | } 73 | } 74 | 75 | #[derive(Serialize)] 76 | pub struct IndexContext<'a, 'b> { 77 | pub states: &'a ServiceStates, 78 | pub announcements: &'a Vec, 79 | pub environment: &'a IndexContextEnvironment, 80 | pub config: &'b IndexContextConfig, 81 | } 82 | 83 | #[derive(Serialize)] 84 | pub struct IndexContextConfig { 85 | pub runtime_version: String, 86 | pub page_title: String, 87 | pub company_name: String, 88 | pub icon_color: String, 89 | pub icon_url: SerdeUrl, 90 | pub icon_mime: ImageMime, 91 | pub logo_color: String, 92 | pub logo_url: SerdeUrl, 93 | pub website_url: SerdeUrl, 94 | pub support_url: SerdeUrl, 95 | pub custom_html: Option, 96 | } 97 | 98 | #[derive(Serialize)] 99 | pub struct IndexContextEnvironment { 100 | pub year: u16, 101 | } 102 | -------------------------------------------------------------------------------- /src/responder/manager.rs: -------------------------------------------------------------------------------- 1 | // Vigil 2 | // 3 | // Microservices Status Page 4 | // Copyright: 2021, Valerian Saliou 5 | // License: Mozilla Public License v2.0 (MPL v2.0) 6 | 7 | use actix_web::{ 8 | dev::ServiceRequest, 9 | guard, 10 | middleware::{self, TrailingSlash}, 11 | rt, web, App, Error as ActixError, HttpServer, 12 | }; 13 | use actix_web_httpauth::{ 14 | extractors::{ 15 | basic::{BasicAuth, Config as ConfigAuth}, 16 | AuthenticationError, 17 | }, 18 | middleware::HttpAuthentication, 19 | }; 20 | use tera::Tera; 21 | 22 | use super::routes; 23 | use crate::APP_CONF; 24 | 25 | pub fn run() { 26 | let runtime = rt::System::new(); 27 | 28 | // Prepare templating engine 29 | let templates: String = APP_CONF 30 | .assets 31 | .path 32 | .canonicalize() 33 | .unwrap() 34 | .join("templates") 35 | .join("*") 36 | .to_str() 37 | .unwrap() 38 | .into(); 39 | 40 | let tera = Tera::new(&templates).unwrap(); 41 | 42 | // Prepare authentication middlewares 43 | let (middleware_reporter_auth, middleware_manager_auth) = ( 44 | HttpAuthentication::basic(authenticate_reporter), 45 | HttpAuthentication::basic(authenticate_manager), 46 | ); 47 | 48 | // Start the HTTP server 49 | let server = HttpServer::new(move || { 50 | App::new() 51 | .app_data(web::Data::new(tera.clone())) 52 | .wrap(middleware::NormalizePath::new(TrailingSlash::Trim)) 53 | .service(routes::assets_javascripts) 54 | .service(routes::assets_stylesheets) 55 | .service(routes::assets_images) 56 | .service(routes::assets_fonts) 57 | .service(routes::badge) 58 | .service(routes::status_text) 59 | .service(routes::robots) 60 | .service(routes::index) 61 | .app_data(ConfigAuth::default().realm("Reporter Token")) 62 | .service( 63 | web::resource("/reporter/{probe_id}/{node_id}") 64 | .wrap(middleware_reporter_auth.clone()) 65 | .guard(guard::Post()) 66 | .to(routes::reporter_report), 67 | ) 68 | .service( 69 | web::resource("/reporter/{probe_id}/{node_id}/{replica_id}") 70 | .wrap(middleware_reporter_auth.clone()) 71 | .guard(guard::Delete()) 72 | .to(routes::reporter_flush), 73 | ) 74 | .service( 75 | web::resource("/manager/announcements") 76 | .wrap(middleware_manager_auth.clone()) 77 | .guard(guard::Get()) 78 | .to(routes::manager_announcements), 79 | ) 80 | .service( 81 | web::resource("/manager/announcement") 82 | .wrap(middleware_manager_auth.clone()) 83 | .guard(guard::Post()) 84 | .to(routes::manager_announcement_insert), 85 | ) 86 | .service( 87 | web::resource("/manager/announcement/{announcement_id}") 88 | .wrap(middleware_manager_auth.clone()) 89 | .guard(guard::Delete()) 90 | .to(routes::manager_announcement_retract), 91 | ) 92 | .service( 93 | web::resource("/manager/prober/alerts") 94 | .wrap(middleware_manager_auth.clone()) 95 | .guard(guard::Get()) 96 | .to(routes::manager_prober_alerts), 97 | ) 98 | .service( 99 | web::resource("/manager/prober/alerts/ignored") 100 | .wrap(middleware_manager_auth.clone()) 101 | .guard(guard::Get()) 102 | .to(routes::manager_prober_alerts_ignored_resolve), 103 | ) 104 | .service( 105 | web::resource("/manager/prober/alerts/ignored") 106 | .wrap(middleware_manager_auth.clone()) 107 | .guard(guard::Put()) 108 | .to(routes::manager_prober_alerts_ignored_update), 109 | ) 110 | }) 111 | .workers(APP_CONF.server.workers) 112 | .bind(APP_CONF.server.inet) 113 | .unwrap() 114 | .run(); 115 | 116 | runtime.block_on(server).unwrap() 117 | } 118 | 119 | fn authenticate( 120 | request: ServiceRequest, 121 | credentials: BasicAuth, 122 | token: &str, 123 | ) -> Result { 124 | let password = if let Some(password) = credentials.password() { 125 | &*password 126 | } else { 127 | "" 128 | }; 129 | 130 | if password == token { 131 | Ok(request) 132 | } else { 133 | let mut error = AuthenticationError::from( 134 | request 135 | .app_data::() 136 | .map(|data| data.clone()) 137 | .unwrap_or_else(ConfigAuth::default), 138 | ); 139 | 140 | *error.status_code_mut() = actix_web::http::StatusCode::FORBIDDEN; 141 | 142 | Err((error.into(), request)) 143 | } 144 | } 145 | 146 | async fn authenticate_reporter( 147 | request: ServiceRequest, 148 | credentials: BasicAuth, 149 | ) -> Result { 150 | authenticate(request, credentials, &APP_CONF.server.reporter_token) 151 | } 152 | 153 | async fn authenticate_manager( 154 | request: ServiceRequest, 155 | credentials: BasicAuth, 156 | ) -> Result { 157 | authenticate(request, credentials, &APP_CONF.server.manager_token) 158 | } 159 | -------------------------------------------------------------------------------- /src/responder/mod.rs: -------------------------------------------------------------------------------- 1 | // Vigil 2 | // 3 | // Microservices Status Page 4 | // Copyright: 2018, Valerian Saliou 5 | // License: Mozilla Public License v2.0 (MPL v2.0) 6 | 7 | mod announcements; 8 | mod context; 9 | mod payload; 10 | mod routes; 11 | 12 | pub mod manager; 13 | -------------------------------------------------------------------------------- /src/responder/payload.rs: -------------------------------------------------------------------------------- 1 | // Vigil 2 | // 3 | // Microservices Status Page 4 | // Copyright: 2021, Valerian Saliou 5 | // License: Mozilla Public License v2.0 (MPL v2.0) 6 | 7 | use crate::prober::status::Status as HealthStatus; 8 | 9 | #[derive(Deserialize)] 10 | pub struct ReporterRequestPayload { 11 | pub replica: String, 12 | pub interval: u64, 13 | pub health: Option, 14 | pub load: Option, 15 | } 16 | 17 | #[derive(Deserialize)] 18 | pub struct ReporterRequestPayloadLoad { 19 | pub cpu: f32, 20 | pub ram: f32, 21 | } 22 | 23 | #[derive(Deserialize)] 24 | pub struct ManagerAnnouncementInsertRequestPayload { 25 | pub title: String, 26 | pub text: String, 27 | } 28 | 29 | #[derive(Deserialize)] 30 | pub struct ManagerProberAlertsIgnoredResolveRequestPayload { 31 | pub reminders_seconds: Option, 32 | } 33 | 34 | #[derive(Serialize)] 35 | pub struct ManagerAnnouncementsResponsePayload { 36 | pub id: String, 37 | pub title: String, 38 | } 39 | 40 | #[derive(Serialize)] 41 | pub struct ManagerAnnouncementInsertResponsePayload { 42 | pub id: String, 43 | } 44 | 45 | #[derive(Serialize, Default)] 46 | pub struct ManagerProberAlertsResponsePayload { 47 | pub dead: Vec, 48 | pub sick: Vec, 49 | } 50 | 51 | #[derive(Serialize)] 52 | pub struct ManagerProberAlertsResponsePayloadEntry { 53 | pub probe: String, 54 | pub node: String, 55 | pub replica: String, 56 | } 57 | 58 | #[derive(Serialize)] 59 | pub struct ManagerProberAlertsIgnoredResolveResponsePayload { 60 | pub reminders_seconds: Option, 61 | } 62 | -------------------------------------------------------------------------------- /src/responder/routes.rs: -------------------------------------------------------------------------------- 1 | // Vigil 2 | // 3 | // Microservices Status Page 4 | // Copyright: 2021, Valerian Saliou 5 | // License: Mozilla Public License v2.0 (MPL v2.0) 6 | 7 | use actix_files::NamedFile; 8 | use actix_web::{get, web, web::Data, web::Json, HttpResponse}; 9 | use std::time::{Duration, SystemTime}; 10 | use tera::Tera; 11 | use time; 12 | use uuid::Uuid; 13 | 14 | use super::announcements::{ 15 | Announcement, DATE_NOW_FORMATTER as ANNOUNCEMENTS_DATE_NOW_FORMATTER, 16 | STORE as ANNOUNCEMENTS_STORE, 17 | }; 18 | use super::context::{IndexContext, INDEX_CONFIG, INDEX_ENVIRONMENT}; 19 | use super::payload::{ 20 | ManagerAnnouncementInsertRequestPayload, ManagerAnnouncementInsertResponsePayload, 21 | ManagerAnnouncementsResponsePayload, ManagerProberAlertsIgnoredResolveRequestPayload, 22 | ManagerProberAlertsIgnoredResolveResponsePayload, ManagerProberAlertsResponsePayload, 23 | ManagerProberAlertsResponsePayloadEntry, ReporterRequestPayload, 24 | }; 25 | use crate::prober::manager::{run_dispatch_plugins, STORE as PROBER_STORE}; 26 | use crate::prober::report::{ 27 | handle_flush as handle_flush_report, handle_health as handle_health_report, 28 | handle_load as handle_load_report, HandleFlushError, HandleHealthError, HandleLoadError, 29 | }; 30 | use crate::prober::status::Status; 31 | use crate::APP_CONF; 32 | 33 | #[get("/")] 34 | async fn index(tera: Data) -> HttpResponse { 35 | // Notice acquire lock in a block to release it ASAP (ie. before template renders) 36 | let context = { 37 | IndexContext { 38 | states: &PROBER_STORE.read().unwrap().states, 39 | announcements: &ANNOUNCEMENTS_STORE.read().unwrap().announcements, 40 | environment: &*INDEX_ENVIRONMENT, 41 | config: &*INDEX_CONFIG, 42 | } 43 | }; 44 | let render = tera.render( 45 | "index.tera", 46 | &tera::Context::from_serialize(context).unwrap(), 47 | ); 48 | if let Ok(s) = render { 49 | HttpResponse::Ok().content_type("text/html").body(s) 50 | } else { 51 | HttpResponse::InternalServerError().body(format!("Template Error {:?}", render)) 52 | } 53 | } 54 | 55 | #[get("/robots.txt")] 56 | async fn robots() -> Option { 57 | NamedFile::open(APP_CONF.assets.path.join("public").join("robots.txt")).ok() 58 | } 59 | 60 | #[get("/status/text")] 61 | async fn status_text() -> &'static str { 62 | &PROBER_STORE.read().unwrap().states.status.as_str() 63 | } 64 | 65 | #[get("/badge/{kind}")] 66 | async fn badge(kind: web::Path) -> Option { 67 | // Notice acquire lock in a block to release it ASAP (ie. before OS access to file) 68 | let status = { &PROBER_STORE.read().unwrap().states.status.as_str() }; 69 | 70 | if let Ok(badge_file) = NamedFile::open( 71 | APP_CONF 72 | .assets 73 | .path 74 | .join("images") 75 | .join("badges") 76 | .join(format!("{}-{}-default.svg", kind, status)), 77 | ) { 78 | // Return badge file without 'Last-Modified' HTTP header, which would otherwise hold the \ 79 | // date the actual badge image file was last modified, which is not what we want there, \ 80 | // as it would make browsers believe they can use a previous cache they hold, on a \ 81 | // badge image that can be for a different status. 82 | Some( 83 | badge_file 84 | .disable_content_disposition() 85 | .use_last_modified(false), 86 | ) 87 | } else { 88 | None 89 | } 90 | } 91 | 92 | #[get("/assets/fonts/{folder}/{file}")] 93 | async fn assets_fonts(path: web::Path<(String, String)>) -> Option { 94 | // Read path information 95 | let info = path.into_inner(); 96 | 97 | let (folder, file) = (info.0, info.1); 98 | 99 | NamedFile::open(APP_CONF.assets.path.join("fonts").join(folder).join(file)).ok() 100 | } 101 | 102 | #[get("/assets/images/{folder}/{file}")] 103 | async fn assets_images(path: web::Path<(String, String)>) -> Option { 104 | // Read path information 105 | let info = path.into_inner(); 106 | 107 | let (folder, file) = (info.0, info.1); 108 | 109 | NamedFile::open(APP_CONF.assets.path.join("images").join(folder).join(file)).ok() 110 | } 111 | 112 | #[get("/assets/stylesheets/{file}")] 113 | async fn assets_stylesheets(file: web::Path) -> Option { 114 | NamedFile::open( 115 | APP_CONF 116 | .assets 117 | .path 118 | .join("stylesheets") 119 | .join(file.into_inner()), 120 | ) 121 | .ok() 122 | } 123 | 124 | #[get("/assets/javascripts/{file}")] 125 | async fn assets_javascripts(file: web::Path) -> Option { 126 | let file = file.into_inner(); 127 | NamedFile::open(APP_CONF.assets.path.join("javascripts").join(file)).ok() 128 | } 129 | 130 | // Notice: reporter report route is managed in manager due to authentication needs 131 | pub async fn reporter_report( 132 | path: web::Path<(String, String)>, 133 | data: Json, 134 | ) -> HttpResponse { 135 | // Read path information 136 | let info = path.into_inner(); 137 | 138 | let (probe_id, node_id) = (info.0, info.1); 139 | 140 | debug!("reporter report: {}:{}", probe_id, node_id); 141 | 142 | // Route report to handler (depending on its contents) 143 | if let Some(ref load) = data.load { 144 | // Load reports should come for 'push' nodes only 145 | match handle_load_report( 146 | &probe_id, 147 | &node_id, 148 | &data.replica, 149 | data.interval, 150 | load.cpu, 151 | load.ram, 152 | ) { 153 | Ok(forward) => { 154 | // Trigger a plugins check 155 | run_dispatch_plugins(&probe_id, &node_id, forward); 156 | 157 | HttpResponse::Ok().finish() 158 | } 159 | Err(HandleLoadError::InvalidLoad) => HttpResponse::BadRequest().finish(), 160 | Err(HandleLoadError::WrongMode) => HttpResponse::PreconditionFailed().finish(), 161 | Err(HandleLoadError::NotFound) => HttpResponse::NotFound().finish(), 162 | } 163 | } else if let Some(ref health) = data.health { 164 | // Health reports should come for 'local' nodes only 165 | match handle_health_report(&probe_id, &node_id, &data.replica, data.interval, health) { 166 | Ok(_) => HttpResponse::Ok().finish(), 167 | Err(HandleHealthError::WrongMode) => HttpResponse::PreconditionFailed().finish(), 168 | Err(HandleHealthError::NotFound) => HttpResponse::NotFound().finish(), 169 | } 170 | } else { 171 | // Report contents is invalid 172 | HttpResponse::BadRequest().finish() 173 | } 174 | } 175 | 176 | // Notice: reporter flush route is managed in manager due to authentication needs 177 | pub async fn reporter_flush(path: web::Path<(String, String, String)>) -> HttpResponse { 178 | // Read path information 179 | let info = path.into_inner(); 180 | 181 | let (probe_id, node_id, replica_id) = (info.0, info.1, info.2); 182 | 183 | debug!("reporter flush: {}:{}:{}", probe_id, node_id, replica_id); 184 | 185 | // Flush reports should come for 'push' and 'local' nodes only 186 | match handle_flush_report(&probe_id, &node_id, &replica_id) { 187 | Ok(()) => HttpResponse::Ok().finish(), 188 | Err(HandleFlushError::WrongMode) => HttpResponse::PreconditionFailed().finish(), 189 | Err(HandleFlushError::NotFound) => HttpResponse::NotFound().finish(), 190 | } 191 | } 192 | 193 | // Notice: manager announcements route is managed in manager due to authentication needs 194 | pub async fn manager_announcements() -> HttpResponse { 195 | // List all announcements in store 196 | HttpResponse::Ok().json( 197 | ANNOUNCEMENTS_STORE 198 | .read() 199 | .unwrap() 200 | .announcements 201 | .iter() 202 | .map(|announcement| ManagerAnnouncementsResponsePayload { 203 | id: announcement.id.to_owned(), 204 | title: announcement.title.to_owned(), 205 | }) 206 | .collect::>(), 207 | ) 208 | } 209 | 210 | // Notice: manager announcement insert route is managed in manager due to authentication needs 211 | pub async fn manager_announcement_insert( 212 | data: Json, 213 | ) -> HttpResponse { 214 | // Validate data 215 | if data.title.len() > 0 && data.text.len() > 0 { 216 | // Generate unique identifier and insert in announcements 217 | let id = Uuid::new_v4().hyphenated().to_string(); 218 | 219 | let mut store = ANNOUNCEMENTS_STORE.write().unwrap(); 220 | 221 | store.announcements.push(Announcement { 222 | id: id.to_owned(), 223 | title: data.title.to_owned(), 224 | text: data.text.to_owned(), 225 | 226 | date: Some( 227 | time::OffsetDateTime::now_utc() 228 | .format(&ANNOUNCEMENTS_DATE_NOW_FORMATTER) 229 | .unwrap_or("?".to_string()), 230 | ), 231 | }); 232 | 233 | HttpResponse::Ok().json(ManagerAnnouncementInsertResponsePayload { id: id }) 234 | } else { 235 | // Announcement data is invalid 236 | HttpResponse::BadRequest().finish() 237 | } 238 | } 239 | 240 | // Notice: manager announcement retract route is managed in manager due to authentication needs 241 | pub async fn manager_announcement_retract(announcement_id: web::Path) -> HttpResponse { 242 | let announcement_id = announcement_id.into_inner(); 243 | let mut store = ANNOUNCEMENTS_STORE.write().unwrap(); 244 | 245 | // Find announcement index (if it exists) 246 | let announcement_index = store 247 | .announcements 248 | .iter() 249 | .position(|announcement| announcement.id == announcement_id); 250 | 251 | if let Some(announcement_index) = announcement_index { 252 | // Remove target announcement 253 | store.announcements.remove(announcement_index); 254 | 255 | HttpResponse::Ok().finish() 256 | } else { 257 | HttpResponse::NotFound().finish() 258 | } 259 | } 260 | 261 | // Notice: manager prober alerts route is managed in manager due to authentication needs 262 | pub async fn manager_prober_alerts() -> HttpResponse { 263 | let mut alerts = ManagerProberAlertsResponsePayload::default(); 264 | 265 | // Classify probes with a non-healthy status 266 | let probes = &PROBER_STORE.read().unwrap().states.probes; 267 | 268 | for (probe_id, probe) in probes.iter() { 269 | for (node_id, node) in probe.nodes.iter() { 270 | for (replica_id, replica) in node.replicas.iter() { 271 | // Replica is either sick or dead, append to alerts 272 | if replica.status == Status::Sick || replica.status == Status::Dead { 273 | let alert_entry = ManagerProberAlertsResponsePayloadEntry { 274 | probe: probe_id.to_owned(), 275 | node: node_id.to_owned(), 276 | replica: replica_id.to_owned(), 277 | }; 278 | 279 | match replica.status { 280 | Status::Sick => alerts.sick.push(alert_entry), 281 | Status::Dead => alerts.dead.push(alert_entry), 282 | _ => {} 283 | } 284 | } 285 | } 286 | } 287 | } 288 | 289 | HttpResponse::Ok().json(alerts) 290 | } 291 | 292 | // Notice: manager prober alerts ignored resolve route is managed in manager due to authentication \ 293 | // needs 294 | pub async fn manager_prober_alerts_ignored_resolve() -> HttpResponse { 295 | let states = &PROBER_STORE.read().unwrap().states; 296 | 297 | // Calculate remaining ignore reminders seconds (if any set or if time is still left) 298 | let reminders_seconds = states 299 | .notifier 300 | .reminder_ignore_until 301 | .and_then(|reminder_ignore_until| { 302 | reminder_ignore_until.duration_since(SystemTime::now()).ok() 303 | }) 304 | .map(|reminder_ignore_duration_since| reminder_ignore_duration_since.as_secs() as u16); 305 | 306 | HttpResponse::Ok().json(ManagerProberAlertsIgnoredResolveResponsePayload { 307 | reminders_seconds: reminders_seconds, 308 | }) 309 | } 310 | 311 | // Notice: manager prober alerts ignored update route is managed in manager due to authentication \ 312 | // needs 313 | pub async fn manager_prober_alerts_ignored_update( 314 | data: Json, 315 | ) -> HttpResponse { 316 | let mut store = PROBER_STORE.write().unwrap(); 317 | 318 | // Assign reminder ignore intil date (re-map from seconds to date time if set) 319 | store.states.notifier.reminder_ignore_until = data 320 | .reminders_seconds 321 | .map(|reminders_seconds| SystemTime::now() + Duration::from_secs(reminders_seconds as _)); 322 | 323 | HttpResponse::Ok().finish() 324 | } 325 | --------------------------------------------------------------------------------