├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ ├── build.yml │ └── template_values.toml ├── .gitignore ├── .pre-commit-config.yaml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── SECURITY.md ├── assets ├── logo.png └── screenshot.png ├── cargo-generate.toml ├── examples ├── Cargo.toml ├── README.md └── gen-axum │ ├── .dockerignore │ ├── .envrc │ ├── .github │ └── CODEOWNERS │ ├── .gitignore │ ├── .rustfmt.toml │ ├── CHANGELOG.md │ ├── CODE_OF_CONDUCT.md │ ├── Cargo.lock │ ├── Cargo.toml │ ├── Dockerfile │ ├── LICENSE-APACHE │ ├── LICENSE-MIT │ ├── README.md │ ├── assets │ └── a_logo.png │ ├── config │ └── settings.toml │ ├── deny.toml │ ├── docs │ └── specs │ │ └── latest.json │ ├── flake.lock │ ├── flake.nix │ ├── rust-toolchain.toml │ ├── src │ ├── bin │ │ └── openapi.rs │ ├── docs.rs │ ├── error.rs │ ├── extract │ │ ├── json.rs │ │ └── mod.rs │ ├── headers │ │ ├── header.rs │ │ └── mod.rs │ ├── lib.rs │ ├── main.rs │ ├── metrics │ │ ├── mod.rs │ │ ├── process.rs │ │ └── prom.rs │ ├── middleware │ │ ├── client │ │ │ ├── metrics.rs │ │ │ └── mod.rs │ │ ├── logging.rs │ │ ├── metrics.rs │ │ ├── mod.rs │ │ ├── request_ext.rs │ │ ├── request_ulid.rs │ │ ├── reqwest_retry.rs │ │ ├── reqwest_tracing.rs │ │ └── runtime.rs │ ├── router.rs │ ├── routes │ │ ├── fallback.rs │ │ ├── health.rs │ │ ├── mod.rs │ │ └── ping.rs │ ├── settings.rs │ ├── tracer.rs │ └── tracing_layers │ │ ├── format_layer.rs │ │ ├── metrics_layer.rs │ │ ├── mod.rs │ │ └── storage_layer.rs │ └── tests │ └── integration_test.rs ├── rust+wasm ├── .dockerignore ├── .envrc ├── .github │ ├── CODEOWNERS │ ├── ISSUE_TEMPLATE │ │ ├── bug_report.md │ │ └── feature_request.md │ ├── PULL_REQUEST_TEMPLATE.md │ ├── dependabot.yml │ └── workflows │ │ ├── audit.yml │ │ ├── bench.yml │ │ ├── coverage.yml │ │ ├── docker.yml │ │ ├── release.yml │ │ └── tests_and_checks.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .release-please-manifest.json ├── .rustfmt.toml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.axum.md ├── README.md ├── SECURITY.md ├── assets │ └── a_logo.png ├── cargo-generate.toml ├── codecov.yml ├── deny.toml ├── docker │ ├── Dockerfile.glibc │ └── Dockerfile.musl ├── examples │ ├── Cargo.toml │ └── counterparts.rs ├── final-msg.rhai ├── final-script.rhai ├── flake.nix ├── init-msg.rhai ├── pre-script.rhai ├── release-please-config.json ├── rust-toolchain.toml ├── {{project-name}}-benches │ ├── Cargo.toml │ └── benches │ │ └── a_benchmark.rs ├── {{project-name}}-wasm │ ├── Cargo.toml │ ├── LICENSE-APACHE │ ├── LICENSE-MIT │ ├── README.md │ ├── src │ │ └── lib.rs │ └── tests │ │ └── web.rs └── {{project-name}} │ ├── .dockerignore │ ├── Cargo.axum.toml │ ├── Cargo.toml │ ├── LICENSE-APACHE │ ├── LICENSE-MIT │ ├── README.md │ ├── config │ └── settings.toml │ ├── docs │ └── specs │ │ └── latest.json │ ├── src.axum │ ├── bin │ │ └── openapi.rs │ ├── docs.rs │ ├── error.rs │ ├── extract │ │ ├── json.rs │ │ └── mod.rs │ ├── headers │ │ ├── header.rs │ │ └── mod.rs │ ├── lib.rs │ ├── main.rs │ ├── metrics │ │ ├── mod.rs │ │ ├── process.rs │ │ └── prom.rs │ ├── middleware │ │ ├── client │ │ │ ├── metrics.rs │ │ │ └── mod.rs │ │ ├── logging.rs │ │ ├── metrics.rs │ │ ├── mod.rs │ │ ├── request_ext.rs │ │ ├── request_ulid.rs │ │ ├── reqwest_retry.rs │ │ ├── reqwest_tracing.rs │ │ └── runtime.rs │ ├── router.rs │ ├── routes │ │ ├── fallback.rs │ │ ├── health.rs │ │ ├── mod.rs │ │ └── ping.rs │ ├── settings.rs │ ├── test_utils │ │ ├── mod.rs │ │ └── rvg.rs │ ├── tracer.rs │ └── tracing_layers │ │ ├── format_layer.rs │ │ ├── metrics_layer.rs │ │ ├── mod.rs │ │ └── storage_layer.rs │ ├── src │ ├── lib.rs │ ├── main.rs │ └── test_utils │ │ ├── mod.rs │ │ └── rvg.rs │ └── tests │ ├── integration_test.axum.rs │ └── integration_test.rs └── rust ├── .dockerignore ├── .envrc ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ ├── audit.yml │ ├── bench.yml │ ├── coverage.yml │ ├── docker.yml │ ├── release.yml │ └── tests_and_checks.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .release-please-manifest.json ├── .rustfmt.toml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Cargo.axum.toml ├── Cargo.lock ├── Cargo.toml ├── Dockerfile.glibc ├── Dockerfile.musl ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.axum.md ├── README.md ├── SECURITY.md ├── assets └── a_logo.png ├── benches └── a_benchmark.rs ├── cargo-generate.toml ├── codecov.yml ├── config └── settings.toml ├── deny.toml ├── docs └── specs │ └── latest.json ├── examples └── counterparts.rs ├── final-msg.rhai ├── final-script.rhai ├── flake.nix ├── init-msg.rhai ├── pre-script.rhai ├── release-please-config.json ├── rust-toolchain.toml ├── src.axum ├── bin │ └── openapi.rs ├── docs.rs ├── error.rs ├── extract │ ├── json.rs │ └── mod.rs ├── headers │ ├── header.rs │ └── mod.rs ├── lib.rs ├── main.rs ├── metrics │ ├── mod.rs │ ├── process.rs │ └── prom.rs ├── middleware │ ├── client │ │ ├── metrics.rs │ │ └── mod.rs │ ├── logging.rs │ ├── metrics.rs │ ├── mod.rs │ ├── request_ext.rs │ ├── request_ulid.rs │ ├── reqwest_retry.rs │ ├── reqwest_tracing.rs │ └── runtime.rs ├── router.rs ├── routes │ ├── fallback.rs │ ├── health.rs │ ├── mod.rs │ └── ping.rs ├── settings.rs ├── test_utils │ ├── mod.rs │ └── rvg.rs ├── tracer.rs └── tracing_layers │ ├── format_layer.rs │ ├── metrics_layer.rs │ ├── mod.rs │ └── storage_layer.rs ├── src ├── lib.rs ├── main.rs └── test_utils │ ├── mod.rs │ └── rvg.rs └── tests ├── a_integration_test.rs ├── integration_test.axum.rs └── integration_test.rs /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Default 2 | * @fission-codes/development 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: "\U0001F41B bug" 6 | assignees: '' 7 | 8 | --- 9 | 10 | # Summary 11 | 12 | ## Problem 13 | 14 | Describe the immediate problem. 15 | 16 | ### Impact 17 | 18 | What's the impact of this bug? 19 | 20 | ## Solution 21 | 22 | Describe the sort of fix that would solve the issue. 23 | 24 | # Detail 25 | 26 | **Describe the bug** 27 | 28 | A clear and concise description of what the bug is. 29 | 30 | **To Reproduce** 31 | 32 | Steps to reproduce the behavior: 33 | 1. Run '...' 34 | 2. Select option '....' 35 | 4. See error 36 | 37 | **Expected behavior** 38 | 39 | A clear and concise description of what you expected to happen. 40 | 41 | **Screenshots** 42 | 43 | Please include a screenshot or copy-paste of the commands to run to reproduce the bug. 44 | 45 | **Desktop (please complete the following information):** 46 | 47 | - OS: [e.g. iOS] 48 | - Version [e.g. 22] 49 | - Rust toolchain [e.g. stable] 50 | 51 | **Additional context** 52 | 53 | Add any other context about the problem here. 54 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: "\U0001F497 enhancement" 6 | assignees: '' 7 | 8 | --- 9 | 10 | NB: Feature requests will only be considered if they solve a pain or present a useful refactoring of the code. 11 | 12 | # Summary 13 | 14 | ## Problem 15 | 16 | Describe the pain that this feature will solve. 17 | 18 | ### Impact 19 | 20 | Describe the impact of not having this feature. 21 | 22 | ## Solution 23 | 24 | Describe the solution. 25 | 26 | # Detail 27 | 28 | **Is your feature request related to a problem? Please describe.** 29 | 30 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 31 | 32 | **Describe the solution you'd like** 33 | 34 | A clear and concise description of what you want to happen. 35 | 36 | **Describe alternatives you've considered** 37 | 38 | A clear and concise description of any alternative solutions or features you've considered. 39 | 40 | **Additional context** 41 | 42 | Add any other context or screenshots about the feature request here. 43 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # Description 2 | 3 | Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. 4 | 5 | ## Link to issue 6 | 7 | Please add a link to any relevant issues/tickets. 8 | 9 | ## Type of change 10 | 11 | - [ ] Bug fix (non-breaking change that fixes an issue) 12 | - [ ] New feature (non-breaking change that adds functionality) 13 | - [ ] Refactor (non-breaking change that updates existing functionality) 14 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 15 | - [ ] This change requires a documentation update 16 | - [ ] Comments have been added/updated 17 | 18 | Please delete options that are not relevant. 19 | 20 | ## Test plan (required) 21 | 22 | Demonstrate the code is solid. Are changes to the templates tested in the top-level project workflows? 23 | 24 | Which commands did you test with and what are the expected results? Which tests have you added or updated? Do the 25 | tests cover all of the changes included in this PR? 26 | 27 | ## Screenshots/Screencaps 28 | 29 | Please include a screenshot or screencap that includes any commands that have been added to the generator. 30 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | 8 | updates: 9 | - package-ecosystem: "cargo" 10 | directory: "/rust" 11 | commit-message: 12 | prefix: "chore" 13 | include: "scope" 14 | target-branch: "main" 15 | schedule: 16 | interval: "weekly" 17 | 18 | - package-ecosystem: "cargo" 19 | directory: "/rust+wasm" 20 | commit-message: 21 | prefix: "chore" 22 | include: "scope" 23 | target-branch: "main" 24 | schedule: 25 | interval: "weekly" 26 | 27 | - package-ecosystem: "cargo" 28 | directory: "/examples/gen-axum" 29 | commit-message: 30 | prefix: "chore(examples)" 31 | include: "scope" 32 | target-branch: "main" 33 | schedule: 34 | interval: "weekly" 35 | 36 | - package-ecosystem: "github-actions" 37 | directory: "/" 38 | commit-message: 39 | prefix: "chore(ci)" 40 | include: "scope" 41 | target-branch: "main" 42 | schedule: 43 | interval: "weekly" 44 | 45 | - package-ecosystem: "github-actions" 46 | directory: "/rust" 47 | commit-message: 48 | prefix: "chore(ci)" 49 | include: "scope" 50 | target-branch: "main" 51 | schedule: 52 | interval: "weekly" 53 | 54 | - package-ecosystem: "github-actions" 55 | directory: "/rust+wasm" 56 | commit-message: 57 | prefix: "chore(ci)" 58 | include: "scope" 59 | target-branch: "main" 60 | schedule: 61 | interval: "weekly" 62 | -------------------------------------------------------------------------------- /.github/workflows/template_values.toml: -------------------------------------------------------------------------------- 1 | [values] 2 | auditable = true 3 | axum = false 4 | bench = true 5 | codecov = true 6 | contributing = true 7 | description = "Rust template for project." 8 | dependabot = true 9 | discordlink = "http://discord.link" 10 | docker = false 11 | github_actions = true 12 | github-codeowner = "rush/geddylee" 13 | github-email = "flybynight@subdivisions.com" 14 | github-name = "geddylee" 15 | have_discord = true 16 | license = "dual" 17 | nix = true 18 | node-or-web = "web" 19 | port = "3000" 20 | metricsport = "4000" 21 | readme = true 22 | repo-name = "fission-codes" 23 | rust_project = false 24 | security_policy = true 25 | templates = true 26 | toolchain = "stable" 27 | wasm_project = false 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | private 2 | *.temp 3 | *.tmp 4 | .history 5 | .DS_Store 6 | 7 | examples/Cargo.lock 8 | examples/target 9 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # pre-commit install 3 | # pre-commit install --hook-type commit-msg 4 | exclude: ^(LICENSE|LICENSE*) 5 | repos: 6 | - repo: https://github.com/compilerla/conventional-pre-commit 7 | rev: v2.1.1 8 | hooks: 9 | - id: conventional-pre-commit 10 | stages: 11 | - commit-msg 12 | 13 | - repo: https://github.com/pre-commit/pre-commit-hooks 14 | rev: v4.3.0 15 | hooks: 16 | - id: no-commit-to-branch 17 | args: ["-b", "main"] 18 | - id: check-merge-conflict 19 | - id: trailing-whitespace 20 | - id: end-of-file-fixer 21 | - id: check-yaml 22 | exclude: ^(rust|rust+wasm) 23 | - id: check-json 24 | exclude: ^(rust|rust+wasm) 25 | - id: check-added-large-files 26 | - id: detect-private-key 27 | exclude: ^(rust|rust+wasm) 28 | - id: check-executables-have-shebangs 29 | - id: check-toml 30 | exclude: ^(rust|rust+wasm) 31 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any 2 | person obtaining a copy of this software and associated 3 | documentation files (the "Software"), to deal in the 4 | Software without restriction, including without 5 | limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following 9 | conditions: 10 | 11 | The above copyright notice and this permission notice 12 | shall be included in all copies or substantial portions 13 | of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | ## Report a security issue or vulnerability 2 | 3 | The [Fission][fission] team welcomes security reports and is committed to 4 | providing prompt attention to security issues. Security issues should be 5 | reported privately via [support@fission.codes][support-email]. Security issues 6 | should not be reported via the public GitHub Issue tracker. 7 | 8 | ## Security advisories 9 | 10 | The project team is committed to transparency in the security issue disclosure 11 | process. The Fission team announces security advisories through our 12 | Github respository's [security portal][sec-advisories] and and the 13 | [RustSec advisory database][rustsec-db]. 14 | 15 | [fission]: https://fission.codes/ 16 | [rustsec-db]: https://github.com/RustSec/advisory-db 17 | [sec-advisories]: https://github.com/fission-codes/rust-template/security/advisories 18 | [support-email]: mailto:support@fission.codes 19 | -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fission-codes/rust-template/64a9bfd65ed4e8fe39249df0784d741cf4838276/assets/logo.png -------------------------------------------------------------------------------- /assets/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fission-codes/rust-template/64a9bfd65ed4e8fe39249df0784d741cf4838276/assets/screenshot.png -------------------------------------------------------------------------------- /cargo-generate.toml: -------------------------------------------------------------------------------- 1 | [template] 2 | sub_templates = [ 3 | "rust", 4 | "rust+wasm" 5 | ] 6 | -------------------------------------------------------------------------------- /examples/Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["*"] 3 | exclude = ["target"] 4 | resolver = "2" 5 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | This workspace contains numerous examples demonstrating template-generated 2 | code and combinations. Each example is setup as its own crate, with its own 3 | dependencies. 4 | 5 | ## Running Examples 6 | 7 | To run any example from this top-level: 8 | 9 | ```console 10 | cargo run -p 11 | ``` 12 | 13 | For example, `cargo run -p gen-axum --all-features` will run a *mostly* 14 | stock-generated rust [axum][axum] server. 15 | 16 | ## Running Tests 17 | 18 | - Run tests 19 | 20 | ```console 21 | cargo test -p 22 | ``` 23 | 24 | [axum]: https://docs.rs/axum/latest/axum/ 25 | -------------------------------------------------------------------------------- /examples/gen-axum/.dockerignore: -------------------------------------------------------------------------------- 1 | * 2 | 3 | !Cargo.lock 4 | !Cargo.toml 5 | !src 6 | !config 7 | -------------------------------------------------------------------------------- /examples/gen-axum/.envrc: -------------------------------------------------------------------------------- 1 | use_flake 2 | -------------------------------------------------------------------------------- /examples/gen-axum/.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Default 2 | * @fission-codes/development 3 | -------------------------------------------------------------------------------- /examples/gen-axum/.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # These are backup files generated by rustfmt 6 | **/*.rs.bk 7 | 8 | # Ignore local environment settings 9 | .envrc.custom 10 | .direnv 11 | 12 | # Other files + dirs 13 | private 14 | *.temp 15 | *.tmp 16 | .history 17 | .DS_Store 18 | -------------------------------------------------------------------------------- /examples/gen-axum/.rustfmt.toml: -------------------------------------------------------------------------------- 1 | edition = "2021" 2 | imports_granularity = "Crate" 3 | -------------------------------------------------------------------------------- /examples/gen-axum/CHANGELOG.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fission-codes/rust-template/64a9bfd65ed4e8fe39249df0784d741cf4838276/examples/gen-axum/CHANGELOG.md -------------------------------------------------------------------------------- /examples/gen-axum/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gen-axum" 3 | version = "0.1.0" 4 | description = "example-axum-project" 5 | keywords = [] 6 | categories = [] 7 | include = ["/src", "README.md", "LICENSE-APACHE", "LICENSE-MIT"] 8 | license = "Apache-2.0 or MIT" 9 | readme = "README.md" 10 | edition = "2021" 11 | rust-version = "1.67" 12 | documentation = "https://docs.rs/gen-axum" 13 | repository = "https://github.com/fission-codes/gen-axum" 14 | authors = ["Zeeshan Lakhani "] 15 | default-run = "gen-axum-app" 16 | 17 | [lib] 18 | path = "src/lib.rs" 19 | doctest = true 20 | 21 | [[bin]] 22 | name = "gen-axum-app" 23 | path = "src/main.rs" 24 | doc = false 25 | 26 | [[bin]] 27 | name = "openapi" 28 | path = "src/bin/openapi.rs" 29 | test = false 30 | doc = false 31 | bench = false 32 | 33 | [dependencies] 34 | ansi_term = { version = "0.12", optional = true, default-features = false } 35 | anyhow = { version = "1.0", features = ["backtrace"] } 36 | async-trait = "0.1" 37 | axum = { version = "0.6", features = ["headers"] } 38 | axum-tracing-opentelemetry = { version = "0.10", features = ["otlp"] } 39 | base64 = "0.21" 40 | chrono = { version = "0.4", default-features = false, features = ["clock"] } 41 | config = "0.13" 42 | console-subscriber = { version = "0.1", default-features = false, features = [ "parking_lot" ], optional = true } 43 | const_format = "0.2" 44 | futures = "0.3" 45 | headers = "0.3" 46 | http = "0.2" 47 | http-serde = "1.1" 48 | hyper = "0.14" 49 | metrics = "0.20" 50 | metrics-exporter-prometheus = "0.11" 51 | metrics-util = { version = "0.14", default-features = true } 52 | mime = "0.3" 53 | num_cpus = "1.0" 54 | once_cell = "1.17" 55 | openssl = { version = "0.10", features = ["vendored"], default-features = false } 56 | opentelemetry = { version = "0.18", features = ["rt-tokio", "trace"] } 57 | opentelemetry-otlp = { version = "0.11", features = ["metrics", "grpc-tonic", "tls-roots"], default-features = false } 58 | opentelemetry-semantic-conventions = "0.10" 59 | parking_lot = "0.12" 60 | reqwest = { version = "0.11", features = ["json"] } 61 | reqwest-middleware = "0.2" 62 | reqwest-retry = "0.2" 63 | reqwest-tracing = { version = "0.4", features = ["opentelemetry_0_17"] } 64 | retry-policies = "0.1" 65 | serde = { version = "1.0", features = ["derive"] } 66 | serde_json = "1.0" 67 | serde_path_to_error = "0.1" 68 | serde_with = "3.0" 69 | sysinfo = "0.28" 70 | task-local-extensions = "0.1" 71 | thiserror = "1.0" 72 | time = { version = "0.3", features = ["serde-well-known", "serde-human-readable"] } 73 | tokio = { version = "1.26", features = ["full", "parking_lot"] } 74 | ## Tied to opentelemetry-otlp dependency 75 | tonic = { version = "0.8" } 76 | tower = "0.4" 77 | tower-http = { version = "0.4", features = ["catch-panic", "request-id", "sensitive-headers", "timeout", "trace", "util"] } 78 | tracing = "0.1" 79 | tracing-appender = "0.2" 80 | tracing-opentelemetry = "0.18" 81 | tracing-subscriber = { version = "0.3", features = ["env-filter", "json", "parking_lot", "registry"] } 82 | ulid = { version = "1.0", features = ["serde"] } 83 | url = "2.3" 84 | utoipa = { version = "3.3", features = ["uuid", "axum_extras"] } 85 | utoipa-swagger-ui = { version = "3.1", features = ["axum"] } 86 | 87 | [dev-dependencies] 88 | assert-json-diff = "2.0" 89 | rsa = { version = "0.8" } 90 | tokio-test = "0.4" 91 | wiremock = "0.5" 92 | 93 | [features] 94 | ansi-logs = ["ansi_term"] 95 | console = ["console-subscriber"] 96 | default = [] 97 | 98 | [package.metadata.docs.rs] 99 | all-features = true 100 | # defines the configuration attribute `docsrs` 101 | rustdoc-args = ["--cfg", "docsrs"] 102 | -------------------------------------------------------------------------------- /examples/gen-axum/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | # AMD64 4 | FROM --platform=$BUILDPLATFORM messense/rust-musl-cross:x86_64-musl as builder-amd64 5 | 6 | # ARM64 7 | FROM --platform=$BUILDPLATFORM messense/rust-musl-cross:aarch64-musl as builder-arm64 8 | 9 | ARG TARGETARCH 10 | FROM builder-$TARGETARCH as builder 11 | 12 | RUN apt update && apt install -y protobuf-compiler 13 | 14 | RUN adduser --disabled-password --disabled-login --gecos "" --no-create-home gen-axum 15 | 16 | RUN cargo init 17 | 18 | # touch lib.rs as we combine both 19 | RUN touch src/lib.rs 20 | 21 | # copy cargo.* 22 | COPY Cargo.lock ./Cargo.lock 23 | COPY Cargo.toml ./Cargo.toml 24 | 25 | # cache depencies 26 | RUN mkdir .cargo 27 | RUN cargo vendor > .cargo/config 28 | RUN --mount=type=cache,target=$CARGO_HOME/registry \ 29 | --mount=type=cache,target=$CARGO_HOME/.git \ 30 | --mount=type=cache,target=gen-axum/target,sharing=locked \ 31 | cargo build --target $CARGO_BUILD_TARGET --bin gen-axum-app --release 32 | 33 | # copy src 34 | COPY src ./src 35 | # copy config 36 | COPY config ./config 37 | 38 | # final build for release 39 | RUN rm ./target/$CARGO_BUILD_TARGET/release/deps/*gen_axum* 40 | RUN --mount=type=cache,target=$CARGO_HOME/registry \ 41 | --mount=type=cache,target=$CARGO_HOME/.git \ 42 | --mount=type=cache,target=gen-axum/target,sharing=locked \ 43 | cargo build --target $CARGO_BUILD_TARGET --bin gen-axum-app --release 44 | 45 | RUN musl-strip ./target/$CARGO_BUILD_TARGET/release/gen-axum-app 46 | 47 | RUN mv ./target/$CARGO_BUILD_TARGET/release/gen-axum* /usr/local/bin 48 | RUN mv ./config /etc/config 49 | 50 | FROM scratch 51 | 52 | ARG backtrace=0 53 | ARG log_level=info 54 | 55 | ENV RUST_BACKTRACE=${backtrace} \ 56 | RUST_LOG=${log_level} 57 | 58 | COPY --from=builder /usr/local/bin/gen-axum* . 59 | COPY --from=builder /etc/config ./config 60 | COPY --from=builder /etc/passwd /etc/passwd 61 | COPY --from=builder /etc/group /etc/group 62 | 63 | USER gen-axum:gen-axum 64 | 65 | EXPOSE 3000 66 | EXPOSE 4000 67 | ENTRYPOINT ["./gen-axum-app"] 68 | -------------------------------------------------------------------------------- /examples/gen-axum/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any 2 | person obtaining a copy of this software and associated 3 | documentation files (the "Software"), to deal in the 4 | Software without restriction, including without 5 | limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following 9 | conditions: 10 | 11 | The above copyright notice and this permission notice 12 | shall be included in all copies or substantial portions 13 | of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /examples/gen-axum/assets/a_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fission-codes/rust-template/64a9bfd65ed4e8fe39249df0784d741cf4838276/examples/gen-axum/assets/a_logo.png -------------------------------------------------------------------------------- /examples/gen-axum/config/settings.toml: -------------------------------------------------------------------------------- 1 | [monitoring] 2 | process_collector_interval = 10 3 | 4 | [otel] 5 | exporter_otlp_endpoint = "http://localhost:4317" 6 | 7 | [server] 8 | environment = "local" 9 | metrics_port = 4000 10 | port = 3000 11 | timeout_ms = 30000 12 | -------------------------------------------------------------------------------- /examples/gen-axum/docs/specs/latest.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.3", 3 | "info": { 4 | "title": "gen-axum", 5 | "description": "example-axum-project", 6 | "contact": { 7 | "name": "Zeeshan Lakhani", 8 | "email": "zeeshan.lakhani@gmail.com" 9 | }, 10 | "license": { 11 | "name": "Apache-2.0 or MIT" 12 | }, 13 | "version": "0.1.0" 14 | }, 15 | "paths": { 16 | "/healthcheck": { 17 | "get": { 18 | "tags": [ 19 | "health" 20 | ], 21 | "summary": "GET handler for checking service health.", 22 | "description": "GET handler for checking service health.", 23 | "operationId": "healthcheck", 24 | "responses": { 25 | "200": { 26 | "description": "gen-axum healthy" 27 | }, 28 | "500": { 29 | "description": "gen-axum not healthy", 30 | "content": { 31 | "application/json": { 32 | "schema": { 33 | "$ref": "#/components/schemas/AppError" 34 | } 35 | } 36 | } 37 | } 38 | }, 39 | "deprecated": false 40 | } 41 | }, 42 | "/ping": { 43 | "get": { 44 | "tags": [ 45 | "ping" 46 | ], 47 | "summary": "GET handler for internal pings and availability", 48 | "description": "GET handler for internal pings and availability", 49 | "operationId": "get", 50 | "responses": { 51 | "200": { 52 | "description": "Ping successful" 53 | }, 54 | "500": { 55 | "description": "Ping not successful", 56 | "content": { 57 | "application/json": { 58 | "schema": { 59 | "$ref": "#/components/schemas/AppError" 60 | } 61 | } 62 | } 63 | } 64 | }, 65 | "deprecated": false 66 | } 67 | } 68 | }, 69 | "components": { 70 | "schemas": { 71 | "AppError": { 72 | "type": "object", 73 | "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", 74 | "required": [ 75 | "status" 76 | ], 77 | "properties": { 78 | "detail": { 79 | "type": "string" 80 | }, 81 | "status": { 82 | "type": "integer", 83 | "format": "int32", 84 | "example": 200 85 | }, 86 | "title": { 87 | "type": "string" 88 | } 89 | } 90 | } 91 | } 92 | }, 93 | "tags": [ 94 | { 95 | "name": "", 96 | "description": "gen-axum service/middleware" 97 | } 98 | ] 99 | } 100 | -------------------------------------------------------------------------------- /examples/gen-axum/flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "locked": { 5 | "lastModified": 1667395993, 6 | "narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=", 7 | "owner": "numtide", 8 | "repo": "flake-utils", 9 | "rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "owner": "numtide", 14 | "repo": "flake-utils", 15 | "type": "github" 16 | } 17 | }, 18 | "nixpkgs": { 19 | "locked": { 20 | "lastModified": 1675154384, 21 | "narHash": "sha256-gUXzyTS3WsO3g2Rz0qOYR2a26whkyL2UfTr1oPH9mm8=", 22 | "owner": "NixOS", 23 | "repo": "nixpkgs", 24 | "rev": "0218941ea68b4c625533bead7bbb94ccce52dceb", 25 | "type": "github" 26 | }, 27 | "original": { 28 | "id": "nixpkgs", 29 | "ref": "nixos-22.11", 30 | "type": "indirect" 31 | } 32 | }, 33 | "root": { 34 | "inputs": { 35 | "flake-utils": "flake-utils", 36 | "nixpkgs": "nixpkgs", 37 | "rust-overlay": "rust-overlay" 38 | } 39 | }, 40 | "rust-overlay": { 41 | "inputs": { 42 | "flake-utils": [ 43 | "flake-utils" 44 | ], 45 | "nixpkgs": [ 46 | "nixpkgs" 47 | ] 48 | }, 49 | "locked": { 50 | "lastModified": 1675132198, 51 | "narHash": "sha256-izOVjdIfdv0OzcfO9rXX0lfGkQn4tdJ0eNm3P3LYo/o=", 52 | "owner": "oxalica", 53 | "repo": "rust-overlay", 54 | "rev": "48b1403150c3f5a9aeee8bc4c77c8926f29c6501", 55 | "type": "github" 56 | }, 57 | "original": { 58 | "owner": "oxalica", 59 | "repo": "rust-overlay", 60 | "type": "github" 61 | } 62 | } 63 | }, 64 | "root": "root", 65 | "version": 7 66 | } 67 | -------------------------------------------------------------------------------- /examples/gen-axum/flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "gen-axum"; 3 | 4 | inputs = { 5 | nixpkgs.url = "nixpkgs/nixos-22.11"; 6 | flake-utils.url = "github:numtide/flake-utils"; 7 | 8 | rust-overlay = { 9 | url = "github:oxalica/rust-overlay"; 10 | inputs.nixpkgs.follows = "nixpkgs"; 11 | inputs.flake-utils.follows = "flake-utils"; 12 | }; 13 | }; 14 | 15 | outputs = { 16 | self, 17 | nixpkgs, 18 | flake-utils, 19 | rust-overlay, 20 | } @ inputs: 21 | flake-utils.lib.eachDefaultSystem ( 22 | system: let 23 | overlays = [(import rust-overlay)]; 24 | pkgs = import nixpkgs {inherit system overlays;}; 25 | 26 | rust-toolchain = (pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml).override { 27 | extensions = ["cargo" "clippy" "rustfmt" "rust-src" "rust-std"]; 28 | }; 29 | 30 | nightly-rustfmt = pkgs.rust-bin.nightly.latest.rustfmt; 31 | 32 | format-pkgs = with pkgs; [ 33 | nixpkgs-fmt 34 | alejandra 35 | ]; 36 | 37 | cargo-installs = with pkgs; [ 38 | cargo-deny 39 | cargo-expand 40 | cargo-outdated 41 | cargo-sort 42 | cargo-udeps 43 | cargo-watch 44 | ]; 45 | in rec 46 | { 47 | devShells.default = pkgs.mkShell { 48 | name = "gen-axum"; 49 | nativeBuildInputs = with pkgs; 50 | [ 51 | # The ordering of these two items is important. For nightly rustfmt to be used instead of 52 | # the rustfmt provided by `rust-toolchain`, it must appear first in the list. This is 53 | # because native build inputs are added to $PATH in the order they're listed here. 54 | nightly-rustfmt 55 | rust-toolchain 56 | pre-commit 57 | protobuf 58 | direnv 59 | self.packages.${system}.irust 60 | ] 61 | ++ format-pkgs 62 | ++ cargo-installs 63 | ++ lib.optionals stdenv.isDarwin [ 64 | darwin.apple_sdk.frameworks.Security 65 | darwin.apple_sdk.frameworks.CoreFoundation 66 | darwin.apple_sdk.frameworks.Foundation 67 | ]; 68 | 69 | shellHook = '' 70 | [ -e .git/hooks/pre-commit ] || pre-commit install --install-hooks && pre-commit install --hook-type commit-msg 71 | ''; 72 | }; 73 | 74 | packages.irust = pkgs.rustPlatform.buildRustPackage rec { 75 | pname = "irust"; 76 | version = "1.65.1"; 77 | src = pkgs.fetchFromGitHub { 78 | owner = "sigmaSd"; 79 | repo = "IRust"; 80 | rev = "v${version}"; 81 | sha256 = "sha256-AMOND5q1XzNhN5smVJp+2sGl/OqbxkGPGuPBCE48Hik="; 82 | }; 83 | 84 | doCheck = false; 85 | cargoSha256 = "sha256-A24O3p85mCRVZfDyyjQcQosj/4COGNnqiQK2a7nCP6I="; 86 | }; 87 | } 88 | ); 89 | } 90 | -------------------------------------------------------------------------------- /examples/gen-axum/rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "stable" 3 | -------------------------------------------------------------------------------- /examples/gen-axum/src/bin/openapi.rs: -------------------------------------------------------------------------------- 1 | use std::{fs::File, io::prelude::*, path::PathBuf}; 2 | use utoipa::OpenApi; 3 | use gen_axum::docs::ApiDoc; 4 | 5 | fn main() { 6 | let json_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("docs/specs/latest.json"); 7 | let json_path_show = json_path.as_path().display().to_string(); 8 | 9 | let mut file = match File::create(json_path) { 10 | Ok(file) => file, 11 | Err(err) => { 12 | eprintln!("error creating file: {err:?}"); 13 | std::process::exit(1) 14 | } 15 | }; 16 | 17 | let json = match ApiDoc::openapi().to_pretty_json() { 18 | Ok(mut json) => { 19 | json.push('\n'); 20 | json 21 | } 22 | Err(err) => { 23 | eprintln!("error generating OpenAPI json: {err:?}"); 24 | std::process::exit(1) 25 | } 26 | }; 27 | 28 | match file.write_all(json.as_bytes()) { 29 | Ok(_) => println!("OpenAPI json written to path: {json_path_show}\n\n{json}"), 30 | Err(err) => { 31 | eprintln!("error writing to file. {err:?}"); 32 | std::process::exit(1) 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /examples/gen-axum/src/docs.rs: -------------------------------------------------------------------------------- 1 | //! OpenAPI doc generation. 2 | 3 | use crate::{ 4 | error::AppError, 5 | routes::{health, ping}, 6 | }; 7 | use utoipa::OpenApi; 8 | 9 | /// API documentation generator. 10 | #[derive(OpenApi)] 11 | #[openapi( 12 | paths(health::healthcheck, ping::get), 13 | components(schemas(AppError)), 14 | tags( 15 | (name = "", description = "gen-axum service/middleware") 16 | ) 17 | )] 18 | 19 | /// Tied to OpenAPI documentation. 20 | #[derive(Debug)] 21 | pub struct ApiDoc; 22 | -------------------------------------------------------------------------------- /examples/gen-axum/src/extract/mod.rs: -------------------------------------------------------------------------------- 1 | //! Custom [axum::extract] Extractors. 2 | 3 | pub mod json; 4 | -------------------------------------------------------------------------------- /examples/gen-axum/src/headers/header.rs: -------------------------------------------------------------------------------- 1 | use axum::{extract::TypedHeader, headers::Header}; 2 | 3 | /// Generate String-focused, generic, custom typed [`Header`]'s. 4 | #[allow(unused)] 5 | macro_rules! header { 6 | ($tname:ident, $hname:ident, $sname:expr) => { 7 | static $hname: once_cell::sync::Lazy = 8 | once_cell::sync::Lazy::new(|| axum::headers::HeaderName::from_static($sname)); 9 | 10 | #[doc = "Generated custom [`axum::headers::Header`] for "] 11 | #[doc = $sname] 12 | #[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] 13 | pub(crate) struct $tname(pub(crate) String); 14 | 15 | impl std::fmt::Display for $tname { 16 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 17 | write!(f, "{}", self.0) 18 | } 19 | } 20 | 21 | impl std::convert::From<&str> for $tname { 22 | fn from(item: &str) -> Self { 23 | $tname(item.to_string()) 24 | } 25 | } 26 | 27 | impl axum::headers::Header for $tname { 28 | fn name() -> &'static axum::headers::HeaderName { 29 | &$hname 30 | } 31 | 32 | fn decode<'i, I>(values: &mut I) -> Result 33 | where 34 | I: Iterator, 35 | { 36 | values 37 | .next() 38 | .and_then(|v| v.to_str().ok()) 39 | .map(|x| $tname(x.to_string())) 40 | .ok_or_else(axum::headers::Error::invalid) 41 | } 42 | 43 | fn encode(&self, values: &mut E) 44 | where 45 | E: Extend, 46 | { 47 | if let Ok(value) = axum::headers::HeaderValue::from_str(&self.0) { 48 | values.extend(std::iter::once(value)); 49 | } 50 | } 51 | } 52 | }; 53 | } 54 | 55 | /// Trait for returning header value directly for passing 56 | /// along to client calls. 57 | pub(crate) trait HeaderValue { 58 | fn header_value(&self) -> String; 59 | } 60 | 61 | impl HeaderValue for TypedHeader 62 | where 63 | T: Header + std::fmt::Display, 64 | { 65 | fn header_value(&self) -> String { 66 | self.0.to_string() 67 | } 68 | } 69 | 70 | impl HeaderValue for &TypedHeader 71 | where 72 | T: Header + std::fmt::Display, 73 | { 74 | fn header_value(&self) -> String { 75 | self.0.to_string() 76 | } 77 | } 78 | 79 | #[cfg(test)] 80 | pub(crate) mod tests { 81 | use axum::{ 82 | headers::{Header, HeaderMapExt}, 83 | http, 84 | }; 85 | 86 | header!(XDummyId, XDUMMY_ID, "x-dummy-id"); 87 | 88 | fn test_decode(values: &[&str]) -> Option { 89 | let mut map = http::HeaderMap::new(); 90 | for val in values { 91 | map.append(T::name(), val.parse().unwrap()); 92 | } 93 | map.typed_get() 94 | } 95 | 96 | fn test_encode(header: T) -> http::HeaderMap { 97 | let mut map = http::HeaderMap::new(); 98 | map.typed_insert(header); 99 | map 100 | } 101 | 102 | #[test] 103 | fn test_dummy_header() { 104 | let s = "18312349-3139-498C-84B6-87326BF1F2A7"; 105 | let dummy_id = test_decode::(&[s]).unwrap(); 106 | let headers = test_encode(dummy_id); 107 | assert_eq!(headers["x-dummy-id"], s); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /examples/gen-axum/src/headers/mod.rs: -------------------------------------------------------------------------------- 1 | //! Working-with and generating custom headers. 2 | 3 | pub(crate) mod header; 4 | -------------------------------------------------------------------------------- /examples/gen-axum/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(docsrs, feature(doc_cfg))] 2 | #![warn(missing_debug_implementations, missing_docs, rust_2018_idioms)] 3 | #![deny(unreachable_pub, private_in_public)] 4 | 5 | //! gen-axum 6 | 7 | pub mod docs; 8 | pub mod error; 9 | pub mod extract; 10 | pub mod headers; 11 | pub mod metrics; 12 | pub mod middleware; 13 | pub mod router; 14 | pub mod routes; 15 | pub mod settings; 16 | pub mod tracer; 17 | pub mod tracing_layers; 18 | -------------------------------------------------------------------------------- /examples/gen-axum/src/metrics/mod.rs: -------------------------------------------------------------------------------- 1 | //! Metrics capture and Prometheus recorder. 2 | 3 | pub mod process; 4 | pub mod prom; 5 | -------------------------------------------------------------------------------- /examples/gen-axum/src/metrics/prom.rs: -------------------------------------------------------------------------------- 1 | //! Metrics Prometheus recorder. 2 | 3 | use crate::metrics::process; 4 | 5 | use metrics_exporter_prometheus::{Matcher, PrometheusBuilder, PrometheusHandle}; 6 | 7 | /// Sets up Prometheus buckets for matched metrics and installs recorder. 8 | pub fn setup_metrics_recorder() -> anyhow::Result { 9 | const EXPONENTIAL_SECONDS: &[f64] = &[ 10 | 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 11 | ]; 12 | 13 | let builder = PrometheusBuilder::new() 14 | .set_buckets_for_metric( 15 | Matcher::Suffix("_duration_seconds".to_string()), 16 | EXPONENTIAL_SECONDS, 17 | )? 18 | .install_recorder()?; 19 | 20 | process::describe(); 21 | 22 | Ok(builder) 23 | } 24 | -------------------------------------------------------------------------------- /examples/gen-axum/src/middleware/client/metrics.rs: -------------------------------------------------------------------------------- 1 | //! Middleware for tracking metrics on each client [reqwest::Request]. 2 | 3 | use reqwest_middleware::Middleware as ReqwestMiddleware; 4 | use std::time::Instant; 5 | use task_local_extensions::Extensions; 6 | 7 | const OK: &str = "ok"; 8 | const ERROR: &str = "error"; 9 | const MIDDLEWARE_ERROR: &str = "middleware_error"; 10 | const NONE: &str = "none"; 11 | const RESULT: &str = "result"; 12 | const STATUS: &str = "status"; 13 | 14 | /// Metrics struct for use as part of middleware. 15 | #[derive(Debug)] 16 | pub struct Metrics { 17 | /// Client name for metric(s) gathering. 18 | pub name: String, 19 | } 20 | 21 | #[async_trait::async_trait] 22 | impl ReqwestMiddleware for Metrics { 23 | async fn handle( 24 | &self, 25 | request: reqwest::Request, 26 | extensions: &mut Extensions, 27 | next: reqwest_middleware::Next<'_>, 28 | ) -> Result { 29 | let now = Instant::now(); 30 | 31 | let url = request.url().clone(); 32 | let request_path: String = url.path().to_string(); 33 | let method = request.method().clone(); 34 | 35 | let result = next.run(request, extensions).await; 36 | let latency = now.elapsed().as_secs_f64(); 37 | 38 | let labels = vec![ 39 | ("client", self.name.to_string()), 40 | ("method", method.to_string()), 41 | ("request_path", request_path), 42 | ]; 43 | 44 | let extended_labels = extend_labels_for_response(labels, &result); 45 | 46 | metrics::increment_counter!("client_http_requests_total", &extended_labels); 47 | metrics::histogram!( 48 | "client_http_request_duration_seconds", 49 | latency, 50 | &extended_labels 51 | ); 52 | 53 | result 54 | } 55 | } 56 | 57 | /// Extend a set of metrics label tuples with dynamic properties 58 | /// around reqwest responses for `result` and `status` fields. 59 | pub fn extend_labels_for_response<'a>( 60 | mut labels: Vec<(&'a str, String)>, 61 | result: &Result, 62 | ) -> Vec<(&'a str, String)> { 63 | match result { 64 | Ok(ref success) => { 65 | match success.status().as_u16() { 66 | 200..=299 => labels.push((RESULT, OK.to_string())), 67 | _ => labels.push((RESULT, ERROR.to_string())), 68 | } 69 | 70 | labels.push((STATUS, success.status().as_u16().to_string())); 71 | } 72 | Err(reqwest_middleware::Error::Reqwest(ref err)) => { 73 | labels.push((RESULT, ERROR.to_string())); 74 | labels.push(( 75 | STATUS, 76 | err.status() 77 | .map(|status| status.as_u16().to_string()) 78 | .unwrap_or_else(|| NONE.to_string()), 79 | )); 80 | } 81 | Err(reqwest_middleware::Error::Middleware(ref _err)) => { 82 | labels.push((RESULT, MIDDLEWARE_ERROR.to_string())); 83 | labels.push((STATUS, NONE.to_string())); 84 | } 85 | }; 86 | 87 | labels 88 | } 89 | -------------------------------------------------------------------------------- /examples/gen-axum/src/middleware/client/mod.rs: -------------------------------------------------------------------------------- 1 | //! Middleware for calls to outside client APIs. 2 | 3 | pub mod metrics; 4 | -------------------------------------------------------------------------------- /examples/gen-axum/src/middleware/metrics.rs: -------------------------------------------------------------------------------- 1 | //! Middleware for tracking metrics on each [axum::http::Request]. 2 | 3 | use crate::middleware::request_ext::RequestExt; 4 | use axum::{http::Request, middleware::Next, response::IntoResponse}; 5 | use std::time::Instant; 6 | 7 | /// Middleware function called to track (and update) http metrics when a route 8 | /// is requested. 9 | pub async fn track(req: Request, next: Next) -> impl IntoResponse { 10 | let start = Instant::now(); 11 | 12 | let method = req.method().clone(); 13 | let path = req.path(); 14 | 15 | let res = next.run(req).await; 16 | let latency = start.elapsed().as_secs_f64(); 17 | let status = res.status().as_u16().to_string(); 18 | 19 | let labels = [ 20 | ("method", method.to_string()), 21 | ("request_path", path), 22 | ("status", status), 23 | ]; 24 | 25 | metrics::increment_counter!("http_requests_total", &labels); 26 | metrics::histogram!("http_request_duration_seconds", latency, &labels); 27 | 28 | res 29 | } 30 | -------------------------------------------------------------------------------- /examples/gen-axum/src/middleware/mod.rs: -------------------------------------------------------------------------------- 1 | //! Additional [axum::middleware]. 2 | 3 | pub mod client; 4 | pub mod logging; 5 | pub mod metrics; 6 | pub(crate) mod request_ext; 7 | pub mod request_ulid; 8 | pub mod reqwest_retry; 9 | pub mod reqwest_tracing; 10 | pub mod runtime; 11 | -------------------------------------------------------------------------------- /examples/gen-axum/src/middleware/request_ext.rs: -------------------------------------------------------------------------------- 1 | //! Middleware for additional [axum::http::Request] methods. 2 | 3 | use axum::{ 4 | extract::{MatchedPath, OriginalUri}, 5 | http::Request, 6 | }; 7 | 8 | /// Trait for extra methods on [`Request`](axum::http::Request) 9 | pub(crate) trait RequestExt { 10 | /// Parse request path on the request. 11 | fn path(&self) -> String; 12 | } 13 | 14 | impl RequestExt for Request { 15 | fn path(&self) -> String { 16 | if let Some(matched_path) = self.extensions().get::() { 17 | matched_path.as_str().to_string() 18 | } else if let Some(uri) = self.extensions().get::() { 19 | uri.0.path().to_string() 20 | } else { 21 | self.uri().path().to_string() 22 | } 23 | } 24 | } 25 | 26 | #[cfg(test)] 27 | mod test { 28 | use super::*; 29 | use axum::http::Request; 30 | 31 | #[test] 32 | fn parse_path() { 33 | let mut req1: Request<()> = Request::default(); 34 | *req1.uri_mut() = "https://www.rust-lang.org/users/:id".parse().unwrap(); 35 | assert_eq!(req1.path(), "/users/:id"); 36 | 37 | let mut req2: Request<()> = Request::default(); 38 | *req2.uri_mut() = "https://www.rust-lang.org/api/users".parse().unwrap(); 39 | assert_eq!(req2.path(), "/api/users"); 40 | 41 | let mut req3: Request<()> = Request::default(); 42 | *req3.uri_mut() = "/api/users".parse().unwrap(); 43 | assert_eq!(req3.path(), "/api/users"); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /examples/gen-axum/src/middleware/request_ulid.rs: -------------------------------------------------------------------------------- 1 | //! Middleware for generating [ulid::Ulid]s on requests. 2 | 3 | use axum::http::Request; 4 | use tower_http::request_id::{MakeRequestId, RequestId}; 5 | use ulid::Ulid; 6 | 7 | /// Make/generate ulid on requests. 8 | #[derive(Copy, Clone, Debug)] 9 | pub struct MakeRequestUlid; 10 | 11 | /// Implement the trait for producing a request ID from the incoming request. 12 | /// In our case, we want to generate a new UUID that we can associate with a single request. 13 | impl MakeRequestId for MakeRequestUlid { 14 | fn make_request_id(&mut self, _: &Request) -> Option { 15 | let req_id = Ulid::new().to_string().parse(); 16 | match req_id { 17 | Ok(id) => Some(RequestId::new(id)), 18 | _ => None, 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /examples/gen-axum/src/middleware/reqwest_tracing.rs: -------------------------------------------------------------------------------- 1 | //! Adding trace information to [reqwest::Request]s. 2 | 3 | use reqwest::{Request, Response}; 4 | use reqwest_middleware::Result; 5 | use reqwest_tracing::{default_on_request_end, reqwest_otel_span, ReqwestOtelSpanBackend}; 6 | use std::time::Instant; 7 | use task_local_extensions::Extensions; 8 | use tracing::Span; 9 | 10 | /// Latency string. 11 | const LATENCY_FIELD: &str = "latency_ms"; 12 | 13 | /// Struct for extending [reqwest_tracing::TracingMiddleware]. 14 | #[derive(Debug)] 15 | pub struct ExtendedTrace; 16 | 17 | impl ReqwestOtelSpanBackend for ExtendedTrace { 18 | fn on_request_start(req: &Request, extension: &mut Extensions) -> Span { 19 | extension.insert(Instant::now()); 20 | reqwest_otel_span!( 21 | name = "reqwest-http-request", 22 | req, 23 | latency_ms = tracing::field::Empty 24 | ) 25 | } 26 | 27 | fn on_request_end(span: &Span, outcome: &Result, extension: &mut Extensions) { 28 | let elapsed_milliseconds = extension.get::().unwrap().elapsed().as_millis() as i64; 29 | default_on_request_end(span, outcome); 30 | span.record(LATENCY_FIELD, elapsed_milliseconds); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /examples/gen-axum/src/middleware/runtime.rs: -------------------------------------------------------------------------------- 1 | //! Middleware for runtime, [tower_http] extensions. 2 | 3 | use crate::error::AppError; 4 | 5 | use axum::response::{IntoResponse, Response}; 6 | use std::any::Any; 7 | 8 | /// Middleware function for catching runtime panics, logging 9 | /// them, and converting them into a `500 Internal Server` response. 10 | pub fn catch_panic(err: Box) -> Response { 11 | let details = if let Some(s) = err.downcast_ref::() { 12 | s.clone() 13 | } else if let Some(s) = err.downcast_ref::<&str>() { 14 | s.to_string() 15 | } else { 16 | "Unknown panic message".to_string() 17 | }; 18 | 19 | let err: AppError = anyhow::anyhow!(details).into(); 20 | err.into_response() 21 | } 22 | 23 | #[cfg(test)] 24 | mod test { 25 | use super::*; 26 | use crate::error::{parse_error, AppError}; 27 | use axum::{ 28 | body::Body, 29 | http::{Request, StatusCode}, 30 | routing::get, 31 | Router, 32 | }; 33 | use tower::{ServiceBuilder, ServiceExt}; 34 | use tower_http::catch_panic::CatchPanicLayer; 35 | 36 | #[tokio::test] 37 | async fn catch_panic_error() { 38 | let middleware = ServiceBuilder::new().layer(CatchPanicLayer::custom(catch_panic)); 39 | 40 | let app = Router::new() 41 | .route("/", get(|| async { panic!("hi") })) 42 | .layer(middleware); 43 | 44 | let res = app 45 | .oneshot(Request::builder().uri("/").body(Body::empty()).unwrap()) 46 | .await 47 | .unwrap(); 48 | 49 | let err = parse_error(res).await; 50 | 51 | assert_eq!( 52 | err, 53 | AppError::new(StatusCode::INTERNAL_SERVER_ERROR, Some("hi")) 54 | ); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /examples/gen-axum/src/router.rs: -------------------------------------------------------------------------------- 1 | //! Main [axum::Router] interface for webserver. 2 | 3 | use crate::{ 4 | middleware::logging::{log_request_response, DebugOnlyLogger, Logger}, 5 | routes::{fallback::notfound_404, health, ping}, 6 | }; 7 | use axum::{routing::get, Router}; 8 | 9 | /// Setup main router for application. 10 | pub fn setup_app_router() -> Router { 11 | let mut router = Router::new() 12 | .route("/ping", get(ping::get)) 13 | .fallback(notfound_404); 14 | 15 | router = router.layer(axum::middleware::from_fn(log_request_response::)); 16 | 17 | let mut healthcheck_router = Router::new().route("/healthcheck", get(health::healthcheck)); 18 | 19 | healthcheck_router = healthcheck_router.layer(axum::middleware::from_fn( 20 | log_request_response::, 21 | )); 22 | 23 | Router::merge(router, healthcheck_router) 24 | } 25 | -------------------------------------------------------------------------------- /examples/gen-axum/src/routes/fallback.rs: -------------------------------------------------------------------------------- 1 | //! Fallback routes. 2 | 3 | use crate::error::AppError; 4 | use axum::http::StatusCode; 5 | 6 | /// 404 fallback. 7 | pub async fn notfound_404() -> AppError { 8 | AppError::new(StatusCode::NOT_FOUND, Some("Route does not exist!")) 9 | } 10 | -------------------------------------------------------------------------------- /examples/gen-axum/src/routes/health.rs: -------------------------------------------------------------------------------- 1 | //! Healthcheck route. 2 | 3 | use crate::error::AppResult; 4 | use axum::{self, http::StatusCode}; 5 | use serde_json::json; 6 | 7 | /// GET handler for checking service health. 8 | #[utoipa::path( 9 | get, 10 | path = "/healthcheck", 11 | responses( 12 | (status = 200, description = "gen-axum healthy"), 13 | (status = 500, description = "gen-axum not healthy", body=AppError) 14 | ) 15 | )] 16 | pub async fn healthcheck() -> AppResult<(StatusCode, axum::Json)> { 17 | Ok((StatusCode::OK, axum::Json(json!({ "msg": "Healthy"})))) 18 | } 19 | -------------------------------------------------------------------------------- /examples/gen-axum/src/routes/mod.rs: -------------------------------------------------------------------------------- 1 | //! Routes for [axum::Router]. 2 | 3 | pub mod fallback; 4 | pub mod health; 5 | pub mod ping; 6 | -------------------------------------------------------------------------------- /examples/gen-axum/src/routes/ping.rs: -------------------------------------------------------------------------------- 1 | //! Generic ping route. 2 | 3 | use crate::error::AppResult; 4 | use axum::{self, http::StatusCode}; 5 | 6 | /// GET handler for internal pings and availability 7 | #[utoipa::path( 8 | get, 9 | path = "/ping", 10 | responses( 11 | (status = 200, description = "Ping successful"), 12 | (status = 500, description = "Ping not successful", body=AppError) 13 | ) 14 | )] 15 | 16 | pub async fn get() -> AppResult { 17 | Ok(StatusCode::OK) 18 | } 19 | -------------------------------------------------------------------------------- /examples/gen-axum/src/tracer.rs: -------------------------------------------------------------------------------- 1 | //! Opentelemetry tracing extensions and setup. 2 | 3 | use crate::settings::Otel; 4 | use anyhow::{anyhow, Result}; 5 | use const_format::formatcp; 6 | use http::Uri; 7 | use opentelemetry::{ 8 | global, runtime, 9 | sdk::{self, propagation::TraceContextPropagator, trace::Tracer, Resource}, 10 | }; 11 | use opentelemetry_otlp::{TonicExporterBuilder, WithExportConfig}; 12 | use opentelemetry_semantic_conventions as otel_semcov; 13 | use tonic::{metadata::MetadataMap, transport::ClientTlsConfig}; 14 | 15 | //const PKG_NAME: &str = env!("CARGO_PKG_NAME"); 16 | const PKG_NAME: &str = "application"; 17 | const VERSION: &str = formatcp!("v{}", env!("CARGO_PKG_VERSION")); 18 | const LANG: &str = "rust"; 19 | 20 | /// Initialize Opentelemetry tracing via the [OTLP protocol]. 21 | /// 22 | /// [OTLP protocol]: 23 | pub fn init_tracer(settings: &Otel) -> Result { 24 | global::set_text_map_propagator(TraceContextPropagator::new()); 25 | 26 | let resource = Resource::new(vec![ 27 | otel_semcov::resource::SERVICE_NAME.string(PKG_NAME), 28 | otel_semcov::resource::SERVICE_VERSION.string(VERSION), 29 | otel_semcov::resource::TELEMETRY_SDK_LANGUAGE.string(LANG), 30 | ]); 31 | 32 | let endpoint = &settings.exporter_otlp_endpoint; 33 | 34 | let map = MetadataMap::with_capacity(2); 35 | 36 | let trace = opentelemetry_otlp::new_pipeline() 37 | .tracing() 38 | .with_exporter(exporter(map, endpoint)?) 39 | .with_trace_config(sdk::trace::config().with_resource(resource)) 40 | .install_batch(runtime::Tokio) 41 | .map_err(|e| anyhow!("failed to intialize tracer: {:#?}", e))?; 42 | 43 | Ok(trace) 44 | } 45 | 46 | fn exporter(map: MetadataMap, endpoint: &Uri) -> Result { 47 | // Over grpc transport 48 | let exporter = opentelemetry_otlp::new_exporter() 49 | .tonic() 50 | .with_endpoint(endpoint.to_string()) 51 | .with_metadata(map); 52 | 53 | match endpoint.scheme_str() { 54 | Some("https") => { 55 | let host = endpoint 56 | .host() 57 | .ok_or_else(|| anyhow!("failed to parse host"))?; 58 | 59 | Ok(exporter.with_tls_config(ClientTlsConfig::new().domain_name(host.to_string()))) 60 | } 61 | _ => Ok(exporter), 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /examples/gen-axum/src/tracing_layers/metrics_layer.rs: -------------------------------------------------------------------------------- 1 | //! Metrics layer. 2 | 3 | use crate::tracing_layers::storage_layer::Storage; 4 | use std::{borrow::Cow, time::Instant}; 5 | use tracing::{Id, Subscriber}; 6 | use tracing_subscriber::{layer::Context, registry::LookupSpan, Layer}; 7 | 8 | const PREFIX_LABEL: &str = "metric_label_"; 9 | const METRIC_NAME: &str = "metric_name"; 10 | const OK: &str = "ok"; 11 | const ERROR: &str = "error"; 12 | const LABEL: &str = "label"; 13 | const RESULT_LABEL: &str = "result"; 14 | const SPAN_LABEL: &str = "span_name"; 15 | 16 | /// Prefix used for capturing metric spans/instrumentations. 17 | pub const METRIC_META_PREFIX: &str = "record."; 18 | 19 | /// Metrics layer for automatically deriving metrics for record.* events. 20 | /// 21 | /// Append to custom [LogFmtLayer](crate::tracing_layers::format_layer::LogFmtLayer). 22 | #[derive(Debug)] 23 | pub struct MetricsLayer; 24 | 25 | impl Layer for MetricsLayer 26 | where 27 | S: Subscriber + for<'span> LookupSpan<'span>, 28 | { 29 | fn on_close(&self, id: Id, ctx: Context<'_, S>) { 30 | let span = ctx.span(&id).expect("Span not found"); 31 | let mut extensions = span.extensions_mut(); 32 | 33 | let elapsed_secs_f64 = extensions 34 | .get_mut::() 35 | .map(|i| i.elapsed().as_secs_f64()) 36 | .unwrap_or(0.0); 37 | 38 | if let Some(visitor) = extensions.get_mut::>() { 39 | let mut labels = vec![]; 40 | for (key, value) in visitor.values() { 41 | if key.starts_with(PREFIX_LABEL) { 42 | labels.push(( 43 | key.strip_prefix(PREFIX_LABEL).unwrap_or(LABEL), 44 | value.to_string(), 45 | )) 46 | } 47 | } 48 | 49 | let span_name = span 50 | .name() 51 | .strip_prefix(METRIC_META_PREFIX) 52 | .unwrap_or_else(|| span.name()); 53 | 54 | labels.push((SPAN_LABEL, span_name.to_string())); 55 | 56 | let name = visitor 57 | .values() 58 | .get(METRIC_NAME) 59 | .unwrap_or(&Cow::from(span_name)) 60 | .to_string(); 61 | 62 | if visitor.values().contains_key(ERROR) { 63 | labels.push((RESULT_LABEL, String::from(ERROR))) 64 | } else { 65 | labels.push((RESULT_LABEL, String::from(OK))) 66 | } 67 | 68 | // Need to sort labels to remain the same across all metrics. 69 | labels.sort_unstable(); 70 | 71 | metrics::increment_counter!(format!("{name}_total"), &labels); 72 | metrics::histogram!( 73 | format!("{name}_duration_seconds"), 74 | elapsed_secs_f64, 75 | &labels 76 | ); 77 | 78 | // Remove storage as this is the last layer. 79 | extensions 80 | .remove::>() 81 | .expect("Visitor not found on 'close'"); 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /examples/gen-axum/src/tracing_layers/mod.rs: -------------------------------------------------------------------------------- 1 | //! Custom [tracing_subscriber::layer::Layer]s for formatting log events, 2 | //! deriving metrics from instrumentation calls, and for storage to augment 3 | //! layers. For more information, please read [Composing an observable Rust application]. 4 | //! 5 | //! [Composing an observable Rust application]: 6 | 7 | pub mod format_layer; 8 | pub mod metrics_layer; 9 | pub mod storage_layer; 10 | -------------------------------------------------------------------------------- /rust+wasm/.dockerignore: -------------------------------------------------------------------------------- 1 | * 2 | 3 | !**/Cargo.toml 4 | !**/Cargo.lock 5 | !**/src{% if bench %} 6 | {{project-name}}-benches{% endif %}{% if axum %} 7 | !**/config{% endif %} 8 | # for now, only include {{project-name}} binary 9 | {{project-name}}-wasm 10 | -------------------------------------------------------------------------------- /rust+wasm/.envrc: -------------------------------------------------------------------------------- 1 | use_flake 2 | -------------------------------------------------------------------------------- /rust+wasm/.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Default 2 | * @{{github-codeowner}} 3 | -------------------------------------------------------------------------------- /rust+wasm/.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: "\U0001F41B bug" 6 | assignees: '' 7 | 8 | --- 9 | 10 | # Summary 11 | 12 | ## Problem 13 | 14 | Describe the immediate problem. 15 | 16 | ### Impact 17 | 18 | What's the impact of this bug? 19 | 20 | ## Solution 21 | 22 | Describe the sort of fix that would solve the issue. 23 | 24 | # Detail 25 | 26 | **Describe the bug** 27 | 28 | A clear and concise description of what the bug is. 29 | 30 | **To Reproduce** 31 | 32 | Steps to reproduce the behavior: 33 | 1. Go to '...' 34 | 2. Click on '....' 35 | 3. Scroll down to '....' 36 | 4. See error 37 | 38 | **Expected behavior** 39 | 40 | A clear and concise description of what you expected to happen. 41 | 42 | **Screenshots** 43 | 44 | If applicable, add screenshots to help explain your problem. 45 | 46 | **Desktop (please complete the following information):** 47 | 48 | - OS: [e.g. iOS] 49 | - Browser [e.g. chrome, safari] 50 | - Version [e.g. 22] 51 | 52 | **Smartphone (please complete the following information):** 53 | 54 | - Device: [e.g. iPhone6] 55 | - OS: [e.g. iOS8.1] 56 | - Browser [e.g. stock browser, safari] 57 | - Version [e.g. 22] 58 | 59 | **Additional context** 60 | 61 | Add any other context about the problem here. 62 | -------------------------------------------------------------------------------- /rust+wasm/.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: "\U0001F497 enhancement" 6 | assignees: '' 7 | 8 | --- 9 | 10 | NB: Feature requests will only be considered if they solve a pain or present a useful refactoring of the code. 11 | 12 | # Summary 13 | 14 | ## Problem 15 | 16 | Describe the pain that this feature will solve. 17 | 18 | ### Impact 19 | 20 | Describe the impact of not having this feature. 21 | 22 | ## Solution 23 | 24 | Describe the solution. 25 | 26 | # Detail 27 | 28 | **Is your feature request related to a problem? Please describe.** 29 | 30 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 31 | 32 | **Describe the solution you'd like** 33 | 34 | A clear and concise description of what you want to happen. 35 | 36 | **Describe alternatives you've considered** 37 | 38 | A clear and concise description of any alternative solutions or features you've considered. 39 | 40 | **Additional context** 41 | 42 | Add any other context or screenshots about the feature request here. 43 | -------------------------------------------------------------------------------- /rust+wasm/.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # Description 2 | 3 | Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. 4 | 5 | ## Link to issue 6 | 7 | Please add a link to any relevant issues/tickets. 8 | 9 | ## Type of change 10 | 11 | - [ ] Bug fix (non-breaking change that fixes an issue) 12 | - [ ] New feature (non-breaking change that adds functionality) 13 | - [ ] Refactor (non-breaking change that updates existing functionality) 14 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 15 | - [ ] This change requires a documentation update 16 | - [ ] Comments have been added/updated 17 | 18 | Please delete options that are not relevant. 19 | 20 | ## Test plan (required) 21 | 22 | Demonstrate the code is solid. Which commands did you test with and what are the expected results? 23 | Which tests have you added or updated? Do the tests cover all of the changes included in this PR? 24 | 25 | ## Screenshots/Screencaps 26 | 27 | Please add previews of any UI Changes. 28 | -------------------------------------------------------------------------------- /rust+wasm/.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | 8 | updates: 9 | - package-ecosystem: "cargo" 10 | directory: "/" 11 | commit-message: 12 | prefix: "chore(cargo)" 13 | include: "scope" 14 | target-branch: "main" 15 | schedule: 16 | interval: "weekly" 17 | 18 | - package-ecosystem: "npm" 19 | directory: "/{{project-name}}-wasm" 20 | commit-message: 21 | prefix: "chore(npm)" 22 | include: "scope" 23 | target-branch: "main" 24 | schedule: 25 | interval: "weekly" 26 | 27 | - package-ecosystem: "github-actions" 28 | directory: "/" 29 | commit-message: 30 | prefix: "chore(ci)" 31 | include: "scope" 32 | target-branch: "main" 33 | schedule: 34 | interval: "weekly" 35 | -------------------------------------------------------------------------------- /rust+wasm/.github/workflows/audit.yml: -------------------------------------------------------------------------------- 1 | name: 🛡 Audit-Check 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * *' 6 | 7 | jobs: 8 | security-audit: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Checkout Repository 13 | uses: actions/checkout@v4 14 | - name: Run Audit-Check 15 | uses: rustsec/audit-check@v1.3.2 16 | with: 17 | token: {{ "${{ secrets.GITHUB_TOKEN " }}}} 18 | -------------------------------------------------------------------------------- /rust+wasm/.github/workflows/bench.yml: -------------------------------------------------------------------------------- 1 | name: 📈 Benchmark 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | 7 | pull_request: 8 | branches: [ '**' ] 9 | 10 | concurrency: 11 | group: {{ "${{ github.workflow " }}}}-{{ "${{ github.ref " }}}} 12 | cancel-in-progress: true 13 | 14 | jobs: 15 | benchmark: 16 | runs-on: ubuntu-latest 17 | 18 | steps:{% if axum %} 19 | - name: Install Protoc 20 | uses: arduino/setup-protoc@v1 21 | with: 22 | repo-token: {{ "${{ secrets.GITHUB_TOKEN " }}}}{% endif %} 23 | - name: Checkout Repository 24 | uses: actions/checkout@v4 25 | 26 | - name: Install Rust Toolchain 27 | uses: actions-rs/toolchain@v1 28 | with: 29 | override: true 30 | toolchain: stable 31 | 32 | - name: Cache Project 33 | uses: Swatinem/rust-cache@v2 34 | 35 | - name: Run Benchmark 36 | run: cargo bench -p {{project-name}}-benches -- --output-format bencher | tee output.txt 37 | 38 | - name: Upload Benchmark Result Artifact 39 | uses: actions/upload-artifact@v3 40 | with: 41 | name: bench_result 42 | path: output.txt 43 | 44 | - name: Create gh-pages Branch 45 | uses: peterjgrainger/action-create-branch@v2.4.0 46 | env: 47 | GITHUB_TOKEN: {{ "${{ secrets.GITHUB_TOKEN " }}}} 48 | with: 49 | branch: gh-pages 50 | 51 | - name: Store Benchmark Result 52 | uses: benchmark-action/github-action-benchmark@v1 53 | with: 54 | name: Rust Benchmark 55 | tool: 'cargo' 56 | output-file-path: output.txt 57 | github-token: {{ "${{ secrets.GITHUB_TOKEN " }}}} 58 | auto-push: {{ "${{ github.event_name == 'push' " }}&& github.repository == '{{github-name}}/{{repo-name}}' && github.ref == 'refs/heads/main' }} 59 | alert-threshold: '200%' 60 | comment-on-alert: true 61 | fail-on-alert: true 62 | alert-comment-cc-users: '@{{github-codeowner}}' 63 | -------------------------------------------------------------------------------- /rust+wasm/.github/workflows/coverage.yml: -------------------------------------------------------------------------------- 1 | name: ☂ Code Coverage 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | 7 | pull_request: 8 | branches: [ '**' ] 9 | 10 | concurrency: 11 | group: {{ "${{ github.workflow " }}}}-{{ "${{ github.ref " }}}} 12 | cancel-in-progress: true 13 | 14 | jobs: 15 | coverage: 16 | runs-on: ubuntu-latest 17 | 18 | steps:{% if axum %} 19 | - name: Install Protoc 20 | uses: arduino/setup-protoc@v1 21 | with: 22 | repo-token: {{ "${{ secrets.GITHUB_TOKEN " }}}}{% endif %} 23 | - name: Checkout Repository 24 | uses: actions/checkout@v4 25 | 26 | - name: Install Rust Toolchain 27 | uses: actions-rs/toolchain@v1 28 | with: 29 | override: true 30 | toolchain: nightly 31 | components: llvm-tools-preview 32 | profile: minimal 33 | 34 | - name: Cache Project 35 | uses: Swatinem/rust-cache@v2 36 | 37 | - name: Generate Code coverage 38 | env: 39 | CARGO_INCREMENTAL: '0' 40 | LLVM_PROFILE_FILE: "{{project-name}}-%p-%m.profraw" 41 | RUSTFLAGS: '-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off' 42 | RUSTDOCFLAGS: '-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off' 43 | ## currently just runs coverage on rust project 44 | run: cargo test -p {{project-name}} --all-features 45 | 46 | - name: Install grcov 47 | run: "curl -L https://github.com/mozilla/grcov/releases/download/v0.8.12/grcov-x86_64-unknown-linux-gnu.tar.bz2 | tar jxf -" 48 | 49 | - name: Run grcov 50 | run: "./grcov . --llvm --binary-path target/debug/ -s . -t lcov --branch --ignore-not-existing --ignore '/*' -o lcov.info" 51 | 52 | - name: Install covfix 53 | uses: actions-rs/install@v0.1 54 | with: 55 | crate: rust-covfix 56 | use-tool-cache: true 57 | 58 | - name: Run covfix 59 | run: rust-covfix lcov.info -o lcov.info --verbose 60 | 61 | - name: Upload to codecov.io 62 | uses: codecov/codecov-action@v3 63 | continue-on-error: true 64 | with: 65 | token: {{ "${{ secrets.CODECOV_TOKEN " }}}} 66 | fail_ci_if_error: false 67 | files: lcov.info 68 | -------------------------------------------------------------------------------- /rust+wasm/.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: 🐳 Docker 2 | 3 | on: 4 | pull_request: 5 | branches: [ '**' ] 6 | 7 | concurrency: 8 | group: {{ "${{ github.workflow " }}}}-{{ "${{ github.ref " }}}} 9 | cancel-in-progress: true 10 | 11 | jobs: 12 | build-docker: 13 | runs-on: ubuntu-latest 14 | if: {{ "${{ github.event_name == 'pull_request' " }}}} 15 | 16 | env: 17 | DOCKER_BUILDKIT: 1 18 | 19 | steps: 20 | - name: Checkout Repository 21 | uses: actions/checkout@v4 22 | 23 | # https://github.com/docker/setup-qemu-action 24 | - name: Setup QEMU 25 | uses: docker/setup-qemu-action@v2 26 | 27 | # https://github.com/docker/setup-buildx-action 28 | - name: Setup Buildx 29 | uses: docker/setup-buildx-action@v2 30 | with: 31 | buildkitd-flags: "--debug" 32 | 33 | - name: Login to GitHub Container Registry 34 | uses: docker/login-action@v2 35 | with: 36 | registry: ghcr.io 37 | username: {{ "${{ github.repository_owner " }}}} 38 | password: {{ "${{ secrets.GITHUB_TOKEN " }}}} 39 | 40 | - name: Docker Build 41 | uses: docker/build-push-action@v4 42 | with:{% if dockerbuild == "glibc" %} 43 | build-args: | 44 | RUST_BUILD_IMG=rust:1.65-slim-bullseye 45 | DEBIAN_TAG=bullseye-slim{% else %} 46 | build-args: | 47 | RUST_BUILD_IMG=rust:1.65-slim-bullseye{% endif %} 48 | cache-from: type=registry,ref=ghcr.io/{{ "${{ github.repository_owner " }}}}/{{project-name}}:latest 49 | cache-to: type=registry,ref=ghcr.io/{{ "${{ github.repository_owner " }}}}/{{project-name}}:latest,mode=max 50 | file: docker/Dockerfile 51 | context: .{% if dockerbuild == "glibc" %} 52 | # We don't add `linux/arm64` here, as it can cause GitHub runners to 53 | # stall for too long. 54 | platforms: linux/amd64{% else %} 55 | platforms: linux/amd64, linux/arm64{% endif %} 56 | push: false 57 | tags: | 58 | {{ "${{ github.repository_owner " }}}}/{{project-name}}:latest 59 | -------------------------------------------------------------------------------- /rust+wasm/.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | **/target/ 4 | {% if crate_type == "lib" %}Cargo.lock{% endif %} 5 | # These are backup files generated by rustfmt 6 | **/*.rs.bk 7 | {% if nix %} 8 | # Ignore local environment settings 9 | .envrc.custom 10 | .direnv 11 | {% endif %} 12 | **/package-lock.json 13 | **/pkg/ 14 | wasm-pack.log 15 | **/node_modules/ 16 | 17 | # Other files + dirs 18 | private 19 | *.temp 20 | *.tmp 21 | **/.history 22 | **/.DS_Store 23 | -------------------------------------------------------------------------------- /rust+wasm/.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # pre-commit install 3 | # pre-commit install --hook-type commit-msg 4 | exclude: ^(LICENSE|LICENSE*) 5 | repos: 6 | - repo: local 7 | hooks: 8 | - id: fmt 9 | name: fmt 10 | description: Format rust files. 11 | entry: {% if nix %}cargo fmt{% else %}cargo +nightly fmt{% endif %} 12 | language: system 13 | types: [rust] 14 | args: ["--all", "--", "--check"] 15 | - id: cargo-check 16 | name: cargo check 17 | description: Check the package for errors. 18 | entry: cargo check 19 | language: system 20 | types: [rust] 21 | pass_filenames: false 22 | - id: clippy 23 | name: clippy 24 | description: Lint via clippy 25 | entry: cargo clippy 26 | language: system 27 | args: ["--workspace", "--", "-D", "warnings"] 28 | types: [rust] 29 | pass_filenames: false{% if nix %} 30 | - id: alejandra 31 | name: alejandra 32 | description: Format nix files. 33 | entry: alejandra 34 | files: \.nix$ 35 | language: system{% endif %} 36 | 37 | - repo: https://github.com/DevinR528/cargo-sort 38 | rev: v1.0.9 39 | hooks: 40 | - id: cargo-sort 41 | args: ["-w"] 42 | 43 | - repo: https://github.com/compilerla/conventional-pre-commit 44 | rev: v2.1.1 45 | hooks: 46 | - id: conventional-pre-commit 47 | stages: 48 | - commit-msg 49 | 50 | - repo: https://github.com/pre-commit/pre-commit-hooks 51 | rev: v4.3.0 52 | hooks: 53 | - id: no-commit-to-branch 54 | args: ["-b", "main"] 55 | - id: check-merge-conflict 56 | - id: trailing-whitespace 57 | - id: end-of-file-fixer 58 | - id: check-yaml 59 | - id: check-json 60 | - id: check-added-large-files 61 | - id: detect-private-key 62 | - id: check-executables-have-shebangs 63 | - id: check-toml 64 | -------------------------------------------------------------------------------- /rust+wasm/.release-please-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "{{project-name}}": "0.1.0", 3 | "{{project-name}}-wasm": "0.1.0" 4 | } 5 | -------------------------------------------------------------------------------- /rust+wasm/.rustfmt.toml: -------------------------------------------------------------------------------- 1 | edition = "2021" 2 | imports_granularity = "Crate" 3 | -------------------------------------------------------------------------------- /rust+wasm/CHANGELOG.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fission-codes/rust-template/64a9bfd65ed4e8fe39249df0784d741cf4838276/rust+wasm/CHANGELOG.md -------------------------------------------------------------------------------- /rust+wasm/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | -------------------------------------------------------------------------------- /rust+wasm/Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "examples", 4 | "{{project-name}}",{%if bench %} 5 | "{{project-name}}-benches",{% endif %} 6 | "{{project-name}}-wasm" 7 | ] 8 | resolver = "2" 9 | 10 | # See https://doc.rust-lang.org/cargo/reference/profiles.html for more info. 11 | [profile.release.package.{{project-name}}-wasm] 12 | # Perform optimizations on all codegen units. 13 | codegen-units = 1 14 | # Tell `rustc` to optimize for small code size. 15 | opt-level = "z" # or 'z' to optimize "aggressively" for size 16 | # Strip debug symbols 17 | # "symbols" issue: https://github.com/rust-lang/rust/issues/93294 18 | ## strip = "debuginfo" 19 | # Amount of debug information. 20 | # 0/false: no debug info at all; 1: line tables only; 2/true: full debug info 21 | ## debug = false 22 | 23 | # Speedup build on macOS 24 | # See https://blog.rust-lang.org/2021/03/25/Rust-1.51.0.html#splitting-debug-information 25 | [profile.dev] 26 | split-debuginfo = "unpacked" 27 | -------------------------------------------------------------------------------- /rust+wasm/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any 2 | person obtaining a copy of this software and associated 3 | documentation files (the "Software"), to deal in the 4 | Software without restriction, including without 5 | limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following 9 | conditions: 10 | 11 | The above copyright notice and this permission notice 12 | shall be included in all copies or substantial portions 13 | of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /rust+wasm/SECURITY.md: -------------------------------------------------------------------------------- 1 | ## Report a security issue or vulnerability 2 | 3 | The {{project-name}} team welcomes security reports and is committed to 4 | providing prompt attention to security issues. Security issues should be 5 | reported privately via [{{github-email}}][support-email]. Security issues should 6 | not be reported via the public GitHub Issue tracker. 7 | 8 | ## Security advisories 9 | 10 | The project team is committed to transparency in the security issue disclosure 11 | process. The {{project-name}} team announces security advisories through our 12 | Github respository's [security portal][sec-advisories] and and the 13 | [RustSec advisory database][rustsec-db]. 14 | 15 | [rustsec-db]: https://github.com/RustSec/advisory-db 16 | [sec-advisories]: https://github.com/{{github-name}}/{{repo-name}}/security/advisories 17 | [support-email]: mailto:{{github-email}} 18 | -------------------------------------------------------------------------------- /rust+wasm/assets/a_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fission-codes/rust-template/64a9bfd65ed4e8fe39249df0784d741cf4838276/rust+wasm/assets/a_logo.png -------------------------------------------------------------------------------- /rust+wasm/codecov.yml: -------------------------------------------------------------------------------- 1 | ignore: 2 | - "{{project-name}}/tests" 3 | - "{{project-name}}-wasm"{% if bench %} 4 | - "{{project-name}}-benches"{% endif %} 5 | - "examples" 6 | 7 | comment: 8 | layout: "reach, diff, flags, files" 9 | require_changes: true 10 | 11 | github_checks: 12 | annotations: false 13 | 14 | coverage: 15 | status: 16 | project: 17 | default: 18 | threshold: 5% 19 | -------------------------------------------------------------------------------- /rust+wasm/docker/Dockerfile.glibc: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | ARG RUST_BUILD_IMG=rust:1.67-slim-bullseye 3 | ARG DEBIAN_TAG=bullseye-slim 4 | 5 | FROM $RUST_BUILD_IMG as base 6 | 7 | # AMD64 8 | FROM --platform=$BUILDPLATFORM base as builder-amd64 9 | ARG TARGET="x86_64-unknown-linux-gnu" 10 | 11 | # ARM64 12 | FROM --platform=$BUILDPLATFORM base as builder-arm64 13 | ARG TARGET="aarch64-unknown-linux-gnu" 14 | 15 | FROM builder-$TARGETARCH as builder 16 | 17 | RUN adduser --disabled-password --disabled-login --gecos "" --no-create-home {{project-name}} 18 | RUN apt update && apt install -y g++{% if axum %} build-essential protobuf-compiler{% endif %} 19 | RUN rustup target add $TARGET 20 | 21 | RUN cargo init {{project-name}} 22 | 23 | WORKDIR /{{project-name}} 24 | 25 | # touch lib.rs as we combine both 26 | Run touch src/lib.rs 27 | 28 | # copy cargo.* 29 | COPY ../Cargo.lock ./Cargo.lock 30 | COPY ../{{project-name}}/Cargo.toml ./Cargo.toml 31 | 32 | # cache depencies 33 | RUN mkdir .cargo 34 | RUN cargo vendor > .cargo/config 35 | RUN --mount=type=cache,target=$CARGO_HOME/registry \ 36 | --mount=type=cache,target=$CARGO_HOME/.git \ 37 | --mount=type=cache,target={{project-name}}/target,sharing=locked \{% if axum %} 38 | cargo build --target $TARGET --bin {{project-name}}-app --release{% else %} 39 | cargo build --target $TARGET --release{% endif %} 40 | 41 | # copy src 42 | COPY ../{{project-name}}/src ./src{% if axum %} 43 | # copy config 44 | COPY ../{{project-name}}/config ./config 45 | {% endif %} 46 | # final build for release 47 | RUN rm ./target/$TARGET/release/deps/*{{crate_name}}* 48 | RUN {% if axum %} cargo build --target $TARGET --bin {{project-name}}-app --release{% else %} cargo build --target $TARGET --bin {{project-name}} --release{% endif %} 49 | {% if axum %} 50 | RUN strip ./target/$TARGET/release/{{project-name}}-app 51 | {% else %} 52 | RUN strip ./target/$TARGET/release/{{project-name}}{% endif %} 53 | RUN mv ./target/$TARGET/release/{{project-name}}* /usr/local/bin{% if axum %} 54 | RUN mv ./config /etc/config{% endif %} 55 | 56 | FROM debian:${DEBIAN_TAG} 57 | 58 | ARG backtrace=0 59 | ARG log_level=info 60 | 61 | ENV RUST_BACKTRACE=${backtrace} \ 62 | RUST_LOG=${log_level} 63 | 64 | COPY --from=builder /usr/local/bin/{{project-name}}* .{% if axum %} 65 | COPY --from=builder /etc/config ./config{% endif %} 66 | COPY --from=builder /etc/passwd /etc/passwd 67 | COPY --from=builder /etc/group /etc/group 68 | 69 | USER {{project-name}}:{{project-name}} 70 | {% if axum %} 71 | EXPOSE {{port}} 72 | EXPOSE {{metricsport}} 73 | ENTRYPOINT ["./{{project-name}}-app"]{% else %}ENTRYPOINT ["./{{project-name}}"]{% endif %} 74 | -------------------------------------------------------------------------------- /rust+wasm/docker/Dockerfile.musl: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | # AMD64 4 | FROM --platform=$BUILDPLATFORM messense/rust-musl-cross:x86_64-musl as builder-amd64 5 | 6 | # ARM64 7 | FROM --platform=$BUILDPLATFORM messense/rust-musl-cross:aarch64-musl as builder-arm64 8 | 9 | ARG TARGETARCH 10 | FROM builder-$TARGETARCH as builder 11 | {% if axum %} 12 | RUN apt update && apt install -y protobuf-compiler 13 | {% endif %} 14 | RUN adduser --disabled-password --disabled-login --gecos "" --no-create-home {{project-name}} 15 | 16 | RUN cargo init 17 | 18 | # touch lib.rs as we combine both 19 | RUN touch src/lib.rs 20 | 21 | # copy cargo.* 22 | COPY Cargo.lock ./Cargo.lock 23 | COPY ../{{project-name}}/Cargo.toml ./Cargo.toml 24 | 25 | # cache depencies 26 | RUN mkdir .cargo 27 | RUN cargo vendor > .cargo/config 28 | RUN --mount=type=cache,target=$CARGO_HOME/registry \ 29 | --mount=type=cache,target=$CARGO_HOME/.git \ 30 | --mount=type=cache,target={{project-name}}/target,sharing=locked \{% if axum %} 31 | cargo build --target $CARGO_BUILD_TARGET --bin {{project-name}}-app --release{% else %} 32 | cargo build --target $CARGO_BUILD_TARGET --release{% endif %} 33 | 34 | # copy src 35 | COPY ../{{project-name}}/src ./src{% if axum %} 36 | # copy config 37 | COPY ../{{project-name}}/config ./config 38 | {% endif %} 39 | # final build for release 40 | RUN rm ./target/$CARGO_BUILD_TARGET/release/deps/*{{crate_name}}* 41 | RUN {% if axum %} cargo build --target $CARGO_BUILD_TARGET --bin {{project-name}}-app --release{% else %} cargo build --target $CARGO_BUILD_TARGET --bin {{project-name}} --release{% endif %} 42 | {% if axum %} 43 | RUN musl-strip ./target/$CARGO_BUILD_TARGET/release/{{project-name}}-app 44 | {% else %} 45 | RUN musl-strip ./target/$CARGO_BUILD_TARGET/release/{{project-name}}{% endif %} 46 | RUN mv ./target/$CARGO_BUILD_TARGET/release/{{project-name}}* /usr/local/bin{% if axum %} 47 | RUN mv ./config /etc/config{% endif %} 48 | 49 | FROM scratch 50 | 51 | ARG backtrace=0 52 | ARG log_level=info 53 | 54 | ENV RUST_BACKTRACE=${backtrace} \ 55 | RUST_LOG=${log_level} 56 | 57 | COPY --from=builder /usr/local/bin/{{project-name}}* .{% if axum %} 58 | COPY --from=builder /etc/config ./config{% endif %} 59 | COPY --from=builder /etc/passwd /etc/passwd 60 | COPY --from=builder /etc/group /etc/group 61 | 62 | USER {{project-name}}:{{project-name}} 63 | {% if axum %} 64 | EXPOSE {{port}} 65 | EXPOSE {{metricsport}} 66 | ENTRYPOINT ["./{{project-name}}-app"]{% else %}ENTRYPOINT ["./{{project-name}}"]{% endif %} 67 | -------------------------------------------------------------------------------- /rust+wasm/examples/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "examples" 3 | version = "0.1.0" 4 | publish = false 5 | edition = "2021" 6 | authors = ["{{authors}}"] 7 | 8 | [dev-dependencies] 9 | {{project-name}} = { path = "../{{project-name}}", version = "0.1" } 10 | 11 | [[example]] 12 | name = "counterparts" 13 | path = "counterparts.rs" 14 | -------------------------------------------------------------------------------- /rust+wasm/examples/counterparts.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | 3 | pub fn main() -> Result<(), Box> { 4 | println!("Alien Shore!"); 5 | Ok(()) 6 | } 7 | -------------------------------------------------------------------------------- /rust+wasm/final-msg.rhai: -------------------------------------------------------------------------------- 1 | print(); 2 | print(); 3 | print("\ 4 | For CI/CD purposes, be aware there's some secrets you'll need to configure in Github, including:\n\ 5 | `CODECOV_TOKEN` if you choose to use coverage via Codecov and have it set up;\n\ 6 | `CARGO_REGISTRY_TOKEN` for publshing Rust packages to crates.io;\n\ 7 | `NPM_TOKEN` for publishing a Wasm project to npm;\n\ 8 | `DOCKERHUB_USERNAME` and `DOCKERHUB_TOKEN` for pushing containers to Docker Hub."); 9 | print(); 10 | -------------------------------------------------------------------------------- /rust+wasm/final-script.rhai: -------------------------------------------------------------------------------- 1 | if variable::is_set("axum") && variable::get("axum") { 2 | let project = variable::get("project-name"); 3 | 4 | let cargo_from = project + "/Cargo.axum.toml"; 5 | let cargo_to = project + "/Cargo.toml"; 6 | 7 | let src_from = project + "/src.axum"; 8 | let src_to = project + "/src"; 9 | 10 | let test_from = project + "/tests/integration_test.axum.rs"; 11 | let test_to = project + "/tests/integration_test.rs"; 12 | 13 | file::rename(src_from, src_to); 14 | file::rename(test_from, test_to); 15 | file::rename(cargo_from, cargo_to); 16 | file::rename("README.axum.md", "README.md"); 17 | } 18 | -------------------------------------------------------------------------------- /rust+wasm/flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "{{project-name}}"; 3 | 4 | inputs = { 5 | nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; 6 | flake-utils.url = "github:numtide/flake-utils"; 7 | 8 | flake-compat = { 9 | url = "github:edolstra/flake-compat"; 10 | flake = false; 11 | }; 12 | 13 | rust-overlay = { 14 | url = "github:oxalica/rust-overlay"; 15 | inputs.nixpkgs.follows = "nixpkgs"; 16 | inputs.flake-utils.follows = "flake-utils"; 17 | }; 18 | }; 19 | 20 | outputs = { 21 | self, 22 | nixpkgs, 23 | flake-compat, 24 | flake-utils, 25 | rust-overlay, 26 | } @ inputs: 27 | flake-utils.lib.eachDefaultSystem ( 28 | system: let 29 | overlays = [(import rust-overlay)]; 30 | pkgs = import nixpkgs {inherit system overlays;}; 31 | 32 | rust-toolchain = (pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml).override { 33 | extensions = ["cargo" "clippy" "rustfmt" "rust-src" "rust-std"]; 34 | targets = ["wasm32-unknown-unknown"]; 35 | }; 36 | 37 | nightly-rustfmt = pkgs.rust-bin.nightly.latest.rustfmt; 38 | 39 | format-pkgs = with pkgs; [ 40 | nixpkgs-fmt 41 | alejandra 42 | ]; 43 | 44 | cargo-installs = with pkgs; [ 45 | binaryen{% if auditable %} 46 | cargo-audit 47 | cargo-auditable{% endif %} 48 | cargo-deny 49 | cargo-expand 50 | cargo-nextest 51 | cargo-outdated 52 | cargo-sort 53 | cargo-spellcheck 54 | cargo-udeps 55 | cargo-watch 56 | twiggy 57 | wasm-pack 58 | wasm-bindgen-cli 59 | wasm-tools 60 | ]; 61 | in rec 62 | { 63 | devShells.default = pkgs.mkShell { 64 | name = "{{project-name}}"; 65 | nativeBuildInputs = with pkgs; 66 | [ 67 | # The ordering of these two items is important. For nightly rustfmt to be used instead of 68 | # the rustfmt provided by `rust-toolchain`, it must appear first in the list. This is 69 | # because native build inputs are added to $PATH in the order they're listed here. 70 | nightly-rustfmt 71 | binaryen 72 | rust-toolchain 73 | pre-commit 74 | protobuf 75 | direnv 76 | geckodriver 77 | self.packages.${system}.irust 78 | ] 79 | ++ format-pkgs 80 | ++ cargo-installs 81 | ++ lib.optionals stdenv.isDarwin [ 82 | darwin.apple_sdk.frameworks.Security 83 | darwin.apple_sdk.frameworks.CoreFoundation 84 | darwin.apple_sdk.frameworks.Foundation 85 | ]; 86 | 87 | shellHook = '' 88 | [ -e .git/hooks/pre-commit ] || pre-commit install --install-hooks && pre-commit install --hook-type commit-msg 89 | ''; 90 | }; 91 | 92 | packages.irust = pkgs.rustPlatform.buildRustPackage rec { 93 | pname = "irust"; 94 | version = "1.70.0"; 95 | src = pkgs.fetchFromGitHub { 96 | owner = "sigmaSd"; 97 | repo = "IRust"; 98 | rev = "v${version}"; 99 | sha256 = "sha256-chZKesbmvGHXwhnJRZbXyX7B8OwJL9dJh0O1Axz/n2E="; 100 | }; 101 | 102 | doCheck = false; 103 | cargoSha256 = "sha256-FmsD3ajMqpPrTkXCX2anC+cmm0a2xuP+3FHqzj56Ma4="; 104 | }; 105 | 106 | formatter = pkgs.alejandra; 107 | } 108 | ); 109 | } 110 | -------------------------------------------------------------------------------- /rust+wasm/init-msg.rhai: -------------------------------------------------------------------------------- 1 | print(); 2 | if variable::get("is_init") { 3 | print("\ 4 | If the generator detects a directory or file that already exists, it will not alter your project,\n\ 5 | so read the prompts carefully."); 6 | print(); 7 | } 8 | 9 | print("Note: this template is for working with a cargo workspace of packages."); 10 | print(); 11 | -------------------------------------------------------------------------------- /rust+wasm/pre-script.rhai: -------------------------------------------------------------------------------- 1 | let license = variable::get("license"); 2 | 3 | while switch license.to_upper() { 4 | "APACHE" => { 5 | file::delete("LICENSE-MIT"); 6 | file::delete("{{project-name}}/LICENSE-MIT"); 7 | file::delete("{{project-name}}-wasm/LICENSE-MIT"); 8 | file::rename("LICENSE-APACHE", "LICENSE"); 9 | file::rename("{{project-name}}/LICENSE-APACHE", "{{project-name}}/LICENSE"); 10 | file::rename("{{project-name}}-wasm/LICENSE-APACHE", "{{project-name}}-wasm/LICENSE"); 11 | false 12 | } 13 | "MIT" => { 14 | file::delete("LICENSE-APACHE"); 15 | file::delete("{{project-name}}/LICENSE-APACHE"); 16 | file::delete("{{project-name}}-wasm/LICENSE-APACHE"); 17 | file::rename("LICENSE-MIT", "LICENSE"); 18 | file::rename("{{project-name}}/LICENSE-MIT", "{{project-name}}/LICENSE"); 19 | file::rename("{{project-name}}-wasm/LICENSE-MIT", "{{project-name}}-wasm/LICENSE"); 20 | false 21 | } 22 | "DUAL" => false, 23 | "USE EXISTING" => { 24 | file::delete("LICENSE-MIT"); 25 | file::delete("LICENSE-APACHE"); 26 | file::delete("{{project-name}}/LICENSE-MIT"); 27 | file::delete("{{project-name}}-wasm/LICENSE-MIT"); 28 | file::delete("{{project-name}}/LICENSE-APACHE"); 29 | file::delete("{{project-name}}-wasm/LICENSE-APACHE"); 30 | false 31 | }, 32 | _ => true, 33 | } { 34 | license = variable::prompt("What license to use?", "dual", [ 35 | "Apache", 36 | "MIT", 37 | "dual", 38 | "use existing" 39 | ]); 40 | } 41 | variable::set("license", license); 42 | 43 | if variable::is_set("docker") && variable::get("docker") { 44 | let dockerbuild = variable::get("dockerbuild"); 45 | while switch dockerbuild.to_upper() { 46 | "GLIBC" => { 47 | file::delete("docker/Dockerfile.musl"); 48 | file::rename("docker/Dockerfile.glibc", "docker/Dockerfile"); 49 | false 50 | } 51 | "MUSL" => { 52 | file::delete("docker/Dockerfile.glibc"); 53 | file::rename("docker/Dockerfile.musl", "docker/Dockerfile"); 54 | false 55 | } 56 | _ => true, 57 | } { 58 | dockerbuild = variable::prompt("For docker, do you want a glibc or musl build?", "musl", [ 59 | "musl", 60 | "glibc" 61 | ]); 62 | } 63 | variable::set("dockerbuild", dockerbuild); 64 | } 65 | 66 | // 67 | let project = variable::get("project-name"); 68 | variable::set("project", project); 69 | 70 | // Set for Copyright Info (Apache) 71 | variable::set("year", "2023"); 72 | let name = variable::get("github-name").to_title_case(); 73 | variable::set("copyright-owner", name); 74 | -------------------------------------------------------------------------------- /rust+wasm/release-please-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["cargo-workspace"], 3 | "changelog-path": "CHANGELOG.md", 4 | "release-type": "rust", 5 | "bump-minor-pre-major": true, 6 | "bump-patch-for-minor-pre-major": true, 7 | "packages": { 8 | "{{project-name}}": {}, 9 | "{{project-name}}-wasm": {} 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /rust+wasm/rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "{{toolchain}}"{% if nix == false %} 3 | components = ["cargo", "clippy", "rustfmt", "rust-src", "rust-std"]{% endif %} 4 | -------------------------------------------------------------------------------- /rust+wasm/{{project-name}}-benches/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "{{project-name}}-benches" 3 | version = "0.1.0" 4 | publish = false 5 | edition = "2021" 6 | authors = ["{{authors}}"] 7 | 8 | [dependencies] 9 | {{project-name}} = { path = "../{{project-name}}", version = "0.1", features = ["test_utils"] } 10 | 11 | [dev-dependencies] 12 | criterion = { version = "0.4", default-features = false } 13 | 14 | [[bench]] 15 | name = "a_benchmark" 16 | harness = false 17 | -------------------------------------------------------------------------------- /rust+wasm/{{project-name}}-benches/benches/a_benchmark.rs: -------------------------------------------------------------------------------- 1 | use criterion::{criterion_group, criterion_main, Criterion}; 2 | 3 | pub fn add_benchmark(c: &mut Criterion) { 4 | let mut rvg = {{crate_name}}::test_utils::Rvg::deterministic(); 5 | let int_val_1 = rvg.sample(&(0..100i32)); 6 | let int_val_2 = rvg.sample(&(0..100i32)); 7 | 8 | c.bench_function("add", |b| { 9 | b.iter(|| { 10 | {{crate_name}}::add(int_val_1, int_val_2); 11 | }) 12 | }); 13 | } 14 | criterion_group!(benches, add_benchmark); 15 | criterion_main!(benches); 16 | -------------------------------------------------------------------------------- /rust+wasm/{{project-name}}-wasm/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "{{project-name}}-wasm" 3 | version = "0.1.0" 4 | description = "{{description}}" 5 | keywords = [] 6 | categories = []{% if license == "Apache" %} 7 | include = ["/src", "README.md", "LICENSE"] 8 | license = "Apache-2.0" 9 | {% elsif license == "MIT" %} 10 | include = ["/src", "README.md", "LICENSE"] 11 | license = "MIT" 12 | {% else %} 13 | include = ["/src", "README.md", "LICENSE-APACHE", "LICENSE-MIT"] 14 | license = "Apache-2.0 or MIT" 15 | {% endif %}readme = "README.md" 16 | edition = "2021" 17 | rust-version = "1.67" 18 | documentation = "https://docs.rs/{{project-name}}-wasm" 19 | repository = "https://github.com/{{github-name}}/{{repo-name}}/tree/main/{{project-name}}-wasm" 20 | authors = ["{{authors}}"] 21 | 22 | [lib] 23 | crate-type = ["cdylib", "rlib"] 24 | path = "src/lib.rs" 25 | 26 | [dependencies] 27 | # The `console_error_panic_hook` crate provides better debugging of panics by 28 | # logging them with `console.error`. This is great for development, but requires 29 | # all the `std::fmt` and `std::panicking` infrastructure, so isn't great for 30 | # code size when deploying. 31 | console_error_panic_hook = { version = "0.1", optional = true } 32 | js-sys = { version = "0.3", optional = true }{% if crate_type == "lib" %} 33 | tracing = "0.1"{% else %} 34 | tracing = "0.1"{% endif %} 35 | wasm-bindgen = { version = "0.2", optional = true, features = ["serde-serialize"] } 36 | wasm-bindgen-futures = { version = "0.4", optional = true } 37 | web-sys = { version = "0.3", optional = true } 38 | {% if crate_type == "lib" %}{{project-name}} = { path = "../{{project-name}}", version = "0.1" } 39 | {% endif %} 40 | [dev-dependencies] 41 | wasm-bindgen-test = "0.3" 42 | 43 | [features] 44 | default = ["js"] 45 | full = ["js", "web"] 46 | js = [ 47 | "console_error_panic_hook", 48 | "js-sys", 49 | "wasm-bindgen", 50 | "wasm-bindgen-futures" 51 | ] 52 | web = ["web-sys"] 53 | 54 | [package.metadata.docs.rs] 55 | all-features = true 56 | # defines the configuration attribute `docsrs` 57 | rustdoc-args = ["--cfg", "docsrs"] 58 | -------------------------------------------------------------------------------- /rust+wasm/{{project-name}}-wasm/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any 2 | person obtaining a copy of this software and associated 3 | documentation files (the "Software"), to deal in the 4 | Software without restriction, including without 5 | limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following 9 | conditions: 10 | 11 | The above copyright notice and this permission notice 12 | shall be included in all copies or substantial portions 13 | of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /rust+wasm/{{project-name}}-wasm/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(docsrs, feature(doc_cfg))] 2 | #![warn(missing_debug_implementations, missing_docs, rust_2018_idioms)] 3 | #![deny(unreachable_pub)] 4 | #![deny(private_bounds)] 5 | #![deny(rustdoc::private_intra_doc_links)] 6 | #![deny(private_interfaces)] 7 | 8 | //! {{project-name}} 9 | 10 | use wasm_bindgen::prelude::wasm_bindgen; 11 | 12 | /// Add two integers together. 13 | #[wasm_bindgen] 14 | pub fn add(a: i32, b: i32) -> i32 { 15 | a + b 16 | } 17 | 18 | //------------------------------------------------------------------------------ 19 | // Utilities 20 | //------------------------------------------------------------------------------ 21 | 22 | /// Panic hook lets us get better error messages if our Rust code ever panics. 23 | /// 24 | /// For more details see 25 | /// 26 | #[wasm_bindgen(js_name = "setPanicHook")] 27 | pub fn set_panic_hook() { 28 | #[cfg(feature = "console_error_panic_hook")] 29 | console_error_panic_hook::set_once(); 30 | } 31 | 32 | #[wasm_bindgen] 33 | extern "C" { 34 | // For alerting 35 | pub(crate) fn alert(s: &str); 36 | // For logging in the console. 37 | #[wasm_bindgen(js_namespace = console)] 38 | pub fn log(s: &str); 39 | } 40 | 41 | //------------------------------------------------------------------------------ 42 | // Macros 43 | //------------------------------------------------------------------------------ 44 | 45 | /// Return a representation of an object owned by JS. 46 | #[macro_export] 47 | macro_rules! value { 48 | ($value:expr) => { 49 | wasm_bindgen::JsValue::from($value) 50 | }; 51 | } 52 | 53 | /// Calls the wasm_bindgen console.log. 54 | #[macro_export] 55 | macro_rules! console_log { 56 | ($($t:tt)*) => ($crate::log(&format_args!($($t)*).to_string())) 57 | } 58 | -------------------------------------------------------------------------------- /rust+wasm/{{project-name}}-wasm/tests/web.rs: -------------------------------------------------------------------------------- 1 | #![cfg(target_arch = "wasm32")] 2 | 3 | //! Test suite for the Web and headless browsers. 4 | {% if node-or-web == "nodejs" %} 5 | use wasm_bindgen_test::wasm_bindgen_test; 6 | {% else %} 7 | use wasm_bindgen_test::{wasm_bindgen_test, wasm_bindgen_test_configure}; 8 | wasm_bindgen_test_configure!(run_in_browser); 9 | {% endif %} 10 | #[wasm_bindgen_test] 11 | fn test_add() { 12 | assert_eq!({{crate_name}}_wasm::add(3, 2), 5); 13 | {{crate_name}}_wasm::console_log!("{}", "Test passes!"); 14 | } 15 | -------------------------------------------------------------------------------- /rust+wasm/{{project-name}}/.dockerignore: -------------------------------------------------------------------------------- 1 | * 2 | 3 | !Cargo.lock 4 | !Cargo.toml 5 | !src 6 | 7 | src/bin 8 | -------------------------------------------------------------------------------- /rust+wasm/{{project-name}}/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "{{project-name}}" 3 | version = "0.1.0" 4 | description = "{{description}}" 5 | keywords = [] 6 | categories = []{% if license == "Apache" %} 7 | license = "Apache-2.0" 8 | include = ["/src", "README.md", "LICENSE"] 9 | {% elsif license == "MIT" %} 10 | include = ["/src", "README.md", "LICENSE"] 11 | license = "MIT" 12 | {% else %} 13 | include = ["/src", "README.md", "LICENSE-APACHE", "LICENSE-MIT"] 14 | license = "Apache-2.0 or MIT" 15 | {% endif %}readme = "README.md" 16 | edition = "2021" 17 | rust-version = "1.67" 18 | documentation = "https://docs.rs/{{project-name}}" 19 | repository = "https://github.com/{{github-name}}/{{repo-name}}/tree/main/{{project-name}}" 20 | authors = ["{{authors}}"] 21 | {% if crate_type == "lib" %} 22 | [lib] 23 | path = "src/lib.rs" 24 | {% else %} 25 | [lib] 26 | path = "src/lib.rs" 27 | doctest = true 28 | 29 | [[bin]] 30 | name = "{{project-name}}" 31 | path = "src/main.rs" 32 | doc = true 33 | {% endif %} 34 | [dependencies]{% if crate_type == "bin" %} 35 | anyhow = "1.0"{% endif %}{% if bench %} 36 | proptest = { version = "1.1", optional = true }{% endif %}{% if crate_type == "lib" %} 37 | thiserror = "1.0" 38 | tracing = "0.1"{% else %} 39 | tracing = "0.1" 40 | tracing-subscriber = "0.3" 41 | {% endif %}{% if bench %} 42 | [dev-dependencies] 43 | proptest = "1.1" 44 | {% endif %} 45 | [features] 46 | default = []{% if bench %} 47 | test_utils = ["proptest"]{% endif %} 48 | 49 | [package.metadata.docs.rs] 50 | all-features = true 51 | # defines the configuration attribute `docsrs` 52 | rustdoc-args = ["--cfg", "docsrs"] 53 | -------------------------------------------------------------------------------- /rust+wasm/{{project-name}}/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any 2 | person obtaining a copy of this software and associated 3 | documentation files (the "Software"), to deal in the 4 | Software without restriction, including without 5 | limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following 9 | conditions: 10 | 11 | The above copyright notice and this permission notice 12 | shall be included in all copies or substantial portions 13 | of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /rust+wasm/{{project-name}}/README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | {{project-name}} Logo 4 | 5 | 6 |

{{project-name}}

7 | 8 |

9 | 10 | Crate 11 | {% if codecov %} 12 | 13 | Code Coverage 14 | {% endif %}{% if github_actions %} 15 | 16 | Build Status 17 | {% endif %}{% if license == "Apache" %} 18 | 19 | License 20 | {% elsif license == "MIT" %} 21 | 22 | License 23 | {% else %} 24 | 25 | License-Apache 26 | 27 | 28 | License-MIT 29 | {% endif %} 30 | 31 | Docs 32 | {% if have_discord %} 33 | 34 | Discord 35 | {% endif %} 36 |

37 |
38 | 39 |
:warning: Work in progress :warning:
40 | 41 | ## 42 | 43 | Description. 44 | 45 | ## License 46 | {% if license == "Apache" %} 47 | This project is licensed under the [Apache License 2.0](./LICENSE), or 48 | [http://www.apache.org/licenses/LICENSE-2.0][apache]. 49 | {% elsif license == "MIT" %} 50 | This project is licensed under the [MIT License](./LICENSE), 51 | or [http://opensource.org/licenses/MIT][mit]. 52 | {% elsif license == "dual" %} 53 | This project is licensed under either of 54 | 55 | - Apache License, Version 2.0, ([LICENSE-APACHE](./LICENSE-APACHE) or [http://www.apache.org/licenses/LICENSE-2.0][apache]) 56 | - MIT license ([LICENSE-MIT](./LICENSE-MIT) or [http://opensource.org/licenses/MIT][mit]) 57 | 58 | at your option. 59 | 60 | ### Contribution 61 | 62 | Unless you explicitly state otherwise, any contribution intentionally 63 | submitted for inclusion in the work by you, as defined in the Apache-2.0 64 | license, shall be dual licensed as above, without any additional terms or 65 | conditions. 66 | {% endif %} 67 | {% if license == "Apache" %} 68 | [apache]: https://www.apache.org/licenses/LICENSE-2.0{% endif %}{% if license == "dual" %} 69 | [apache]: https://www.apache.org/licenses/LICENSE-2.0{% endif %}{% if license == "MIT" %} 70 | [mit]: http://opensource.org/licenses/MIT{% endif %}{% if license == "dual" %} 71 | [mit]: http://opensource.org/licenses/MIT{% endif %} 72 | -------------------------------------------------------------------------------- /rust+wasm/{{project-name}}/config/settings.toml: -------------------------------------------------------------------------------- 1 | [monitoring] 2 | process_collector_interval = 10 3 | 4 | [otel] 5 | exporter_otlp_endpoint = "http://localhost:4317" 6 | 7 | [server] 8 | environment = "local" 9 | metrics_port = {{metricsport}} 10 | port = {{port}} 11 | timeout_ms = 30000 12 | -------------------------------------------------------------------------------- /rust+wasm/{{project-name}}/docs/specs/latest.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.3", 3 | "info": { 4 | "title": "{{project-name}}", 5 | "description": "{{description}}", 6 | "contact": { 7 | "name": "Zeeshan Lakhani", 8 | "email": "zeeshan.lakhani@gmail.com" 9 | }, 10 | "license": { 11 | "name": {% if license == "Apache" %}"Apache-2.0"{% elsif license == "MIT" %}"MIT"{% else %}"Apache-2.0 or MIT"{% endif %} 12 | }, 13 | "version": "0.1.0" 14 | }, 15 | "paths": { 16 | "/healthcheck": { 17 | "get": { 18 | "tags": [ 19 | "health" 20 | ], 21 | "summary": "GET handler for checking service health.", 22 | "description": "GET handler for checking service health.", 23 | "operationId": "healthcheck", 24 | "responses": { 25 | "200": { 26 | "description": "{{project-name}} healthy" 27 | }, 28 | "500": { 29 | "description": "{{project-name}} not healthy", 30 | "content": { 31 | "application/json": { 32 | "schema": { 33 | "$ref": "#/components/schemas/AppError" 34 | } 35 | } 36 | } 37 | } 38 | }, 39 | "deprecated": false 40 | } 41 | }, 42 | "/ping": { 43 | "get": { 44 | "tags": [ 45 | "ping" 46 | ], 47 | "summary": "GET handler for internal pings and availability", 48 | "description": "GET handler for internal pings and availability", 49 | "operationId": "get", 50 | "responses": { 51 | "200": { 52 | "description": "Ping successful" 53 | }, 54 | "500": { 55 | "description": "Ping not successful", 56 | "content": { 57 | "application/json": { 58 | "schema": { 59 | "$ref": "#/components/schemas/AppError" 60 | } 61 | } 62 | } 63 | } 64 | }, 65 | "deprecated": false 66 | } 67 | } 68 | }, 69 | "components": { 70 | "schemas": { 71 | "AppError": { 72 | "type": "object", 73 | "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", 74 | "required": [ 75 | "status" 76 | ], 77 | "properties": { 78 | "detail": { 79 | "type": "string" 80 | }, 81 | "status": { 82 | "type": "integer", 83 | "format": "int32", 84 | "example": 200 85 | }, 86 | "title": { 87 | "type": "string" 88 | } 89 | } 90 | } 91 | } 92 | }, 93 | "tags": [ 94 | { 95 | "name": "", 96 | "description": "{{project-name}} service/middleware" 97 | } 98 | ] 99 | } 100 | -------------------------------------------------------------------------------- /rust+wasm/{{project-name}}/src.axum/bin/openapi.rs: -------------------------------------------------------------------------------- 1 | use std::{env, fs::File, io::prelude::*, path::PathBuf}; 2 | use utoipa::OpenApi; 3 | use {{crate_name}}::docs::ApiDoc; 4 | 5 | fn main() { 6 | let json_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("docs/specs/latest.json"); 7 | let json_path_show = json_path.as_path().display().to_string(); 8 | 9 | let mut file = match File::create(json_path) { 10 | Ok(file) => file, 11 | Err(err) => { 12 | eprintln!("error creating file: {err:?}"); 13 | std::process::exit(1) 14 | } 15 | }; 16 | 17 | let json = match ApiDoc::openapi().to_pretty_json() { 18 | Ok(mut json) => { 19 | json.push('\n'); 20 | json 21 | } 22 | Err(err) => { 23 | eprintln!("error generating OpenAPI json: {err:?}"); 24 | std::process::exit(1) 25 | } 26 | }; 27 | 28 | match file.write_all(json.as_bytes()) { 29 | Ok(_) => println!("OpenAPI json written to path: {json_path_show}\n\n{json}"), 30 | Err(err) => { 31 | eprintln!("error writing to file. {err:?}"); 32 | std::process::exit(1) 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /rust+wasm/{{project-name}}/src.axum/docs.rs: -------------------------------------------------------------------------------- 1 | //! OpenAPI doc generation. 2 | 3 | use crate::{ 4 | error::AppError, 5 | routes::{health, ping}, 6 | }; 7 | use utoipa::OpenApi; 8 | 9 | /// API documentation generator. 10 | #[derive(OpenApi)] 11 | #[openapi( 12 | paths(health::healthcheck, ping::get), 13 | components(schemas(AppError)), 14 | tags( 15 | (name = "", description = "{{project-name}} service/middleware") 16 | ) 17 | )] 18 | 19 | /// Tied to OpenAPI documentation. 20 | #[derive(Debug)] 21 | pub struct ApiDoc; 22 | -------------------------------------------------------------------------------- /rust+wasm/{{project-name}}/src.axum/extract/mod.rs: -------------------------------------------------------------------------------- 1 | //! Custom [axum::extract] Extractors. 2 | 3 | pub mod json; 4 | -------------------------------------------------------------------------------- /rust+wasm/{{project-name}}/src.axum/headers/header.rs: -------------------------------------------------------------------------------- 1 | use axum_extra::{headers::Header, TypedHeader}; 2 | 3 | /// Generate String-focused, generic, custom typed [`Header`]'s. 4 | #[allow(unused)] 5 | macro_rules! header { 6 | ($tname:ident, $hname:ident, $sname:expr) => { 7 | static $hname: once_cell::sync::Lazy = 8 | once_cell::sync::Lazy::new(|| axum_extra::headers::HeaderName::from_static($sname)); 9 | 10 | #[doc = "Generated custom [`axum_extra::headers::Header`] for "] 11 | #[doc = $sname] 12 | #[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] 13 | pub(crate) struct $tname(pub(crate) String); 14 | 15 | impl std::fmt::Display for $tname { 16 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 17 | write!(f, "{}", self.0) 18 | } 19 | } 20 | 21 | impl std::convert::From<&str> for $tname { 22 | fn from(item: &str) -> Self { 23 | $tname(item.to_string()) 24 | } 25 | } 26 | 27 | impl axum_extra::headers::Header for $tname { 28 | fn name() -> &'static axum_extra::headers::HeaderName { 29 | &$hname 30 | } 31 | 32 | fn decode<'i, I>(values: &mut I) -> Result 33 | where 34 | I: Iterator, 35 | { 36 | values 37 | .next() 38 | .and_then(|v| v.to_str().ok()) 39 | .map(|x| $tname(x.to_string())) 40 | .ok_or_else(axum_extra::headers::Error::invalid) 41 | } 42 | 43 | fn encode(&self, values: &mut E) 44 | where 45 | E: Extend, 46 | { 47 | if let Ok(value) = axum_extra::headers::HeaderValue::from_str(&self.0) { 48 | values.extend(std::iter::once(value)); 49 | } 50 | } 51 | } 52 | }; 53 | } 54 | 55 | /// Trait for returning header value directly for passing 56 | /// along to client calls. 57 | #[allow(unused)] 58 | pub(crate) trait HeaderValue { 59 | fn header_value(&self) -> String; 60 | } 61 | 62 | impl HeaderValue for TypedHeader 63 | where 64 | T: Header + std::fmt::Display, 65 | { 66 | fn header_value(&self) -> String { 67 | self.0.to_string() 68 | } 69 | } 70 | 71 | impl HeaderValue for &TypedHeader 72 | where 73 | T: Header + std::fmt::Display, 74 | { 75 | fn header_value(&self) -> String { 76 | self.0.to_string() 77 | } 78 | } 79 | 80 | #[cfg(test)] 81 | pub(crate) mod tests { 82 | use axum::http; 83 | use axum_extra::headers::{Header, HeaderMapExt}; 84 | 85 | header!(XDummyId, XDUMMY_ID, "x-dummy-id"); 86 | 87 | fn test_decode(values: &[&str]) -> Option { 88 | let mut map = http::HeaderMap::new(); 89 | for val in values { 90 | map.append(T::name(), val.parse().unwrap()); 91 | } 92 | map.typed_get() 93 | } 94 | 95 | fn test_encode(header: T) -> http::HeaderMap { 96 | let mut map = http::HeaderMap::new(); 97 | map.typed_insert(header); 98 | map 99 | } 100 | 101 | #[test] 102 | fn test_dummy_header() { 103 | let s = "18312349-3139-498C-84B6-87326BF1F2A7"; 104 | let dummy_id = test_decode::(&[s]).unwrap(); 105 | let headers = test_encode(dummy_id); 106 | assert_eq!(headers["x-dummy-id"], s); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /rust+wasm/{{project-name}}/src.axum/headers/mod.rs: -------------------------------------------------------------------------------- 1 | //! Working-with and generating custom headers. 2 | 3 | pub(crate) mod header; 4 | -------------------------------------------------------------------------------- /rust+wasm/{{project-name}}/src.axum/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(docsrs, feature(doc_cfg))] 2 | #![warn(missing_debug_implementations, missing_docs, rust_2018_idioms)] 3 | #![deny(unreachable_pub)] 4 | #![deny(private_bounds)] 5 | #![deny(rustdoc::private_intra_doc_links)] 6 | #![deny(private_interfaces)] 7 | 8 | //! {{project-name}} 9 | 10 | pub mod docs; 11 | pub mod error; 12 | pub mod extract; 13 | pub mod headers; 14 | pub mod metrics; 15 | pub mod middleware; 16 | pub mod router; 17 | pub mod routes; 18 | pub mod settings; 19 | pub mod tracer; 20 | pub mod tracing_layers; 21 | {% if bench %} 22 | /// Test utilities. 23 | #[cfg(any(test, feature = "test_utils"))] 24 | #[cfg_attr(docsrs, doc(cfg(feature = "test_utils")))] 25 | pub mod test_utils; 26 | /// Add two integers together. 27 | pub fn add(a: i32, b: i32) -> i32 { 28 | a + b 29 | }{% endif %} 30 | -------------------------------------------------------------------------------- /rust+wasm/{{project-name}}/src.axum/metrics/mod.rs: -------------------------------------------------------------------------------- 1 | //! Metrics capture and Prometheus recorder. 2 | 3 | pub mod process; 4 | pub mod prom; 5 | -------------------------------------------------------------------------------- /rust+wasm/{{project-name}}/src.axum/metrics/prom.rs: -------------------------------------------------------------------------------- 1 | //! Metrics Prometheus recorder. 2 | 3 | use crate::metrics::process; 4 | 5 | use metrics_exporter_prometheus::{Matcher, PrometheusBuilder, PrometheusHandle}; 6 | 7 | /// Sets up Prometheus buckets for matched metrics and installs recorder. 8 | pub fn setup_metrics_recorder() -> anyhow::Result { 9 | const EXPONENTIAL_SECONDS: &[f64] = &[ 10 | 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 11 | ]; 12 | 13 | let builder = PrometheusBuilder::new() 14 | .set_buckets_for_metric( 15 | Matcher::Suffix("_duration_seconds".to_string()), 16 | EXPONENTIAL_SECONDS, 17 | )? 18 | .install_recorder()?; 19 | 20 | process::describe(); 21 | 22 | Ok(builder) 23 | } 24 | -------------------------------------------------------------------------------- /rust+wasm/{{project-name}}/src.axum/middleware/client/metrics.rs: -------------------------------------------------------------------------------- 1 | //! Middleware for tracking metrics on each client [reqwest::Request]. 2 | 3 | use http::Extensions; 4 | use reqwest_middleware::Middleware as ReqwestMiddleware; 5 | use std::time::Instant; 6 | 7 | const OK: &str = "ok"; 8 | const ERROR: &str = "error"; 9 | const MIDDLEWARE_ERROR: &str = "middleware_error"; 10 | const NONE: &str = "none"; 11 | const RESULT: &str = "result"; 12 | const STATUS: &str = "status"; 13 | 14 | /// Metrics struct for use as part of middleware. 15 | #[derive(Debug)] 16 | pub struct Metrics { 17 | /// Client name for metric(s) gathering. 18 | pub name: String, 19 | } 20 | 21 | #[async_trait::async_trait] 22 | impl ReqwestMiddleware for Metrics { 23 | async fn handle( 24 | &self, 25 | request: reqwest::Request, 26 | extensions: &mut Extensions, 27 | next: reqwest_middleware::Next<'_>, 28 | ) -> Result { 29 | let now = Instant::now(); 30 | 31 | let url = request.url().clone(); 32 | let request_path = url.path().to_string(); 33 | let method = request.method().clone(); 34 | 35 | let result = next.run(request, extensions).await; 36 | let latency = now.elapsed().as_secs_f64(); 37 | 38 | let labels = vec![ 39 | ("client", self.name.to_string()), 40 | ("method", method.to_string()), 41 | ("request_path", request_path.clone()), 42 | ]; 43 | 44 | let extended_labels = extend_labels_for_response(labels, &result); 45 | 46 | metrics::counter!("client_http_requests_total").increment(1u64); 47 | metrics::counter!("client_http_requests_total", &extended_labels).increment(1u64); 48 | metrics::counter!("client_http_requests_total", "client" => self.name.to_string()) 49 | .increment(1u64); 50 | metrics::counter!("client_http_requests_total", "client" => self.name.to_string(), "request_path" => request_path.clone()).increment(1u64); 51 | metrics::histogram!("client_http_request_duration_seconds").record(latency); 52 | metrics::histogram!("client_http_request_duration_seconds", &extended_labels) 53 | .record(latency); 54 | metrics::histogram!("client_http_request_duration_seconds", "client" => self.name.to_string()).record(latency); 55 | metrics::histogram!("client_http_request_duration_seconds", "client" => self.name.to_string(), "request_path" => request_path.clone()).record(latency); 56 | 57 | result 58 | } 59 | } 60 | 61 | /// Extend a set of metrics label tuples with dynamic properties 62 | /// around reqwest responses for `result` and `status` fields. 63 | pub fn extend_labels_for_response<'a>( 64 | mut labels: Vec<(&'a str, String)>, 65 | result: &Result, 66 | ) -> Vec<(&'a str, String)> { 67 | match result { 68 | Ok(ref success) => { 69 | match success.status().as_u16() { 70 | 200..=299 => labels.push((RESULT, OK.to_string())), 71 | _ => labels.push((RESULT, ERROR.to_string())), 72 | } 73 | 74 | labels.push((STATUS, success.status().as_u16().to_string())); 75 | } 76 | Err(reqwest_middleware::Error::Reqwest(ref err)) => { 77 | labels.push((RESULT, ERROR.to_string())); 78 | labels.push(( 79 | STATUS, 80 | err.status() 81 | .map(|status| status.as_u16().to_string()) 82 | .unwrap_or_else(|| NONE.to_string()), 83 | )); 84 | } 85 | Err(reqwest_middleware::Error::Middleware(ref _err)) => { 86 | labels.push((RESULT, MIDDLEWARE_ERROR.to_string())); 87 | labels.push((STATUS, NONE.to_string())); 88 | } 89 | }; 90 | 91 | labels 92 | } 93 | -------------------------------------------------------------------------------- /rust+wasm/{{project-name}}/src.axum/middleware/client/mod.rs: -------------------------------------------------------------------------------- 1 | //! Middleware for calls to outside client APIs. 2 | 3 | pub mod metrics; 4 | -------------------------------------------------------------------------------- /rust+wasm/{{project-name}}/src.axum/middleware/metrics.rs: -------------------------------------------------------------------------------- 1 | //! Middleware for tracking metrics on each [axum::http::Request]. 2 | 3 | use crate::middleware::request_ext::RequestExt; 4 | use axum::{body::Body, http::Request, middleware::Next, response::IntoResponse}; 5 | use std::time::Instant; 6 | 7 | /// Middleware function called to track (and update) http metrics when a route 8 | /// is requested. 9 | pub async fn track(req: Request, next: Next) -> impl IntoResponse { 10 | let start = Instant::now(); 11 | 12 | let path = req.path().to_string(); 13 | 14 | let res = next.run(req).await; 15 | let latency = start.elapsed().as_secs_f64(); 16 | let status = res.status().as_u16(); 17 | 18 | metrics::counter!("http_requests_total").increment(1); 19 | metrics::counter!("http_requests_total", "request_path" => path.clone() ).increment(1); 20 | metrics::counter!("http_requests_total", "request_path" => path.clone(), "status" => status.to_string() ).increment(1); 21 | metrics::histogram!("http_request_duration_seconds").record(latency); 22 | metrics::histogram!("http_request_duration_seconds", "request_path" => path.clone()) 23 | .record(latency); 24 | metrics::histogram!("http_request_duration_seconds", "request_path" => path.clone(), "status" => status.to_string()).record(latency); 25 | res 26 | } 27 | -------------------------------------------------------------------------------- /rust+wasm/{{project-name}}/src.axum/middleware/mod.rs: -------------------------------------------------------------------------------- 1 | //! Additional [axum::middleware]. 2 | 3 | pub mod client; 4 | pub mod logging; 5 | pub mod metrics; 6 | pub(crate) mod request_ext; 7 | pub mod request_ulid; 8 | pub mod reqwest_retry; 9 | pub mod reqwest_tracing; 10 | pub mod runtime; 11 | -------------------------------------------------------------------------------- /rust+wasm/{{project-name}}/src.axum/middleware/request_ext.rs: -------------------------------------------------------------------------------- 1 | //! Middleware for additional [axum::http::Request] methods. 2 | 3 | use axum::{ 4 | extract::{MatchedPath, OriginalUri}, 5 | http::Request, 6 | }; 7 | 8 | /// Trait for extra methods on [`Request`](axum::http::Request) 9 | pub(crate) trait RequestExt { 10 | /// Parse request path on the request. 11 | fn path(&self) -> String; 12 | } 13 | 14 | impl RequestExt for Request { 15 | fn path(&self) -> String { 16 | if let Some(matched_path) = self.extensions().get::() { 17 | matched_path.as_str().to_string() 18 | } else if let Some(uri) = self.extensions().get::() { 19 | uri.0.path().to_string() 20 | } else { 21 | self.uri().path().to_string() 22 | } 23 | } 24 | } 25 | 26 | #[cfg(test)] 27 | mod test { 28 | use super::*; 29 | use axum::http::Request; 30 | 31 | #[test] 32 | fn parse_path() { 33 | let mut req1: Request<()> = Request::default(); 34 | *req1.uri_mut() = "https://www.rust-lang.org/users/:id".parse().unwrap(); 35 | assert_eq!(req1.path(), "/users/:id"); 36 | 37 | let mut req2: Request<()> = Request::default(); 38 | *req2.uri_mut() = "https://www.rust-lang.org/api/users".parse().unwrap(); 39 | assert_eq!(req2.path(), "/api/users"); 40 | 41 | let mut req3: Request<()> = Request::default(); 42 | *req3.uri_mut() = "/api/users".parse().unwrap(); 43 | assert_eq!(req3.path(), "/api/users"); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /rust+wasm/{{project-name}}/src.axum/middleware/request_ulid.rs: -------------------------------------------------------------------------------- 1 | //! Middleware for generating [ulid::Ulid]s on requests. 2 | 3 | use axum::http::Request; 4 | use tower_http::request_id::{MakeRequestId, RequestId}; 5 | use ulid::Ulid; 6 | 7 | /// Make/generate ulid on requests. 8 | #[derive(Copy, Clone, Debug)] 9 | pub struct MakeRequestUlid; 10 | 11 | /// Implement the trait for producing a request ID from the incoming request. 12 | /// In our case, we want to generate a new UUID that we can associate with a single request. 13 | impl MakeRequestId for MakeRequestUlid { 14 | fn make_request_id(&mut self, _: &Request) -> Option { 15 | let req_id = Ulid::new().to_string().parse(); 16 | match req_id { 17 | Ok(id) => Some(RequestId::new(id)), 18 | _ => None, 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /rust+wasm/{{project-name}}/src.axum/middleware/reqwest_tracing.rs: -------------------------------------------------------------------------------- 1 | //! Adding trace information to [reqwest::Request]s. 2 | 3 | use http::Extensions; 4 | use reqwest::{Request, Response}; 5 | use reqwest_middleware::Result; 6 | use reqwest_tracing::{default_on_request_end, reqwest_otel_span, ReqwestOtelSpanBackend}; 7 | use std::time::Instant; 8 | use tracing::Span; 9 | 10 | /// Latency string. 11 | const LATENCY_FIELD: &str = "latency_ms"; 12 | 13 | /// Struct for extending [reqwest_tracing::TracingMiddleware]. 14 | #[derive(Debug)] 15 | pub struct ExtendedTrace; 16 | 17 | impl ReqwestOtelSpanBackend for ExtendedTrace { 18 | fn on_request_start(req: &Request, extension: &mut Extensions) -> Span { 19 | extension.insert(Instant::now()); 20 | reqwest_otel_span!( 21 | name = "reqwest-http-request", 22 | req, 23 | latency_ms = tracing::field::Empty, 24 | ) 25 | } 26 | 27 | fn on_request_end(span: &Span, outcome: &Result, extension: &mut Extensions) { 28 | let elapsed_milliseconds = extension.get::().unwrap().elapsed().as_millis() as i64; 29 | default_on_request_end(span, outcome); 30 | span.record(LATENCY_FIELD, elapsed_milliseconds); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /rust+wasm/{{project-name}}/src.axum/middleware/runtime.rs: -------------------------------------------------------------------------------- 1 | //! Middleware for runtime, [tower_http] extensions. 2 | 3 | use crate::error::AppError; 4 | 5 | use axum::response::{IntoResponse, Response}; 6 | use std::any::Any; 7 | 8 | /// Middleware function for catching runtime panics, logging 9 | /// them, and converting them into a `500 Internal Server` response. 10 | pub fn catch_panic(err: Box) -> Response { 11 | let details = if let Some(s) = err.downcast_ref::() { 12 | s.clone() 13 | } else if let Some(s) = err.downcast_ref::<&str>() { 14 | s.to_string() 15 | } else { 16 | "Unknown panic message".to_string() 17 | }; 18 | 19 | let err: AppError = anyhow::anyhow!(details).into(); 20 | err.into_response() 21 | } 22 | 23 | #[cfg(test)] 24 | mod test { 25 | use super::*; 26 | use crate::error::{parse_error, AppError}; 27 | use axum::http::StatusCode; 28 | use axum::{body::Body, http::Request, routing::get, Router}; 29 | use tower::{ServiceBuilder, ServiceExt}; 30 | use tower_http::catch_panic::CatchPanicLayer; 31 | 32 | #[tokio::test] 33 | async fn catch_panic_error() { 34 | let middleware = ServiceBuilder::new().layer(CatchPanicLayer::custom(catch_panic)); 35 | 36 | let app = Router::new() 37 | .route("/", get(|| async { panic!("hi") })) 38 | .layer(middleware); 39 | 40 | let res = app 41 | .oneshot(Request::builder().uri("/").body(Body::empty()).unwrap()) 42 | .await 43 | .unwrap(); 44 | 45 | let err = parse_error(res).await; 46 | 47 | assert_eq!( 48 | err, 49 | AppError::new(StatusCode::INTERNAL_SERVER_ERROR, Some("hi")) 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /rust+wasm/{{project-name}}/src.axum/router.rs: -------------------------------------------------------------------------------- 1 | //! Main [axum::Router] interface for webserver. 2 | 3 | use crate::{ 4 | middleware::logging::{log_request_response, DebugOnlyLogger, Logger}, 5 | routes::{fallback::notfound_404, health, ping}, 6 | }; 7 | use axum::{routing::get, Router}; 8 | 9 | /// Setup main router for application. 10 | pub fn setup_app_router() -> Router { 11 | let mut router = Router::new() 12 | .route("/ping", get(ping::get)) 13 | .fallback(notfound_404); 14 | 15 | router = router.layer(axum::middleware::from_fn(log_request_response::)); 16 | 17 | let mut healthcheck_router = Router::new().route("/healthcheck", get(health::healthcheck)); 18 | 19 | healthcheck_router = healthcheck_router.layer(axum::middleware::from_fn( 20 | log_request_response::, 21 | )); 22 | 23 | Router::merge(router, healthcheck_router) 24 | } 25 | -------------------------------------------------------------------------------- /rust+wasm/{{project-name}}/src.axum/routes/fallback.rs: -------------------------------------------------------------------------------- 1 | //! Fallback routes. 2 | 3 | use crate::error::AppError; 4 | use axum::http::StatusCode; 5 | 6 | /// 404 fallback. 7 | pub async fn notfound_404() -> AppError { 8 | AppError::new(StatusCode::NOT_FOUND, Some("Route does not exist!")) 9 | } 10 | -------------------------------------------------------------------------------- /rust+wasm/{{project-name}}/src.axum/routes/health.rs: -------------------------------------------------------------------------------- 1 | //! Healthcheck route. 2 | 3 | use crate::error::AppResult; 4 | use axum::{self, http::StatusCode}; 5 | use serde_json::json; 6 | 7 | /// GET handler for checking service health. 8 | #[utoipa::path( 9 | get, 10 | path = "/healthcheck", 11 | responses( 12 | (status = 200, description = "{{project-name}} healthy"), 13 | (status = 500, description = "{{project-name}} not healthy", body=AppError) 14 | ) 15 | )] 16 | pub async fn healthcheck() -> AppResult<(StatusCode, axum::Json)> { 17 | Ok((StatusCode::OK, axum::Json(json!({ "msg": "Healthy"})))) 18 | } 19 | -------------------------------------------------------------------------------- /rust+wasm/{{project-name}}/src.axum/routes/mod.rs: -------------------------------------------------------------------------------- 1 | //! Routes for [axum::Router]. 2 | 3 | pub mod fallback; 4 | pub mod health; 5 | pub mod ping; 6 | -------------------------------------------------------------------------------- /rust+wasm/{{project-name}}/src.axum/routes/ping.rs: -------------------------------------------------------------------------------- 1 | //! Generic ping route. 2 | 3 | use crate::error::AppResult; 4 | use axum::{self, http::StatusCode}; 5 | 6 | /// GET handler for internal pings and availability 7 | #[utoipa::path( 8 | get, 9 | path = "/ping", 10 | responses( 11 | (status = 200, description = "Ping successful"), 12 | (status = 500, description = "Ping not successful", body=AppError) 13 | ) 14 | )] 15 | 16 | pub async fn get() -> AppResult { 17 | Ok(StatusCode::OK) 18 | } 19 | -------------------------------------------------------------------------------- /rust+wasm/{{project-name}}/src.axum/test_utils/mod.rs: -------------------------------------------------------------------------------- 1 | /// Random value generator for sampling data. 2 | #[cfg(feature = "test_utils")] 3 | mod rvg; 4 | #[cfg(feature = "test_utils")] 5 | pub use rvg::*; 6 | -------------------------------------------------------------------------------- /rust+wasm/{{project-name}}/src.axum/test_utils/rvg.rs: -------------------------------------------------------------------------------- 1 | use proptest::{ 2 | collection::vec, 3 | strategy::{Strategy, ValueTree}, 4 | test_runner::{Config, TestRunner}, 5 | }; 6 | 7 | /// A random value generator (RVG), which, given proptest strategies, will 8 | /// generate random values based on those strategies. 9 | #[derive(Debug, Default)] 10 | pub struct Rvg { 11 | runner: TestRunner, 12 | } 13 | 14 | impl Rvg { 15 | /// Creates a new RVG with the default random number generator. 16 | pub fn new() -> Self { 17 | Rvg { 18 | runner: TestRunner::new(Config::default()), 19 | } 20 | } 21 | 22 | /// Creates a new RVG with a deterministic random number generator, 23 | /// using the same seed across test runs. 24 | pub fn deterministic() -> Self { 25 | Rvg { 26 | runner: TestRunner::deterministic(), 27 | } 28 | } 29 | 30 | /// Samples a value for the given strategy. 31 | /// 32 | /// # Example 33 | /// 34 | /// ``` 35 | /// use {{crate_name}}::test_utils::Rvg; 36 | /// 37 | /// let mut rvg = Rvg::new(); 38 | /// let int = rvg.sample(&(0..100i32)); 39 | /// ``` 40 | pub fn sample(&mut self, strategy: &S) -> S::Value { 41 | strategy 42 | .new_tree(&mut self.runner) 43 | .expect("No value can be generated") 44 | .current() 45 | } 46 | 47 | /// Samples a vec of some length with a value for the given strategy. 48 | /// 49 | /// # Example 50 | /// 51 | /// ``` 52 | /// use {{crate_name}}::test_utils::Rvg; 53 | /// 54 | /// let mut rvg = Rvg::new(); 55 | /// let ints = rvg.sample_vec(&(0..100i32), 10); 56 | /// ``` 57 | pub fn sample_vec(&mut self, strategy: &S, len: usize) -> Vec { 58 | vec(strategy, len..=len) 59 | .new_tree(&mut self.runner) 60 | .expect("No value can be generated") 61 | .current() 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /rust+wasm/{{project-name}}/src.axum/tracer.rs: -------------------------------------------------------------------------------- 1 | //! Opentelemetry tracing extensions and setup. 2 | 3 | use crate::settings::Otel; 4 | use anyhow::{anyhow, Result}; 5 | use const_format::formatcp; 6 | use http::Uri; 7 | use opentelemetry::{global, KeyValue}; 8 | use opentelemetry_otlp::{TonicExporterBuilder, WithExportConfig}; 9 | use opentelemetry_sdk::{ 10 | self, propagation::TraceContextPropagator, runtime, trace::Tracer, Resource, 11 | }; 12 | use opentelemetry_semantic_conventions as otel_semcov; 13 | use tonic::{metadata::MetadataMap, transport::ClientTlsConfig}; 14 | 15 | //const PKG_NAME: &str = env!("CARGO_PKG_NAME"); 16 | const PKG_NAME: &str = "application"; 17 | const VERSION: &str = formatcp!("v{}", env!("CARGO_PKG_VERSION")); 18 | const LANG: &str = "rust"; 19 | 20 | /// Initialize Opentelemetry tracing via the [OTLP protocol]. 21 | /// 22 | /// [OTLP protocol]: 23 | pub fn init_tracer(settings: &Otel) -> Result { 24 | global::set_text_map_propagator(TraceContextPropagator::new()); 25 | 26 | let resource = Resource::new(vec![ 27 | KeyValue::new(otel_semcov::resource::SERVICE_NAME, PKG_NAME), 28 | KeyValue::new(otel_semcov::resource::SERVICE_VERSION, VERSION), 29 | KeyValue::new(otel_semcov::resource::TELEMETRY_SDK_LANGUAGE, LANG), 30 | ]); 31 | 32 | let endpoint = &settings.exporter_otlp_endpoint; 33 | 34 | let map = MetadataMap::with_capacity(2); 35 | 36 | let trace = opentelemetry_otlp::new_pipeline() 37 | .tracing() 38 | .with_exporter(exporter(map, endpoint)?) 39 | .with_trace_config(opentelemetry_sdk::trace::config().with_resource(resource)) 40 | .install_batch(runtime::Tokio) 41 | .map_err(|e| anyhow!("failed to intialize tracer: {:#?}", e))?; 42 | 43 | Ok(trace) 44 | } 45 | 46 | fn exporter(map: MetadataMap, endpoint: &Uri) -> Result { 47 | // Over grpc transport 48 | let exporter = opentelemetry_otlp::new_exporter() 49 | .tonic() 50 | .with_endpoint(endpoint.to_string()) 51 | .with_metadata(map); 52 | 53 | match endpoint.scheme_str() { 54 | Some("https") => { 55 | let host = endpoint 56 | .host() 57 | .ok_or_else(|| anyhow!("failed to parse host"))?; 58 | 59 | Ok(exporter.with_tls_config(ClientTlsConfig::new().domain_name(host.to_string()))) 60 | } 61 | _ => Ok(exporter), 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /rust+wasm/{{project-name}}/src.axum/tracing_layers/metrics_layer.rs: -------------------------------------------------------------------------------- 1 | //! Metrics layer. 2 | 3 | use crate::tracing_layers::storage_layer::Storage; 4 | use std::{borrow::Cow, time::Instant}; 5 | use tracing::{Id, Subscriber}; 6 | use tracing_subscriber::{layer::Context, registry::LookupSpan, Layer}; 7 | 8 | const PREFIX_LABEL: &str = "metric_label_"; 9 | const METRIC_NAME: &str = "metric_name"; 10 | const OK: &str = "ok"; 11 | const ERROR: &str = "error"; 12 | const LABEL: &str = "label"; 13 | const RESULT_LABEL: &str = "result"; 14 | const SPAN_LABEL: &str = "span_name"; 15 | 16 | /// Prefix used for capturing metric spans/instrumentations. 17 | pub const METRIC_META_PREFIX: &str = "record."; 18 | 19 | /// Metrics layer for automatically deriving metrics for record.* events. 20 | /// 21 | /// Append to custom [LogFmtLayer](crate::tracing_layers::format_layer::LogFmtLayer). 22 | #[derive(Debug)] 23 | pub struct MetricsLayer; 24 | 25 | impl Layer for MetricsLayer 26 | where 27 | S: Subscriber + for<'span> LookupSpan<'span>, 28 | { 29 | fn on_close(&self, id: Id, ctx: Context<'_, S>) { 30 | let span = ctx.span(&id).expect("Span not found"); 31 | let mut extensions = span.extensions_mut(); 32 | 33 | let elapsed_secs_f64 = extensions 34 | .get_mut::() 35 | .map(|i| i.elapsed().as_secs_f64()) 36 | .unwrap_or(0.0); 37 | 38 | if let Some(visitor) = extensions.get_mut::>() { 39 | let mut labels = vec![]; 40 | for (key, value) in visitor.values() { 41 | if key.starts_with(PREFIX_LABEL) { 42 | labels.push(( 43 | key.strip_prefix(PREFIX_LABEL).unwrap_or(LABEL), 44 | value.to_string(), 45 | )) 46 | } 47 | } 48 | 49 | let span_name = span 50 | .name() 51 | .strip_prefix(METRIC_META_PREFIX) 52 | .unwrap_or_else(|| span.name()); 53 | 54 | labels.push((SPAN_LABEL, span_name.to_string())); 55 | 56 | let name = visitor 57 | .values() 58 | .get(METRIC_NAME) 59 | .unwrap_or(&Cow::from(span_name)) 60 | .to_string(); 61 | 62 | if visitor.values().contains_key(ERROR) { 63 | labels.push((RESULT_LABEL, String::from(ERROR))) 64 | } else { 65 | labels.push((RESULT_LABEL, String::from(OK))) 66 | } 67 | 68 | // Need to sort labels to remain the same across all metrics. 69 | labels.sort_unstable(); 70 | 71 | metrics::counter!(format!("{name}_total"), &labels).increment(1); 72 | metrics::histogram!(format!("{name}_duration_seconds"), &labels) 73 | .record(elapsed_secs_f64); 74 | 75 | // Remove storage as this is the last layer. 76 | extensions 77 | .remove::>() 78 | .expect("Visitor not found on 'close'"); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /rust+wasm/{{project-name}}/src.axum/tracing_layers/mod.rs: -------------------------------------------------------------------------------- 1 | //! Custom [tracing_subscriber::layer::Layer]s for formatting log events, 2 | //! deriving metrics from instrumentation calls, and for storage to augment 3 | //! layers. For more information, please read [Composing an observable Rust application]. 4 | //! 5 | //! [Composing an observable Rust application]: 6 | 7 | pub mod format_layer; 8 | pub mod metrics_layer; 9 | pub mod storage_layer; 10 | -------------------------------------------------------------------------------- /rust+wasm/{{project-name}}/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(docsrs, feature(doc_cfg))] 2 | #![warn(missing_debug_implementations, missing_docs, rust_2018_idioms)] 3 | #![deny(unreachable_pub)] 4 | #![deny(private_bounds)] 5 | #![deny(rustdoc::private_intra_doc_links)] 6 | #![deny(private_interfaces)] 7 | 8 | //! {{project-name}} 9 | {% if bench %} 10 | /// Test utilities. 11 | #[cfg(any(test, feature = "test_utils"))] 12 | #[cfg_attr(docsrs, doc(cfg(feature = "test_utils")))] 13 | pub mod test_utils; 14 | {% endif %} 15 | /// Add two integers together. 16 | pub fn add(a: i32, b: i32) -> i32 { 17 | a + b 18 | } 19 | 20 | /// Multiplies two integers together. 21 | pub fn mult(a: i32, b: i32) -> i32 { 22 | a * b 23 | } 24 | 25 | #[cfg(test)] 26 | mod tests { 27 | use super::*; 28 | 29 | #[test] 30 | fn test_mult() { 31 | assert_eq!(mult(3, 2), 6); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /rust+wasm/{{project-name}}/src/main.rs: -------------------------------------------------------------------------------- 1 | //! {{project-name}} 2 | 3 | /// Main entry point. 4 | fn main() { 5 | println!("Welcome!") 6 | } 7 | -------------------------------------------------------------------------------- /rust+wasm/{{project-name}}/src/test_utils/mod.rs: -------------------------------------------------------------------------------- 1 | /// Random value generator for sampling data. 2 | #[cfg(feature = "test_utils")] 3 | mod rvg; 4 | #[cfg(feature = "test_utils")] 5 | pub use rvg::*; 6 | -------------------------------------------------------------------------------- /rust+wasm/{{project-name}}/src/test_utils/rvg.rs: -------------------------------------------------------------------------------- 1 | use proptest::{ 2 | collection::vec, 3 | strategy::{Strategy, ValueTree}, 4 | test_runner::{Config, TestRunner}, 5 | }; 6 | 7 | /// A random value generator (RVG), which, given proptest strategies, will 8 | /// generate random values based on those strategies. 9 | #[derive(Debug, Default)] 10 | pub struct Rvg { 11 | runner: TestRunner, 12 | } 13 | 14 | impl Rvg { 15 | /// Creates a new RVG with the default random number generator. 16 | pub fn new() -> Self { 17 | Rvg { 18 | runner: TestRunner::new(Config::default()), 19 | } 20 | } 21 | 22 | /// Creates a new RVG with a deterministic random number generator, 23 | /// using the same seed across test runs. 24 | pub fn deterministic() -> Self { 25 | Rvg { 26 | runner: TestRunner::deterministic(), 27 | } 28 | } 29 | 30 | /// Samples a value for the given strategy. 31 | /// 32 | /// # Example 33 | /// 34 | /// ``` 35 | /// use {{crate_name}}::test_utils::Rvg; 36 | /// 37 | /// let mut rvg = Rvg::new(); 38 | /// let int = rvg.sample(&(0..100i32)); 39 | /// ``` 40 | pub fn sample(&mut self, strategy: &S) -> S::Value { 41 | strategy 42 | .new_tree(&mut self.runner) 43 | .expect("No value can be generated") 44 | .current() 45 | } 46 | 47 | /// Samples a vec of some length with a value for the given strategy. 48 | /// 49 | /// # Example 50 | /// 51 | /// ``` 52 | /// use {{crate_name}}::test_utils::Rvg; 53 | /// 54 | /// let mut rvg = Rvg::new(); 55 | /// let ints = rvg.sample_vec(&(0..100i32), 10); 56 | /// ``` 57 | pub fn sample_vec(&mut self, strategy: &S, len: usize) -> Vec { 58 | vec(strategy, len..=len) 59 | .new_tree(&mut self.runner) 60 | .expect("No value can be generated") 61 | .current() 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /rust+wasm/{{project-name}}/tests/integration_test.rs: -------------------------------------------------------------------------------- 1 | #[test] 2 | fn test_add() { 3 | assert_eq!({{crate_name}}::add(3, 2), 5); 4 | } 5 | -------------------------------------------------------------------------------- /rust/.dockerignore: -------------------------------------------------------------------------------- 1 | * 2 | 3 | !Cargo.lock 4 | !Cargo.toml 5 | !src{% if bench %} 6 | !benches{% endif %}{% if axum %} 7 | !config{% endif %} 8 | -------------------------------------------------------------------------------- /rust/.envrc: -------------------------------------------------------------------------------- 1 | use_flake 2 | -------------------------------------------------------------------------------- /rust/.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Default 2 | * @{{github-codeowner}} 3 | -------------------------------------------------------------------------------- /rust/.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: "\U0001F41B bug" 6 | assignees: '' 7 | 8 | --- 9 | 10 | # Summary 11 | 12 | ## Problem 13 | 14 | Describe the immediate problem. 15 | 16 | ### Impact 17 | 18 | What's the impact of this bug? 19 | 20 | ## Solution 21 | 22 | Describe the sort of fix that would solve the issue. 23 | 24 | # Detail 25 | 26 | **Describe the bug** 27 | 28 | A clear and concise description of what the bug is. 29 | 30 | **To Reproduce** 31 | 32 | Steps to reproduce the behavior: 33 | 1. Go to '...' 34 | 2. Click on '....' 35 | 3. Scroll down to '....' 36 | 4. See error 37 | 38 | **Expected behavior** 39 | 40 | A clear and concise description of what you expected to happen. 41 | 42 | **Screenshots** 43 | 44 | If applicable, add screenshots to help explain your problem. 45 | 46 | **Desktop (please complete the following information):** 47 | 48 | - OS: [e.g. iOS] 49 | - Browser [e.g. chrome, safari] 50 | - Version [e.g. 22] 51 | 52 | **Smartphone (please complete the following information):** 53 | 54 | - Device: [e.g. iPhone6] 55 | - OS: [e.g. iOS8.1] 56 | - Browser [e.g. stock browser, safari] 57 | - Version [e.g. 22] 58 | 59 | **Additional context** 60 | 61 | Add any other context about the problem here. 62 | -------------------------------------------------------------------------------- /rust/.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: "\U0001F497 enhancement" 6 | assignees: '' 7 | 8 | --- 9 | 10 | NB: Feature requests will only be considered if they solve a pain or present a useful refactoring of the code. 11 | 12 | # Summary 13 | 14 | ## Problem 15 | 16 | Describe the pain that this feature will solve. 17 | 18 | ### Impact 19 | 20 | Describe the impact of not having this feature. 21 | 22 | ## Solution 23 | 24 | Describe the solution. 25 | 26 | # Detail 27 | 28 | **Is your feature request related to a problem? Please describe.** 29 | 30 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 31 | 32 | **Describe the solution you'd like** 33 | 34 | A clear and concise description of what you want to happen. 35 | 36 | **Describe alternatives you've considered** 37 | 38 | A clear and concise description of any alternative solutions or features you've considered. 39 | 40 | **Additional context** 41 | 42 | Add any other context or screenshots about the feature request here. 43 | -------------------------------------------------------------------------------- /rust/.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # Description 2 | 3 | Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. 4 | 5 | ## Link to issue 6 | 7 | Please add a link to any relevant issues/tickets. 8 | 9 | ## Type of change 10 | 11 | - [ ] Bug fix (non-breaking change that fixes an issue) 12 | - [ ] New feature (non-breaking change that adds functionality) 13 | - [ ] Refactor (non-breaking change that updates existing functionality) 14 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 15 | - [ ] This change requires a documentation update 16 | - [ ] Comments have been added/updated 17 | 18 | Please delete options that are not relevant. 19 | 20 | ## Test plan (required) 21 | 22 | Demonstrate the code is solid. Which commands did you test with and what are the expected results? 23 | Which tests have you added or updated? Do the tests cover all of the changes included in this PR? 24 | 25 | ## Screenshots/Screencaps 26 | 27 | Please add previews of any UI Changes. 28 | -------------------------------------------------------------------------------- /rust/.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | 8 | updates: 9 | - package-ecosystem: "cargo" 10 | directory: "/" 11 | commit-message: 12 | prefix: "chore" 13 | include: "scope" 14 | target-branch: "main" 15 | schedule: 16 | interval: "weekly" 17 | 18 | - package-ecosystem: "github-actions" 19 | directory: "/" 20 | commit-message: 21 | prefix: "chore(ci)" 22 | include: "scope" 23 | target-branch: "main" 24 | schedule: 25 | interval: "weekly" 26 | -------------------------------------------------------------------------------- /rust/.github/workflows/audit.yml: -------------------------------------------------------------------------------- 1 | name: 🛡 Audit-Check 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * *' 6 | 7 | jobs: 8 | security-audit: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Checkout Repository 13 | uses: actions/checkout@v4 14 | - name: Run Audit-Check 15 | uses: rustsec/audit-check@v1.3.2 16 | with: 17 | token: {{ "${{ secrets.GITHUB_TOKEN " }}}} 18 | -------------------------------------------------------------------------------- /rust/.github/workflows/bench.yml: -------------------------------------------------------------------------------- 1 | name: 📈 Benchmark 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | 7 | pull_request: 8 | branches: [ '**' ] 9 | 10 | concurrency: 11 | group: {{ "${{ github.workflow " }}}}-{{ "${{ github.ref " }}}} 12 | cancel-in-progress: true 13 | 14 | jobs: 15 | benchmark: 16 | runs-on: ubuntu-latest 17 | 18 | steps:{% if axum %} 19 | - name: Install Protoc 20 | uses: arduino/setup-protoc@v1 21 | with: 22 | repo-token: {{ "${{ secrets.GITHUB_TOKEN " }}}}{% endif %} 23 | - name: Checkout Repository 24 | uses: actions/checkout@v4 25 | 26 | - name: Install Rust Toolchain 27 | uses: actions-rs/toolchain@v1 28 | with: 29 | override: true 30 | toolchain: stable 31 | 32 | - name: Cache Project 33 | uses: Swatinem/rust-cache@v2 34 | 35 | - name: Run Benchmark 36 | run: cargo bench --features test_utils -- --output-format bencher | tee output.txt 37 | 38 | - name: Upload Benchmark Result Artifact 39 | uses: actions/upload-artifact@v3 40 | with: 41 | name: bench_result 42 | path: output.txt 43 | 44 | - name: Create gh-pages Branch 45 | uses: peterjgrainger/action-create-branch@v2.4.0 46 | env: 47 | GITHUB_TOKEN: {{ "${{ secrets.GITHUB_TOKEN " }}}} 48 | with: 49 | branch: gh-pages 50 | 51 | - name: Store Benchmark Result 52 | uses: benchmark-action/github-action-benchmark@v1 53 | with: 54 | name: Rust Benchmark 55 | tool: 'cargo' 56 | output-file-path: output.txt 57 | github-token: {{ "${{ secrets.GITHUB_TOKEN " }}}} 58 | auto-push: {{ "${{ github.event_name == 'push' " }}&& github.repository == '{{github-name}}/{{repo-name}}' && github.ref == 'refs/heads/main' }} 59 | alert-threshold: '200%' 60 | comment-on-alert: true 61 | fail-on-alert: true 62 | alert-comment-cc-users: '@{{github-codeowner}}' 63 | -------------------------------------------------------------------------------- /rust/.github/workflows/coverage.yml: -------------------------------------------------------------------------------- 1 | name: ☂ Code Coverage 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | 7 | pull_request: 8 | branches: [ '**' ] 9 | 10 | concurrency: 11 | group: {{ "${{ github.workflow " }}}}-{{ "${{ github.ref " }}}} 12 | cancel-in-progress: true 13 | 14 | jobs: 15 | coverage: 16 | runs-on: ubuntu-latest 17 | 18 | steps:{% if axum %} 19 | - name: Install Protoc 20 | uses: arduino/setup-protoc@v1 21 | with: 22 | repo-token: {{ "${{ secrets.GITHUB_TOKEN " }}}}{% endif %} 23 | - name: Checkout Repository 24 | uses: actions/checkout@v4 25 | 26 | - name: Install Rust Toolchain 27 | uses: actions-rs/toolchain@v1 28 | with: 29 | override: true 30 | toolchain: nightly 31 | components: llvm-tools-preview 32 | profile: minimal 33 | 34 | - name: Cache Project 35 | uses: Swatinem/rust-cache@v2 36 | 37 | - name: Generate Code coverage 38 | env: 39 | CARGO_INCREMENTAL: '0' 40 | LLVM_PROFILE_FILE: "{{project-name}}-%p-%m.profraw" 41 | RUSTFLAGS: '-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off' 42 | RUSTDOCFLAGS: '-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off' 43 | run: cargo test --all-features 44 | 45 | - name: Install grcov 46 | run: "curl -L https://github.com/mozilla/grcov/releases/download/v0.8.12/grcov-x86_64-unknown-linux-gnu.tar.bz2 | tar jxf -" 47 | 48 | - name: Run grcov 49 | run: "./grcov . --llvm --binary-path target/debug/ -s . -t lcov --branch --ignore-not-existing --ignore '/*' -o lcov.info" 50 | 51 | - name: Install covfix 52 | uses: actions-rs/install@v0.1 53 | with: 54 | crate: rust-covfix 55 | use-tool-cache: true 56 | 57 | - name: Run covfix 58 | run: rust-covfix lcov.info -o lcov.info --verbose 59 | 60 | - name: Upload to codecov.io 61 | uses: codecov/codecov-action@v3 62 | continue-on-error: true 63 | with: 64 | token: {{ "${{ secrets.CODECOV_TOKEN " }}}} 65 | fail_ci_if_error: false 66 | files: lcov.info 67 | -------------------------------------------------------------------------------- /rust/.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: 🐳 Docker 2 | 3 | on: 4 | pull_request: 5 | branches: [ '**' ] 6 | 7 | concurrency: 8 | group: {{ "${{ github.workflow " }}}}-{{ "${{ github.ref " }}}} 9 | cancel-in-progress: true 10 | 11 | jobs: 12 | build-docker: 13 | runs-on: ubuntu-latest 14 | if: {{ "${{ github.event_name == 'pull_request' " }}}} 15 | 16 | env: 17 | DOCKER_BUILDKIT: 1 18 | 19 | steps: 20 | - name: Checkout Repository 21 | uses: actions/checkout@v4 22 | 23 | # https://github.com/docker/setup-qemu-action 24 | - name: Setup QEMU 25 | uses: docker/setup-qemu-action@v2 26 | 27 | # https://github.com/docker/setup-buildx-action 28 | - name: Setup Buildx 29 | uses: docker/setup-buildx-action@v2 30 | with: 31 | buildkitd-flags: "--debug" 32 | 33 | - name: Login to GitHub Container Registry 34 | uses: docker/login-action@v2 35 | with: 36 | registry: ghcr.io 37 | username: {{ "${{ github.repository_owner " }}}} 38 | password: {{ "${{ secrets.GITHUB_TOKEN " }}}} 39 | 40 | - name: Docker Build 41 | uses: docker/build-push-action@v4 42 | with:{% if dockerbuild == "glibc" %} 43 | build-args: | 44 | RUST_BUILD_IMG=rust:1.65-slim-bullseye 45 | DEBIAN_TAG=bullseye-slim{% endif %} 46 | cache-from: type=registry,ref=ghcr.io/{{ "${{ github.repository_owner " }}}}/{{project-name}}:latest 47 | cache-to: type=registry,ref=ghcr.io/{{ "${{ github.repository_owner " }}}}/{{project-name}}:latest,mode=max 48 | context: .{% if dockerbuild == "glibc" %} 49 | # We don't add `linux/arm64` here, as it can cause GitHub runners to 50 | # stall for too long. 51 | platforms: linux/amd64 52 | {% else %} 53 | platforms: linux/amd64, linux/arm64{% endif %} 54 | push: false 55 | tags: | 56 | {{ "${{ github.repository_owner " }}}}/{{project-name}}:latest 57 | -------------------------------------------------------------------------------- /rust/.github/workflows/tests_and_checks.yml: -------------------------------------------------------------------------------- 1 | name: 🧪 Tests and Checks 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | 7 | pull_request: 8 | branches: [ '**' ] 9 | 10 | concurrency: 11 | group: {{ "${{ github.workflow " }}}}-{{ "${{ github.ref " }}}} 12 | cancel-in-progress: true 13 | 14 | jobs: 15 | run-checks: 16 | runs-on: ubuntu-latest 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | rust-toolchain: 21 | - stable 22 | - nightly 23 | # minimum version 24 | - "1.75" 25 | steps:{% if axum %} 26 | - name: Install Protoc 27 | uses: arduino/setup-protoc@v1 28 | with: 29 | repo-token: {{ "${{ secrets.GITHUB_TOKEN " }}}}{% endif %} 30 | - name: Checkout Repository 31 | uses: actions/checkout@v4 32 | 33 | # Smarter caching action, speeds up build times compared to regular cache: 34 | # https://github.com/Swatinem/rust-cache 35 | - name: Cache Project 36 | uses: Swatinem/rust-cache@v2 37 | 38 | # Widely adopted suite of Rust-specific boilerplate actions, especially 39 | # toolchain/cargo use: https://actions-rs.github.io/ 40 | - name: Install Rust Toolchain 41 | uses: actions-rs/toolchain@v1 42 | with: 43 | override: true 44 | components: rustfmt, clippy 45 | toolchain: {{ "${{ matrix.rust-toolchain "}}}} 46 | 47 | - name: Check Format 48 | uses: actions-rs/cargo@v1 49 | with: 50 | args: --all -- --check 51 | command: fmt 52 | toolchain: {{ "${{ matrix.rust-toolchain "}}}} 53 | 54 | - name: Run Linter 55 | uses: actions-rs/cargo@v1 56 | with: 57 | args: --all -- -D warnings 58 | command: clippy 59 | toolchain: {{ "${{ matrix.rust-toolchain "}}}} 60 | 61 | # Check for security advisories 62 | - name: Check Advisories 63 | if: {{ "${{ matrix.rust-toolchain == 'stable' "}}}} 64 | uses: EmbarkStudios/cargo-deny-action@v1 65 | with: 66 | command: check advisories 67 | continue-on-error: true 68 | 69 | # Audit licenses, unreleased crates, and unexpected duplicate versions. 70 | - name: Check Bans, Licenses, and Sources 71 | if: {{ "${{ matrix.rust-toolchain == 'stable' "}}}} 72 | uses: EmbarkStudios/cargo-deny-action@v1 73 | with: 74 | command: check bans licenses sources 75 | 76 | # Only "test" release build on push event. 77 | - name: Test Release 78 | if: {{ "${{ matrix.rust-toolchain == 'stable' " }} && github.event_name == 'push' }} 79 | run: cargo build --release 80 | 81 | run-tests: 82 | runs-on: ubuntu-latest 83 | strategy: 84 | fail-fast: false 85 | matrix: 86 | rust-toolchain: 87 | - stable 88 | - nightly 89 | steps:{% if axum %} 90 | - name: Install Protoc 91 | uses: arduino/setup-protoc@v1 92 | with: 93 | repo-token: {{ "${{ secrets.GITHUB_TOKEN " }}}}{% endif %} 94 | - name: Checkout Repository 95 | uses: actions/checkout@v4 96 | 97 | - name: Install Environment Packages 98 | run: | 99 | sudo apt-get update -qqy 100 | sudo apt-get install jq 101 | 102 | - name: Cache Project 103 | uses: Swatinem/rust-cache@v2 104 | 105 | - name: Install Rust Toolchain 106 | uses: actions-rs/toolchain@v1 107 | with: 108 | override: true 109 | toolchain: {{ "${{ matrix.rust-toolchain "}}}} 110 | 111 | - name: Run Tests 112 | run: cargo test --all-features 113 | -------------------------------------------------------------------------------- /rust/.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | {% if crate_type == "lib" %}Cargo.lock{% endif %} 5 | # These are backup files generated by rustfmt 6 | **/*.rs.bk 7 | {% if nix %} 8 | # Ignore local environment settings 9 | .envrc.custom 10 | .direnv 11 | {% endif %} 12 | # Other files + dirs 13 | private 14 | *.temp 15 | *.tmp 16 | .history 17 | .DS_Store 18 | -------------------------------------------------------------------------------- /rust/.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # pre-commit install 3 | # pre-commit install --hook-type commit-msg 4 | exclude: ^(LICENSE|LICENSE*) 5 | repos: 6 | - repo: local 7 | hooks: 8 | - id: fmt 9 | name: fmt 10 | description: Format rust files. 11 | entry: {% if nix %}cargo fmt{% else %}cargo +nightly fmt{% endif %} 12 | language: system 13 | types: [rust] 14 | args: ["--", "--check"] 15 | - id: cargo-check 16 | name: cargo check 17 | description: Check the package for errors. 18 | entry: cargo check 19 | language: system 20 | types: [rust] 21 | pass_filenames: false 22 | - id: clippy 23 | name: clippy 24 | description: Lint via clippy 25 | entry: cargo clippy 26 | language: system 27 | args: ["--", "-D", "warnings"] 28 | types: [rust] 29 | pass_filenames: false{% if nix %} 30 | - id: alejandra 31 | name: alejandra 32 | description: Format nix files. 33 | entry: alejandra 34 | files: \.nix$ 35 | language: system{% endif %} 36 | 37 | - repo: https://github.com/DevinR528/cargo-sort 38 | rev: v1.0.9 39 | hooks: 40 | - id: cargo-sort 41 | args: [] 42 | 43 | - repo: https://github.com/compilerla/conventional-pre-commit 44 | rev: v2.1.1 45 | hooks: 46 | - id: conventional-pre-commit 47 | stages: 48 | - commit-msg 49 | 50 | - repo: https://github.com/pre-commit/pre-commit-hooks 51 | rev: v4.3.0 52 | hooks: 53 | - id: no-commit-to-branch 54 | args: ["-b", "main"] 55 | - id: check-merge-conflict 56 | - id: trailing-whitespace 57 | - id: end-of-file-fixer 58 | - id: check-yaml 59 | - id: check-json 60 | - id: check-added-large-files 61 | - id: detect-private-key 62 | - id: check-executables-have-shebangs 63 | - id: check-toml 64 | -------------------------------------------------------------------------------- /rust/.release-please-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | ".": "0.1.0" 3 | } 4 | -------------------------------------------------------------------------------- /rust/.rustfmt.toml: -------------------------------------------------------------------------------- 1 | edition = "2021" 2 | imports_granularity = "Crate" 3 | -------------------------------------------------------------------------------- /rust/CHANGELOG.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fission-codes/rust-template/64a9bfd65ed4e8fe39249df0784d741cf4838276/rust/CHANGELOG.md -------------------------------------------------------------------------------- /rust/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | -------------------------------------------------------------------------------- /rust/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "{{project-name}}" 3 | version = "0.1.0" 4 | description = "{{description}}" 5 | keywords = [] 6 | categories = []{% if license == "Apache" %} 7 | {% if crate_type == "lib" %}include = ["/src", "/examples"{% if bench %}, "/benches"{% endif %}, "README.md", "LICENSE"]{% else %} 8 | include = ["/src"{% if bench %}, "/benches"{% endif %}, "README.md", "LICENSE"]{% endif %} 9 | license = "Apache-2.0" 10 | {% elsif license == "MIT" %} 11 | {% if crate_type == "lib" %}include = ["/src", "/examples"{% if bench %}, "/benches"{% endif %}, "README.md", "LICENSE"]{% else %} 12 | include = ["/src"{% if bench %}, "/benches"{% endif %}, "README.md", "LICENSE"]{% endif %} 13 | license = "MIT" 14 | {% else %} 15 | {% if crate_type == "lib" %}include = ["/src", "/examples"{% if bench %}, "/benches"{% endif %}, "README.md", "LICENSE-APACHE", "LICENSE-MIT"]{% else %} 16 | include = ["/src"{% if bench %}, "/benches"{% endif %}, "README.md", "LICENSE-APACHE", "LICENSE-MIT"]{% endif %} 17 | license = "Apache-2.0 or MIT" 18 | {% endif %}readme = "README.md" 19 | edition = "2021" 20 | rust-version = "1.67" 21 | documentation = "https://docs.rs/{{project-name}}" 22 | repository = "https://github.com/{{github-name}}/{{repo-name}}" 23 | authors = ["{{authors}}"] 24 | {% if crate_type == "lib" %} 25 | [lib] 26 | path = "src/lib.rs"{% if bench %} 27 | bench = false{% endif %} 28 | {% else %} 29 | [lib] 30 | path = "src/lib.rs"{% if bench %} 31 | bench = false{% endif %} 32 | doctest = true 33 | 34 | [[bin]] 35 | name = "{{project-name}}" 36 | path = "src/main.rs" 37 | doc = false{% if bench %} 38 | bench = false{% endif %} 39 | {% endif %}{% if bench %} 40 | [[bench]] 41 | name = "a_benchmark" 42 | harness = false 43 | required-features = ["test_utils"] 44 | {% endif %}{% if crate_type == "lib" %} 45 | [[example]] 46 | name = "counterparts" 47 | path = "examples/counterparts.rs" 48 | {% endif %} 49 | [dependencies]{% if crate_type == "bin" %} 50 | anyhow = "1.0"{% endif %}{% if bench %} 51 | proptest = { version = "1.1", optional = true }{% endif %}{% if crate_type == "lib" %} 52 | thiserror = "1.0" 53 | tracing = "0.1"{% else %} 54 | tracing = "0.1" 55 | tracing-subscriber = "0.3"{% endif %} 56 | {% if bench %} 57 | [dev-dependencies] 58 | criterion = "0.4" 59 | proptest = "1.1" 60 | {% endif %} 61 | [features] 62 | default = []{% if bench %} 63 | test_utils = ["proptest"]{% endif %} 64 | 65 | [metadata.docs.rs] 66 | all-features = true 67 | # defines the configuration attribute `docsrs` 68 | rustdoc-args = ["--cfg", "docsrs"] 69 | # 70 | # See https://doc.rust-lang.org/cargo/reference/profiles.html for more info. 71 | # [profile.release] 72 | # Do not perform backtrace for panic on release builds. 73 | ## panic = 'abort' 74 | # Perform optimizations on all codegen units. 75 | ## codegen-units = 1 76 | # Tell `rustc` to optimize for small code size. 77 | ## opt-level = "s" # or 'z' to optimize "aggressively" for size 78 | # Enable link time optimization. 79 | ## lto = true 80 | # Amount of debug information. 81 | # 0/false: no debug info at all; 1: line tables only; 2/true: full debug info 82 | ## debug = false 83 | # Strip debug symbols 84 | ## strip = "symbols" 85 | 86 | # Speedup build on macOS 87 | # See https://blog.rust-lang.org/2021/03/25/Rust-1.51.0.html#splitting-debug-information 88 | [profile.dev] 89 | split-debuginfo = "unpacked" 90 | -------------------------------------------------------------------------------- /rust/Dockerfile.glibc: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | ARG RUST_BUILD_IMG=rust:1.67-slim-bullseye 3 | ARG DEBIAN_TAG=bullseye-slim 4 | 5 | FROM $RUST_BUILD_IMG as base 6 | 7 | # AMD64 8 | FROM --platform=$BUILDPLATFORM base as builder-amd64 9 | ARG TARGET="x86_64-unknown-linux-gnu" 10 | 11 | # ARM64 12 | FROM --platform=$BUILDPLATFORM base as builder-arm64 13 | ARG TARGET="aarch64-unknown-linux-gnu" 14 | 15 | FROM builder-$TARGETARCH as builder 16 | 17 | RUN adduser --disabled-password --disabled-login --gecos "" --no-create-home {{project-name}} 18 | RUN apt update && apt install -y g++{% if axum %} build-essential protobuf-compiler{% endif %} 19 | RUN rustup target add $TARGET 20 | 21 | RUN cargo init {{project-name}} 22 | 23 | WORKDIR /{{project-name}} 24 | 25 | # touch lib.rs as we combine both 26 | Run touch src/lib.rs 27 | {% if bench %} 28 | # touch benches as it's part of Cargo.toml 29 | RUN mkdir benches 30 | RUN touch benches/a_benchmark.rs 31 | {% endif %} 32 | # copy cargo.* 33 | COPY Cargo.lock ./Cargo.lock 34 | COPY Cargo.toml ./Cargo.toml 35 | 36 | # cache depencies 37 | RUN mkdir .cargo 38 | RUN cargo vendor > .cargo/config 39 | RUN --mount=type=cache,target=$CARGO_HOME/registry \ 40 | --mount=type=cache,target=$CARGO_HOME/.git \ 41 | --mount=type=cache,target={{project-name}}/target,sharing=locked \{% if axum %} 42 | cargo build --target $TARGET --bin {{project-name}}-app --release{% else %} 43 | cargo build --target $TARGET --release{% endif %} 44 | 45 | COPY src ./src 46 | # copy src 47 | COPY src ./src{% if bench %} 48 | # copy benches 49 | COPY benches ./benches 50 | {% endif %}{% if axum %} 51 | # copy config 52 | COPY config ./config 53 | {% endif %} 54 | # final build for release 55 | RUN rm ./target/$TARGET/release/deps/*{{crate_name}}* 56 | RUN{% if axum %} cargo build --target $TARGET --bin {{project-name}}-app --release{% else %} cargo build --target $TARGET --bin {{project-name}} --release{% endif %} 57 | {% if axum %} 58 | RUN strip ./target/$TARGET/release/{{project-name}}-app 59 | {% else %} 60 | RUN strip ./target/$TARGET/release/{{project-name}}{% endif %} 61 | RUN mv ./target/$TARGET/release/{{project-name}}* /usr/local/bin{% if axum %} 62 | RUN mv ./config /etc/config{% endif %} 63 | 64 | FROM debian:${DEBIAN_TAG} 65 | 66 | ARG backtrace=0 67 | ARG log_level=info 68 | 69 | ENV RUST_BACKTRACE=${backtrace} \ 70 | RUST_LOG=${log_level} 71 | 72 | COPY --from=builder /usr/local/bin/{{project-name}}* .{% if axum %} 73 | COPY --from=builder /etc/config ./config{% endif %} 74 | COPY --from=builder /etc/passwd /etc/passwd 75 | COPY --from=builder /etc/group /etc/group 76 | 77 | USER {{project-name}}:{{project-name}} 78 | {% if axum %} 79 | EXPOSE {{port}} 80 | EXPOSE {{metricsport}} 81 | ENTRYPOINT ["./{{project-name}}-app"]{% else %}ENTRYPOINT ["./{{project-name}}"]{% endif %} 82 | -------------------------------------------------------------------------------- /rust/Dockerfile.musl: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | # AMD64 4 | FROM --platform=$BUILDPLATFORM messense/rust-musl-cross:x86_64-musl as builder-amd64 5 | 6 | # ARM64 7 | FROM --platform=$BUILDPLATFORM messense/rust-musl-cross:aarch64-musl as builder-arm64 8 | 9 | ARG TARGETARCH 10 | FROM builder-$TARGETARCH as builder 11 | {% if axum %} 12 | RUN apt update && apt install -y protobuf-compiler 13 | {% endif %} 14 | RUN adduser --disabled-password --disabled-login --gecos "" --no-create-home {{project-name}} 15 | 16 | RUN cargo init 17 | 18 | # touch lib.rs as we combine both 19 | RUN touch src/lib.rs 20 | {% if bench %} 21 | # touch benches as it's part of Cargo.toml 22 | RUN mkdir benches 23 | RUN touch benches/a_benchmark.rs 24 | {% endif %} 25 | # copy cargo.* 26 | COPY Cargo.lock ./Cargo.lock 27 | COPY Cargo.toml ./Cargo.toml 28 | 29 | # cache depencies 30 | RUN mkdir .cargo 31 | RUN cargo vendor > .cargo/config 32 | RUN --mount=type=cache,id=cargo,target=$CARGO_HOME/registry \ 33 | --mount=type=cache,id=git,target=$CARGO_HOME/.git \ 34 | --mount=type=cache,id=target,target=./{{project-name}}/target,sharing=locked \{% if axum %} 35 | cargo build --target $CARGO_BUILD_TARGET --bin {{project-name}}-app --release{% else %} 36 | cargo build --target $CARGO_BUILD_TARGET --release{% endif %} 37 | 38 | # copy src 39 | COPY src ./src{% if bench %} 40 | # copy benches 41 | COPY benches ./benches 42 | {% endif %}{% if axum %} 43 | # copy config 44 | COPY config ./config 45 | {% endif %} 46 | # final build for release 47 | RUN rm ./target/$CARGO_BUILD_TARGET/release/deps/*{{crate_name}}* 48 | RUN{% if axum %} cargo build --target $CARGO_BUILD_TARGET --bin {{project-name}}-app --release{% else %} cargo build --target $CARGO_BUILD_TARGET --bin {{project-name}} --release{% endif %} 49 | {% if axum %} 50 | RUN musl-strip ./target/$CARGO_BUILD_TARGET/release/{{project-name}}-app 51 | {% else %} 52 | RUN musl-strip ./target/$CARGO_BUILD_TARGET/release/{{project-name}}{% endif %} 53 | RUN mv ./target/$CARGO_BUILD_TARGET/release/{{project-name}}* /usr/local/bin{% if axum %} 54 | RUN mv ./config /etc/config{% endif %} 55 | 56 | FROM scratch 57 | 58 | ARG backtrace=0 59 | ARG log_level=info 60 | 61 | ENV RUST_BACKTRACE=${backtrace} \ 62 | RUST_LOG=${log_level} 63 | 64 | COPY --from=builder /usr/local/bin/{{project-name}}* .{% if axum %} 65 | COPY --from=builder /etc/config ./config{% endif %} 66 | COPY --from=builder /etc/passwd /etc/passwd 67 | COPY --from=builder /etc/group /etc/group 68 | 69 | USER {{project-name}}:{{project-name}} 70 | {% if axum %} 71 | EXPOSE {{port}} 72 | EXPOSE {{metricsport}} 73 | ENTRYPOINT ["./{{project-name}}-app"]{% else %}ENTRYPOINT ["./{{project-name}}"]{% endif %} 74 | -------------------------------------------------------------------------------- /rust/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any 2 | person obtaining a copy of this software and associated 3 | documentation files (the "Software"), to deal in the 4 | Software without restriction, including without 5 | limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following 9 | conditions: 10 | 11 | The above copyright notice and this permission notice 12 | shall be included in all copies or substantial portions 13 | of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /rust/SECURITY.md: -------------------------------------------------------------------------------- 1 | ## Report a security issue or vulnerability 2 | 3 | The {{project-name}} team welcomes security reports and is committed to 4 | providing prompt attention to security issues. Security issues should be 5 | reported privately via [{{github-email}}][support-email]. Security issues should 6 | not be reported via the public GitHub Issue tracker. 7 | 8 | ## Security advisories 9 | 10 | The project team is committed to transparency in the security issue disclosure 11 | process. The {{project-name}} team announces security advisories through our 12 | Github respository's [security portal][sec-advisories] and and the 13 | [RustSec advisory database][rustsec-db]. 14 | 15 | [rustsec-db]: https://github.com/RustSec/advisory-db 16 | [sec-advisories]: https://github.com/{{github-name}}/{{repo-name}}/security/advisories 17 | [support-email]: mailto:{{github-email}} 18 | -------------------------------------------------------------------------------- /rust/assets/a_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fission-codes/rust-template/64a9bfd65ed4e8fe39249df0784d741cf4838276/rust/assets/a_logo.png -------------------------------------------------------------------------------- /rust/benches/a_benchmark.rs: -------------------------------------------------------------------------------- 1 | use criterion::{criterion_group, criterion_main, Criterion}; 2 | 3 | pub fn add_benchmark(c: &mut Criterion) { 4 | let mut rvg = {{crate_name}}::test_utils::Rvg::deterministic(); 5 | let int_val_1 = rvg.sample(&(0..100i32)); 6 | let int_val_2 = rvg.sample(&(0..100i32)); 7 | 8 | c.bench_function("add", |b| { 9 | b.iter(|| { 10 | {{crate_name}}::add(int_val_1, int_val_2); 11 | }) 12 | }); 13 | } 14 | 15 | criterion_group!(benches, add_benchmark); 16 | criterion_main!(benches); 17 | -------------------------------------------------------------------------------- /rust/codecov.yml: -------------------------------------------------------------------------------- 1 | ignore: 2 | - "tests"{% if bench %} 3 | - "benches"{% endif %}{% if crate_type == "lib" %} 4 | - "examples"{% endif %} 5 | 6 | comment: 7 | layout: "reach, diff, flags, files" 8 | require_changes: true 9 | 10 | github_checks: 11 | annotations: false 12 | 13 | coverage: 14 | status: 15 | project: 16 | default: 17 | threshold: 5% 18 | -------------------------------------------------------------------------------- /rust/config/settings.toml: -------------------------------------------------------------------------------- 1 | [monitoring] 2 | process_collector_interval = 10 3 | 4 | [otel] 5 | exporter_otlp_endpoint = "http://localhost:4317" 6 | 7 | [server] 8 | environment = "local" 9 | metrics_port = {{metricsport}} 10 | port = {{port}} 11 | timeout_ms = 30000 12 | -------------------------------------------------------------------------------- /rust/docs/specs/latest.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.3", 3 | "info": { 4 | "title": "{{project-name}}", 5 | "description": "{{description}}", 6 | "contact": { 7 | "name": "Zeeshan Lakhani", 8 | "email": "zeeshan.lakhani@gmail.com" 9 | }, 10 | "license": { 11 | "name": {% if license == "Apache" %}"Apache-2.0"{% elsif license == "MIT" %}"MIT"{% else %}"Apache-2.0 or MIT"{% endif %} 12 | }, 13 | "version": "0.1.0" 14 | }, 15 | "paths": { 16 | "/healthcheck": { 17 | "get": { 18 | "tags": [ 19 | "health" 20 | ], 21 | "summary": "GET handler for checking service health.", 22 | "description": "GET handler for checking service health.", 23 | "operationId": "healthcheck", 24 | "responses": { 25 | "200": { 26 | "description": "{{project-name}} healthy" 27 | }, 28 | "500": { 29 | "description": "{{project-name}} not healthy", 30 | "content": { 31 | "application/json": { 32 | "schema": { 33 | "$ref": "#/components/schemas/AppError" 34 | } 35 | } 36 | } 37 | } 38 | }, 39 | "deprecated": false 40 | } 41 | }, 42 | "/ping": { 43 | "get": { 44 | "tags": [ 45 | "ping" 46 | ], 47 | "summary": "GET handler for internal pings and availability", 48 | "description": "GET handler for internal pings and availability", 49 | "operationId": "get", 50 | "responses": { 51 | "200": { 52 | "description": "Ping successful" 53 | }, 54 | "500": { 55 | "description": "Ping not successful", 56 | "content": { 57 | "application/json": { 58 | "schema": { 59 | "$ref": "#/components/schemas/AppError" 60 | } 61 | } 62 | } 63 | } 64 | }, 65 | "deprecated": false 66 | } 67 | } 68 | }, 69 | "components": { 70 | "schemas": { 71 | "AppError": { 72 | "type": "object", 73 | "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", 74 | "required": [ 75 | "status" 76 | ], 77 | "properties": { 78 | "detail": { 79 | "type": "string" 80 | }, 81 | "status": { 82 | "type": "integer", 83 | "format": "int32", 84 | "example": 200 85 | }, 86 | "title": { 87 | "type": "string" 88 | } 89 | } 90 | } 91 | } 92 | }, 93 | "tags": [ 94 | { 95 | "name": "", 96 | "description": "{{project-name}} service/middleware" 97 | } 98 | ] 99 | } 100 | -------------------------------------------------------------------------------- /rust/examples/counterparts.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | 3 | pub fn main() -> Result<(), Box> { 4 | println!("Alien Shore!"); 5 | Ok(()) 6 | } 7 | -------------------------------------------------------------------------------- /rust/final-msg.rhai: -------------------------------------------------------------------------------- 1 | print(); 2 | print(); 3 | print("\ 4 | For CI/CD purposes, be aware there's some secrets you'll need to configure in Github, including:\n\ 5 | `CODECOV_TOKEN` if you choose to use coverage via Codecov and have it set up;\n\ 6 | `CARGO_REGISTRY_TOKEN` for publshing Rust packages to crates.io;\n\ 7 | `DOCKERHUB_USERNAME` and `DOCKERHUB_TOKEN` for pushing containers to Docker Hub."); 8 | print(); 9 | -------------------------------------------------------------------------------- /rust/final-script.rhai: -------------------------------------------------------------------------------- 1 | if variable::is_set("axum") && variable::get("axum") { 2 | file::rename("src.axum", "src"); 3 | file::rename("tests/integration_test.axum.rs", "tests/integration_test.rs"); 4 | file::rename("Cargo.axum.toml", "Cargo.toml"); 5 | file::rename("README.axum.md", "README.md"); 6 | } 7 | -------------------------------------------------------------------------------- /rust/flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "{{project-name}}"; 3 | 4 | inputs = { 5 | nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; 6 | flake-utils.url = "github:numtide/flake-utils"; 7 | 8 | flake-compat = { 9 | url = "github:edolstra/flake-compat"; 10 | flake = false; 11 | }; 12 | 13 | rust-overlay = { 14 | url = "github:oxalica/rust-overlay"; 15 | inputs.nixpkgs.follows = "nixpkgs"; 16 | inputs.flake-utils.follows = "flake-utils"; 17 | }; 18 | }; 19 | 20 | outputs = { 21 | self, 22 | nixpkgs, 23 | flake-compat, 24 | flake-utils, 25 | rust-overlay, 26 | } @ inputs: 27 | flake-utils.lib.eachDefaultSystem ( 28 | system: let 29 | overlays = [(import rust-overlay)]; 30 | pkgs = import nixpkgs {inherit system overlays;}; 31 | 32 | rust-toolchain = (pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml).override { 33 | extensions = ["cargo" "clippy" "rustfmt" "rust-src" "rust-std"]; 34 | }; 35 | 36 | nightly-rustfmt = pkgs.rust-bin.nightly.latest.rustfmt; 37 | 38 | format-pkgs = with pkgs; [ 39 | nixpkgs-fmt 40 | alejandra 41 | ]; 42 | 43 | cargo-installs = with pkgs; [{% if auditable %} 44 | cargo-audit 45 | cargo-auditable{% endif %} 46 | cargo-deny 47 | cargo-expand 48 | cargo-nextest 49 | cargo-outdated 50 | cargo-spellcheck 51 | cargo-sort 52 | cargo-udeps 53 | cargo-watch 54 | ]; 55 | in rec 56 | { 57 | devShells.default = pkgs.mkShell { 58 | name = "{{project-name}}"; 59 | nativeBuildInputs = with pkgs; 60 | [ 61 | # The ordering of these two items is important. For nightly rustfmt to be used instead of 62 | # the rustfmt provided by `rust-toolchain`, it must appear first in the list. This is 63 | # because native build inputs are added to $PATH in the order they're listed here. 64 | nightly-rustfmt 65 | rust-toolchain 66 | pre-commit 67 | protobuf 68 | direnv 69 | self.packages.${system}.irust 70 | ] 71 | ++ format-pkgs 72 | ++ cargo-installs 73 | ++ lib.optionals stdenv.isDarwin [ 74 | darwin.apple_sdk.frameworks.Security 75 | darwin.apple_sdk.frameworks.CoreFoundation 76 | darwin.apple_sdk.frameworks.Foundation 77 | ]; 78 | 79 | shellHook = '' 80 | [ -e .git/hooks/pre-commit ] || pre-commit install --install-hooks && pre-commit install --hook-type commit-msg 81 | ''; 82 | }; 83 | 84 | packages.irust = pkgs.rustPlatform.buildRustPackage rec { 85 | pname = "irust"; 86 | version = "1.70.0"; 87 | src = pkgs.fetchFromGitHub { 88 | owner = "sigmaSd"; 89 | repo = "IRust"; 90 | rev = "v${version}"; 91 | sha256 = "sha256-chZKesbmvGHXwhnJRZbXyX7B8OwJL9dJh0O1Axz/n2E="; 92 | }; 93 | 94 | doCheck = false; 95 | cargoSha256 = "sha256-FmsD3ajMqpPrTkXCX2anC+cmm0a2xuP+3FHqzj56Ma4="; 96 | }; 97 | 98 | formatter = pkgs.alejandra; 99 | } 100 | ); 101 | } 102 | -------------------------------------------------------------------------------- /rust/init-msg.rhai: -------------------------------------------------------------------------------- 1 | if variable::get("is_init") { 2 | print(); 3 | print("\ 4 | If the generator detects a directory or file conflict, it will not alter your project,\n\ 5 | so carefully read the prompts."); 6 | print(); 7 | } 8 | -------------------------------------------------------------------------------- /rust/pre-script.rhai: -------------------------------------------------------------------------------- 1 | let license = variable::get("license"); 2 | 3 | while switch license.to_upper() { 4 | "APACHE" => { 5 | file::delete("LICENSE-MIT"); 6 | file::rename("LICENSE-APACHE", "LICENSE"); 7 | false 8 | } 9 | "MIT" => { 10 | file::delete("LICENSE-APACHE"); 11 | file::rename("LICENSE-MIT", "LICENSE"); 12 | false 13 | } 14 | "DUAL" => false, 15 | "USE EXISTING" => { 16 | file::delete("LICENSE-MIT"); 17 | file::delete("LICENSE-APACHE"); 18 | false 19 | }, 20 | _ => true, 21 | } { 22 | license = variable::prompt("What license to use?", "dual", [ 23 | "Apache", 24 | "MIT", 25 | "dual", 26 | "use existing" 27 | ]); 28 | } 29 | variable::set("license", license); 30 | 31 | if variable::is_set("docker") && variable::get("docker") { 32 | let dockerbuild = variable::get("dockerbuild"); 33 | while switch dockerbuild.to_upper() { 34 | "GLIBC" => { 35 | file::delete("Dockerfile.musl"); 36 | file::rename("Dockerfile.glibc", "Dockerfile"); 37 | false 38 | } 39 | "MUSL" => { 40 | file::delete("Dockerfile.glibc"); 41 | file::rename("Dockerfile.musl", "Dockerfile"); 42 | false 43 | } 44 | _ => true, 45 | } { 46 | dockerbuild = variable::prompt("For docker, do you want a glibc or musl build?", "musl", [ 47 | "musl", 48 | "glibc" 49 | ]); 50 | } 51 | variable::set("dockerbuild", dockerbuild); 52 | } 53 | 54 | // Set for Copyright Info 55 | variable::set("year", "2023"); 56 | let name = variable::get("github-name").to_title_case(); 57 | variable::set("copyright-owner", name); 58 | -------------------------------------------------------------------------------- /rust/release-please-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [], 3 | "changelog-path": "CHANGELOG.md", 4 | "release-type": "rust", 5 | "bump-minor-pre-major": true, 6 | "bump-patch-for-minor-pre-major": true, 7 | "packages": { 8 | ".": {} 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /rust/rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "{{toolchain}}"{% if nix == false %} 3 | components = ["cargo", "clippy", "rustfmt", "rust-src", "rust-std"]{% endif %} 4 | -------------------------------------------------------------------------------- /rust/src.axum/bin/openapi.rs: -------------------------------------------------------------------------------- 1 | use std::{fs::File, io::prelude::*, path::PathBuf}; 2 | use utoipa::OpenApi; 3 | use {{crate_name}}::docs::ApiDoc; 4 | 5 | fn main() { 6 | let json_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("docs/specs/latest.json"); 7 | let json_path_show = json_path.as_path().display().to_string(); 8 | 9 | let mut file = match File::create(json_path) { 10 | Ok(file) => file, 11 | Err(err) => { 12 | eprintln!("error creating file: {err:?}"); 13 | std::process::exit(1) 14 | } 15 | }; 16 | 17 | let json = match ApiDoc::openapi().to_pretty_json() { 18 | Ok(mut json) => { 19 | json.push('\n'); 20 | json 21 | } 22 | Err(err) => { 23 | eprintln!("error generating OpenAPI json: {err:?}"); 24 | std::process::exit(1) 25 | } 26 | }; 27 | 28 | match file.write_all(json.as_bytes()) { 29 | Ok(_) => println!("OpenAPI json written to path: {json_path_show}\n\n{json}"), 30 | Err(err) => { 31 | eprintln!("error writing to file. {err:?}"); 32 | std::process::exit(1) 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /rust/src.axum/docs.rs: -------------------------------------------------------------------------------- 1 | //! OpenAPI doc generation. 2 | 3 | use crate::{ 4 | error::AppError, 5 | routes::{health, ping}, 6 | }; 7 | use utoipa::OpenApi; 8 | 9 | /// API documentation generator. 10 | #[derive(OpenApi)] 11 | #[openapi( 12 | paths(health::healthcheck, ping::get), 13 | components(schemas(AppError)), 14 | tags( 15 | (name = "", description = "{{project-name}} service/middleware") 16 | ) 17 | )] 18 | 19 | /// Tied to OpenAPI documentation. 20 | #[derive(Debug)] 21 | pub struct ApiDoc; 22 | -------------------------------------------------------------------------------- /rust/src.axum/extract/mod.rs: -------------------------------------------------------------------------------- 1 | //! Custom [axum::extract] Extractors. 2 | 3 | pub mod json; 4 | -------------------------------------------------------------------------------- /rust/src.axum/headers/header.rs: -------------------------------------------------------------------------------- 1 | use axum_extra::{headers::Header, TypedHeader}; 2 | 3 | /// Generate String-focused, generic, custom typed [`Header`]'s. 4 | #[allow(unused)] 5 | macro_rules! header { 6 | ($tname:ident, $hname:ident, $sname:expr) => { 7 | static $hname: once_cell::sync::Lazy = 8 | once_cell::sync::Lazy::new(|| axum_extra::headers::HeaderName::from_static($sname)); 9 | 10 | #[doc = "Generated custom [`axum_extra::headers::Header`] for "] 11 | #[doc = $sname] 12 | #[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] 13 | pub(crate) struct $tname(pub(crate) String); 14 | 15 | impl std::fmt::Display for $tname { 16 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 17 | write!(f, "{}", self.0) 18 | } 19 | } 20 | 21 | impl std::convert::From<&str> for $tname { 22 | fn from(item: &str) -> Self { 23 | $tname(item.to_string()) 24 | } 25 | } 26 | 27 | impl axum_extra::headers::Header for $tname { 28 | fn name() -> &'static axum_extra::headers::HeaderName { 29 | &$hname 30 | } 31 | 32 | fn decode<'i, I>(values: &mut I) -> Result 33 | where 34 | I: Iterator, 35 | { 36 | values 37 | .next() 38 | .and_then(|v| v.to_str().ok()) 39 | .map(|x| $tname(x.to_string())) 40 | .ok_or_else(axum_extra::headers::Error::invalid) 41 | } 42 | 43 | fn encode(&self, values: &mut E) 44 | where 45 | E: Extend, 46 | { 47 | if let Ok(value) = axum_extra::headers::HeaderValue::from_str(&self.0) { 48 | values.extend(std::iter::once(value)); 49 | } 50 | } 51 | } 52 | }; 53 | } 54 | 55 | /// Trait for returning header value directly for passing 56 | /// along to client calls. 57 | #[allow(unused)] 58 | pub(crate) trait HeaderValue { 59 | fn header_value(&self) -> String; 60 | } 61 | 62 | impl HeaderValue for TypedHeader 63 | where 64 | T: Header + std::fmt::Display, 65 | { 66 | fn header_value(&self) -> String { 67 | self.0.to_string() 68 | } 69 | } 70 | 71 | impl HeaderValue for &TypedHeader 72 | where 73 | T: Header + std::fmt::Display, 74 | { 75 | fn header_value(&self) -> String { 76 | self.0.to_string() 77 | } 78 | } 79 | 80 | #[cfg(test)] 81 | pub(crate) mod tests { 82 | use axum::http; 83 | use axum_extra::headers::{Header, HeaderMapExt}; 84 | 85 | header!(XDummyId, XDUMMY_ID, "x-dummy-id"); 86 | 87 | fn test_decode(values: &[&str]) -> Option { 88 | let mut map = http::HeaderMap::new(); 89 | for val in values { 90 | map.append(T::name(), val.parse().unwrap()); 91 | } 92 | map.typed_get() 93 | } 94 | 95 | fn test_encode(header: T) -> http::HeaderMap { 96 | let mut map = http::HeaderMap::new(); 97 | map.typed_insert(header); 98 | map 99 | } 100 | 101 | #[test] 102 | fn test_dummy_header() { 103 | let s = "18312349-3139-498C-84B6-87326BF1F2A7"; 104 | let dummy_id = test_decode::(&[s]).unwrap(); 105 | let headers = test_encode(dummy_id); 106 | assert_eq!(headers["x-dummy-id"], s); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /rust/src.axum/headers/mod.rs: -------------------------------------------------------------------------------- 1 | //! Working-with and generating custom headers. 2 | 3 | pub(crate) mod header; 4 | -------------------------------------------------------------------------------- /rust/src.axum/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(docsrs, feature(doc_cfg))] 2 | #![warn(missing_debug_implementations, missing_docs, rust_2018_idioms)] 3 | #![deny(unreachable_pub)] 4 | #![deny(private_bounds)] 5 | #![deny(rustdoc::private_intra_doc_links)] 6 | #![deny(private_interfaces)] 7 | 8 | //! {{project-name}} 9 | 10 | pub mod docs; 11 | pub mod error; 12 | pub mod extract; 13 | pub mod headers; 14 | pub mod metrics; 15 | pub mod middleware; 16 | pub mod router; 17 | pub mod routes; 18 | pub mod settings; 19 | pub mod tracer; 20 | pub mod tracing_layers; 21 | {% if bench %} 22 | /// Test utilities. 23 | #[cfg(any(test, feature = "test_utils"))] 24 | #[cfg_attr(docsrs, doc(cfg(feature = "test_utils")))] 25 | pub mod test_utils; 26 | /// Add two integers together. 27 | pub fn add(a: i32, b: i32) -> i32 { 28 | a + b 29 | }{% endif %} 30 | -------------------------------------------------------------------------------- /rust/src.axum/metrics/mod.rs: -------------------------------------------------------------------------------- 1 | //! Metrics capture and Prometheus recorder. 2 | 3 | pub mod process; 4 | pub mod prom; 5 | -------------------------------------------------------------------------------- /rust/src.axum/metrics/prom.rs: -------------------------------------------------------------------------------- 1 | //! Metrics Prometheus recorder. 2 | 3 | use crate::metrics::process; 4 | 5 | use metrics_exporter_prometheus::{Matcher, PrometheusBuilder, PrometheusHandle}; 6 | 7 | /// Sets up Prometheus buckets for matched metrics and installs recorder. 8 | pub fn setup_metrics_recorder() -> anyhow::Result { 9 | const EXPONENTIAL_SECONDS: &[f64] = &[ 10 | 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 11 | ]; 12 | 13 | let builder = PrometheusBuilder::new() 14 | .set_buckets_for_metric( 15 | Matcher::Suffix("_duration_seconds".to_string()), 16 | EXPONENTIAL_SECONDS, 17 | )? 18 | .install_recorder()?; 19 | 20 | process::describe(); 21 | 22 | Ok(builder) 23 | } 24 | -------------------------------------------------------------------------------- /rust/src.axum/middleware/client/metrics.rs: -------------------------------------------------------------------------------- 1 | //! Middleware for tracking metrics on each client [reqwest::Request]. 2 | 3 | use http::Extensions; 4 | use reqwest_middleware::Middleware as ReqwestMiddleware; 5 | use std::time::Instant; 6 | 7 | const OK: &str = "ok"; 8 | const ERROR: &str = "error"; 9 | const MIDDLEWARE_ERROR: &str = "middleware_error"; 10 | const NONE: &str = "none"; 11 | const RESULT: &str = "result"; 12 | const STATUS: &str = "status"; 13 | 14 | /// Metrics struct for use as part of middleware. 15 | #[derive(Debug)] 16 | pub struct Metrics { 17 | /// Client name for metric(s) gathering. 18 | pub name: String, 19 | } 20 | 21 | #[async_trait::async_trait] 22 | impl ReqwestMiddleware for Metrics { 23 | async fn handle( 24 | &self, 25 | request: reqwest::Request, 26 | extensions: &mut Extensions, 27 | next: reqwest_middleware::Next<'_>, 28 | ) -> Result { 29 | let now = Instant::now(); 30 | 31 | let url = request.url().clone(); 32 | let request_path = url.path().to_string(); 33 | let method = request.method().clone(); 34 | 35 | let result = next.run(request, extensions).await; 36 | let latency = now.elapsed().as_secs_f64(); 37 | 38 | let labels = vec![ 39 | ("client", self.name.to_string()), 40 | ("method", method.to_string()), 41 | ("request_path", request_path.clone()), 42 | ]; 43 | 44 | let extended_labels = extend_labels_for_response(labels, &result); 45 | 46 | metrics::counter!("client_http_requests_total").increment(1u64); 47 | metrics::counter!("client_http_requests_total", &extended_labels).increment(1u64); 48 | metrics::counter!("client_http_requests_total", "client" => self.name.to_string()) 49 | .increment(1u64); 50 | metrics::counter!("client_http_requests_total", "client" => self.name.to_string(), "request_path" => request_path.clone()).increment(1u64); 51 | metrics::histogram!("client_http_request_duration_seconds").record(latency); 52 | metrics::histogram!("client_http_request_duration_seconds", &extended_labels) 53 | .record(latency); 54 | metrics::histogram!("client_http_request_duration_seconds", "client" => self.name.to_string()).record(latency); 55 | metrics::histogram!("client_http_request_duration_seconds", "client" => self.name.to_string(), "request_path" => request_path.clone()).record(latency); 56 | 57 | result 58 | } 59 | } 60 | 61 | /// Extend a set of metrics label tuples with dynamic properties 62 | /// around reqwest responses for `result` and `status` fields. 63 | pub fn extend_labels_for_response<'a>( 64 | mut labels: Vec<(&'a str, String)>, 65 | result: &Result, 66 | ) -> Vec<(&'a str, String)> { 67 | match result { 68 | Ok(ref success) => { 69 | match success.status().as_u16() { 70 | 200..=299 => labels.push((RESULT, OK.to_string())), 71 | _ => labels.push((RESULT, ERROR.to_string())), 72 | } 73 | 74 | labels.push((STATUS, success.status().as_u16().to_string())); 75 | } 76 | Err(reqwest_middleware::Error::Reqwest(ref err)) => { 77 | labels.push((RESULT, ERROR.to_string())); 78 | labels.push(( 79 | STATUS, 80 | err.status() 81 | .map(|status| status.as_u16().to_string()) 82 | .unwrap_or_else(|| NONE.to_string()), 83 | )); 84 | } 85 | Err(reqwest_middleware::Error::Middleware(ref _err)) => { 86 | labels.push((RESULT, MIDDLEWARE_ERROR.to_string())); 87 | labels.push((STATUS, NONE.to_string())); 88 | } 89 | }; 90 | 91 | labels 92 | } 93 | -------------------------------------------------------------------------------- /rust/src.axum/middleware/client/mod.rs: -------------------------------------------------------------------------------- 1 | //! Middleware for calls to outside client APIs. 2 | 3 | pub mod metrics; 4 | -------------------------------------------------------------------------------- /rust/src.axum/middleware/metrics.rs: -------------------------------------------------------------------------------- 1 | //! Middleware for tracking metrics on each [axum::http::Request]. 2 | 3 | use crate::middleware::request_ext::RequestExt; 4 | use axum::{body::Body, http::Request, middleware::Next, response::IntoResponse}; 5 | use std::time::Instant; 6 | 7 | /// Middleware function called to track (and update) http metrics when a route 8 | /// is requested. 9 | pub async fn track(req: Request, next: Next) -> impl IntoResponse { 10 | let start = Instant::now(); 11 | 12 | let path = req.path().to_string(); 13 | 14 | let res = next.run(req).await; 15 | let latency = start.elapsed().as_secs_f64(); 16 | let status = res.status().as_u16(); 17 | 18 | metrics::counter!("http_requests_total").increment(1); 19 | metrics::counter!("http_requests_total", "request_path" => path.clone() ).increment(1); 20 | metrics::counter!("http_requests_total", "request_path" => path.clone(), "status" => status.to_string() ).increment(1); 21 | metrics::histogram!("http_request_duration_seconds").record(latency); 22 | metrics::histogram!("http_request_duration_seconds", "request_path" => path.clone()) 23 | .record(latency); 24 | metrics::histogram!("http_request_duration_seconds", "request_path" => path.clone(), "status" => status.to_string()).record(latency); 25 | res 26 | } 27 | -------------------------------------------------------------------------------- /rust/src.axum/middleware/mod.rs: -------------------------------------------------------------------------------- 1 | //! Additional [axum::middleware]. 2 | 3 | pub mod client; 4 | pub mod logging; 5 | pub mod metrics; 6 | pub(crate) mod request_ext; 7 | pub mod request_ulid; 8 | pub mod reqwest_retry; 9 | pub mod reqwest_tracing; 10 | pub mod runtime; 11 | -------------------------------------------------------------------------------- /rust/src.axum/middleware/request_ext.rs: -------------------------------------------------------------------------------- 1 | //! Middleware for additional [axum::http::Request] methods. 2 | 3 | use axum::{ 4 | extract::{MatchedPath, OriginalUri}, 5 | http::Request, 6 | }; 7 | 8 | /// Trait for extra methods on [`Request`](axum::http::Request) 9 | pub(crate) trait RequestExt { 10 | /// Parse request path on the request. 11 | fn path(&self) -> String; 12 | } 13 | 14 | impl RequestExt for Request { 15 | fn path(&self) -> String { 16 | if let Some(matched_path) = self.extensions().get::() { 17 | matched_path.as_str().to_string() 18 | } else if let Some(uri) = self.extensions().get::() { 19 | uri.0.path().to_string() 20 | } else { 21 | self.uri().path().to_string() 22 | } 23 | } 24 | } 25 | 26 | #[cfg(test)] 27 | mod test { 28 | use super::*; 29 | use axum::http::Request; 30 | 31 | #[test] 32 | fn parse_path() { 33 | let mut req1: Request<()> = Request::default(); 34 | *req1.uri_mut() = "https://www.rust-lang.org/users/:id".parse().unwrap(); 35 | assert_eq!(req1.path(), "/users/:id"); 36 | 37 | let mut req2: Request<()> = Request::default(); 38 | *req2.uri_mut() = "https://www.rust-lang.org/api/users".parse().unwrap(); 39 | assert_eq!(req2.path(), "/api/users"); 40 | 41 | let mut req3: Request<()> = Request::default(); 42 | *req3.uri_mut() = "/api/users".parse().unwrap(); 43 | assert_eq!(req3.path(), "/api/users"); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /rust/src.axum/middleware/request_ulid.rs: -------------------------------------------------------------------------------- 1 | //! Middleware for generating [ulid::Ulid]s on requests. 2 | 3 | use axum::http::Request; 4 | use tower_http::request_id::{MakeRequestId, RequestId}; 5 | use ulid::Ulid; 6 | 7 | /// Make/generate ulid on requests. 8 | #[derive(Copy, Clone, Debug)] 9 | pub struct MakeRequestUlid; 10 | 11 | /// Implement the trait for producing a request ID from the incoming request. 12 | /// In our case, we want to generate a new UUID that we can associate with a single request. 13 | impl MakeRequestId for MakeRequestUlid { 14 | fn make_request_id(&mut self, _: &Request) -> Option { 15 | let req_id = Ulid::new().to_string().parse(); 16 | match req_id { 17 | Ok(id) => Some(RequestId::new(id)), 18 | _ => None, 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /rust/src.axum/middleware/reqwest_tracing.rs: -------------------------------------------------------------------------------- 1 | //! Adding trace information to [reqwest::Request]s. 2 | 3 | use http::Extensions; 4 | use reqwest::{Request, Response}; 5 | use reqwest_middleware::Result; 6 | use reqwest_tracing::{default_on_request_end, reqwest_otel_span, ReqwestOtelSpanBackend}; 7 | use std::time::Instant; 8 | use tracing::Span; 9 | 10 | /// Latency string. 11 | const LATENCY_FIELD: &str = "latency_ms"; 12 | 13 | /// Struct for extending [reqwest_tracing::TracingMiddleware]. 14 | #[derive(Debug)] 15 | pub struct ExtendedTrace; 16 | 17 | impl ReqwestOtelSpanBackend for ExtendedTrace { 18 | fn on_request_start(req: &Request, extension: &mut Extensions) -> Span { 19 | extension.insert(Instant::now()); 20 | reqwest_otel_span!( 21 | name = "reqwest-http-request", 22 | req, 23 | latency_ms = tracing::field::Empty, 24 | ) 25 | } 26 | 27 | fn on_request_end(span: &Span, outcome: &Result, extension: &mut Extensions) { 28 | let elapsed_milliseconds = extension.get::().unwrap().elapsed().as_millis() as i64; 29 | default_on_request_end(span, outcome); 30 | span.record(LATENCY_FIELD, elapsed_milliseconds); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /rust/src.axum/middleware/runtime.rs: -------------------------------------------------------------------------------- 1 | //! Middleware for runtime, [tower_http] extensions. 2 | 3 | use crate::error::AppError; 4 | 5 | use axum::response::{IntoResponse, Response}; 6 | use std::any::Any; 7 | 8 | /// Middleware function for catching runtime panics, logging 9 | /// them, and converting them into a `500 Internal Server` response. 10 | pub fn catch_panic(err: Box) -> Response { 11 | let details = if let Some(s) = err.downcast_ref::() { 12 | s.clone() 13 | } else if let Some(s) = err.downcast_ref::<&str>() { 14 | s.to_string() 15 | } else { 16 | "Unknown panic message".to_string() 17 | }; 18 | 19 | let err: AppError = anyhow::anyhow!(details).into(); 20 | err.into_response() 21 | } 22 | 23 | #[cfg(test)] 24 | mod test { 25 | use super::*; 26 | use crate::error::{parse_error, AppError}; 27 | use axum::http::StatusCode; 28 | use axum::{body::Body, http::Request, routing::get, Router}; 29 | use tower::{ServiceBuilder, ServiceExt}; 30 | use tower_http::catch_panic::CatchPanicLayer; 31 | 32 | #[tokio::test] 33 | async fn catch_panic_error() { 34 | let middleware = ServiceBuilder::new().layer(CatchPanicLayer::custom(catch_panic)); 35 | 36 | let app = Router::new() 37 | .route("/", get(|| async { panic!("hi") })) 38 | .layer(middleware); 39 | 40 | let res = app 41 | .oneshot(Request::builder().uri("/").body(Body::empty()).unwrap()) 42 | .await 43 | .unwrap(); 44 | 45 | let err = parse_error(res).await; 46 | 47 | assert_eq!( 48 | err, 49 | AppError::new(StatusCode::INTERNAL_SERVER_ERROR, Some("hi")) 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /rust/src.axum/router.rs: -------------------------------------------------------------------------------- 1 | //! Main [axum::Router] interface for webserver. 2 | 3 | use crate::{ 4 | middleware::logging::{log_request_response, DebugOnlyLogger, Logger}, 5 | routes::{fallback::notfound_404, health, ping}, 6 | }; 7 | use axum::{routing::get, Router}; 8 | 9 | /// Setup main router for application. 10 | pub fn setup_app_router() -> Router { 11 | let mut router = Router::new() 12 | .route("/ping", get(ping::get)) 13 | .fallback(notfound_404); 14 | 15 | router = router.layer(axum::middleware::from_fn(log_request_response::)); 16 | 17 | let mut healthcheck_router = Router::new().route("/healthcheck", get(health::healthcheck)); 18 | 19 | healthcheck_router = healthcheck_router.layer(axum::middleware::from_fn( 20 | log_request_response::, 21 | )); 22 | 23 | Router::merge(router, healthcheck_router) 24 | } 25 | -------------------------------------------------------------------------------- /rust/src.axum/routes/fallback.rs: -------------------------------------------------------------------------------- 1 | //! Fallback routes. 2 | 3 | use crate::error::AppError; 4 | use axum::http::StatusCode; 5 | 6 | /// 404 fallback. 7 | pub async fn notfound_404() -> AppError { 8 | AppError::new(StatusCode::NOT_FOUND, Some("Route does not exist!")) 9 | } 10 | -------------------------------------------------------------------------------- /rust/src.axum/routes/health.rs: -------------------------------------------------------------------------------- 1 | //! Healthcheck route. 2 | 3 | use crate::error::AppResult; 4 | use axum::{self, http::StatusCode}; 5 | use serde_json::json; 6 | 7 | /// GET handler for checking service health. 8 | #[utoipa::path( 9 | get, 10 | path = "/healthcheck", 11 | responses( 12 | (status = 200, description = "{{project-name}} healthy"), 13 | (status = 500, description = "{{project-name}} not healthy", body=AppError) 14 | ) 15 | )] 16 | pub async fn healthcheck() -> AppResult<(StatusCode, axum::Json)> { 17 | Ok((StatusCode::OK, axum::Json(json!({ "msg": "Healthy"})))) 18 | } 19 | -------------------------------------------------------------------------------- /rust/src.axum/routes/mod.rs: -------------------------------------------------------------------------------- 1 | //! Routes for [axum::Router]. 2 | 3 | pub mod fallback; 4 | pub mod health; 5 | pub mod ping; 6 | -------------------------------------------------------------------------------- /rust/src.axum/routes/ping.rs: -------------------------------------------------------------------------------- 1 | //! Generic ping route. 2 | 3 | use crate::error::AppResult; 4 | use axum::{self, http::StatusCode}; 5 | 6 | /// GET handler for internal pings and availability 7 | #[utoipa::path( 8 | get, 9 | path = "/ping", 10 | responses( 11 | (status = 200, description = "Ping successful"), 12 | (status = 500, description = "Ping not successful", body=AppError) 13 | ) 14 | )] 15 | 16 | pub async fn get() -> AppResult { 17 | Ok(StatusCode::OK) 18 | } 19 | -------------------------------------------------------------------------------- /rust/src.axum/test_utils/mod.rs: -------------------------------------------------------------------------------- 1 | /// Random value generator for sampling data. 2 | #[cfg(feature = "test_utils")] 3 | mod rvg; 4 | #[cfg(feature = "test_utils")] 5 | pub use rvg::*; 6 | -------------------------------------------------------------------------------- /rust/src.axum/test_utils/rvg.rs: -------------------------------------------------------------------------------- 1 | use proptest::{ 2 | collection::vec, 3 | strategy::{Strategy, ValueTree}, 4 | test_runner::{Config, TestRunner}, 5 | }; 6 | 7 | /// A random value generator (RVG), which, given proptest strategies, will 8 | /// generate random values based on those strategies. 9 | #[derive(Debug, Default)] 10 | pub struct Rvg { 11 | runner: TestRunner, 12 | } 13 | 14 | impl Rvg { 15 | /// Creates a new RVG with the default random number generator. 16 | pub fn new() -> Self { 17 | Rvg { 18 | runner: TestRunner::new(Config::default()), 19 | } 20 | } 21 | 22 | /// Creates a new RVG with a deterministic random number generator, 23 | /// using the same seed across test runs. 24 | pub fn deterministic() -> Self { 25 | Rvg { 26 | runner: TestRunner::deterministic(), 27 | } 28 | } 29 | 30 | /// Samples a value for the given strategy. 31 | /// 32 | /// # Example 33 | /// 34 | /// ``` 35 | /// use {{crate_name}}::test_utils::Rvg; 36 | /// 37 | /// let mut rvg = Rvg::new(); 38 | /// let int = rvg.sample(&(0..100i32)); 39 | /// ``` 40 | pub fn sample(&mut self, strategy: &S) -> S::Value { 41 | strategy 42 | .new_tree(&mut self.runner) 43 | .expect("No value can be generated") 44 | .current() 45 | } 46 | 47 | /// Samples a vec of some length with a value for the given strategy. 48 | /// 49 | /// # Example 50 | /// 51 | /// ``` 52 | /// use {{crate_name}}::test_utils::Rvg; 53 | /// 54 | /// let mut rvg = Rvg::new(); 55 | /// let ints = rvg.sample_vec(&(0..100i32), 10); 56 | /// ``` 57 | pub fn sample_vec(&mut self, strategy: &S, len: usize) -> Vec { 58 | vec(strategy, len..=len) 59 | .new_tree(&mut self.runner) 60 | .expect("No value can be generated") 61 | .current() 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /rust/src.axum/tracer.rs: -------------------------------------------------------------------------------- 1 | //! Opentelemetry tracing extensions and setup. 2 | 3 | use crate::settings::Otel; 4 | use anyhow::{anyhow, Result}; 5 | use const_format::formatcp; 6 | use http::Uri; 7 | use opentelemetry::{global, KeyValue}; 8 | use opentelemetry_otlp::{TonicExporterBuilder, WithExportConfig}; 9 | use opentelemetry_sdk::{ 10 | self, propagation::TraceContextPropagator, runtime, trace::Tracer, Resource, 11 | }; 12 | use opentelemetry_semantic_conventions as otel_semcov; 13 | use tonic::{metadata::MetadataMap, transport::ClientTlsConfig}; 14 | 15 | //const PKG_NAME: &str = env!("CARGO_PKG_NAME"); 16 | const PKG_NAME: &str = "application"; 17 | const VERSION: &str = formatcp!("v{}", env!("CARGO_PKG_VERSION")); 18 | const LANG: &str = "rust"; 19 | 20 | /// Initialize Opentelemetry tracing via the [OTLP protocol]. 21 | /// 22 | /// [OTLP protocol]: 23 | pub fn init_tracer(settings: &Otel) -> Result { 24 | global::set_text_map_propagator(TraceContextPropagator::new()); 25 | 26 | let resource = Resource::new(vec![ 27 | KeyValue::new(otel_semcov::resource::SERVICE_NAME, PKG_NAME), 28 | KeyValue::new(otel_semcov::resource::SERVICE_VERSION, VERSION), 29 | KeyValue::new(otel_semcov::resource::TELEMETRY_SDK_LANGUAGE, LANG), 30 | ]); 31 | 32 | let endpoint = &settings.exporter_otlp_endpoint; 33 | 34 | let map = MetadataMap::with_capacity(2); 35 | 36 | let trace = opentelemetry_otlp::new_pipeline() 37 | .tracing() 38 | .with_exporter(exporter(map, endpoint)?) 39 | .with_trace_config(opentelemetry_sdk::trace::config().with_resource(resource)) 40 | .install_batch(runtime::Tokio) 41 | .map_err(|e| anyhow!("failed to intialize tracer: {:#?}", e))?; 42 | 43 | Ok(trace) 44 | } 45 | 46 | fn exporter(map: MetadataMap, endpoint: &Uri) -> Result { 47 | // Over grpc transport 48 | let exporter = opentelemetry_otlp::new_exporter() 49 | .tonic() 50 | .with_endpoint(endpoint.to_string()) 51 | .with_metadata(map); 52 | 53 | match endpoint.scheme_str() { 54 | Some("https") => { 55 | let host = endpoint 56 | .host() 57 | .ok_or_else(|| anyhow!("failed to parse host"))?; 58 | 59 | Ok(exporter.with_tls_config(ClientTlsConfig::new().domain_name(host.to_string()))) 60 | } 61 | _ => Ok(exporter), 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /rust/src.axum/tracing_layers/metrics_layer.rs: -------------------------------------------------------------------------------- 1 | //! Metrics layer. 2 | 3 | use crate::tracing_layers::storage_layer::Storage; 4 | use std::{borrow::Cow, time::Instant}; 5 | use tracing::{Id, Subscriber}; 6 | use tracing_subscriber::{layer::Context, registry::LookupSpan, Layer}; 7 | 8 | const PREFIX_LABEL: &str = "metric_label_"; 9 | const METRIC_NAME: &str = "metric_name"; 10 | const OK: &str = "ok"; 11 | const ERROR: &str = "error"; 12 | const LABEL: &str = "label"; 13 | const RESULT_LABEL: &str = "result"; 14 | const SPAN_LABEL: &str = "span_name"; 15 | 16 | /// Prefix used for capturing metric spans/instrumentations. 17 | pub const METRIC_META_PREFIX: &str = "record."; 18 | 19 | /// Metrics layer for automatically deriving metrics for record.* events. 20 | /// 21 | /// Append to custom [LogFmtLayer](crate::tracing_layers::format_layer::LogFmtLayer). 22 | #[derive(Debug)] 23 | pub struct MetricsLayer; 24 | 25 | impl Layer for MetricsLayer 26 | where 27 | S: Subscriber + for<'span> LookupSpan<'span>, 28 | { 29 | fn on_close(&self, id: Id, ctx: Context<'_, S>) { 30 | let span = ctx.span(&id).expect("Span not found"); 31 | let mut extensions = span.extensions_mut(); 32 | 33 | let elapsed_secs_f64 = extensions 34 | .get_mut::() 35 | .map(|i| i.elapsed().as_secs_f64()) 36 | .unwrap_or(0.0); 37 | 38 | if let Some(visitor) = extensions.get_mut::>() { 39 | let mut labels = vec![]; 40 | for (key, value) in visitor.values() { 41 | if key.starts_with(PREFIX_LABEL) { 42 | labels.push(( 43 | key.strip_prefix(PREFIX_LABEL).unwrap_or(LABEL), 44 | value.to_string(), 45 | )) 46 | } 47 | } 48 | 49 | let span_name = span 50 | .name() 51 | .strip_prefix(METRIC_META_PREFIX) 52 | .unwrap_or_else(|| span.name()); 53 | 54 | labels.push((SPAN_LABEL, span_name.to_string())); 55 | 56 | let name = visitor 57 | .values() 58 | .get(METRIC_NAME) 59 | .unwrap_or(&Cow::from(span_name)) 60 | .to_string(); 61 | 62 | if visitor.values().contains_key(ERROR) { 63 | labels.push((RESULT_LABEL, String::from(ERROR))) 64 | } else { 65 | labels.push((RESULT_LABEL, String::from(OK))) 66 | } 67 | 68 | // Need to sort labels to remain the same across all metrics. 69 | labels.sort_unstable(); 70 | 71 | metrics::counter!(format!("{name}_total"), &labels).increment(1); 72 | metrics::histogram!(format!("{name}_duration_seconds"), &labels) 73 | .record(elapsed_secs_f64); 74 | 75 | // Remove storage as this is the last layer. 76 | extensions 77 | .remove::>() 78 | .expect("Visitor not found on 'close'"); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /rust/src.axum/tracing_layers/mod.rs: -------------------------------------------------------------------------------- 1 | //! Custom [tracing_subscriber::layer::Layer]s for formatting log events, 2 | //! deriving metrics from instrumentation calls, and for storage to augment 3 | //! layers. For more information, please read [Composing an observable Rust application]. 4 | //! 5 | //! [Composing an observable Rust application]: 6 | 7 | pub mod format_layer; 8 | pub mod metrics_layer; 9 | pub mod storage_layer; 10 | -------------------------------------------------------------------------------- /rust/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(docsrs, feature(doc_cfg))] 2 | #![warn(missing_debug_implementations, missing_docs, rust_2018_idioms)] 3 | #![deny(unreachable_pub)] 4 | #![deny(private_bounds)] 5 | #![deny(rustdoc::private_intra_doc_links)] 6 | #![deny(private_interfaces)] 7 | 8 | //! {{project-name}} 9 | {% if bench %} 10 | /// Test utilities. 11 | #[cfg(any(test, feature = "test_utils"))] 12 | #[cfg_attr(docsrs, doc(cfg(feature = "test_utils")))] 13 | pub mod test_utils; 14 | {% endif %} 15 | /// Add two integers together. 16 | pub fn add(a: i32, b: i32) -> i32 { 17 | a + b 18 | } 19 | 20 | /// Multiplies two integers together. 21 | pub fn mult(a: i32, b: i32) -> i32 { 22 | a * b 23 | } 24 | 25 | #[cfg(test)] 26 | mod tests { 27 | use super::*; 28 | 29 | #[test] 30 | fn test_mult() { 31 | assert_eq!(mult(3, 2), 6); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /rust/src/main.rs: -------------------------------------------------------------------------------- 1 | //! {{project-name}} 2 | 3 | /// Main entry point. 4 | fn main() { 5 | println!("Welcome!") 6 | } 7 | -------------------------------------------------------------------------------- /rust/src/test_utils/mod.rs: -------------------------------------------------------------------------------- 1 | /// Random value generator for sampling data. 2 | #[cfg(feature = "test_utils")] 3 | mod rvg; 4 | #[cfg(feature = "test_utils")] 5 | pub use rvg::*; 6 | -------------------------------------------------------------------------------- /rust/src/test_utils/rvg.rs: -------------------------------------------------------------------------------- 1 | use proptest::{ 2 | collection::vec, 3 | strategy::{Strategy, ValueTree}, 4 | test_runner::{Config, TestRunner}, 5 | }; 6 | 7 | /// A random value generator (RVG), which, given proptest strategies, will 8 | /// generate random values based on those strategies. 9 | #[derive(Debug, Default)] 10 | pub struct Rvg { 11 | runner: TestRunner, 12 | } 13 | 14 | impl Rvg { 15 | /// Creates a new RVG with the default random number generator. 16 | pub fn new() -> Self { 17 | Rvg { 18 | runner: TestRunner::new(Config::default()), 19 | } 20 | } 21 | 22 | /// Creates a new RVG with a deterministic random number generator, 23 | /// using the same seed across test runs. 24 | pub fn deterministic() -> Self { 25 | Rvg { 26 | runner: TestRunner::deterministic(), 27 | } 28 | } 29 | 30 | /// Samples a value for the given strategy. 31 | /// 32 | /// # Example 33 | /// 34 | /// ``` 35 | /// use {{crate_name}}::test_utils::Rvg; 36 | /// 37 | /// let mut rvg = Rvg::new(); 38 | /// let int = rvg.sample(&(0..100i32)); 39 | /// ``` 40 | pub fn sample(&mut self, strategy: &S) -> S::Value { 41 | strategy 42 | .new_tree(&mut self.runner) 43 | .expect("No value can be generated") 44 | .current() 45 | } 46 | 47 | /// Samples a vec of some length with a value for the given strategy. 48 | /// 49 | /// # Example 50 | /// 51 | /// ``` 52 | /// use {{crate_name}}::test_utils::Rvg; 53 | /// 54 | /// let mut rvg = Rvg::new(); 55 | /// let ints = rvg.sample_vec(&(0..100i32), 10); 56 | /// ``` 57 | pub fn sample_vec(&mut self, strategy: &S, len: usize) -> Vec { 58 | vec(strategy, len..=len) 59 | .new_tree(&mut self.runner) 60 | .expect("No value can be generated") 61 | .current() 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /rust/tests/a_integration_test.rs: -------------------------------------------------------------------------------- 1 | #[test] 2 | fn test_add() { 3 | assert!(true); 4 | } 5 | -------------------------------------------------------------------------------- /rust/tests/integration_test.rs: -------------------------------------------------------------------------------- 1 | #[test] 2 | fn test_add() { 3 | assert_eq!({{crate_name}}::add(3, 2), 5); 4 | } 5 | --------------------------------------------------------------------------------