diff --git a/.gitignore b/.gitignore index b630a4c..45cc071 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ # will have compiled files and executables debug/ target/ +.direnv # 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 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..2b032e0 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,16 @@ +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v3.1.0 + hooks: + - id: check-byte-order-marker + - id: check-case-conflict + - id: check-merge-conflict + - id: check-symlinks + - id: check-yaml + - id: end-of-file-fixer + - id: mixed-line-ending + - id: trailing-whitespace +- repo: https://github.com/pre-commit/pre-commit + rev: v2.5.1 + hooks: + - id: validate_manifest diff --git a/CHANGELOG.md b/CHANGELOG.md index ac02311..33c7bc0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,15 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] +### upcoming changes. + ## [efected-cotoemory] + +## v1.0.0 added nix module
added Actix Web Framework
changed the app: used cargo-generate to create boilerplate. +add fission-codes/rust-template. diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 278b1f2..31be868 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -1,3 +1,108 @@ # Code of Conduct -This project adheres to the Rust Code of Conduct, which can be found [here](https://www.rust-lang.org/conduct.html). +**TL;DR Be kind, inclusive, and considerate.** + +In the interest of fostering an open, inclusive, and welcoming environment, all +members, contributors, and maintainers interacting within our online community +(including Discord, Discourse, etc.), on affiliated projects and repositories +(including issues, pull requests, and discussions on Github), and/or involved +with associated events pledge to accept and observe the following Code of +Conduct. + +As members, contributors, and maintainers, we pledge to make participation in +our projects and community a harassment-free experience, ensuring a safe +environment for all, regardless of background, gender, gender identity and +expression, age, sexual orientation, disability, physical appearance, body size, +race, ethnicity, religion (or lack thereof), or any other dimension of +diversity. + +Sexual language and imagery will not be accepted in any way. Be kind to others. +Do not insult or put down people within the community. Behave professionally. +Remember that harassment and sexist, racist, or exclusionary jokes are not +appropriate in any form. Participants violating these rules may be sanctioned or +expelled from the community and related projects. + +## Spelling it out. + +Harassment includes offensive verbal comments or actions related to or involving + +- background +- gender +- gender identity and expression +- age +- sexual orientation +- disability +- physical appearance +- body size +- race +- ethnicity +- religion (or lack thereof) +- economic status +- geographic location +- technology choices +- sexual imagery +- deliberate intimidation +- violence and threats of violence +- stalking +- doxing +- inappropriate or unwelcome physical contact in public spaces +- unwelcomed sexual attention +- influencing unacceptable behavior +- any other dimension of diversity + +## Our Responsibilities + +Maintainers of the community and associated projects are not only subject to the +anti-harassment policy, but also responsible for executing the policy, +moderating related forums, and for taking appropriate and fair corrective action +in response to any instances of unacceptable behavior that breach the policy. + +Maintainers have the right to remove and reject comments, threads, commits, +code, documentation, pull requests, issues, and contributions not aligned with +this Code of Conduct. + +## Scope + +This Code of Conduct applies within all project and community spaces, as well as +in any public spaces where an individual representing the community is involved. +This covers + +- Interactions on the Github repository, including discussions, issues, pull + requests, commits, and wikis +- Interactions on any affiliated Discord, Slack, IRC, or related online + communities and forums like Discourse, etc. +- Any official project emails and social media posts +- Individuals representing the community at public events like meetups, talks, + and presentations + +## Enforcement + +All instances of abusive, harassing, or otherwise unacceptable behavior should +be reported by contacting the project and community maintainers at +[alexeusgr@gmail.com][support-email]. All complaints will be reviewed and +investigated and will result in a response that is deemed necessary and +appropriate to the circumstances. + +Maintainers of the community and associated projects are obligated to maintain +confidentiality with regard to the reporter of an incident. Further details of +specific enforcement policies may be posted separately. + +Anyone asked to stop abusive, harassing, or otherwise unacceptable behavior are +expected to comply immediately and accept the response decided on by the +maintainers of the community and associated projects. + +## Need help? + +If you are experiencing harassment, witness an incident or have concerns about +content please contact us at [alexeusgr@gmail.com][support-email]. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant, v2.1][contributor-cov], +among other sources like [!!con’s Code of Conduct][!!con] and +[Mozilla’s Community Participation Guidelines][mozilla]. + +[!!con]: https://bangbangcon.com/conduct.html +[contributor-cov]: https://www.contributor-covenant.org/version/2/1/code_of_conduct/ +[mozilla]: https://www.mozilla.org/en-US/about/governance/policies/participation/ +[support-email]: mailto:alexeusgr@gmail.com diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9cc0177..fe8babc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,80 +1,152 @@ -# Contribution guidelines - -First off, thank you for considering contributing to efected-cotoemory. - -If your contribution is not straightforward, please first discuss the change you -wish to make by creating a new issue before making the change. - -## Reporting issues - -Before reporting an issue on the -[issue tracker](https://github.com/aleeusgr/efected-cotoemory/issues), -please check that it has not already been reported by searching for some related -keywords. - -## Pull requests - -Try to do one pull request per change. - -### Updating the changelog - -Update the changes you have made in -[CHANGELOG](https://github.com/aleeusgr/efected-cotoemory/blob/main/CHANGELOG.md) -file under the **Unreleased** section. - -Add the changes of your pull request to one of the following subsections, -depending on the types of changes defined by -[Keep a changelog](https://keepachangelog.com/en/1.0.0/): - -- `Added` for new features. -- `Changed` for changes in existing functionality. -- `Deprecated` for soon-to-be removed features. -- `Removed` for now removed features. -- `Fixed` for any bug fixes. -- `Security` in case of vulnerabilities. - -If the required subsection does not exist yet under **Unreleased**, create it! - -## Developing - -### Set up - -This is no different than other Rust projects. - -```shell -git clone https://github.com/aleeusgr/efected-cotoemory -cd efected-cotoemory -cargo test -``` - -### Useful Commands - -- Build and run release version: - - ```shell - cargo build --release && cargo run --release - ``` - -- Run Clippy: - - ```shell - cargo clippy --all-targets --all-features --workspace - ``` - -- Run all tests: - - ```shell - cargo test --all-features --workspace - ``` - -- Check to see if there are code formatting issues - - ```shell - cargo fmt --all -- --check - ``` - -- Format the code in the project - - ```shell - cargo fmt --all - ``` +# Contributing to efected-coto-emmory + +We welcome everyone to contribute what and where they can. Whether you are brand +new, just want to contribute a little bit, or want to contribute a lot there is +probably something you can help out with. Check out our +[good first issues][good-first-issues] label for in the issues tab to see a list +of issue that good for those new to the project. + +## Where to Get Help + +The main way to get help is by sending an email to [alexeusgr@gmail.com][support-email]. +Though, this guide should help you get started. It may be slightly lengthy, but it's +designed for those who are new so please don't let length intimidate you. + +## Code of Conduct + +Please be kind, inclusive, and considerate when interacting when interacting +with others and follow our [code of conduct](./CODE_OF_CONDUCT.md). + +## How to Contribute + +If the code adds a feature that is not already present in an issue, you can +create a new issue for the feature and add the pull request to it. If the code +adds a feature that is not already present in an issue, you can create a new +issue for the feature and add the pull request to it. + +### Contributing by Adding a Topic for Discussion + +#### Issues + +If you have found a bug and would like to report it or if you have a feature +that you feel we should add, then we'd love it if you opened an issue! ❤️ +Before you do, please search the other issues to avoid creating a duplicate +issue. + +To submit a new issue just hit the issue button and a choice between two +templates should appear. Then, follow along with the template you chose. If you +don't know how to fill in all parts of the template go ahead and skip those +parts. You can edit the issue later. + +#### Discussion + +If you have a new discussion you want to start but it isn't a bug or feature +add, then you can start a [GitHub discussion][gh-discussions]. Some examples of +what kinds of things that are good discussion topics can include, but are not +limited to the following: + +- Community announcements and/or asking the community for feedback +- Discussing a new release +- Asking questions, Q&A that isn't for sure a bug report + +### Contributing through Code + +In order to contribute through code follow the steps below. Note that you don't +need to be the best programmer to contribute. + + 1. **Pick a feature** you would like to add or a bug you would like to fix + - If you wish to contribute but what you want to fix/add is not already + covered in an existing issue, please open a new issue. + + 2. **Discuss** the issue with the rest of the community + - Before you write any code, it is recommended that you discuss your + intention to write the code on the issue you are attempting to edit. + - This helps to stop you from wasting your time duplicating the work of + others that maybe working on the same issue; at the same time. + - This step also allows you to get helpful pointers on the community on some + problems they may have encountered on similar issues. + + 3. **Fork** the repository + - A fork creates a copy of the code on your Github, so you can work on it + separately from everyone else. + - You can learn more about forking [here][forking]. + + 4. Ensure that you have **commit signing** enabled + - This ensures that the code you submit was committed by you and not someone + else who claims to be you. + - You can learn more about how to setup commit signing [here][commit-signing]. + - If you have already made some commits that you wish to put in a pull + request without signing them, then you can follow [this guide][post-signing] + on how to fix that. + + 5. **Clone** the repository to your local computer + - This puts a copy of your fork on your computer so you can edit it + - You can learn more about cloning repositories [here][git-clone]. + + 6. **Build** the project + - For a detailed look on how to build efected-coto-emmory look at our + [README file](./README.md). + + 7. **Start writing** your code + - Open up your favorite code editor and make the changes that you wanted to + make to the repository. + - Make sure to test your code with the test command(s) found in our + [README file](./README.md). + + 8. **Write tests** for your code + - If you are adding a new feature, you should write tests that ensure that + if someone make changes to the code it cannot break your new feature + without breaking the test. + - If your code adds a new feature, you should also write at least one + documentation test. The documentation test's purpose is to demonstrate and + document how to use the API feature. + - If your code fixes a bug, you should write tests that ensure that if + someone makes code changes in the future the bug does not re-emerge + without breaking test. + - Please create integration tests, if the addition is large enough to + warrant them, and unit tests. + * Unit tests are tests that ensure the functionality of a single + function or small section of code. + * Integration tests test large large sections of code. + * Read more about the differences [here][unit-and-integration]. + - For more information on test organization, take a look [here][test-org]. + + 9. Ensure that the code that you made follows our Rust **coding guidelines** + - You can find a list of some Rust guidelines [here][rust-style-guide]. This + is a courtesy to the programmers that come after you. The easier your code + is to read, the easier it will be for the next person to make modifications. + - If you find it difficult to follow the guidelines or if the guidelines or + unclear, please reach out to us through our email linked above, or you + can just continue and leave a comment at the pull request stage. + + 10. **Commit and Push** your code + - This sends your changes to your repository branch. + - You can learn more about committing code [here][commiting-code] and + pushing it to a remote repository [here][push-remote]. + - We use conventional commits for the names and description of commits. + You can find out more about them [here][conventional-commits]. + + 11. The final step is to create **pull request** to our main branch 🎉 + - A pull request is how you merge the code you just worked so hard on with + the code everyone else has access to. + - Once you have submitted your pull request, we will review your code and + check to make sure the code implements the feature or fixes the bug. We + may leave some feedback and suggest edits. You can make the changes we + suggest by committing more code to your fork. + - You can learn more about pull requests [here][prs]. + + +[conventional-commits]: https://www.conventionalcommits.org/en/v1.0.0/ +[commiting-code]: https://docs.github.com/en/desktop/contributing-and-collaborating-using-github-desktop/making-changes-in-a-branch/committing-and-reviewing-changes-to-your-project +[commit-signing]: https://www.freecodecamp.org/news/what-is-commit-signing-in-git/ +[forking]: https://docs.github.com/en/get-started/quickstart/fork-a-repo +[gh-discussions]: https://docs.github.com/en/discussions +[git-clone]: https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository +[good-first-issues]: [https://build.prestashop-project.org/news/a-definition-of-the-good-first-issue-label/] +[post-signing]: https://dev.to/jmarhee/signing-existing-commits-with-gpg-5b58 +[prs]: https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests +[push-remote]: https://docs.github.com/en/get-started/using-git/pushing-commits-to-a-remote-repository +[rust-style-guide]: https://rust-lang.github.io/api-guidelines/about.html +[test-org]: https://doc.rust-lang.org/book/ch11-03-test-organization.html +[support-email]: mailto:alexeusgr@gmail.com +[unit-and-integration]: https://www.geeksforgeeks.org/difference-between-unit-testing-and-integration-testing/ diff --git a/Cargo.lock b/Cargo.lock index 0b3ce8f..a67f878 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3,5 +3,4411 @@ version = 3 [[package]] -name = "efected-cotoemory" +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 = "ahash" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a824f2aa7e75a0c98c5a504fceb80649e9c35265d44525b5f94de4771a395cd" +dependencies = [ + "getrandom 0.2.11", + "once_cell", + "version_check", +] + +[[package]] +name = "aho-corasick" +version = "0.7.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" +dependencies = [ + "memchr", +] + +[[package]] +name = "aho-corasick" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +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 = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi", +] + +[[package]] +name = "anyhow" +version = "1.0.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59d2a3357dde987206219e78ecfbbb6e8dad06cbb65292758d3270e6254f7355" +dependencies = [ + "backtrace", +] + +[[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 2.0.43", +] + +[[package]] +name = "async-trait" +version = "0.1.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdf6721fb0140e4f897002dd086c06f6c27775df19cfe1fccb21181a48fd2c98" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.43", +] + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi 0.1.19", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "axum" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf" +dependencies = [ + "async-trait", + "axum-core", + "bitflags 1.3.2", + "bytes", + "futures-util", + "headers", + "http", + "http-body", + "hyper", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", +] + +[[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", + "http-body", + "mime", + "rustversion", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-tracing-opentelemetry" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "164b95427e83b79583c7699a72b4a6b485a12bbdef5b5c054ee5ff2296d82f52" +dependencies = [ + "axum", + "futures", + "http", + "opentelemetry 0.18.0", + "opentelemetry-otlp", + "opentelemetry-semantic-conventions", + "tower", + "tower-http 0.3.5", + "tracing", + "tracing-opentelemetry 0.18.0", +] + +[[package]] +name = "backtrace" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +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.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" + +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + +[[package]] +name = "bit_field" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" + +[[package]] +name = "bytemuck" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374d28ec25809ee0e23827c2ab573d729e293f281dfe393500e7ad618baa61c6" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" + +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + +[[package]] +name = "cc" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "libc", +] + +[[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.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "num-traits", + "serde", + "windows-targets 0.48.5", +] + +[[package]] +name = "ciborium" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "effd91f6c78e5a4ace8a5d3c0b6bfaec9e2baaef55f3efc00e45fb2e477ee926" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdf919175532b369853f5d5e20b26b43112613fd6fe7aee757e35f7a44642656" + +[[package]] +name = "ciborium-ll" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "defaa24ecc093c77630e6c15e17c51f5e187bf35ee514f4e2d67baaa96dae22b" +dependencies = [ + "ciborium-io", + "half 1.8.2", +] + +[[package]] +name = "clap" +version = "3.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" +dependencies = [ + "bitflags 1.3.2", + "clap_lex", + "indexmap 1.9.3", + "textwrap", +] + +[[package]] +name = "clap_lex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" +dependencies = [ + "os_str_bytes", +] + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[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 = "config" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23738e11972c7643e4ec947840fc463b6a571afcd3e735bdfce7d03c7a784aca" +dependencies = [ + "async-trait", + "json5", + "lazy_static", + "nom", + "pathdiff", + "ron", + "rust-ini", + "serde", + "serde_json", + "toml", + "yaml-rust", +] + +[[package]] +name = "console-api" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2895653b4d9f1538a83970077cb01dfc77a4810524e51a110944688e916b18e" +dependencies = [ + "prost", + "prost-types", + "tonic 0.9.2", + "tracing-core", +] + +[[package]] +name = "console-subscriber" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4cf42660ac07fcebed809cfe561dd8730bcd35b075215e6479c516bcd0d11cb" +dependencies = [ + "console-api", + "crossbeam-channel", + "crossbeam-utils", + "futures", + "hdrhistogram", + "humantime", + "parking_lot 0.12.1", + "prost-types", + "serde", + "serde_json", + "thread_local", + "tokio", + "tokio-stream", + "tonic 0.9.2", + "tracing", + "tracing-core", + "tracing-subscriber", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "const_format" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a214c7af3d04997541b18d432afaff4c455e79e2029079647e72fc2bd27673" +dependencies = [ + "const_format_proc_macros", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f6ff08fd20f4f299298a28e2dfa8a8ba1036e6cd2460ac1de7b425d76f2500" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[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 = "cpufeatures" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce420fe07aecd3e67c5f910618fe65e94158f6dcc0adf44e00d69ce2bdfe0fd0" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "criterion" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c76e09c1aae2bc52b3d2f29e13c6572553b30c4aa1b8a49fd70de6412654cb" +dependencies = [ + "anes", + "atty", + "cast", + "ciborium", + "clap", + "criterion-plot", + "itertools", + "lazy_static", + "num-traits", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +dependencies = [ + "cast", + "itertools", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82a9b73a36529d9c47029b9fb3a6f0ea3cc916a261195352ba19e770fc1748b2" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fca89a0e215bab21874660c67903c5f143333cab1da83d041c7ded6053774751" +dependencies = [ + "cfg-if", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e3681d554572a651dda4186cd47240627c3d0114d45a95f6ad27f2f22e7548d" +dependencies = [ + "autocfg", + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3a430a770ebd84726f584a90ee7f020d28db52c6d02138900f22341f866d39c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "darling" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0209d94da627ab5605dcccf08bb18afa5009cfbef48d8a8b7d7bdbc79be25c5e" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "177e3443818124b357d8e76f53be906d60937f0d3a90773a664fa63fa253e621" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.43", +] + +[[package]] +name = "darling_macro" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.43", +] + +[[package]] +name = "dashmap" +version = "5.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +dependencies = [ + "cfg-if", + "hashbrown 0.14.3", + "lock_api", + "once_cell", + "parking_lot_core 0.9.9", +] + +[[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 = "der" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1a467a65c5e759bce6e65eaf91cc29f466cdc57cb65777bd646872a8a1fd4de" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eb30d70a07a3b04884d2677f06bec33509dc67ca60d92949e5535352d3191dc" +dependencies = [ + "powerfmt", + "serde", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", +] + +[[package]] +name = "dirs" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "dlv-list" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257" + +[[package]] +name = "efected-coto-emmory" +version = "0.1.0" +dependencies = [ + "ansi_term", + "anyhow", + "assert-json-diff", + "async-trait", + "axum", + "axum-tracing-opentelemetry", + "base64 0.21.5", + "chrono", + "config", + "console-subscriber", + "const_format", + "criterion", + "futures", + "headers", + "http", + "http-serde", + "hyper", + "image", + "metrics", + "metrics-exporter-prometheus", + "metrics-util", + "mime", + "num_cpus", + "once_cell", + "openssl", + "openssl-sys", + "opentelemetry 0.18.0", + "opentelemetry-otlp", + "opentelemetry-semantic-conventions", + "parking_lot 0.12.1", + "proptest", + "reqwest", + "reqwest-middleware", + "reqwest-retry", + "reqwest-tracing", + "retry-policies", + "rsa", + "serde", + "serde_json", + "serde_path_to_error", + "serde_with", + "sysinfo", + "task-local-extensions", + "thiserror", + "time", + "tokio", + "tokio-test", + "tonic 0.8.3", + "tower", + "tower-http 0.4.4", + "tracing", + "tracing-appender", + "tracing-opentelemetry 0.18.0", + "tracing-subscriber", + "ulid", + "url", + "utoipa", + "utoipa-swagger-ui", + "wiremock", +] + +[[package]] +name = "either" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" + +[[package]] +name = "encoding_rs" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "endian-type" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" + +[[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 = "exr" +version = "1.71.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "832a761f35ab3e6664babfbdc6cef35a4860e816ec3916dcfd0882954e98a8a8" +dependencies = [ + "bit_field", + "flume", + "half 2.2.1", + "lebe", + "miniz_oxide", + "rayon-core", + "smallvec", + "zune-inflate", +] + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" + +[[package]] +name = "fdeflate" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "209098dd6dfc4445aa6111f0e98653ac323eaa4dfd212c9ca3931bf9955c31bd" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + +[[package]] +name = "flate2" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "flume" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" +dependencies = [ + "spin 0.9.8", +] + +[[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 2.0.43", +] + +[[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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c" + +[[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 = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.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.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "gif" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80792593675e051cf94a4b111980da2ba60d4a83e43e0048c5693baab3977045" +dependencies = [ + "color_quant", + "weezl", +] + +[[package]] +name = "gimli" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" + +[[package]] +name = "h2" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d6250322ef6e60f93f9a2162799302cd6f68f79f6e5d85c8c16f14d1d958178" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap 2.1.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "half" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" + +[[package]] +name = "half" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b4af3693f1b705df946e9fe5631932443781d0aabb423b62fcd4d73f6d2fd0" +dependencies = [ + "crunchy", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashbrown" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" + +[[package]] +name = "hdrhistogram" +version = "7.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "765c9198f173dd59ce26ff9f95ef0aafd0a0fe01fb9d72841bc5066a4c06511d" +dependencies = [ + "base64 0.21.5", + "byteorder", + "flate2", + "nom", + "num-traits", +] + +[[package]] +name = "headers" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06683b93020a07e3dbcf5f8c0f6d40080d725bea7936fc01ad345c01b97dc270" +dependencies = [ + "base64 0.21.5", + "bytes", + "headers-core", + "http", + "httpdate", + "mime", + "sha1", +] + +[[package]] +name = "headers-core" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" +dependencies = [ + "http", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hermit-abi" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "home" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "http" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb" +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", + "pin-project-lite", +] + +[[package]] +name = "http-range-header" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "add0ab9360ddbd88cfeb3bd9574a1d85cfdfa14db10b3e21d3700dbc4328758f" + +[[package]] +name = "http-serde" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f560b665ad9f1572cfcaf034f7fb84338a7ce945216d64a90fd81f046a3caee" +dependencies = [ + "http", + "serde", +] + +[[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", + "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 = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[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", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-timeout" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" +dependencies = [ + "hyper", + "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", + "native-tls", + "tokio", + "tokio-native-tls", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20" +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 = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[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 = "image" +version = "0.24.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f3dfdbdd72063086ff443e297b61695500514b1e41095b6fb9a5ab48a70a711" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "exr", + "gif", + "jpeg-decoder", + "num-rational", + "num-traits", + "png", + "qoi", + "tiff", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" +dependencies = [ + "equivalent", + "hashbrown 0.14.3", + "serde", +] + +[[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", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "ipnet" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" + +[[package]] +name = "jpeg-decoder" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc0000e42512c92e31c2252315bda326620a4e034105e900c98ec492fa077b3e" +dependencies = [ + "rayon", +] + +[[package]] +name = "js-sys" +version = "0.3.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cee9c64da59eae3b50095c18d3e74f8b73c0b86d2792824ff01bbce68ba229ca" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "json5" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" +dependencies = [ + "pest", + "pest_derive", + "serde", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +dependencies = [ + "spin 0.5.2", +] + +[[package]] +name = "lebe" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" + +[[package]] +name = "libc" +version = "0.2.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" + +[[package]] +name = "libm" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" + +[[package]] +name = "libredox" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8" +dependencies = [ + "bitflags 2.4.1", + "libc", + "redox_syscall 0.4.1", +] + +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + +[[package]] +name = "linux-raw-sys" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456" + +[[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.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" + +[[package]] +name = "mach" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b823e83b2affd8f40a9ee8c29dbc56404c1e34cd2710921f2801e2cf29527afa" +dependencies = [ + "libc", +] + +[[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.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" + +[[package]] +name = "metrics" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b9b8653cec6897f73b519a43fba5ee3d50f62fe9af80b428accdcc093b4a849" +dependencies = [ + "ahash", + "metrics-macros", + "portable-atomic 0.3.20", +] + +[[package]] +name = "metrics-exporter-prometheus" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8603921e1f54ef386189335f288441af761e0fc61bcb552168d9cedfe63ebc70" +dependencies = [ + "hyper", + "indexmap 1.9.3", + "ipnet", + "metrics", + "metrics-util", + "parking_lot 0.12.1", + "portable-atomic 0.3.20", + "quanta", + "thiserror", + "tokio", + "tracing", +] + +[[package]] +name = "metrics-macros" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "731f8ecebd9f3a4aa847dfe75455e4757a45da40a7793d2f0b1f9b6ed18b23f3" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "metrics-util" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d24dc2dbae22bff6f1f9326ffce828c9f07ef9cc1e8002e5279f845432a30a" +dependencies = [ + "aho-corasick 0.7.20", + "crossbeam-epoch", + "crossbeam-utils", + "hashbrown 0.12.3", + "indexmap 1.9.3", + "metrics", + "num_cpus", + "ordered-float", + "parking_lot 0.12.1", + "portable-atomic 0.3.20", + "quanta", + "radix_trie", + "sketches-ddsketch", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" +dependencies = [ + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.48.0", +] + +[[package]] +name = "multimap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" + +[[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 = "nibble_vec" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" +dependencies = [ + "smallvec", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "ntapi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" +dependencies = [ + "winapi", +] + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.5", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi 0.3.3", + "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 = "oorandom" +version = "11.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" + +[[package]] +name = "openssl" +version = "0.10.62" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cde4d2d9200ad5909f8dac647e29482e07c3a35de8a13fce7c9c7747ad9f671" +dependencies = [ + "bitflags 2.4.1", + "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 2.0.43", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-src" +version = "300.2.1+3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fe476c29791a5ca0d1273c697e96085bbabbbea2ef7afd5617e78a4b40332d3" +dependencies = [ + "cc", +] + +[[package]] +name = "openssl-sys" +version = "0.9.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1665caf8ab2dc9aef43d1c0023bd904633a6a05cb30b0ad59bec2ae986e57a7" +dependencies = [ + "cc", + "libc", + "openssl-src", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "opentelemetry" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6105e89802af13fdf48c49d7646d3b533a70e536d818aae7e78ba0433d01acb8" +dependencies = [ + "async-trait", + "crossbeam-channel", + "futures-channel", + "futures-executor", + "futures-util", + "js-sys", + "lazy_static", + "percent-encoding", + "pin-project", + "rand 0.8.5", + "thiserror", +] + +[[package]] +name = "opentelemetry" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69d6c3d7288a106c0a363e4b0e8d308058d56902adefb16f4936f417ffef086e" +dependencies = [ + "opentelemetry_api", + "opentelemetry_sdk", +] + +[[package]] +name = "opentelemetry-http" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc79add46364183ece1a4542592ca593e6421c60807232f5b8f7a31703825d" +dependencies = [ + "async-trait", + "bytes", + "http", + "opentelemetry_api", +] + +[[package]] +name = "opentelemetry-otlp" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1c928609d087790fc936a1067bdc310ae702bdf3b090c3f281b713622c8bbde" +dependencies = [ + "async-trait", + "futures", + "futures-util", + "http", + "opentelemetry 0.18.0", + "opentelemetry-http", + "opentelemetry-proto", + "prost", + "thiserror", + "tokio", + "tonic 0.8.3", +] + +[[package]] +name = "opentelemetry-proto" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d61a2f56df5574508dd86aaca016c917489e589ece4141df1b5e349af8d66c28" +dependencies = [ + "futures", + "futures-util", + "opentelemetry 0.18.0", + "prost", + "tonic 0.8.3", + "tonic-build", +] + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b02e0230abb0ab6636d18e2ba8fa02903ea63772281340ccac18e0af3ec9eeb" +dependencies = [ + "opentelemetry 0.18.0", +] + +[[package]] +name = "opentelemetry_api" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c24f96e21e7acc813c7a8394ee94978929db2bcc46cf6b5014fc612bf7760c22" +dependencies = [ + "fnv", + "futures-channel", + "futures-util", + "indexmap 1.9.3", + "js-sys", + "once_cell", + "pin-project-lite", + "thiserror", +] + +[[package]] +name = "opentelemetry_sdk" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ca41c4933371b61c2a2f214bf16931499af4ec90543604ec828f7a625c09113" +dependencies = [ + "async-trait", + "crossbeam-channel", + "dashmap", + "fnv", + "futures-channel", + "futures-executor", + "futures-util", + "once_cell", + "opentelemetry_api", + "percent-encoding", + "rand 0.8.5", + "thiserror", + "tokio", + "tokio-stream", +] + +[[package]] +name = "ordered-float" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" +dependencies = [ + "num-traits", +] + +[[package]] +name = "ordered-multimap" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccd746e37177e1711c20dd619a1620f34f5c8b569c53590a72dedd5344d8924a" +dependencies = [ + "dlv-list", + "hashbrown 0.12.3", +] + +[[package]] +name = "os_str_bytes" +version = "6.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1" + +[[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.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core 0.8.6", +] + +[[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 0.9.9", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" +dependencies = [ + "cfg-if", + "instant", + "libc", + "redox_syscall 0.2.16", + "smallvec", + "winapi", +] + +[[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 0.4.1", + "smallvec", + "windows-targets 0.48.5", +] + +[[package]] +name = "pathdiff" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" + +[[package]] +name = "pem-rfc7468" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d159833a9105500e0398934e205e0773f0b27529557134ecfc51c27646adac" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pest" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae9cee2a55a544be8b89dc6848072af97a20f2422603c10865be2a42b580fff5" +dependencies = [ + "memchr", + "thiserror", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81d78524685f5ef2a3b3bd1cafbc9fcabb036253d9b1463e726a91cd16e2dfc2" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68bd1206e71118b5356dae5ddc61c8b11e28b09ef6a31acbd15ea48a28e0c227" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.43", +] + +[[package]] +name = "pest_meta" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c747191d4ad9e4a4ab9c8798f1e82a39affe7ef9648390b7e5548d18e099de6" +dependencies = [ + "once_cell", + "pest", + "sha2", +] + +[[package]] +name = "petgraph" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9" +dependencies = [ + "fixedbitset", + "indexmap 2.1.0", +] + +[[package]] +name = "pin-project" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.43", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkcs1" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eff33bdbdfc54cc98a2eca766ebdec3e1b8fb7387523d5c9c9a2891da856f719" +dependencies = [ + "der", + "pkcs8", + "spki", + "zeroize", +] + +[[package]] +name = "pkcs8" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69d3587f8a9e599cc7ec2c00e331f71c4e69a5f9a4b8a6efd5b07466b9736f9a" + +[[package]] +name = "plotters" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2c224ba00d7cadd4d5c660deaf2098e5e80e07846537c51f9cfa4be50c1fd45" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e76628b4d3a7581389a35d5b6e2139607ad7c75b17aed325f210aa91f4a9609" + +[[package]] +name = "plotters-svg" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38f6d39893cca0701371e3c27294f09797214b86f1fb951b89ade8ec04e2abab" +dependencies = [ + "plotters-backend", +] + +[[package]] +name = "png" +version = "0.17.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd75bf2d8dd3702b9707cdbc56a5b9ef42cec752eb8b3bafc01234558442aa64" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "portable-atomic" +version = "0.3.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e30165d31df606f5726b090ec7592c308a0eaf61721ff64c9a3018e344a8753e" +dependencies = [ + "portable-atomic 1.6.0", +] + +[[package]] +name = "portable-atomic" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "prettyplease" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8646e95016a7a6c4adea95bafa8a16baab64b583356217f2c85db4a39d9a86" +dependencies = [ + "proc-macro2", + "syn 1.0.109", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75cb1540fadbd5b8fbccc4dddad2734eba435053f725621c070711a14bb5f4b8" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "proptest" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31b476131c3c86cb68032fdc5cb6d5a1045e3e42d96b69fa599fd77701e1f5bf" +dependencies = [ + "bit-set", + "bit-vec", + "bitflags 2.4.1", + "lazy_static", + "num-traits", + "rand 0.8.5", + "rand_chacha 0.3.1", + "rand_xorshift", + "regex-syntax 0.8.2", + "rusty-fork", + "tempfile", + "unarray", +] + +[[package]] +name = "prost" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b82eaa1d779e9a4bc1c3217db8ffbeabaae1dca241bf70183242128d48681cd" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-build" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "119533552c9a7ffacc21e099c24a0ac8bb19c2a2a3f363de84cd9b844feab270" +dependencies = [ + "bytes", + "heck", + "itertools", + "lazy_static", + "log", + "multimap", + "petgraph", + "prettyplease", + "prost", + "prost-types", + "regex", + "syn 1.0.109", + "tempfile", + "which", +] + +[[package]] +name = "prost-derive" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d2d8d10f3c6ded6da8b05b5fb3b8a5082514344d56c9f871412d29b4e075b4" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "prost-types" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213622a1460818959ac1181aaeb2dc9c7f63df720db7d788b3e24eacd1983e13" +dependencies = [ + "prost", +] + +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "quanta" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7e31331286705f455e56cca62e0e717158474ff02b7936c1fa596d983f4ae27" +dependencies = [ + "crossbeam-utils", + "libc", + "mach", + "once_cell", + "raw-cpuid", + "wasi 0.10.2+wasi-snapshot-preview1", + "web-sys", + "winapi", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quote" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "radix_trie" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd" +dependencies = [ + "endian-type", + "nibble_vec", +] + +[[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.11", +] + +[[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 = "rand_xorshift" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" +dependencies = [ + "rand_core 0.6.4", +] + +[[package]] +name = "raw-cpuid" +version = "10.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c297679cb867470fa8c9f67dbba74a78d78e3e98d7cf2b08d6d71540f797332" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "rayon" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c27db03db7734835b3f53954b534c91069375ce6ccaa2e065441e07d9b6cdb1" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ce3fb6ad83f861aac485e76e1985cd109d9a3713802152be56c3b1f0e0658ed" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags 1.3.2", +] + +[[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 = "redox_users" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4" +dependencies = [ + "getrandom 0.2.11", + "libredox", + "thiserror", +] + +[[package]] +name = "regex" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" +dependencies = [ + "aho-corasick 1.1.2", + "memchr", + "regex-automata 0.4.3", + "regex-syntax 0.8.2", +] + +[[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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" +dependencies = [ + "aho-corasick 1.1.2", + "memchr", + "regex-syntax 0.8.2", +] + +[[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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" + +[[package]] +name = "reqwest" +version = "0.11.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b1ae8d9ac08420c66222fb9096fc5de435c3c48542bc5336c51892cffafb41" +dependencies = [ + "base64 0.21.5", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-tls", + "ipnet", + "js-sys", + "log", + "mime", + "mime_guess", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "system-configuration", + "tokio", + "tokio-native-tls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + +[[package]] +name = "reqwest-middleware" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a3e86aa6053e59030e7ce2d2a3b258dd08fc2d337d52f73f6cb480f5858690" +dependencies = [ + "anyhow", + "async-trait", + "http", + "reqwest", + "serde", + "task-local-extensions", + "thiserror", +] + +[[package]] +name = "reqwest-retry" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c6a11c05102e5bec712c0619b8c7b7eda8b21a558a0bd981ceee15c38df8be4" +dependencies = [ + "anyhow", + "async-trait", + "chrono", + "futures", + "getrandom 0.2.11", + "http", + "hyper", + "parking_lot 0.11.2", + "reqwest", + "reqwest-middleware", + "retry-policies", + "task-local-extensions", + "tokio", + "tracing", + "wasm-timer", +] + +[[package]] +name = "reqwest-tracing" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14b1e66540e0cac90acadaf7109bf99c90d95abcc94b4c096bfa16a2d7aa7a71" +dependencies = [ + "anyhow", + "async-trait", + "getrandom 0.2.11", + "matchit", + "opentelemetry 0.17.0", + "reqwest", + "reqwest-middleware", + "task-local-extensions", + "tracing", + "tracing-opentelemetry 0.17.4", +] + +[[package]] +name = "retain_mut" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4389f1d5789befaf6029ebd9f7dac4af7f7e3d61b69d4f30e2ac02b57e7712b0" + +[[package]] +name = "retry-policies" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e09bbcb5003282bcb688f0bae741b278e9c7e8f378f561522c9806c58e075d9b" +dependencies = [ + "anyhow", + "chrono", + "rand 0.8.5", +] + +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin 0.5.2", + "untrusted 0.7.1", + "web-sys", + "winapi", +] + +[[package]] +name = "ring" +version = "0.17.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "688c63d65483050968b2a8937f7995f443e27041a0f7700aa59b0822aedebb74" +dependencies = [ + "cc", + "getrandom 0.2.11", + "libc", + "spin 0.9.8", + "untrusted 0.9.0", + "windows-sys 0.48.0", +] + +[[package]] +name = "ron" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88073939a61e5b7680558e6be56b419e208420c2adb92be54921fa6b72283f1a" +dependencies = [ + "base64 0.13.1", + "bitflags 1.3.2", + "serde", +] + +[[package]] +name = "rsa" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55a77d189da1fee555ad95b7e50e7457d91c0e089ec68ca69ad2989413bbdab4" +dependencies = [ + "byteorder", + "digest", + "num-bigint-dig", + "num-integer", + "num-iter", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "signature", + "subtle", + "zeroize", +] + +[[package]] +name = "rust-embed" +version = "6.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a36224c3276f8c4ebc8c20f158eca7ca4359c8db89991c4925132aaaf6702661" +dependencies = [ + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "6.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49b94b81e5b2c284684141a2fb9e2a31be90638caf040bf9afbc5a0416afe1ac" +dependencies = [ + "proc-macro2", + "quote", + "rust-embed-utils", + "shellexpand", + "syn 2.0.43", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "7.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d38ff6bf570dc3bb7100fce9f7b60c33fa71d80e88da3f2580df4ff2bdded74" +dependencies = [ + "sha2", + "walkdir", +] + +[[package]] +name = "rust-ini" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6d5f2436026b4f6e79dc829837d467cc7e9a55ee40e750d716713540715a2df" +dependencies = [ + "cfg-if", + "ordered-multimap", +] + +[[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.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72e572a5e8ca657d7366229cdde4bd14c4eb5499a9573d4d366fe1b599daa316" +dependencies = [ + "bitflags 2.4.1", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls" +version = "0.20.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b80e3dec595989ea8510028f30c408a4630db12c9cbb8de34203b89d6577e99" +dependencies = [ + "log", + "ring 0.16.20", + "sct", + "webpki", +] + +[[package]] +name = "rustls-native-certs" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" +dependencies = [ + "openssl-probe", + "rustls-pemfile", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64 0.21.5", +] + +[[package]] +name = "rustversion" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" + +[[package]] +name = "rusty-fork" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f" +dependencies = [ + "fnv", + "quick-error", + "tempfile", + "wait-timeout", +] + +[[package]] +name = "ryu" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" +dependencies = [ + "windows-sys 0.48.0", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring 0.17.7", + "untrusted 0.9.0", +] + +[[package]] +name = "security-framework" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "serde" +version = "1.0.193" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.193" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.43", +] + +[[package]] +name = "serde_json" +version = "1.0.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4beec8bce849d58d06238cb50db2e1c417cfeafa4c63f692b15c82b7c80f8335" +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_with" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64cd236ccc1b7a29e7e2739f27c0b2dd199804abc4290e32f59f3b68d6405c23" +dependencies = [ + "base64 0.21.5", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.1.0", + "serde", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93634eb5f75a2323b16de4748022ac4297f9e76b6dced2be287a099f41b5e788" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.43", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shellexpand" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ccc8076840c4da029af4f87e4e8daeb0fca6b87bbb02e10cb60b791450e11e4" +dependencies = [ + "dirs", +] + +[[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 = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core 0.6.4", +] + +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "sketches-ddsketch" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68a406c1882ed7f29cd5e248c9848a80e7cb6ae0fea82346d2746f2f941c07e1" + +[[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.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" + +[[package]] +name = "socket2" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" +dependencies = [ + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "subtle" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee659fb5f3d355364e1f3e5bc10fb82068efbf824a1e9d1c9504244a6469ad53" +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 = "sysinfo" +version = "0.28.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c2f3ca6693feb29a89724516f016488e9aafc7f37264f898593ee4b942f31b" +dependencies = [ + "cfg-if", + "core-foundation-sys", + "libc", + "ntapi", + "once_cell", + "rayon", + "winapi", +] + +[[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 = "task-local-extensions" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba323866e5d033818e3240feeb9f7db2c4296674e4d9e16b97b7bf8f490434e8" +dependencies = [ + "pin-utils", +] + +[[package]] +name = "tempfile" +version = "3.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5" +dependencies = [ + "cfg-if", + "fastrand 2.0.1", + "redox_syscall 0.4.1", + "rustix", + "windows-sys 0.48.0", +] + +[[package]] +name = "textwrap" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" + +[[package]] +name = "thiserror" +version = "1.0.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83a48fd946b02c0a526b2e9481c8e2a17755e47039164a86c4070446e3a4614d" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7fbe9b594d6568a6a1443250a7e67d80b74e1e96f6d1715e1e21cc1888291d3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.43", +] + +[[package]] +name = "thread_local" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "tiff" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d172b0f4d3fba17ba89811858b9d3d97f928aece846475bbda076ca46736211" +dependencies = [ + "flate2", + "jpeg-decoder", + "weezl", +] + +[[package]] +name = "time" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f657ba42c3f86e7680e53c8cd3af8abbe56b5491790b46e22e19c0d57463583e" +dependencies = [ + "deranged", + "itoa", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26197e33420244aeb70c3e8c78376ca46571bc4e701e4791c2cd9f57dcb3a43f" +dependencies = [ + "time-core", +] + +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + +[[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.35.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89b4efa943be685f629b149f53829423f8f5531ea21249408e8e2f8671ec104" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "num_cpus", + "parking_lot 0.12.1", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "tracing", + "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 2.0.43", +] + +[[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-rustls" +version = "0.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" +dependencies = [ + "rustls", + "tokio", + "webpki", +] + +[[package]] +name = "tokio-stream" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-test" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89b3cbabd3ae862100094ae433e1def582cf86451b4e9bf83aa7ac1d8a7d719" +dependencies = [ + "async-stream", + "bytes", + "futures-core", + "tokio", + "tokio-stream", +] + +[[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 = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + +[[package]] +name = "tonic" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f219fad3b929bef19b1f86fbc0358d35daed8f2cac972037ac0dc10bbb8d5fb" +dependencies = [ + "async-stream", + "async-trait", + "axum", + "base64 0.13.1", + "bytes", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-timeout", + "percent-encoding", + "pin-project", + "prost", + "prost-derive", + "rustls-native-certs", + "rustls-pemfile", + "tokio", + "tokio-rustls", + "tokio-stream", + "tokio-util", + "tower", + "tower-layer", + "tower-service", + "tracing", + "tracing-futures", +] + +[[package]] +name = "tonic" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3082666a3a6433f7f511c7192923fa1fe07c69332d3c6a2e6bb040b569199d5a" +dependencies = [ + "async-trait", + "axum", + "base64 0.21.5", + "bytes", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-timeout", + "percent-encoding", + "pin-project", + "prost", + "tokio", + "tokio-stream", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tonic-build" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bf5e9b9c0f7e0a7c027dcfaba7b2c60816c7049171f679d99ee2ff65d0de8c4" +dependencies = [ + "prettyplease", + "proc-macro2", + "prost-build", + "quote", + "syn 1.0.109", +] + +[[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-http" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f873044bf02dd1e8239e9c1293ea39dad76dc594ec16185d0a1bf31d8dc8d858" +dependencies = [ + "bitflags 1.3.2", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-range-header", + "pin-project-lite", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c5bb1d698276a2443e5ecfabc1008bf15a36c12e6a7176e7bf089ea9131140" +dependencies = [ + "bitflags 2.4.1", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-range-header", + "pin-project-lite", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", + "uuid", +] + +[[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-appender" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf" +dependencies = [ + "crossbeam-channel", + "thiserror", + "time", + "tracing-subscriber", +] + +[[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 2.0.43", +] + +[[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-futures" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" +dependencies = [ + "pin-project", + "tracing", +] + +[[package]] +name = "tracing-log" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f751112709b4e791d8ce53e32c4ed2d353565a795ce84da2285393f41557bdf2" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[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-opentelemetry" +version = "0.17.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbbe89715c1dbbb790059e2565353978564924ee85017b5fff365c872ff6721f" +dependencies = [ + "once_cell", + "opentelemetry 0.17.0", + "tracing", + "tracing-core", + "tracing-log 0.1.4", + "tracing-subscriber", +] + +[[package]] +name = "tracing-opentelemetry" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21ebb87a95ea13271332df069020513ab70bdb5637ca42d6e492dc3bbbad48de" +dependencies = [ + "once_cell", + "opentelemetry 0.18.0", + "tracing", + "tracing-core", + "tracing-log 0.1.4", + "tracing-subscriber", +] + +[[package]] +name = "tracing-serde" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc6b213177105856957181934e4920de57730fc69bf42c37ee5bb664d406d9e1" +dependencies = [ + "serde", + "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", + "parking_lot 0.12.1", + "regex", + "serde", + "serde_json", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log 0.2.0", + "tracing-serde", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "ucd-trie" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" + +[[package]] +name = "ulid" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e37c4b6cbcc59a8dcd09a6429fbc7890286bcbb79215cea7b38a3c4c0921d93" +dependencies = [ + "rand 0.8.5", + "serde", +] + +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + +[[package]] +name = "unicase" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" +dependencies = [ + "version_check", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f2528f27a9eb2b21e69c95319b30bd0efd85d09c379741b0f78ea1d86be2416" + +[[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.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-xid" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utoipa" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d82b1bc5417102a73e8464c686eef947bdfb99fcdfc0a4f228e81afa9526470a" +dependencies = [ + "indexmap 2.1.0", + "serde", + "serde_json", + "utoipa-gen", +] + +[[package]] +name = "utoipa-gen" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05d96dcd6fc96f3df9b3280ef480770af1b7c5d14bc55192baa9b067976d920c" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "regex", + "syn 2.0.43", + "uuid", +] + +[[package]] +name = "utoipa-swagger-ui" +version = "3.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84614caa239fb25b2bb373a52859ffd94605ceb256eeb1d63436325cf81e3653" +dependencies = [ + "axum", + "mime_guess", + "regex", + "rust-embed", + "serde", + "serde_json", + "utoipa", + "zip", +] + +[[package]] +name = "uuid" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e395fcf16a7a3d8127ec99782007af141946b4795001f876d54fb0d55978560" +dependencies = [ + "getrandom 0.2.11", + "serde", +] + +[[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 = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "wait-timeout" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" +dependencies = [ + "libc", +] + +[[package]] +name = "waker-fn" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c4517f54858c779bbcbf228f4fca63d121bf85fbecb2dc578cdf4a39395690" + +[[package]] +name = "walkdir" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" +dependencies = [ + "same-file", + "winapi-util", +] + +[[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.10.2+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" + +[[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.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ed0d4f68a3015cc185aff4db9506a015f4b96f95303897bfa23f846db54064e" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b56f625e64f3a1084ded111c4d5f477df9f8c92df113852fa5a374dbda78826" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.43", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac36a15a220124ac510204aec1c3e5db8a22ab06fd6706d881dc6149f8ed9a12" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0162dbf37223cd2afce98f3d0785506dcb8d266223983e4b5b525859e6e182b2" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.43", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ab9b36309365056cd639da3134bf87fa8f3d86008abf99e612384a6eecd459f" + +[[package]] +name = "wasm-timer" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be0ecb0db480561e9a7642b5d3e4187c128914e58aa84330b9493e3eb68c5e7f" +dependencies = [ + "futures", + "js-sys", + "parking_lot 0.11.2", + "pin-utils", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50c24a44ec86bb68fbecd1b3efed7e85ea5621b39b35ef2766b66cd984f8010f" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed63aea5ce73d0ff405984102c42de94fc55a6b75765d621c65262469b3c9b53" +dependencies = [ + "ring 0.17.7", + "untrusted 0.9.0", +] + +[[package]] +name = "weezl" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb" + +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix", +] + +[[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-util" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +dependencies = [ + "winapi", +] + +[[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.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" +dependencies = [ + "windows-targets 0.48.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.0", +] + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +dependencies = [ + "windows_aarch64_gnullvm 0.52.0", + "windows_aarch64_msvc 0.52.0", + "windows_i686_gnu 0.52.0", + "windows_i686_msvc 0.52.0", + "windows_x86_64_gnu 0.52.0", + "windows_x86_64_gnullvm 0.52.0", + "windows_x86_64_msvc 0.52.0", +] + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" + +[[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.5", + "deadpool", + "futures", + "futures-timer", + "http-types", + "hyper", + "log", + "once_cell", + "regex", + "serde", + "serde_json", + "tokio", +] + +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] + +[[package]] +name = "zeroize" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" + +[[package]] +name = "zip" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" +dependencies = [ + "byteorder", + "crc32fast", + "crossbeam-utils", + "flate2", +] + +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] diff --git a/Cargo.toml b/Cargo.toml index d7e50e1..90c84b2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,11 +1,119 @@ [package] -name = "efected-cotoemory" +name = "efected-coto-emmory" version = "0.1.0" +description = "a cloud service" +keywords = [] +categories = [] +include = ["/src", "/benches", "README.md", "LICENSE-APACHE", "LICENSE-MIT"] +license = "Apache-2.0 or MIT" +readme = "README.md" edition = "2021" -description = "a web service" -repository = "https://github.com/aleeusgr/efected-cotoemory" -license = "MIT OR Apache-2.0" +rust-version = "1.67" +documentation = "https://docs.rs/efected-coto-emmory" +repository = "https://github.com/aleeusgr/efected-coto-emmory" +authors = ["Alex "] +default-run = "efected-coto-emmory-app" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[lib] +path = "src/lib.rs" +bench = false +doctest = true + +[[bin]] +name = "efected-coto-emmory-app" +path = "src/main.rs" +doc = false +bench = false + +[[bin]] +name = "openapi" +path = "src/bin/openapi.rs" +test = false +doc = false +bench = false + +[[bench]] +name = "a_benchmark" +harness = false +required-features = ["test_utils"] [dependencies] +ansi_term = { version = "0.12", optional = true, default-features = false } +anyhow = { version = "1.0", features = ["backtrace"] } +async-trait = "0.1" +axum = { version = "0.6", features = ["headers"] } +axum-tracing-opentelemetry = { version = "0.10", features = ["otlp"] } +base64 = "0.21" +chrono = { version = "0.4", default-features = false, features = ["clock"] } +config = "0.13" +console-subscriber = { version = "0.1", default-features = false, features = [ "parking_lot" ], optional = true } +const_format = "0.2" +futures = "0.3" +headers = "0.3" +image = "0.24" +http = "0.2" +http-serde = "1.1" +hyper = "0.14" +metrics = "0.20" +metrics-exporter-prometheus = "0.11" +metrics-util = { version = "0.14", default-features = true } +mime = "0.3" +num_cpus = "1.0" +once_cell = "1.14" +openssl = { version = "0.10", features = ["vendored"], default-features = false } +openssl-sys = "0.9.98" +opentelemetry = { version = "0.18", features = ["rt-tokio", "trace"] } +opentelemetry-otlp = { version = "0.11", features = ["metrics", "grpc-tonic", "tls-roots"], default-features = false } +opentelemetry-semantic-conventions = "0.10" +parking_lot = "0.12" +proptest = { version = "1.1", optional = true } +reqwest = { version = "0.11", features = ["json"] } +reqwest-middleware = "0.2" +reqwest-retry = "0.2" +reqwest-tracing = { version = "0.4", features = ["opentelemetry_0_17"] } +retry-policies = "0.1" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +serde_path_to_error = "0.1" +serde_with = "3.0" +sysinfo = "0.28" +task-local-extensions = "0.1" +thiserror = "1.0" +time = { version = "0.3", features = ["serde-well-known", "serde-human-readable"] } +tokio = { version = "1.26", features = ["full", "parking_lot"] } +## Tied to opentelemetry-otlp dependency +tonic = { version = "0.8" } +tower = "0.4" +tower-http = { version = "0.4", features = ["catch-panic", "request-id", "sensitive-headers", "timeout", "trace", "util"] } +tracing = "0.1" +tracing-appender = "0.2" +tracing-opentelemetry = "0.18" +tracing-subscriber = { version = "0.3", features = ["env-filter", "json", "parking_lot", "registry"] } +ulid = { version = "1.0", features = ["serde"] } +url = "2.3" +utoipa = { version = "3.3", features = ["uuid", "axum_extras"] } +utoipa-swagger-ui = { version = "3.1", features = ["axum"] } + +[dev-dependencies] +assert-json-diff = "2.0" +criterion = "0.4" +proptest = "1.1" +rsa = { version = "0.8" } +tokio-test = "0.4" +wiremock = "0.5" + +[features] +ansi-logs = ["ansi_term"] +console = ["console-subscriber"] +default = [] +test_utils = ["proptest"] + +[package.metadata.docs.rs] +all-features = true +# defines the configuration attribute `docsrs` +rustdoc-args = ["--cfg", "docsrs"] + +# Speedup build on macOS +# See https://blog.rust-lang.org/2021/03/25/Rust-1.51.0.html#splitting-debug-information +[profile.dev] +split-debuginfo = "unpacked" diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..1bb4114 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,79 @@ +# syntax=docker/dockerfile:1 +ARG RUST_BUILD_IMG=rust:1.67-slim-bullseye +ARG DEBIAN_TAG=bullseye-slim + +FROM $RUST_BUILD_IMG as base + +# AMD64 +FROM --platform=$BUILDPLATFORM base as builder-amd64 +ARG TARGET="x86_64-unknown-linux-gnu" + +# ARM64 +FROM --platform=$BUILDPLATFORM base as builder-arm64 +ARG TARGET="aarch64-unknown-linux-gnu" + +FROM builder-$TARGETARCH as builder + +RUN adduser --disabled-password --disabled-login --gecos "" --no-create-home efected-coto-emmory +RUN apt update && apt install -y g++ build-essential protobuf-compiler +RUN rustup target add $TARGET + +RUN cargo init efected-coto-emmory + +WORKDIR /efected-coto-emmory + +# touch lib.rs as we combine both +Run touch src/lib.rs + +# touch benches as it's part of Cargo.toml +RUN mkdir benches +RUN touch benches/a_benchmark.rs + +# copy cargo.* +COPY Cargo.lock ./Cargo.lock +COPY Cargo.toml ./Cargo.toml + +# cache depencies +RUN mkdir .cargo +RUN cargo vendor > .cargo/config +RUN --mount=type=cache,target=$CARGO_HOME/registry \ + --mount=type=cache,target=$CARGO_HOME/.git \ + --mount=type=cache,target=efected-coto-emmory/target,sharing=locked \ + cargo build --target $TARGET --bin efected-coto-emmory-app --release + +COPY src ./src +# copy src +COPY src ./src +# copy benches +COPY benches ./benches + +# copy config +COPY config ./config + +# final build for release +RUN rm ./target/$TARGET/release/deps/*efected_coto_emmory* +RUN cargo build --target $TARGET --bin efected-coto-emmory-app --release + +RUN strip ./target/$TARGET/release/efected-coto-emmory-app + +RUN mv ./target/$TARGET/release/efected-coto-emmory* /usr/local/bin +RUN mv ./config /etc/config + +FROM debian:${DEBIAN_TAG} + +ARG backtrace=0 +ARG log_level=info + +ENV RUST_BACKTRACE=${backtrace} \ + RUST_LOG=${log_level} + +COPY --from=builder /usr/local/bin/efected-coto-emmory* . +COPY --from=builder /etc/config ./config +COPY --from=builder /etc/passwd /etc/passwd +COPY --from=builder /etc/group /etc/group + +USER efected-coto-emmory:efected-coto-emmory + +EXPOSE 3000 +EXPOSE 4000 +ENTRYPOINT ["./efected-coto-emmory-app"] diff --git a/LICENSE-APACHE b/LICENSE-APACHE index 1b5ec8b..afd62f4 100644 --- a/LICENSE-APACHE +++ b/LICENSE-APACHE @@ -1,176 +1,201 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2023 Aleeusgr + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/LICENSE-MIT b/LICENSE-MIT index 4e1a27d..31aa793 100644 --- a/LICENSE-MIT +++ b/LICENSE-MIT @@ -1,21 +1,23 @@ -MIT License +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: -Copyright (c) 2023 Alex +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index efc3df6..41c1810 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,20 @@ # efected-cotoemory -a PBL in project management. +the first project in the PBL: Project Management.
+Implement a cloud service in Rust to leverage its advantages for Performance, Memory Safety and Concurrency. -## [What is PBL?](https://www.pblworks.org/what-is-pbl) +## [What is a PBL?](https://www.pblworks.org/what-is-pbl) Project Based Learning (PBL) is a teaching method in which students learn by actively engaging in real-world and personally meaningful projects. + +## How to run? +1. `git clone` +2. nix `develop` +3. cargo `test` (optional) +4. cargo `run` + +Swagger UI is accessible [in the browser](http://localhost:3000/swagger-ui/) or http://localhost:3000/test-lib + +Maybe try nix `flake update`? + +docs:
+1. https://medium.com/@alexeusgr/optimizing-python-service-on-ae-cloud-using-rust-b189ced94309 +2. https://github.com/fission-codes/rust-template diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..dded5b5 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,17 @@ +## Report a security issue or vulnerability + +The efected-coto-emmory team welcomes security reports and is committed to +providing prompt attention to security issues. Security issues should be +reported privately via [alexeusgr@gmail.com][support-email]. Security issues should +not be reported via the public GitHub Issue tracker. + +## Security advisories + +The project team is committed to transparency in the security issue disclosure +process. The efected-coto-emmory team announces security advisories through our +Github respository's [security portal][sec-advisories] and and the +[RustSec advisory database][rustsec-db]. + +[rustsec-db]: https://github.com/RustSec/advisory-db +[sec-advisories]: https://github.com/aleeusgr/efected-coto-emmory/security/advisories +[support-email]: mailto:alexeusgr@gmail.com diff --git a/assets/a_logo.png b/assets/a_logo.png new file mode 100644 index 0000000..f86c0ff Binary files /dev/null and b/assets/a_logo.png differ diff --git a/benches/a_benchmark.rs b/benches/a_benchmark.rs new file mode 100644 index 0000000..286409b --- /dev/null +++ b/benches/a_benchmark.rs @@ -0,0 +1,16 @@ +use criterion::{criterion_group, criterion_main, Criterion}; + +pub fn add_benchmark(c: &mut Criterion) { + let mut rvg = efected_coto_emmory::test_utils::Rvg::deterministic(); + let int_val_1 = rvg.sample(&(0..100i32)); + let int_val_2 = rvg.sample(&(0..100i32)); + + c.bench_function("add", |b| { + b.iter(|| { + efected_coto_emmory::add(int_val_1, int_val_2); + }) + }); +} + +criterion_group!(benches, add_benchmark); +criterion_main!(benches); diff --git a/config/settings.toml b/config/settings.toml new file mode 100644 index 0000000..8f12d8e --- /dev/null +++ b/config/settings.toml @@ -0,0 +1,11 @@ +[monitoring] +process_collector_interval = 10 + +[otel] +exporter_otlp_endpoint = "http://localhost:4317" + +[server] +environment = "local" +metrics_port = 4000 +port = 3000 +timeout_ms = 30000 diff --git a/deny.toml b/deny.toml new file mode 100644 index 0000000..c749950 --- /dev/null +++ b/deny.toml @@ -0,0 +1,200 @@ +# This template contains all of the possible sections and their default values + +# Note that all fields that take a lint level have these possible values: +# * deny - An error will be produced and the check will fail +# * warn - A warning will be produced, but the check will not fail +# * allow - No warning or error will be produced, though in some cases a note +# will be + +# The values provided in this template are the default values that will be used +# when any section or field is not specified in your own configuration + +# If 1 or more target triples (and optionally, target_features) are specified, +# only the specified targets will be checked when running `cargo deny check`. +# This means, if a particular package is only ever used as a target specific +# dependency, such as, for example, the `nix` crate only being used via the +# `target_family = "unix"` configuration, that only having windows targets in +# this list would mean the nix crate, as well as any of its exclusive +# dependencies not shared by any other crates, would be ignored, as the target +# list here is effectively saying which targets you are building for. +targets = [ + # The triple can be any string, but only the target triples built in to + # rustc (as of 1.40) can be checked against actual config expressions + #{ triple = "x86_64-unknown-linux-musl" }, + # You can also specify which target_features you promise are enabled for a + # particular target. target_features are currently not validated against + # the actual valid features supported by the target architecture. + #{ triple = "wasm32-unknown-unknown", features = ["atomics"] }, +] + +# This section is considered when running `cargo deny check advisories` +# More documentation for the advisories section can be found here: +# https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html +[advisories] +# The path where the advisory database is cloned/fetched into +db-path = "~/.cargo/advisory-db" +# The url(s) of the advisory databases to use +db-urls = ["https://github.com/rustsec/advisory-db"] +# The lint level for security vulnerabilities +vulnerability = "deny" +# The lint level for unmaintained crates +unmaintained = "warn" +# The lint level for crates that have been yanked from their source registry +yanked = "deny" +# The lint level for crates with security notices. Note that as of +# 2019-12-17 there are no security notice advisories in +# https://github.com/rustsec/advisory-db +notice = "warn" +# A list of advisory IDs to ignore. Note that ignored advisories will still +# output a note when they are encountered. +#ignore = [ +#] +# Threshold for security vulnerabilities, any vulnerability with a CVSS score +# lower than the range specified will be ignored. Note that ignored advisories +# will still output a note when they are encountered. +# * None - CVSS Score 0.0 +# * Low - CVSS Score 0.1 - 3.9 +# * Medium - CVSS Score 4.0 - 6.9 +# * High - CVSS Score 7.0 - 8.9 +# * Critical - CVSS Score 9.0 - 10.0 +#severity-threshold = + +# This section is considered when running `cargo deny check licenses` +# More documentation for the licenses section can be found here: +# https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html +[licenses] +# The lint level for crates which do not have a detectable license +unlicensed = "warn" +# List of explicitly allowed licenses +# See https://spdx.org/licenses/ for list of possible licenses +# [possible values: any SPDX 3.7 short identifier (+ optional exception)]. +allow = [ + "Apache-2.0", + "CC0-1.0", + "MIT", + "BSD-2-Clause", + "BSD-3-Clause", + "ISC", + "Zlib" +] +# List of explicitly disallowed licenses +# See https://spdx.org/licenses/ for list of possible licenses +# [possible values: any SPDX 3.7 short identifier (+ optional exception)]. +deny = [ + #"Nokia", +] +# Lint level for licenses considered copyleft +copyleft = "deny" +# Blanket approval or denial for OSI-approved or FSF Free/Libre licenses +# * both - The license will be approved if it is both OSI-approved *AND* FSF +# * either - The license will be approved if it is either OSI-approved *OR* FSF +# * osi-only - The license will be approved if is OSI-approved *AND NOT* FSF +# * fsf-only - The license will be approved if is FSF *AND NOT* OSI-approved +# * neither - This predicate is ignored and the default lint level is used +allow-osi-fsf-free = "neither" +# Lint level used when no other predicates are matched +# 1. License isn't in the allow or deny lists +# 2. License isn't copyleft +# 3. License isn't OSI/FSF, or allow-osi-fsf-free = "neither" +default = "deny" +# The confidence threshold for detecting a license from license text. +# The higher the value, the more closely the license text must be to the +# canonical license text of a valid SPDX license file. +# [possible values: any between 0.0 and 1.0]. +confidence-threshold = 0.8 +# Allow 1 or more licenses on a per-crate basis, so that particular licenses +# aren't accepted for every possible crate as with the normal allow list +exceptions = [ + # The Unicode-DFS-2016 license is necessary for unicode-ident because they + # use data from the unicode tables to generate the tables which are + # included in the application. We do not distribute those data files so + # this is not a problem for us. See https://github.com/dtolnay/unicode-ident/pull/9/files + { allow = ["Unicode-DFS-2016"], name = "unicode-ident", version = "*"}, +] + +# Some crates don't have (easily) machine readable licensing information, +# adding a clarification entry for it allows you to manually specify the +# licensing information +#[[licenses.clarify]] +# The name of the crate the clarification applies to +#name = "ring" +# The optional version constraint for the crate +#version = "*" +# The SPDX expression for the license requirements of the crate +#expression = "MIT AND ISC AND OpenSSL" +# One or more files in the crate's source used as the "source of truth" for +# the license expression. If the contents match, the clarification will be used +# when running the license check, otherwise the clarification will be ignored +# and the crate will be checked normally, which may produce warnings or errors +# depending on the rest of your configuration +#license-files = [ + # Each entry is a crate relative path, and the (opaque) hash of its contents + #{ path = "LICENSE", hash = 0xbd0eed23 } +#] + +[licenses.private] +# If true, ignores workspace crates that aren't published, or are only +# published to private registries +ignore = false +# One or more private registries that you might publish crates to, if a crate +# is only published to private registries, and ignore is true, the crate will +# not have its license(s) checked +registries = [ + #"https://sekretz.com/registry +] + +# This section is considered when running `cargo deny check bans`. +# More documentation about the 'bans' section can be found here: +# https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html +[bans] +# Lint level for when multiple versions of the same crate are detected +multiple-versions = "warn" +# Lint level for when a crate version requirement is `*` +wildcards = "allow" +# The graph highlighting used when creating dotgraphs for crates +# with multiple versions +# * lowest-version - The path to the lowest versioned duplicate is highlighted +# * simplest-path - The path to the version with the fewest edges is highlighted +# * all - Both lowest-version and simplest-path are used +highlight = "all" +# List of crates to deny +deny = [ + # Each entry the name of a crate and a version range. If version is + # not specified, all versions will be matched. + #{ name = "ansi_term", version = "=0.11.0" }, +] +# Certain crates/versions that will be skipped when doing duplicate detection. +skip = [ + #{ name = "ansi_term", version = "=0.11.0" }, +] +# Similarly to `skip` allows you to skip certain crates during duplicate +# detection. Unlike skip, it also includes the entire tree of transitive +# dependencies starting at the specified crate, up to a certain depth, which is +# by default infinite +skip-tree = [ + #{ name = "ansi_term", version = "=0.11.0", depth = 20 }, +] + +# This section is considered when running `cargo deny check sources`. +# More documentation about the 'sources' section can be found here: +# https://embarkstudios.github.io/cargo-deny/checks/sources/cfg.html +[sources] +# Lint level for what to happen when a crate from a crate registry that is not +# in the allow list is encountered +unknown-registry = "deny" +# Lint level for what to happen when a crate from a git repository that is not +# in the allow list is encountered +unknown-git = "deny" +# List of URLs for allowed crate registries. Defaults to the crates.io index +# if not specified. If it is specified but empty, no registries are allowed. +allow-registry = ["https://github.com/rust-lang/crates.io-index"] +# List of URLs for allowed Git repositories +allow-git = [] + +#[sources.allow-org] +# 1 or more github.com organizations to allow git sources for +#github = [""] +# 1 or more gitlab.com organizations to allow git sources for +#gitlab = [""] +# 1 or more bitbucket.org organizations to allow git sources for +#bitbucket = [""] diff --git a/docs/specs/latest.json b/docs/specs/latest.json new file mode 100644 index 0000000..1d6f24d --- /dev/null +++ b/docs/specs/latest.json @@ -0,0 +1,99 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "efected-coto-emmory", + "description": "a cloud service", + "contact": { + "name": "Zeeshan Lakhani", + "email": "zeeshan.lakhani@gmail.com" + }, + "license": { + "name": "Apache-2.0 or MIT" + }, + "version": "0.1.0" + }, + "paths": { + "/healthcheck": { + "get": { + "tags": [ + "health" + ], + "summary": "GET handler for checking service health.", + "description": "GET handler for checking service health.", + "operationId": "healthcheck", + "responses": { + "200": { + "description": "efected-coto-emmory healthy" + }, + "500": { + "description": "efected-coto-emmory not healthy", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AppError" + } + } + } + } + }, + "deprecated": false + } + }, + "/ping": { + "get": { + "tags": [ + "ping" + ], + "summary": "GET handler for internal pings and availability", + "description": "GET handler for internal pings and availability", + "operationId": "get", + "responses": { + "200": { + "description": "Ping successful" + }, + "500": { + "description": "Ping not successful", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AppError" + } + } + } + } + }, + "deprecated": false + } + } + }, + "components": { + "schemas": { + "AppError": { + "type": "object", + "description": "Encodes [JSONAPI error object responses](https://jsonapi.org/examples/#error-objects).\n\nJSONAPI error object - ALL Fields are technically optional.\n\nThis struct uses the following guidelines:\n\n1. Always encode the StatusCode of the response\n2. Set the title to the `canonical_reason` of the status code.\nAccording to spec, this should NOT change over time.\n3. For unrecoverable errors, encode the detail as the to_string of the error\n\nOther fields not currently captured (but can be added)\n\n- id - a unique identifier for the problem\n- links - a link object with further information about the problem\n- source - a JSON pointer indicating a problem in the request json OR\na parameter specifying a problematic query parameter\n- meta - a meta object containing arbitrary information about the error", + "required": [ + "status" + ], + "properties": { + "detail": { + "type": "string" + }, + "status": { + "type": "integer", + "format": "int32", + "example": 200 + }, + "title": { + "type": "string" + } + } + } + } + }, + "tags": [ + { + "name": "", + "description": "efected-coto-emmory service/middleware" + } + ] +} diff --git a/flake.lock b/flake.lock index ad68d6f..9324f4a 100644 --- a/flake.lock +++ b/flake.lock @@ -1,45 +1,46 @@ { "nodes": { - "naersk": { - "inputs": { - "nixpkgs": "nixpkgs" - }, + "flake-compat": { + "flake": false, "locked": { - "lastModified": 1698420672, - "narHash": "sha256-/TdeHMPRjjdJub7p7+w55vyABrsJlt5QkznPYy55vKA=", - "owner": "nix-community", - "repo": "naersk", - "rev": "aeb58d5e8faead8980a807c840232697982d47b9", + "lastModified": 1696426674, + "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", "type": "github" }, "original": { - "owner": "nix-community", - "ref": "master", - "repo": "naersk", + "owner": "edolstra", + "repo": "flake-compat", "type": "github" } }, - "nixpkgs": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, "locked": { - "lastModified": 1703134684, - "narHash": "sha256-SQmng1EnBFLzS7WSRyPM9HgmZP2kLJcPAz+Ug/nug6o=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "d6863cbcbbb80e71cecfc03356db1cda38919523", + "lastModified": 1701680307, + "narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "4022d587cbbfd70fe950c1e2083a02621806a725", "type": "github" }, "original": { - "id": "nixpkgs", - "type": "indirect" + "owner": "numtide", + "repo": "flake-utils", + "type": "github" } }, - "nixpkgs_2": { + "nixpkgs": { "locked": { - "lastModified": 1703134684, - "narHash": "sha256-SQmng1EnBFLzS7WSRyPM9HgmZP2kLJcPAz+Ug/nug6o=", + "lastModified": 1704626572, + "narHash": "sha256-VwRTEKzK4wSSv64G+g3RLF3t6yBHrhR2VK3kZ5UWisU=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "d6863cbcbbb80e71cecfc03356db1cda38919523", + "rev": "24fe8bb4f552ad3926274d29e083b79d84707da6", "type": "github" }, "original": { @@ -51,9 +52,33 @@ }, "root": { "inputs": { - "naersk": "naersk", - "nixpkgs": "nixpkgs_2", - "utils": "utils" + "flake-compat": "flake-compat", + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs", + "rust-overlay": "rust-overlay" + } + }, + "rust-overlay": { + "inputs": { + "flake-utils": [ + "flake-utils" + ], + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1704853054, + "narHash": "sha256-xD87M7isL2XqlFr+2f+j86jy8s5lfIaAEWO4TpQQZUA=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "6dea03e0c8a81cf28340564259d4762b6d6f01de", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" } }, "systems": { @@ -70,24 +95,6 @@ "repo": "default", "type": "github" } - }, - "utils": { - "inputs": { - "systems": "systems" - }, - "locked": { - "lastModified": 1701680307, - "narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "4022d587cbbfd70fe950c1e2083a02621806a725", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } } }, "root": "root", diff --git a/flake.nix b/flake.nix index 2f907a6..b0886aa 100644 --- a/flake.nix +++ b/flake.nix @@ -1,21 +1,102 @@ { + description = "efected-coto-emmory"; + inputs = { - naersk.url = "github:nix-community/naersk/master"; nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; - utils.url = "github:numtide/flake-utils"; + flake-utils.url = "github:numtide/flake-utils"; + + flake-compat = { + url = "github:edolstra/flake-compat"; + flake = false; + }; + + rust-overlay = { + url = "github:oxalica/rust-overlay"; + inputs.nixpkgs.follows = "nixpkgs"; + inputs.flake-utils.follows = "flake-utils"; + }; }; - outputs = { self, nixpkgs, utils, naersk }: - utils.lib.eachDefaultSystem (system: - let - pkgs = import nixpkgs { inherit system; }; - naersk-lib = pkgs.callPackage naersk { }; - in + outputs = { + self, + nixpkgs, + flake-compat, + flake-utils, + rust-overlay, + } @ inputs: + flake-utils.lib.eachDefaultSystem ( + system: let + overlays = [(import rust-overlay)]; + pkgs = import nixpkgs {inherit system overlays;}; + + rust-toolchain = (pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml).override { + extensions = ["cargo" "clippy" "rustfmt" "rust-src" "rust-std"]; + }; + + nightly-rustfmt = pkgs.rust-bin.nightly.latest.rustfmt; + + format-pkgs = with pkgs; [ + nixpkgs-fmt + alejandra + ]; + + cargo-installs = with pkgs; [ + cargo-deny + cargo-expand + cargo-nextest + cargo-outdated + cargo-spellcheck + cargo-sort + cargo-udeps + cargo-watch + ]; + in rec { - defaultPackage = naersk-lib.buildPackage ./.; - devShell = with pkgs; mkShell { - buildInputs = [ cargo rustc rustfmt pre-commit rustPackages.clippy ]; - RUST_SRC_PATH = rustPlatform.rustLibSrc; + devShells.default = pkgs.mkShell { + name = "efected-coto-emmory"; + nativeBuildInputs = with pkgs; + [ + # The ordering of these two items is important. For nightly rustfmt to be used instead of + # the rustfmt provided by `rust-toolchain`, it must appear first in the list. This is + # because native build inputs are added to $PATH in the order they're listed here. + nightly-rustfmt + rust-toolchain + openssl + pkg-config + ffmpeg + pre-commit + protobuf + direnv + self.packages.${system}.irust + ] + ++ format-pkgs + ++ cargo-installs + ++ lib.optionals stdenv.isDarwin [ + darwin.apple_sdk.frameworks.Security + darwin.apple_sdk.frameworks.CoreFoundation + darwin.apple_sdk.frameworks.Foundation + ]; + + shellHook = '' + [ -e .git/hooks/pre-commit ] || pre-commit install --install-hooks && pre-commit install --hook-type commit-msg + ''; + }; + + packages.irust = pkgs.rustPlatform.buildRustPackage rec { + pname = "irust"; + version = "1.70.0"; + src = pkgs.fetchFromGitHub { + owner = "sigmaSd"; + repo = "IRust"; + rev = "v${version}"; + sha256 = "sha256-chZKesbmvGHXwhnJRZbXyX7B8OwJL9dJh0O1Axz/n2E="; + }; + + doCheck = false; + cargoSha256 = "sha256-FmsD3ajMqpPrTkXCX2anC+cmm0a2xuP+3FHqzj56Ma4="; }; - }); + + formatter = pkgs.alejandra; + } + ); } diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..292fe49 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "stable" diff --git a/src/bin/openapi.rs b/src/bin/openapi.rs new file mode 100644 index 0000000..e802e87 --- /dev/null +++ b/src/bin/openapi.rs @@ -0,0 +1,35 @@ +use std::{fs::File, io::prelude::*, path::PathBuf}; +use utoipa::OpenApi; +use efected_coto_emmory::docs::ApiDoc; + +fn main() { + let json_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("docs/specs/latest.json"); + let json_path_show = json_path.as_path().display().to_string(); + + let mut file = match File::create(json_path) { + Ok(file) => file, + Err(err) => { + eprintln!("error creating file: {err:?}"); + std::process::exit(1) + } + }; + + let json = match ApiDoc::openapi().to_pretty_json() { + Ok(mut json) => { + json.push('\n'); + json + } + Err(err) => { + eprintln!("error generating OpenAPI json: {err:?}"); + std::process::exit(1) + } + }; + + match file.write_all(json.as_bytes()) { + Ok(_) => println!("OpenAPI json written to path: {json_path_show}\n\n{json}"), + Err(err) => { + eprintln!("error writing to file. {err:?}"); + std::process::exit(1) + } + } +} diff --git a/src/docs.rs b/src/docs.rs new file mode 100644 index 0000000..0842870 --- /dev/null +++ b/src/docs.rs @@ -0,0 +1,21 @@ +//! OpenAPI doc generation. + +use crate::{ + error::AppError, + routes::{health, ping}, +}; +use utoipa::OpenApi; + +/// API documentation generator. +#[derive(OpenApi)] +#[openapi( + paths(health::healthcheck, ping::get), + components(schemas(AppError)), + tags( + (name = "", description = "efected-coto-emmory service/middleware") + ) + )] + +/// Tied to OpenAPI documentation. +#[derive(Debug)] +pub struct ApiDoc; diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..88b390f --- /dev/null +++ b/src/error.rs @@ -0,0 +1,217 @@ +//! Generic result/error resprentation(s). + +use axum::{ + http::StatusCode, + response::{IntoResponse, Response}, + Json, +}; + +use serde::{Deserialize, Serialize}; +use tracing::warn; +use ulid::Ulid; +use utoipa::ToSchema; + +/// Standard return type out of routes / handlers +pub type AppResult = std::result::Result; + +/// Encodes [JSONAPI error object responses](https://jsonapi.org/examples/#error-objects). +/// +/// JSONAPI error object - ALL Fields are technically optional. +/// +/// This struct uses the following guidelines: +/// +/// 1. Always encode the StatusCode of the response +/// 2. Set the title to the `canonical_reason` of the status code. +/// According to spec, this should NOT change over time. +/// 3. For unrecoverable errors, encode the detail as the to_string of the error +/// +/// Other fields not currently captured (but can be added) +/// +/// - id - a unique identifier for the problem +/// - links - a link object with further information about the problem +/// - source - a JSON pointer indicating a problem in the request json OR +/// a parameter specifying a problematic query parameter +/// - meta - a meta object containing arbitrary information about the error +#[derive(ToSchema, thiserror::Error, Eq, PartialEq, Debug, Deserialize, Serialize)] +pub struct AppError { + #[schema(value_type = u16, example = 200)] + #[serde(with = "crate::error::serde_status_code")] + status: StatusCode, + detail: Option, + title: Option, +} + +impl AppError { + /// New instance of [AppError]. + pub fn new(status_code: StatusCode, message: Option) -> AppError { + Self { + status: status_code, + title: Self::canonical_reason_to_string(&status_code), + detail: message.map(|m| m.to_string()), + } + } + + /// [AppError] for [StatusCode::NOT_FOUND]. + pub fn not_found(id: Ulid) -> AppError { + Self::new( + StatusCode::NOT_FOUND, + Some(format!("Entity with id {id} not found")), + ) + } + + fn canonical_reason_to_string(status_code: &StatusCode) -> Option { + status_code.canonical_reason().map(|r| r.to_string()) + } +} + +#[derive(Debug, Deserialize, Serialize)] +/// Error in JSON API response format. +pub struct ErrorResponse { + errors: Vec, +} + +impl From for ErrorResponse { + fn from(e: AppError) -> Self { + Self { errors: vec![e] } + } +} + +impl From for (StatusCode, Json) { + fn from(app_error: AppError) -> Self { + (app_error.status, Json(app_error.into())) + } +} + +impl IntoResponse for AppError { + fn into_response(self) -> Response { + let error_response: (StatusCode, Json) = self.into(); + error_response.into_response() + } +} + +impl From for AppError { + fn from(err: anyhow::Error) -> Self { + warn!( + subject = "app_error", + category = "app_error", + "encountered unexpected error {:#}: {:#}", + err, + err.backtrace() + ); + Self { + status: StatusCode::INTERNAL_SERVER_ERROR, + title: StatusCode::INTERNAL_SERVER_ERROR + .canonical_reason() + .map(|r| r.to_string()), + detail: Some(err.to_string()), + } + } +} + +/// Serialize/Deserializer for status codes. +/// +/// This is needed because status code according to JSON API spec must +/// be the status code as a STRING. +/// +/// We could have used http_serde, but it encodes the status code as a NUMBER. +pub mod serde_status_code { + use http::StatusCode; + use serde::{de::Unexpected, Deserialize, Deserializer, Serialize, Serializer}; + + /// Serialize [StatusCode]s. + pub fn serialize(status: &StatusCode, ser: S) -> Result { + String::serialize(&status.as_u16().to_string(), ser) + } + + /// Deserialize [StatusCode]s. + pub fn deserialize<'de, D>(de: D) -> Result + where + D: Deserializer<'de>, + { + let str = String::deserialize(de)?; + StatusCode::from_bytes(str.as_bytes()).map_err(|_| { + serde::de::Error::invalid_value( + Unexpected::Str(str.as_str()), + &"A valid http status code", + ) + }) + } +} + +// Needed to support thiserror::Error, outputs debug for AppError +impl std::fmt::Display for AppError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{self:?}") + } +} + +#[cfg(test)] +/// Parse the app error out of the json body +pub async fn parse_error(response: Response) -> AppError { + let body_bytes = hyper::body::to_bytes(response.into_body()).await.unwrap(); + let mut err_response: ErrorResponse = serde_json::from_slice(&body_bytes).unwrap(); + err_response.errors.remove(0) +} + +#[cfg(test)] +mod tests { + use super::*; + use axum::{http::StatusCode, response::IntoResponse}; + use ulid::Ulid; + + #[test] + fn test_from_anyhow_error() { + let err: AppError = anyhow::anyhow!("FAIL").into(); + assert_eq!(err.detail.unwrap(), "FAIL".to_string()); + assert_eq!( + err.title, + StatusCode::INTERNAL_SERVER_ERROR + .canonical_reason() + .map(|r| r.to_string()) + ); + + assert_eq!(err.status, StatusCode::INTERNAL_SERVER_ERROR); + } + + #[test] + fn test_not_found() { + let id = Ulid::new(); + let err = AppError::not_found(id); + + assert_eq!(err.status, StatusCode::NOT_FOUND); + assert_eq!( + err.title, + StatusCode::NOT_FOUND + .canonical_reason() + .map(|r| r.to_string()) + ); + assert_eq!( + err.detail.unwrap(), + format!("Entity with id {id} not found") + ); + } + + #[tokio::test] + async fn test_json_api_error_response() { + // verify that our json api response complies with the standard + let id = Ulid::new(); + let err = AppError::not_found(id); + let response = err.into_response(); + assert_eq!(response.status(), StatusCode::NOT_FOUND); + + let err = parse_error(response).await; + + // Check that the result is all good + assert_eq!(err.status, StatusCode::NOT_FOUND); + assert_eq!( + err.title, + StatusCode::NOT_FOUND + .canonical_reason() + .map(|r| r.to_string()) + ); + assert_eq!( + err.detail.unwrap(), + format!("Entity with id {id} not found") + ); + } +} diff --git a/src/extract/json.rs b/src/extract/json.rs new file mode 100644 index 0000000..7ccaa08 --- /dev/null +++ b/src/extract/json.rs @@ -0,0 +1,315 @@ +//! JSON Extrator / Response replacement for [axum::extract::Json]. + +use async_trait::async_trait; +use axum::{ + body::{Bytes, HttpBody}, + extract::FromRequest, + response::{IntoResponse, Response}, + BoxError, +}; +use http::{ + header::{self, HeaderMap, HeaderValue}, + Request, StatusCode, +}; +use serde::{de::DeserializeOwned, Serialize}; +use std::ops::{Deref, DerefMut}; +use tracing::warn; + +use crate::error::AppError; + +/// JSON Extractor / Response. +/// Built to replace axum::extract::Json, due to the manner of error response. +/// [axum::extract::Json] does not provide much useful information when json parsing fails. +/// This will parse the json using [`serde_path_to_error`] which adds much better +/// context on parsing errors. +/// +/// When used as an extractor, it can deserialize request bodies into some type that +/// implements [serde::Deserialize]. The request will be rejected (and a [AppError] will +/// be returned) if: +/// +/// - The request doesn't have a `Content-Type: application/json` (or similar) header. +/// - The body doesn't contain syntactically valid JSON. +/// - The body contains syntactically valid JSON but it couldn't be deserialized into the target +/// type. +/// - Buffering the request body fails. +/// +/// See [AppError] for more details. +/// +/// # Extractor example +/// +/// ```rust,no_run +/// use axum::{ +/// response, +/// routing::post, +/// Router, +/// }; +/// use efected_coto_emmory::extract; +/// use serde::Deserialize; +/// +/// #[derive(Deserialize)] +/// struct CreateUser { +/// email: String, +/// password: String, +/// } +/// +/// async fn create_user(extract::json::Json(payload): extract::json::Json) { +/// // payload is a `CreateUser` +/// } +/// +/// let app = Router::new().route("/users", post(create_user)); +/// # async { +/// # axum::Server::bind(&"".parse().unwrap()).serve(app.into_make_service()).await.unwrap(); +/// # }; +/// ``` +#[derive(Debug, Clone, Copy, Default)] +pub struct Json(pub T); + +#[async_trait] +impl FromRequest for Json +where + T: DeserializeOwned, + B: HttpBody + Send + 'static, + B::Data: Send, + B::Error: Into, + S: Send + Sync, +{ + type Rejection = AppError; + + async fn from_request(req: Request, state: &S) -> Result { + if json_content_type(req.headers()) { + let bytes = Bytes::from_request(req, state).await.map_err(|err| { + warn!( + subject = "request", + category = "parsing", + "unable to parse request body {:#}", + err, + ); + AppError::new( + StatusCode::BAD_REQUEST, + Some("Unable to parse request body"), + ) + })?; + let jd = &mut serde_json::Deserializer::from_slice(bytes.as_ref()); + let result: Result = serde_path_to_error::deserialize(jd); + match result { + Ok(value) => Ok(Json(value)), + Err(err) => { + let err_response = match err.inner().classify() { + serde_json::error::Category::Data => { + warn!( + subject = "request", + category = "parsing", + json_error_path = ?err.path().to_string(), + "failed to deserialize the JSON body into the target type, json error: {:#}", + err.inner() + ); + AppError::new( + StatusCode::UNPROCESSABLE_ENTITY, + Some(format!( + "failed to deserialize the JSON body into the target type, json error path: {}; json error {:#}", + err.path(), + err.inner() + )) + ) + } + _ => { + warn!( + subject = "request", + category = "parsing", + "failed to parse the request body as JSON; json error {:#}", + err.inner() + ); + AppError::new( + StatusCode::BAD_REQUEST, + Some(format!( + "failed to parse the request body as JSON; json error {:#}", + err.inner() + )), + ) + } + }; + Err(err_response) + } + } + } else { + Err(AppError::new( + StatusCode::UNSUPPORTED_MEDIA_TYPE, + Some("Expected request with `Content-Type: application/json`"), + )) + } + } +} + +fn json_content_type(headers: &HeaderMap) -> bool { + headers + .get(header::CONTENT_TYPE) + .and_then(|content_type| content_type.to_str().ok()) + .and_then(|content_type| content_type.parse::().ok()) + .map(|mime| { + mime.type_() == "application" + && (mime.subtype() == "json" || mime.suffix().map_or(false, |name| name == "json")) + }) + .unwrap_or(false) +} + +impl Deref for Json { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for Json { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl From for Json { + fn from(inner: T) -> Self { + Self(inner) + } +} + +impl IntoResponse for Json +where + T: Serialize, +{ + fn into_response(self) -> Response { + match serde_json::to_vec(&self.0) { + Ok(bytes) => ( + [( + header::CONTENT_TYPE, + HeaderValue::from_static(mime::APPLICATION_JSON.as_ref()), + )], + bytes, + ) + .into_response(), + Err(err) => ( + StatusCode::INTERNAL_SERVER_ERROR, + [( + header::CONTENT_TYPE, + HeaderValue::from_static(mime::TEXT_PLAIN_UTF_8.as_ref()), + )], + err.to_string(), + ) + .into_response(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use axum::routing::{get, Router}; + use http::Request; + use hyper::Body; + use serde::Deserialize; + use tower::ServiceExt; + + #[derive(Debug, Deserialize)] + struct Input { + foo: String, + } + + #[tokio::test] + async fn deserialize_body() { + let app = Router::new().route( + "/", + get(|input: Json| async { (StatusCode::OK, input.0.foo) }), + ); + let contents = r#"{ "foo": "bar" }"#; + let req_body: Body = contents.into(); + let response = app + .oneshot( + Request::builder() + .uri("/") + .header("Content-Type", "application/json") + .body(req_body) + .unwrap(), + ) + .await + .unwrap(); + + let body = hyper::body::to_bytes(response.into_body()).await.unwrap(); + let body_text = std::str::from_utf8(&body[..]).unwrap(); + dbg!(body_text); + assert_eq!(body_text, "bar"); + } + + #[tokio::test] + async fn consume_body_to_json_requires_json_content_type() { + let app = Router::new().route( + "/", + get(|input: Json| async { (StatusCode::OK, input.0.foo) }), + ); + let contents = r#"{ "foo": "bar" }"#; + let req_body: Body = contents.into(); + let response = app + .oneshot( + Request::builder() + .uri("/") + .header("Content-Type", "application/text") + .body(req_body) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::UNSUPPORTED_MEDIA_TYPE); + } + + #[tokio::test] + async fn json_content_types() { + async fn valid_json_content_type(content_type: &str) -> bool { + dbg!(content_type); + + let app = Router::new().route( + "/", + get(|input: Json| async { (StatusCode::OK, input.0.foo) }), + ); + let contents = r#"{ "foo": "bar" }"#; + let req_body: Body = contents.into(); + let response = app + .oneshot( + Request::builder() + .uri("/") + .header("Content-Type", content_type) + .body(req_body) + .unwrap(), + ) + .await + .unwrap(); + + response.status() == StatusCode::OK + } + + assert!(valid_json_content_type("application/json").await); + assert!(valid_json_content_type("application/json; charset=utf-8").await); + assert!(valid_json_content_type("application/json;charset=utf-8").await); + assert!(valid_json_content_type("application/cloudevents+json").await); + assert!(!valid_json_content_type("text/json").await); + } + + #[tokio::test] + async fn invalid_json_syntax() { + let app = Router::new().route( + "/", + get(|input: Json| async { (StatusCode::OK, input.0.foo) }), + ); + let contents = "{"; + let req_body: Body = contents.into(); + let response = app + .oneshot( + Request::builder() + .uri("/") + .header("Content-Type", "application/json") + .body(req_body) + .unwrap(), + ) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::BAD_REQUEST); + } +} diff --git a/src/extract/mod.rs b/src/extract/mod.rs new file mode 100644 index 0000000..502ae65 --- /dev/null +++ b/src/extract/mod.rs @@ -0,0 +1,3 @@ +//! Custom [axum::extract] Extractors. + +pub mod json; diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs new file mode 100644 index 0000000..d5c1c84 --- /dev/null +++ b/src/handlers/mod.rs @@ -0,0 +1 @@ +pub mod my; diff --git a/src/handlers/my.rs b/src/handlers/my.rs new file mode 100644 index 0000000..66ce4a4 --- /dev/null +++ b/src/handlers/my.rs @@ -0,0 +1,7 @@ + +// this is a handler, a function that is used in a Router +// A handler is an async function that accepts zero or more “extractors” as arguments and returns something that can be converted into a response. +pub async fn say_hello() -> String { + // integrate new functionality here: + return "Hello!!!".to_string(); +} diff --git a/src/headers/header.rs b/src/headers/header.rs new file mode 100644 index 0000000..d042e04 --- /dev/null +++ b/src/headers/header.rs @@ -0,0 +1,109 @@ +use axum::{extract::TypedHeader, headers::Header}; + +/// Generate String-focused, generic, custom typed [`Header`]'s. +#[allow(unused)] +macro_rules! header { + ($tname:ident, $hname:ident, $sname:expr) => { + static $hname: once_cell::sync::Lazy = + once_cell::sync::Lazy::new(|| axum::headers::HeaderName::from_static($sname)); + + #[doc = "Generated custom [`axum::headers::Header`] for "] + #[doc = $sname] + #[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] + pub(crate) struct $tname(pub(crate) String); + + impl std::fmt::Display for $tname { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } + } + + impl std::convert::From<&str> for $tname { + fn from(item: &str) -> Self { + $tname(item.to_string()) + } + } + + impl axum::headers::Header for $tname { + fn name() -> &'static axum::headers::HeaderName { + &$hname + } + + fn decode<'i, I>(values: &mut I) -> Result + where + I: Iterator, + { + values + .next() + .and_then(|v| v.to_str().ok()) + .map(|x| $tname(x.to_string())) + .ok_or_else(axum::headers::Error::invalid) + } + + fn encode(&self, values: &mut E) + where + E: Extend, + { + if let Ok(value) = axum::headers::HeaderValue::from_str(&self.0) { + values.extend(std::iter::once(value)); + } + } + } + }; +} + +/// Trait for returning header value directly for passing +/// along to client calls. +pub(crate) trait HeaderValue { + fn header_value(&self) -> String; +} + +impl HeaderValue for TypedHeader +where + T: Header + std::fmt::Display, +{ + fn header_value(&self) -> String { + self.0.to_string() + } +} + +impl HeaderValue for &TypedHeader +where + T: Header + std::fmt::Display, +{ + fn header_value(&self) -> String { + self.0.to_string() + } +} + +#[cfg(test)] +pub(crate) mod tests { + use axum::{ + headers::{Header, HeaderMapExt}, + http, + }; + + header!(XDummyId, XDUMMY_ID, "x-dummy-id"); + + fn test_decode(values: &[&str]) -> Option { + let mut map = http::HeaderMap::new(); + for val in values { + map.append(T::name(), val.parse().unwrap()); + } + map.typed_get() + } + + fn test_encode(header: T) -> http::HeaderMap { + let mut map = http::HeaderMap::new(); + map.typed_insert(header); + map + } + + #[test] + fn test_dummy_header() { + let s = "18312349-3139-498C-84B6-87326BF1F2A7"; + let dummy_id = test_decode::(&[s]).unwrap(); + let headers = test_encode(dummy_id); + assert_eq!(headers["x-dummy-id"], s); + } +} diff --git a/src/headers/mod.rs b/src/headers/mod.rs new file mode 100644 index 0000000..98dbe75 --- /dev/null +++ b/src/headers/mod.rs @@ -0,0 +1,3 @@ +//! Working-with and generating custom headers. + +pub(crate) mod header; diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..a4cffb1 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,53 @@ +#![cfg_attr(docsrs, feature(doc_cfg))] +#![warn(missing_debug_implementations, missing_docs, rust_2018_idioms)] +#![deny(unreachable_pub)] + +//! efected-coto-emmory + +pub mod docs; +pub mod error; +pub mod extract; +pub mod headers; +pub mod metrics; +pub mod middleware; +pub mod router; +pub mod routes; +pub mod settings; +pub mod tracer; +pub mod tracing_layers; + +use axum::{ + Json, + response::{Html, IntoResponse}, + http::{StatusCode, Uri, header::{self, HeaderMap, HeaderName}}, +}; +use std::path::PathBuf; +use std::io::{BufWriter, Cursor}; +use image::ImageFormat; + +/// Test utilities. +#[cfg(any(test, feature = "test_utils"))] +#[cfg_attr(docsrs, doc(cfg(feature = "test_utils")))] +pub mod test_utils; +/// Add two integers together. +pub fn add(a: i32, b: i32) -> i32 { + a + b +} +// stream. +pub async fn get_image() -> impl axum::response::IntoResponse { + let img_path = PathBuf::from("assets/").join("a_logo.png"); + let image = image::io::Reader::open(&img_path).unwrap().decode().unwrap(); + let mut buffer = BufWriter::new(Cursor::new(Vec::new())); + image.write_to(&mut buffer, ImageFormat::Png).unwrap(); + let bytes: Vec = buffer.into_inner().unwrap().into_inner(); + ( + axum::response::AppendHeaders([(header::CONTENT_TYPE, "image/png")]), + bytes + // image.into_bytes() + ) +} + +pub async fn html() -> Html<&'static str> { + Html("

Hello, World!

") +} +// roadmap - show webcam on laptop screen. diff --git a/src/main.rs b/src/main.rs index b35ed0f..4110189 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,15 +1,222 @@ -use std::io; +//! efected-coto-emmory -fn main() { - println!("Guess the number!"); +use anyhow::Result; +use axum::{extract::Extension, headers::HeaderName, routing::get, Router}; +use axum_tracing_opentelemetry::{opentelemetry_tracing_layer, response_with_trace_layer}; +use http::header; +use std::{ + future::ready, + io, + net::{IpAddr, Ipv4Addr, SocketAddr}, + time::Duration, +}; +use tokio::signal::{ + self, + unix::{signal, SignalKind}, +}; +use tower::ServiceBuilder; +use tower_http::{ + catch_panic::CatchPanicLayer, sensitive_headers::SetSensitiveHeadersLayer, + timeout::TimeoutLayer, ServiceBuilderExt, +}; +use tracing::info; +use tracing_subscriber::{ + filter::{dynamic_filter_fn, filter_fn, LevelFilter}, + prelude::*, + EnvFilter, +}; +use utoipa::OpenApi; +use utoipa_swagger_ui::SwaggerUi; +use efected_coto_emmory::{ + docs::ApiDoc, + get_image, + metrics::{process, prom::setup_metrics_recorder}, + middleware::{self, request_ulid::MakeRequestUlid, runtime}, + router, + routes::fallback::notfound_404, + settings::{Otel, Settings}, + tracer::init_tracer, + tracing_layers::{ + format_layer::LogFmtLayer, + metrics_layer::{MetricsLayer, METRIC_META_PREFIX}, + storage_layer::StorageLayer, + }, +}; - println!("Please input your guess."); +use handlers::my; +pub mod handlers; - let mut guess = String::new(); +/// Request identifier field. +const REQUEST_ID: &str = "request_id"; - io::stdin() - .read_line(&mut guess) - .expect("Failed to read line"); +#[tokio::main] +async fn main() -> Result<()> { + // IO + let (stdout_writer, _stdout_guard) = tracing_appender::non_blocking(io::stdout()); - println!("You guessed: {guess}"); + // sui ex + let settings = Settings::load()?; + setup_tracing(stdout_writer, settings.otel())?; + + // tracing? + info!( + subject = "app_settings", + category = "init", + "starting with settings: {:?}", + settings, + ); + + let env = settings.environment(); + let recorder_handle = setup_metrics_recorder()?; + + let app_metrics = async { + let metrics_router = Router::new() + .route("/metrics", get(move || ready(recorder_handle.render()))) + .fallback(notfound_404); + + let router = metrics_router.layer(CatchPanicLayer::custom(runtime::catch_panic)); + + // Spawn tick-driven process collection task + tokio::task::spawn(process::collect_metrics( + settings.monitoring().process_collector_interval, + )); + + serve("Metrics", router, settings.server().metrics_port).await + }; + + let app = async { + let req_id = HeaderName::from_static(REQUEST_ID); // used by the ServiceBuilder + // Router is used to set up which paths goes to which services: + let router = router::setup_app_router() + .route_layer(axum::middleware::from_fn(middleware::metrics::track)) + // layer adds additional processing to a request for a group of routes + .layer(Extension(env)) + // Include trace context as header into the response. + .layer(response_with_trace_layer()) + // Opentelemetry tracing middleware. + // This returns a `TraceLayer` configured to use + // OpenTelemetry’s conventional span field names. + .layer(opentelemetry_tracing_layer()) + // Set and propagate "request_id" (as a ulid) per request. + .layer( + ServiceBuilder::new() + .set_request_id(req_id.clone(), MakeRequestUlid) + .propagate_request_id(req_id), + ) + // Applies the `tower_http::timeout::Timeout` middleware which + // applies a timeout to requests. + .layer(TimeoutLayer::new(Duration::from_millis( + settings.server().timeout_ms, + ))) + // Catches runtime panics and converts them into + // `500 Internal Server` responses. + .layer(CatchPanicLayer::custom(runtime::catch_panic)) + // Mark headers as sensitive on both requests and responses. + .layer(SetSensitiveHeadersLayer::new([header::AUTHORIZATION])) + .route("/say-hello", get(handlers::my::say_hello)) + .route("/test-lib", get(get_image)) + .merge(SwaggerUi::new("/swagger-ui").url("/api-doc/openapi.json", ApiDoc::openapi())); + + serve("Application", router, settings.server().port).await + }; + + tokio::try_join!(app, app_metrics)?; + Ok(()) +} +// to serve means to run with a Router app at a port and address. +async fn serve(name: &str, app: Router, port: u16) -> Result<()> { + let bind_addr: SocketAddr = SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), port); + info!( + subject = "app_start", + category = "init", + "{} server listening on {}", + name, + bind_addr + ); + + // server binds an address to serve the app at it + axum::Server::bind(&bind_addr) + .serve(app.into_make_service_with_connect_info::()) + .with_graceful_shutdown(shutdown()) + .await?; + + Ok(()) +} + +/// Captures and waits for system signals. +async fn shutdown() { + #[cfg(unix)] + let term = async { + signal(SignalKind::terminate()) + .expect("Failed to listen for SIGTERM") + .recv() + .await + }; + + #[cfg(not(unix))] + let term = std::future::pending::<()>(); + + tokio::select! { + _ = signal::ctrl_c() => {} + _ = term => {} + } +} + +/// Setup all [tracing][tracing] layers for storage, request/response tracing, +/// logging and metrics. +fn setup_tracing( + writer: tracing_appender::non_blocking::NonBlocking, + settings_otel: &Otel, +) -> Result<()> { + let tracer = init_tracer(settings_otel)?; + + let registry = tracing_subscriber::Registry::default() + .with(StorageLayer.with_filter(LevelFilter::TRACE)) + .with( + tracing_opentelemetry::layer() + .with_tracer(tracer) + .with_filter(LevelFilter::DEBUG) + .with_filter(dynamic_filter_fn(|_metadata, ctx| { + !ctx.lookup_current() + // Exclude the rustls session "Connection" events + // which don't have a parent span + .map(|s| s.parent().is_none() && s.name() == "Connection") + .unwrap_or_default() + })), + ) + .with(LogFmtLayer::new(writer).with_target(true).with_filter( + EnvFilter::try_from_default_env().unwrap_or_else(|_| { + EnvFilter::new( + std::env::var("RUST_LOG") + .unwrap_or_else(|_| "efected_coto_emmory=info,tower_http=info,reqwest_retry=info,axum_tracing_opentelemetry=info".into()), + ) + }), + )) + .with( + MetricsLayer + .with_filter(LevelFilter::TRACE) + .with_filter(filter_fn(|metadata| { + // Filter and allow only: + // a) special metric prefix; + // b) any event + metadata.name().starts_with(METRIC_META_PREFIX) || metadata.is_event() + })), + ); + + #[cfg(all(feature = "console", tokio_unstable))] + #[cfg_attr(docsrs, doc(cfg(feature = "console")))] + { + let console_layer = console_subscriber::ConsoleLayer::builder() + .retention(Duration::from_secs(60)) + .spawn(); + + registry.with(console_layer).init(); + } + + #[cfg(any(not(feature = "console"), not(tokio_unstable)))] + { + registry.init(); + } + + Ok(()) } diff --git a/src/metrics/mod.rs b/src/metrics/mod.rs new file mode 100644 index 0000000..4760b7d --- /dev/null +++ b/src/metrics/mod.rs @@ -0,0 +1,4 @@ +//! Metrics capture and Prometheus recorder. + +pub mod process; +pub mod prom; diff --git a/src/metrics/process.rs b/src/metrics/process.rs new file mode 100644 index 0000000..9ae4aa4 --- /dev/null +++ b/src/metrics/process.rs @@ -0,0 +1,117 @@ +//! Server process metrics, including cpu, memory, disk, etc. + +use anyhow::{anyhow, Context, Result}; +use metrics::{describe_gauge, Unit}; +use std::time::Duration; +use sysinfo::{get_current_pid, ProcessExt, System, SystemExt}; +use tracing::{info, warn}; + +/// Create and describe gauges for process metrics. +pub(crate) fn describe() { + describe_gauge!( + "process_cpu_usage_percentage", + Unit::Percent, + "The CPU percentage used." + ); + describe_gauge!( + "process_virtual_memory_bytes", + Unit::Bytes, + "The virtual memory size in bytes." + ); + describe_gauge!("process_memory_bytes", Unit::Bytes, "Memory size in bytes."); + describe_gauge!( + "process_disk_total_written_bytes", + Unit::Bytes, + "The total bytes written to disk." + ); + describe_gauge!( + "process_disk_written_bytes", + Unit::Bytes, + "The bytes written to disk." + ); + describe_gauge!( + "process_disk_total_read_bytes", + Unit::Bytes, + "Total bytes Read from disk." + ); + describe_gauge!( + "process_disk_read_bytes", + Unit::Bytes, + "The bytes read from disk." + ); + describe_gauge!( + "process_disk_written_bytes", + Unit::Bytes, + "The bytes written to disk." + ); + describe_gauge!( + "process_uptime_seconds", + Unit::Seconds, + "How much time the process has been running in seconds." + ); +} + +/// Collection process metrics on a settings-defined interval. +pub async fn collect_metrics(interval: u64) { + let mut interval = tokio::time::interval(Duration::from_secs(interval)); + + loop { + interval.tick().await; + let sys_info = System::new(); + if let Err(err) = get_proc_stats(sys_info).await { + warn!( + subject = "metrics.process_collection", + category = "metrics", + "failure to get process statistics {:#?}", + err + ); + } + } +} + +async fn get_proc_stats(mut sys: System) -> Result<()> { + let pid = get_current_pid().map_err(|e| anyhow!("no process pid found {}", e))?; + + let is_process_refreshed = sys.refresh_process(pid); + + if is_process_refreshed { + let proc = sys.process(pid).context("no process associated with pid")?; + let cpus = num_cpus::get(); + let disk = proc.disk_usage(); + + // cpu-usage divided by # of cores. + metrics::gauge!( + "process_cpu_usage_percentage", + f64::from(proc.cpu_usage() / (cpus as f32)) + ); + + // The docs for sysinfo indicate that `virtual_memory` + // returns in KB, but that is incorrect. + // See this issue: https://github.com/GuillaumeGomez/sysinfo/issues/428#issuecomment-774098021 + // And this PR: https://github.com/GuillaumeGomez/sysinfo/pull/430/files + metrics::gauge!( + "process_virtual_memory_bytes", + (proc.virtual_memory()) as f64 + ); + metrics::gauge!("process_memory_bytes", (proc.memory() * 1_000) as f64); + metrics::gauge!("process_uptime_seconds", proc.run_time() as f64); + metrics::gauge!( + "process_disk_total_written_bytes", + disk.total_written_bytes as f64, + ); + metrics::gauge!("process_disk_written_bytes", disk.written_bytes as f64); + metrics::gauge!( + "process_disk_total_read_bytes", + disk.total_read_bytes as f64, + ); + metrics::gauge!("process_disk_read_bytes", disk.read_bytes as f64); + } else { + info!( + subject = "metrics.process_collection", + category = "metrics", + "failed to refresh process information, metrics may show old results" + ); + } + + Ok(()) +} diff --git a/src/metrics/prom.rs b/src/metrics/prom.rs new file mode 100644 index 0000000..abff339 --- /dev/null +++ b/src/metrics/prom.rs @@ -0,0 +1,23 @@ +//! Metrics Prometheus recorder. + +use crate::metrics::process; + +use metrics_exporter_prometheus::{Matcher, PrometheusBuilder, PrometheusHandle}; + +/// Sets up Prometheus buckets for matched metrics and installs recorder. +pub fn setup_metrics_recorder() -> anyhow::Result { + const EXPONENTIAL_SECONDS: &[f64] = &[ + 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, + ]; + + let builder = PrometheusBuilder::new() + .set_buckets_for_metric( + Matcher::Suffix("_duration_seconds".to_string()), + EXPONENTIAL_SECONDS, + )? + .install_recorder()?; + + process::describe(); + + Ok(builder) +} diff --git a/src/middleware/client/metrics.rs b/src/middleware/client/metrics.rs new file mode 100644 index 0000000..0d70507 --- /dev/null +++ b/src/middleware/client/metrics.rs @@ -0,0 +1,88 @@ +//! Middleware for tracking metrics on each client [reqwest::Request]. + +use reqwest_middleware::Middleware as ReqwestMiddleware; +use std::time::Instant; +use task_local_extensions::Extensions; + +const OK: &str = "ok"; +const ERROR: &str = "error"; +const MIDDLEWARE_ERROR: &str = "middleware_error"; +const NONE: &str = "none"; +const RESULT: &str = "result"; +const STATUS: &str = "status"; + +/// Metrics struct for use as part of middleware. +#[derive(Debug)] +pub struct Metrics { + /// Client name for metric(s) gathering. + pub name: String, +} + +#[async_trait::async_trait] +impl ReqwestMiddleware for Metrics { + async fn handle( + &self, + request: reqwest::Request, + extensions: &mut Extensions, + next: reqwest_middleware::Next<'_>, + ) -> Result { + let now = Instant::now(); + + let url = request.url().clone(); + let request_path: String = url.path().to_string(); + let method = request.method().clone(); + + let result = next.run(request, extensions).await; + let latency = now.elapsed().as_secs_f64(); + + let labels = vec![ + ("client", self.name.to_string()), + ("method", method.to_string()), + ("request_path", request_path), + ]; + + let extended_labels = extend_labels_for_response(labels, &result); + + metrics::increment_counter!("client_http_requests_total", &extended_labels); + metrics::histogram!( + "client_http_request_duration_seconds", + latency, + &extended_labels + ); + + result + } +} + +/// Extend a set of metrics label tuples with dynamic properties +/// around reqwest responses for `result` and `status` fields. +pub fn extend_labels_for_response<'a>( + mut labels: Vec<(&'a str, String)>, + result: &Result, +) -> Vec<(&'a str, String)> { + match result { + Ok(ref success) => { + match success.status().as_u16() { + 200..=299 => labels.push((RESULT, OK.to_string())), + _ => labels.push((RESULT, ERROR.to_string())), + } + + labels.push((STATUS, success.status().as_u16().to_string())); + } + Err(reqwest_middleware::Error::Reqwest(ref err)) => { + labels.push((RESULT, ERROR.to_string())); + labels.push(( + STATUS, + err.status() + .map(|status| status.as_u16().to_string()) + .unwrap_or_else(|| NONE.to_string()), + )); + } + Err(reqwest_middleware::Error::Middleware(ref _err)) => { + labels.push((RESULT, MIDDLEWARE_ERROR.to_string())); + labels.push((STATUS, NONE.to_string())); + } + }; + + labels +} diff --git a/src/middleware/client/mod.rs b/src/middleware/client/mod.rs new file mode 100644 index 0000000..001fd3d --- /dev/null +++ b/src/middleware/client/mod.rs @@ -0,0 +1,3 @@ +//! Middleware for calls to outside client APIs. + +pub mod metrics; diff --git a/src/middleware/logging.rs b/src/middleware/logging.rs new file mode 100644 index 0000000..276ff10 --- /dev/null +++ b/src/middleware/logging.rs @@ -0,0 +1,368 @@ +//! Middleware for logging requests/responses for server and client calls. + +use crate::{error::AppError, middleware::request_ext::RequestExt, settings::AppEnvironment}; +use anyhow::{anyhow, Result}; +use async_trait::async_trait; +use axum::{ + body::{Body, BoxBody, Bytes}, + http::{Request, StatusCode}, + middleware::Next, + response::{IntoResponse, Response}, +}; +use http::header; +use reqwest_middleware::Middleware as ReqwestMiddleware; +use task_local_extensions::Extensions; +use tracing::{debug, info, warn}; + +/// Generic "null" field for unset logs/fields. +const NULL: &str = "null"; +/// None field. +const NONE: &str = "none"; +/// Request identifier field. +const REQUEST_ID: &str = "request_id"; + +/// Middleware function for logging request and response body data. +pub async fn log_request_response( + request: Request, + next: Next, +) -> Result { + let req = L::log_request(request).await?; + let path = req.path(); + let res = next.run(req).await; + let res = L::log_response(res, path).await; + Ok(res) +} + +/// Debug-only Request/Response Logger. +#[derive(Debug)] +pub struct DebugOnlyLogger; +/// Request/Response Logger. +#[derive(Debug)] +pub struct Logger; + +/// Trait for request/response for logging. +/// Involves turning the request into bytes and re-forming it. +#[async_trait] +pub trait RequestResponseLogger { + /// Log requests. + /// + /// Note: always on for debugging. + async fn log_request(request: Request) -> Result, AppError> { + let path = request.path(); + let (parts, body) = request.into_parts(); + + debug!( + subject = "request", + category="http.request", + msg = "started processing request", + request_path = %path, + query_string = parts.uri.query()); + + let bytes = buffer("Request", body).await?; + if let Ok(body) = std::str::from_utf8(&bytes) { + debug!(subject="request", category="http.request", body=?body, request_path=%path); + } + let req = Request::from_parts(parts, Body::from(bytes)); + Ok(req) + } + + /// Log responses at different levels based on [StatusCode]. + async fn log_response( + response: Response, + path: String, + ) -> Result, AppError> { + let status_code = response.status().as_u16(); + let headers = response.headers().clone(); + + match status_code { + 200..=299 => { + debug!( + subject = "response", + category="http.response", + status = ?status_code, + response_headers = ?headers, + "finished processing request") + } + 500..=599 => { + warn!( + subject = "response", + category="http.response", + status = ?status_code, + response_headers = ?headers, + "finished processing request") + } + _ => { + info!( + subject = "response", + category="http.response", + status = ?status_code, + response_headers = ?headers, + "finished processing request") + } + } + + let (parts, body) = response.into_parts(); + + let bytes = buffer("Response", body).await?; + if let Ok(body) = std::str::from_utf8(&bytes) { + debug!(subject="response", category="http.response", body=?body, request_path=%path); + } + let res = Response::from_parts(parts, Body::from(bytes)); + Ok(res) + } +} + +#[async_trait] +impl RequestResponseLogger for DebugOnlyLogger {} + +#[async_trait] +impl RequestResponseLogger for Logger { + async fn log_request(request: Request) -> Result, AppError> { + let path = request.path(); + let (parts, body) = request.into_parts(); + + match parts.extensions.get::() { + Some(&AppEnvironment::Local) | Some(&AppEnvironment::Dev) => info!( + subject = "request", + category="http.request", + msg = "started processing request", + request_id = parts + .headers + .get(REQUEST_ID) + .map(|h| h.to_str().unwrap_or(NULL)), + request_path = %path, + query_string = parts.uri.query(), + authorization = parts + .headers + .get(header::AUTHORIZATION) + .map(|h| h.to_str().unwrap_or(NULL)) + .unwrap_or(NULL)), + _ => { + info!( + subject = "request", + category="http.request", + msg = "started processing request", + request_id = parts + .headers + .get(REQUEST_ID) + .map(|h| h.to_str().unwrap_or(NULL)), + request_path = %path, + query_string = parts.uri.query(), + authorization= parts + .headers + .get(header::AUTHORIZATION) + .map(|h| if h.is_sensitive() { + "" + } else { + h.to_str().unwrap_or(NULL) + }) + .unwrap_or(NULL)) + } + }; + + let bytes = buffer("Request", body).await?; + + if let Ok(body) = std::str::from_utf8(&bytes) { + debug!(subject="request", category="http.request", body=?body); + } + + let req = Request::from_parts(parts, Body::from(bytes)); + Ok(req) + } +} + +#[async_trait] +impl ReqwestMiddleware for Logger { + async fn handle( + &self, + request: reqwest::Request, + extensions: &mut Extensions, + next: reqwest_middleware::Next<'_>, + ) -> Result { + log_reqwest(&request, extensions); + let url = request.url().clone(); + let _ = extensions.insert(url); + match next.run(request, extensions).await { + Ok(success) => { + let response = log_reqwest_response(success, extensions).await?; + Ok(response) + } + Err(reqwest_middleware::Error::Reqwest(err)) => { + log_reqwest_error(&err, extensions)?; + Err(reqwest_middleware::Error::Reqwest(err)) + } + Err(reqwest_middleware::Error::Middleware(err)) => { + log_middleware_error(&err, extensions)?; + Err(reqwest_middleware::Error::Middleware(err)) + } + } + } +} + +fn log_reqwest(request: &reqwest::Request, extensions: &mut Extensions) { + let user_agent = request + .headers() + .get(header::USER_AGENT) + .map(|h| h.to_str().unwrap_or(NULL)) + .unwrap_or(NULL); + + let host_hdr = request + .headers() + .get(header::HOST) + .map(|h| h.to_str().unwrap_or(NULL)) + .unwrap_or(NULL); + let host = request.url().host_str().unwrap_or(host_hdr); + + match extensions.get::() { + Some(&AppEnvironment::Local) | Some(&AppEnvironment::Dev) => { + info!( + subject = "client.request", + category="http.request", + client.method = %request.method(), + client.url = %request.url(), + client.host = host, + client.request_path = request.url().path(), + client.query_string = request.url().query(), + client.user_agent = user_agent, + client.version = ?request.version(), + client.authorization= request + .headers() + .get(header::AUTHORIZATION) + .map(|h| h.to_str().unwrap_or(NULL)) + .unwrap_or(NULL), + "started processing client request") + } + _ => { + info!( + subject = "client.request", + category="http.request", + client.method = %request.method(), + client.url = %request.url(), + client.host = host, + client.request_path = request.url().path(), + client.query_string = request.url().query(), + client.user_agent = user_agent, + client.version = ?request.version(), + client.authorization= request + .headers() + .get(header::AUTHORIZATION) + .map(|_h| "") + .unwrap_or(NULL), + "started processing client request") + } + } +} + +async fn log_reqwest_response( + response: reqwest::Response, + extensions: &mut Extensions, +) -> Result { + /// Turn reqwest body, headers, status, and version into + /// a generic [`http::Response`] and to capture body + parts, + /// and then turn it back into a [`reqwest::Response`]. + /// + /// For logging body information, the original response is + /// eliminated, and a new one is formed. There's no way to + /// from a Response::from_parts as in http/axum. + async fn into_reqwest_response( + body: reqwest::Body, + headers: reqwest::header::HeaderMap, + status_code: u16, + version: reqwest::Version, + ) -> Result { + let mut builder = http::Response::builder() + .status(StatusCode::from_u16(status_code)?) + .version(version); + + let headers_iter = headers.into_iter(); + let headers = builder + .headers_mut() + .ok_or_else(|| anyhow!("failed to convert response headers"))?; + + headers.extend(headers_iter.map(|(k, v)| (k, v))); + + let res = builder.body(body)?; + Ok(reqwest::Response::from(res)) + } + + let url = extensions + .get::() + .ok_or_else(|| anyhow!("failed to find Url extension"))?; + + let status_code = response.status().as_u16(); + + let post_log_response = match status_code { + 400..=599 => { + let version = response.version(); + let headers = response.headers().clone(); + let bytes = response.bytes().await?; + if let Ok(body) = std::str::from_utf8(&bytes) { + warn!( + subject = "client.response", + category="http.response", + body = ?body, + client.status = ?status_code, + client.response_headers = ?headers, + client.url = %url, + client.request_path = url.path(), + "error while processing client request"); + } + + let body = reqwest::Body::from(bytes); + into_reqwest_response(body, headers, status_code, version).await? + } + _ => response, + }; + + Ok(post_log_response) +} + +fn log_reqwest_error(error: &reqwest::Error, extensions: &mut Extensions) -> Result<()> { + let url = extensions + .get::() + .ok_or_else(|| anyhow!("failed to find Url extension"))?; + + warn!( + subject = "client.response", + category="http.response", + client.error = format!("{:#?}", error.to_string()), + client.request_path = url.path(), + client.status = ?error.status().map(|status_code| status_code.as_u16().to_string()).unwrap_or_else(|| NONE.to_string()), + client.url = %url, + "error processing client request"); + + Ok(()) +} + +fn log_middleware_error(error: &anyhow::Error, extensions: &mut Extensions) -> Result<()> { + let url = extensions + .get::() + .ok_or_else(|| anyhow!("failed to find Url extension"))?; + + warn!( + subject = "client.response", + category="http.response", + error = format!("{:#?}", error.to_string()), + client.url = %url, + client.request_path = url.path(), + client.status = NONE, + "error processing client request within efected-coto-emmory middleware"); + + Ok(()) +} + +async fn buffer(direction: &str, body: B) -> Result +where + B: axum::body::HttpBody, + B::Error: std::fmt::Display, +{ + let bytes = match hyper::body::to_bytes(body).await { + Ok(bytes) => bytes, + Err(err) => anyhow::bail!(AppError::new( + StatusCode::BAD_REQUEST, + Some(format!("failed to read {direction} body: {err}")), + )), + }; + + Ok(bytes) +} diff --git a/src/middleware/metrics.rs b/src/middleware/metrics.rs new file mode 100644 index 0000000..4795462 --- /dev/null +++ b/src/middleware/metrics.rs @@ -0,0 +1,29 @@ +//! Middleware for tracking metrics on each [axum::http::Request]. + +use crate::middleware::request_ext::RequestExt; +use axum::{http::Request, middleware::Next, response::IntoResponse}; +use std::time::Instant; + +/// Middleware function called to track (and update) http metrics when a route +/// is requested. +pub async fn track(req: Request, next: Next) -> impl IntoResponse { + let start = Instant::now(); + + let method = req.method().clone(); + let path = req.path(); + + let res = next.run(req).await; + let latency = start.elapsed().as_secs_f64(); + let status = res.status().as_u16().to_string(); + + let labels = [ + ("method", method.to_string()), + ("request_path", path), + ("status", status), + ]; + + metrics::increment_counter!("http_requests_total", &labels); + metrics::histogram!("http_request_duration_seconds", latency, &labels); + + res +} diff --git a/src/middleware/mod.rs b/src/middleware/mod.rs new file mode 100644 index 0000000..f62a40e --- /dev/null +++ b/src/middleware/mod.rs @@ -0,0 +1,10 @@ +//! Additional [axum::middleware]. + +pub mod client; +pub mod logging; +pub mod metrics; +pub(crate) mod request_ext; +pub mod request_ulid; +pub mod reqwest_retry; +pub mod reqwest_tracing; +pub mod runtime; diff --git a/src/middleware/request_ext.rs b/src/middleware/request_ext.rs new file mode 100644 index 0000000..986c553 --- /dev/null +++ b/src/middleware/request_ext.rs @@ -0,0 +1,45 @@ +//! Middleware for additional [axum::http::Request] methods. + +use axum::{ + extract::{MatchedPath, OriginalUri}, + http::Request, +}; + +/// Trait for extra methods on [`Request`](axum::http::Request) +pub(crate) trait RequestExt { + /// Parse request path on the request. + fn path(&self) -> String; +} + +impl RequestExt for Request { + fn path(&self) -> String { + if let Some(matched_path) = self.extensions().get::() { + matched_path.as_str().to_string() + } else if let Some(uri) = self.extensions().get::() { + uri.0.path().to_string() + } else { + self.uri().path().to_string() + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use axum::http::Request; + + #[test] + fn parse_path() { + let mut req1: Request<()> = Request::default(); + *req1.uri_mut() = "https://www.rust-lang.org/users/:id".parse().unwrap(); + assert_eq!(req1.path(), "/users/:id"); + + let mut req2: Request<()> = Request::default(); + *req2.uri_mut() = "https://www.rust-lang.org/api/users".parse().unwrap(); + assert_eq!(req2.path(), "/api/users"); + + let mut req3: Request<()> = Request::default(); + *req3.uri_mut() = "/api/users".parse().unwrap(); + assert_eq!(req3.path(), "/api/users"); + } +} diff --git a/src/middleware/request_ulid.rs b/src/middleware/request_ulid.rs new file mode 100644 index 0000000..4f334ef --- /dev/null +++ b/src/middleware/request_ulid.rs @@ -0,0 +1,21 @@ +//! Middleware for generating [ulid::Ulid]s on requests. + +use axum::http::Request; +use tower_http::request_id::{MakeRequestId, RequestId}; +use ulid::Ulid; + +/// Make/generate ulid on requests. +#[derive(Copy, Clone, Debug)] +pub struct MakeRequestUlid; + +/// Implement the trait for producing a request ID from the incoming request. +/// In our case, we want to generate a new UUID that we can associate with a single request. +impl MakeRequestId for MakeRequestUlid { + fn make_request_id(&mut self, _: &Request) -> Option { + let req_id = Ulid::new().to_string().parse(); + match req_id { + Ok(id) => Some(RequestId::new(id)), + _ => None, + } + } +} diff --git a/src/middleware/reqwest_retry.rs b/src/middleware/reqwest_retry.rs new file mode 100644 index 0000000..b39c58f --- /dev/null +++ b/src/middleware/reqwest_retry.rs @@ -0,0 +1,180 @@ +//! [RetryTransientMiddleware] implements retrying requests on transient errors. +//! This variant minorly extends [TrueLayer's request-retry middleware]. +//! +//! [TrueLayer's request-retry middleware]: +//! + +use crate::middleware::client; +use anyhow::anyhow; +use chrono::Utc; +use reqwest::{Request, Response}; +use reqwest_middleware::{Error, Middleware, Next, Result}; +use reqwest_retry::{RetryPolicy, Retryable}; +use retry_policies::RetryDecision; +use task_local_extensions::Extensions; +use tracing::warn; + +/// We limit the number of retries to a maximum of `10` to avoid stack-overflow issues due to the recursion. +static MAXIMUM_NUMBER_OF_RETRIES: u32 = 10; + +/// `RetryTransientMiddleware` offers retry logic for requests that fail in a transient manner +/// and can be safely executed again. +/// +/// Currently, it allows setting a [RetryPolicy][retry_policies::RetryPolicy] algorithm for calculating the __wait_time__ +/// between each request retry. +/// +///```rust,no_run +/// use efected_coto_emmory::middleware::reqwest_retry::RetryTransientMiddleware; +/// use reqwest_middleware::ClientBuilder; +/// +/// use reqwest_retry::policies::ExponentialBackoff; +/// use reqwest::Client; +/// +/// // We create a ExponentialBackoff retry policy which implements `RetryPolicy`. +/// let retry_policy = ExponentialBackoff::builder().build_with_max_retries(3); +/// +/// let client_name = "YoMTVDocs"; +/// let retry_transient_middleware = RetryTransientMiddleware::new_with_policy(retry_policy, client_name.to_string()); +/// let reqwest_client = Client::builder() +/// .pool_idle_timeout(std::time::Duration::from_millis(50)) +/// .timeout(std::time::Duration::from_millis(100)) +/// .build()?; +/// let client = ClientBuilder::new(reqwest_client).with(retry_transient_middleware).build(); +/// # Ok::<(), reqwest::Error>(()) +///``` +/// +/// # Note +/// +/// This middleware always errors when given requests with streaming bodies, before even executing +/// the request. When this happens you'll get an [`Error::Middleware`] with the message +/// 'Request object is not clonable. Are you passing a streaming body?'. +/// +/// Some workaround suggestions: +/// * If you can fit the data in memory, you can instead build static request bodies e.g. with +/// `Body`'s `From` or `From` implementations. +/// * You can wrap this middleware in a custom one which skips retries for streaming requests. +/// * You can write a custom retry middleware that builds new streaming requests from the data +/// source directly, avoiding the issue of streaming requests not being clonable. +#[derive(Debug)] +pub struct RetryTransientMiddleware { + client_name: String, + retry_policy: T, +} + +impl RetryTransientMiddleware { + /// Construct `RetryTransientMiddleware` with a [retry_policy][retry_policies::RetryPolicy]. + pub fn new_with_policy(retry_policy: T, client_name: String) -> Self { + Self { + client_name, + retry_policy, + } + } +} + +#[async_trait::async_trait] +impl Middleware for RetryTransientMiddleware { + async fn handle( + &self, + request: Request, + extensions: &mut Extensions, + next: Next<'_>, + ) -> Result { + // TODO: Ideally we should create a new instance of the `Extensions` map to pass + // downstream. This will guard against previous retries poluting `Extensions`. + // That is, we only return what's populated in the typemap for the last retry attempt + // and copy those into the the `global` Extensions map. + self.execute_with_retry(request, next, extensions).await + } +} + +impl RetryTransientMiddleware { + /// This function will try to execute the request, if it fails + /// with an error classified as transient it will call itself + /// to retry the request. + async fn execute_with_retry<'a>( + &'a self, + request: Request, + next: Next<'a>, + extensions: &'a mut Extensions, + ) -> Result { + let mut n_past_retries = 0; + loop { + // Cloning the request object before-the-fact is not ideal.. + // However, if the body of the request is not static, e.g of type `Bytes`, + // the Clone operation should be of constant complexity and not O(N) + // since the byte abstraction is a shared pointer over a buffer. + let duplicate_request = request.try_clone().ok_or_else(|| { + Error::Middleware(anyhow!( + "Request object is not clonable. Are you passing a streaming body?".to_string() + )) + })?; + + // Only generate metrics here upon retries, i.e. after + // `n_past_retries`==0, 0 being the init index + let result = if n_past_retries > 0 { + self.handle_retry_metric(duplicate_request, extensions, next.clone()) + .await + } else { + next.clone().run(duplicate_request, extensions).await + }; + + // We classify the response which will return None if not + // errors were returned. + break match Retryable::from_reqwest_response(&result) { + Some(retryable) + if retryable == Retryable::Transient + && n_past_retries < MAXIMUM_NUMBER_OF_RETRIES => + { + // If the response failed and the error type was transient + // we can safely try to retry the request. + let retry_decicion = self.retry_policy.should_retry(n_past_retries); + if let RetryDecision::Retry { execute_after } = retry_decicion { + let duration = (execute_after - Utc::now()) + .to_std() + .map_err(Error::middleware)?; + warn!( + subject = "client.retry", + category = "client", + retry_attempt = n_past_retries + 1, + wait_duration = ?duration, + "retrying call with backoff policy", + ); + // Sleep the requested amount before we try again. + tokio::time::sleep(duration).await; + + n_past_retries += 1; + continue; + } else { + result + } + } + Some(_) | None => result, + }; + } + } + + /// Handle response metrics associated with a retry in the loop. + async fn handle_retry_metric<'a>( + &'a self, + request: Request, + extensions: &mut Extensions, + next: Next<'a>, + ) -> Result { + let url = request.url().clone(); + let request_path: String = url.path().to_string(); + let method = request.method().clone(); + + let result = next.run(request, extensions).await; + + let labels = vec![ + ("client", self.client_name.to_string()), + ("method", method.to_string()), + ("request_path", request_path), + ]; + + let extended_labels = client::metrics::extend_labels_for_response(labels, &result); + + metrics::increment_counter!("client_http_requests_retry_total", &extended_labels); + result + } +} diff --git a/src/middleware/reqwest_tracing.rs b/src/middleware/reqwest_tracing.rs new file mode 100644 index 0000000..32f3cd7 --- /dev/null +++ b/src/middleware/reqwest_tracing.rs @@ -0,0 +1,32 @@ +//! Adding trace information to [reqwest::Request]s. + +use reqwest::{Request, Response}; +use reqwest_middleware::Result; +use reqwest_tracing::{default_on_request_end, reqwest_otel_span, ReqwestOtelSpanBackend}; +use std::time::Instant; +use task_local_extensions::Extensions; +use tracing::Span; + +/// Latency string. +const LATENCY_FIELD: &str = "latency_ms"; + +/// Struct for extending [reqwest_tracing::TracingMiddleware]. +#[derive(Debug)] +pub struct ExtendedTrace; + +impl ReqwestOtelSpanBackend for ExtendedTrace { + fn on_request_start(req: &Request, extension: &mut Extensions) -> Span { + extension.insert(Instant::now()); + reqwest_otel_span!( + name = "reqwest-http-request", + req, + latency_ms = tracing::field::Empty + ) + } + + fn on_request_end(span: &Span, outcome: &Result, extension: &mut Extensions) { + let elapsed_milliseconds = extension.get::().unwrap().elapsed().as_millis() as i64; + default_on_request_end(span, outcome); + span.record(LATENCY_FIELD, elapsed_milliseconds); + } +} diff --git a/src/middleware/runtime.rs b/src/middleware/runtime.rs new file mode 100644 index 0000000..191206d --- /dev/null +++ b/src/middleware/runtime.rs @@ -0,0 +1,56 @@ +//! Middleware for runtime, [tower_http] extensions. + +use crate::error::AppError; + +use axum::response::{IntoResponse, Response}; +use std::any::Any; + +/// Middleware function for catching runtime panics, logging +/// them, and converting them into a `500 Internal Server` response. +pub fn catch_panic(err: Box) -> Response { + let details = if let Some(s) = err.downcast_ref::() { + s.clone() + } else if let Some(s) = err.downcast_ref::<&str>() { + s.to_string() + } else { + "Unknown panic message".to_string() + }; + + let err: AppError = anyhow::anyhow!(details).into(); + err.into_response() +} + +#[cfg(test)] +mod test { + use super::*; + use crate::error::{parse_error, AppError}; + use axum::{ + body::Body, + http::{Request, StatusCode}, + routing::get, + Router, + }; + use tower::{ServiceBuilder, ServiceExt}; + use tower_http::catch_panic::CatchPanicLayer; + + #[tokio::test] + async fn catch_panic_error() { + let middleware = ServiceBuilder::new().layer(CatchPanicLayer::custom(catch_panic)); + + let app = Router::new() + .route("/", get(|| async { panic!("hi") })) + .layer(middleware); + + let res = app + .oneshot(Request::builder().uri("/").body(Body::empty()).unwrap()) + .await + .unwrap(); + + let err = parse_error(res).await; + + assert_eq!( + err, + AppError::new(StatusCode::INTERNAL_SERVER_ERROR, Some("hi")) + ); + } +} diff --git a/src/router.rs b/src/router.rs new file mode 100644 index 0000000..0cc6820 --- /dev/null +++ b/src/router.rs @@ -0,0 +1,24 @@ +//! Main [axum::Router] interface for webserver. + +use crate::{ + middleware::logging::{log_request_response, DebugOnlyLogger, Logger}, + routes::{fallback::notfound_404, health, ping}, +}; +use axum::{routing::get, Router}; + +/// Setup main router for application. +pub fn setup_app_router() -> Router { + let mut router = Router::new() + .route("/ping", get(ping::get)) + .fallback(notfound_404); + + router = router.layer(axum::middleware::from_fn(log_request_response::)); + + let mut healthcheck_router = Router::new().route("/healthcheck", get(health::healthcheck)); + + healthcheck_router = healthcheck_router.layer(axum::middleware::from_fn( + log_request_response::, + )); + + Router::merge(router, healthcheck_router) +} diff --git a/src/routes/fallback.rs b/src/routes/fallback.rs new file mode 100644 index 0000000..802dfdb --- /dev/null +++ b/src/routes/fallback.rs @@ -0,0 +1,9 @@ +//! Fallback routes. + +use crate::error::AppError; +use axum::http::StatusCode; + +/// 404 fallback. +pub async fn notfound_404() -> AppError { + AppError::new(StatusCode::NOT_FOUND, Some("Route does not exist!")) +} diff --git a/src/routes/health.rs b/src/routes/health.rs new file mode 100644 index 0000000..6a5dc39 --- /dev/null +++ b/src/routes/health.rs @@ -0,0 +1,18 @@ +//! Healthcheck route. + +use crate::error::AppResult; +use axum::{self, http::StatusCode}; +use serde_json::json; + +/// GET handler for checking service health. +#[utoipa::path( + get, + path = "/healthcheck", + responses( + (status = 200, description = "efected-coto-emmory healthy"), + (status = 500, description = "efected-coto-emmory not healthy", body=AppError) + ) +)] +pub async fn healthcheck() -> AppResult<(StatusCode, axum::Json)> { + Ok((StatusCode::OK, axum::Json(json!({ "msg": "Healthy"})))) +} diff --git a/src/routes/mod.rs b/src/routes/mod.rs new file mode 100644 index 0000000..397b57b --- /dev/null +++ b/src/routes/mod.rs @@ -0,0 +1,5 @@ +//! Routes for [axum::Router]. + +pub mod fallback; +pub mod health; +pub mod ping; diff --git a/src/routes/ping.rs b/src/routes/ping.rs new file mode 100644 index 0000000..8ccd564 --- /dev/null +++ b/src/routes/ping.rs @@ -0,0 +1,18 @@ +//! Generic ping route. + +use crate::error::AppResult; +use axum::{self, http::StatusCode}; + +/// GET handler for internal pings and availability +#[utoipa::path( + get, + path = "/ping", + responses( + (status = 200, description = "Ping successful"), + (status = 500, description = "Ping not successful", body=AppError) + ) +)] + +pub async fn get() -> AppResult { + Ok(StatusCode::OK) +} diff --git a/src/settings.rs b/src/settings.rs new file mode 100644 index 0000000..f788b9f --- /dev/null +++ b/src/settings.rs @@ -0,0 +1,245 @@ +//! Settings / Configuration. + +use config::{Config, ConfigError, Environment, File}; +use http::Uri; +use serde::Deserialize; +use serde_with::serde_as; +use std::{path::PathBuf, time::Duration}; + +/// Names of environments for efected-coto-emmory. +/// Overrides serialization to force lower case in settings and +/// environment variables +#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "lowercase")] +pub enum AppEnvironment { + /// Local environment (local testing). + Local, + /// Official Develop environment. + Dev, + /// Official environment. + Staging, + /// Official Production environment. + Prod, +} + +/// Implement display to force environment to lower case +impl std::fmt::Display for AppEnvironment { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", format!("{self:?}").to_lowercase()) + } +} + +/// Server settings. +#[derive(Debug, Deserialize)] +pub struct Server { + /// Server [AppEnvironment]. + pub environment: AppEnvironment, + /// Server port. + pub port: u16, + /// Server metrics port. + pub metrics_port: u16, + /// Server timeout in milliseconds. + pub timeout_ms: u64, +} + +/// Process monitoring settings. +#[derive(Debug, Deserialize)] +pub struct Monitoring { + /// Monitoring collection interval. + pub process_collector_interval: u64, +} + +/// [Opentelemetry] settings. +/// +/// [Opentelemetry]: https://opentelemetry.io/ +#[serde_as] +#[derive(Deserialize)] +pub struct Otel { + /// Exporter [Uri] for OTEL protocol. + #[serde(with = "http_serde::uri")] + pub exporter_otlp_endpoint: Uri, +} + +impl std::fmt::Debug for Otel { + fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + fmt.debug_struct("Otel") + .field("exporter_otlp_endpoint", &self.exporter_otlp_endpoint) + .finish() + } +} + +#[derive(Debug, Deserialize)] +/// Application settings. +pub struct Settings { + monitoring: Monitoring, + server: Server, + otel: Otel, +} + +impl Settings { + /// Environment settings getter. + pub fn environment(&self) -> AppEnvironment { + self.server().environment + } + + /// Monitoring settings getter. + pub fn monitoring(&self) -> &Monitoring { + &self.monitoring + } + + /// OTEL settings getter. + pub fn otel(&self) -> &Otel { + &self.otel + } + + /// Server settings getter. + pub fn server(&self) -> &Server { + &self.server + } +} + +impl Settings { + /// Load settings. + pub fn load() -> Result { + let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("config/settings.toml"); + // inject environment variables naming them properly on the settings + // e.g. [database] url="foo" + // would be injected with environment variable APP__DATABASE__URL="foo" + // using a double underscore as defined by the separator below + let s = Config::builder() + .add_source(File::with_name(&path.as_path().display().to_string())) + .add_source(Environment::with_prefix("APP").separator("__")) + .build()?; + s.try_deserialize() + } +} + +/// Http-client retry options. +#[derive(Clone, Debug, Deserialize)] +pub struct HttpClientRetryOptions { + /// Retry count. + pub count: u8, + /// Retry lower bounds for [reqwest_retry::policies::ExponentialBackoff]. + pub bounds_low_ms: u64, + /// Retry upper bounds for [reqwest_retry::policies::ExponentialBackoff]. + pub bounds_high_ms: u64, +} + +impl Default for HttpClientRetryOptions { + fn default() -> Self { + Self { + bounds_high_ms: 5_000, + bounds_low_ms: 100, + count: 3, + } + } +} + +/// Settings for Http clients. +#[derive(Clone, Debug, Deserialize)] +pub struct HttpClient { + /// Optional timeout for idle sockets being kept-alive. + /// Using `None` to disable timeout. + pub pool_idle_timeout_ms: Option, + #[serde(default)] + /// Http-client retry options. + pub retry_options: HttpClientRetryOptions, + /// Client timeout in milliseconds. + pub timeout_ms: u64, +} + +impl Default for HttpClient { + fn default() -> Self { + Self { + pool_idle_timeout_ms: Some(5_000), + retry_options: HttpClientRetryOptions::default(), + timeout_ms: 30_000, + } + } +} + +impl HttpClient { + /// Convert `pool_idle_timeout_ms` to [Duration]. + pub fn pool_idle_timeout(&self) -> Option { + self.pool_idle_timeout_ms.and_then(|timeout| { + if timeout != 0 { + Some(Duration::from_millis(timeout)) + } else { + None + } + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[serde_as] + #[derive(Debug, Deserialize)] + pub(crate) struct Client { + #[serde(default)] + http_client: HttpClient, + } + + #[test] + fn test_default_http_client_settings() { + let settings = Client { + http_client: HttpClient::default(), + }; + + assert_eq!( + settings.http_client.pool_idle_timeout(), + Some(Duration::from_millis(5_000)) + ); + assert_eq!(settings.http_client.retry_options.bounds_high_ms, 5_000); + assert_eq!(settings.http_client.retry_options.bounds_low_ms, 100); + assert_eq!(settings.http_client.retry_options.count, 3); + assert_eq!(settings.http_client.timeout_ms, 30_000); + } + + #[test] + fn test_http_client_overrides() { + let settings = Client { + http_client: HttpClient { + pool_idle_timeout_ms: Some(0), + retry_options: HttpClientRetryOptions { + bounds_low_ms: 10, + bounds_high_ms: 100, + count: 10, + }, + timeout_ms: 100, + }, + }; + + assert_eq!(settings.http_client.pool_idle_timeout(), None); + assert_eq!(settings.http_client.retry_options.bounds_high_ms, 100); + assert_eq!(settings.http_client.retry_options.bounds_low_ms, 10); + assert_eq!(settings.http_client.retry_options.count, 10); + assert_eq!(settings.http_client.timeout_ms, 100); + } + + #[test] + fn test_http_client_partial_overrides() { + let settings = Client { + http_client: HttpClient { + pool_idle_timeout_ms: Some(5_000), + retry_options: HttpClientRetryOptions { + bounds_low_ms: 10, + bounds_high_ms: 5_000, + count: 1, + }, + timeout_ms: 10_000, + }, + }; + + assert_eq!( + settings.http_client.pool_idle_timeout(), + Some(Duration::from_millis(5_000)) + ); + assert_eq!(settings.http_client.retry_options.bounds_high_ms, 5_000); + assert_eq!(settings.http_client.retry_options.bounds_low_ms, 10); + assert_eq!(settings.http_client.retry_options.count, 1); + assert_eq!(settings.http_client.timeout_ms, 10_000); + } +} diff --git a/src/test_utils/mod.rs b/src/test_utils/mod.rs new file mode 100644 index 0000000..4a30e2a --- /dev/null +++ b/src/test_utils/mod.rs @@ -0,0 +1,5 @@ +/// Random value generator for sampling data. +#[cfg(feature = "test_utils")] +mod rvg; +#[cfg(feature = "test_utils")] +pub use rvg::*; diff --git a/src/test_utils/rvg.rs b/src/test_utils/rvg.rs new file mode 100644 index 0000000..748c69c --- /dev/null +++ b/src/test_utils/rvg.rs @@ -0,0 +1,63 @@ +use proptest::{ + collection::vec, + strategy::{Strategy, ValueTree}, + test_runner::{Config, TestRunner}, +}; + +/// A random value generator (RVG), which, given proptest strategies, will +/// generate random values based on those strategies. +#[derive(Debug, Default)] +pub struct Rvg { + runner: TestRunner, +} + +impl Rvg { + /// Creates a new RVG with the default random number generator. + pub fn new() -> Self { + Rvg { + runner: TestRunner::new(Config::default()), + } + } + + /// Creates a new RVG with a deterministic random number generator, + /// using the same seed across test runs. + pub fn deterministic() -> Self { + Rvg { + runner: TestRunner::deterministic(), + } + } + + /// Samples a value for the given strategy. + /// + /// # Example + /// + /// ``` + /// use efected_coto_emmory::test_utils::Rvg; + /// + /// let mut rvg = Rvg::new(); + /// let int = rvg.sample(&(0..100i32)); + /// ``` + pub fn sample(&mut self, strategy: &S) -> S::Value { + strategy + .new_tree(&mut self.runner) + .expect("No value can be generated") + .current() + } + + /// Samples a vec of some length with a value for the given strategy. + /// + /// # Example + /// + /// ``` + /// use efected_coto_emmory::test_utils::Rvg; + /// + /// let mut rvg = Rvg::new(); + /// let ints = rvg.sample_vec(&(0..100i32), 10); + /// ``` + pub fn sample_vec(&mut self, strategy: &S, len: usize) -> Vec { + vec(strategy, len..=len) + .new_tree(&mut self.runner) + .expect("No value can be generated") + .current() + } +} diff --git a/src/tracer.rs b/src/tracer.rs new file mode 100644 index 0000000..a91ec81 --- /dev/null +++ b/src/tracer.rs @@ -0,0 +1,63 @@ +//! Opentelemetry tracing extensions and setup. + +use crate::settings::Otel; +use anyhow::{anyhow, Result}; +use const_format::formatcp; +use http::Uri; +use opentelemetry::{ + global, runtime, + sdk::{self, propagation::TraceContextPropagator, trace::Tracer, Resource}, +}; +use opentelemetry_otlp::{TonicExporterBuilder, WithExportConfig}; +use opentelemetry_semantic_conventions as otel_semcov; +use tonic::{metadata::MetadataMap, transport::ClientTlsConfig}; + +//const PKG_NAME: &str = env!("CARGO_PKG_NAME"); +const PKG_NAME: &str = "application"; +const VERSION: &str = formatcp!("v{}", env!("CARGO_PKG_VERSION")); +const LANG: &str = "rust"; + +/// Initialize Opentelemetry tracing via the [OTLP protocol]. +/// +/// [OTLP protocol]: +pub fn init_tracer(settings: &Otel) -> Result { + global::set_text_map_propagator(TraceContextPropagator::new()); + + let resource = Resource::new(vec![ + otel_semcov::resource::SERVICE_NAME.string(PKG_NAME), + otel_semcov::resource::SERVICE_VERSION.string(VERSION), + otel_semcov::resource::TELEMETRY_SDK_LANGUAGE.string(LANG), + ]); + + let endpoint = &settings.exporter_otlp_endpoint; + + let map = MetadataMap::with_capacity(2); + + let trace = opentelemetry_otlp::new_pipeline() + .tracing() + .with_exporter(exporter(map, endpoint)?) + .with_trace_config(sdk::trace::config().with_resource(resource)) + .install_batch(runtime::Tokio) + .map_err(|e| anyhow!("failed to intialize tracer: {:#?}", e))?; + + Ok(trace) +} + +fn exporter(map: MetadataMap, endpoint: &Uri) -> Result { + // Over grpc transport + let exporter = opentelemetry_otlp::new_exporter() + .tonic() + .with_endpoint(endpoint.to_string()) + .with_metadata(map); + + match endpoint.scheme_str() { + Some("https") => { + let host = endpoint + .host() + .ok_or_else(|| anyhow!("failed to parse host"))?; + + Ok(exporter.with_tls_config(ClientTlsConfig::new().domain_name(host.to_string()))) + } + _ => Ok(exporter), + } +} diff --git a/src/tracing_layers/format_layer.rs b/src/tracing_layers/format_layer.rs new file mode 100644 index 0000000..669cd30 --- /dev/null +++ b/src/tracing_layers/format_layer.rs @@ -0,0 +1,672 @@ +//! [Logfmt]'ed event logging [Layer] with augmented telemetry info. +//! +//! Inspired by [influxdata's (Influx DB's) version]. +//! +//! [Logfmt]: +//! [Layer]: tracing_subscriber::Layer +//! [influxdata's (Influx DB's) version]: + +use crate::tracing_layers::storage_layer::Storage; +use parking_lot::RwLock; +use std::{ + borrow::Cow, + fmt, + io::{self, Write}, + time::SystemTime, +}; +use time::{format_description::well_known::Rfc3339, OffsetDateTime}; +use tracing::{ + field::{Field, Visit}, + metadata::LevelFilter, + span::{Attributes, Id}, + Event, Level, Subscriber, +}; +use tracing_subscriber::{fmt::MakeWriter, layer::Context, registry::LookupSpan, Layer}; + +/// Fields to persist from [Storage](Storage) for `new_span` logs via context. +const SPAN_FIELDS: [&str; 13] = [ + "category", + "follows_from", + "follows_from.trace_id", + "http.client_ip", + "http.host", + "http.method", + "http.route", + "latency_ms", + "parent_span", + "request_id", + "span", + "subject", + "trace_id", +]; + +/// Fields to skip from [Storage](Storage) spans for `on_event` logs via +/// context. +const ON_EVENT_SKIP_FIELDS: [&str; 6] = [ + "authorization", + "category", + "error", + "msg", + "return", + "subject", +]; + +/// Fields to persist from [Storage](Storage) for `on_close` span logs via +/// context. +const ON_CLOSE_FIELDS: [&str; 12] = [ + "category", + "follows_from", + "follows_from.trace_id", + "http.client_ip", + "http.host", + "http.method", + "http.route", + "latency_ms", + "parent_span", + "request_id", + "subject", + "trace_id", +]; + +#[cfg(feature = "ansi-logs")] +const GRAY: u8 = 245; + +/// Logging layer for formatting and outputting event-driven logs. +#[derive(Debug)] +pub struct LogFmtLayer io::Stdout> +where + Wr: Write, + W: for<'writer> MakeWriter<'writer>, +{ + writer: W, + printer: RwLock>, +} + +impl LogFmtLayer +where + Wr: Write, + W: for<'writer> MakeWriter<'writer, Writer = Wr>, +{ + /// Create a new logfmt Layer to pass into tracing_subscriber + /// + /// Note this layer simply formats and writes to the specified writer. It + /// does not do any filtering for levels itself. Filtering can be done + /// using a EnvFilter. + /// + /// # Example + /// + /// ```rust,no_run + /// use efected_coto_emmory::tracing_layers::format_layer::LogFmtLayer; + /// use tracing_subscriber::{EnvFilter, prelude::*, self}; + /// + /// // setup debug logging level + /// std::env::set_var("RUST_LOG", "debug"); + /// + /// // setup formatter to write to stderr + /// let formatter = + /// LogFmtLayer::new(std::io::stderr); + /// + /// tracing_subscriber::registry() + /// .with(EnvFilter::from_default_env()) + /// .with(formatter) + /// .init(); + /// ``` + pub fn new(writer: W) -> Self { + let make_writer = writer.make_writer(); + Self { + writer, + printer: RwLock::new(FieldPrinter::new(make_writer, true)), + } + } + + /// Control whether target and location attributes are displayed (on by default). + /// + /// Note: this API mimics that of other fmt layers in tracing-subscriber crate. + pub fn with_target(self, display_target: bool) -> Self { + let make_writer = self.writer.make_writer(); + Self { + writer: self.writer, + printer: RwLock::new(FieldPrinter::new(make_writer, display_target)), + } + } +} + +impl Layer for LogFmtLayer +where + Wr: Write + 'static, + W: for<'writer> MakeWriter<'writer> + 'static, + S: Subscriber + for<'span> LookupSpan<'span>, +{ + fn max_level_hint(&self) -> Option { + None + } + + fn on_new_span(&self, _attrs: &Attributes<'_>, id: &Id, ctx: Context<'_, S>) { + let mut p = self.printer.write(); + + let metadata = ctx.metadata(id).expect("Span missing metadata"); + p.write_level(metadata.level()); + p.write_span_name(metadata.name()); + p.write_span_id(id); + p.write_span_event("new_span"); + p.write_timestamp(); + + let span = ctx.span(id).expect("Span not found"); + let extensions = span.extensions(); + if let Some(visitor) = extensions.get::>() { + for (key, value) in visitor.values() { + match *metadata.level() { + Level::TRACE | Level::DEBUG => p.write_kv( + decorate_field_name(translate_field_name(key)), + value.to_string(), + ), + + _ => { + if SPAN_FIELDS.contains(key) { + p.write_kv( + decorate_field_name(translate_field_name(key)), + value.to_string(), + ) + } + } + } + } + } + p.write_newline(); + } + + fn on_event(&self, event: &Event<'_>, ctx: Context<'_, S>) { + let mut p = self.printer.write(); + + p.write_level(event.metadata().level()); + event.record(&mut *p); + + //record source information + p.write_source_info(event); + p.write_timestamp(); + + ctx.lookup_current().map(|current_span| { + p.write_span_id(¤t_span.id()); + let extensions = current_span.extensions(); + extensions.get::>().map(|visitor| { + for (key, value) in visitor.values() { + if !ON_EVENT_SKIP_FIELDS.contains(key) { + p.write_kv( + decorate_field_name(translate_field_name(key)), + value.to_string(), + ) + } + } + }) + }); + + p.write_newline(); + } + + fn on_close(&self, id: Id, ctx: Context<'_, S>) { + let mut p = self.printer.write(); + + let metadata = ctx.metadata(&id).expect("Span missing metadata"); + let span = ctx.span(&id).expect("Span not found"); + + p.write_level(metadata.level()); + p.write_span_name(metadata.name()); + p.write_span_id(&span.id()); + p.write_span_event("close_span"); + p.write_timestamp(); + + let mut extensions = span.extensions_mut(); + + if let Some(visitor) = extensions.get_mut::>() { + for (key, value) in visitor.values() { + if ON_CLOSE_FIELDS.contains(key) { + p.write_kv( + decorate_field_name(translate_field_name(key)), + value.to_string(), + ) + } + } + } + + p.write_newline(); + } +} + +/// This is responsible for actually printing log information to +/// the layer's writer. +#[derive(Debug)] +struct FieldPrinter { + writer: Wr, + display_target: bool, +} + +impl FieldPrinter { + fn new(writer: W, display_target: bool) -> Self { + Self { + writer, + display_target, + } + } + + #[cfg(feature = "ansi-logs")] + fn write_level(&mut self, level: &Level) { + let level_str = match *level { + Level::TRACE => "trace", + Level::DEBUG => "debug", + Level::INFO => "info", + Level::WARN => "warn", + Level::ERROR => "error", + } + .to_uppercase(); + + let level_name = match *level { + Level::TRACE => ansi_term::Color::Purple, + Level::DEBUG => ansi_term::Color::Blue, + Level::INFO => ansi_term::Color::Green, + Level::WARN => ansi_term::Color::Yellow, + Level::ERROR => ansi_term::Color::Red, + } + .bold() + .paint(level_str); + + write!( + self.writer, + r#"{}={}"#, + decorate_field_name("level"), + level_name + ) + .ok(); + } + + #[cfg(not(feature = "ansi-logs"))] + fn write_level(&mut self, level: &Level) { + let level_str = match *level { + Level::TRACE => "trace", + Level::DEBUG => "debug", + Level::INFO => "info", + Level::WARN => "warn", + Level::ERROR => "error", + }; + + write!( + self.writer, + r#"{}={}"#, + decorate_field_name("level"), + level_str + ) + .ok(); + } + + fn write_span_name(&mut self, value: &str) { + write!( + self.writer, + " {}={}", + decorate_field_name("span_name"), + quote_and_escape(value) + ) + .ok(); + } + + fn write_source_info(&mut self, event: &Event<'_>) { + if !self.display_target { + return; + } + + let metadata = event.metadata(); + + if metadata.target() != "log" { + write!( + self.writer, + " {}=\"{}\"", + decorate_field_name("target"), + quote_and_escape(metadata.target()) + ) + .ok(); + } + + if let Some(module_path) = metadata.module_path() { + if metadata.target() != module_path { + write!( + self.writer, + " {}=\"{}\"", + decorate_field_name("module_path"), + module_path + ) + .ok(); + } + } + if let (Some(file), Some(line)) = (metadata.file(), metadata.line()) { + write!( + self.writer, + " {}=\"{}:{}\"", + decorate_field_name("location"), + file, + line + ) + .ok(); + } + } + + fn write_span_id(&mut self, id: &Id) { + write!( + self.writer, + " {}={}", + decorate_field_name("span"), + id.into_u64() + ) + .ok(); + } + + fn write_span_event(&mut self, hook: &str) { + write!( + self.writer, + " {}={}", + decorate_field_name("span_event"), + hook + ) + .ok(); + } + + fn write_timestamp(&mut self) { + write!( + self.writer, + " {}={}", + decorate_field_name("timestamp"), + to_rfc3339(&SystemTime::now()) + ) + .ok(); + } + + fn write_kv(&mut self, key: String, value: String) { + write!(self.writer, " {}={}", key, quote_and_escape(value.as_str())).ok(); + } + + fn write_newline(&mut self) { + writeln!(self.writer).ok(); + } +} + +impl Visit for FieldPrinter { + /// Visit a signed 64-bit integer value. + fn record_i64(&mut self, field: &Field, value: i64) { + write!( + self.writer, + " {}={}", + decorate_field_name(translate_field_name(field.name())), + value + ) + .ok(); + } + + /// Visit an unsigned 64-bit integer value. + fn record_u64(&mut self, field: &Field, value: u64) { + write!( + self.writer, + " {}={}", + decorate_field_name(translate_field_name(field.name())), + value + ) + .ok(); + } + + /// Visit a boolean value. + fn record_bool(&mut self, field: &Field, value: bool) { + write!( + self.writer, + " {}={}", + decorate_field_name(translate_field_name(field.name())), + value + ) + .ok(); + } + + /// Visit a string value. + fn record_str(&mut self, field: &Field, value: &str) { + write!( + self.writer, + " {}={}", + decorate_field_name(translate_field_name(field.name())), + quote_and_escape(value) + ) + .ok(); + } + + fn record_debug(&mut self, field: &Field, value: &dyn fmt::Debug) { + // Note this appears to be invoked via `debug!` and `info! macros + let formatted_value = format!("{value:?}"); + write!( + self.writer, + " {}={}", + decorate_field_name(translate_field_name(field.name())), + quote_and_escape(&formatted_value) + ) + .ok(); + } + + fn record_error(&mut self, field: &Field, value: &(dyn std::error::Error + 'static)) { + let field_name = translate_field_name(field.name()); + + let debug_formatted = format!("{value:?}"); + write!( + self.writer, + " {}={:?}", + decorate_field_name(field_name), + quote_and_escape(&debug_formatted) + ) + .ok(); + + let display_formatted = format!("{value}"); + write!( + self.writer, + " {}.display={}", + decorate_field_name(field_name), + quote_and_escape(&display_formatted) + ) + .ok(); + } +} + +/// The type of record we are dealing with: entering a span, exiting a span, an event. +#[derive(Debug)] +pub enum Type { + /// Starting span. + EnterSpan, + /// Exiting span. + ExitSpan, + /// Event. + Event, +} + +impl fmt::Display for Type { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let repr = match self { + Type::EnterSpan => "START", + Type::ExitSpan => "END", + Type::Event => "EVENT", + }; + write!(f, "{repr}") + } +} + +/// Translate the field name from tracing into the logfmt style. +fn translate_field_name(name: &str) -> &str { + let name = slice_field_name(name); + + if name == "message" { + "msg" + } else { + name + } +} + +/// Decorates field names in logs if the `ansi-logs` is on. +#[cfg(feature = "ansi-logs")] +#[inline] +fn decorate_field_name(name: &str) -> String { + ansi_term::Color::Fixed(GRAY) + .italic() + .paint(name) + .to_string() +} + +/// Decorates field names in logs when `ansi-logs` is not on. +#[cfg(not(feature = "ansi-logs"))] +#[inline] +fn decorate_field_name(name: &str) -> String { + name.to_string() +} + +/// Converts system time to `rfc3339` format. +fn to_rfc3339(st: &SystemTime) -> String { + st.duration_since(SystemTime::UNIX_EPOCH) + .ok() + .and_then(|duration| TryFrom::try_from(duration).ok()) + .and_then(|duration| OffsetDateTime::UNIX_EPOCH.checked_add(duration)) + .and_then(|dt| dt.format(&Rfc3339).ok()) + .unwrap_or_default() +} + +/// Return true if the string value already starts/ends with quotes and is +/// already properly escaped (all spaces escaped). +fn needs_quotes_and_escaping(value: &str) -> bool { + // mismatches beginning / end quotes + if value.starts_with('"') != value.ends_with('"') { + return true; + } + + // ignore beginning/ending quotes, if any + let pre_quoted = value.len() >= 2 && value.starts_with('"') && value.ends_with('"'); + + let value = if pre_quoted { + &value[1..value.len() - 1] + } else { + value + }; + + // unescaped quotes + let c0 = value.chars(); + let c1 = value.chars().skip(1); + if c0.zip(c1).any(|(c0, c1)| c0 != '\\' && c1 == '"') { + return true; + } + + // Quote any strings that contain a literal '=' which the logfmt parser + // interprets as a key/value separator. + if value.chars().any(|c| c == '=') && !pre_quoted { + return true; + } + + if value.bytes().any(|b| b <= b' ') && !pre_quoted { + return true; + } + + false +} + +/// Escape any characters in name as needed, otherwise return string as is. +fn quote_and_escape(value: &'_ str) -> Cow<'_, str> { + if needs_quotes_and_escaping(value) { + Cow::Owned(format!("{value:?}")) + } else { + Cow::Borrowed(value) + } +} + +/// slice / cut fields with a `.`. +fn slice_field_name(name: &str) -> &str { + match name { + name if name.starts_with("log.") => &name[4..], + name => name, + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn quote_and_escape_len0() { + assert_eq!(quote_and_escape(""), ""); + } + + #[test] + fn quote_and_escape_len1() { + assert_eq!(quote_and_escape("f"), "f"); + } + + #[test] + fn quote_and_escape_len2() { + assert_eq!(quote_and_escape("fo"), "fo"); + } + + #[test] + fn quote_and_escape_len3() { + assert_eq!(quote_and_escape("foo"), "foo"); + } + + #[test] + fn quote_and_escape_len3_1quote_start() { + assert_eq!(quote_and_escape("\"foo"), "\"\\\"foo\""); + } + + #[test] + fn quote_and_escape_len3_1quote_end() { + assert_eq!(quote_and_escape("foo\""), "\"foo\\\"\""); + } + + #[test] + fn quote_and_escape_len3_2quote() { + assert_eq!(quote_and_escape("\"foo\""), "\"foo\""); + } + + #[test] + fn quote_and_escape_space() { + assert_eq!(quote_and_escape("foo bar"), "\"foo bar\""); + } + + #[test] + fn quote_and_escape_space_prequoted() { + assert_eq!(quote_and_escape("\"foo bar\""), "\"foo bar\""); + } + + #[test] + fn quote_and_escape_space_prequoted_but_not_escaped() { + assert_eq!(quote_and_escape("\"foo \"bar\""), "\"\\\"foo \\\"bar\\\"\""); + } + + #[test] + fn quote_and_escape_quoted_quotes() { + assert_eq!(quote_and_escape("foo:\"bar\""), "\"foo:\\\"bar\\\"\""); + } + + #[test] + fn quote_and_escape_nested_1() { + assert_eq!(quote_and_escape(r#"a "b" c"#), r#""a \"b\" c""#); + } + + #[test] + fn quote_and_escape_nested_2() { + assert_eq!( + quote_and_escape(r#"a "0 \"1\" 2" c"#), + r#""a \"0 \\\"1\\\" 2\" c""# + ); + } + + #[test] + fn quote_not_printable() { + assert_eq!(quote_and_escape("foo\nbar"), r#""foo\nbar""#); + assert_eq!(quote_and_escape("foo\r\nbar"), r#""foo\r\nbar""#); + assert_eq!(quote_and_escape("foo\0bar"), r#""foo\0bar""#); + } + + #[test] + fn not_quote_unicode_unnecessarily() { + assert_eq!(quote_and_escape("mikuličić"), "mikuličić"); + } + + #[test] + fn test_uri_quoted() { + assert_eq!(quote_and_escape("/api/v2/write?bucket=06fddb4f912a0d7f&org=9df0256628d1f506&orgID=9df0256628d1f506&precision=ns"), + r#""/api/v2/write?bucket=06fddb4f912a0d7f&org=9df0256628d1f506&orgID=9df0256628d1f506&precision=ns""#); + } +} diff --git a/src/tracing_layers/metrics_layer.rs b/src/tracing_layers/metrics_layer.rs new file mode 100644 index 0000000..cae1451 --- /dev/null +++ b/src/tracing_layers/metrics_layer.rs @@ -0,0 +1,84 @@ +//! Metrics layer. + +use crate::tracing_layers::storage_layer::Storage; +use std::{borrow::Cow, time::Instant}; +use tracing::{Id, Subscriber}; +use tracing_subscriber::{layer::Context, registry::LookupSpan, Layer}; + +const PREFIX_LABEL: &str = "metric_label_"; +const METRIC_NAME: &str = "metric_name"; +const OK: &str = "ok"; +const ERROR: &str = "error"; +const LABEL: &str = "label"; +const RESULT_LABEL: &str = "result"; +const SPAN_LABEL: &str = "span_name"; + +/// Prefix used for capturing metric spans/instrumentations. +pub const METRIC_META_PREFIX: &str = "record."; + +/// Metrics layer for automatically deriving metrics for record.* events. +/// +/// Append to custom [LogFmtLayer](crate::tracing_layers::format_layer::LogFmtLayer). +#[derive(Debug)] +pub struct MetricsLayer; + +impl Layer for MetricsLayer +where + S: Subscriber + for<'span> LookupSpan<'span>, +{ + fn on_close(&self, id: Id, ctx: Context<'_, S>) { + let span = ctx.span(&id).expect("Span not found"); + let mut extensions = span.extensions_mut(); + + let elapsed_secs_f64 = extensions + .get_mut::() + .map(|i| i.elapsed().as_secs_f64()) + .unwrap_or(0.0); + + if let Some(visitor) = extensions.get_mut::>() { + let mut labels = vec![]; + for (key, value) in visitor.values() { + if key.starts_with(PREFIX_LABEL) { + labels.push(( + key.strip_prefix(PREFIX_LABEL).unwrap_or(LABEL), + value.to_string(), + )) + } + } + + let span_name = span + .name() + .strip_prefix(METRIC_META_PREFIX) + .unwrap_or_else(|| span.name()); + + labels.push((SPAN_LABEL, span_name.to_string())); + + let name = visitor + .values() + .get(METRIC_NAME) + .unwrap_or(&Cow::from(span_name)) + .to_string(); + + if visitor.values().contains_key(ERROR) { + labels.push((RESULT_LABEL, String::from(ERROR))) + } else { + labels.push((RESULT_LABEL, String::from(OK))) + } + + // Need to sort labels to remain the same across all metrics. + labels.sort_unstable(); + + metrics::increment_counter!(format!("{name}_total"), &labels); + metrics::histogram!( + format!("{name}_duration_seconds"), + elapsed_secs_f64, + &labels + ); + + // Remove storage as this is the last layer. + extensions + .remove::>() + .expect("Visitor not found on 'close'"); + } + } +} diff --git a/src/tracing_layers/mod.rs b/src/tracing_layers/mod.rs new file mode 100644 index 0000000..2aa7799 --- /dev/null +++ b/src/tracing_layers/mod.rs @@ -0,0 +1,9 @@ +//! Custom [tracing_subscriber::layer::Layer]s for formatting log events, +//! deriving metrics from instrumentation calls, and for storage to augment +//! layers. For more information, please read [Composing an observable Rust application]. +//! +//! [Composing an observable Rust application]: + +pub mod format_layer; +pub mod metrics_layer; +pub mod storage_layer; diff --git a/src/tracing_layers/storage_layer.rs b/src/tracing_layers/storage_layer.rs new file mode 100644 index 0000000..1ab1609 --- /dev/null +++ b/src/tracing_layers/storage_layer.rs @@ -0,0 +1,195 @@ +//! Storage layer. + +use std::{borrow::Cow, collections::HashMap, fmt, time::Instant}; +use tracing::{ + field::{Field, Visit}, + span::{Attributes, Record}, + Event, Id, Subscriber, +}; +use tracing_subscriber::{layer::Context, registry::LookupSpan, Layer}; + +/// Storage fields for events. +const ON_EVENT_KEEP_FIELDS: [&str; 1] = ["error"]; + +const TRACE_ID: &str = "trace_id"; +const PARENT_SPAN: &str = "parent_span"; +const FOLLOWS_FROM_TRACE_ID: &str = "follows_from.trace_id"; +const FOLLOWS_FROM_FIELD: &str = "follows_from"; +const LATENCY_FIELD: &str = "latency_ms"; + +/// Storage layer for contextual trace information. +/// +/// Prepend to custom [LogFmtLayer](crate::tracing_layers::format_layer::LogFmtLayer). +#[derive(Clone, Debug)] +pub struct StorageLayer; + +#[derive(Clone, Debug, Default)] +pub(crate) struct Storage<'a> { + values: HashMap<&'a str, Cow<'a, str>>, +} + +impl<'a> Storage<'a> { + pub(crate) fn values(&self) -> &HashMap<&'a str, Cow<'a, str>> { + &self.values + } +} + +impl Visit for Storage<'_> { + /// Visit a signed 64-bit integer value. + fn record_i64(&mut self, field: &Field, value: i64) { + self.values + .insert(field.name(), Cow::from(value.to_string())); + } + + /// Visit an unsigned 64-bit integer value. + fn record_u64(&mut self, field: &Field, value: u64) { + self.values + .insert(field.name(), Cow::from(value.to_string())); + } + + /// Visit a boolean value. + fn record_bool(&mut self, field: &Field, value: bool) { + self.values + .insert(field.name(), Cow::from(value.to_string())); + } + + /// Visit a string value. + fn record_str(&mut self, field: &Field, value: &str) { + self.values + .insert(field.name(), Cow::from(value.to_string())); + } + + fn record_debug(&mut self, field: &Field, value: &dyn fmt::Debug) { + // Note this appears to be invoked via `debug!` and `info! macros + match field.name() { + name if name.starts_with("log.") => (), + _ => { + let debug_formatted = format!("{value:?}"); + self.values.insert(field.name(), Cow::from(debug_formatted)); + } + } + } + + fn record_error(&mut self, field: &Field, value: &(dyn std::error::Error + 'static)) { + match field.name() { + name if name.starts_with("log.") => (), + _ => { + let display_formatted = format!("{value}"); + self.values + .insert(field.name(), Cow::from(display_formatted)); + } + } + } +} + +impl Layer for StorageLayer +where + S: Subscriber + for<'span> LookupSpan<'span>, +{ + fn on_new_span(&self, attrs: &Attributes<'_>, id: &Id, ctx: Context<'_, S>) { + let span = ctx.span(id).expect("Span not found"); + + // We want to inherit the fields from the parent span, if there is one. + let mut visitor = if let Some(parent_span) = span.parent() { + let mut extensions = parent_span.extensions_mut(); + + let mut inner = extensions + .get_mut::>() + .map(|v| v.to_owned()) + .unwrap_or_default(); + + inner.values.insert( + PARENT_SPAN, + Cow::from(parent_span.id().into_u64().to_string()), + ); + inner + } else { + Storage::default() + }; + + let mut extensions = span.extensions_mut(); + + attrs.record(&mut visitor); + extensions.insert(visitor); + } + + fn on_record(&self, span: &Id, values: &Record<'_>, ctx: Context<'_, S>) { + let span = ctx.span(span).expect("Span not found"); + + let mut extensions = span.extensions_mut(); + let visitor = extensions + .get_mut::>() + .expect("Visitor not found on 'record'!"); + + values.record(visitor); + } + + fn on_follows_from(&self, span: &Id, follows: &Id, ctx: Context<'_, S>) { + let span = ctx.span(span).expect("Span not found"); + let follows_span = ctx.span(follows).expect("Span not found"); + + let mut extensions = span.extensions_mut(); + let follows_extensions = follows_span.extensions(); + + if let Some((visitor, follows_visitor)) = extensions + .get_mut::>() + .zip(follows_extensions.get::>()) + { + // insert "follows_from" span name + visitor + .values + .insert(FOLLOWS_FROM_FIELD, Cow::from(follows_span.name())); + + // insert "follows_from" trace_id + let follows_trace = follows_visitor + .values + .get(TRACE_ID) + .unwrap_or(&Cow::from(format!("{follows:?}"))) + .to_string(); + visitor + .values + .insert(FOLLOWS_FROM_TRACE_ID, Cow::from(follows_trace)); + }; + } + + fn on_event(&self, event: &Event<'_>, ctx: Context<'_, S>) { + ctx.lookup_current().map(|current_span| { + let mut extensions = current_span.extensions_mut(); + extensions.get_mut::>().map(|visitor| { + if event + .fields() + .any(|f| ON_EVENT_KEEP_FIELDS.contains(&f.name())) + { + event.record(visitor); + } + }) + }); + } + + fn on_enter(&self, span: &Id, ctx: Context<'_, S>) { + let span = ctx.span(span).expect("Span not found"); + + let mut extensions = span.extensions_mut(); + if extensions.get_mut::().is_none() { + extensions.insert(Instant::now()); + } + } + fn on_close(&self, id: Id, ctx: Context<'_, S>) { + let span = ctx.span(&id).expect("Span not found"); + + let mut extensions = span.extensions_mut(); + + let elapsed_milliseconds = extensions + .get_mut::() + .map(|i| i.elapsed().as_millis()) + .unwrap_or(0); + + let visitor = extensions + .get_mut::>() + .expect("Visitor not found on 'record'"); + + visitor + .values + .insert(LATENCY_FIELD, Cow::from(format!("{elapsed_milliseconds}"))); + } +} diff --git a/test_rhea.sh b/test_rhea.sh new file mode 100755 index 0000000..a646d74 --- /dev/null +++ b/test_rhea.sh @@ -0,0 +1 @@ +~/.cargo/bin/rhea --url http://localhost:3000/say-hello --concurrency 100 --requests 1000 diff --git a/test_svs.sh b/test_svs.sh new file mode 100755 index 0000000..ada1e1a --- /dev/null +++ b/test_svs.sh @@ -0,0 +1 @@ +UST_LOG="info" /home/alex/.cargo/bin/static-video-server --assets-root "/home/alex/Videos" --port 9092 --host "0.0.0.0" diff --git a/tests/integration_test.rs b/tests/integration_test.rs new file mode 100644 index 0000000..2ec3e27 --- /dev/null +++ b/tests/integration_test.rs @@ -0,0 +1,118 @@ +use http::Uri; +use reqwest::Client; +use reqwest_middleware::{ClientBuilder, ClientWithMiddleware}; +use reqwest_retry::policies::ExponentialBackoff; +use reqwest_tracing::TracingMiddleware; +use serde::Deserialize; +use serde_with::serde_as; +use std::time::Duration; +use wiremock::{ + matchers::{method, path}, + Mock, MockServer, ResponseTemplate, +}; +use efected_coto_emmory::{ + middleware::{ + client::metrics::Metrics, logging::Logger, reqwest_retry::RetryTransientMiddleware, + reqwest_tracing::ExtendedTrace, + }, + settings::{AppEnvironment, HttpClient, HttpClientRetryOptions, Settings}, +}; + +/// Test loading settings. +#[test] +fn test_settings() { + let settings = Settings::load().unwrap(); + assert_eq!(settings.environment(), AppEnvironment::Local); +} + +#[serde_as] +#[derive(Debug, Deserialize)] +struct ClientSettings { + #[serde(default)] + pub http_client: HttpClient, + #[serde(with = "http_serde::uri")] + pub url: Uri, +} + +/// A reqwest-based HTTP client for all Sentilink API operations. +/// +/// 500s are retried. +#[derive(Debug)] +struct AClient { + client: ClientWithMiddleware, + url: String, +} + +impl AClient { + fn load(settings: ClientSettings) -> anyhow::Result { + let retry_policy = ExponentialBackoff::builder() + .retry_bounds( + Duration::from_millis(settings.http_client.retry_options.bounds_low_ms), + Duration::from_millis(settings.http_client.retry_options.bounds_high_ms), + ) + .build_with_max_retries(settings.http_client.retry_options.count.into()); + + // reqwest::Client by default has a timeout of 30s + let reqwest_client = Client::builder() + .pool_idle_timeout(settings.http_client.pool_idle_timeout()) + .timeout(Duration::from_millis(settings.http_client.timeout_ms)) + .build(); + + Ok(Self { + client: ClientBuilder::new(reqwest_client?) + .with(TracingMiddleware::::new()) + .with(Logger) + .with(RetryTransientMiddleware::new_with_policy( + retry_policy, + "AClient".to_string(), + )) + .with(Metrics { + name: "AClient".to_string(), + }) + .build(), + + url: settings.url.to_string(), + }) + } + + async fn query(&self) -> anyhow::Result { + // Send the actual http request. + let response = self + .client + .get(format!("{}query", self.url.to_owned())) + .send() + .await?; + Ok(response) + } +} + +/// Test example reqwest-client call via wiremock. +#[tokio::test] +async fn test_client() { + let mock_server = MockServer::start().await; + + let settings = ClientSettings { + http_client: HttpClient { + pool_idle_timeout_ms: Some(5000), + retry_options: HttpClientRetryOptions { + bounds_low_ms: 100, + bounds_high_ms: 5000, + count: 3, + }, + timeout_ms: 100, + }, + url: mock_server.uri().parse::().unwrap(), + }; + + let client = AClient::load(settings).unwrap(); + + Mock::given(method("GET")) + .and(path("/query")) + .respond_with(ResponseTemplate::new(200)) + .expect(1) // number of expected requests + .mount(&mock_server) + .await; + + let res = client.query().await.unwrap(); + assert_eq!(res.status().as_u16(), 200); +}