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);
+}