From c720d78b3f3c36423d8a042c6ba4e4a8a78b9b9d Mon Sep 17 00:00:00 2001 From: David Stevens Date: Sun, 21 Jul 2024 10:38:26 +0200 Subject: [PATCH] feat: Too many for title (#16) * chore: Fix clippy warnings In order to clean the code up a bit, I fixed everything that `cargo clippy` complained about. The warnings were of types: - redundant field names in struct initialization - unused imports - `&String` being used instead of `&str`, fixing this saves a copy - unnecessary `return` statements - use of `or_insert_with` instead of `or_default` for constructing default values - some references which are immediately dereferenced by the compiler - unneeded late initializations before `match` expressions - single-character string constants being used instead of a char - module inception in `test_utils::test_utils`. I renamed the inner module to `probe_test_utils` to facilitate other types of test util modules being added to that parent module. Hopefully this should make the code a little bit more idiomatic, although I'm far from a Rust expert so take the changes with a grain of salt. * feat: Add root span to more deeply instrument stories * chore: Move otel setup to otel module * fix: Add nested spans * feat: Switch to tokio-tracing with tracing_opentelemetry * feat: Add support for specifying other files through CLI * chore: Update RAM usage in README The changes thus far have increased RAM usage from 8 to around 14 MB on my machine when running the default `prozilla.yml`. Hopefully this isn't too bad, if necessary I could probably profile where the memory usage is coming from but I'm fairly certain it's from OpenTelemetry since there's a proper pipeline going on now. * chore: move otel resource init to module root * feat: Otel metrics, pick exporters with env Adds some basic OpenTelemetry metrics support using tracing_opentelemetry as well as support for choosing between stdout and otlp exporters for both traces and metrics using the standard OpenTelemetry environment variable conventions. Both traces and metrics are disabled if the corresponding environment variables are not set. * feat: Support environment variable substitution * feat: Add trace ID to alerts * Revert "feat: Switch to tokio-tracing with tracing_opentelemetry" This reverts commit 3bbb42dac7828f5466d7c91c11630a2bca4dbfc6. * fix: Fix broken test * chore: Run cargo fmt on entire project * chore: Add .git-blame-ignore-revs to hide chore commits * feat: Add 'Matches' expectation which tests a regular expression * feat: Set up OTLP HTTP, remove tracing-opentelemetry * feat: Trim variables to support whitespace in substitutions * chore: Refactor metrics * fix: Add 0 to error metrics on success This is done so that the error metrics are initialized for that particular set of labels to 0. By doing this, any backend that receives the data will receive a time series that starts at 0 instead of eventually receiving a new time series that starts at 1 once the first error arrives. In the latter case, a promql query for `rate` e.g. would fail to detect the transition from 0 -> 1 as the transition is actually from non-existant to 1. * feat: Add support for Slack webhooks * feat: Enable use of env vars globally in config The previous implementation hooked in to the same substitution logic as for step outputs and generated values. This had the consequence that it would only apply during probes, and can not be used for other parts of the configuration file such as webhook URLs. This commit moves the enivronment variable substitution to the configuration loading step of Prodzilla's initialization in order to allow environment variables to be used anywhere in the configuration. * feat: Add basic dockerfile * feat: Add GHA to build and publish docker image * fix: Support multiplatform build through QEMU * chore: Track Cargo.lock It seems that the guidance used to be that Cargo.lock should be tracked for binaries (like Prodzilla) but not for libraries. It should have been tracked from the start according to this guidance. Updated guidance is available at https://blog.rust-lang.org/2023/08/29/committing-lockfiles.html , where the new recommendation is to simply do what is best for the project but defualt to tracking Cargo.lock. For Prodzilla, tracking Cargo.lock helps make the Docker and binary builds reproducible so I believe that it is beneficial to this project. Further reference: - https://doc.rust-lang.org/nightly/cargo/faq.html#why-have-cargolock-in-version-control - https://github.com/rust-lang/cargo/issues/8728 * fix: Add more build dependencies * fix: Set prodzilla as docker entrypoint * fix: Fix dockerfile * fix: Make whitespace around env vars optional * fix: Drastically simplify dockerfile * chore: Add test to catch earlier bug with whitespace in env vars * chore: Document slack_webhook parameter * docs: Update feature roadmap * chore: revert accidental change to TOC format * docs: Document new features * fix: Fix regression with missing parent span IDs Previously, opentelemetry tracing wouldn't be initialized at all if an exporter isn't configured. This leads to parent trace IDs not being available. This commit re-introduces the earlier Prodzilla behvaiour of including a parent trace ID which is propagated in outgoing web requests even if the root spans aren't exported. * fix: Vendor openssl to enable cross-compilation * feat: Add release workflow * chore: Only build docker image on tagged releases * chore: Update package version to v0.0.3 * feat: Add musl build targets for Alpine Linux * fix: Improve error message on missing config file * chore: add missing space in readme * fix: Correct error metrics for probes * feat: Include error message in webhook alerts * feat!: Remove slack_webhook config parameter and route based on url instead * chore: Update package version to v0.0.4 * chore: Cleanup error unwrapping * feat: Include status code and body in alerts, style Slack * feat: Add support for marking steps as sensitive This leads to logs and alerts being redacted so that the sensitive response bodies aren't included. * fix: Report span status on probe error * chore: Remove unused import * fix: escape newlines in logged bodies * feat: Make request timeout configurable * chore: Use tidier Option unwrapping * fix: Remove openssl vendoring, binary builds OpenSSL vendoring was causing issues with builds on Windows so I think for now it's probably best to just remove it and the binary builds that required it. This means that releases will no longer include executables, but the Docker images are still published and the source code can be compiled for the target platform by the user. It might be worth revisiting binary builds in the future with static linking, but I don't have the time (or the use-case at $WORK) to do that at the moment and would like to avoid this being a blocker. --- .dockerignore | 32 + .git-blame-ignore-revs | 2 + .github/workflows/docker.yaml | 66 + .github/workflows/release.yaml | 20 + .gitignore | 4 - Cargo.lock | 2488 ++++++++++++++++++++++++++++++++ Cargo.toml | 19 +- Dockerfile | 38 + README.md | 108 +- src/alerts/mod.rs | 2 +- src/alerts/model.rs | 31 +- src/alerts/outbound_webhook.rs | 218 ++- src/app_state.rs | 25 +- src/config.rs | 45 +- src/errors.rs | 34 + src/main.rs | 25 +- src/otel/metrics.rs | 92 ++ src/otel/mod.rs | 61 + src/otel/tracing.rs | 47 + src/probe/expectations.rs | 145 +- src/probe/http_probe.rs | 157 +- src/probe/mod.rs | 4 +- src/probe/model.rs | 28 +- src/probe/probe_logic.rs | 314 ++-- src/probe/schedule.rs | 12 +- src/probe/variables.rs | 145 +- src/test_utils.rs | 55 +- src/web_server/model.rs | 2 +- src/web_server/probes.rs | 15 +- src/web_server/stories.rs | 16 +- 30 files changed, 3889 insertions(+), 361 deletions(-) create mode 100644 .dockerignore create mode 100644 .git-blame-ignore-revs create mode 100644 .github/workflows/docker.yaml create mode 100644 .github/workflows/release.yaml create mode 100644 Cargo.lock create mode 100644 Dockerfile create mode 100644 src/otel/metrics.rs create mode 100644 src/otel/mod.rs create mode 100644 src/otel/tracing.rs diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..fff0f03 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,32 @@ +# Include any files or directories that you don't want to be copied to your +# container here (e.g., local build artifacts, temporary files, etc.). +# +# For more help, visit the .dockerignore file reference guide at +# https://docs.docker.com/go/build-context-dockerignore/ + +**/.DS_Store +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/charts +**/docker-compose* +**/compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/secrets.dev.yaml +**/values.dev.yaml +/bin +/target +LICENSE +README.md diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 0000000..3e6a5e0 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,2 @@ +# cargo fmt +c4cad928287973eb223d31177af10deaaf84ef3d diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml new file mode 100644 index 0000000..cb63995 --- /dev/null +++ b/.github/workflows/docker.yaml @@ -0,0 +1,66 @@ +# +name: Create and publish a Docker image + +# Configures this workflow to run every time a change is pushed to the branch called `release`. +on: + push: + tags: ["v*"] + +# Defines two custom environment variables for the workflow. These are used for the Container registry domain, and a name for the Docker image that this workflow builds. +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +# There is a single job in this workflow. It's configured to run on the latest available version of Ubuntu. +jobs: + build-and-push-image: + runs-on: ubuntu-latest + # Sets the permissions granted to the `GITHUB_TOKEN` for the actions in this job. + permissions: + contents: read + packages: write + attestations: write + id-token: write + # + steps: + - name: Checkout repository + uses: actions/checkout@v4 + # Uses the `docker/login-action` action to log in to the Container registry registry using the account and password that will publish the packages. Once published, the packages are scoped to the account defined here. + - name: Log in to the Container registry + uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + # This step uses [docker/metadata-action](https://github.com/docker/metadata-action#about) to extract tags and labels that will be applied to the specified image. The `id` "meta" allows the output of this step to be referenced in a subsequent step. The `images` value provides the base name for the tags and labels. + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + # These two steps configure multi-platform building + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + # This step uses the `docker/build-push-action` action to build the image, based on your repository's `Dockerfile`. If the build succeeds, it pushes the image to GitHub Packages. + # It uses the `context` parameter to define the build's context as the set of files located in the specified path. For more information, see "[Usage](https://github.com/docker/build-push-action#usage)" in the README of the `docker/build-push-action` repository. + # It uses the `tags` and `labels` parameters to tag and label the image with the output from the "meta" step. + - name: Build and push Docker image + id: push + uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + platforms: linux/amd64,linux/arm64 + + # This step generates an artifact attestation for the image, which is an unforgeable statement about where and how it was built. It increases supply chain security for people who consume the image. For more information, see "[AUTOTITLE](/actions/security-guides/using-artifact-attestations-to-establish-provenance-for-builds)." + - name: Generate artifact attestation + uses: actions/attest-build-provenance@v1 + with: + subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}} + subject-digest: ${{ steps.push.outputs.digest }} + push-to-registry: true diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..4b5b859 --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,20 @@ +name: Release + +permissions: + contents: write + +on: + push: + tags: + - v[0-9]+.* + +jobs: + create-release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + # v1 tag as of 2024-06-13 + - uses: taiki-e/create-gh-release-action@72d65cee1f8033ef0c8b5d79eaf0c45c7c578ce3 + with: + # (required) GitHub token for creating GitHub Releases. + token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index 07650c6..49fb815 100644 --- a/.gitignore +++ b/.gitignore @@ -5,10 +5,6 @@ debug/ target/ -# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries -# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html -Cargo.lock - # These are backup files generated by rustfmt **/*.rs.bk diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..bba7a1a --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,2488 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "0.6.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" + +[[package]] +name = "anstyle-parse" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a64c907d4e79225ac72e2a354c9ce84d50ebb4586dee56c82b3ee73004f537f5" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" +dependencies = [ + "anstyle", + "windows-sys 0.52.0", +] + +[[package]] +name = "anyhow" +version = "1.0.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519" + +[[package]] +name = "assert-json-diff" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "async-channel" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" +dependencies = [ + "concurrent-queue", + "event-listener", + "futures-core", +] + +[[package]] +name = "async-stream" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async-trait" +version = "0.1.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "autocfg" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" + +[[package]] +name = "axum" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf" +dependencies = [ + "async-trait", + "axum-core 0.3.4", + "bitflags 1.3.2", + "bytes", + "futures-util", + "http 0.2.12", + "http-body 0.4.6", + "hyper 0.14.28", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "sync_wrapper 0.1.2", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a6c9af12842a67734c9a2e355436e5d03b22383ed60cf13cd0c18fbfe3dcbcf" +dependencies = [ + "async-trait", + "axum-core 0.4.3", + "bytes", + "futures-util", + "http 1.1.0", + "http-body 1.0.0", + "http-body-util", + "hyper 1.3.1", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper 1.0.1", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http 0.2.12", + "http-body 0.4.6", + "mime", + "rustversion", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-core" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a15c63fd72d41492dc4f497196f5da1fb04fb7529e631d73630d1b491e47a2e3" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http 1.1.0", + "http-body 1.0.0", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper 0.1.2", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "backtrace" +version = "0.3.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "bytes" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" + +[[package]] +name = "cc" +version = "1.0.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17f6e324229dc011159fcc089755d1e2e216a90d43a7dea6853ca740b84f35e7" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-targets 0.52.5", +] + +[[package]] +name = "clap" +version = "4.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" + +[[package]] +name = "colorchoice" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" + +[[package]] +name = "concurrent-queue" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d16048cd947b08fa32c24458a22f5dc5e835264f689f4f5653210c69fd107363" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + +[[package]] +name = "crossbeam-utils" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" + +[[package]] +name = "deadpool" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "421fe0f90f2ab22016f32a9881be5134fdd71c65298917084b0c7477cbc3856e" +dependencies = [ + "async-trait", + "deadpool-runtime", + "num_cpus", + "retain_mut", + "tokio", +] + +[[package]] +name = "deadpool-runtime" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63dfa964fe2a66f3fde91fc70b267fe193d822c7e603e2a675a49a7f46ad3f49" + +[[package]] +name = "either" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b" + +[[package]] +name = "encoding_rs" +version = "0.8.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + +[[package]] +name = "fastrand" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "658bd65b1cf4c852a3cc96f18a8ce7b5640f6b703f905c7d74532294c2a63984" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" + +[[package]] +name = "futures-executor" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" + +[[package]] +name = "futures-lite" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" +dependencies = [ + "fastrand 1.9.0", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + +[[package]] +name = "futures-macro" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" + +[[package]] +name = "futures-task" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" + +[[package]] +name = "futures-timer" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" + +[[package]] +name = "futures-util" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "gimli" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "h2" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.12", + "indexmap 2.2.6", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http 0.2.12", + "pin-project-lite", +] + +[[package]] +name = "http-body" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" +dependencies = [ + "bytes", + "http 1.1.0", +] + +[[package]] +name = "http-body-util" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0475f8b2ac86659c21b64320d5d653f9efe42acd2a4e560073ec61a155a34f1d" +dependencies = [ + "bytes", + "futures-core", + "http 1.1.0", + "http-body 1.0.0", + "pin-project-lite", +] + +[[package]] +name = "http-types" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9b187a72d63adbfba487f48095306ac823049cb504ee195541e91c7775f5ad" +dependencies = [ + "anyhow", + "async-channel", + "base64 0.13.1", + "futures-lite", + "http 0.2.12", + "infer", + "pin-project-lite", + "rand 0.7.3", + "serde", + "serde_json", + "serde_qs", + "serde_urlencoded", + "url", +] + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "0.14.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http 0.2.12", + "http-body 0.4.6", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe575dd17d0862a9a33781c8c4696a55c320909004a67a00fb286ba8b1bc496d" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http 1.1.0", + "http-body 1.0.0", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", +] + +[[package]] +name = "hyper-timeout" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" +dependencies = [ + "hyper 0.14.28", + "pin-project-lite", + "tokio", + "tokio-io-timeout", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper 0.14.28", + "native-tls", + "tokio", + "tokio-native-tls", +] + +[[package]] +name = "hyper-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca38ef113da30126bbff9cd1705f9273e15d45498615d138b0c20279ac7a76aa" +dependencies = [ + "bytes", + "futures-util", + "http 1.1.0", + "http-body 1.0.0", + "hyper 1.3.1", + "pin-project-lite", + "socket2", + "tokio", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", +] + +[[package]] +name = "indexmap" +version = "2.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +dependencies = [ + "equivalent", + "hashbrown 0.14.3", +] + +[[package]] +name = "infer" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64e9829a50b42bb782c1df523f78d332fe371b10c661e78b7a3c34b0198e9fac" + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "ipnet" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "js-sys" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.153" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" + +[[package]] +name = "linux-raw-sys" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" + +[[package]] +name = "lock_api" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + +[[package]] +name = "memchr" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.48.0", +] + +[[package]] +name = "native-tls" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "num-traits" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "object" +version = "0.32.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "openssl" +version = "0.10.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f" +dependencies = [ + "bitflags 2.5.0", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c597637d56fbc83893a35eb0dd04b2b8e7a50c91e64e9493e398b5df4fb45fa2" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "opentelemetry" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b69a91d4893e713e06f724597ad630f1fa76057a5e1026c0ca67054a9032a76" +dependencies = [ + "futures-core", + "futures-sink", + "js-sys", + "once_cell", + "pin-project-lite", + "thiserror", +] + +[[package]] +name = "opentelemetry-http" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0ba633e55c5ea6f431875ba55e71664f2fa5d3a90bd34ec9302eecc41c865dd" +dependencies = [ + "async-trait", + "bytes", + "http 0.2.12", + "opentelemetry", + "reqwest", +] + +[[package]] +name = "opentelemetry-otlp" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a94c69209c05319cdf7460c6d4c055ed102be242a0a6245835d7bc42c6ec7f54" +dependencies = [ + "async-trait", + "futures-core", + "http 0.2.12", + "opentelemetry", + "opentelemetry-http", + "opentelemetry-proto", + "opentelemetry_sdk", + "prost", + "reqwest", + "serde_json", + "thiserror", + "tokio", + "tonic", +] + +[[package]] +name = "opentelemetry-proto" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "984806e6cf27f2b49282e2a05e288f30594f3dbc74eb7a6e99422bc48ed78162" +dependencies = [ + "hex", + "opentelemetry", + "opentelemetry_sdk", + "prost", + "serde", + "tonic", +] + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1869fb4bb9b35c5ba8a1e40c9b128a7b4c010d07091e864a29da19e4fe2ca4d7" + +[[package]] +name = "opentelemetry-stdout" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6d080bf06af02b738feb2e6830cf72c30b76ca18b40f555cdf1b53e7b491bfe" +dependencies = [ + "async-trait", + "chrono", + "futures-util", + "opentelemetry", + "opentelemetry_sdk", + "ordered-float", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "opentelemetry_sdk" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae312d58eaa90a82d2e627fd86e075cf5230b3f11794e2ed74199ebbe572d4fd" +dependencies = [ + "async-trait", + "futures-channel", + "futures-executor", + "futures-util", + "glob", + "lazy_static", + "once_cell", + "opentelemetry", + "ordered-float", + "percent-encoding", + "rand 0.8.5", + "serde_json", + "thiserror", + "tokio", + "tokio-stream", +] + +[[package]] +name = "ordered-float" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a76df7075c7d4d01fdcb46c912dd17fba5b60c78ea480b475f2b6ab6f666584e" +dependencies = [ + "num-traits", +] + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "parking" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.48.5", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pin-project" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "proc-macro2" +version = "1.0.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "prodzilla" +version = "0.0.3-rc.5" +dependencies = [ + "axum 0.7.5", + "chrono", + "clap", + "futures", + "lazy_static", + "opentelemetry", + "opentelemetry-http", + "opentelemetry-otlp", + "opentelemetry-semantic-conventions", + "opentelemetry-stdout", + "opentelemetry_sdk", + "regex", + "reqwest", + "serde", + "serde_json", + "serde_yaml", + "tokio", + "tracing", + "tracing-subscriber", + "uuid", + "wiremock", +] + +[[package]] +name = "prost" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "deb1435c188b76130da55f17a466d252ff7b1418b2ad3e037d127b94e3411f29" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-derive" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "quote" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.14", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "regex" +version = "1.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata 0.4.6", + "regex-syntax 0.8.3", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.3", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" + +[[package]] +name = "reqwest" +version = "0.11.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" +dependencies = [ + "base64 0.21.7", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http 0.2.12", + "http-body 0.4.6", + "hyper 0.14.28", + "hyper-tls", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper 0.1.2", + "system-configuration", + "tokio", + "tokio-native-tls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + +[[package]] +name = "retain_mut" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4389f1d5789befaf6029ebd9f7dac4af7f7e3d61b69d4f30e2ac02b57e7712b0" + +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + +[[package]] +name = "rustix" +version = "0.38.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89" +dependencies = [ + "bitflags 2.5.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64 0.21.7", +] + +[[package]] +name = "rustversion" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80af6f9131f277a45a3fba6ce8e2258037bb0477a67e610d3c1fe046ab31de47" + +[[package]] +name = "ryu" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" + +[[package]] +name = "schannel" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "770452e37cad93e0a50d5abc3990d2bc351c36d0328f86cefec2f2fb206eaef6" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f3cc463c0ef97e11c3461a9d3787412d30e8e7eb907c79180c4a57bf7c04ef" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "serde" +version = "1.0.198" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9846a40c979031340571da2545a4e5b7c4163bdae79b301d5f86d03979451fcc" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.198" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e88edab869b01783ba905e7d0153f9fc1a6505a96e4ad3018011eedb838566d9" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.116" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" +dependencies = [ + "itoa", + "serde", +] + +[[package]] +name = "serde_qs" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7715380eec75f029a4ef7de39a9200e0a63823176b759d055b613f5a87df6a6" +dependencies = [ + "percent-encoding", + "serde", + "thiserror", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap 2.2.6", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "socket2" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "sync_wrapper" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" + +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tempfile" +version = "3.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" +dependencies = [ + "cfg-if", + "fastrand 2.0.2", + "rustix", + "windows-sys 0.52.0", +] + +[[package]] +name = "thiserror" +version = "1.0.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "num_cpus", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.48.0", +] + +[[package]] +name = "tokio-io-timeout" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf" +dependencies = [ + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-macros" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + +[[package]] +name = "tonic" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76c4eb7a4e9ef9d4763600161f12f5070b92a578e1b634db88a6887844c91a13" +dependencies = [ + "async-stream", + "async-trait", + "axum 0.6.20", + "base64 0.21.7", + "bytes", + "h2", + "http 0.2.12", + "http-body 0.4.6", + "hyper 0.14.28", + "hyper-timeout", + "percent-encoding", + "pin-project", + "prost", + "tokio", + "tokio-stream", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "indexmap 1.9.3", + "pin-project", + "pin-project-lite", + "rand 0.8.5", + "slab", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "unicode-bidi" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-normalization" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + +[[package]] +name = "url" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + +[[package]] +name = "uuid" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" +dependencies = [ + "getrandom 0.2.14", +] + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "waker-fn" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c4517f54858c779bbcbf228f4fca63d121bf85fbecb2dc578cdf4a39395690" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" + +[[package]] +name = "web-sys" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.5", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.5", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" +dependencies = [ + "windows_aarch64_gnullvm 0.52.5", + "windows_aarch64_msvc 0.52.5", + "windows_i686_gnu 0.52.5", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.5", + "windows_x86_64_gnu 0.52.5", + "windows_x86_64_gnullvm 0.52.5", + "windows_x86_64_msvc 0.52.5", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "wiremock" +version = "0.5.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13a3a53eaf34f390dd30d7b1b078287dd05df2aa2e21a589ccb80f5c7253c2e9" +dependencies = [ + "assert-json-diff", + "async-trait", + "base64 0.21.7", + "deadpool", + "futures", + "futures-timer", + "http-types", + "hyper 0.14.28", + "log", + "once_cell", + "regex", + "serde", + "serde_json", + "tokio", +] diff --git a/Cargo.toml b/Cargo.toml index 2bbb499..c6ed10f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "prodzilla" -version = "0.1.0" +version = "0.0.3-rc.5" edition = "2021" [dependencies] @@ -17,9 +17,18 @@ reqwest = { version = "0.11" } lazy_static = "1.4.0" futures = "0.3.29" wiremock = "0.5.22" -chrono = {version = "0.4.31", features = ["serde"] } +chrono = { version = "0.4.31", features = ["serde"] } regex = "1.10.3" uuid = { version = "1", features = ["v4"] } -opentelemetry = "0.21.0" -opentelemetry-http = "0.10.0" -opentelemetry_sdk = { version = "0.21.2" , features = ["rt-tokio"] } +opentelemetry = "0.23.0" +opentelemetry-http = "0.12.0" +opentelemetry_sdk = { version = "0.23.0", features = ["rt-tokio"] } +opentelemetry-otlp = { version = "0.16.0", features = [ + "metrics", + "http-json", + "http-proto", + "reqwest-client", +] } +opentelemetry-semantic-conventions = "0.15.0" +clap = { version = "4.5.4", features = ["derive"] } +opentelemetry-stdout = { version = "0.4.0", features = ["metrics", "trace"] } diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..d10bac6 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,38 @@ +# syntax=docker/dockerfile:1 + +ARG RUST_VERSION=1.78 + +FROM --platform=$BUILDPLATFORM rust:${RUST_VERSION}-slim-bookworm AS build + +WORKDIR /app + +RUN apt-get update -y && apt-get install -y libssl-dev pkg-config + +COPY . . +RUN cargo build --locked --release --target-dir target && cp ./target/release/prodzilla /bin/prodzilla + +FROM debian:bookworm-slim AS final + +RUN apt-get update && apt-get install -y libssl-dev ca-certificates +# Create a non-privileged user that the app will run under. +# See https://docs.docker.com/go/dockerfile-user-best-practices/ +ARG UID=10001 +RUN adduser \ + --disabled-password \ + --gecos "" \ + --home "/nonexistent" \ + --shell "/sbin/nologin" \ + --no-create-home \ + --uid "${UID}" \ + appuser +USER appuser + + +# Copy the executable from the "build" stage. +COPY --from=build /bin/prodzilla /bin/ + +# Expose the port that the application listens on. +EXPOSE 3000 + +# What the container should run when it is started. +ENTRYPOINT ["/bin/prodzilla"] diff --git a/README.md b/README.md index ff5a7f0..2c49960 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Prodzilla is a modern synthetic monitoring tool built in Rust. It's focused on t Prodzilla supports chained requests to endpoints, passing of values from one response to another request, verifying responses are as expected, and outputting alerts via webhooks on failures. It also exposes an API that allow viewing results in json and manual triggering of probes. It's integrated with OpenTelemetry, so includes a trace_id for every request made to your system. May add a UI in future. -It's also lightning fast, runs with < 8mb of ram, and is free to host on [Shuttle](https://shuttle.rs/). +It's also lightning fast, runs with < 15mb of ram, and is free to host on [Shuttle](https://shuttle.rs/). The long-term goals of Prodzilla are: - Reduce divergence and duplication of code between blackbox, end-to-end testing and production observability @@ -15,6 +15,7 @@ To be part of the community, or for any questions, join our [Discord](https://di ## Table of Contents +- [Table of Contents](#table-of-contents) - [Getting Started](#getting-started) - [Configuring Synthetic Monitors](#configuring-synthetic-monitors) - [Probes](#probes) @@ -25,8 +26,13 @@ To be part of the community, or for any questions, join our [Discord](https://di - [Prodzilla Server Endpoints](#prodzilla-server-endpoints) - [Get Probes and Stories](#get-probes-and-stories) - [Get Probe and Story Results](#get-probe-and-story-results) - - [Trigger Probe or Story](#trigger-probe-or-story-in-development) -- [Deploying on Shuttle for free](#deploying-on-shuttle-for-free) + - [Trigger Probe or Story (In Development)](#trigger-probe-or-story-in-development) +- [Monitoring Prodzilla](#monitoring-prodzilla) + - [Tracked metrics](#tracked-metrics) + - [Traces](#traces) + - [Configuring OpenTelemetry export](#configuring-opentelemetry-export) + - [Configuring log level](#configuring-log-level) +- [Deploying on Shuttle for Free](#deploying-on-shuttle-for-free) - [Feature Roadmap](#feature-roadmap) ## Getting Started @@ -37,7 +43,13 @@ To get started probing your services, clone this repo, and in the root execute t cargo run ``` -The application parses the [prodzilla.yml](/prodzilla.yml) file to generate a list of probes executed on a given schedule, and decide how to alert. +You can also use Docker, as Prodzilla is published to `ghcr.io/prodzilla/prodzilla`: + +``` +docker run -v $(pwd)/prodzilla.yml:/prodzilla.yml ghcr.io/prodzilla/prodzilla:main +``` + +The application parses the [prodzilla.yml](/prodzilla.yml) file to generate a list of probes executed on a given schedule, and decide how to alert. Other configuration file paths can be selected using the `-f` flag. Execute `cargo run -- --help` or `prodzilla --help` to see a full list of configuration flags. The bare minimum config required is: @@ -64,10 +76,12 @@ A complete Probe config looks as follows: - name: Your Post Url url: https://your.site/some/path http_method: POST + sensitive: false with: headers: x-client-id: ClientId body: '"{"test": true}"' + timeout_seconds: 10 expectations: - field: StatusCode operation: Equals @@ -111,19 +125,21 @@ stories: ### Variables -One unique aspect of Prodzilla is the ability to substitute in values from earlier steps, or generated values, as in the example above. Prodzilla currently supports the following variable substitutions. +One unique aspect of Prodzilla is the ability to substitute in values from earlier steps, environment variables, or generated values, as in the example above. Prodzilla currently supports the following variable substitutions. | Substitute Value | Behaviour | -|----------------------------------------------|----------------------------------------------------------------------------------------------------------------------| +| -------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | | ${{steps.step-name.response.body}} | Inserts the whole response body from the given step. | | ${{steps.step-name.response.body.fieldName}} | Inserts the value of a specific JSON field from a response body from a given step. Doesn't currently support arrays. | -| ${{generate.uuid}} | Inserts a generated UUID. | +| ${{generate.uuid}} | Inserts a generated UUID. | +| ${{env.VAR_NAME}} | Insert the environment variable VAR_NAME | Note that if a step name is used in a parameter but does not yet exist, Prodzilla will default to substituting an empty string. +If a requested environment variable is not set, Prodzilla will log a warning and substitute an empty string. ### Expectations -Expectations can be declared using the `expectations` block and supports an unlimited number of rules. Currently, the supported fields are `StatusCode` and `Body`, and the supported operations are `Equals`, `Contains`, and `IsOneOf` (which accepts a string value separated by the pipe symbol `|`). +Expectations can be declared using the `expectations` block and supports an unlimited number of rules. Currently, the supported fields are `StatusCode` and `Body`, and the supported operations are `Equals`, `Contains`, `Matches` which accepts a regular expression, and `IsOneOf` (which accepts a string value separated by the pipe symbol `|`). Expectations can be put on Probes, or Steps within Stories. @@ -137,6 +153,7 @@ If expectations aren't met for a Probe or Story, a webhook will be sent to any u ... alerts: - url: https://webhook.site/54a9a526-c104-42a7-9b76-788e897390d8 + - url: https://hooks.slack.com/services/T000/B000/XXXX ``` @@ -145,12 +162,38 @@ The webhook looks as such: { "message": "Probe failed.", "probe_name": "Your Probe", - "failure_timestamp": "2024-01-26T02:41:02.983025Z" + "failure_timestamp": "2024-01-26T02:41:02.983025Z", + "trace_id": "123456789abcdef", + "error_message": "Failed to meet expectation for field 'StatusCode' with operation Equals \"200\".", + "status_code": 500, + "body": "Internal Server Error" } ``` -Slack, OpsGenie, and PagerDuty notification integrations are planned. +Response bodies are truncated to 500 characters. If a step or probe is marked as sensitive, the request body will be redacted from logs and alerts. + +Prodzilla will also recognize the Slack webhook domain `hooks.slack.com` and produce messages like: + +> **"Your Probe" failed.** +> +> Error message: +> +> > Failed to meet expectation for field 'StatusCode' with operation Equals "429". +> +> Received status code **500** +> +> Received body: +> +> ``` +> Internal Server Error +> ``` +> +> Time: **2024-06-26 14:36:30.094126 UTC** +> +> Trace ID: **e03cc9b03185db8004400049264331de** + +OpsGenie, and PagerDuty notification integrations are planned. ## Prodzilla Server Endpoints @@ -235,6 +278,45 @@ Example Response (for stories, probes will look slightly different): } ``` +## Monitoring Prodzilla +Prodzilla generates OpenTelemetry traces and metrics for each probe and story execution. +It also outputs structured logs to standard out. + +### Tracked metrics +Prodzilla tracks the following metrics: + +| Name | Type | Description | +| ---- | ---- | ----------- || +| runs | Counter(u64) | The total number of executions for this test | +| duration | Histogram(u64) | Time taken to execute the test | +| errors | Counter(u64) | The total number of errors for this test | + +All metrics have the attributes `name` and `type`. +`type` is either `probe` for metrics measuring a probe, `story` for metrics measuring an entire story, or `step` for measuring an individual step in a story. +`name` is the name of the probe, story, or step that is being measured. +Metrics for an individual step have the additional attribute `story_name` which is the name of the story that the step is part of. + + +### Traces +Prodzilla generates a root span for each story or probe that is being run, and further spans for each step and HTTP call that is made within that test. The trace ID is propagated in these HTTP requests to downstream services, enabling fully distributed insight into the backends that are being called. + +Errors occuring in steps and probes or expectations not being met lead to the span in question being marked with the `error` status. Furthermore, the error message and truncated HTTP response body is attached as a span event. + +### Configuring OpenTelemetry export +Both metrics and traces can be exported with the OTLP protocol over either HTTP or gRPC. +Configuration follows the OpenTelemetry standard environment variables: + +- `OTEL_EXPORTER_OTLP_ENDPOINT` is used to define the collector endpoint. Defaults to `http://localhost:431` +- `OTEL_EXPORTER_OTLP_PROTOCOL` is used to define the protocol that is used in export. Supported values are `http/protobuf`, `http/json` and `grpc`. Defaults to `grpc`. +- `OTEL_EXPORTER_OTLP_TIMEOUT` is used to set an exporter timeout in seconds. Defaults to 10 seconds. +- `OTEL_METRICS_EXPORTER` is used to define how metrics are exported. Supported values are `otlp` and `stdout`. If unset, metrics will not be exported. +- `OTEL_TRACES_EXPORTER` is used to define how traces are exported. Supported values are `otlp` and `stdout`. If unset, traces will not exported. + +Furthermore, resource attributes can be set with `OTEL_RESOURCE_ATTRIBUTES`. + +### Configuring log level +The logging level can be set using the environment variable `RUST_LOG`. Supported levels are `trace`, `debug`, `info`, `warn`, and `error` in ascending order of severity. + ## Deploying on Shuttle for Free [Shuttle.rs](https://shuttle.rs) allows hosting of Rust apps for free. Check out [How I'm Getting Free Synthetic Monitoring](https://codingupastorm.dev/2023/11/07/prodzilla-and-shuttle/) for a tutorial on how to deploy Prodzilla to Shuttle for free. @@ -265,11 +347,12 @@ Progress on the base set of synthetic monitoring features is loosely tracked bel - Status code :white_check_mark: - Response body :white_check_mark: - Specific fields - - Regex + - Regex :white_check_mark: - Yaml Objects / Reusable parameters / Human Readability - Reusable Request bodies - Reusable Authenticated users - Reusable Validation + - Environment variable interpolation in configuration file :white_check_mark: - Result storage - In Memory :white_check_mark: - In a Database @@ -294,6 +377,9 @@ Progress on the base set of synthetic monitoring features is loosely tracked bel - CI / CD Integration - Standalone easy-to-install image :bricks: - Github Actions integration to trigger tests / use as smoke tests :bricks: + - Docker images for main branch and tagged releases :white_check_mark: - Otel Support - TraceIds for every request :white_check_mark: + - OTLP trace export over gRPC or HTTP :white_check_mark: + - Metrics for runs, durations and failures exported over OTLP :white_check_mark: diff --git a/src/alerts/mod.rs b/src/alerts/mod.rs index 7746baf..7c040b5 100644 --- a/src/alerts/mod.rs +++ b/src/alerts/mod.rs @@ -1,2 +1,2 @@ +mod model; pub(crate) mod outbound_webhook; -mod model; \ No newline at end of file diff --git a/src/alerts/model.rs b/src/alerts/model.rs index ef0c1e6..89d8722 100644 --- a/src/alerts/model.rs +++ b/src/alerts/model.rs @@ -1,9 +1,36 @@ use chrono::{DateTime, Utc}; -use serde::{Serialize, Deserialize}; +use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct WebhookNotification { pub message: String, pub probe_name: String, pub failure_timestamp: DateTime, -} \ No newline at end of file + pub error_message: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub trace_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub status_code: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub body: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SlackNotification { + pub blocks: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SlackBlock { + pub r#type: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub elements: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub text: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SlackTextBlock { + pub r#type: String, + pub text: String, +} diff --git a/src/alerts/outbound_webhook.rs b/src/alerts/outbound_webhook.rs index 0312c8c..12235df 100644 --- a/src/alerts/outbound_webhook.rs +++ b/src/alerts/outbound_webhook.rs @@ -1,11 +1,13 @@ use std::time::Duration; -use crate::alerts::model::WebhookNotification; use crate::errors::MapToSendError; use crate::probe::model::ProbeAlert; +use crate::{alerts::model::WebhookNotification, probe::model::ProbeResponse}; use chrono::{DateTime, Utc}; use lazy_static::lazy_static; -use tracing::info; +use tracing::{info, warn}; + +use super::model::{SlackBlock, SlackNotification, SlackTextBlock}; const REQUEST_TIMEOUT_SECS: u64 = 10; @@ -18,40 +20,65 @@ lazy_static! { pub async fn alert_if_failure( success: bool, - probe_name: &String, + error: Option<&str>, + probe_response: Option<&ProbeResponse>, + probe_name: &str, failure_timestamp: DateTime, alerts: &Option>, -) -> Result<(), Box> { + trace_id: &Option, +) -> Result<(), Vec>> { if success { return Ok(()); } - + let error_message = error.unwrap_or("No error message"); + let status_code = probe_response.map(|r| r.status_code); + let truncated_body = match probe_response { + Some(r) if !r.sensitive => Some(r.truncated_body(500)), + Some(_) => Some("Redacted".to_owned()), + None => None, + }; + let log_body = truncated_body + .as_ref() + .unwrap_or(&"N/A".to_owned()) + .replace('\n', "\\n"); + warn!( + "Probe {probe_name} failed at {failure_timestamp} with trace ID {}. Status code: {}. Error: {error_message}. Body: {}", + trace_id.as_ref().unwrap_or(&"N/A".to_owned()), + status_code.map_or("N/A".to_owned(), |code| code.to_string()), + log_body, + ); + let mut errors = Vec::new(); if let Some(alerts_vec) = alerts { for alert in alerts_vec { - send_alert(alert, probe_name.clone(), failure_timestamp).await?; + if let Err(e) = send_alert( + alert, + probe_name.to_owned(), + status_code, + truncated_body.as_deref(), + error_message, + failure_timestamp, + trace_id.clone(), + ) + .await + { + errors.push(e); + } } } - return Ok(()); + if !errors.is_empty() { + Err(errors) + } else { + Ok(()) + } } -pub async fn send_alert( - alert: &ProbeAlert, - probe_name: String, - failure_timestamp: DateTime, +pub async fn send_generic_webhook( + url: &String, + body: String, ) -> Result<(), Box> { - // When we have other alert types, add them in some kind of switch here - - let mut request = CLIENT.post(&alert.url); - - let request_body = WebhookNotification { - message: "Probe failed.".to_owned(), - probe_name: probe_name, - failure_timestamp, - }; - - let json = serde_json::to_string(&request_body).map_to_send_err()?; - request = request.body(json); + let mut request = CLIENT.post(url); + request = request.body(body); let alert_response = request .timeout(Duration::from_secs(REQUEST_TIMEOUT_SECS)) @@ -63,7 +90,139 @@ pub async fn send_alert( alert_response.status().to_owned() ); - return Ok(()); + Ok(()) +} + +pub async fn send_webhook_alert( + url: &String, + probe_name: String, + status_code: Option, + body: Option<&str>, + error_message: &str, + failure_timestamp: DateTime, + trace_id: Option, +) -> Result<(), Box> { + let request_body = WebhookNotification { + message: "Probe failed.".to_owned(), + probe_name, + error_message: error_message.to_owned(), + failure_timestamp, + trace_id, + body: body.map(|s| s.to_owned()), + status_code, + }; + + let json = serde_json::to_string(&request_body).map_to_send_err()?; + send_generic_webhook(url, json).await +} + +pub async fn send_slack_alert( + webhook_url: &String, + probe_name: String, + status_code: Option, + body: Option<&str>, + error_message: &str, + failure_timestamp: DateTime, + trace_id: Option, +) -> Result<(), Box> { + // Uses Slack's Block Kit UI to make the message prettier + let mut blocks = vec![ + SlackBlock { + r#type: "header".to_owned(), + text: Some(SlackTextBlock { + r#type: "plain_text".to_owned(), + text: format!("\"{}\" failed.", probe_name), + }), + elements: None, + }, + SlackBlock { + r#type: "section".to_owned(), + text: Some(SlackTextBlock { + r#type: "mrkdwn".to_owned(), + text: format!("Error message:\n\n> {}", error_message), + }), + elements: None, + }, + ]; + + if let Some(code) = status_code { + blocks.push(SlackBlock { + r#type: "section".to_owned(), + elements: None, + text: Some(SlackTextBlock { + r#type: "mrkdwn".to_owned(), + text: format!("Received status code *{}*", code,), + }), + }) + } + + if let Some(s) = body { + blocks.push(SlackBlock { + r#type: "section".to_owned(), + elements: None, + text: Some(SlackTextBlock { + r#type: "mrkdwn".to_owned(), + text: format!("Received body:\n```\n{}\n```", s,), + }), + }) + } + + blocks.push(SlackBlock { + r#type: "context".to_owned(), + elements: Some(vec![ + SlackTextBlock { + r#type: "mrkdwn".to_owned(), + text: format!("Time: *{}*", failure_timestamp), + }, + SlackTextBlock { + r#type: "mrkdwn".to_owned(), + text: format!("Trace ID: *{}*", trace_id.unwrap_or("N/A".to_owned())), + }, + ]), + text: None, + }); + let request_body = SlackNotification { blocks }; + let json = serde_json::to_string(&request_body).map_to_send_err()?; + println!("{}", json); + send_generic_webhook(webhook_url, json).await +} + +pub async fn send_alert( + alert: &ProbeAlert, + probe_name: String, + status_code: Option, + body: Option<&str>, + error_message: &str, + failure_timestamp: DateTime, + trace_id: Option, +) -> Result<(), Box> { + let domain = alert.url.split('/').nth(2).unwrap_or(""); + match domain { + "hooks.slack.com" => { + send_slack_alert( + &alert.url, + probe_name.clone(), + status_code, + body, + error_message, + failure_timestamp, + trace_id.clone(), + ) + .await + } + _ => { + send_webhook_alert( + &alert.url, + probe_name.clone(), + status_code, + body, + error_message, + failure_timestamp, + trace_id.clone(), + ) + .await + } + } } #[cfg(test)] @@ -95,7 +254,16 @@ mod webhook_tests { }]); let failure_timestamp = Utc::now(); - let alert_result = alert_if_failure(false, &probe_name, failure_timestamp, &alerts).await; + let alert_result = alert_if_failure( + false, + Some("Test error"), + None, + &probe_name, + failure_timestamp, + &alerts, + &None, + ) + .await; assert!(alert_result.is_ok()); } diff --git a/src/app_state.rs b/src/app_state.rs index 3853f82..8790241 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -1,6 +1,10 @@ -use std::{sync::RwLock, collections::HashMap}; +use std::{collections::HashMap, sync::RwLock}; -use crate::{config::Config, probe::model::{ProbeResult, StoryResult}}; +use crate::{ + config::Config, + otel::metrics::Metrics, + probe::model::{ProbeResult, StoryResult}, +}; // Limits the number of results we store per probe. Once we go over this amount we remove the earliest. const PROBE_RESULT_LIMIT: usize = 100; @@ -8,23 +12,24 @@ const PROBE_RESULT_LIMIT: usize = 100; pub struct AppState { pub probe_results: RwLock>>, pub story_results: RwLock>>, - pub config: Config + pub config: Config, + pub metrics: Metrics, } impl AppState { - pub fn new(config: Config) -> AppState { - return AppState { + AppState { probe_results: RwLock::new(HashMap::new()), story_results: RwLock::new(HashMap::new()), - config: config - }; + config, + metrics: Metrics::new(), + } } pub fn add_probe_result(&self, probe_name: String, result: ProbeResult) { let mut write_lock = self.probe_results.write().unwrap(); - let results = write_lock.entry(probe_name).or_insert_with(Vec::new); + let results = write_lock.entry(probe_name).or_default(); results.push(result); // Ensure only the latest 100 elements are kept @@ -36,7 +41,7 @@ impl AppState { pub fn add_story_result(&self, story_name: String, result: StoryResult) { let mut write_lock = self.story_results.write().unwrap(); - let results = write_lock.entry(story_name).or_insert_with(Vec::new); + let results = write_lock.entry(story_name).or_default(); results.push(result); // Ensure only the latest 100 elements are kept @@ -44,4 +49,4 @@ impl AppState { results.remove(0); } } -} \ No newline at end of file +} diff --git a/src/config.rs b/src/config.rs index f39c11d..a2ab3d0 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,6 +1,7 @@ use std::path::PathBuf; use serde::{Deserialize, Serialize}; +use tracing::warn; use crate::probe::model::Probe; use crate::probe::model::Story; @@ -15,14 +16,43 @@ pub struct Config { pub async fn load_config>(path: P) -> Result> { let path = path.into(); - let config = tokio::fs::read_to_string(path).await?; + let config = match tokio::fs::read_to_string(path.clone()).await { + Ok(content) => content, + Err(ref e) if e.kind() == std::io::ErrorKind::NotFound => { + panic!("Config file not found: {:?}", path) + } + Err(e) => { + panic!("Failed to read config file: {:?}, err {}", path, e) + } + }; + let config = replace_env_vars(&config); let config: Config = serde_yaml::from_str(&config)?; Ok(config) } +pub fn replace_env_vars(content: &str) -> String { + let re = regex::Regex::new(r"\$\{\{\s*env\.(.*?)\s*\}\}").unwrap(); + let replaced = re.replace_all(content, |caps: ®ex::Captures| { + let var_name = &caps[1]; + // panics on missing enivronment variables, probably desirable? + match std::env::var(var_name) { + Ok(val) => val, + Err(_) => { + warn!( + "Environment variable {} not found, defaulting to empty string.", + var_name + ); + "".to_string() + } + } + }); + replaced.to_string() +} + #[cfg(test)] mod config_tests { use crate::{config::load_config, PRODZILLA_YAML}; + use std::env; #[tokio::test] async fn test_app_yaml_can_load() { @@ -36,6 +66,17 @@ mod config_tests { // Perform multiple tests using borrowed references assert_eq!(1, config.probes.len(), "Probes length should be 1"); - assert_eq!(1, config.stories.len(), "Stories length should be 1"); + assert_eq!(1, config.stories.len(), "Stories length should be 1"); + } + + #[tokio::test] + async fn test_env_substitution() { + env::set_var("TEST_ENV_VAR", "test_value"); + let content = "Environment variable ${{ env.TEST_ENV_VAR }} should be replaced even with varying whitespace ${{env.TEST_ENV_VAR}}${{ env.TEST_ENV_VAR}} ${{env.TEST_ENV_VAR }}${{ env.TEST_ENV_VAR }}, missing ${{ env.MISSING_VAR }} should be empty"; + let replaced = super::replace_env_vars(content); + assert_eq!( + "Environment variable test_value should be replaced even with varying whitespace test_valuetest_value test_valuetest_value, missing should be empty", + replaced + ); } } diff --git a/src/errors.rs b/src/errors.rs index bd4de6f..73cc4c2 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -1,3 +1,7 @@ +use std::error::Error; + +use crate::probe::model::{ExpectField, ExpectOperation}; + pub trait MapToSendError { fn map_to_send_err(self) -> Result>; } @@ -10,3 +14,33 @@ where self.map_err(|e| Box::new(e) as Box) } } + +pub struct ExpectationFailedError { + pub field: ExpectField, + pub expected: String, + pub body: String, + pub operation: ExpectOperation, + pub status_code: u32, +} + +impl Error for ExpectationFailedError {} + +impl std::fmt::Display for ExpectationFailedError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!( + f, + "Failed to meet expectation for field '{:?}' with operation {:?} {:?}.", + self.field, self.operation, self.expected, + ) + } +} + +impl std::fmt::Debug for ExpectationFailedError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!( + f, + "Failed to meet expectation for field '{:?}' with operation {:?} {:?}. Received: status '{}', body '{}'", + self.field, self.operation, self.expected, self.status_code, self.body + ) + } +} diff --git a/src/main.rs b/src/main.rs index 8932ce0..1eabb2e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,25 +2,34 @@ mod alerts; mod app_state; mod config; mod errors; +mod otel; mod probe; mod web_server; -use probe::http_probe::init_otel_tracing; +use clap::Parser; use probe::schedule::schedule_probes; use probe::schedule::schedule_stories; use std::sync::Arc; -use tracing_subscriber::EnvFilter; use web_server::start_axum_server; use crate::{app_state::AppState, config::load_config}; const PRODZILLA_YAML: &str = "prodzilla.yml"; +#[derive(Parser, Debug)] +#[command(version, about, long_about = None)] +struct Args { + // Test definition file to execute + #[arg(short, long, default_value = PRODZILLA_YAML)] + file: String, +} + #[tokio::main] async fn main() -> Result<(), Box> { - init_tracing(); + let args = Args::parse(); + let _guard = otel::init(); - let config = load_config(PRODZILLA_YAML).await?; + let config = load_config(args.file).await?; let app_state = Arc::new(AppState::new(config)); @@ -31,15 +40,7 @@ async fn main() -> Result<(), Box> { Ok(()) } -fn init_tracing() { - let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")); - - tracing_subscriber::fmt().with_env_filter(filter).init(); -} - async fn start_monitoring(app_state: Arc) -> Result<(), Box> { - init_otel_tracing(); - schedule_probes(&app_state.config.probes, app_state.clone()); schedule_stories(&app_state.config.stories, app_state.clone()); Ok(()) diff --git a/src/otel/metrics.rs b/src/otel/metrics.rs new file mode 100644 index 0000000..90a7960 --- /dev/null +++ b/src/otel/metrics.rs @@ -0,0 +1,92 @@ +use opentelemetry::{ + global, + metrics::{Counter, Histogram}, +}; +use opentelemetry_otlp::WithExportConfig; +use opentelemetry_sdk::{ + metrics::{ + reader::{DefaultAggregationSelector, DefaultTemporalitySelector}, + MeterProviderBuilder, PeriodicReader, SdkMeterProvider, + }, + runtime, +}; +use std::env; +use tracing::debug; + +use crate::otel::create_otlp_export_config; + +use super::resource; + +pub fn create_meter_provider() -> Option { + let reader = match env::var("OTEL_METRICS_EXPORTER") { + Ok(exporter_type) if exporter_type == "otlp" => { + debug!("Using OTLP metrics exporter"); + let export_config = create_otlp_export_config(); + let exporter = match export_config.protocol { + opentelemetry_otlp::Protocol::Grpc => { + debug!("Using OTLP gRPC exporter"); + opentelemetry_otlp::new_exporter() + .tonic() + .with_export_config(export_config) + .build_metrics_exporter( + Box::new(DefaultAggregationSelector::new()), + Box::new(DefaultTemporalitySelector::new()), + ) + .unwrap() + } + _ => { + debug!("Using OTLP HTTP exporter"); + match opentelemetry_otlp::new_exporter() + .http() + .with_protocol(export_config.protocol) + .with_endpoint(format!("{}/v1/metrics", export_config.endpoint)) + // .with_export_config(export_config) + .build_metrics_exporter( + Box::new(DefaultAggregationSelector::new()), + Box::new(DefaultTemporalitySelector::new()), + ) { + Ok(exporter) => exporter, + Err(err) => { + panic!("Failed to create OTLP HTTP metrics exporter: {}", err); + } + } + } + }; + PeriodicReader::builder(exporter, runtime::Tokio).build() + } + Ok(exporter_type) if exporter_type == "stdout" => { + debug!("Using stdout metrics exporter"); + let exporter = opentelemetry_stdout::MetricsExporter::default(); + PeriodicReader::builder(exporter, runtime::Tokio).build() + } + _ => { + debug!("No metrics exporter configured"); + return None; + } + }; + let meter_provider = MeterProviderBuilder::default() + .with_resource(resource()) + .with_reader(reader) + .build(); + + global::set_meter_provider(meter_provider.clone()); + + Some(meter_provider) +} + +pub struct Metrics { + pub duration: Histogram, + pub runs: Counter, + pub errors: Counter, +} + +impl Metrics { + pub fn new() -> Metrics { + let meter = opentelemetry::global::meter("prodzilla"); + Metrics { + duration: meter.u64_histogram("duration").init(), + runs: meter.u64_counter("runs").init(), + errors: meter.u64_counter("errors").init(), + } + } +} diff --git a/src/otel/mod.rs b/src/otel/mod.rs new file mode 100644 index 0000000..20952b4 --- /dev/null +++ b/src/otel/mod.rs @@ -0,0 +1,61 @@ +use std::{env, time::Duration}; + +use opentelemetry_otlp::{ExportConfig, Protocol}; +use opentelemetry_sdk::{ + metrics::SdkMeterProvider, + resource::{EnvResourceDetector, ResourceDetector}, + Resource, +}; +use tracing_subscriber::prelude::*; +use tracing_subscriber::EnvFilter; + +pub(crate) mod metrics; +pub(crate) mod tracing; + +pub fn resource() -> Resource { + Resource::default().merge(&EnvResourceDetector::new().detect(Duration::from_secs(3))) +} + +pub struct OtelGuard { + meter_provider: Option, +} + +impl Drop for OtelGuard { + fn drop(&mut self) { + if let Some(Err(err)) = self.meter_provider.as_ref().map(|mp| mp.shutdown()) { + eprintln!("Failed to shutdown meter provider: {err:?}"); + } + + opentelemetry::global::shutdown_tracer_provider(); + } +} + +pub fn init() -> OtelGuard { + let meter_provider = metrics::create_meter_provider(); + tracing::create_tracer(); + let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")); + tracing_subscriber::registry() + .with(filter) + .with(tracing_subscriber::fmt::layer()) + .init(); + + OtelGuard { meter_provider } +} + +fn create_otlp_export_config() -> ExportConfig { + ExportConfig { + endpoint: env::var("OTEL_EXPORTER_OTLP_ENDPOINT") + .unwrap_or_else(|_| "http://localhost:4317".to_string()), + protocol: match env::var("OTEL_EXPORTER_OTLP_PROTOCOL") { + Ok(protocol) if protocol == "http/protobuf" => Protocol::HttpBinary, + Ok(protocol) if protocol == "http/json" => Protocol::HttpJson, + _ => Protocol::Grpc, + }, + timeout: Duration::from_secs( + env::var("OTEL_EXPORTER_OTLP_TIMEOUT") + .unwrap_or_else(|_| "10".to_string()) + .parse::() + .expect("OTEL_EXPORTER_OTLP_TIMEOUT must be a number"), + ), + } +} diff --git a/src/otel/tracing.rs b/src/otel/tracing.rs new file mode 100644 index 0000000..5debb30 --- /dev/null +++ b/src/otel/tracing.rs @@ -0,0 +1,47 @@ +use std::env; + +use opentelemetry::global; +use opentelemetry_otlp::WithExportConfig; +use opentelemetry_sdk::propagation::TraceContextPropagator; + +use opentelemetry_sdk::trace::TracerProvider; +use tracing::debug; + +use super::{create_otlp_export_config, resource}; + +pub fn create_tracer() { + let provider = match env::var("OTEL_TRACES_EXPORTER") { + Ok(exporter) if exporter == "otlp" => { + let export_config = create_otlp_export_config(); + let span_exporter = match export_config.protocol { + opentelemetry_otlp::Protocol::Grpc => { + debug!("Using OTLP gRPC exporter"); + opentelemetry_otlp::new_exporter() + .tonic() + .with_export_config(export_config) + .build_span_exporter() + .unwrap() + } + _ => { + debug!("Using OTLP HTTP exporter"); + opentelemetry_otlp::new_exporter() + .http() + .with_protocol(export_config.protocol) + .with_endpoint(format!("{}/v1/traces", export_config.endpoint)) + .build_span_exporter() + .unwrap() + } + }; + TracerProvider::builder() + .with_batch_exporter(span_exporter, opentelemetry_sdk::runtime::Tokio) + .with_config(opentelemetry_sdk::trace::Config::default().with_resource(resource())) + .build() + } + Ok(exporter) if exporter == "stdout" => TracerProvider::builder() + .with_simple_exporter(opentelemetry_stdout::SpanExporter::default()) + .build(), + _ => TracerProvider::default(), + }; + global::set_tracer_provider(provider); + global::set_text_map_propagator(TraceContextPropagator::new()); +} diff --git a/src/probe/expectations.rs b/src/probe/expectations.rs index 50bc47a..6fe652f 100644 --- a/src/probe/expectations.rs +++ b/src/probe/expectations.rs @@ -1,28 +1,28 @@ +use crate::errors::ExpectationFailedError; use crate::probe::model::ExpectField; use crate::probe::model::ExpectOperation; use crate::probe::model::ProbeExpectation; +use regex::Regex; use tracing::debug; -use super::model::EndpointResult; - pub fn validate_response( step_name: &String, - endpoint_result: &EndpointResult, + status_code: u32, + body: String, expectations: &Option>, -) -> bool { - +) -> Result<(), ExpectationFailedError> { match expectations { - Some(expect_back) => { - let validation_result = validate_response_internal(&expect_back, endpoint_result.status_code, &endpoint_result.body); - if validation_result { + Some(expect_back) => match validate_response_internal(expect_back, status_code, body) { + Ok(_) => { debug!("Successful response for {}, as expected", step_name); - } else { + Ok(()) + } + Err(e) => { debug!("Successful response for {}, not as expected!", step_name); + Err(e) } - return validation_result; - } + }, None => { - // TODO: // If we don't have any expectations, default to checking status is 200 @@ -30,7 +30,7 @@ pub fn validate_response( "Successfully probed {}, no expectation so success is true", step_name ); - return true; + Ok(()) } } } @@ -38,105 +38,114 @@ pub fn validate_response( pub fn validate_response_internal( expect: &Vec, status_code: u32, - body: &String, -) -> bool { - let status_string = status_code.to_string(); - + body: String, +) -> Result<(), ExpectationFailedError> { for expectation in expect { - let expectation_result: bool; - match expectation.field { - ExpectField::Body => { - expectation_result = - validate_expectation(&expectation.operation, &expectation.value, &body); - } - ExpectField::StatusCode => { - expectation_result = validate_expectation( - &expectation.operation, - &expectation.value, - &status_string, - ); - } - } - - if !expectation_result { - return false; - } + validate_expectation(expectation, status_code, &body)?; } - return true; + Ok(()) } -fn validate_expectation( - operation: &ExpectOperation, - expected_value: &String, - value: &String, -) -> bool { +fn expectation_met(operation: &ExpectOperation, expected: &String, received: &String) -> bool { match operation { - ExpectOperation::Equals => { - return value == expected_value; - } - ExpectOperation::Contains => { - return value.contains(expected_value); - } - ExpectOperation::IsOneOf => { - let parts = expected_value.split("|"); - for part in parts { - if value == part { - return true; - } - } - return false; - } + ExpectOperation::Equals => expected == received, + ExpectOperation::Contains => received.contains(expected), + ExpectOperation::IsOneOf => expected.split('|').any(|part| part == received), + // TODO: This regex could probably be pre-compiled? + ExpectOperation::Matches => Regex::new(expected).unwrap().is_match(received), + } +} + +fn validate_expectation( + expect: &ProbeExpectation, + status_code: u32, + body: &String, +) -> Result<(), ExpectationFailedError> { + let expected_value = &expect.value; + let status_string = status_code.to_string(); + let received_value = match expect.field { + ExpectField::Body => body, + ExpectField::StatusCode => &status_string, + }; + let success = expectation_met(&expect.operation, expected_value, received_value); + if success { + Ok(()) + } else { + Err(ExpectationFailedError { + expected: expect.value.clone(), + body: body.clone(), + operation: expect.operation.clone(), + field: expect.field.clone(), + status_code, + }) } } #[tokio::test] async fn test_validate_expectations_equals() { - let success_result = validate_expectation( + let success_result = expectation_met( &ExpectOperation::Equals, &"Test".to_owned(), &"Test".to_owned(), ); - assert_eq!(success_result, true); + assert!(success_result); - let fail_result = validate_expectation( + let fail_result = expectation_met( &ExpectOperation::Equals, &"Test123".to_owned(), &"Test".to_owned(), ); - assert_eq!(fail_result, false); + assert!(!fail_result); } #[tokio::test] async fn test_validate_expectations_contains() { - let success_result = validate_expectation( + let success_result = expectation_met( &ExpectOperation::Contains, &"Test".to_owned(), &"Test123".to_owned(), ); - assert_eq!(success_result, true); + assert!(success_result); - let fail_result = validate_expectation( + let fail_result = expectation_met( &ExpectOperation::Contains, &"Test123".to_owned(), &"Test".to_owned(), ); - assert_eq!(fail_result, false); + assert!(!fail_result); } #[tokio::test] async fn test_validate_expectations_isoneof() { - let success_result = validate_expectation( + let success_result = expectation_met( &ExpectOperation::IsOneOf, &"Test|Yes|No".to_owned(), &"Test".to_owned(), ); - assert_eq!(success_result, true); + assert!(success_result); - let fail_result = validate_expectation( + let fail_result = expectation_met( &ExpectOperation::IsOneOf, &"Test|Yes|No".to_owned(), &"Yest".to_owned(), ); - assert_eq!(fail_result, false); + assert!(!fail_result); +} + +#[tokio::test] +async fn test_validate_expectations_matches() { + let success_result = expectation_met( + &ExpectOperation::Matches, + &r#"^\d{5}$"#.to_owned(), + &"12345".to_owned(), + ); + assert!(success_result); + + let fail_result = expectation_met( + &ExpectOperation::Matches, + &r#"^\d{5}$"#.to_owned(), + &"1234".to_owned(), + ); + assert!(!fail_result); } diff --git a/src/probe/http_probe.rs b/src/probe/http_probe.rs index 44fb432..dff9d6e 100644 --- a/src/probe/http_probe.rs +++ b/src/probe/http_probe.rs @@ -4,9 +4,14 @@ use std::time::Duration; use crate::errors::MapToSendError; use chrono::Utc; use lazy_static::lazy_static; +use opentelemetry::KeyValue; +use opentelemetry_semantic_conventions::trace as semconv; + +use opentelemetry::trace::FutureExt; use opentelemetry::trace::Span; -use opentelemetry_sdk::propagation::TraceContextPropagator; -use opentelemetry_sdk::trace::TracerProvider; +use opentelemetry::trace::SpanId; +use opentelemetry::trace::TraceId; + use reqwest::header::HeaderMap; use reqwest::RequestBuilder; @@ -16,7 +21,7 @@ use opentelemetry::trace::TraceContextExt; use opentelemetry::Context; use opentelemetry::{global, trace::Tracer}; -const REQUEST_TIMEOUT_SECS: u64 = 10; +const DEFAULT_REQUEST_TIMEOUT_SECS: u64 = 10; lazy_static! { static ref CLIENT: reqwest::Client = reqwest::ClientBuilder::new() @@ -26,35 +31,66 @@ lazy_static! { } pub async fn call_endpoint( - http_method: &String, + http_method: &str, url: &String, input_parameters: &Option, + sensitive: bool, ) -> Result> { let timestamp_start = Utc::now(); - let (otel_headers, trace_id) = get_otel_headers(); + let (otel_headers, cx, span_id, trace_id) = + get_otel_headers(format!("{} {}", http_method, url)); let request = build_request(http_method, url, input_parameters, otel_headers)?; + let request_timeout = Duration::from_secs( + input_parameters + .as_ref() + .and_then(|params| params.timeout_seconds) + .unwrap_or(DEFAULT_REQUEST_TIMEOUT_SECS), + ); let response = request - .timeout(Duration::from_secs(REQUEST_TIMEOUT_SECS)) + .timeout(request_timeout) .send() + .with_context(cx.clone()) .await .map_to_send_err()?; let timestamp_response = Utc::now(); - return Ok(EndpointResult { + let result = EndpointResult { timestamp_request_started: timestamp_start, timestamp_response_received: timestamp_response, status_code: response.status().as_u16() as u32, body: response.text().await.map_to_send_err()?, - trace_id: trace_id - }); + sensitive, + trace_id: trace_id.to_string(), + span_id: span_id.to_string(), + }; + let span = cx.span(); + span.set_attributes(vec![ + KeyValue::new(semconv::HTTP_METHOD, http_method.to_owned()), + KeyValue::new(semconv::HTTP_URL, url.clone()), + ]); + span.set_attribute(KeyValue::new( + semconv::HTTP_STATUS_CODE, + result.status_code.to_string(), + )); + if !sensitive { + span.add_event( + "response", + vec![KeyValue::new( + "body", + result.body.chars().take(500).collect::(), + )], + ) + } + + Ok(result) } -fn get_otel_headers() -> (HeaderMap, String) { - - let tracer = global::tracer("prodzilla_tracer"); - let span = tracer.start("prodzilla_call"); +fn get_otel_headers(span_name: String) -> (HeaderMap, Context, SpanId, TraceId) { + let span = global::tracer("http_probe").start(span_name); + let span_id = span.span_context().span_id(); + let trace_id = span.span_context().trace_id(); let cx = Context::current_with_span(span); let mut headers = HeaderMap::new(); @@ -62,23 +98,14 @@ fn get_otel_headers() -> (HeaderMap, String) { propagator.inject_context(&cx, &mut opentelemetry_http::HeaderInjector(&mut headers)); }); - let trace_id = cx.span().span_context().trace_id().to_string(); - - (headers, trace_id) -} - -// Needs to be called to enable trace ids -pub fn init_otel_tracing() { - let provider = TracerProvider::default(); - global::set_tracer_provider(provider); - global::set_text_map_propagator(TraceContextPropagator::new()); + (headers, cx, span_id, trace_id) } fn build_request( - http_method: &String, + http_method: &str, url: &String, input_parameters: &Option, - otel_headers: HeaderMap + otel_headers: HeaderMap, ) -> Result> { let method = reqwest::Method::from_str(http_method).map_to_send_err()?; @@ -96,22 +123,25 @@ fn build_request( } } - return Ok(request); + Ok(request) } #[cfg(test)] mod http_tests { + use std::env; use std::time::Duration; + use crate::otel; use crate::probe::expectations::validate_response; - use crate::probe::http_probe::{call_endpoint, init_otel_tracing}; - use crate::test_utils::test_utils::{ - probe_get_with_expected_status, probe_post_with_expected_body, + use crate::probe::http_probe::call_endpoint; + use crate::test_utils::probe_test_utils::{ + probe_get_with_expected_status, probe_get_with_timeout_and_expected_status, + probe_post_with_expected_body, }; use reqwest::StatusCode; - use wiremock::matchers::{body_string, header_exists, header_regex, method, path}; + use wiremock::matchers::{body_string, header_exists, method, path}; use wiremock::{Mock, MockServer, ResponseTemplate}; // Note: These tests are a bit odd because they have been updated since a refactor @@ -131,13 +161,17 @@ mod http_tests { format!("{}/test", mock_server.uri()), "".to_owned(), ); - let endpoint_result = call_endpoint(&probe.http_method, &probe.url, &probe.with) + let endpoint_result = call_endpoint(&probe.http_method, &probe.url, &probe.with, false) .await .unwrap(); - let check_expectations_result = - validate_response(&probe.name, &endpoint_result, &probe.expectations); + let check_expectations_result = validate_response( + &probe.name, + endpoint_result.status_code, + endpoint_result.body, + &probe.expectations, + ); - assert_eq!(check_expectations_result, true); + assert!(check_expectations_result.is_ok()); } #[tokio::test] @@ -157,7 +191,32 @@ mod http_tests { format!("{}/test", mock_server.uri()), body.to_string(), ); - let endpoint_result = call_endpoint(&probe.http_method, &probe.url, &probe.with).await; + let endpoint_result = + call_endpoint(&probe.http_method, &probe.url, &probe.with, false).await; + + assert!(endpoint_result.is_err()); + } + + #[tokio::test] + async fn test_request_timeout_configuration() { + let mock_server = MockServer::start().await; + + let body = "test body"; + + Mock::given(method("GET")) + .and(path("/five_second_response")) + .respond_with(ResponseTemplate::new(404).set_delay(Duration::from_secs(5))) + .mount(&mock_server) + .await; + + let probe = probe_get_with_timeout_and_expected_status( + StatusCode::NOT_FOUND, + format!("{}/five_second_response", mock_server.uri()), + body.to_string(), + Some(1), // Timeout is 1 second, reduced from default of 10 + ); + let endpoint_result = + call_endpoint(&probe.http_method, &probe.url, &probe.with, false).await; assert!(endpoint_result.is_err()); } @@ -180,18 +239,24 @@ mod http_tests { format!("{}/test", mock_server.uri()), body.to_string(), ); - let endpoint_result = call_endpoint(&probe.http_method, &probe.url, &probe.with) + let endpoint_result = call_endpoint(&probe.http_method, &probe.url, &probe.with, false) .await .unwrap(); - let check_expectations_result = - validate_response(&probe.name, &endpoint_result, &probe.expectations); + let check_expectations_result = validate_response( + &probe.name, + endpoint_result.status_code, + endpoint_result.body, + &probe.expectations, + ); - assert_eq!(check_expectations_result, true); + assert!(check_expectations_result.is_ok()); } #[tokio::test] async fn test_requests_post_200_with_body() { - init_otel_tracing(); + // necessary for trace propagation + env::set_var("OTEL_TRACES_EXPORTER", "otlp"); + otel::tracing::create_tracer(); let mock_server = MockServer::start().await; let request_body = "request body"; @@ -211,12 +276,16 @@ mod http_tests { format!("{}/test", mock_server.uri()), request_body.to_owned(), ); - let endpoint_result = call_endpoint(&probe.http_method, &probe.url, &probe.with) + let endpoint_result = call_endpoint(&probe.http_method, &probe.url, &probe.with, false) .await .unwrap(); - let check_expectations_result = - validate_response(&probe.name, &endpoint_result, &probe.expectations); + let check_expectations_result = validate_response( + &probe.name, + endpoint_result.status_code, + endpoint_result.body, + &probe.expectations, + ); - assert_eq!(check_expectations_result, true); + assert!(check_expectations_result.is_ok()); } } diff --git a/src/probe/mod.rs b/src/probe/mod.rs index 15b37e5..cf28377 100644 --- a/src/probe/mod.rs +++ b/src/probe/mod.rs @@ -1,6 +1,6 @@ pub(crate) mod expectations; pub(crate) mod http_probe; pub(crate) mod model; -pub(crate) mod schedule; pub(crate) mod probe_logic; -pub(crate) mod variables; \ No newline at end of file +pub(crate) mod schedule; +pub(crate) mod variables; diff --git a/src/probe/model.rs b/src/probe/model.rs index e37aee8..965173d 100644 --- a/src/probe/model.rs +++ b/src/probe/model.rs @@ -1,4 +1,5 @@ use chrono::{DateTime, Utc}; + use serde::{Deserialize, Serialize}; use std::collections::HashMap; @@ -11,6 +12,8 @@ pub struct Probe { pub expectations: Option>, pub schedule: ProbeScheduleParameters, pub alerts: Option>, + #[serde(default)] // default to false + pub sensitive: bool, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -18,6 +21,7 @@ pub struct ProbeInputParameters { #[serde(default)] pub headers: Option>, pub body: Option, + pub timeout_seconds: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -32,6 +36,7 @@ pub enum ExpectOperation { Equals, IsOneOf, Contains, + Matches, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -57,6 +62,8 @@ pub struct ProbeResult { pub timestamp_started: DateTime, pub success: bool, #[serde(skip_serializing_if = "Option::is_none")] + pub error_message: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub response: Option, #[serde(skip_serializing_if = "Option::is_none")] pub trace_id: Option, @@ -69,8 +76,14 @@ pub struct ProbeResponse { pub timestamp_received: DateTime, pub status_code: u32, pub body: String, + pub sensitive: bool, } +impl ProbeResponse { + pub fn truncated_body(&self, n: usize) -> String { + self.body.chars().take(n).collect() + } +} #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Story { @@ -87,6 +100,8 @@ pub struct Step { pub http_method: String, pub with: Option, pub expectations: Option>, + #[serde(default)] // default to false + pub sensitive: bool, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -103,9 +118,13 @@ pub struct StepResult { pub timestamp_started: DateTime, pub success: bool, #[serde(skip_serializing_if = "Option::is_none")] + pub error_message: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub response: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub trace_id: Option + pub trace_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub span_id: Option, } pub struct EndpointResult { @@ -114,14 +133,17 @@ pub struct EndpointResult { pub status_code: u32, pub body: String, pub trace_id: String, + pub span_id: String, + pub sensitive: bool, } impl EndpointResult { pub fn to_probe_response(&self) -> ProbeResponse { - return ProbeResponse { + ProbeResponse { timestamp_received: self.timestamp_response_received, status_code: self.status_code, body: self.body.clone(), + sensitive: self.sensitive, } } -} \ No newline at end of file +} diff --git a/src/probe/probe_logic.rs b/src/probe/probe_logic.rs index 687b547..4d5f548 100644 --- a/src/probe/probe_logic.rs +++ b/src/probe/probe_logic.rs @@ -1,6 +1,15 @@ use std::sync::Arc; use chrono::Utc; +use opentelemetry::global; +use opentelemetry::trace; +use opentelemetry::trace::FutureExt; +use opentelemetry::trace::Status; +use opentelemetry::trace::TraceContextExt; +use opentelemetry::trace::Tracer; +use opentelemetry::Context; +use opentelemetry::KeyValue; +use opentelemetry_semantic_conventions as semconv; use tracing::error; use tracing::info; @@ -26,6 +35,12 @@ pub trait Monitorable { fn get_schedule(&self) -> &ProbeScheduleParameters; } +fn time_since(timestamp: &chrono::DateTime) -> u64 { + Utc::now() + .signed_duration_since(*timestamp) + .num_milliseconds() as u64 +} + // TODOs here: Step / Probe can be the same object // The timestamps are a little disorganised // Reduce nested code @@ -33,139 +48,261 @@ pub trait Monitorable { impl Monitorable for Story { async fn probe_and_store_result(&self, app_state: Arc) { + let story_attributes = [ + KeyValue::new("name", self.name.clone()), + KeyValue::new("type", "story"), + ]; + app_state.metrics.runs.add(1, &story_attributes); let mut story_variables = StoryVariables::new(); let mut step_results: Vec = vec![]; let timestamp_started = Utc::now(); + let tracer = global::tracer("probe_logic"); + let root_span = tracer.start(self.name.clone()); + let root_cx = Context::default().with_span(root_span); for step in &self.steps { - + let step_started = Utc::now(); + let step_tags = [ + KeyValue::new("name", step.name.clone()), + KeyValue::new("story_name", self.name.clone()), + KeyValue::new("type", "step"), + ]; + app_state.metrics.runs.add(1, &step_tags); + let step_span = tracer.start_with_context(step.name.clone(), &root_cx); + let step_cx = root_cx.with_span(step_span); + let url = substitute_variables(&step.url, &story_variables); let input_parameters = substitute_input_parameters(&step.with, &story_variables); let call_endpoint_result = - call_endpoint(&step.http_method, &url, &input_parameters).await; + call_endpoint(&step.http_method, &url, &input_parameters, step.sensitive) + .with_context(step_cx.clone()) + .await; match call_endpoint_result { Ok(endpoint_result) => { - let expectations_result = - validate_response(&step.name, &endpoint_result, &step.expectations); - + let probe_response = endpoint_result.to_probe_response(); + let span = step_cx.span(); + span.set_attribute(opentelemetry::KeyValue::new( + semconv::trace::HTTP_RESPONSE_STATUS_CODE, + endpoint_result.status_code.to_string(), + )); + let expectations_result = validate_response( + &step.name, + endpoint_result.status_code, + endpoint_result.body, + &step.expectations, + ); + if let Err(err) = expectations_result.as_ref() { + span.record_error(&err); + span.set_status(Status::Error { + description: "Expectation failed".into(), + }); + app_state + .metrics + .duration + .record(time_since(&step_started), &step_tags); + app_state.metrics.errors.add(1, &step_tags); + } let step_result = StepResult { step_name: step.name.clone(), timestamp_started: endpoint_result.timestamp_request_started, - success: expectations_result, - response: Some(endpoint_result.to_probe_response()), - trace_id: Some(endpoint_result.trace_id) + success: expectations_result.is_ok(), + error_message: expectations_result.as_ref().err().map(|e| e.to_string()), + response: Some(probe_response), + trace_id: Some(endpoint_result.trace_id), + span_id: Some(endpoint_result.span_id), }; step_results.push(step_result); - if !expectations_result { + if expectations_result.is_err() { break; } - let step_variables = StepVariables{ - response_body: step_results.last().unwrap().response.clone().unwrap().body + // Add 0 to ensure this is exported with value 0, so e.g. rate + // queries in promql don't miss the step from 0 -> 1 + app_state.metrics.errors.add(0, &step_tags); + step_cx.span().set_status(Status::Ok); + let step_variables = StepVariables { + response_body: step_results.last().unwrap().response.clone().unwrap().body, }; - story_variables.steps.insert(step.name.clone(), step_variables); + story_variables + .steps + .insert(step.name.clone(), step_variables); + app_state + .metrics + .duration + .record(time_since(×tamp_started), &step_tags); } Err(e) => { error!("Error calling endpoint: {}", e); + trace::get_active_span(|span| { + span.record_error(&*e); + }); step_results.push(StepResult { step_name: step.name.clone(), success: false, + error_message: Some(e.to_string()), timestamp_started: Utc::now(), response: None, - trace_id: None + trace_id: None, + span_id: None, }); + app_state + .metrics + .duration + .record(time_since(×tamp_started), &step_tags); break; } }; } - - let story_success = step_results.last().unwrap().success; - - let story_result = StoryResult { - story_name: self.name.clone(), - timestamp_started: timestamp_started, - success: story_success, - step_results: step_results, - }; - - app_state.add_story_result(self.name.clone(), story_result); + let last_step = step_results.last().unwrap(); + let story_success = last_step.success; + if !story_success { + app_state.metrics.errors.add(1, &story_attributes); + } else { + app_state.metrics.errors.add(0, &story_attributes); + } + app_state + .metrics + .duration + .record(time_since(×tamp_started), &story_attributes); info!( "Finished scheduled story {}, success: {}", &self.name, story_success ); - let send_alert_result = - alert_if_failure(story_success, &self.name, timestamp_started, &self.alerts).await; + let send_alert_result = alert_if_failure( + story_success, + last_step.error_message.as_deref(), + last_step.response.as_ref(), + &self.name, + timestamp_started, + &self.alerts, + &last_step.trace_id, + ) + .await; if let Err(e) = send_alert_result { - error!("Error sending out alert: {}", e); + for error in e { + error!("Error sending out alert: {}", error); + } } + let story_result = StoryResult { + story_name: self.name.clone(), + timestamp_started, + success: story_success, + step_results, + }; + + app_state.add_story_result(self.name.clone(), story_result); } fn get_name(&self) -> String { - return self.name.clone(); + self.name.clone() } fn get_schedule(&self) -> &ProbeScheduleParameters { - return &self.schedule; + &self.schedule } } impl Monitorable for Probe { async fn probe_and_store_result(&self, app_state: Arc) { - let call_endpoint_result = call_endpoint(&self.http_method, &self.url, &self.with).await; + let probe_attributes = [ + KeyValue::new("name", self.name.clone()), + KeyValue::new("type", "probe"), + ]; + app_state.metrics.runs.add(1, &probe_attributes); + + let root_span = global::tracer("probe_logic").start(self.name.clone()); - let probe_result; + let root_cx = Context::default().with_span(root_span); + let call_endpoint_result = + call_endpoint(&self.http_method, &self.url, &self.with, self.sensitive) + .with_context(root_cx.clone()) + .await; - match call_endpoint_result { + let probe_result = match call_endpoint_result { Ok(endpoint_result) => { - let expectations_result = - validate_response(&self.name, &endpoint_result, &self.expectations); + let probe_response = endpoint_result.to_probe_response(); + let expectations_result = validate_response( + &self.name, + endpoint_result.status_code, + endpoint_result.body, + &self.expectations, + ); + + if let Err(err) = expectations_result.as_ref() { + root_cx.span().record_error(&err); + } - probe_result = ProbeResult { + ProbeResult { probe_name: self.name.clone(), timestamp_started: endpoint_result.timestamp_request_started, - success: expectations_result, - response: Some(endpoint_result.to_probe_response()), - trace_id: Some(endpoint_result.trace_id) - }; + success: expectations_result.is_ok(), + error_message: expectations_result.err().map(|e| e.to_string()), + response: Some(probe_response), + trace_id: Some(endpoint_result.trace_id), + } } Err(e) => { error!("Error calling endpoint: {}", e); - probe_result = ProbeResult { + root_cx.span().record_error(&*e); + ProbeResult { + success: false, probe_name: self.name.clone(), timestamp_started: Utc::now(), - success: false, + error_message: Some(e.to_string()), response: None, - trace_id: None - }; + trace_id: None, + } } }; - let success = probe_result.success; + if probe_result.success { + app_state.metrics.errors.add(0, &probe_attributes); + root_cx.span().set_status(Status::Ok); + } else { + app_state.metrics.errors.add(1, &probe_attributes); + root_cx.span().set_status(Status::Error { + description: "Expectation failed".into(), + }); + } let timestamp = probe_result.timestamp_started; - app_state.add_probe_result(self.name.clone(), probe_result); + app_state + .metrics + .duration + .record(time_since(×tamp), &probe_attributes); info!( "Finished scheduled probe {}, success: {}", - &self.name, success + &self.name, probe_result.success, ); - let send_alert_result = - alert_if_failure(success, &self.name, timestamp, &self.alerts).await; + let send_alert_result = alert_if_failure( + probe_result.success, + probe_result.error_message.as_deref(), + probe_result.response.as_ref(), + &self.name, + timestamp, + &self.alerts, + &probe_result.trace_id, + ) + .await; if let Err(e) = send_alert_result { - error!("Error sending out alert: {}", e); + for error in e { + error!("Error sending out alert: {}", error); + } } + app_state.add_probe_result(self.name.clone(), probe_result); } fn get_name(&self) -> String { - return self.name.clone(); + self.name.clone() } fn get_schedule(&self) -> &ProbeScheduleParameters { - return &self.schedule; + &self.schedule } } @@ -177,7 +314,10 @@ mod probe_logic_tests { use crate::app_state::AppState; use crate::config::Config; - use crate::probe::model::{ExpectField, ExpectOperation, ProbeAlert, ProbeExpectation, ProbeInputParameters, ProbeScheduleParameters, Step, Story}; + use crate::probe::model::{ + ExpectField, ExpectOperation, ProbeAlert, ProbeExpectation, ProbeInputParameters, + ProbeScheduleParameters, Step, Story, + }; use crate::probe::probe_logic::Monitorable; use wiremock::matchers::{header, method, path}; use wiremock::{Mock, MockServer, ResponseTemplate}; @@ -188,7 +328,10 @@ mod probe_logic_tests { let step1_path = "/test1"; let step2_path = "/test2"; let story_name = "User Flow"; - let app_state = Arc::new(AppState::new(Config{probes: vec![], stories: vec![]})); + let app_state = Arc::new(AppState::new(Config { + probes: vec![], + stories: vec![], + })); Mock::given(method("GET")) .and(path(step1_path)) @@ -213,6 +356,7 @@ mod probe_logic_tests { with: None, http_method: "GET".to_owned(), expectations: None, + sensitive: false, }, Step { name: "Step 2".to_owned(), @@ -220,6 +364,7 @@ mod probe_logic_tests { with: None, http_method: "GET".to_owned(), expectations: None, + sensitive: false, }, ], schedule: ProbeScheduleParameters { @@ -235,7 +380,7 @@ mod probe_logic_tests { let results = &story_result_map[story_name]; assert_eq!(1, results.len()); let story_result = &results[0]; - assert_eq!(true, story_result.success); + assert!(story_result.success); assert_eq!(2, story_result.step_results.len()); } @@ -246,7 +391,10 @@ mod probe_logic_tests { let step2_path = "/test2"; let alert_path = "/alert-test"; let story_name = "User Flow"; - let app_state = Arc::new(AppState::new(Config{probes: vec![], stories: vec![]})); + let app_state = Arc::new(AppState::new(Config { + probes: vec![], + stories: vec![], + })); Mock::given(method("GET")) .and(path(step1_path)) @@ -271,21 +419,19 @@ mod probe_logic_tests { with: None, http_method: "GET".to_owned(), expectations: None, + sensitive: false, }, Step { name: "Step 2".to_owned(), url: format!("{}{}", mock_server.uri(), step2_path.to_owned()), with: None, http_method: "GET".to_owned(), - expectations: Some( - vec![ - ProbeExpectation{ - field: ExpectField::StatusCode, - operation: ExpectOperation::Equals, - value: "200".to_owned(), - } - ] - ), + expectations: Some(vec![ProbeExpectation { + field: ExpectField::StatusCode, + operation: ExpectOperation::Equals, + value: "200".to_owned(), + }]), + sensitive: false, }, ], schedule: ProbeScheduleParameters { @@ -303,9 +449,8 @@ mod probe_logic_tests { let results = &story_result_map[story_name]; assert_eq!(1, results.len()); let story_result = &results[0]; - assert_eq!(false, story_result.success); + assert!(!story_result.success); assert_eq!(2, story_result.step_results.len()); - } #[tokio::test] @@ -319,14 +464,17 @@ mod probe_logic_tests { let step2_path = "/${{steps.step1.response.body.path}}/test2"; let step2_constructed_path = "/value/test2"; - let step2_headers = HashMap::from([ - ("Authorization".to_owned(), "Bearer ${{steps.step1.response.body.token}}".to_owned()) - ]); + let step2_headers = HashMap::from([( + "Authorization".to_owned(), + "Bearer ${{steps.step1.response.body.token}}".to_owned(), + )]); let step2_body_str = r#"{"uuid": "${{generate.uuid}}"}"#; - let story_name = "User Flow"; - let app_state = Arc::new(AppState::new(Config{probes: vec![], stories: vec![]})); + let app_state = Arc::new(AppState::new(Config { + probes: vec![], + stories: vec![], + })); Mock::given(method("GET")) .and(path(step1_path)) @@ -335,7 +483,7 @@ mod probe_logic_tests { .mount(&mock_server) .await; - Mock::given(method("POST")) + Mock::given(method("POST")) .and(path(step2_constructed_path)) .and(header("Authorization", "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c")) .respond_with(ResponseTemplate::new(200)) @@ -352,24 +500,23 @@ mod probe_logic_tests { with: None, http_method: "GET".to_owned(), expectations: None, + sensitive: false, }, Step { name: "Step 2".to_owned(), url: format!("{}{}", mock_server.uri(), step2_path.to_owned()), - with: Some(ProbeInputParameters{ + with: Some(ProbeInputParameters { headers: Some(step2_headers), body: Some(step2_body_str.to_owned()), + timeout_seconds: None, }), http_method: "POST".to_owned(), - expectations: Some( - vec![ - ProbeExpectation{ - field: ExpectField::StatusCode, - operation: ExpectOperation::Equals, - value: "200".to_owned(), - } - ] - ), + expectations: Some(vec![ProbeExpectation { + field: ExpectField::StatusCode, + operation: ExpectOperation::Equals, + value: "200".to_owned(), + }]), + sensitive: false, }, ], schedule: ProbeScheduleParameters { @@ -385,8 +532,7 @@ mod probe_logic_tests { let results = &story_result_map[story_name]; assert_eq!(1, results.len()); let story_result = &results[0]; - assert_eq!(true, story_result.success); + assert!(story_result.success); assert_eq!(2, story_result.step_results.len()); } - } diff --git a/src/probe/schedule.rs b/src/probe/schedule.rs index cd329ea..2eea3e9 100644 --- a/src/probe/schedule.rs +++ b/src/probe/schedule.rs @@ -50,14 +50,12 @@ pub async fn probing_loop(monitorable: &T, app_state: Arc + pub steps: HashMap, } impl StoryVariables { pub fn new() -> StoryVariables { - return StoryVariables { - steps: HashMap::new() - }; + StoryVariables { + steps: HashMap::new(), + } } } pub struct StepVariables { - pub response_body: String + pub response_body: String, } lazy_static! { static ref SUB_REGEX: Regex = Regex::new(r"\$\{\{(.*?)\}\}").unwrap(); } -pub fn substitute_input_parameters(input_parameters: &Option, variables: &StoryVariables) -> Option { - input_parameters.as_ref().map(|input| { - ProbeInputParameters { - body: input.body.as_ref().map(|body| substitute_variables(body, variables)), - headers: input.headers.as_ref().map(|headers| substitute_variables_in_headers(headers, variables)), - } +pub fn substitute_input_parameters( + input_parameters: &Option, + variables: &StoryVariables, +) -> Option { + input_parameters.as_ref().map(|input| ProbeInputParameters { + body: input + .body + .as_ref() + .map(|body| substitute_variables(body, variables)), + headers: input + .headers + .as_ref() + .map(|headers| substitute_variables_in_headers(headers, variables)), + timeout_seconds: input.timeout_seconds, }) } -pub fn substitute_variables_in_headers(headers: &HashMap, variables: &StoryVariables) -> HashMap { - headers.iter().map(|(key, value)| { - let substituted_key = substitute_variables(key, variables); - let substituted_value = substitute_variables(value, variables); - (substituted_key, substituted_value) - }).collect() +pub fn substitute_variables_in_headers( + headers: &HashMap, + variables: &StoryVariables, +) -> HashMap { + headers + .iter() + .map(|(key, value)| { + let substituted_key = substitute_variables(key, variables); + let substituted_value = substitute_variables(value, variables); + (substituted_key, substituted_value) + }) + .collect() } // This could return an error in future - for now it fills an empty string -pub fn substitute_variables(content: &String, variables: &StoryVariables) -> String { - SUB_REGEX.replace_all(content, |caps: ®ex::Captures| { - let placeholder = &caps[1]; - let parts: Vec<&str> = placeholder.split('.').collect(); - - match parts[0] { - "steps" => substitute_step_value(&parts[1..], variables), - "generate" => get_generated_value(parts.get(1)), - _ => "".to_string(), - } - }).to_string() +pub fn substitute_variables(content: &str, variables: &StoryVariables) -> String { + SUB_REGEX + .replace_all(content, |caps: ®ex::Captures| { + let placeholder = &caps[1].trim(); + let parts: Vec<&str> = placeholder.split('.').collect(); + + match parts[0] { + "steps" => substitute_step_value(&parts[1..], variables), + "generate" => get_generated_value(parts.get(1)), + _ => "".to_string(), + } + }) + .to_string() } fn get_generated_value(type_to_generate: Option<&&str>) -> String { @@ -76,7 +92,7 @@ fn substitute_step_value(parts: &[&str], variables: &StoryVariables) -> String { } else { step.response_body.clone() } - }, + } None => { error!("Error: Step name '{}' not found.", step_name); "".to_string() @@ -90,8 +106,8 @@ fn get_nested_json_value(parts: &[&str], json_string: &String) -> String { Ok(val) => val, Err(_) => { error!("Error parsing json response: {}", json_string); - return "".to_string() - }, + return "".to_string(); + } }; let mut current_value = &json_value; @@ -100,8 +116,8 @@ fn get_nested_json_value(parts: &[&str], json_string: &String) -> String { Some(value) => value, None => { error!("Error finding value in json payload: {}", part); - return "".to_string() - }, + return "".to_string(); + } }; } @@ -121,7 +137,8 @@ async fn test_substitute_several_variables() { entire_body: ${{steps.get-token.response.body}} token: ${{steps.get-token.response.body.token}} uuid: "${{generate.uuid}}" - "#.to_owned(); + "# + .to_owned(); let body_str = r#"{ "token": "12345", @@ -129,11 +146,12 @@ async fn test_substitute_several_variables() { }"#; let variables = StoryVariables { - steps: HashMap::from([ - ("get-token".to_string(), StepVariables{ - response_body: body_str.to_string() - }) - ]) + steps: HashMap::from([( + "get-token".to_string(), + StepVariables { + response_body: body_str.to_string(), + }, + )]), }; let result = substitute_variables(&content, &variables); @@ -149,24 +167,28 @@ async fn test_substitute_input_parameters() { }"#; let variables = StoryVariables { - steps: HashMap::from([ - ("get-token".to_string(), StepVariables{ - response_body: body_str.to_string() - }) - ]) + steps: HashMap::from([( + "get-token".to_string(), + StepVariables { + response_body: body_str.to_string(), + }, + )]), }; let input_parameters = Some(ProbeInputParameters { body: Some("entire_body: ${{steps.get-token.response.body}}".to_owned()), - headers: Some(HashMap::from( - [ - ("Authorization".to_owned(), "Bearer ${{steps.get-token.response.body.token}}".to_owned()) - ] - )) + headers: Some(HashMap::from([( + "Authorization".to_owned(), + "Bearer ${{steps.get-token.response.body.token}}".to_owned(), + )])), + timeout_seconds: None, }); let result = substitute_input_parameters(&input_parameters, &variables); - assert_eq!("Bearer 12345", result.unwrap().headers.unwrap()["Authorization"]); + assert_eq!( + "Bearer 12345", + result.unwrap().headers.unwrap()["Authorization"] + ); } #[tokio::test] @@ -185,11 +207,12 @@ async fn test_substitute_variable_doesnt_exist_in_json() { }"#; let variables = StoryVariables { - steps: HashMap::from([ - ("get-token".to_string(), StepVariables{ - response_body: body_str.to_string() - }) - ]) + steps: HashMap::from([( + "get-token".to_string(), + StepVariables { + response_body: body_str.to_string(), + }, + )]), }; let result = substitute_variables(&content, &variables); @@ -201,11 +224,11 @@ async fn test_substitute_variable_step_doesnt_exist() { let content = r#"field: ${{steps.get-token.response.body.invalid}}"#.to_owned(); let variables = StoryVariables { - steps: HashMap::new() + steps: HashMap::new(), }; let result = substitute_variables(&content, &variables); assert_eq!("field: ".to_owned(), result); } -// TODO test what happens with spaces in the ${{ steps.etc }} \ No newline at end of file +// TODO test what happens with spaces in the ${{ steps.etc }} diff --git a/src/test_utils.rs b/src/test_utils.rs index d748f4a..1b2d084 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -1,5 +1,5 @@ #[cfg(test)] -pub mod test_utils { +pub mod probe_test_utils { use std::collections::HashMap; use reqwest::StatusCode; @@ -9,18 +9,48 @@ pub mod test_utils { ProbeScheduleParameters, }; + pub fn probe_get_with_timeout_and_expected_status( + status_code: StatusCode, + url: String, + body: String, + timeout_seconds: Option, + ) -> Probe { + Probe { + name: "Test probe".to_string(), + url, + http_method: "GET".to_string(), + with: Some(ProbeInputParameters { + body: Some(body), + headers: Some(HashMap::new()), + timeout_seconds, + }), + expectations: Some(vec![ProbeExpectation { + field: ExpectField::StatusCode, + operation: ExpectOperation::Equals, + value: status_code.as_str().into(), + }]), + schedule: ProbeScheduleParameters { + initial_delay: 0, + interval: 0, + }, + alerts: None, + sensitive: false, + } + } + pub fn probe_get_with_expected_status( status_code: StatusCode, url: String, body: String, ) -> Probe { - return Probe { + Probe { name: "Test probe".to_string(), - url: url, + url, http_method: "GET".to_string(), with: Some(ProbeInputParameters { body: Some(body), headers: Some(HashMap::new()), + timeout_seconds: None, }), expectations: Some(vec![ProbeExpectation { field: ExpectField::StatusCode, @@ -32,7 +62,8 @@ pub mod test_utils { interval: 0, }, alerts: None, - }; + sensitive: false, + } } pub fn probe_get_with_expected_status_and_alert( @@ -41,13 +72,14 @@ pub mod test_utils { body: String, alert_url: String, ) -> Probe { - return Probe { + Probe { name: "Test probe".to_string(), - url: url, + url, http_method: "GET".to_string(), with: Some(ProbeInputParameters { body: Some(body), headers: Some(HashMap::new()), + timeout_seconds: None, }), expectations: Some(vec![ProbeExpectation { field: ExpectField::StatusCode, @@ -59,7 +91,8 @@ pub mod test_utils { interval: 0, }, alerts: Some(vec![ProbeAlert { url: alert_url }]), - }; + sensitive: false, + } } pub fn probe_post_with_expected_body( @@ -67,13 +100,14 @@ pub mod test_utils { url: String, body: String, ) -> Probe { - return Probe { + Probe { name: "Test probe".to_string(), - url: url, + url, http_method: "POST".to_string(), with: Some(ProbeInputParameters { body: Some(body), headers: Some(HashMap::new()), + timeout_seconds: None, }), expectations: Some(vec![ ProbeExpectation { @@ -92,6 +126,7 @@ pub mod test_utils { interval: 0, }, alerts: None, - }; + sensitive: false, + } } } diff --git a/src/web_server/model.rs b/src/web_server/model.rs index 67d14e0..a7c98d4 100644 --- a/src/web_server/model.rs +++ b/src/web_server/model.rs @@ -11,4 +11,4 @@ pub struct ProbeResponse { pub name: String, pub status: String, pub last_probed: DateTime, -} \ No newline at end of file +} diff --git a/src/web_server/probes.rs b/src/web_server/probes.rs index e68ed44..ac93872 100644 --- a/src/web_server/probes.rs +++ b/src/web_server/probes.rs @@ -5,7 +5,10 @@ use axum::{ use std::sync::Arc; use tracing::debug; -use crate::{app_state::AppState, probe::{model::ProbeResult, probe_logic::Monitorable}}; +use crate::{ + app_state::AppState, + probe::{model::ProbeResult, probe_logic::Monitorable}, +}; use super::model::{ProbeQueryParams, ProbeResponse}; @@ -29,7 +32,7 @@ pub async fn get_probe_results( } } - return Json(cloned_results); + Json(cloned_results) } pub async fn probes(Extension(state): Extension>) -> Json> { @@ -50,11 +53,13 @@ pub async fn probes(Extension(state): Extension>) -> Json, -Extension(state): Extension>) -> Json { +pub async fn probe_trigger( + Path(name): Path, + Extension(state): Extension>, +) -> Json { debug!("Probe trigger called"); let probe = &state.config.probes.iter().find(|x| x.name == name).unwrap(); diff --git a/src/web_server/stories.rs b/src/web_server/stories.rs index 3465f0b..62220e9 100644 --- a/src/web_server/stories.rs +++ b/src/web_server/stories.rs @@ -5,7 +5,10 @@ use axum::{ use std::sync::Arc; use tracing::debug; -use crate::{app_state::AppState, probe::{model::StoryResult, probe_logic::Monitorable}}; +use crate::{ + app_state::AppState, + probe::{model::StoryResult, probe_logic::Monitorable}, +}; use super::model::{ProbeQueryParams, ProbeResponse}; @@ -33,7 +36,7 @@ pub async fn get_story_results( } } - return Json(cloned_results); + Json(cloned_results) } pub async fn stories(Extension(state): Extension>) -> Json> { @@ -54,7 +57,7 @@ pub async fn stories(Extension(state): Extension>) -> Json Json { debug!("Story trigger called"); - let story = &state.config.stories.iter().find(|x| x.name == name).unwrap(); + let story = &state + .config + .stories + .iter() + .find(|x| x.name == name) + .unwrap(); story.probe_and_store_result(state.clone()).await;