├── .cargo └── config.toml ├── .github └── workflows │ └── rust.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE ├── PUBLISH.md ├── README.md ├── Review.md ├── benches └── log_fmt.rs ├── examples ├── configuration │ ├── Cargo.toml │ ├── app.yaml │ └── src │ │ └── main.rs ├── deserialize_ext │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── examples_resources │ ├── Cargo.toml │ ├── build.rs │ ├── certs │ │ ├── openssl.conf │ │ ├── tls.cert │ │ └── tls.key │ ├── proto │ │ ├── echo.proto │ │ └── hello.proto │ └── src │ │ ├── grpc.rs │ │ ├── lib.rs │ │ └── proto │ │ ├── description.bin │ │ ├── echo.rs │ │ ├── hello.rs │ │ └── mod.rs ├── grafana │ ├── Cargo.toml │ ├── README.md │ ├── otel-config.yaml │ ├── src │ │ ├── client.rs │ │ └── server.rs │ └── traces.png ├── health │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── hello-world │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── log-level-change │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── metrics-callback │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── proxy-layer │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── reflection │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── tls │ ├── Cargo.toml │ └── src │ │ ├── client_native_tls.rs │ │ ├── client_rustls.rs │ │ └── server.rs └── tonic │ ├── Cargo.toml │ └── src │ └── main.rs ├── src ├── application.rs ├── application │ ├── health.rs │ ├── management.rs │ ├── tls.rs │ └── version.rs ├── bootstrap.rs ├── configuration.rs ├── configuration │ ├── application.rs │ ├── management.rs │ ├── observability.rs │ ├── source.rs │ └── tls.rs ├── error.rs ├── extensions.rs ├── extensions │ ├── axum_tonic.rs │ ├── headers_ext.rs │ ├── http_req_ext.rs │ ├── reqwest_ext.rs │ ├── serde_ext.rs │ └── tonic_ext.rs ├── lib.rs ├── middleware.rs ├── middleware │ ├── proxy_layer.rs │ ├── proxy_layer │ │ ├── error.rs │ │ ├── layer.rs │ │ ├── service.rs │ │ └── shared.rs │ ├── tracing.rs │ └── tracing │ │ ├── common.rs │ │ ├── grpc_req.rs │ │ └── http_req.rs ├── observability.rs ├── observability │ ├── headers_filter.rs │ ├── metrics.rs │ ├── metrics │ │ ├── cgroupv2.rs │ │ ├── proc_limits.rs │ │ ├── recorder.rs │ │ ├── sys_info.rs │ │ └── tokio_metrics.rs │ ├── tracing.rs │ └── tracing │ │ ├── event_formatter.rs │ │ ├── floor_char_boundary.rs │ │ ├── log_layer.rs │ │ ├── otlp_layer.rs │ │ ├── tracing_fields.rs │ │ └── writer.rs ├── resources │ ├── default_conf.toml │ └── favicon.png ├── static_assert.rs ├── sugar.rs └── sugar │ ├── grpc_codes.rs │ ├── hash_builder.rs │ └── yaml_response.rs └── tests ├── app_config.rs ├── app_config_from_env.rs ├── app_config_source_order.rs ├── app_config_tls.rs ├── bootstrap.rs ├── bootstrap_multiple_calls.rs ├── default_headers_ext.rs ├── exclude_all.rs ├── exclude_one.rs ├── headers_ext.rs ├── include_all.rs ├── include_one.rs ├── log_fmt.rs ├── resources └── test_conf.toml ├── sanitize_all.rs ├── sanitize_one.rs ├── sugar_grpc_codes.rs ├── tls.rs └── traing_fields.rs /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | rustflags = [ 3 | "--cfg=tracing_unstable", 4 | "--cfg=tokio_unstable", 5 | ] 6 | rustdocflags = [ 7 | "--cfg=tracing_unstable", 8 | "--cfg=tokio_unstable", 9 | ] 10 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | on: 2 | pull_request: 3 | branches: [ "main" ] 4 | 5 | jobs: 6 | fmt: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v1 10 | - uses: actions-rs/toolchain@v1 11 | with: 12 | toolchain: stable 13 | components: rustfmt 14 | - uses: arduino/setup-protoc@v1 15 | with: 16 | repo-token: ${{ secrets.GITHUB_TOKEN }} 17 | - uses: actions-rs/cargo@v1 18 | with: 19 | command: fmt 20 | args: --all -- --check 21 | 22 | build-features: 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v1 26 | - uses: actions-rs/toolchain@v1 27 | with: 28 | toolchain: stable 29 | - uses: arduino/setup-protoc@v1 30 | with: 31 | repo-token: ${{ secrets.GITHUB_TOKEN }} 32 | 33 | - name: tokio-metrics 34 | run: cargo build --features tokio-metrics 35 | 36 | - name: reqwest 37 | run: cargo build --features reqwest 38 | 39 | - name: use_native_tls 40 | run: cargo build --features use_native_tls 41 | 42 | - name: use_rustls 43 | run: cargo build --features use_rustls 44 | 45 | clippy: 46 | runs-on: ubuntu-latest 47 | steps: 48 | - uses: actions/checkout@v1 49 | - uses: actions-rs/toolchain@v1 50 | with: 51 | toolchain: stable 52 | components: clippy 53 | - uses: arduino/setup-protoc@v1 54 | with: 55 | repo-token: ${{ secrets.GITHUB_TOKEN }} 56 | - uses: actions-rs/cargo@v1 57 | with: 58 | command: clippy 59 | args: --all -- -D warnings 60 | 61 | test: 62 | runs-on: ubuntu-latest 63 | steps: 64 | - uses: actions/checkout@v1 65 | - uses: arduino/setup-protoc@v1 66 | with: 67 | repo-token: ${{ secrets.GITHUB_TOKEN }} 68 | - uses: actions-rs/cargo@v1 69 | with: 70 | command: test 71 | args: #--features native-tls 72 | 73 | test-native-tls: 74 | runs-on: ubuntu-latest 75 | steps: 76 | - uses: actions/checkout@v1 77 | - uses: arduino/setup-protoc@v1 78 | with: 79 | repo-token: ${{ secrets.GITHUB_TOKEN }} 80 | - uses: actions-rs/cargo@v1 81 | with: 82 | command: test 83 | args: --features use_native_tls -- --ignored 84 | 85 | test-rustls: 86 | runs-on: ubuntu-latest 87 | steps: 88 | - uses: actions/checkout@v1 89 | - uses: arduino/setup-protoc@v1 90 | with: 91 | repo-token: ${{ secrets.GITHUB_TOKEN }} 92 | - uses: actions-rs/cargo@v1 93 | with: 94 | command: test 95 | args: --features use_rustls -- --ignored 96 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 7 | Cargo.lock 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk 11 | *.iml 12 | 13 | # Files generated by IDE 14 | .idea/ -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fregate" 3 | version = "0.19.0-3" 4 | edition = "2021" 5 | license = "Apache-2.0" 6 | repository = "https://github.com/elefant-dev/fregate-rs" 7 | homepage = "https://github.com/elefant-dev/fregate-rs" 8 | description = "Framework for services creation" 9 | categories = ["web-programming"] 10 | keywords = ["http", "grpc", "service"] 11 | 12 | [workspace] 13 | members = ["examples/*"] 14 | 15 | [features] 16 | tls = [] 17 | use_native_tls = [ 18 | "tls", 19 | "reqwest/native-tls", 20 | "tokio-native-tls", 21 | "async-stream", 22 | "futures-util", 23 | "tokio/fs", 24 | "tokio/net", 25 | "tokio-stream" 26 | ] 27 | use_native_tls_vendored = [ 28 | "use_native_tls", 29 | "reqwest/native-tls-vendored", 30 | "native-tls/vendored" 31 | ] 32 | use_rustls = [ 33 | "tls", 34 | "tokio-rustls", 35 | "rustls-pemfile", 36 | "async-stream", 37 | "futures-util", 38 | "tokio/fs", 39 | "tokio/net", 40 | "tokio-stream" 41 | ] 42 | use_rustls_tls12 = [ 43 | "tls", 44 | "use_rustls", 45 | "tokio-rustls/tls12" 46 | ] 47 | 48 | [dependencies] 49 | ahash = { version = "0.8.*", optional = true } 50 | axum = { version = "0.6.*", features = ["headers", "http1", "http2", "json", "matched-path", "original-uri", "ws"] } 51 | chrono = "0.4.*" 52 | config = "0.13.*" 53 | hyper = { version = "0.14.*", features = ["full"] } 54 | metrics = "0.21.*" 55 | metrics-exporter-prometheus = "0.12.*" 56 | num_cpus = "1" 57 | opentelemetry = { version = "0.19.*", features = ["rt-tokio"] } 58 | opentelemetry-http = "0.8.*" 59 | opentelemetry-otlp = "0.12.*" 60 | pin-project-lite = "0.2.*" 61 | prost = "0.11.*" 62 | reqwest = { version = "0.11.*", default-features = false, optional = true } 63 | sealed = "0.5.*" 64 | serde = { version = "1.0.*", features = ["derive"] } 65 | serde_json = "1.0.*" 66 | sysinfo = "0.29.*" 67 | thiserror = "1.0.*" 68 | time = "0.3.*" 69 | tokio = { version = "1", features = ["signal"] } 70 | tonic = "0.9.*" 71 | tower = { version = "0.4.*" } 72 | tower-http = { version = "0.4.*", features = ["util", "map-response-body"] } 73 | tracing = { version = "0.1.*", features = ["valuable"] } 74 | tracing-appender = { version = "0.2.*" } 75 | tracing-opentelemetry = "0.19.*" 76 | tracing-subscriber = { version = "0.3.*", features = ["env-filter", "fmt", "time", "registry"] } 77 | valuable = "0.1.*" 78 | valuable-serde = "0.1.*" 79 | uuid = {version = "1.4.*", features=["v4"]} 80 | zip = "0.6.*" 81 | 82 | tokio-metrics = { version = "0.2.*", optional = true } 83 | 84 | # common deps for tls 85 | async-stream = { version = "0.3.*", optional = true } 86 | futures-util = { version = "0.3.*", optional = true } 87 | tokio-stream = { version = "0.1.*", optional = true, features = ["net"] } 88 | 89 | # native-tls deps 90 | native-tls = { version = "0.2.*", optional = true, features = ["alpn"] } 91 | tokio-native-tls = { version = "0.3.*", optional = true } 92 | 93 | # rustls deps 94 | rustls-pemfile = { version = "1.0.*", optional = true } 95 | tokio-rustls = { version = "0.24.*", optional = true, default-features = false, features = ["logging"] } 96 | 97 | [dev-dependencies] 98 | criterion = "0.*" 99 | hyper-rustls = { version = "0.24.*", default-features = false, features = ["native-tokio", "http1", "tls12"] } 100 | rustls = { version = "0.21.*", features = ["tls12", "dangerous_configuration"] } 101 | tokio = { version = "1", features = ["rt-multi-thread"] } 102 | tracing-subscriber = { version = "0.3.*", features = ["env-filter", "json", "time"] } 103 | valuable-derive = "0.1.*" 104 | 105 | [[bench]] 106 | name = "log_fmt" 107 | harness = false 108 | -------------------------------------------------------------------------------- /PUBLISH.md: -------------------------------------------------------------------------------- 1 | Add changes to [CHANGELOG.md](https://github.com/elefant-dev/fregate-rs/blob/main/CHANGELOG.md) 2 | 3 | Make sure you have updated version in Cargo.toml with the versioning rules your team agreed on: 4 | ```toml 5 | [package] 6 | version = "0.2.2" 7 | ``` 8 | Prior to publishing run: 9 | ```bash 10 | cargo-checkmate 11 | ``` 12 | so to do multiple checks: check, fmt, clippy, build, test, doc and audit 13 | 14 | You might want to check if you have any unused dependencies in toml: 15 | ```bash 16 | cargo +nightly udeps --all-targets 17 | ``` 18 | It is recommended before publishing you run: 19 | ```bash 20 | cargo publish --dry-run 21 | ``` 22 | This will perform some checks and compress source code into .crate and verify that it compiles but won't upload to https://crates.io 23 | 24 | Finally to upload new version of crate run: 25 | ```bash 26 | cargo publish 27 | ``` 28 | 29 | For more information visit: 30 | https://doc.rust-lang.org/cargo/reference/publishing.html 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fregate-rs 2 | 3 | Set of instruments to simplify http server set-up. 4 | 5 | ## Work in progress 6 | This project is in progress and might change a lot from version to version. 7 | 8 | ## Example: 9 | ```rust 10 | use fregate::{ 11 | axum::{routing::get, Router}, 12 | bootstrap, tokio, AppConfig, Application, 13 | }; 14 | 15 | async fn handler() -> &'static str { 16 | "Hello, World!" 17 | } 18 | 19 | #[tokio::main] 20 | async fn main() { 21 | let config: AppConfig = bootstrap([]).unwrap(); 22 | 23 | Application::new(config) 24 | .router(Router::new().route("/", get(handler))) 25 | .serve() 26 | .await 27 | .unwrap(); 28 | } 29 | ``` 30 | 31 | ### More examples can be found [`here`](https://github.com/elefant-dev/fregate-rs/tree/main/examples). -------------------------------------------------------------------------------- /Review.md: -------------------------------------------------------------------------------- 1 | # Usage 2 | 3 | "Swiss knife" crate, consists of a lot of useful instruments such as system health diagnostics, logging, proxies etc. 4 | 5 | # Questions 6 | 7 | ### ❓ Dependencies logic 8 | 9 | https://github.com/Wandalen/fregate_review/blob/master/examples/grafana/Cargo.toml 10 | 11 | Not clear what is logic of including dependencies. 12 | Lots of dependencies come from `fregate` and public, 13 | but some are not public and included by a user 14 | 15 | ```toml 16 | fregate = { path = "../.." } 17 | tracing = "0.1.36" 18 | tokio = { version = "1", features = ["full"] } 19 | prost = "0.11.0" 20 | tonic = "0.8.0" 21 | ``` 22 | 23 | Why these dependencies are not in `fregate`? 24 | Is it possible to remove dependency `axum`? 25 | 26 | ### ❓ Consider vector 27 | 28 | Consider maybe [vector](https://vector.dev/). 29 | 30 | # Review 31 | 32 | ### ✅ No major problem 33 | 34 | ### ✅ Idiomatic: code is short and concise 35 | 36 | ### ✅ Disciplinar: Library code is mostly covered in unit tests 37 | 38 | Consider measuring test coverage. 39 | 40 | ### ✅ Architectural: following of deep module / information hiding principles 41 | 42 | Read more on [deep module / information hiding] principles](https://medium.com/@nathan.fooo/5-notes-information-hiding-and-leakage-e5bd75dc41f7) 43 | 44 | ### ✅ Disciplinar: sticking to the best practices 45 | 46 | `cargo fmt` - this utility formats all bin and lib files of the current crate using rustfmt. It's used ✅. 47 | 48 | `cargo clippy` - checks a package to catch common mistakes and improve your Rust code. Seems it's ignored ✅. 49 | Use (reasonable) preset of wanring/clippy rules ✅. 50 | 51 | [`cargo miri`](https://github.com/rust-lang/miri) - an experimental interpreter for Rust's mid-level intermediate representation (MIR). It can run binaries and test suites of cargo projects and detect certain classes of undefined behavior. Use compile-time conditionals to separate code which `miri` can interpret. As long as crate has no `unsafe` code you can use `#![forbid(unsafe_code)]` to forbid unsafe code. If your library is not intended to have `unsafe`, consider adding `#![forbid(unsafe_code)]`, which will ease reasoning about the code for potential security auditors, and will require quite a reasoning to add any `unsafe` in future.❕ 52 | 53 | [`cargo-audit`](https://github.com/RustSec/rustsec/tree/main/cargo-audit) - audit Cargo files for crates with security vulnerabilities reported to the RustSec Advisory Database ✅. 54 | 55 | [`cargo-checkmate`](https://github.com/cargo-checkmate/cargo-checkmate) - ensures your project builds, tests pass, has good format, doesn't have dependencies with known vulnerabilities ✅. 56 | 57 | [`cargo-udeps`](https://github.com/est31/cargo-udeps) - ensures there is no unused dependencies in Cargo.toml ✅. 58 | 59 | Consider having a `Makefile` tools for audit as targets❕. 60 | 61 | These tools give free suggestion how to improve quality of code and to avoid common pitfalls. 62 | 63 | ### ❕ Architectural: modularity is okay 64 | 65 | There is no sense to decompose the crate since it looks lightweight (seems to be design requirenment) and there is no advantages in transforming crate into complex system ✅. 66 | 67 | Pub usage of `axum` ✅ 68 | `fregate` manages the `axum` versions. The end-user cannot do anything with this. To have a single web server, the user must build his application based on `fregate` dependencies. 69 | 70 | No access to `tokio` ❕ 71 | Crate does not provide public access to `tokio`, that's why even "hello world" example needs to add this dependency again to run the main function. 72 | 73 | Extensive wildcard imports into root ❕ 74 | Crate root namespace should be reserved for entities that will be immediately needed by a user. 75 | Currently everything is exported through the crate root. 76 | What is somewhat excessive. 77 | Structs like `NoHealth` and `AlwaysReadyAndAlive` should not pollute the root namespace. 78 | https://www.lurklurk.org/effective-rust/wildcard.html 79 | 80 | ### ❕ Architectural: framework vs toolkit approach ( vendorlock vs agnostic ) 81 | 82 | Overuse of inversion of control and adapter/facade patterns is obeservable, what is typical for frameworks. 83 | 84 | It creates several disadvantages: 85 | 86 | 1. It decreases maintainability because isolatation from original crates by facade. New features are not necessarily available for user of the framework. 87 | 2. It decreases flexibility and extendability of the codebase of users of the crate. Because many parameters are hidden. Interfaces are changed, but new interfaces are not necessarily better than original. 88 | 3. It increase time for onboarding. New developers should spend more time to learn custom interfaces of `fregate`. 89 | 90 | There is risk that this crate will become framework. Better I would suggest to keep principle of being agnostics and transparent, providing fundamentals components instead of aggregating eagerly them into higher-order entities. 91 | 92 | ### ❕ Architectural: risk of utility anti-pattern 93 | 94 | What is responsibility of the crate? List all and evaluate does not it have too much responsibilities? 95 | 96 | - Telemetry + logging feels okay. 97 | - Telemetry + logging + creating server feels too much for me. 98 | 99 | https://www.yanglinzhao.com/posts/utils-antipattern/ 100 | 101 | ### ❕ Security: Potential sensitive information leak through errors 102 | 103 | in `src/middleware/proxy.rs:fn handle_result`, Error is directly 104 | passed through to the caller of the API. Errors potentially contain sensitive 105 | information (such as secret keys, URLs, etc), and one should be careful 106 | about passing errors directly to the caller. 107 | 108 | ### ❕ Performance: async trait. 109 | 110 | Async traits use dynamic dispatch under the hood, which has runtime performance cost. 111 | - [Async trait downsides](https://internals.rust-lang.org/t/async-traits-the-less-dynamic-allocations-edition/13048/2) 112 | - [Async trait under the hood](https://smallcultfollowing.com/babysteps/blog/2019/10/26/async-fn-in-traits-are-hard/) 113 | 114 | ### ❕ Structural: lack of features 115 | 116 | Forward control over features of exposed dependencies and dependencies themselves. 117 | 118 | ### ❕ Structural: lack of documentation 119 | 120 | Public functions, structs, traits, modules should be descibed well. 121 | Example files should be described. 122 | Most functions/structs should have snipets with examples. 123 | Some example files look more like smoke tests than examples. 124 | 125 | ### ❕ Strategic: value for open source 126 | 127 | The goal: to answer the question whether I should use this crate in my (including commercial) projects and, if so, in what cases it is most useful. 128 | 129 | **PROS** 130 | * couple of built-in tools, most of which could be necessary for common web application 131 | * reputation of the company 132 | 133 | **CONS** 134 | * `fregate` doesn't give you any guarantees about versions of it's dependencies 135 | * zero flexibility in choosing web frameworks. axum is required 136 | * too framework-like with disadvantages explained above 137 | -------------------------------------------------------------------------------- /benches/log_fmt.rs: -------------------------------------------------------------------------------- 1 | use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; 2 | use fregate::observability::EventFormatter; 3 | use std::io; 4 | use time::format_description::well_known::Rfc3339; 5 | use tracing::subscriber::with_default; 6 | use tracing_subscriber::fmt::format::{DefaultFields, Format, Json, JsonFields}; 7 | use tracing_subscriber::fmt::time::UtcTime; 8 | use tracing_subscriber::fmt::{MakeWriter, Subscriber}; 9 | use tracing_subscriber::EnvFilter; 10 | 11 | fn benchmark(c: &mut Criterion) { 12 | let i = 1000; 13 | let data = unsafe { String::from_utf8_unchecked(vec![b'X'; 3000]) }; 14 | 15 | let mut group = c.benchmark_group("Event Formatter"); 16 | 17 | group.bench_with_input( 18 | BenchmarkId::new("Default tracing", i), 19 | &(i, &data), 20 | |b, (i, data)| { 21 | let subscriber = tracing_subscriber(); 22 | 23 | with_default(subscriber, || { 24 | b.iter(|| { 25 | for _ in 0..*i { 26 | tracing::info!(secret = "12345", "message = {data}"); 27 | } 28 | }); 29 | }); 30 | }, 31 | ); 32 | group.bench_with_input( 33 | BenchmarkId::new("Fregate None", i), 34 | &(i, &data), 35 | |b, (i, data)| { 36 | let subscriber = subscriber(EventFormatter::new_with_limit(None)); 37 | 38 | with_default(subscriber, || { 39 | b.iter(|| { 40 | for _ in 0..*i { 41 | tracing::info!(secret = "12345", "message = {data}"); 42 | } 43 | }); 44 | }); 45 | }, 46 | ); 47 | group.bench_with_input( 48 | BenchmarkId::new("Fregate Some(256)", i), 49 | &(i, &data), 50 | |b, (i, data)| { 51 | let subscriber = subscriber(EventFormatter::new_with_limit(Some(256))); 52 | 53 | with_default(subscriber, || { 54 | b.iter(|| { 55 | for _ in 0..*i { 56 | tracing::info!(secret = "12345", "message = {data}"); 57 | } 58 | }); 59 | }); 60 | }, 61 | ); 62 | group.finish(); 63 | } 64 | 65 | criterion_group!(benches, benchmark); 66 | criterion_main!(benches); 67 | 68 | #[derive(Clone, Debug)] 69 | struct MockWriter; 70 | 71 | #[derive(Clone, Debug)] 72 | struct MakeMockWriter; 73 | 74 | impl MakeMockWriter { 75 | fn new() -> Self { 76 | Self {} 77 | } 78 | } 79 | 80 | impl MockWriter { 81 | fn new() -> Self { 82 | Self {} 83 | } 84 | } 85 | 86 | impl io::Write for MockWriter { 87 | fn write(&mut self, buf: &[u8]) -> io::Result { 88 | Ok(buf.len()) 89 | } 90 | 91 | fn flush(&mut self) -> io::Result<()> { 92 | Ok(()) 93 | } 94 | } 95 | 96 | impl<'a> MakeWriter<'a> for MakeMockWriter { 97 | type Writer = MockWriter; 98 | 99 | fn make_writer(&'a self) -> Self::Writer { 100 | MockWriter::new() 101 | } 102 | } 103 | 104 | fn subscriber( 105 | formatter: EventFormatter, 106 | ) -> Subscriber { 107 | Subscriber::builder() 108 | .event_format(formatter) 109 | .with_writer(MakeMockWriter::new()) 110 | .with_env_filter("info") 111 | .finish() 112 | } 113 | 114 | fn tracing_subscriber( 115 | ) -> Subscriber>, EnvFilter, MakeMockWriter> { 116 | tracing_subscriber::fmt() 117 | .json() 118 | .with_timer::<_>(UtcTime::rfc_3339()) 119 | .with_writer(MakeMockWriter::new()) 120 | .flatten_event(true) 121 | .with_target(true) 122 | .with_current_span(false) 123 | .with_env_filter("info") 124 | .finish() 125 | } 126 | -------------------------------------------------------------------------------- /examples/configuration/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "configuration" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | fregate = { path = "../.." } 8 | serde = { version = "1", features = ["derive"] } 9 | tokio = { version = "1", features = ["rt-multi-thread"] } 10 | -------------------------------------------------------------------------------- /examples/configuration/app.yaml: -------------------------------------------------------------------------------- 1 | host: "127.0.0.1" 2 | port: 8011 3 | -------------------------------------------------------------------------------- /examples/configuration/src/main.rs: -------------------------------------------------------------------------------- 1 | use fregate::axum::{routing::get, Router}; 2 | use fregate::config::FileFormat; 3 | use fregate::{bootstrap, tokio}; 4 | use fregate::{AppConfig, Application, ConfigSource, Empty}; 5 | use serde::Deserialize; 6 | 7 | async fn handler() -> &'static str { 8 | "Hello, Configuration!" 9 | } 10 | 11 | #[allow(dead_code)] 12 | #[derive(Deserialize, Debug)] 13 | struct Custom { 14 | number: u32, 15 | } 16 | 17 | #[tokio::main] 18 | async fn main() { 19 | std::env::set_var("TEST_PORT", "3333"); 20 | std::env::set_var("TEST_NUMBER", "1010"); 21 | std::env::set_var("TEST_LOG_LEVEL", "debug"); 22 | std::env::set_var("TEST_TRACE_LEVEL", "debug"); 23 | std::env::set_var("TEST_COMPONENT_NAME", "configuration"); 24 | std::env::set_var("TEST_MANAGEMENT_ENDPOINTS_VERSION", "/give/me/version"); 25 | std::env::set_var("TEST_COMPONENT_VERSION", "0.0.0"); 26 | std::env::set_var("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", "http://0.0.0.0:4317"); 27 | std::env::set_var("OTEL_SERVICE_NAME", "CONFIGURATION"); 28 | 29 | // There are multiple ways to read AppConfig: 30 | 31 | // This will read AppConfig and call init_tracing() with arguments read in AppConfig 32 | let conf_0: AppConfig = bootstrap([ 33 | ConfigSource::File("./examples/configuration/app.yaml"), 34 | ConfigSource::EnvPrefix("TEST"), 35 | ]) 36 | .unwrap(); 37 | 38 | // Read default AppConfig 39 | let _conf = AppConfig::default(); 40 | 41 | // Set up AppConfig through builder, nothing added by default 42 | let _conf = AppConfig::::builder() 43 | .add_default() 44 | .add_env_prefixed("TEST") 45 | .add_file("./examples/configuration/app.yaml") 46 | .add_str(include_str!("../app.yaml"), FileFormat::Yaml) 47 | .build() 48 | .unwrap(); 49 | 50 | // Read default config with private field struct Custom with specified file and environment variables with specified prefix and "_" separator 51 | let _conf: AppConfig = 52 | AppConfig::default_with("./examples/configuration/app.yaml", "TEST").unwrap(); 53 | 54 | Application::new(conf_0) 55 | .router(Router::new().route("/", get(handler))) 56 | .serve() 57 | .await 58 | .unwrap(); 59 | } 60 | 61 | // Check version with: 62 | // curl http://localhost:3333/configuration/give/me/version 63 | -------------------------------------------------------------------------------- /examples/deserialize_ext/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "deserialize_ext" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | fregate = { path = "../.." } 8 | serde = "1" 9 | serde_json = "1.0.85" 10 | -------------------------------------------------------------------------------- /examples/deserialize_ext/src/main.rs: -------------------------------------------------------------------------------- 1 | use fregate::{extensions::DeserializeExt, AppConfig}; 2 | use serde::{ 3 | de::{DeserializeOwned, Error as DeError}, 4 | Deserialize, Deserializer, 5 | }; 6 | use serde_json::Value; 7 | use std::error::Error as StdError; 8 | 9 | const SCALICA_ADDRESS_PATH: &str = "/scalica/address"; 10 | const SCALICA_NEW_ADDRESS_PATH: &str = "/scalica/new/address"; 11 | 12 | #[derive(Debug)] 13 | struct Configuration { 14 | scalica_address: Box, 15 | private: T, 16 | } 17 | 18 | impl<'de, T: DeserializeOwned> Deserialize<'de> for Configuration { 19 | fn deserialize(deserializer: D) -> Result 20 | where 21 | D: Deserializer<'de>, 22 | { 23 | let value = Value::deserialize(deserializer)?; 24 | 25 | let scalica_address = value.pointer_and_deserialize(SCALICA_ADDRESS_PATH)?; 26 | let private = T::deserialize(value).map_err(DeError::custom)?; 27 | 28 | Ok(Self { 29 | scalica_address, 30 | private, 31 | }) 32 | } 33 | } 34 | 35 | #[derive(Debug)] 36 | struct ExtendedConfiguration { 37 | scalica_new_address: Box, 38 | } 39 | 40 | impl<'de> Deserialize<'de> for ExtendedConfiguration { 41 | fn deserialize(deserializer: D) -> Result 42 | where 43 | D: Deserializer<'de>, 44 | { 45 | let value = Value::deserialize(deserializer)?; 46 | 47 | let scalica_new_address = value.pointer_and_deserialize(SCALICA_NEW_ADDRESS_PATH)?; 48 | 49 | Ok(Self { 50 | scalica_new_address, 51 | }) 52 | } 53 | } 54 | 55 | fn main() -> Result<(), Box> { 56 | std::env::set_var("BOHEMIA_SCALICA_ADDRESS", "Earth"); 57 | std::env::set_var("BOHEMIA_SCALICA_NEW_ADDRESS", "Mars"); 58 | 59 | let config = AppConfig::>::builder() 60 | .add_default() 61 | .add_env_prefixed("BOHEMIA") 62 | .build()?; 63 | 64 | assert_eq!(config.private.scalica_address.as_ref(), "Earth"); 65 | assert_eq!(config.private.private.scalica_new_address.as_ref(), "Mars"); 66 | 67 | println!("configuration: `{config:#?}`."); 68 | 69 | Ok(()) 70 | } 71 | -------------------------------------------------------------------------------- /examples/examples_resources/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "resources" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | hyper = "0.14.*" 8 | prost = "0.11.*" 9 | tonic = "0.9.*" 10 | 11 | [build-dependencies] 12 | tonic-build = { version = "0.9.*", features = ["prost"] } 13 | -------------------------------------------------------------------------------- /examples/examples_resources/build.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | 3 | const OUT_FOLDER: &str = "src/proto"; 4 | 5 | fn main() -> Result<(), Box> { 6 | if fs::metadata(OUT_FOLDER).is_ok() { 7 | fs::remove_dir_all(OUT_FOLDER)?; 8 | } 9 | fs::create_dir(OUT_FOLDER)?; 10 | 11 | tonic_build::configure() 12 | .include_file("mod.rs") 13 | .file_descriptor_set_path(format!("{OUT_FOLDER}/description.bin")) 14 | .build_server(true) 15 | .build_client(true) 16 | .out_dir(OUT_FOLDER) 17 | .compile(&["./proto/echo.proto", "./proto/hello.proto"], &["./proto"])?; 18 | 19 | Ok(()) 20 | } 21 | -------------------------------------------------------------------------------- /examples/examples_resources/certs/openssl.conf: -------------------------------------------------------------------------------- 1 | # openssl req -config ./openssl.conf -extensions v3_req -new -nodes -x509 -keyout tls.key -out tls.cert 2 | 3 | [req] 4 | default_bits = 4096 5 | default_days = 36500 6 | default_md = sha512 7 | string_mask = utf8only 8 | distinguished_name = req_distinguished_name 9 | req_extensions = v3_req 10 | prompt = no 11 | 12 | [req_distinguished_name] 13 | 0.organizationName = Jindřich Ltd. 14 | organizationalUnitName = Squad 15 | emailAddress = null@skalica.cz 16 | localityName = Skalica 17 | stateOrProvinceName = Bohemia 18 | countryName = CZ 19 | commonName = localhost 20 | 21 | [v3_req] 22 | basicConstraints = critical,CA:FALSE 23 | subjectKeyIdentifier = hash 24 | keyUsage = nonRepudiation, digitalSignature, keyEncipherment, dataEncipherment 25 | extendedKeyUsage = critical,serverAuth, clientAuth 26 | subjectAltName = critical,DNS:localhost,email:move 27 | -------------------------------------------------------------------------------- /examples/examples_resources/certs/tls.cert: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIGDjCCA/agAwIBAgIJAKze3MRdaNp0MA0GCSqGSIb3DQEBDQUAMIGQMRkwFwYD 3 | VQQKDBBKaW5kw4XCmWljaCBMdGQuMQ4wDAYDVQQLDAVTcXVhZDEeMBwGCSqGSIb3 4 | DQEJARYPbnVsbEBza2FsaWNhLmN6MRAwDgYDVQQHDAdTa2FsaWNhMRAwDgYDVQQI 5 | DAdCb2hlbWlhMQswCQYDVQQGEwJDWjESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTIy 6 | MTAyNTEyMjU1N1oXDTIyMTEyNDEyMjU1N1owcDEZMBcGA1UECgwQSmluZMOFwplp 7 | Y2ggTHRkLjEOMAwGA1UECwwFU3F1YWQxEDAOBgNVBAcMB1NrYWxpY2ExEDAOBgNV 8 | BAgMB0JvaGVtaWExCzAJBgNVBAYTAkNaMRIwEAYDVQQDDAlsb2NhbGhvc3QwggIi 9 | MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCsVXDSAeILBLDCRa8ytnqC6y7b 10 | PADSn/QvyHJFyS6l5slahHfIqbhZsX3OrqfulnH7bpYVXvLIGJVOZFM0uJhc0cVs 11 | B9AkywGH90A/FRZ7sp+POCGn86rKKpPJ4rkRLVzWShRODdSN5pT1la3GnP16heCV 12 | EFNzrddeMjYjNWHCs2hGFalx56dlgGrgPnPVH18D/z2t07V9leA0KgvvTS1qPdQ/ 13 | 1b3tQn7T/LN8Cq/OYB3Zy0wJVuDIDY9QA9ID2yxNAFpK0qrQ+Oc/wjwpgirKwvYH 14 | kVNxLIZkeUZL5VYF0HG4Y9dRsyOBYH7fff33piKtjoiEx0mJVZucwzAgKB4h2Is1 15 | gxtLroSVfgFauiJqj81n23uT73LU5jjwZjpEVCxje8HpNdz5hvtIWxEfH9mtJ64P 16 | KgVjDGb8BJpJJQj+xWQ5P6qixDo0PySFoZwSKTwzTidk/X1eTN7Eanvy8crugHKk 17 | eI92ImB3RDEixIeB7VHJlH/JTsMyujZKjBTcFK0c6qRcKqZE5MPX3eyL+UIKZphT 18 | HnbEDFKPygGwRjhQVM/bIrfXSdmcN88EbhjWcD+Uk+KZRX7h77FWyhqW/JvX1kS3 19 | RafrM+uW/4cFcQQEiLUxjbWh+eZhTFOkMKnkvfB43IANYM/Y5akaayJcnHf/qRJL 20 | 8iVs4rOR7/D+A0ic0wIDAQABo4GJMIGGMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYE 21 | FLpfLEkIbyhwgnCboP08keGhweKvMAsGA1UdDwQEAwIE8DAgBgNVHSUBAf8EFjAU 22 | BggrBgEFBQcDAQYIKwYBBQUHAwIwKAYDVR0RAQH/BB4wHIIJbG9jYWxob3N0gQ9u 23 | dWxsQHNrYWxpY2EuY3owDQYJKoZIhvcNAQENBQADggIBAFGm2y9Uu/E50V8xDJs2 24 | KP8leERMLJvjCnu+xSvtSxCu6DEvoyWjJiteBfsm7Fy2vcyhRHjC8yv9ReWgjddE 25 | FH/lbxASdLmeYS57NBTbuzpOcnsBUGSqvWZa1t/ubGVT0sQ0mWH16RBsIVfNN3zk 26 | f48E0XWoxzdAvEGEfgV/6Ggfa4wQGrnbgG+e5GH/8G0ALckErwaD8maX5cOVdCoI 27 | FErGURuz+e6Fr7Wg1A+EqT/390Rfw98GqOfJB+dZL+t9kQxFzzhVaozgyHtMHNYp 28 | GTPOWoq86+Y1fiE7qSMMMxPxjH3ksm3NDFndfpu4ilJHMW+qvOamYgx6J+4IFLDd 29 | SZdIJoAJUxpndgSI1ruWzd6v1vPKbXgYJZhF7Xt0Ds2q5PRlZcV2jxwm/1r8p80Z 30 | WEg9y0yGJzumPZNXNBKN886R79V6PjC+jidaLLh4W4KoQC0BwH/9zPFIXX2FVLeJ 31 | iA9Zvwki1LQqi7e9GYWIcaAT/8+jRY1U+XG8Qr2sRYcao5dKcyYMyz+VapJajByA 32 | K4Km/kCahC8S/DvF4Eowo+OXd8Skvk5QzoWigh/8KB8v6iYVyuLz3iQtpOJvIOKK 33 | Glc3n8ZHBVEBNdHjWv/tiveeol25wNwI3pMqg/5ba6EP731jjTqjhl8BailQ35Tq 34 | lfimh+UkuejJUV+bcRIeK6Ru 35 | -----END CERTIFICATE----- 36 | -------------------------------------------------------------------------------- /examples/examples_resources/certs/tls.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQCsVXDSAeILBLDC 3 | Ra8ytnqC6y7bPADSn/QvyHJFyS6l5slahHfIqbhZsX3OrqfulnH7bpYVXvLIGJVO 4 | ZFM0uJhc0cVsB9AkywGH90A/FRZ7sp+POCGn86rKKpPJ4rkRLVzWShRODdSN5pT1 5 | la3GnP16heCVEFNzrddeMjYjNWHCs2hGFalx56dlgGrgPnPVH18D/z2t07V9leA0 6 | KgvvTS1qPdQ/1b3tQn7T/LN8Cq/OYB3Zy0wJVuDIDY9QA9ID2yxNAFpK0qrQ+Oc/ 7 | wjwpgirKwvYHkVNxLIZkeUZL5VYF0HG4Y9dRsyOBYH7fff33piKtjoiEx0mJVZuc 8 | wzAgKB4h2Is1gxtLroSVfgFauiJqj81n23uT73LU5jjwZjpEVCxje8HpNdz5hvtI 9 | WxEfH9mtJ64PKgVjDGb8BJpJJQj+xWQ5P6qixDo0PySFoZwSKTwzTidk/X1eTN7E 10 | anvy8crugHKkeI92ImB3RDEixIeB7VHJlH/JTsMyujZKjBTcFK0c6qRcKqZE5MPX 11 | 3eyL+UIKZphTHnbEDFKPygGwRjhQVM/bIrfXSdmcN88EbhjWcD+Uk+KZRX7h77FW 12 | yhqW/JvX1kS3RafrM+uW/4cFcQQEiLUxjbWh+eZhTFOkMKnkvfB43IANYM/Y5aka 13 | ayJcnHf/qRJL8iVs4rOR7/D+A0ic0wIDAQABAoICABd8vZehshEWXpDbtnkO7buS 14 | Ghy/HM2YB0vL4eco+DacMa0oiLrMhteSnTbiDvkxf+9orwPSoPwsfYzll4GD9yAP 15 | ZvzGQ9P/5wGE7Tirwxiwy5ZVbCDb0Nck3meRgu+YYyLBjerlb6a3Wm3uLHT5SIK+ 16 | ZFFsnDMT6lpoNYCu8df7Y6bJpQJlNCddTTOqz1aoo+aDjwB17jJjjt8NK/s9ZYcp 17 | Thg1JWGEwoxZ0IyDWFqkNu1MC9zp/lDFqFabbrAf2vXTqnqwI5+/jKTf8BsoJnTk 18 | BWwanmi6TMa80Jvfcc3xDs62rM3xwFENubz0Cv0Jo4hL+Mc/8gHJrR+8an63eQOi 19 | l/R2TIQZvd7b4MpbH6geMAO2mLM6ASwdMuomuk7bqh4i3KMtSsOqq6cJN5qN4K2J 20 | ZC1NpfBn4/HTz7uDluCEflPuf0zJ8lCJfBAtl59LlIVBANEhdGzM+brWZuCmsm/+ 21 | EdjyHXjA9jux2IuTBguvMQvV6PHRhpvbADuAUN5OpfNrIUAl5jMbcnGdxWdK9JM+ 22 | weXvwFBvO6uEpf41KSOdH0jZ2MFvZ2joZg/0fJAHZxWoHTzQBsnzVN0/YX4VnAB9 23 | zp1wRTZwlKI1bMIbmsZucCWDccgSilriQ9OmduSL8jWOWyX9jMmNhSvFyDGWhLPP 24 | aiCLKCzhT4MBxRvgD+ZBAoIBAQDaQqCMib6PBh1YtKCl4QZrE2zmey96rkSh3hSe 25 | A0/d8wPCFj2nywWyGnrUtcx2ASPBQ+pQIzXZIArNHOvsfxH2T8K2Xog8BGlzOPql 26 | 7p8qBG6LZZuqSkvmkC/fzGNDPUCqwNwmFmv/riE5+xLcPayUTT0/trkp2RN+OfFi 27 | JTlXuZDvNuxYgDPZiVfgCUuJF6IYaxWLC9Ymlv09G/UpdZrgy16FHwXaTGQc+50z 28 | c/mzsZ6TtEWdL+pGaevMUPIInbtI/qPkcgUx+Cmn0e4TMUgYiIF1hl04GI05XuwU 29 | iSEe9kGQh90uuvL1XtuBNJIls4KM8hVDHvrVVFWRR0gONnubAoIBAQDKIdv6UuMK 30 | koPKDZvQyX7NeocXf12dVyHpWv3mH6MCD9I5/gpsxIpRZqh4S2TZcfJ4IqS90UPY 31 | 7XiA7ol+LHy0g//bLNOIHwWAAjHbd+KRgkNmELcIECfMRH821PZbk8rwlUOuHmN8 32 | 81IGa8oZ/1oR6uVTKHRjChFuT9ofYNQp2xGC6uDFAxJB+9+2m3Q/N2nt9SjkhWge 33 | 7+LKwwbNN2TohwJ8w0HIQwkbMwGU2p2RlqS9uBovQX9WxO+995MYKQr1oBfEpUE2 34 | fAC3PEUZWv1KuguXkwXO6yLua+TeuHdqgZFGrPjNdZGb8tmZOHExdPlEFMAa4nI9 35 | KtWcrpfjFAMpAoIBAHFkLATXiyjDBHwRW2TSg4MdlHYpiYEzCHUP66YsElI87rbm 36 | 1yFVWKAvIaFg0dh8vxapMhJwOImVHAdz/x3e5nYQ+hfFBQIpSJ+T+qQ6VHZ/1u6U 37 | 20qdTtF6F2UtymQkbnRHvhgLjhBHZvu4dRP29rIVbryrMYeMP5RUhhN3Q1NQFPwy 38 | jJduA4IA2KaMLbILlSsadxbGD3v89ZPJ8pSXhN9EyNZgR6oiBeEI16Ljnda9cKVM 39 | At6nBg+O9/IuG5BeYe6KXJtSoWBUjU+iwQ03jT0xrhBgvg1ms/gaWIxSseJkDawI 40 | 6eBdP9w6a8+0gDkWCb0wB9vXPHmYVtwjLEw2AgECggEAajfmyCGca6bYmGoUUmBA 41 | QSw9J0zn3dG24VDOkYpYd7HmsFDeG5Age2wt5aEA6v9lAlp6JcF9HNaVd3NiTyqD 42 | kby3y+4/bo2Wr1D38DOMnRhN4Kmx9QvATihEjYTVvQPqJgjaGvqfHz8iAHvOJWE5 43 | bKb6QXvFxXG5/TT7E3gnpaMYDart2Lmnc4MvaV9BdCLjiIdHKOct8uvuSsy3m0mb 44 | vlGMLhHRVLJda6yfDSDgomv+QDApmmGZz/gHX9Vkt9KBjtfFBbItlbsOCTwjt5JH 45 | /mfLxagd6kFIBvAtwhg/sHlL5U7qI9W/Yar5S/oMCYhFoNAirz4F+Dy1KfPZnxua 46 | GQKCAQEAvE/MsKeTUW1HDddJP2U3R/fy6G5ka3NQim/egyY985VCCQQHl+0nW2a/ 47 | sbE2bWKH+o8wbTJDenESZjIsqLo0WcO7iB2HyGYxhWyFv9RItt10vhW0SO73C6Vj 48 | thiYfVPOP6QVtHGaSwvrllaJ+nAqIf3hYAuASAeefBL3jC+Kn/yx+WznEEsXqCwk 49 | 8pcmT13T0ixdWCu/wkkNRM75tamILEdXKUpFyVVNiVyHtLSNpReJJZh8WxVx1/Zz 50 | OTgXBAawnVBIHbHwYEC74cEC1CR3Pez0R67b3cebNX6/LZCjAgvqEszE1YirLnc2 51 | dFPkVFibmuDEECBgiDtwQcX1DUNpHA== 52 | -----END PRIVATE KEY----- 53 | -------------------------------------------------------------------------------- /examples/examples_resources/proto/echo.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package echo; 4 | 5 | service Echo { 6 | rpc ping (EchoRequest) returns (EchoResponse); 7 | } 8 | 9 | message EchoRequest 10 | { 11 | string message = 1; 12 | } 13 | 14 | message EchoResponse 15 | { 16 | string message = 1; 17 | } 18 | -------------------------------------------------------------------------------- /examples/examples_resources/proto/hello.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package hello; 4 | 5 | service Hello { 6 | rpc SayHello (HelloRequest) returns (HelloResponse) {} 7 | } 8 | 9 | message HelloRequest { 10 | string name = 1; 11 | } 12 | 13 | message HelloResponse { 14 | string message = 1; 15 | } 16 | -------------------------------------------------------------------------------- /examples/examples_resources/src/grpc.rs: -------------------------------------------------------------------------------- 1 | use crate::proto::{ 2 | echo::{echo_server::Echo, EchoRequest, EchoResponse}, 3 | hello::{hello_server::Hello, HelloRequest, HelloResponse}, 4 | }; 5 | use tonic::{async_trait, Request as TonicRequest, Response as TonicResponse, Status}; 6 | 7 | #[derive(Default)] 8 | pub struct MyHello; 9 | 10 | #[derive(Default)] 11 | pub struct MyEcho; 12 | 13 | #[async_trait] 14 | impl Hello for MyHello { 15 | async fn say_hello( 16 | &self, 17 | request: TonicRequest, 18 | ) -> Result, Status> { 19 | let reply = HelloResponse { 20 | message: format!("Hello From Tonic Server {}!", request.into_inner().name), 21 | }; 22 | 23 | Ok(TonicResponse::new(reply)) 24 | } 25 | } 26 | 27 | #[async_trait] 28 | impl Echo for MyEcho { 29 | async fn ping( 30 | &self, 31 | request: TonicRequest, 32 | ) -> Result, Status> { 33 | let reply = EchoResponse { 34 | message: request.into_inner().message, 35 | }; 36 | 37 | Ok(TonicResponse::new(reply)) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /examples/examples_resources/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod grpc; 2 | #[allow(clippy::derive_partial_eq_without_eq, clippy::large_enum_variant)] 3 | pub mod proto; 4 | 5 | use tonic::{ 6 | body::BoxBody, 7 | codegen::http::{self}, 8 | Status, 9 | }; 10 | 11 | pub const FILE_DESCRIPTOR_SET: &[u8] = include_bytes!("./proto/description.bin"); 12 | 13 | pub async fn deny_middleware( 14 | _req: hyper::Request, 15 | _next: Next, 16 | ) -> http::Response { 17 | Status::permission_denied("You shall not pass").to_http() 18 | } 19 | -------------------------------------------------------------------------------- /examples/examples_resources/src/proto/description.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elefant-dev/fregate-rs/311a4fc2adf9efbd88d39eef0b45c31c468f37b8/examples/examples_resources/src/proto/description.bin -------------------------------------------------------------------------------- /examples/examples_resources/src/proto/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod echo { 2 | include!("echo.rs"); 3 | } 4 | pub mod hello { 5 | include!("hello.rs"); 6 | } 7 | -------------------------------------------------------------------------------- /examples/grafana/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "grafana" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [[bin]] 7 | name = "server" 8 | path = "src/server.rs" 9 | 10 | [[bin]] 11 | name = "client" 12 | path = "src/client.rs" 13 | 14 | [dependencies] 15 | fregate = { path = "../..", features = ["reqwest", "tokio-metrics"] } 16 | resources = { path = "../examples_resources" } 17 | 18 | opentelemetry = { version = "0.19.*", features = ["rt-tokio", "trace"]} 19 | reqwest = "0.11.*" 20 | tokio = { version = "1", features = ["rt-multi-thread"] } 21 | tracing-opentelemetry = "0.19.*" 22 | -------------------------------------------------------------------------------- /examples/grafana/README.md: -------------------------------------------------------------------------------- 1 | # Traces 2 | Example shows Tonic grpc Client and Server OpenTelemetry context propagation and export to Grafana Cloud's Tempo service. 3 | 4 | To export traces to grafana you will need to set up [otel-config.yaml](https://github.com/elefant-dev/fregate-rs/blob/main/examples/grafana/otel-config.yaml) 5 | You might want to refer to [grafana tutorial](https://grafana.com/blog/2021/04/13/how-to-send-traces-to-grafana-clouds-tempo-service-with-opentelemetry-collector) on how to get Grafana Cloud URL and credentials for Tempo. 6 | After otel-config.yaml set up run: 7 | 8 | ```zsh 9 | docker run -v {path to your otel-config.yaml}:/etc/otelcol/config.yaml -d -p 4317:4317 otel/opentelemetry-collector:0.54.0 10 | ``` 11 | 12 | Run Server: 13 | ```zsh 14 | cargo run --package grafana --bin server 15 | ``` 16 | 17 | Run Client: 18 | ```zsh 19 | cargo run --package grafana --bin client 20 | ``` 21 | 22 | View traces in grafana: 23 | ![](traces.png) 24 | -------------------------------------------------------------------------------- /examples/grafana/otel-config.yaml: -------------------------------------------------------------------------------- 1 | receivers: 2 | otlp: 3 | protocols: 4 | grpc: 5 | http: 6 | 7 | processors: 8 | batch: 9 | 10 | exporters: 11 | otlp: 12 | endpoint: tempo-eu-west-0.grafana.net:443 13 | headers: 14 | authorization: Basic 15 | 16 | service: 17 | pipelines: 18 | traces: 19 | receivers: [otlp] 20 | processors: [batch] 21 | exporters: [otlp] 22 | logs: 23 | receivers: [otlp] 24 | processors: [batch] 25 | exporters: [otlp] 26 | -------------------------------------------------------------------------------- /examples/grafana/src/client.rs: -------------------------------------------------------------------------------- 1 | use fregate::extensions::{ReqwestExt, TonicReqExt}; 2 | use fregate::hyper::StatusCode; 3 | use fregate::observability::init_tracing; 4 | use fregate::{tokio, tonic, tracing, LoggerConfig}; 5 | use opentelemetry::global::shutdown_tracer_provider; 6 | use reqwest::Url; 7 | use resources::proto::hello::{hello_client::HelloClient, HelloRequest, HelloResponse}; 8 | use tonic::transport::Channel; 9 | use tonic::{Request, Response, Status}; 10 | use tracing::info; 11 | 12 | #[tracing::instrument(name = "get_check_status")] 13 | async fn get_check_status() -> StatusCode { 14 | let http_client = reqwest::Client::new(); 15 | 16 | let response = http_client 17 | .get(Url::parse("http://0.0.0.0:8000/check").unwrap()) 18 | .inject_from_current_span() 19 | .send() 20 | .await 21 | .unwrap(); 22 | 23 | response.status() 24 | } 25 | 26 | #[tracing::instrument(name = "send_hello")] 27 | async fn send_hello( 28 | client: &mut HelloClient, 29 | mut request: Request, 30 | ) -> Result, Status> { 31 | if get_check_status().await == StatusCode::OK { 32 | request.inject_from_current_span(); 33 | 34 | info!("Outgoing Request: {:?}", request); 35 | let response = client.say_hello(request).await; 36 | info!("Incoming Response: {:?}", response); 37 | response 38 | } else { 39 | Err(Status::cancelled("Service is unhealthy")) 40 | } 41 | } 42 | 43 | #[tokio::main] 44 | async fn main() -> Result<(), Box> { 45 | let logger_config = LoggerConfig { 46 | log_level: "info".to_string(), 47 | logging_path: None, 48 | logging_file: None, 49 | msg_length: None, 50 | buffered_lines_limit: None, 51 | logging_interval: None, 52 | logging_max_file_size: None, 53 | logging_max_history: None, 54 | logging_max_file_count: None, 55 | logging_enable_compression: false, 56 | headers_filter: None, 57 | }; 58 | 59 | let _guard = init_tracing( 60 | &logger_config, 61 | "info", 62 | "0.0.0", 63 | "fregate", 64 | "client", 65 | Some("http://0.0.0.0:4317"), 66 | ) 67 | .unwrap(); 68 | 69 | let channel = tonic::transport::Endpoint::from_static("http://0.0.0.0:8000") 70 | .connect() 71 | .await 72 | .unwrap(); 73 | 74 | let mut client = HelloClient::new(channel); 75 | let request = Request::new(HelloRequest { 76 | name: "Tonic".into(), 77 | }); 78 | 79 | send_hello(&mut client, request).await?; 80 | 81 | shutdown_tracer_provider(); 82 | Ok(()) 83 | } 84 | -------------------------------------------------------------------------------- /examples/grafana/src/server.rs: -------------------------------------------------------------------------------- 1 | use axum::Router; 2 | use fregate::axum::routing::get; 3 | use fregate::hyper::StatusCode; 4 | use fregate::{axum, bootstrap, extensions::RouterTonicExt, tokio, AppConfig, Application}; 5 | use resources::{grpc::MyHello, proto::hello::hello_server::HelloServer}; 6 | 7 | #[tokio::main] 8 | async fn main() { 9 | std::env::set_var("OTEL_COMPONENT_NAME", "server"); 10 | std::env::set_var("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", "http://0.0.0.0:4317"); 11 | 12 | let config: AppConfig = bootstrap([]).unwrap(); 13 | let hello_service = HelloServer::new(MyHello); 14 | 15 | let rest = Router::new().route("/check", get(|| async { StatusCode::OK })); 16 | let grpc = Router::from_tonic_service(hello_service); 17 | 18 | let router = grpc.merge(rest); 19 | 20 | Application::new(config) 21 | .router(router) 22 | .serve() 23 | .await 24 | .unwrap(); 25 | } 26 | -------------------------------------------------------------------------------- /examples/grafana/traces.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elefant-dev/fregate-rs/311a4fc2adf9efbd88d39eef0b45c31c468f37b8/examples/grafana/traces.png -------------------------------------------------------------------------------- /examples/health/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "health" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | fregate = { path = "../.." } 8 | tokio = { version = "1", features = ["rt-multi-thread"] } 9 | -------------------------------------------------------------------------------- /examples/health/src/main.rs: -------------------------------------------------------------------------------- 1 | use fregate::hyper::StatusCode; 2 | use fregate::{axum, bootstrap, health::HealthExt, Application}; 3 | use fregate::{tokio, AppConfig}; 4 | use std::sync::atomic::{AtomicU8, Ordering}; 5 | use std::sync::Arc; 6 | 7 | #[derive(Default, Debug, Clone)] 8 | pub struct CustomHealth { 9 | status: Arc, 10 | } 11 | 12 | #[axum::async_trait] 13 | impl HealthExt for CustomHealth { 14 | type HealthResponse = (StatusCode, &'static str); 15 | type ReadyResponse = StatusCode; 16 | 17 | async fn alive(&self) -> Self::HealthResponse { 18 | (StatusCode::OK, "OK") 19 | } 20 | 21 | async fn ready(&self) -> Self::ReadyResponse { 22 | match self.status.fetch_add(1, Ordering::SeqCst) { 23 | 0..=2 => StatusCode::SERVICE_UNAVAILABLE, 24 | _ => StatusCode::OK, 25 | } 26 | } 27 | } 28 | 29 | #[tokio::main] 30 | async fn main() { 31 | let config: AppConfig = bootstrap([]).unwrap(); 32 | 33 | Application::new(config) 34 | .health_indicator(CustomHealth::default()) 35 | .serve() 36 | .await 37 | .unwrap(); 38 | } 39 | 40 | /* 41 | curl http://0.0.0.0:8000/health 42 | curl http://0.0.0.0:8000/live 43 | curl http://0.0.0.0:8000/ready 44 | */ 45 | -------------------------------------------------------------------------------- /examples/hello-world/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hello-world" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | fregate = { path = "../.." } 8 | tokio = { version = "1", features = ["rt-multi-thread"] } 9 | -------------------------------------------------------------------------------- /examples/hello-world/src/main.rs: -------------------------------------------------------------------------------- 1 | use fregate::{ 2 | axum::{routing::get, Router}, 3 | bootstrap, tokio, AppConfig, Application, 4 | }; 5 | 6 | async fn handler() -> &'static str { 7 | "Hello, World!" 8 | } 9 | 10 | #[tokio::main] 11 | async fn main() { 12 | let config: AppConfig = bootstrap([]).unwrap(); 13 | 14 | Application::new(config) 15 | .router(Router::new().route("/", get(handler))) 16 | .serve() 17 | .await 18 | .unwrap(); 19 | } 20 | -------------------------------------------------------------------------------- /examples/log-level-change/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "log-level-change" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | fregate = { path = "../.." } 8 | tracing-subscriber = "0.3.15" 9 | tokio = { version = "1", features = ["rt-multi-thread"] } 10 | -------------------------------------------------------------------------------- /examples/log-level-change/src/main.rs: -------------------------------------------------------------------------------- 1 | use fregate::observability::{LOG_LAYER_HANDLE, OTLP_LAYER_HANDLE}; 2 | use fregate::tracing::trace_span; 3 | use fregate::{ 4 | axum::{routing::get, Router}, 5 | bootstrap, Application, 6 | }; 7 | use fregate::{tokio, AppConfig}; 8 | use std::str::FromStr; 9 | use std::time::Duration; 10 | use tracing_subscriber::EnvFilter; 11 | 12 | // Change log level after 10 seconds. 13 | // Default log level is INFO 14 | // Will be changed to TRACE 15 | #[tokio::main] 16 | async fn main() { 17 | std::env::set_var("OTEL_COMPONENT_NAME", "server"); 18 | std::env::set_var("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", "http://0.0.0.0:4317"); 19 | 20 | let config: AppConfig = bootstrap([]).unwrap(); 21 | 22 | tokio::spawn(async move { 23 | tokio::time::sleep(Duration::from_secs(10)).await; 24 | 25 | let trace_span = trace_span!("This won't be sent by default"); 26 | drop(trace_span); 27 | 28 | let log_filter_reloader = LOG_LAYER_HANDLE.get().unwrap(); 29 | 30 | log_filter_reloader 31 | .modify(|filter| *filter = EnvFilter::from_str("trace").unwrap()) 32 | .unwrap(); 33 | 34 | let trace_filter_reloader = OTLP_LAYER_HANDLE.get().unwrap(); 35 | trace_filter_reloader 36 | .modify(|filter| *filter = EnvFilter::from_str("trace").unwrap()) 37 | .unwrap(); 38 | 39 | let trace_span = trace_span!("Will be sent after reload"); 40 | drop(trace_span); 41 | }); 42 | 43 | let rest = Router::new().route("/", get(handler)); 44 | 45 | Application::new(config).router(rest).serve().await.unwrap(); 46 | } 47 | 48 | async fn handler() -> &'static str { 49 | "Hello, World!" 50 | } 51 | 52 | /* 53 | curl http://0.0.0.0:8000 54 | */ 55 | -------------------------------------------------------------------------------- /examples/metrics-callback/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "metrics-callback" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | fregate = { path = "../..", features = ["tokio-metrics"] } 8 | 9 | metrics = "0.21.*" 10 | tokio = { version = "1", features = ["rt-multi-thread"] } 11 | -------------------------------------------------------------------------------- /examples/metrics-callback/src/main.rs: -------------------------------------------------------------------------------- 1 | use fregate::{ 2 | axum::{routing::get, Router}, 3 | bootstrap, tokio, AppConfig, Application, 4 | }; 5 | use metrics::counter; 6 | use std::alloc::{GlobalAlloc, Layout}; 7 | use std::sync::atomic::{AtomicU64, Ordering}; 8 | 9 | static ALLOC: AtomicU64 = AtomicU64::new(0); 10 | static DEALLOC: AtomicU64 = AtomicU64::new(0); 11 | 12 | async fn handler() -> &'static str { 13 | "Hello, World!" 14 | } 15 | 16 | #[tokio::main] 17 | async fn main() { 18 | let config: AppConfig = bootstrap([]).unwrap(); 19 | 20 | Application::new(config) 21 | .router(Router::new().route("/", get(handler))) 22 | .metrics_callback(|| { 23 | counter!("allocations", ALLOC.load(Ordering::Relaxed)); 24 | counter!("deallocations", DEALLOC.load(Ordering::Relaxed)); 25 | }) 26 | .serve() 27 | .await 28 | .unwrap(); 29 | } 30 | 31 | pub struct SystemWrapper; 32 | 33 | unsafe impl GlobalAlloc for SystemWrapper { 34 | unsafe fn alloc(&self, layout: Layout) -> *mut u8 { 35 | let ret = std::alloc::System.alloc(layout); 36 | if !ret.is_null() { 37 | ALLOC.fetch_add(1, Ordering::Relaxed); 38 | } 39 | ret 40 | } 41 | 42 | unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) { 43 | std::alloc::System.dealloc(ptr, layout); 44 | DEALLOC.fetch_add(1, Ordering::Relaxed); 45 | } 46 | 47 | unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 { 48 | let ret = std::alloc::System.alloc_zeroed(layout); 49 | if layout.align() <= MIN_ALIGN && layout.align() <= layout.size() && !ret.is_null() { 50 | ALLOC.fetch_add(1, Ordering::Relaxed); 51 | } 52 | ret 53 | } 54 | 55 | unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 { 56 | let ret = std::alloc::System.realloc(ptr, layout, new_size); 57 | if layout.align() <= MIN_ALIGN && layout.align() <= layout.size() && !ret.is_null() { 58 | ALLOC.fetch_add(1, Ordering::Relaxed); 59 | } 60 | ret 61 | } 62 | } 63 | 64 | // FROM: std::sys::common::alloc::MIN_ALIGN 65 | // The minimum alignment guaranteed by the architecture. This value is used to 66 | // add fast paths for low alignment values. 67 | #[cfg(any( 68 | target_arch = "x86", 69 | target_arch = "arm", 70 | target_arch = "mips", 71 | target_arch = "powerpc", 72 | target_arch = "powerpc64", 73 | target_arch = "sparc", 74 | target_arch = "asmjs", 75 | target_arch = "wasm32", 76 | target_arch = "hexagon", 77 | all(target_arch = "riscv32", not(target_os = "espidf")), 78 | all(target_arch = "xtensa", not(target_os = "espidf")), 79 | ))] 80 | pub const MIN_ALIGN: usize = 8; 81 | #[cfg(any( 82 | target_arch = "x86_64", 83 | target_arch = "aarch64", 84 | target_arch = "mips64", 85 | target_arch = "s390x", 86 | target_arch = "sparc64", 87 | target_arch = "riscv64", 88 | target_arch = "wasm64", 89 | ))] 90 | pub const MIN_ALIGN: usize = 16; 91 | // The allocator on the esp-idf platform guarantees 4 byte alignment. 92 | #[cfg(any( 93 | all(target_arch = "riscv32", target_os = "espidf"), 94 | all(target_arch = "xtensa", target_os = "espidf"), 95 | ))] 96 | pub const MIN_ALIGN: usize = 4; 97 | -------------------------------------------------------------------------------- /examples/proxy-layer/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "proxy-layer" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | fregate = { path = "../.." } 8 | tokio = { version = "1", features = ["rt-multi-thread"] } 9 | -------------------------------------------------------------------------------- /examples/proxy-layer/src/main.rs: -------------------------------------------------------------------------------- 1 | use fregate::axum::response::IntoResponse; 2 | use fregate::axum::routing::get; 3 | use fregate::axum::{Json, Router}; 4 | use fregate::hyper::{Body, HeaderMap, Request, Response, StatusCode}; 5 | use fregate::middleware::{ProxyError, ProxyLayer}; 6 | use fregate::ConfigSource::EnvPrefix; 7 | use fregate::{axum, bootstrap, hyper, tracing, AppConfig, Application, Empty}; 8 | use std::sync::atomic::{AtomicUsize, Ordering}; 9 | use std::sync::Arc; 10 | 11 | fn on_proxy_request(request: &mut Request, _ext: &()) { 12 | let headers = request.headers_mut(); 13 | headers.insert("CallbackHeader", "true".parse().unwrap()); 14 | tracing::info!("Proxy sends request to: {}", request.uri()); 15 | } 16 | 17 | fn on_proxy_response(response: &mut Response, _ext: &()) { 18 | tracing::info!("Proxy response status code: {}", response.status()); 19 | } 20 | 21 | fn on_proxy_error(err: ProxyError, _ext: &()) -> axum::response::Response { 22 | tracing::info!("Proxy error: {:?}", err); 23 | StatusCode::INTERNAL_SERVER_ERROR.into_response() 24 | } 25 | 26 | // Proxy every second request. 27 | async fn should_proxy(counter: Arc) -> Result { 28 | let cnt = counter.fetch_add(1, Ordering::Acquire); 29 | 30 | if cnt % 3 == 0 { 31 | Err(( 32 | StatusCode::INTERNAL_SERVER_ERROR, 33 | "Early return from should proxy fn", 34 | ) 35 | .into_response()) 36 | } else if cnt % 2 == 0 { 37 | Ok(true) 38 | } else { 39 | Ok(false) 40 | } 41 | } 42 | 43 | #[tokio::main] 44 | async fn main() { 45 | std::env::set_var("LOCAL_PORT", "8001"); 46 | let conf: AppConfig = bootstrap([EnvPrefix("LOCAL")]).unwrap(); 47 | 48 | start_server(); 49 | 50 | let counter = Arc::new(AtomicUsize::new(0)); 51 | let http_client = hyper::Client::new(); 52 | 53 | // You might want apply additional layers to Client. 54 | // use fregate::tower::timeout::TimeoutLayer; 55 | // use fregate::tower::ServiceBuilder; 56 | // use std::time::Duration; 57 | // 58 | // let http_client = ServiceBuilder::new() 59 | // .layer(TimeoutLayer::new(Duration::from_nanos(10))) 60 | // .service(hyper::Client::new()); 61 | 62 | let proxy_layer = ProxyLayer::new( 63 | http_client, 64 | "http://localhost:8000", 65 | on_proxy_error, 66 | on_proxy_request, 67 | on_proxy_response, 68 | move |_request, _ext| Box::pin(should_proxy(counter.clone())), 69 | ) 70 | .unwrap(); 71 | 72 | let local_handler = Router::new() 73 | .route("/local", get(|| async { Json("Hello, Local Handler!") })) 74 | .layer(proxy_layer); 75 | 76 | Application::new(conf) 77 | .router(local_handler) 78 | .use_default_tracing_layer(false) 79 | .serve() 80 | .await 81 | .unwrap(); 82 | } 83 | 84 | async fn remote_handler(headers: HeaderMap) -> impl IntoResponse { 85 | let is_callback_header_found = headers.get("CallbackHeader").is_some(); 86 | Json(format!( 87 | "Hello, Remote Handler!. Found header: {is_callback_header_found}" 88 | )) 89 | } 90 | 91 | fn start_server() { 92 | let remote_handler = Router::new().route("/local", get(remote_handler)); 93 | 94 | // This will start server on 8000 port by default 95 | tokio::task::spawn(async { 96 | Application::new(AppConfig::::default()) 97 | .router(remote_handler) 98 | .use_default_tracing_layer(false) 99 | .serve() 100 | .await 101 | .unwrap(); 102 | }); 103 | } 104 | 105 | // curl http://0.0.0.0:8001/local 106 | -------------------------------------------------------------------------------- /examples/reflection/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "reflection" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | fregate = { path = "../.." } 8 | resources = { path = "../examples_resources" } 9 | 10 | tokio = { version = "1", features = ["rt-multi-thread"] } 11 | tonic-reflection = "0.9.*" 12 | -------------------------------------------------------------------------------- /examples/reflection/src/main.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::derive_partial_eq_without_eq)] 2 | 3 | use axum::{routing::get, Router}; 4 | use fregate::{axum, bootstrap, extensions::RouterTonicExt, tokio, AppConfig, Application}; 5 | use resources::{grpc::MyEcho, proto::echo::echo_server::EchoServer, FILE_DESCRIPTOR_SET}; 6 | 7 | #[tokio::main] 8 | async fn main() { 9 | let config: AppConfig = bootstrap([]).unwrap(); 10 | let echo_service = EchoServer::new(MyEcho); 11 | 12 | let service = tonic_reflection::server::Builder::configure() 13 | .register_encoded_file_descriptor_set(FILE_DESCRIPTOR_SET) 14 | .build() 15 | .unwrap(); 16 | 17 | let reflection = Router::from_tonic_service(service); 18 | 19 | let rest = Router::new().route("/", get(|| async { "Hello, World!" })); 20 | let grpc = Router::from_tonic_service(echo_service); 21 | 22 | let app_router = rest.merge(grpc).merge(reflection); 23 | 24 | Application::new(config) 25 | .router(app_router) 26 | .serve() 27 | .await 28 | .unwrap(); 29 | } 30 | 31 | /* 32 | grpcurl -plaintext 0.0.0.0:8000 list 33 | grpcurl -plaintext -d '{"message": "Echo"}' 0.0.0.0:8000 echo.Echo/ping 34 | curl http://0.0.0.0:8000 35 | curl http://0.0.0.0:8000/health 36 | curl http://0.0.0.0:8000/ready 37 | curl http://0.0.0.0:8000/live 38 | */ 39 | -------------------------------------------------------------------------------- /examples/tls/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tls" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [[bin]] 7 | name = "server" 8 | path = "src/server.rs" 9 | 10 | [[bin]] 11 | name = "client-native-tls" 12 | path = "src/client_native_tls.rs" 13 | # TODO: https://github.com/rust-lang/rfcs/pull/2887 14 | required-features = ["hyper-tls"] 15 | 16 | [[bin]] 17 | name = "client-rustls" 18 | path = "src/client_rustls.rs" 19 | # TODO: https://github.com/rust-lang/rfcs/pull/2887 20 | required-features = ["hyper-rustls", "rustls"] 21 | 22 | [dependencies] 23 | fregate = { path = "../..", features = ["use_native_tls"] } 24 | #fregate = { path = "../..", features = ["use_rustls"] } 25 | #fregate = { path = "../..", features = ["use_rustls_tls12"] } 26 | resources = { path = "../examples_resources" } 27 | 28 | hyper-rustls = { version = "0.24.*", optional = true } 29 | hyper-tls = { version = "0.5.*", optional = true } 30 | rustls = { version = "0.21.*", optional = true, features = ["tls12", "dangerous_configuration"] } 31 | tokio = { version = "1", features = ["net", "rt-multi-thread"] } 32 | tonic-reflection = "0.9.*" 33 | -------------------------------------------------------------------------------- /examples/tls/src/client_native_tls.rs: -------------------------------------------------------------------------------- 1 | use fregate::{hyper, tonic}; 2 | use hyper::{client::HttpConnector, Client, Uri}; 3 | use hyper_tls::{native_tls, HttpsConnector}; 4 | use resources::proto::{ 5 | echo::{echo_client::EchoClient, EchoRequest}, 6 | hello::{hello_client::HelloClient, HelloRequest}, 7 | }; 8 | use tonic::Request; 9 | 10 | #[tokio::main] 11 | async fn main() { 12 | let mut http = HttpConnector::new(); 13 | http.enforce_http(false); 14 | 15 | let tls_connector = native_tls::TlsConnector::builder() 16 | .danger_accept_invalid_certs(true) 17 | .build() 18 | .unwrap() 19 | .into(); 20 | 21 | let https = HttpsConnector::from((http, tls_connector)); 22 | let hyper = Client::builder().http2_only(true).build(https); 23 | let origin: Uri = "https://localhost:8000".parse().unwrap(); 24 | let mut echo_client = EchoClient::with_origin(hyper.clone(), origin.clone()); 25 | let mut hello_client = HelloClient::with_origin(hyper, origin); 26 | 27 | let response = echo_client 28 | .ping(Request::new(EchoRequest { 29 | message: "Hello from Jindřich from Skalica!".to_string(), 30 | })) 31 | .await 32 | .unwrap_err(); 33 | eprintln!("response: `{response:?}`."); 34 | 35 | let response = hello_client 36 | .say_hello(Request::new(HelloRequest { 37 | name: "Jindřich".to_owned(), 38 | })) 39 | .await 40 | .unwrap() 41 | .into_inner(); 42 | println!("response: `{response:?}`."); 43 | } 44 | -------------------------------------------------------------------------------- /examples/tls/src/client_rustls.rs: -------------------------------------------------------------------------------- 1 | use fregate::{hyper, tonic}; 2 | use hyper::{Client, Uri}; 3 | use hyper_rustls::{ConfigBuilderExt, HttpsConnectorBuilder}; 4 | use resources::proto::{ 5 | echo::{echo_client::EchoClient, EchoRequest}, 6 | hello::{hello_client::HelloClient, HelloRequest}, 7 | }; 8 | use rustls::client::{ServerCertVerified, ServerCertVerifier}; 9 | use rustls::{Certificate, ClientConfig, Error, ServerName}; 10 | use std::{sync::Arc, time::SystemTime}; 11 | use tonic::Request; 12 | 13 | struct DummyServerCertVerifier; 14 | 15 | impl ServerCertVerifier for DummyServerCertVerifier { 16 | fn verify_server_cert( 17 | &self, 18 | _: &Certificate, 19 | _: &[Certificate], 20 | _: &ServerName, 21 | _: &mut dyn Iterator, 22 | _: &[u8], 23 | _: SystemTime, 24 | ) -> Result { 25 | Ok(ServerCertVerified::assertion()) 26 | } 27 | } 28 | 29 | #[tokio::main] 30 | async fn main() { 31 | let mut tls = ClientConfig::builder() 32 | .with_safe_defaults() 33 | .with_native_roots() 34 | .with_no_client_auth(); 35 | tls.dangerous() 36 | .set_certificate_verifier(Arc::new(DummyServerCertVerifier)); 37 | 38 | let https = HttpsConnectorBuilder::new() 39 | .with_tls_config(tls) 40 | .https_only() 41 | .enable_http1() 42 | .build(); 43 | let hyper = Client::builder().http2_only(true).build(https); 44 | let origin: Uri = "https://localhost:8000".parse().unwrap(); 45 | let mut echo_client = EchoClient::with_origin(hyper.clone(), origin.clone()); 46 | let mut hello_client = HelloClient::with_origin(hyper, origin); 47 | 48 | let response = echo_client 49 | .ping(Request::new(EchoRequest { 50 | message: "Hello from Jindřich from Skalica!".to_string(), 51 | })) 52 | .await 53 | .unwrap_err(); 54 | eprintln!("response: `{response:?}`."); 55 | 56 | let response = hello_client 57 | .say_hello(Request::new(HelloRequest { 58 | name: "Jindřich".to_owned(), 59 | })) 60 | .await 61 | .unwrap() 62 | .into_inner(); 63 | println!("response: `{response:?}`."); 64 | } 65 | -------------------------------------------------------------------------------- /examples/tls/src/server.rs: -------------------------------------------------------------------------------- 1 | use axum::{middleware::from_fn, routing::get, Router}; 2 | use fregate::{ 3 | axum, bootstrap, extensions::RouterTonicExt, tokio, AppConfig, Application, 4 | ConfigSource::EnvPrefix, 5 | }; 6 | use resources::{ 7 | deny_middleware, 8 | grpc::{MyEcho, MyHello}, 9 | proto::{echo::echo_server::EchoServer, hello::hello_server::HelloServer}, 10 | FILE_DESCRIPTOR_SET, 11 | }; 12 | 13 | #[tokio::main] 14 | async fn main() { 15 | const TLS_KEY_FULL_PATH: &str = concat!( 16 | env!("CARGO_MANIFEST_DIR"), 17 | "/../examples_resources/certs/tls.key" 18 | ); 19 | const TLS_CERTIFICATE_FULL_PATH: &str = concat!( 20 | env!("CARGO_MANIFEST_DIR"), 21 | "/../examples_resources/certs/tls.cert" 22 | ); 23 | 24 | std::env::set_var("TEST_HOST", "::0"); 25 | std::env::set_var("TEST_SERVER_TLS_KEY_PATH", TLS_KEY_FULL_PATH); 26 | std::env::set_var("TEST_SERVER_TLS_CERT_PATH", TLS_CERTIFICATE_FULL_PATH); 27 | 28 | let config: AppConfig = bootstrap([EnvPrefix("TEST")]).unwrap(); 29 | 30 | let grpc_reflection = tonic_reflection::server::Builder::configure() 31 | .register_encoded_file_descriptor_set(FILE_DESCRIPTOR_SET) 32 | .build() 33 | .unwrap(); 34 | let echo_service = EchoServer::new(MyEcho); 35 | let hello_service = HelloServer::new(MyHello); 36 | 37 | let rest = Router::new().route("/", get(|| async { "Hello, World!" })); 38 | 39 | // Echo service will always deny request 40 | let grpc_router = Router::from_tonic_service(echo_service) 41 | .layer(from_fn(deny_middleware)) 42 | .merge(Router::from_tonic_service(hello_service)) 43 | .merge(Router::from_tonic_service(grpc_reflection)); 44 | 45 | let app_router = rest.merge(grpc_router); 46 | 47 | Application::new(config) 48 | .router(app_router) 49 | .serve_tls() 50 | .await 51 | .unwrap(); 52 | } 53 | 54 | /* 55 | grpcurl -insecure -d '{"name": "Tonic"}' 0.0.0.0:8000 hello.Hello/SayHello 56 | grpcurl -insecure -d '{"message": "Echo"}' 0.0.0.0:8000 echo.Echo/ping 57 | curl --insecure https://0.0.0.0:8000 58 | curl --insecure https://0.0.0.0:8000/health 59 | curl --insecure https://0.0.0.0:8000/ready 60 | curl --insecure https://0.0.0.0:8000/live 61 | */ 62 | -------------------------------------------------------------------------------- /examples/tonic/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tonic" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | fregate = { path = "../.." } 8 | resources = { path = "../examples_resources" } 9 | tokio = { version = "1", features = ["rt-multi-thread"] } 10 | -------------------------------------------------------------------------------- /examples/tonic/src/main.rs: -------------------------------------------------------------------------------- 1 | use axum::{middleware::from_fn, routing::get, Router}; 2 | use fregate::{axum, bootstrap, extensions::RouterTonicExt, tokio, AppConfig, Application}; 3 | use resources::{ 4 | deny_middleware, 5 | grpc::{MyEcho, MyHello}, 6 | proto::{echo::echo_server::EchoServer, hello::hello_server::HelloServer}, 7 | }; 8 | 9 | #[tokio::main] 10 | async fn main() { 11 | let config: AppConfig = bootstrap([]).unwrap(); 12 | 13 | let echo_service = EchoServer::new(MyEcho); 14 | let hello_service = HelloServer::new(MyHello); 15 | 16 | let rest = Router::new().route("/", get(|| async { "Hello, World!" })); 17 | 18 | // Echo service will always deny request 19 | let grpc = Router::from_tonic_service(echo_service) 20 | .layer(from_fn(deny_middleware)) 21 | .merge(Router::from_tonic_service(hello_service)); 22 | 23 | let app_router = rest.merge(grpc); 24 | 25 | Application::new(config) 26 | .router(app_router) 27 | .serve() 28 | .await 29 | .unwrap(); 30 | } 31 | 32 | /* 33 | grpcurl -plaintext -import-path ./proto -proto hello.proto -d '{"name": "Tonic"}' 0.0.0.0:8000 hello.Hello/SayHello 34 | grpcurl -plaintext -import-path ./proto -proto echo.proto -d '{"message": "Echo"}' 0.0.0.0:8000 echo.Echo/ping 35 | curl http://0.0.0.0:8000 36 | curl http://0.0.0.0:8000/health 37 | curl http://0.0.0.0:8000/ready 38 | curl http://0.0.0.0:8000/live 39 | */ 40 | -------------------------------------------------------------------------------- /src/application/health.rs: -------------------------------------------------------------------------------- 1 | //! Trait to implement custom Health checks 2 | use axum::http::StatusCode; 3 | use axum::response::IntoResponse; 4 | 5 | /// Trait to implement custom health check which will be used to respond to health check requests 6 | #[axum::async_trait] 7 | pub trait HealthExt: Send + Sync + 'static + Clone { 8 | /// return type for health check 9 | type HealthResponse: IntoResponse; 10 | /// return type for ready check 11 | type ReadyResponse: IntoResponse; 12 | 13 | /// returns [`Self::HealthResponse`] in response to configured endpoint. By default `/health`. 14 | /// For more information see [`crate::configuration::ManagementConfig`] 15 | async fn alive(&self) -> Self::HealthResponse; 16 | 17 | /// returns [`Self::ReadyResponse`] in response to configured endpoint. By default `/ready`. 18 | /// For more information see [`crate::configuration::ManagementConfig`] 19 | async fn ready(&self) -> Self::ReadyResponse; 20 | } 21 | 22 | /// Default structure to mark application always alive and ready. 23 | #[derive(Default, Debug, Clone, Copy)] 24 | pub struct AlwaysReadyAndAlive; 25 | 26 | #[axum::async_trait] 27 | impl HealthExt for AlwaysReadyAndAlive { 28 | type HealthResponse = (StatusCode, &'static str); 29 | type ReadyResponse = (StatusCode, &'static str); 30 | 31 | async fn alive(&self) -> (StatusCode, &'static str) { 32 | (StatusCode::OK, "OK") 33 | } 34 | 35 | async fn ready(&self) -> (StatusCode, &'static str) { 36 | (StatusCode::OK, "OK") 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/application/management.rs: -------------------------------------------------------------------------------- 1 | use crate::application::health::HealthExt; 2 | use crate::observability::render_metrics; 3 | use crate::version::VersionExt; 4 | use crate::{AppConfig, ManagementConfig}; 5 | use axum::{routing::get, Extension, Router}; 6 | use std::sync::Arc; 7 | 8 | pub(crate) fn build_management_router( 9 | app_cfg: &Arc>, 10 | health_indicator: H, 11 | version: V, 12 | callback: Option>, 13 | ) -> Router 14 | where 15 | H: HealthExt, 16 | V: VersionExt, 17 | T: Send + Sync + 'static, 18 | { 19 | Router::new() 20 | .merge(build_health_router( 21 | &app_cfg.management_cfg, 22 | health_indicator, 23 | )) 24 | .merge(build_metrics_router(&app_cfg.management_cfg, callback)) 25 | .merge(build_version_router(app_cfg, version)) 26 | } 27 | 28 | fn build_health_router( 29 | management_cfg: &ManagementConfig, 30 | health_indicator: H, 31 | ) -> Router { 32 | // TODO: separate health and alive handlers 33 | let alive_handler = |health: Extension| async move { health.alive().await }; 34 | let ready_handler = |health: Extension| async move { health.ready().await }; 35 | 36 | Router::new() 37 | .route(management_cfg.endpoints.health.as_ref(), get(alive_handler)) 38 | .route(management_cfg.endpoints.live.as_ref(), get(alive_handler)) 39 | .route(management_cfg.endpoints.ready.as_ref(), get(ready_handler)) 40 | .layer(Extension(health_indicator)) 41 | } 42 | 43 | fn build_metrics_router( 44 | management_cfg: &ManagementConfig, 45 | callback: Option>, 46 | ) -> Router { 47 | Router::new().route( 48 | management_cfg.endpoints.metrics.as_ref(), 49 | get(move || std::future::ready(render_metrics(callback.as_deref()))), 50 | ) 51 | } 52 | 53 | fn build_version_router(app_cfg: &Arc>, version: V) -> Router 54 | where 55 | V: VersionExt, 56 | T: Send + Sync + 'static, 57 | { 58 | let config = Arc::clone(app_cfg); 59 | let endpoint = app_cfg.management_cfg.endpoints.version.as_ref(); 60 | let version_handler = |version: Extension| async move { version.get_version(&config) }; 61 | 62 | Router::new() 63 | .route(endpoint, get(version_handler)) 64 | .layer(Extension(version)) 65 | } 66 | 67 | #[cfg(test)] 68 | #[allow(clippy::unwrap_used)] 69 | mod management_test { 70 | use super::*; 71 | use crate::version::DefaultVersion; 72 | use crate::Empty; 73 | use axum::http::{Request, StatusCode}; 74 | use axum::Json; 75 | use serde::Deserialize; 76 | use tower::ServiceExt; 77 | 78 | #[derive(Debug, Deserialize, Clone)] 79 | pub struct Config { 80 | pub version: String, 81 | } 82 | 83 | #[derive(Default, Debug, Clone)] 84 | pub struct CustomHealth; 85 | 86 | #[derive(Default, Debug, Clone)] 87 | pub struct CustomVersion; 88 | 89 | impl Default for Config { 90 | fn default() -> Self { 91 | Self { 92 | version: "123.220.0".to_owned(), 93 | } 94 | } 95 | } 96 | 97 | #[axum::async_trait] 98 | impl HealthExt for CustomHealth { 99 | type HealthResponse = (StatusCode, &'static str); 100 | type ReadyResponse = (StatusCode, &'static str); 101 | 102 | async fn alive(&self) -> Self::HealthResponse { 103 | (StatusCode::OK, "OK") 104 | } 105 | 106 | async fn ready(&self) -> Self::ReadyResponse { 107 | (StatusCode::SERVICE_UNAVAILABLE, "UNAVAILABLE") 108 | } 109 | } 110 | 111 | impl VersionExt for CustomVersion { 112 | type Response = (StatusCode, Json); 113 | 114 | fn get_version(&self, cfg: &AppConfig) -> Self::Response { 115 | let version = cfg.private.version.clone(); 116 | (StatusCode::OK, Json(version)) 117 | } 118 | } 119 | 120 | #[tokio::test] 121 | async fn health_test() { 122 | let app_cfg = Arc::new(AppConfig::::default()); 123 | 124 | let router = build_management_router(&app_cfg, CustomHealth, DefaultVersion, None); 125 | let request = Request::builder() 126 | .uri("http://0.0.0.0/health") 127 | .method("GET") 128 | .body(hyper::Body::empty()) 129 | .unwrap(); 130 | 131 | let response = router.oneshot(request).await.unwrap(); 132 | let status = response.status(); 133 | let body = hyper::body::to_bytes(response.into_body()).await.unwrap(); 134 | 135 | assert_eq!(StatusCode::OK, status); 136 | assert_eq!(&body[..], b"OK"); 137 | } 138 | 139 | #[tokio::test] 140 | async fn live_test() { 141 | let app_cfg = Arc::new(AppConfig::::default()); 142 | 143 | let router = build_management_router(&app_cfg, CustomHealth, DefaultVersion, None); 144 | let request = Request::builder() 145 | .uri("http://0.0.0.0/live") 146 | .method("GET") 147 | .body(hyper::Body::empty()) 148 | .unwrap(); 149 | 150 | let response = router.oneshot(request).await.unwrap(); 151 | let status = response.status(); 152 | let body = hyper::body::to_bytes(response.into_body()).await.unwrap(); 153 | 154 | assert_eq!(StatusCode::OK, status); 155 | assert_eq!(&body[..], b"OK"); 156 | } 157 | 158 | #[tokio::test] 159 | async fn ready_test() { 160 | let app_cfg = Arc::new(AppConfig::default()); 161 | 162 | let router = build_management_router(&app_cfg, CustomHealth, DefaultVersion, None); 163 | let request = Request::builder() 164 | .uri("http://0.0.0.0/ready") 165 | .method("GET") 166 | .body(hyper::Body::empty()) 167 | .unwrap(); 168 | 169 | let response = router.oneshot(request).await.unwrap(); 170 | let status = response.status(); 171 | let body = hyper::body::to_bytes(response.into_body()).await.unwrap(); 172 | 173 | assert_eq!(StatusCode::SERVICE_UNAVAILABLE, status); 174 | assert_eq!(&body[..], b"UNAVAILABLE"); 175 | } 176 | 177 | #[cfg(not(feature = "tls"))] 178 | #[tokio::test] 179 | async fn version_test() { 180 | use axum::http::HeaderValue; 181 | use std::net::{IpAddr, Ipv4Addr}; 182 | 183 | let app_cfg = Arc::new(AppConfig { 184 | host: IpAddr::V4(Ipv4Addr::LOCALHOST), 185 | port: 8000, 186 | observability_cfg: Default::default(), 187 | management_cfg: Default::default(), 188 | private: Config::default(), 189 | worker_guard: None, 190 | }); 191 | 192 | let router = build_management_router(&app_cfg, CustomHealth, CustomVersion, None); 193 | let request = Request::builder() 194 | .uri("http://0.0.0.0:8000/version") 195 | .method("GET") 196 | .body(hyper::Body::empty()) 197 | .unwrap(); 198 | 199 | let response = router.oneshot(request).await.unwrap(); 200 | let status = response.status(); 201 | let content_type = response.headers().get("Content-Type").cloned(); 202 | let body = hyper::body::to_bytes(response.into_body()).await.unwrap(); 203 | 204 | assert_eq!(StatusCode::OK, status); 205 | assert_eq!( 206 | content_type, 207 | Some(HeaderValue::from_static("application/json")) 208 | ); 209 | assert_eq!(&body[..], b"\"123.220.0\""); 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /src/application/tls.rs: -------------------------------------------------------------------------------- 1 | #[cfg(all(feature = "use_native_tls", feature = "use_rustls"))] 2 | compile_error!("native-tls and rustls cannot be used together"); 3 | 4 | #[cfg(not(all( 5 | feature = "tls", 6 | any(feature = "use_native_tls", feature = "use_rustls") 7 | )))] 8 | compile_error!("can't use tls flags directly"); 9 | 10 | use crate::error::{Error, Result}; 11 | use async_stream::stream; 12 | use axum::Router; 13 | use futures_util::{ 14 | stream::{FuturesUnordered, Stream}, 15 | StreamExt, TryStreamExt, 16 | }; 17 | use hyper::{server::accept, Server}; 18 | pub(crate) use reexport::*; 19 | use std::{sync::Arc, time::Duration}; 20 | use tokio::{ 21 | net::{TcpListener, TcpStream}, 22 | select, 23 | task::JoinHandle, 24 | time::timeout, 25 | }; 26 | use tokio_stream::wrappers::TcpListenerStream; 27 | use tracing::{info, warn}; 28 | 29 | use crate::application::shutdown_signal; 30 | use crate::tls::TlsStream; 31 | use axum::extract::connect_info::Connected; 32 | use std::net::{IpAddr, Ipv4Addr, SocketAddr}; 33 | 34 | pub(in crate::application) async fn run_service( 35 | socket: &SocketAddr, 36 | router: Router, 37 | tls_handshake_timeout: Duration, 38 | pem: Vec, 39 | key: Vec, 40 | ) -> Result<()> { 41 | let acceptor = create_acceptor(&pem, &key)?; 42 | drop((pem, key)); 43 | 44 | let stream = bind_tls_stream(socket, acceptor, tls_handshake_timeout).await?; 45 | let incoming = accept::from_stream(stream); 46 | 47 | let app = router.into_make_service_with_connect_info::(); 48 | let server = Server::builder(incoming).serve(app); 49 | 50 | info!(target: "server", "Started: https://{socket}"); 51 | 52 | Ok(server.with_graceful_shutdown(shutdown_signal()).await?) 53 | } 54 | 55 | #[allow(clippy::useless_conversion)] 56 | async fn bind_tls_stream( 57 | socket: &SocketAddr, 58 | acceptor: TlsAcceptor, 59 | tls_handshake_timeout: Duration, 60 | ) -> Result>> { 61 | let listener = TcpListener::bind(socket).await?; 62 | let mut tcp_stream = TcpListenerStream::new(listener); 63 | 64 | let acceptor = Arc::new(acceptor); 65 | let ret = stream! { 66 | let mut tasks = FuturesUnordered::new(); 67 | 68 | loop { 69 | match fetch_tls_handle_commands(&mut tcp_stream, &mut tasks).await { 70 | Ok(TlsHandleCommands::TcpStream(tcp_stream)) => { 71 | let acceptor = acceptor.clone(); 72 | tasks.push(tokio::task::spawn(async move { 73 | let ret = timeout(tls_handshake_timeout, acceptor.accept(tcp_stream)) 74 | .await 75 | .map_err(|_| Error::TlsHandshakeTimeout)?? 76 | .into(); 77 | Ok::<_, Error>(ret) 78 | })); 79 | }, 80 | Ok(TlsHandleCommands::TlsStream(tls_stream)) => yield Ok(tls_stream), 81 | Ok(TlsHandleCommands::Break) => break, 82 | Err(error) => warn!("Got error on incoming: `{error}`."), 83 | } 84 | } 85 | }; 86 | 87 | Ok(ret) 88 | } 89 | 90 | enum TlsHandleCommands { 91 | TcpStream(TcpStream), 92 | TlsStream(TlsStream), 93 | Break, 94 | } 95 | 96 | async fn fetch_tls_handle_commands( 97 | tcp_stream: &mut TcpListenerStream, 98 | tasks: &mut FuturesUnordered>>, 99 | ) -> Result { 100 | let ret = if tasks.is_empty() { 101 | match tcp_stream.try_next().await? { 102 | None => TlsHandleCommands::Break, 103 | Some(tcp_stream) => TlsHandleCommands::TcpStream(tcp_stream), 104 | } 105 | } else { 106 | select! { 107 | tcp_stream = tcp_stream.try_next() => { 108 | tcp_stream?.map_or(TlsHandleCommands::Break, TlsHandleCommands::TcpStream) 109 | } 110 | tls_stream = tasks.next() => { 111 | #[allow(clippy::expect_used)] 112 | let tls_stream = tls_stream.expect("FuturesUnordered stream can't be closed in ordinary circumstances")??; 113 | TlsHandleCommands::TlsStream(tls_stream) 114 | } 115 | } 116 | }; 117 | 118 | Ok(ret) 119 | } 120 | 121 | #[cfg(feature = "use_native_tls")] 122 | mod reexport { 123 | use crate::error::Result; 124 | use tokio_native_tls::native_tls::{self, Identity}; 125 | use tracing::info; 126 | 127 | pub(crate) type TlsStream = tokio_native_tls::TlsStream; 128 | pub(in crate::application) type TlsAcceptor = tokio_native_tls::TlsAcceptor; 129 | 130 | pub(in crate::application) fn create_acceptor(pem: &[u8], key: &[u8]) -> Result { 131 | info!("Use native-tls"); 132 | 133 | let identity = Identity::from_pkcs8(pem, key)?; 134 | let acceptor = native_tls::TlsAcceptor::new(identity)?; 135 | 136 | Ok(acceptor.into()) 137 | } 138 | } 139 | 140 | #[cfg(feature = "use_rustls")] 141 | mod reexport { 142 | use crate::error::{Error, Result}; 143 | use rustls_pemfile::{certs, pkcs8_private_keys}; 144 | use std::{io::BufReader, sync::Arc}; 145 | use tokio_rustls::rustls::{Certificate, PrivateKey, ServerConfig}; 146 | use tracing::info; 147 | 148 | // Box because of: https://rust-lang.github.io/rust-clippy/master/index.html#large_enum_variant 149 | pub(crate) type TlsStream = Box>; 150 | pub(in crate::application) type TlsAcceptor = tokio_rustls::TlsAcceptor; 151 | 152 | pub(in crate::application) fn create_acceptor(pem: &[u8], key: &[u8]) -> Result { 153 | info!("Use rustls"); 154 | 155 | fn extract_single_key(data: Vec>) -> Result> { 156 | let [data]: [Vec; 1] = data 157 | .try_into() 158 | .map_err(|_| Error::CustomError("expect one key".into()))?; 159 | 160 | Ok(data) 161 | } 162 | 163 | let certs = certs(&mut BufReader::new(pem))? 164 | .drain(..) 165 | .map(Certificate) 166 | .collect::>(); 167 | let key = pkcs8_private_keys(&mut BufReader::new(key)) 168 | .map(extract_single_key)? 169 | .map(PrivateKey)?; 170 | let config = ServerConfig::builder() 171 | .with_safe_defaults() 172 | .with_no_client_auth() 173 | .with_single_cert(certs, key)?; 174 | 175 | Ok(Arc::new(config).into()) 176 | } 177 | } 178 | 179 | #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] 180 | /// Wrapper for SocketAddr to implement [`Connected`] so 181 | /// we can run [`Router::into_make_service_with_connect_info`] with [`TlsStream>`] 182 | pub struct RemoteAddr(pub SocketAddr); 183 | 184 | #[cfg(feature = "use_native_tls")] 185 | impl Connected<&TlsStream> for RemoteAddr { 186 | fn connect_info(target: &TlsStream) -> Self { 187 | Self( 188 | target 189 | .get_ref() 190 | .get_ref() 191 | .get_ref() 192 | .peer_addr() 193 | .unwrap_or(SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 0)), 194 | ) 195 | } 196 | } 197 | 198 | #[cfg(feature = "use_rustls")] 199 | impl Connected<&TlsStream> for RemoteAddr { 200 | fn connect_info(target: &TlsStream) -> Self { 201 | Self( 202 | target 203 | .get_ref() 204 | .0 205 | .peer_addr() 206 | .unwrap_or(SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 0)), 207 | ) 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /src/application/version.rs: -------------------------------------------------------------------------------- 1 | //! Trait to implement custom version response 2 | use crate::AppConfig; 3 | use axum::response::IntoResponse; 4 | 5 | /// Trait to implement custom /version response 6 | pub trait VersionExt: Send + Sync + 'static + Clone { 7 | /// return type for health check 8 | type Response: IntoResponse; 9 | 10 | /// returns [`Self::Response`] in response to configured endpoint. Default: `/version`. 11 | /// For more information see [`crate::configuration::ManagementConfig`] 12 | fn get_version(&self, cfg: &AppConfig) -> Self::Response; 13 | } 14 | 15 | /// Returns plain text version of service. 16 | #[derive(Default, Debug, Clone, Copy)] 17 | pub struct DefaultVersion; 18 | 19 | impl VersionExt for DefaultVersion { 20 | type Response = String; 21 | 22 | fn get_version(&self, cfg: &AppConfig) -> Self::Response { 23 | cfg.observability_cfg.version.clone() 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/bootstrap.rs: -------------------------------------------------------------------------------- 1 | //!This is a shortcut fn to read [`AppConfig`] and call [`init_tracing`] and [`init_metrics`] fn. 2 | use crate::observability::cgroupv2::init_cgroup_metrics; 3 | use crate::observability::sys_info::init_sys_metrics; 4 | #[cfg(feature = "tokio-metrics")] 5 | use crate::observability::tokio_metrics::init_tokio_metrics_task; 6 | use crate::observability::{init_metrics, init_tracing}; 7 | use crate::{error::Result, *}; 8 | use serde::de::DeserializeOwned; 9 | use std::fmt::Debug; 10 | 11 | /// Reads AppConfig and calls [`init_tracing`]. 12 | /// Return Error if fails to read [`AppConfig`] or [`init_tracing`] returns error. 13 | /// Return Error if called twice because of internal call to [`tracing_subscriber::registry().try_init()`]. 14 | ///```no_run 15 | /// use fregate::*; 16 | /// use fregate::axum::{Router, routing::get, response::IntoResponse}; 17 | /// 18 | /// #[tokio::main] 19 | /// async fn main() { 20 | /// std::env::set_var("TEST_PORT", "3333"); 21 | /// std::env::set_var("TEST_NUMBER", "1010"); 22 | /// 23 | /// let config: AppConfig = bootstrap([ 24 | /// ConfigSource::File("./examples/configuration/app.yaml"), 25 | /// ConfigSource::EnvPrefix("TEST"), 26 | /// ]) 27 | /// .unwrap(); 28 | /// 29 | /// Application::new(config) 30 | /// .router(Router::new().route("/", get(|| async { "Hello World"}))) 31 | /// .serve() 32 | /// .await 33 | /// .unwrap(); 34 | /// } 35 | /// ``` 36 | pub fn bootstrap<'a, ConfigExt, S>(sources: S) -> Result> 37 | where 38 | S: IntoIterator>, 39 | ConfigExt: Debug + DeserializeOwned, 40 | { 41 | bootstrap_with_callback(sources, |_s| {}) 42 | } 43 | 44 | /// This has same functionalilty as [`bootstrap`] function, but calls `callback` before setting 45 | /// logging, tracing and metrics allowing to modify some of the variables. 46 | /// Example: 47 | ///```no_run 48 | /// use fregate::*; 49 | /// use fregate::axum::{Router, routing::get, response::IntoResponse}; 50 | /// 51 | /// #[tokio::main] 52 | /// async fn main() { 53 | /// std::env::set_var("TEST_PORT", "3333"); 54 | /// std::env::set_var("TEST_NUMBER", "1010"); 55 | /// 56 | /// let config: AppConfig = bootstrap_with_callback([ 57 | /// ConfigSource::File("./examples/configuration/app.yaml"), 58 | /// ConfigSource::EnvPrefix("TEST") 59 | /// ], 60 | /// |cfg| { 61 | /// // version is used for logging and tracing and you might want to set it from compile time env var. 62 | /// if let Some(build_version) = option_env!("BUILD_VERSION") { 63 | /// cfg.observability_cfg.version = build_version.to_owned(); 64 | /// } 65 | /// }) 66 | /// .unwrap(); 67 | /// 68 | /// Application::new(config) 69 | /// .router(Router::new().route("/", get(|| async { "Hello World"}))) 70 | /// .serve() 71 | /// .await 72 | /// .unwrap(); 73 | /// } 74 | /// ``` 75 | pub fn bootstrap_with_callback<'a, ConfigExt, S>( 76 | sources: S, 77 | callback: impl FnOnce(&mut AppConfig), 78 | ) -> Result> 79 | where 80 | S: IntoIterator>, 81 | ConfigExt: Debug + DeserializeOwned, 82 | { 83 | let mut config = AppConfig::::load_from(sources)?; 84 | callback(&mut config); 85 | let ObservabilityConfig { 86 | service_name, 87 | component_name, 88 | version, 89 | logger_config, 90 | cgroup_metrics, 91 | metrics_update_interval, 92 | trace_level, 93 | traces_endpoint, 94 | } = &config.observability_cfg; 95 | 96 | let worker_guard = init_tracing( 97 | logger_config, 98 | trace_level, 99 | version, 100 | service_name, 101 | component_name, 102 | traces_endpoint.as_deref(), 103 | )?; 104 | 105 | config.worker_guard.replace(worker_guard); 106 | init_metrics(*cgroup_metrics)?; 107 | 108 | #[cfg(feature = "tokio-metrics")] 109 | init_tokio_metrics_task(*metrics_update_interval); 110 | 111 | if *cgroup_metrics { 112 | init_cgroup_metrics(*metrics_update_interval) 113 | } else { 114 | init_sys_metrics(*metrics_update_interval); 115 | } 116 | 117 | tracing::info!("Configuration: `{config:?}`."); 118 | Ok(config) 119 | } 120 | -------------------------------------------------------------------------------- /src/configuration.rs: -------------------------------------------------------------------------------- 1 | //! See in [`examples`](https://github.com/elefant-dev/fregate-rs/blob/main/examples/configuration/src/main.rs) how to configure your [`crate::Application`] 2 | mod application; 3 | mod observability; 4 | mod source; 5 | 6 | mod management; 7 | #[cfg(feature = "tls")] 8 | mod tls; 9 | 10 | #[doc(inline)] 11 | pub use application::*; 12 | #[doc(inline)] 13 | pub use management::*; 14 | #[doc(inline)] 15 | pub use observability::*; 16 | #[doc(inline)] 17 | pub use source::*; 18 | -------------------------------------------------------------------------------- /src/configuration/application.rs: -------------------------------------------------------------------------------- 1 | use crate::configuration::observability::ObservabilityConfig; 2 | use crate::configuration::source::ConfigSource; 3 | use crate::{error::Result, extensions::DeserializeExt, ManagementConfig}; 4 | use config::{builder::DefaultState, ConfigBuilder, Environment, File, FileFormat}; 5 | use serde::{ 6 | de::{DeserializeOwned, Error}, 7 | Deserialize, Deserializer, 8 | }; 9 | use serde_json::Value; 10 | use std::marker::PhantomData; 11 | use std::{fmt::Debug, net::IpAddr}; 12 | use tracing_appender::non_blocking::WorkerGuard; 13 | 14 | #[cfg(feature = "tls")] 15 | use crate::configuration::tls::TlsConfigurationVariables; 16 | 17 | const HOST_PTR: &str = "/host"; 18 | const PORT_PTR: &str = "/port"; 19 | const PORT_SERVER_PTR: &str = "/server/port"; 20 | const MANAGEMENT_PTR: &str = "/management"; 21 | 22 | const DEFAULT_CONFIG: &str = include_str!("../resources/default_conf.toml"); 23 | const DEFAULT_SEPARATOR: &str = "_"; 24 | 25 | /// Default private config for [`AppConfig`]. 26 | #[derive(Deserialize, Debug, PartialEq, Eq, Copy, Clone)] 27 | pub struct Empty {} 28 | 29 | /// AppConfig reads and saves application configuration from different sources 30 | #[derive(Debug)] 31 | pub struct AppConfig { 32 | /// host address where to start Application 33 | pub host: IpAddr, 34 | /// When serialized uses ``_PORT or ``_SERVER_PORT names. 35 | /// ``_SERVER_PORT has higher priority. 36 | pub port: u16, 37 | /// configuration for logs and traces 38 | pub observability_cfg: ObservabilityConfig, 39 | /// configures management endpoints 40 | pub management_cfg: ManagementConfig, 41 | /// TLS configuration parameters 42 | #[cfg(feature = "tls")] 43 | pub tls: TlsConfigurationVariables, 44 | /// field for each application specific configuration 45 | pub private: ConfigExt, 46 | /// Why it is here read more: [`https://docs.rs/tracing-appender/latest/tracing_appender/non_blocking/struct.WorkerGuard.html`] 47 | /// This one will not be cloned and will be set to [`None`] in clone. 48 | pub worker_guard: Option, 49 | } 50 | 51 | impl Clone for AppConfig 52 | where 53 | ConfigExt: Clone, 54 | { 55 | fn clone(&self) -> Self { 56 | Self { 57 | host: self.host, 58 | port: self.port, 59 | observability_cfg: self.observability_cfg.clone(), 60 | management_cfg: self.management_cfg.clone(), 61 | #[cfg(feature = "tls")] 62 | tls: self.tls.clone(), 63 | private: self.private.clone(), 64 | worker_guard: None, 65 | } 66 | } 67 | } 68 | 69 | impl<'de, ConfigExt> Deserialize<'de> for AppConfig 70 | where 71 | ConfigExt: Debug + DeserializeOwned, 72 | { 73 | fn deserialize(deserializer: D) -> std::result::Result 74 | where 75 | D: Deserializer<'de>, 76 | { 77 | let config = Value::deserialize(deserializer)?; 78 | let host = config.pointer_and_deserialize(HOST_PTR)?; 79 | let port = config 80 | .pointer_and_deserialize(PORT_SERVER_PTR) 81 | .or_else(|_err: D::Error| config.pointer_and_deserialize(PORT_PTR))?; 82 | 83 | let management_cfg = config 84 | .pointer_and_deserialize::<_, D::Error>(MANAGEMENT_PTR) 85 | .unwrap_or_default(); 86 | let observability_cfg = ObservabilityConfig::deserialize(&config).map_err(Error::custom)?; 87 | #[cfg(feature = "tls")] 88 | let tls = TlsConfigurationVariables::deserialize(&config).map_err(Error::custom)?; 89 | let private = ConfigExt::deserialize(config).map_err(Error::custom)?; 90 | 91 | Ok(AppConfig:: { 92 | host, 93 | port, 94 | observability_cfg, 95 | management_cfg, 96 | #[cfg(feature = "tls")] 97 | tls, 98 | private, 99 | worker_guard: None, 100 | }) 101 | } 102 | } 103 | 104 | impl Default for AppConfig { 105 | #[allow(clippy::expect_used)] 106 | fn default() -> Self { 107 | AppConfig::builder() 108 | .add_default() 109 | .add_env_prefixed("OTEL") 110 | .build() 111 | .expect("Default config never fails") 112 | } 113 | } 114 | 115 | impl AppConfig { 116 | /// Creates [`AppConfigBuilder`] to add different sources to config 117 | pub fn builder() -> AppConfigBuilder { 118 | AppConfigBuilder::new() 119 | } 120 | 121 | /// Load file by given path and add environment variables with given prefix in addition to default config 122 | /// 123 | /// Environment variables have highet priority then file and then default configuration 124 | pub fn default_with(file_path: &str, env_prefix: &str) -> Result 125 | where 126 | ConfigExt: Debug + DeserializeOwned, 127 | { 128 | AppConfig::builder() 129 | .add_default() 130 | .add_env_prefixed("OTEL") 131 | .add_file(file_path) 132 | .add_env_prefixed(env_prefix) 133 | .build() 134 | } 135 | 136 | /// Load configuration from provided container with [`ConfigSource`] which override default config. 137 | pub fn load_from<'a, S>(sources: S) -> Result 138 | where 139 | ConfigExt: Debug + DeserializeOwned, 140 | S: IntoIterator>, 141 | { 142 | let mut config_builder = AppConfig::::builder() 143 | .add_default() 144 | .add_env_prefixed("OTEL"); 145 | 146 | for source in sources { 147 | config_builder = match source { 148 | ConfigSource::String(str, format) => config_builder.add_str(str, format), 149 | ConfigSource::File(path) => config_builder.add_file(path), 150 | ConfigSource::EnvPrefix(prefix) => config_builder.add_env_prefixed(prefix), 151 | }; 152 | } 153 | 154 | config_builder.build() 155 | } 156 | } 157 | 158 | /// AppConfig builder to set up multiple sources 159 | #[derive(Debug, Default)] 160 | pub struct AppConfigBuilder { 161 | builder: ConfigBuilder, 162 | phantom: PhantomData, 163 | } 164 | 165 | impl AppConfigBuilder { 166 | /// Creates new [`AppConfigBuilder`] 167 | pub fn new() -> Self { 168 | Self { 169 | builder: ConfigBuilder::default(), 170 | phantom: PhantomData, 171 | } 172 | } 173 | 174 | /// Reads all registered sources 175 | pub fn build(self) -> Result> 176 | where 177 | ConfigExt: Debug + DeserializeOwned, 178 | { 179 | Ok(self 180 | .builder 181 | .build()? 182 | .try_deserialize::>()?) 183 | } 184 | 185 | /// Add default config 186 | #[must_use] 187 | pub fn add_default(mut self) -> Self { 188 | self.builder = self 189 | .builder 190 | .add_source(File::from_str(DEFAULT_CONFIG, FileFormat::Toml)); 191 | self 192 | } 193 | 194 | /// Add file 195 | #[must_use] 196 | pub fn add_file(mut self, path: &str) -> Self { 197 | self.builder = self.builder.add_source(File::with_name(path)); 198 | self 199 | } 200 | 201 | /// Add string 202 | #[must_use] 203 | pub fn add_str(mut self, str: &str, format: FileFormat) -> Self { 204 | self.builder = self.builder.add_source(File::from_str(str, format)); 205 | self 206 | } 207 | 208 | /// Add environment variables with specified prefix and default separator: "_" 209 | #[must_use] 210 | pub fn add_env_prefixed(mut self, prefix: &str) -> Self { 211 | self.builder = self.builder.add_source( 212 | Environment::with_prefix(prefix) 213 | .try_parsing(true) 214 | .separator(DEFAULT_SEPARATOR), 215 | ); 216 | self 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /src/configuration/management.rs: -------------------------------------------------------------------------------- 1 | use crate::extensions::DeserializeExt; 2 | use crate::static_assert; 3 | use serde::de::{Error, Unexpected}; 4 | use serde::{Deserialize, Deserializer}; 5 | use serde_json::Value; 6 | 7 | const HEALTH_ENDPOINT: &str = "/health"; 8 | const LIVE_ENDPOINT: &str = "/live"; 9 | const READY_ENDPOINT: &str = "/ready"; 10 | const METRICS_ENDPOINT: &str = "/metrics"; 11 | const VERSION_ENDPOINT: &str = "/version"; 12 | 13 | const HEALTH_PTR: &str = "/health"; 14 | const LIVE_PTR: &str = "/live"; 15 | const READY_PTR: &str = "/ready"; 16 | const METRICS_PTR: &str = "/metrics"; 17 | const VERSION_PTR: &str = "/version"; 18 | 19 | #[derive(Debug, Default, Clone, Deserialize)] 20 | /// [`Management`](https://github.com/elefant-dev/fregate-rs/blob/main/src/application/management.rs) configuration. Currently only endpoints configuration is supported. 21 | pub struct ManagementConfig { 22 | /// health and metrics endpoints. 23 | pub endpoints: Endpoints, 24 | } 25 | 26 | /// By default endpoints are: 27 | /// ```no_run 28 | /// const HEALTH_ENDPOINT: &str = "/health"; 29 | /// const LIVE_ENDPOINT: &str = "/live"; 30 | /// const READY_ENDPOINT: &str = "/ready"; 31 | /// const METRICS_ENDPOINT: &str = "/metrics"; 32 | /// const VERSION_ENDPOINT: &str = "/version"; 33 | /// ``` 34 | /// You might want to change those:\ 35 | /// Example: 36 | /// ```no_run 37 | /// use fregate::{ 38 | /// axum::{routing::get, Router}, 39 | /// bootstrap, tokio, AppConfig, Application, ConfigSource, 40 | /// }; 41 | /// 42 | /// async fn handler() -> &'static str { 43 | /// "Hello, World!" 44 | /// } 45 | /// 46 | /// #[tokio::main] 47 | /// async fn main() { 48 | /// std::env::set_var("TEST_MANAGEMENT_ENDPOINTS_METRICS", "/observability"); 49 | /// std::env::set_var("TEST_MANAGEMENT_ENDPOINTS_HEALTH", "///also_valid"); 50 | /// // this is invalid default "/live" endpoint will be used. 51 | /// std::env::set_var("TEST_MANAGEMENT_ENDPOINTS_LIVE", "invalid"); 52 | /// // this is invalid default "/ready" endpoint will be used. 53 | /// std::env::set_var("TEST_MANAGEMENT_ENDPOINTS_READY", ""); 54 | /// 55 | /// let config: AppConfig = bootstrap([ConfigSource::EnvPrefix("TEST")]).unwrap(); 56 | /// 57 | /// Application::new(config) 58 | /// .router(Router::new().route("/", get(handler))) 59 | /// .serve() 60 | /// .await 61 | /// .unwrap(); 62 | /// } 63 | /// ``` 64 | #[derive(Debug, Clone)] 65 | pub struct Endpoints { 66 | /// health endpoint 67 | pub health: Endpoint, 68 | /// live endpoint 69 | pub live: Endpoint, 70 | /// ready endpoint 71 | pub ready: Endpoint, 72 | /// metrics endpoint 73 | pub metrics: Endpoint, 74 | /// version endpoint 75 | pub version: Endpoint, 76 | } 77 | 78 | #[allow(clippy::indexing_slicing)] 79 | impl<'de> Deserialize<'de> for Endpoints { 80 | fn deserialize(deserializer: D) -> Result 81 | where 82 | D: Deserializer<'de>, 83 | { 84 | static_assert!(HEALTH_ENDPOINT.as_bytes()[0] == b'/'); 85 | static_assert!(LIVE_ENDPOINT.as_bytes()[0] == b'/'); 86 | static_assert!(READY_ENDPOINT.as_bytes()[0] == b'/'); 87 | static_assert!(METRICS_ENDPOINT.as_bytes()[0] == b'/'); 88 | static_assert!(VERSION_ENDPOINT.as_bytes()[0] == b'/'); 89 | 90 | let value = Value::deserialize(deserializer)?; 91 | 92 | let health = value 93 | .pointer_and_deserialize::<_, D::Error>(HEALTH_PTR) 94 | .unwrap_or_else(|_| Endpoint(HEALTH_ENDPOINT.to_owned())); 95 | let live = value 96 | .pointer_and_deserialize::<_, D::Error>(LIVE_PTR) 97 | .unwrap_or_else(|_| Endpoint(LIVE_ENDPOINT.to_owned())); 98 | let ready = value 99 | .pointer_and_deserialize::<_, D::Error>(READY_PTR) 100 | .unwrap_or_else(|_| Endpoint(READY_ENDPOINT.to_owned())); 101 | let metrics = value 102 | .pointer_and_deserialize::<_, D::Error>(METRICS_PTR) 103 | .unwrap_or_else(|_| Endpoint(METRICS_ENDPOINT.to_owned())); 104 | let version = value 105 | .pointer_and_deserialize::<_, D::Error>(VERSION_PTR) 106 | .unwrap_or_else(|_| Endpoint(VERSION_ENDPOINT.to_owned())); 107 | 108 | Ok(Endpoints { 109 | health, 110 | live, 111 | ready, 112 | metrics, 113 | version, 114 | }) 115 | } 116 | } 117 | 118 | #[allow(clippy::indexing_slicing)] 119 | impl Default for Endpoints { 120 | fn default() -> Self { 121 | static_assert!(HEALTH_ENDPOINT.as_bytes()[0] == b'/'); 122 | static_assert!(LIVE_ENDPOINT.as_bytes()[0] == b'/'); 123 | static_assert!(READY_ENDPOINT.as_bytes()[0] == b'/'); 124 | static_assert!(METRICS_ENDPOINT.as_bytes()[0] == b'/'); 125 | static_assert!(VERSION_ENDPOINT.as_bytes()[0] == b'/'); 126 | 127 | Self { 128 | health: Endpoint(HEALTH_ENDPOINT.to_owned()), 129 | live: Endpoint(LIVE_ENDPOINT.to_owned()), 130 | ready: Endpoint(READY_ENDPOINT.to_owned()), 131 | metrics: Endpoint(METRICS_ENDPOINT.to_owned()), 132 | version: Endpoint(VERSION_ENDPOINT.to_owned()), 133 | } 134 | } 135 | } 136 | 137 | #[derive(Debug, Clone)] 138 | /// This is simply a wrapper over [`String`] but it checks if [`String`] starts with '/' symbol. 139 | pub struct Endpoint(String); 140 | 141 | impl Endpoint { 142 | /// Creates new [`Endpoint`]. 143 | /// Returns error if str does not start with '/' symbol. 144 | pub fn new(path: &str) -> Result { 145 | if path.starts_with('/') { 146 | Ok(Endpoint(path.to_owned())) 147 | } else { 148 | Err("Endpoint must start with a `/`") 149 | } 150 | } 151 | } 152 | 153 | impl<'de> Deserialize<'de> for Endpoint { 154 | fn deserialize(deserializer: D) -> Result 155 | where 156 | D: Deserializer<'de>, 157 | { 158 | let endpoint = String::deserialize(deserializer)?; 159 | Endpoint::new(endpoint.as_str()) 160 | .map_err(|err| D::Error::invalid_value(Unexpected::Str(&endpoint), &err)) 161 | } 162 | } 163 | 164 | impl AsRef for Endpoint { 165 | fn as_ref(&self) -> &str { 166 | self.0.as_ref() 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /src/configuration/observability.rs: -------------------------------------------------------------------------------- 1 | use crate::extensions::DeserializeExt; 2 | use crate::observability::HeadersFilter; 3 | use serde::de::Error; 4 | use serde::{Deserialize, Deserializer}; 5 | use serde_json::{from_value, Value}; 6 | use std::time::Duration; 7 | 8 | const SERVER_METRICS_UPDATE_INTERVAL_PTR: &str = "/server/metrics/update_interval"; 9 | const LOG_LEVEL_PTR: &str = "/log/level"; 10 | const LOG_MSG_LENGTH_PTR: &str = "/log/msg/length"; 11 | const LOGGING_FILE_PTR: &str = "/logging/file"; 12 | const LOGGING_PATH_PTR: &str = "/logging/path"; 13 | const LOGGING_INTERVAL_PTR: &str = "/logging/interval"; 14 | const LOGGING_MAX_FILE_SIZE_PTR: &str = "/logging/max/file/size"; 15 | const LOGGING_MAX_HISTORY_PTR: &str = "/logging/max/history"; 16 | const LOGGING_MAX_FILE_COUNT_PTR: &str = "/logging/max/file/count"; 17 | const LOGGING_ENABLE_COMPRESSION_PTR: &str = "/logging/enable/compression"; 18 | const BUFFERED_LINES_LIMIT_PTR: &str = "/buffered/lines/limit"; 19 | const TRACE_LEVEL_PTR: &str = "/trace/level"; 20 | const SERVICE_NAME_PTR: &str = "/service/name"; 21 | const COMPONENT_NAME_PTR: &str = "/component/name"; 22 | const COMPONENT_VERSION_PTR: &str = "/component/version"; 23 | const TRACES_ENDPOINT_PTR: &str = "/exporter/otlp/traces/endpoint"; 24 | const CGROUP_METRICS_PTR: &str = "/cgroup/metrics"; 25 | const HEADERS_PTR: &str = "/headers"; 26 | 27 | /// Configuration for logs and traces 28 | #[derive(Debug, Clone, Default)] 29 | pub struct ObservabilityConfig { 30 | /// service name to be used in logs 31 | pub service_name: String, 32 | /// component name to be used in logs and traces 33 | pub component_name: String, 34 | /// component version 35 | pub version: String, 36 | /// logger configuration 37 | pub logger_config: LoggerConfig, 38 | /// if it set true then metrics will be supplied from cgroup v2. 39 | pub cgroup_metrics: bool, 40 | /// metrics update interval 41 | pub metrics_update_interval: Duration, 42 | /// trace level read to string and later parsed into EnvFilter 43 | pub trace_level: String, 44 | /// configures [`tracing_opentelemetry::layer`] endpoint for sending traces. 45 | pub traces_endpoint: Option, 46 | } 47 | 48 | /// Configuration for Logs 49 | #[derive(Debug, Clone, Default)] 50 | pub struct LoggerConfig { 51 | /// log level read to string and later parsed into EnvFilter 52 | pub log_level: String, 53 | /// path where to write logs, if set logs will be written to file 54 | pub logging_path: Option, 55 | /// log file prefix, if written to file all log files will be with this prefix.\ 56 | /// by default `component_name` from [`ObservabilityConfig`] is used 57 | pub logging_file: Option, 58 | /// Maximum message field length, if set: message field will be cut if len() exceed this limit 59 | pub msg_length: Option, 60 | /// Sets limit for [`tracing_appender::non_blocking::NonBlocking`] 61 | pub buffered_lines_limit: Option, 62 | /// interval to split file into chunks with fixed interval 63 | pub logging_interval: Option, 64 | /// file size limit in bytes 65 | pub logging_max_file_size: Option, 66 | /// maximum duration files kept in seconds 67 | pub logging_max_history: Option, 68 | /// maximum number of files kept 69 | pub logging_max_file_count: Option, 70 | /// enable files compression 71 | pub logging_enable_compression: bool, 72 | /// initialize [`crate::observability::HEADERS_FILTER`] static variable in [`crate::bootstrap()`] or [`crate::observability::init_tracing()`] fn. 73 | pub headers_filter: Option, 74 | } 75 | 76 | impl<'de> Deserialize<'de> for ObservabilityConfig { 77 | fn deserialize(deserializer: D) -> Result 78 | where 79 | D: Deserializer<'de>, 80 | { 81 | let mut config = Value::deserialize(deserializer)?; 82 | 83 | let trace_level = config.pointer_and_deserialize(TRACE_LEVEL_PTR)?; 84 | let service_name = config.pointer_and_deserialize(SERVICE_NAME_PTR)?; 85 | let component_name = config.pointer_and_deserialize(COMPONENT_NAME_PTR)?; 86 | 87 | let cgroup_metrics = config 88 | .pointer_and_deserialize::<_, D::Error>(CGROUP_METRICS_PTR) 89 | .unwrap_or_default(); 90 | let version = config.pointer_and_deserialize(COMPONENT_VERSION_PTR)?; 91 | let traces_endpoint = config 92 | .pointer_mut(TRACES_ENDPOINT_PTR) 93 | .map(Value::take) 94 | .map(from_value::) 95 | .transpose() 96 | .map_err(D::Error::custom)?; 97 | let metrics_update_interval = 98 | config.pointer_and_deserialize::(SERVER_METRICS_UPDATE_INTERVAL_PTR)?; 99 | let logger_config = LoggerConfig::deserialize(config).map_err(Error::custom)?; 100 | 101 | Ok(ObservabilityConfig { 102 | version, 103 | trace_level, 104 | service_name, 105 | component_name, 106 | traces_endpoint, 107 | metrics_update_interval: Duration::from_millis(metrics_update_interval), 108 | cgroup_metrics, 109 | logger_config, 110 | }) 111 | } 112 | } 113 | 114 | impl<'de> Deserialize<'de> for LoggerConfig { 115 | fn deserialize(deserializer: D) -> Result 116 | where 117 | D: Deserializer<'de>, 118 | { 119 | let config = Value::deserialize(deserializer)?; 120 | 121 | let log_level = config.pointer_and_deserialize(LOG_LEVEL_PTR)?; 122 | let msg_length = config 123 | .pointer_and_deserialize::<_, D::Error>(LOG_MSG_LENGTH_PTR) 124 | .ok(); 125 | let buffered_lines_limit = config 126 | .pointer_and_deserialize::<_, D::Error>(BUFFERED_LINES_LIMIT_PTR) 127 | .ok(); 128 | let logging_file = config 129 | .pointer_and_deserialize::<_, D::Error>(LOGGING_FILE_PTR) 130 | .unwrap_or_default(); 131 | let logging_path = config 132 | .pointer_and_deserialize::<_, D::Error>(LOGGING_PATH_PTR) 133 | .unwrap_or_default(); 134 | let headers_filter: Option = config 135 | .pointer_and_deserialize::<_, D::Error>(HEADERS_PTR) 136 | .ok(); 137 | 138 | let logging_interval = config 139 | .pointer_and_deserialize::(LOGGING_INTERVAL_PTR) 140 | .ok() 141 | .map(Duration::from_secs); 142 | let logging_max_file_size = config 143 | .pointer_and_deserialize::<_, D::Error>(LOGGING_MAX_FILE_SIZE_PTR) 144 | .ok(); 145 | let logging_max_history = config 146 | .pointer_and_deserialize::(LOGGING_MAX_HISTORY_PTR) 147 | .ok() 148 | .map(Duration::from_secs); 149 | let logging_max_file_count = config 150 | .pointer_and_deserialize::<_, D::Error>(LOGGING_MAX_FILE_COUNT_PTR) 151 | .ok(); 152 | let logging_enable_compression = config 153 | .pointer_and_deserialize::<_, D::Error>(LOGGING_ENABLE_COMPRESSION_PTR) 154 | .unwrap_or_default(); 155 | 156 | Ok(LoggerConfig { 157 | log_level, 158 | msg_length, 159 | buffered_lines_limit, 160 | logging_file, 161 | logging_path, 162 | logging_interval, 163 | logging_max_file_size, 164 | logging_max_history, 165 | logging_max_file_count, 166 | logging_enable_compression, 167 | headers_filter, 168 | }) 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/configuration/source.rs: -------------------------------------------------------------------------------- 1 | use config::FileFormat; 2 | 3 | /// Enum to specify configuration source type: 4 | #[derive(Clone, Debug)] 5 | pub enum ConfigSource<'a> { 6 | /// Load from string 7 | String(&'a str, FileFormat), 8 | /// Read file by given path 9 | File(&'a str), 10 | /// Read environment variables with specified prefix 11 | EnvPrefix(&'a str), 12 | } 13 | -------------------------------------------------------------------------------- /src/configuration/tls.rs: -------------------------------------------------------------------------------- 1 | use crate::extensions::DeserializeExt; 2 | use serde::{Deserialize, Deserializer}; 3 | use serde_json::Value; 4 | use std::time::Duration; 5 | 6 | const TLS_HANDSHAKE_TIMEOUT: &str = "/server/tls/handshake_timeout"; 7 | const TLS_KEY_PATH: &str = "/server/tls/key/path"; 8 | const TLS_CERTIFICATE_PATH: &str = "/server/tls/cert/path"; 9 | 10 | #[derive(Debug, Default, Clone)] 11 | pub struct TlsConfigurationVariables { 12 | /// TLS handshake timeout 13 | pub handshake_timeout: Duration, 14 | /// path to TLS key file 15 | pub key_path: Option>, 16 | /// path to TLS certificate file 17 | pub cert_path: Option>, 18 | } 19 | 20 | impl<'de> Deserialize<'de> for TlsConfigurationVariables { 21 | fn deserialize(deserializer: D) -> Result 22 | where 23 | D: Deserializer<'de>, 24 | { 25 | let config = Value::deserialize(deserializer)?; 26 | 27 | let tls_handshake_timeout = 28 | config.pointer_and_deserialize::(TLS_HANDSHAKE_TIMEOUT)?; 29 | let tls_key_path = config 30 | .pointer_and_deserialize::<_, D::Error>(TLS_KEY_PATH) 31 | .ok(); 32 | let tls_cert_path = config 33 | .pointer_and_deserialize::<_, D::Error>(TLS_CERTIFICATE_PATH) 34 | .ok(); 35 | 36 | Ok(Self { 37 | handshake_timeout: Duration::from_millis(tls_handshake_timeout), 38 | key_path: tls_key_path, 39 | cert_path: tls_cert_path, 40 | }) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | //! fregate Errors 2 | use config::ConfigError; 3 | use hyper::Error as HyperError; 4 | use metrics::SetRecorderError; 5 | use opentelemetry::trace::TraceError; 6 | use tracing_subscriber::util::TryInitError; 7 | 8 | /// Possible Errors which might occur in fregate 9 | #[derive(thiserror::Error, Debug)] 10 | pub enum Error { 11 | /// Error returned when AppConfigBuilder fails to build configuration 12 | #[error("Got ConfigError: `{0}`")] 13 | ConfigError(#[from] ConfigError), 14 | /// Error returned on init_tracing() 15 | #[error("Got TraceError: `{0}`")] 16 | TraceError(#[from] TraceError), 17 | /// Error returned on init_tracing() 18 | #[error("Got TryInitError: `{0}`")] 19 | TryInitError(#[from] TryInitError), 20 | /// Error returned on Application::serve() 21 | #[error("Got HyperError: `{0}`")] 22 | HyperError(#[from] HyperError), 23 | /// Error returned on init_metrics() 24 | #[error("Got SetRecorderError: `{0}`")] 25 | SetRecorderError(#[from] SetRecorderError), 26 | /// Error returned by serde_json crate 27 | #[error("Got SerdeError: `{0}`")] 28 | SerdeError(#[from] serde_json::Error), 29 | /// Custom fregate Error 30 | #[error("Got CustomError: `{0}`")] 31 | CustomError(String), 32 | /// Some std IO Error 33 | #[error("Got IoError: `{0}`")] 34 | IoError(#[from] std::io::Error), 35 | /// Variant for [`opentelemetry::global::Error`] 36 | #[error("Got OpentelemetryError: `{0}`")] 37 | OpentelemetryError(#[from] opentelemetry::global::Error), 38 | /// tokio JoinHandle error 39 | #[cfg(feature = "tls")] 40 | #[error("Got JoinHandleError: `{0}`")] 41 | JoinHandleError(#[from] tokio::task::JoinError), 42 | /// TLS HandshakeTimeout 43 | #[cfg(feature = "tls")] 44 | #[error("Got TlsHandshakeTimeout")] 45 | TlsHandshakeTimeout, 46 | /// Error returned by native-tls 47 | #[cfg(feature = "use_native_tls")] 48 | #[error("Got NativeTlsError: `{0}`")] 49 | NativeTlsError(#[from] tokio_native_tls::native_tls::Error), 50 | /// Error returned by rustls 51 | #[cfg(feature = "use_rustls")] 52 | #[error("Got RustlsError: `{0}`")] 53 | RustlsError(#[from] tokio_rustls::rustls::Error), 54 | } 55 | 56 | /// fregate Result alias 57 | pub type Result = std::result::Result; 58 | -------------------------------------------------------------------------------- /src/extensions.rs: -------------------------------------------------------------------------------- 1 | //! Extensions traits for different crates 2 | mod axum_tonic; 3 | mod headers_ext; 4 | mod http_req_ext; 5 | #[cfg(feature = "reqwest")] 6 | mod reqwest_ext; 7 | mod serde_ext; 8 | mod tonic_ext; 9 | 10 | pub use axum_tonic::*; 11 | pub use headers_ext::*; 12 | pub use http_req_ext::*; 13 | #[cfg(feature = "reqwest")] 14 | pub use reqwest_ext::*; 15 | pub use serde_ext::*; 16 | pub use tonic_ext::*; 17 | -------------------------------------------------------------------------------- /src/extensions/axum_tonic.rs: -------------------------------------------------------------------------------- 1 | use axum::body::boxed; 2 | use axum::Router; 3 | use hyper::{Body, Request, Response}; 4 | use sealed::sealed; 5 | use std::convert::Infallible; 6 | use tonic::body::BoxBody; 7 | use tonic::transport::NamedService; 8 | use tower::{Service, ServiceBuilder}; 9 | use tower_http::ServiceBuilderExt; 10 | 11 | /// Takes Tonic [`Service`] and converts it into [`Router`] 12 | #[sealed] 13 | pub trait RouterTonicExt { 14 | /// Takes Tonic [`Service`] and converts it into [`Router`] 15 | fn from_tonic_service(service: S) -> Self 16 | where 17 | Self: Sized, 18 | S: Service, Response = Response, Error = Infallible> 19 | + NamedService 20 | + Clone 21 | + Send 22 | + 'static, 23 | S::Future: Send + 'static; 24 | } 25 | 26 | #[sealed] 27 | impl RouterTonicExt for Router { 28 | fn from_tonic_service(service: S) -> Self 29 | where 30 | Self: Sized, 31 | S: Service, Response = Response, Error = Infallible> 32 | + NamedService 33 | + Clone 34 | + Send 35 | + 'static, 36 | S::Future: Send + 'static, 37 | { 38 | // this piece of code is taken from: 39 | // https://github.com/EmbarkStudios/server-framework/blob/83ff44b0ad19e4fcbc163bc652f00e04f4143365/src/server.rs#L679-L685 40 | let svc = ServiceBuilder::new() 41 | .map_err(|err: Infallible| match err {}) 42 | .map_response_body(boxed) 43 | .service(service); 44 | 45 | Router::new().route_service(&format!("/{}/*rest", S::NAME), svc) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/extensions/headers_ext.rs: -------------------------------------------------------------------------------- 1 | use crate::observability::{Filter, HeadersFilter, HEADERS_FILTER, SANITIZED_VALUE}; 2 | use axum::headers::{HeaderMap, HeaderName}; 3 | use hyper::http::HeaderValue; 4 | use std::borrow::Cow; 5 | 6 | /// Extension trait to get filtered headers. 7 | /// Current implementation relies on [`HEADERS_FILTER`]. 8 | #[sealed::sealed] 9 | pub trait HeaderFilterExt 10 | where 11 | Self: Clone, 12 | { 13 | /// Extension trait to get filtered values. 14 | fn get_filtered(&self) -> Cow<'_, Self>; 15 | } 16 | 17 | #[sealed::sealed] 18 | impl HeaderFilterExt for HeaderMap { 19 | /// If [`HEADERS_FILTER`] is uninitialised returns [`Cow::Borrowed`] otherwise creates clone and returns [`Cow::Owned`] from included and sanitized fields. 20 | fn get_filtered(&self) -> Cow<'_, Self> { 21 | HEADERS_FILTER 22 | .get() 23 | .map( 24 | |HeadersFilter { 25 | sanitize, 26 | exclude, 27 | include, 28 | }| { 29 | let filtered = self 30 | .iter() 31 | .map(|(name, value)| { 32 | let lowercase = name.as_str().to_ascii_lowercase(); 33 | (lowercase, name, value) 34 | }) 35 | .filter_map(|(lowercase, name, value)| { 36 | include_value(include, lowercase, name, value) 37 | }) 38 | .filter_map(|(lowercase, name, value)| { 39 | exclude_value(exclude, lowercase, name, value) 40 | }) 41 | .map(|(lowercase, name, value)| { 42 | sanitize_value(sanitize, lowercase, name, value) 43 | }) 44 | .collect(); 45 | Cow::Owned(filtered) 46 | }, 47 | ) 48 | .unwrap_or(Cow::Borrowed(self)) 49 | } 50 | } 51 | 52 | fn sanitize_value<'a>( 53 | sanitize: &'a Filter, 54 | lowercase: String, 55 | name: &'a HeaderName, 56 | value: &'a HeaderValue, 57 | ) -> (HeaderName, HeaderValue) { 58 | match sanitize { 59 | Filter::All => (name.clone(), HeaderValue::from_static(SANITIZED_VALUE)), 60 | Filter::Set(set) => { 61 | if set.contains(&lowercase) { 62 | (name.clone(), HeaderValue::from_static(SANITIZED_VALUE)) 63 | } else { 64 | (name.clone(), value.clone()) 65 | } 66 | } 67 | } 68 | } 69 | 70 | fn include_value<'a>( 71 | include: &'a Filter, 72 | lowercase: String, 73 | name: &'a HeaderName, 74 | value: &'a HeaderValue, 75 | ) -> Option<(String, &'a HeaderName, &'a HeaderValue)> { 76 | match include { 77 | Filter::All => Some((lowercase, name, value)), 78 | Filter::Set(set) => { 79 | if set.contains(&lowercase) { 80 | Some((lowercase, name, value)) 81 | } else { 82 | None 83 | } 84 | } 85 | } 86 | } 87 | 88 | fn exclude_value<'a>( 89 | exclude: &'a Filter, 90 | lowercase: String, 91 | name: &'a HeaderName, 92 | value: &'a HeaderValue, 93 | ) -> Option<(String, &'a HeaderName, &'a HeaderValue)> { 94 | match exclude { 95 | Filter::All => None, 96 | Filter::Set(set) => { 97 | if set.contains(&lowercase) { 98 | None 99 | } else { 100 | Some((lowercase, name, value)) 101 | } 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/extensions/http_req_ext.rs: -------------------------------------------------------------------------------- 1 | use hyper::http; 2 | use opentelemetry::global::get_text_map_propagator; 3 | use opentelemetry_http::HeaderInjector; 4 | use sealed::sealed; 5 | use tracing::Span; 6 | use tracing_opentelemetry::OpenTelemetrySpanExt; 7 | 8 | #[sealed] 9 | /// Injects [`Span`] context into request headers; 10 | pub trait HttpReqExt { 11 | /// Injects Context from [`Span::current()`] 12 | fn inject_from_current_span(&mut self); 13 | 14 | /// Injects Context from given [`Span`] 15 | fn inject_from_span(&mut self, span: &Span); 16 | } 17 | 18 | #[sealed] 19 | impl HttpReqExt for http::Request { 20 | fn inject_from_current_span(&mut self) { 21 | self.inject_from_span(&Span::current()) 22 | } 23 | 24 | fn inject_from_span(&mut self, span: &Span) { 25 | get_text_map_propagator(|propagator| { 26 | propagator.inject_context(&span.context(), &mut HeaderInjector(self.headers_mut())) 27 | }); 28 | } 29 | } 30 | 31 | #[sealed] 32 | impl HttpReqExt for http::HeaderMap { 33 | fn inject_from_current_span(&mut self) { 34 | self.inject_from_span(&Span::current()) 35 | } 36 | 37 | fn inject_from_span(&mut self, span: &Span) { 38 | get_text_map_propagator(|propagator| { 39 | propagator.inject_context(&span.context(), &mut HeaderInjector(self)) 40 | }); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/extensions/reqwest_ext.rs: -------------------------------------------------------------------------------- 1 | use opentelemetry::global::get_text_map_propagator; 2 | use opentelemetry_http::HeaderInjector; 3 | use sealed::sealed; 4 | use tracing::Span; 5 | use tracing_opentelemetry::OpenTelemetrySpanExt; 6 | 7 | #[sealed] 8 | /// Injects [`Span`] context into request headers; 9 | pub trait ReqwestExt { 10 | /// Injects Context from [`Span::current()`] 11 | #[must_use] 12 | fn inject_from_current_span(self) -> Self; 13 | 14 | /// Injects Context from given [`Span`] 15 | #[must_use] 16 | fn inject_from_span(self, span: &Span) -> Self; 17 | } 18 | 19 | #[sealed] 20 | impl ReqwestExt for reqwest::RequestBuilder { 21 | fn inject_from_current_span(self) -> Self { 22 | self.inject_from_span(&Span::current()) 23 | } 24 | 25 | fn inject_from_span(self, span: &Span) -> Self { 26 | let mut headers = reqwest::header::HeaderMap::with_capacity(2); 27 | 28 | get_text_map_propagator(|propagator| { 29 | propagator.inject_context(&span.context(), &mut HeaderInjector(&mut headers)) 30 | }); 31 | 32 | self.headers(headers) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/extensions/serde_ext.rs: -------------------------------------------------------------------------------- 1 | use sealed::sealed; 2 | use serde::de::Error; 3 | use serde::Deserialize; 4 | use serde_json::Value; 5 | 6 | // TODO: FIXME: https://github.com/serde-rs/serde/issues/2261 7 | /// Needed for overcoming overlapping path in config deserialization. 8 | #[sealed] 9 | pub trait DeserializeExt { 10 | /// find value by given pointer and try to deserialize 11 | fn pointer_and_deserialize<'de, T, E>(&'de self, pointer: &'static str) -> Result 12 | where 13 | T: Deserialize<'de>, 14 | E: Error; 15 | } 16 | 17 | #[sealed] 18 | impl DeserializeExt for Value { 19 | fn pointer_and_deserialize<'de, T, E>(&'de self, pointer: &'static str) -> Result 20 | where 21 | T: Deserialize<'de>, 22 | E: Error, 23 | { 24 | let raw_ret = self 25 | .pointer(pointer) 26 | .ok_or_else(|| E::missing_field(pointer))?; 27 | 28 | T::deserialize(raw_ret).map_err(E::custom) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/extensions/tonic_ext.rs: -------------------------------------------------------------------------------- 1 | use opentelemetry::global::get_text_map_propagator; 2 | use opentelemetry::propagation::Injector; 3 | use sealed::sealed; 4 | use tonic::metadata::{MetadataKey, MetadataValue}; 5 | use tracing::Span; 6 | use tracing_opentelemetry::OpenTelemetrySpanExt; 7 | 8 | struct MetadataMap<'a>(&'a mut tonic::metadata::MetadataMap); 9 | 10 | impl<'a> Injector for MetadataMap<'a> { 11 | /// Set a key and value in the MetadataMap. Does nothing if the key or value are not valid inputs 12 | fn set(&mut self, key: &str, value: String) { 13 | if let (Ok(key), Ok(val)) = ( 14 | MetadataKey::from_bytes(key.as_bytes()), 15 | MetadataValue::try_from(value), 16 | ) { 17 | self.0.insert(key, val); 18 | } 19 | } 20 | } 21 | 22 | #[sealed] 23 | /// Injects [`Span`] context into request headers; 24 | pub trait TonicReqExt { 25 | /// Injects Context from [`Span::current()`] 26 | fn inject_from_current_span(&mut self); 27 | 28 | /// Injects Context from given [`Span`] 29 | fn inject_from_span(&mut self, span: &Span); 30 | } 31 | 32 | #[sealed] 33 | impl TonicReqExt for tonic::Request { 34 | fn inject_from_current_span(&mut self) { 35 | self.inject_from_span(&Span::current()) 36 | } 37 | 38 | fn inject_from_span(&mut self, span: &Span) { 39 | get_text_map_propagator(|propagator| { 40 | propagator.inject_context(&span.context(), &mut MetadataMap(self.metadata_mut())) 41 | }); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny(unused_must_use)] 2 | #![warn( 3 | rust_2018_idioms, 4 | rust_2021_compatibility, 5 | missing_docs, 6 | missing_debug_implementations, 7 | clippy::expect_used, 8 | clippy::missing_panics_doc, 9 | clippy::panic_in_result_fn, 10 | clippy::panicking_unwrap, 11 | clippy::unwrap_used, 12 | clippy::if_let_mutex, 13 | clippy::map_unwrap_or, 14 | clippy::if_let_mutex, 15 | clippy::indexing_slicing, 16 | clippy::return_self_not_must_use 17 | )] 18 | #![warn(clippy::all)] 19 | #![forbid(non_ascii_idents)] 20 | #![forbid(unsafe_code)] 21 | 22 | //! Set of instruments to simplify http server set-up.\ 23 | //! 24 | //! This project is in progress and might change a lot from version to version. 25 | //! 26 | //! Example: 27 | //! ```no_run 28 | //! use fregate::{ 29 | //! axum::{routing::get, Router}, 30 | //! bootstrap, tokio, AppConfig, Application, 31 | //! }; 32 | //! 33 | //! async fn handler() -> &'static str { 34 | //! "Hello, World!" 35 | //! } 36 | //! 37 | //! #[tokio::main] 38 | //! async fn main() { 39 | //! let config: AppConfig = bootstrap([]).unwrap(); 40 | //! 41 | //! Application::new(config) 42 | //! .router(Router::new().route("/", get(handler))) 43 | //! .serve() 44 | //! .await 45 | //! .unwrap(); 46 | //! } 47 | //! ``` 48 | //! 49 | //! 50 | //! # Examples 51 | //! 52 | //! Examples can be found [`here`](https://github.com/elefant-dev/fregate-rs/tree/main/examples). 53 | 54 | mod application; 55 | mod static_assert; 56 | 57 | pub mod bootstrap; 58 | pub mod configuration; 59 | pub mod error; 60 | pub mod extensions; 61 | pub mod middleware; 62 | pub mod observability; 63 | pub mod sugar; 64 | 65 | #[doc(inline)] 66 | pub use application::*; 67 | #[doc(inline)] 68 | pub use bootstrap::*; 69 | #[doc(inline)] 70 | pub use configuration::*; 71 | #[allow(unused_imports)] 72 | pub use static_assert::*; 73 | 74 | pub use axum; 75 | pub use config; 76 | #[cfg(feature = "tls")] 77 | pub use futures_util; 78 | pub use hyper; 79 | pub use thiserror; 80 | pub use tokio; 81 | pub use tonic; 82 | pub use tower; 83 | pub use tower_http; 84 | pub use tracing; 85 | pub use tracing_subscriber; 86 | pub use valuable; 87 | -------------------------------------------------------------------------------- /src/middleware.rs: -------------------------------------------------------------------------------- 1 | //! Set of middlewares 2 | mod proxy_layer; 3 | mod tracing; 4 | 5 | pub use self::proxy_layer::*; 6 | pub use self::tracing::*; 7 | -------------------------------------------------------------------------------- /src/middleware/proxy_layer.rs: -------------------------------------------------------------------------------- 1 | mod shared; 2 | 3 | pub mod error; 4 | pub mod layer; 5 | pub mod service; 6 | 7 | pub use error::*; 8 | pub use layer::ProxyLayer; 9 | -------------------------------------------------------------------------------- /src/middleware/proxy_layer/error.rs: -------------------------------------------------------------------------------- 1 | //!Errors [`crate::middleware::proxy_layer::ProxyLayer`] may return in runtime. 2 | 3 | use hyper::http; 4 | use std::error::Error; 5 | 6 | #[derive(thiserror::Error, Debug)] 7 | ///Errors enum. 8 | pub enum ProxyError { 9 | #[error("`{0}`")] 10 | /// Returned if fail to build new uri for [`hyper::Request`] 11 | UriBuilder(http::Error), 12 | #[error("`{0}`")] 13 | /// Returned on any other error while sending [`hyper::Request`] 14 | SendRequest(Box<(dyn Error + Send + Sync + 'static)>), 15 | } 16 | 17 | /// Result Alias 18 | pub type ProxyResult = Result; 19 | -------------------------------------------------------------------------------- /src/middleware/proxy_layer/service.rs: -------------------------------------------------------------------------------- 1 | //! Middleware that runs [`ShouldProxyCallback`] and on `true` proxy [`Request`] to a new destination otherwise pass [`Request`] to next handler.\ 2 | //! See in [examples](https://github.com/elefant-dev/fregate-rs/blob/main/examples/proxy-layer/src/main.rs) how it might be used 3 | use crate::middleware::proxy_layer::error::ProxyError; 4 | use crate::middleware::proxy_layer::shared::{get_extension, Shared}; 5 | use axum::body::{Bytes, HttpBody}; 6 | use axum::response::{IntoResponse, Response as AxumResponse}; 7 | use hyper::Request; 8 | use hyper::Response; 9 | use std::any::type_name; 10 | use std::convert::Infallible; 11 | use std::error::Error; 12 | use std::fmt::{Debug, Formatter}; 13 | use std::future::Future; 14 | use std::pin::Pin; 15 | use std::sync::Arc; 16 | use std::task::{Context, Poll}; 17 | use tower::Service; 18 | 19 | #[allow(clippy::type_complexity)] 20 | /// Middleware that runs [`ShouldProxyCallback`] and on `true` proxy [`Request`] to a new destination otherwise pass [`Request`] to next handler. 21 | pub struct ProxyService< 22 | TClient, 23 | TBody, 24 | TRespBody, 25 | ShouldProxyCallback, 26 | OnProxyErrorCallback, 27 | OnProxyRequestCallback, 28 | OnProxyResponseCallback, 29 | TService, 30 | TExtension = (), 31 | > { 32 | pub(crate) shared: Arc< 33 | Shared< 34 | TBody, 35 | TRespBody, 36 | ShouldProxyCallback, 37 | OnProxyErrorCallback, 38 | OnProxyRequestCallback, 39 | OnProxyResponseCallback, 40 | TExtension, 41 | >, 42 | >, 43 | pub(crate) client: TClient, 44 | pub(crate) inner: TService, 45 | pub(crate) poll_error: Option>, 46 | } 47 | 48 | impl< 49 | TClient, 50 | TBody, 51 | TRespBody, 52 | ShouldProxyCallback, 53 | OnProxyErrorCallback, 54 | OnProxyRequestCallback, 55 | OnProxyResponseCallback, 56 | TService, 57 | TExtension, 58 | > Clone 59 | for ProxyService< 60 | TClient, 61 | TBody, 62 | TRespBody, 63 | ShouldProxyCallback, 64 | OnProxyErrorCallback, 65 | OnProxyRequestCallback, 66 | OnProxyResponseCallback, 67 | TService, 68 | TExtension, 69 | > 70 | where 71 | TService: Clone, 72 | TClient: Clone, 73 | { 74 | fn clone(&self) -> Self { 75 | Self { 76 | shared: Arc::clone(&self.shared), 77 | inner: self.inner.clone(), 78 | client: self.client.clone(), 79 | poll_error: None, 80 | } 81 | } 82 | } 83 | 84 | impl< 85 | TClient, 86 | TBody, 87 | TRespBody, 88 | ShouldProxyCallback, 89 | OnProxyErrorCallback, 90 | OnProxyRequestCallback, 91 | OnProxyResponseCallback, 92 | TService, 93 | TExtension, 94 | > Debug 95 | for ProxyService< 96 | TClient, 97 | TBody, 98 | TRespBody, 99 | ShouldProxyCallback, 100 | OnProxyErrorCallback, 101 | OnProxyRequestCallback, 102 | OnProxyResponseCallback, 103 | TService, 104 | TExtension, 105 | > 106 | where 107 | TService: Debug, 108 | { 109 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 110 | f.debug_struct("Proxy") 111 | .field("shared", &self.shared) 112 | .field("inner", &self.inner) 113 | .field("client", &format_args!("{}", type_name::())) 114 | .finish() 115 | } 116 | } 117 | 118 | impl< 119 | TBody, 120 | TRespBody, 121 | TClient, 122 | ShouldProxyCallback, 123 | OnProxyErrorCallback, 124 | OnProxyRequestCallback, 125 | OnProxyResponseCallback, 126 | TService, 127 | TExtension, 128 | > Service> 129 | for ProxyService< 130 | TClient, 131 | TBody, 132 | TRespBody, 133 | ShouldProxyCallback, 134 | OnProxyErrorCallback, 135 | OnProxyRequestCallback, 136 | OnProxyResponseCallback, 137 | TService, 138 | TExtension, 139 | > 140 | where 141 | TClient: Service, Response = Response>, 142 | TClient: Clone + Send + Sync + 'static, 143 | >>::Future: Send + 'static, 144 | >>::Error: 145 | Into> + Send, 146 | TExtension: Default + Clone + Send + Sync + 'static, 147 | ShouldProxyCallback: for<'any> Fn( 148 | &'any Request, 149 | &'any TExtension, 150 | ) 151 | -> Pin> + Send + 'any>> 152 | + Send 153 | + Sync 154 | + 'static, 155 | OnProxyErrorCallback: Fn(ProxyError, &TExtension) -> AxumResponse + Send + Sync + 'static, 156 | OnProxyRequestCallback: Fn(&mut Request, &TExtension) + Send + Sync + 'static, 157 | OnProxyResponseCallback: Fn(&mut Response, &TExtension) + Send + Sync + 'static, 158 | TBody: Sync + Send + 'static, 159 | TRespBody: HttpBody + Sync + Send + 'static, 160 | TRespBody::Error: Into>, 161 | TService: Service, Response = AxumResponse, Error = Infallible> 162 | + Clone 163 | + Send 164 | + 'static, 165 | TService::Future: Send + 'static, 166 | { 167 | type Response = TService::Response; 168 | type Error = TService::Error; 169 | type Future = 170 | Pin> + Send + 'static>>; 171 | 172 | fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { 173 | match (self.client.poll_ready(cx), self.inner.poll_ready(cx)) { 174 | (Poll::Ready(Ok(())), Poll::Ready(Ok(()))) => Poll::Ready(Ok(())), 175 | (Poll::Ready(Err(e)), _) => { 176 | self.poll_error.replace(e.into()); 177 | Poll::Ready(Ok(())) 178 | } 179 | (_, Poll::Ready(Err(e))) => Poll::Ready(Err(e)), 180 | _ => Poll::Pending, 181 | } 182 | } 183 | 184 | fn call(&mut self, request: Request) -> Self::Future { 185 | let not_ready_inner_clone = self.inner.clone(); 186 | let mut ready_inner_clone = std::mem::replace(&mut self.inner, not_ready_inner_clone); 187 | 188 | let client = self.client.clone(); 189 | let ready_client = std::mem::replace(&mut self.client, client); 190 | 191 | let shared = self.shared.clone(); 192 | let poll_error = self.poll_error.take(); 193 | 194 | let future = async move { 195 | let extension = get_extension(&request); 196 | 197 | match (shared.should_proxy)(&request, &extension).await { 198 | Ok(true) => Ok(shared 199 | .proxy(request, ready_client, extension, poll_error) 200 | .await 201 | .into_response()), 202 | Ok(false) => match ready_inner_clone.call(request).await { 203 | Ok(resp) => Ok(resp), 204 | Err(err) => Err(err), 205 | }, 206 | Err(err) => Ok(err), 207 | } 208 | }; 209 | 210 | Box::pin(future) 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /src/middleware/tracing.rs: -------------------------------------------------------------------------------- 1 | mod common; 2 | mod grpc_req; 3 | mod http_req; 4 | 5 | pub use common::*; 6 | pub use grpc_req::*; 7 | pub use http_req::*; 8 | 9 | use axum::http::Request; 10 | use axum::middleware::Next; 11 | use axum::response::IntoResponse; 12 | use tracing::Instrument; 13 | use tracing_opentelemetry::OpenTelemetrySpanExt; 14 | 15 | /// Fn to be used with [`axum::middleware::from_fn`] 16 | pub async fn trace_request( 17 | req: Request, 18 | next: Next, 19 | service_name: String, 20 | component_name: String, 21 | ) -> impl IntoResponse { 22 | if is_grpc(req.headers()) { 23 | let grpc_span = make_grpc_span(); 24 | let parent_context = extract_context(&req); 25 | grpc_span.set_parent(parent_context); 26 | 27 | trace_grpc_request(req, next, &service_name, &component_name) 28 | .instrument(grpc_span) 29 | .await 30 | .into_response() 31 | } else { 32 | let http_span = make_http_span(); 33 | let parent_context = extract_context(&req); 34 | http_span.set_parent(parent_context); 35 | 36 | trace_http_request(req, next, &service_name, &component_name) 37 | .instrument(http_span) 38 | .await 39 | .into_response() 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/middleware/tracing/common.rs: -------------------------------------------------------------------------------- 1 | //! Code for http/grpc requests tracing and logging. 2 | #[cfg(feature = "tls")] 3 | use crate::application::tls::RemoteAddr; 4 | use axum::extract::ConnectInfo; 5 | use axum::http::HeaderMap; 6 | use hyper::header::CONTENT_TYPE; 7 | use hyper::Request; 8 | use opentelemetry::{global::get_text_map_propagator, Context}; 9 | use opentelemetry_http::HeaderExtractor; 10 | use std::net::SocketAddr; 11 | 12 | /// Extracts remote Ip and Port from [`Request`] 13 | #[cfg(not(feature = "tls"))] 14 | pub fn extract_remote_address(request: &Request) -> Option<&SocketAddr> { 15 | request 16 | .extensions() 17 | .get::>() 18 | .map(|ConnectInfo(addr)| addr) 19 | } 20 | 21 | /// Extracts remote Ip and Port from [`Request`] 22 | #[cfg(feature = "tls")] 23 | pub fn extract_remote_address(request: &Request) -> Option<&SocketAddr> { 24 | request 25 | .extensions() 26 | .get::>() 27 | .map(|ConnectInfo(RemoteAddr(addr))| addr) 28 | } 29 | 30 | /// Extracts [`Context`] from [`Request`] 31 | pub fn extract_context(request: &Request) -> Context { 32 | get_text_map_propagator(|propagator| propagator.extract(&HeaderExtractor(request.headers()))) 33 | } 34 | 35 | /// Return [`true`] if incoming request [`CONTENT_TYPE`] header value starts with "application/grpc" 36 | pub fn is_grpc(headers: &HeaderMap) -> bool { 37 | headers.get(CONTENT_TYPE).map_or(false, |content_type| { 38 | content_type.as_bytes().starts_with(b"application/grpc") 39 | }) 40 | } 41 | -------------------------------------------------------------------------------- /src/middleware/tracing/grpc_req.rs: -------------------------------------------------------------------------------- 1 | use crate::extensions::HttpReqExt; 2 | use crate::middleware::tracing::common::extract_remote_address; 3 | use axum::middleware::Next; 4 | use axum::response::IntoResponse; 5 | use hyper::header::HeaderValue; 6 | use hyper::{HeaderMap, Request}; 7 | use std::str::FromStr; 8 | use tokio::time::Instant; 9 | use tracing::{Level, Span}; 10 | 11 | const HEADER_GRPC_STATUS: &str = "grpc-status"; 12 | const PROTOCOL_GRPC: &str = "grpc"; 13 | 14 | /// Fn to be used with [`axum::middleware::from_fn`] to trace grpc request 15 | pub async fn trace_grpc_request( 16 | request: Request, 17 | next: Next, 18 | service_name: &str, 19 | component_name: &str, 20 | ) -> impl IntoResponse { 21 | let span = Span::current(); 22 | 23 | let req_method = request.method().to_string(); 24 | let grpc_method = request.uri().path().to_owned(); 25 | let remote_address = extract_remote_address(&request); 26 | 27 | tracing::info!( 28 | url = &grpc_method, 29 | ">>> [Request] [{req_method}] [{grpc_method}]" 30 | ); 31 | 32 | span.record("service", service_name); 33 | span.record("component", component_name); 34 | span.record("rpc.method", &grpc_method); 35 | 36 | if let Some(addr) = remote_address { 37 | span.record("net.peer.ip", addr.ip().to_string()); 38 | span.record("net.peer.port", addr.port()); 39 | } 40 | 41 | let duration = Instant::now(); 42 | let mut response = next.run(request).await; 43 | let elapsed = duration.elapsed(); 44 | 45 | let duration = elapsed.as_millis(); 46 | 47 | let status: i32 = extract_grpc_status_code(response.headers()) 48 | .unwrap_or(tonic::Code::Unknown) 49 | .into(); 50 | 51 | span.record("rpc.grpc.status_code", status); 52 | 53 | tracing::info!( 54 | url = &grpc_method, 55 | duration = duration, 56 | statusCode = status, 57 | "[Response] <<< [{req_method}] [{grpc_method}] [{PROTOCOL_GRPC}] [{status}] in [{duration}ms]" 58 | ); 59 | 60 | response.headers_mut().inject_from_current_span(); 61 | response 62 | } 63 | 64 | /// Creates GRPC [`Span`] with predefined empty attributes. 65 | pub fn make_grpc_span() -> Span { 66 | tracing::span!( 67 | Level::INFO, 68 | "grpc-request", 69 | service = tracing::field::Empty, 70 | component = tracing::field::Empty, 71 | rpc.system = "grpc", 72 | rpc.method = tracing::field::Empty, 73 | rpc.grpc.stream.id = tracing::field::Empty, 74 | rpc.grpc.status_code = tracing::field::Empty, 75 | net.peer.ip = tracing::field::Empty, 76 | net.peer.port = tracing::field::Empty, 77 | trace.level = "INFO" 78 | ) 79 | } 80 | /// Extracts grpc status from [`HeaderMap`] 81 | pub fn extract_grpc_status_code(headers: &HeaderMap) -> Option { 82 | headers 83 | .get(HEADER_GRPC_STATUS) 84 | .map(HeaderValue::to_str) 85 | .and_then(Result::ok) 86 | .map(i32::from_str) 87 | .and_then(Result::ok) 88 | .map(tonic::Code::from) 89 | } 90 | -------------------------------------------------------------------------------- /src/middleware/tracing/http_req.rs: -------------------------------------------------------------------------------- 1 | use crate::extensions::HttpReqExt; 2 | use crate::middleware::extract_remote_address; 3 | use axum::middleware::Next; 4 | use axum::response::IntoResponse; 5 | use hyper::Request; 6 | use tokio::time::Instant; 7 | use tracing::{Level, Span}; 8 | 9 | const PROTOCOL_HTTP: &str = "http"; 10 | 11 | /// Fn to be used with [`axum::middleware::from_fn`] to trace http request 12 | pub async fn trace_http_request( 13 | request: Request, 14 | next: Next, 15 | service_name: &str, 16 | component_name: &str, 17 | ) -> impl IntoResponse { 18 | let span = Span::current(); 19 | 20 | let req_method = request.method().to_string(); 21 | let remote_address = extract_remote_address(&request); 22 | 23 | span.record("service", service_name); 24 | span.record("component", component_name); 25 | span.record("http.method", &req_method); 26 | 27 | if let Some(addr) = remote_address { 28 | span.record("net.peer.ip", addr.ip().to_string()); 29 | span.record("net.peer.port", addr.port()); 30 | } 31 | 32 | let url = request 33 | .uri() 34 | .path_and_query() 35 | .map_or_else(|| request.uri().path(), |p| p.as_str()) 36 | .to_owned(); 37 | 38 | tracing::info!( 39 | method = &req_method, 40 | url = &url, 41 | ">>> [Request] [{req_method}] [{url}]" 42 | ); 43 | 44 | let duration = Instant::now(); 45 | let mut response = next.run(request).await; 46 | let elapsed = duration.elapsed(); 47 | 48 | let duration = elapsed.as_millis(); 49 | let status = response.status(); 50 | 51 | span.record("http.status_code", status.as_str()); 52 | 53 | tracing::info!( 54 | method = &req_method, 55 | url = &url, 56 | duration = duration, 57 | statusCode = status.as_str(), 58 | "[Response] <<< [{req_method}] [{url}] [{PROTOCOL_HTTP}] [{status}] in [{duration}ms]" 59 | ); 60 | 61 | response.headers_mut().inject_from_current_span(); 62 | response 63 | } 64 | 65 | /// Creates HTTP [`Span`] with predefined empty attributes. 66 | pub fn make_http_span() -> Span { 67 | tracing::span!( 68 | Level::INFO, 69 | "http-request", 70 | service = tracing::field::Empty, 71 | component = tracing::field::Empty, 72 | http.method = tracing::field::Empty, 73 | http.status_code = tracing::field::Empty, 74 | net.peer.ip = tracing::field::Empty, 75 | net.peer.port = tracing::field::Empty, 76 | trace.level = "INFO" 77 | ) 78 | } 79 | -------------------------------------------------------------------------------- /src/observability.rs: -------------------------------------------------------------------------------- 1 | //! This is a collection of useful functions and structs to be used with [`::metrics`] and [`::tracing`] crates. 2 | mod headers_filter; 3 | mod metrics; 4 | mod tracing; 5 | 6 | #[doc(inline)] 7 | pub use self::headers_filter::*; 8 | #[doc(inline)] 9 | pub use self::metrics::*; 10 | #[doc(inline)] 11 | pub use self::tracing::*; 12 | -------------------------------------------------------------------------------- /src/observability/headers_filter.rs: -------------------------------------------------------------------------------- 1 | //! [`HeadersFilter`] definition 2 | use crate::extensions::DeserializeExt; 3 | use serde::{Deserialize, Deserializer}; 4 | use serde_json::Value; 5 | use std::collections::HashSet; 6 | use std::sync::OnceLock; 7 | 8 | const SANITIZE_PTR: &str = "/sanitize"; 9 | const INCLUDE_PTR: &str = "/include"; 10 | const EXCLUDE_PTR: &str = "/exclude"; 11 | 12 | /// This is uninitialised unless you call [`crate::bootstrap()`] or [`crate::observability::init_tracing()`] functions. 13 | /// If initialised but env variables are not set fregate will include all headers. 14 | /// Expects string values separated with ',' or standalone "*" character meaning: all. 15 | /// Example: 16 | /// ```no_run 17 | /// std::env::set_var("TEST_HEADERS_SANITIZE", "password,login,client_id"); 18 | /// std::env::set_var("TEST_HEADERS_EXCLUDE", "authorization"); 19 | /// std::env::set_var("TEST_HEADERS_INCLUDE", "*"); 20 | /// ``` 21 | /// In [`crate::extensions::HeaderFilterExt`] trait implementation will have next behaviour: 22 | /// Include all headers except for "authorization" and sanitize "password,login,client_id" headers. 23 | pub static HEADERS_FILTER: OnceLock = OnceLock::new(); 24 | 25 | /// Headers filter options 26 | #[derive(Debug, Clone)] 27 | pub enum Filter { 28 | /// All headers 29 | All, 30 | /// Set of header names to filter 31 | Set(HashSet), 32 | } 33 | 34 | /// Struct to save headers filters. 35 | #[derive(Debug, Clone)] 36 | pub struct HeadersFilter { 37 | /// Headers to be included. 38 | pub include: Filter, 39 | /// Headers to be excluded. 40 | pub exclude: Filter, 41 | /// Headers to be sanitized. 42 | pub sanitize: Filter, 43 | } 44 | 45 | impl<'de> Deserialize<'de> for HeadersFilter { 46 | fn deserialize(deserializer: D) -> Result 47 | where 48 | D: Deserializer<'de>, 49 | { 50 | let config = Value::deserialize(deserializer)?; 51 | 52 | let sanitize: Option = config 53 | .pointer_and_deserialize::<_, D::Error>(SANITIZE_PTR) 54 | .ok(); 55 | let include: Option = config 56 | .pointer_and_deserialize::<_, D::Error>(INCLUDE_PTR) 57 | .ok(); 58 | let exclude: Option = config 59 | .pointer_and_deserialize::<_, D::Error>(EXCLUDE_PTR) 60 | .ok(); 61 | 62 | Ok(HeadersFilter { 63 | include: from_str_to_filter(include), 64 | exclude: from_str_to_filter(exclude), 65 | sanitize: from_str_to_filter(sanitize), 66 | }) 67 | } 68 | } 69 | 70 | fn from_str_to_filter(str: Option) -> Filter { 71 | str.map_or(Filter::Set(HashSet::default()), |str| { 72 | let str = str.trim(); 73 | 74 | if str == "*" { 75 | Filter::All 76 | } else { 77 | Filter::Set( 78 | str.split(',') 79 | .map(|field| field.trim().to_ascii_lowercase()) 80 | .collect::>(), 81 | ) 82 | } 83 | }) 84 | } 85 | -------------------------------------------------------------------------------- /src/observability/metrics.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod cgroupv2; 2 | #[cfg(target_os = "linux")] 3 | pub(crate) mod proc_limits; 4 | pub(crate) mod recorder; 5 | pub(crate) mod sys_info; 6 | #[cfg(feature = "tokio-metrics")] 7 | pub mod tokio_metrics; 8 | 9 | use crate::error::Result; 10 | use crate::observability::metrics::recorder::{get_handle, get_recorder}; 11 | 12 | /// Return rendered metrics. 13 | /// By default fregate sets `/metrics` endpoint for your [`Application]` which uses [`metrics_exporter_prometheus::PrometheusHandle::render`] fn to get currently available metrics. 14 | /// How callback might be used see in [`example`](https://github.com/elefant-dev/fregate-rs/tree/main/examples/metrics-callback). 15 | pub fn render_metrics(callback: Option<&(dyn Fn() + Send + Sync + 'static)>) -> String { 16 | if let Some(callback) = callback { 17 | callback(); 18 | } 19 | 20 | get_handle().render() 21 | } 22 | 23 | /// Initialise PrometheusRecorder 24 | pub fn init_metrics(cgroup_metrics: bool) -> Result<()> { 25 | metrics::set_recorder(get_recorder())?; 26 | 27 | #[cfg(feature = "tokio-metrics")] 28 | tokio_metrics::register_metrics(); 29 | 30 | if cgroup_metrics { 31 | cgroupv2::register_cgroup_metrics(); 32 | } else { 33 | sys_info::register_sys_metrics(); 34 | } 35 | 36 | Ok(()) 37 | } 38 | -------------------------------------------------------------------------------- /src/observability/metrics/proc_limits.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | use std::str::FromStr; 3 | 4 | #[derive(Debug, Clone)] 5 | pub(crate) struct Limits { 6 | pub(crate) max_cpu_limit: Limit, 7 | } 8 | 9 | #[derive(Debug, Clone)] 10 | pub(crate) struct Limit { 11 | pub(crate) soft_limit: Option, 12 | } 13 | 14 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 15 | pub(crate) enum UnlimitedValue { 16 | Unlimited, 17 | Value(u64), 18 | } 19 | 20 | pub(crate) fn read_process_limits(pid: u32) -> Result> { 21 | let limits = std::fs::read_to_string(format!("/proc/{pid}/limits"))?; 22 | read_limits(limits) 23 | } 24 | 25 | fn read_limits(limits: String) -> Result> { 26 | for line in limits.lines() { 27 | if let Some((_, values)) = line.split_once("Max cpu time") { 28 | let mut values = values.split_whitespace(); 29 | 30 | let soft_limit = values.next().and_then(|soft_limit| match soft_limit { 31 | "unlimited" => Some(UnlimitedValue::Unlimited), 32 | _ => u64::from_str(soft_limit.trim()) 33 | .ok() 34 | .map(UnlimitedValue::Value), 35 | }); 36 | 37 | return Ok(Limits { 38 | max_cpu_limit: Limit { soft_limit }, 39 | }); 40 | } 41 | } 42 | 43 | Err("Could not find `Max cpu time` soft limit".into()) 44 | } 45 | 46 | #[cfg(test)] 47 | mod read_test { 48 | use super::read_limits; 49 | use super::UnlimitedValue; 50 | 51 | #[test] 52 | fn read_max_cpu_time_unlimited() { 53 | let limits = 54 | r#"Limit Soft Limit Hard Limit Units 55 | Max cpu time unlimited unlimited seconds 56 | Max file size unlimited unlimited bytes 57 | Max data size unlimited unlimited bytes 58 | Max stack size 8388608 unlimited bytes 59 | Max core file size 0 unlimited bytes 60 | Max resident set unlimited unlimited bytes 61 | Max processes unlimited unlimited processes 62 | Max open files 1048576 1048576 files 63 | Max locked memory 65536 65536 bytes 64 | Max address space unlimited unlimited bytes 65 | Max file locks unlimited unlimited locks 66 | Max pending signals 63487 63487 signals 67 | Max msgqueue size 819200 819200 bytes 68 | Max nice priority 0 0 69 | Max realtime priority 0 0 70 | Max realtime timeout unlimited unlimited us"# 71 | .to_owned(); 72 | 73 | let limits = read_limits(limits).unwrap(); 74 | assert_eq!( 75 | limits.max_cpu_limit.soft_limit, 76 | Some(UnlimitedValue::Unlimited) 77 | ) 78 | } 79 | 80 | #[test] 81 | fn read_max_cpu_time_exact() { 82 | let limits = 83 | r#"Limit Soft Limit Hard Limit Units 84 | Max cpu time 8388608 unlimited seconds 85 | Max file size unlimited unlimited bytes 86 | Max data size unlimited unlimited bytes 87 | Max stack size 8388608 unlimited bytes 88 | Max core file size 0 unlimited bytes 89 | Max resident set unlimited unlimited bytes 90 | Max processes unlimited unlimited processes 91 | Max open files 1048576 1048576 files 92 | Max locked memory 65536 65536 bytes 93 | Max address space unlimited unlimited bytes 94 | Max file locks unlimited unlimited locks 95 | Max pending signals 63487 63487 signals 96 | Max msgqueue size 819200 819200 bytes 97 | Max nice priority 0 0 98 | Max realtime priority 0 0 99 | Max realtime timeout unlimited unlimited us"# 100 | .to_owned(); 101 | 102 | let limits = read_limits(limits).unwrap(); 103 | assert_eq!( 104 | limits.max_cpu_limit.soft_limit, 105 | Some(UnlimitedValue::Value(8388608)) 106 | ) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/observability/metrics/recorder.rs: -------------------------------------------------------------------------------- 1 | use metrics_exporter_prometheus::{PrometheusBuilder, PrometheusHandle, PrometheusRecorder}; 2 | use std::sync::OnceLock; 3 | 4 | pub(crate) fn get_recorder() -> &'static PrometheusRecorder { 5 | static RECORDER: OnceLock = OnceLock::new(); 6 | 7 | RECORDER.get_or_init(|| PrometheusBuilder::new().build_recorder()) 8 | } 9 | 10 | pub(crate) fn get_handle() -> &'static PrometheusHandle { 11 | static HANDLE: OnceLock = OnceLock::new(); 12 | 13 | HANDLE.get_or_init(|| get_recorder().handle()) 14 | } 15 | -------------------------------------------------------------------------------- /src/observability/metrics/sys_info.rs: -------------------------------------------------------------------------------- 1 | use metrics::{describe_gauge, gauge, register_gauge}; 2 | use std::time::Duration; 3 | use sysinfo::{Pid, ProcessExt, System, SystemExt}; 4 | 5 | pub fn init_sys_metrics(metrics_update_ms: Duration) { 6 | let u32_pid = std::process::id(); 7 | let pid = Pid::from(u32_pid as usize); 8 | 9 | tokio::task::spawn(async move { 10 | let mut system = System::new(); 11 | 12 | loop { 13 | { 14 | #[cfg(target_os = "linux")] 15 | { 16 | use crate::observability::proc_limits::{read_process_limits, UnlimitedValue}; 17 | 18 | match read_process_limits(u32_pid) { 19 | Ok(limits) => match limits.max_cpu_limit.soft_limit { 20 | Some(UnlimitedValue::Unlimited) => gauge!("max_cpu_time", -1_f64), 21 | Some(UnlimitedValue::Value(v)) => gauge!("max_cpu_time", v as f64), 22 | _ => {} 23 | }, 24 | Err(err) => { 25 | tracing::error!("Could not update limits: {err}"); 26 | } 27 | } 28 | } 29 | 30 | system.refresh_memory(); 31 | system.refresh_process(pid); 32 | let process = system.process(pid); 33 | 34 | if let Some(process) = process { 35 | gauge!("memory_used", process.memory() as f64); 36 | gauge!("cpu_used", process.cpu_usage() as f64); 37 | } 38 | 39 | gauge!("num_cpus", num_cpus::get() as f64); 40 | gauge!("memory_available", system.total_memory() as f64); 41 | } 42 | 43 | tokio::time::sleep(metrics_update_ms).await; 44 | } 45 | }); 46 | } 47 | 48 | pub(crate) fn register_sys_metrics() { 49 | for (name, describe) in [ 50 | ( 51 | "num_cpus", 52 | "Returns the number of available CPUs of the current system.", 53 | ), 54 | ( 55 | "memory_available", 56 | "Returns the size of available memory in bytes.", 57 | ), 58 | ("memory_used", "Returns the memory usage in bytes."), 59 | ("cpu_used", "Returns the total CPU usage (in %). Notice that it might be bigger than 100 if run on a multi-core machine."), 60 | ] { 61 | describe_gauge!(name, describe); 62 | register_gauge!(name); 63 | } 64 | 65 | #[cfg(target_os = "linux")] 66 | { 67 | describe_gauge!( 68 | "max_cpu_time", 69 | "Returns max cpu time soft limit in seconds. `-1` means `unlimited`" 70 | ); 71 | register_gauge!("max_cpu_time"); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/observability/tracing.rs: -------------------------------------------------------------------------------- 1 | //! Tools initialise logging and tracing 2 | 3 | mod event_formatter; 4 | pub mod floor_char_boundary; 5 | mod log_layer; 6 | mod otlp_layer; 7 | mod tracing_fields; 8 | mod writer; 9 | 10 | pub use event_formatter::*; 11 | pub use log_layer::*; 12 | pub use otlp_layer::*; 13 | pub use tracing_fields::*; 14 | pub use writer::*; 15 | 16 | use crate::error::Result; 17 | use crate::observability::HEADERS_FILTER; 18 | use crate::LoggerConfig; 19 | use opentelemetry::global::set_error_handler; 20 | use std::sync::OnceLock; 21 | use tracing_appender::non_blocking::WorkerGuard; 22 | use tracing_subscriber::layer::Layered; 23 | use tracing_subscriber::util::SubscriberInitExt; 24 | use tracing_subscriber::{ 25 | filter::EnvFilter, layer::SubscriberExt, registry, reload::Handle, Layer, Registry, 26 | }; 27 | 28 | /// Global value to be used everywhere. 29 | pub const SANITIZED_VALUE: &str = "*****"; 30 | 31 | /// This by default uninitialised unless you call [`crate::bootstrap()`] or [`init_tracing`] functions. 32 | /// Used to change log level filter 33 | /// See in [`example`](https://github.com/elefant-dev/fregate-rs/tree/main/examples/log-level-change) how it might be used. 34 | pub static LOG_LAYER_HANDLE: OnceLock = OnceLock::new(); 35 | 36 | /// This by default uninitialised unless you call [`crate::bootstrap()`] or [`init_tracing`] functions. 37 | /// Used to change trace level filter 38 | /// See in [`example`](https://github.com/elefant-dev/fregate-rs/tree/main/examples/log-level-change) how it might be used. 39 | pub static OTLP_LAYER_HANDLE: OnceLock = OnceLock::new(); 40 | 41 | /// Alias for [`Handle + Send + Sync>>, Registry>>`] 42 | pub type LogLayerHandle = 43 | Handle + Send + Sync>>, Registry>>; 44 | 45 | /// Alias for [`Handle`] 46 | pub type TraceLayerHandle = Handle; 47 | 48 | /// Set-up:\ 49 | /// 1. [`log_layer()`] with custom event formatter [`EventFormatter`].\ 50 | /// 2. [`otlp_layer()`].\ 51 | /// 3. Reload filters for both layers: [`OTLP_LAYER_HANDLE`] and [`LOG_LAYER_HANDLE`].\ 52 | /// 4. [`HEADERS_FILTER`] to be used in [`crate::extensions::HeaderFilterExt`].\ 53 | /// 5. Sets panic hook.\ 54 | /// Uses [`tracing_appender`] crate to do non blocking writes to stdout, so returns [`WorkerGuard`]. Read more here: [`https://docs.rs/tracing-appender/latest/tracing_appender/non_blocking/struct.WorkerGuard.html`] 55 | #[allow(clippy::too_many_arguments)] 56 | pub fn init_tracing( 57 | logger_config: &LoggerConfig, 58 | trace_level: &str, 59 | version: &str, 60 | service_name: &str, 61 | component_name: &str, 62 | traces_endpoint: Option<&str>, 63 | ) -> Result { 64 | let (log_layer, log_reload, worker) = 65 | log_layer(logger_config, version, service_name, component_name)?; 66 | let (otlp_layer, otlp_reload) = otlp_layer(trace_level, component_name, traces_endpoint)?; 67 | registry().with(otlp_layer).with(log_layer).try_init()?; 68 | 69 | let _ = LOG_LAYER_HANDLE.get_or_init(|| log_reload); 70 | if let Some(otlp_reload) = otlp_reload { 71 | let _ = OTLP_LAYER_HANDLE.get_or_init(|| otlp_reload); 72 | } 73 | if let Some(headers_filter) = logger_config.headers_filter.clone() { 74 | let _ = HEADERS_FILTER.get_or_init(|| headers_filter); 75 | } 76 | 77 | set_error_handler(|err| { 78 | tracing::error!("{err}"); 79 | })?; 80 | set_panic_hook(); 81 | 82 | Ok(worker) 83 | } 84 | 85 | fn set_panic_hook() { 86 | // Capture the span context in which the program panicked 87 | std::panic::set_hook(Box::new(|panic| { 88 | // If the panic has a source location, record it as structured fields. 89 | if let Some(location) = panic.location() { 90 | tracing::error!( 91 | message = %panic, 92 | panic.file = location.file(), 93 | panic.line = location.line(), 94 | panic.column = location.column(), 95 | ); 96 | } else { 97 | tracing::error!(message = %panic); 98 | } 99 | })); 100 | } 101 | -------------------------------------------------------------------------------- /src/observability/tracing/floor_char_boundary.rs: -------------------------------------------------------------------------------- 1 | //! Since Rust will panic on attempt to get slice on invalid char boundaries 2 | //! It might be useful to find valid char boundaries and avoid looping over each element of string. 3 | 4 | /// This is a copy of floor_char_boundary fn which is [`unstable`](https://github.com/rust-lang/rust/issues/93743). 5 | /// Once it is stabilised this will be removed from fregate. 6 | pub fn floor_char_boundary(val: &str, index: usize) -> usize { 7 | if index >= val.len() { 8 | val.len() 9 | } else { 10 | let lower_bound = index.saturating_sub(3); 11 | let new_index = val 12 | .as_bytes() 13 | .get(lower_bound..=index) 14 | .unwrap_or_default() 15 | .iter() 16 | .rposition(|b| is_utf8_char_boundary(*b)); 17 | 18 | let new_index = match new_index { 19 | Some(val) => val, 20 | None => unreachable!("floor_char_boundary fn should never fail"), 21 | }; 22 | 23 | lower_bound + new_index 24 | } 25 | } 26 | 27 | #[inline] 28 | const fn is_utf8_char_boundary(byte: u8) -> bool { 29 | (byte as i8) >= -0x40 30 | } 31 | -------------------------------------------------------------------------------- /src/observability/tracing/log_layer.rs: -------------------------------------------------------------------------------- 1 | use crate::error::Result; 2 | use crate::observability::tracing::writer::RollingFileWriter; 3 | use crate::observability::tracing::{ 4 | event_formatter::EventFormatter, COMPONENT, INSTANCE_ID, SERVICE, VERSION, 5 | }; 6 | use crate::LoggerConfig; 7 | use std::io::Write; 8 | use std::str::FromStr; 9 | use tracing::Subscriber; 10 | use tracing_appender::non_blocking::{WorkerGuard, DEFAULT_BUFFERED_LINES_LIMIT}; 11 | use tracing_subscriber::{ 12 | filter::EnvFilter, filter::Filtered, registry::LookupSpan, reload, reload::Handle, Layer, 13 | }; 14 | use uuid; 15 | 16 | /// Returns [`Layer`] with custom event formatter [`EventFormatter`] 17 | /// Configured with non-blocking writer [`tracing_appender::non_blocking::NonBlocking`] to [`std::io::stdout()`] 18 | #[allow(clippy::too_many_arguments)] 19 | #[allow(clippy::type_complexity)] 20 | pub fn log_layer( 21 | logger_config: &LoggerConfig, 22 | version: &str, 23 | service_name: &str, 24 | component_name: &str, 25 | ) -> Result<( 26 | Filtered + Send + Sync>, reload::Layer, S>, 27 | Handle, 28 | WorkerGuard, 29 | )> 30 | where 31 | S: Subscriber + for<'a> LookupSpan<'a>, 32 | { 33 | let LoggerConfig { 34 | ref log_level, 35 | ref logging_path, 36 | ref logging_file, 37 | msg_length, 38 | buffered_lines_limit, 39 | logging_interval: logging_file_interval, 40 | logging_max_file_size: logging_file_limit, 41 | logging_max_history: logging_file_max_age, 42 | logging_max_file_count: logging_file_max_count, 43 | logging_enable_compression: enable_zip, 44 | headers_filter: _, 45 | } = logger_config; 46 | 47 | let mut formatter = EventFormatter::new_with_limit(*msg_length); 48 | 49 | formatter.add_default_field_to_events(VERSION, version)?; 50 | formatter.add_default_field_to_events(SERVICE, service_name)?; 51 | formatter.add_default_field_to_events(COMPONENT, component_name)?; 52 | formatter.add_default_field_to_events(INSTANCE_ID, uuid::Uuid::new_v4().to_string())?; 53 | let buffered_lines_limit = buffered_lines_limit.unwrap_or(DEFAULT_BUFFERED_LINES_LIMIT); 54 | 55 | let dest: Box = if let Some(logging_path) = logging_path { 56 | let file_name_prefix = logging_file 57 | .as_deref() 58 | .map(|v| v.to_owned()) 59 | .unwrap_or(format!("{component_name}.log")); 60 | 61 | let to_file = RollingFileWriter::new( 62 | logging_path, 63 | file_name_prefix, 64 | *logging_file_interval, 65 | *logging_file_limit, 66 | *logging_file_max_age, 67 | *logging_file_max_count, 68 | *enable_zip, 69 | ); 70 | Box::new(to_file) as _ 71 | } else { 72 | Box::new(std::io::stdout()) as _ 73 | }; 74 | 75 | let (writer, guard) = tracing_appender::non_blocking::NonBlockingBuilder::default() 76 | .lossy(true) 77 | .buffered_lines_limit(buffered_lines_limit) 78 | .finish(dest); 79 | 80 | let layer = tracing_subscriber::fmt::layer() 81 | .with_writer(writer) 82 | .event_format(formatter) 83 | .boxed(); 84 | 85 | let filter = EnvFilter::from_str(log_level).unwrap_or_default(); 86 | let (filter, reload) = reload::Layer::new(filter); 87 | let layer = layer.with_filter(filter); 88 | 89 | Ok((layer, reload, guard)) 90 | } 91 | -------------------------------------------------------------------------------- /src/observability/tracing/otlp_layer.rs: -------------------------------------------------------------------------------- 1 | use crate::error::Result; 2 | use opentelemetry::sdk::propagation::TraceContextPropagator; 3 | use opentelemetry::{global, sdk, sdk::Resource, KeyValue}; 4 | use opentelemetry_otlp::WithExportConfig; 5 | use std::str::FromStr; 6 | use tracing::Subscriber; 7 | use tracing_subscriber::filter::filter_fn; 8 | use tracing_subscriber::reload::Handle; 9 | use tracing_subscriber::{registry::LookupSpan, reload, EnvFilter, Layer}; 10 | 11 | /// Sets [`tracing_opentelemetry::layer()`] with given arguments. 12 | /// Returns optional boxed [`Layer`] and optional [`Handle`] so filter might be changed in runtime. 13 | /// Uses batched span processor with [`opentelemetry::runtime::Tokio`] runtime. 14 | /// If traces_endpoint is [`None`] skips layer configuration and returns [`None`] 15 | #[allow(clippy::type_complexity)] 16 | pub fn otlp_layer( 17 | trace_level: &str, 18 | component_name: &str, 19 | traces_endpoint: Option<&str>, 20 | ) -> Result<( 21 | Option + Send + Sync>>, 22 | Option>, 23 | )> 24 | where 25 | S: Subscriber + for<'a> LookupSpan<'a> + Send + Sync, 26 | { 27 | global::set_text_map_propagator(TraceContextPropagator::new()); 28 | 29 | let trace_layer = if let Some(traces_endpoint) = traces_endpoint { 30 | let tracer = opentelemetry_otlp::new_pipeline() 31 | .tracing() 32 | .with_exporter( 33 | opentelemetry_otlp::new_exporter() 34 | .tonic() 35 | .with_endpoint(traces_endpoint), 36 | ) 37 | .with_trace_config(sdk::trace::config().with_resource(Resource::new(vec![ 38 | // Here it is service.name, but in our logs it is component_name. 39 | KeyValue::new("service.name", component_name.to_owned()), 40 | ]))) 41 | .install_batch(opentelemetry::runtime::Tokio)?; 42 | 43 | let layer = tracing_opentelemetry::layer().with_tracer(tracer); 44 | 45 | let filter = EnvFilter::from_str(trace_level).unwrap_or_default(); 46 | let (filter, reload) = reload::Layer::new(filter); 47 | 48 | let trace_layer = layer 49 | .with_filter(filter) 50 | .with_filter(filter_fn(|metadata| metadata.is_span())) 51 | .boxed(); 52 | 53 | (Some(trace_layer), Some(reload)) 54 | } else { 55 | (None, None) 56 | }; 57 | 58 | Ok(trace_layer) 59 | } 60 | -------------------------------------------------------------------------------- /src/observability/tracing/tracing_fields.rs: -------------------------------------------------------------------------------- 1 | //! Instrument for logging 2 | use std::collections::HashMap; 3 | use std::fmt::{Debug, Display, Formatter}; 4 | use valuable::{Fields, NamedField, NamedValues, StructDef, Structable, Valuable, Value, Visit}; 5 | 6 | pub(crate) const TRACING_FIELDS_STRUCTURE_NAME: &str = 7 | "tracing_fields:fc848aeb-3723-438e-b3c3-35162b737a98"; 8 | 9 | /// If used in pair with [`crate::observability::EventFormatter`] and unstable [`tracing feature`](https://github.com/tokio-rs/tracing/discussions/1906) this has custom logging behaviour. See in example below:\ 10 | /// Once feature is stabilised this might change. 11 | /// 12 | /// Example: 13 | ///```rust 14 | /// use fregate::observability::init_tracing; 15 | /// use fregate::observability::TracingFields; 16 | /// use fregate::LoggerConfig; 17 | /// use fregate::valuable::Valuable; 18 | /// use fregate::{tokio, tracing::info}; 19 | /// use std::net::{IpAddr, Ipv4Addr, SocketAddr}; 20 | /// 21 | /// const STATIC_KEY: &str = "STATIC_KEY"; 22 | /// const STATIC_VALUE: &str = "STATIC_VALUE"; 23 | /// 24 | /// #[tokio::main] 25 | /// async fn main() { 26 | /// let logger_config = LoggerConfig { 27 | /// log_level: "info".to_string(), 28 | /// logging_path: None, 29 | /// logging_file: None, 30 | /// msg_length: None, 31 | /// buffered_lines_limit: None, 32 | /// logging_interval: None, 33 | /// logging_max_file_size: None, 34 | /// logging_max_history: None, 35 | /// logging_max_file_count: None, 36 | /// logging_enable_compression: false, 37 | /// headers_filter: None, 38 | /// }; 39 | /// 40 | /// let _guard = init_tracing( 41 | /// &logger_config, 42 | /// "info", 43 | /// "0.0.0", 44 | /// "fregate", 45 | /// "marker", 46 | /// None, 47 | /// ) 48 | /// .unwrap(); 49 | /// 50 | /// 51 | /// let mut marker = TracingFields::with_capacity(10); 52 | /// let socket = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8080); 53 | /// 54 | /// marker.insert_ref(STATIC_KEY, &STATIC_VALUE); 55 | /// marker.insert_as_string("address", &socket); 56 | /// marker.insert_as_debug("address_debug", &socket); 57 | /// 58 | /// info!(marker = marker.as_value(), "message"); 59 | /// } 60 | /// 61 | /// ``` 62 | /// Output: 63 | ///```json 64 | /// {"time":1676630313178421000,"timestamp":"2023-02-17T10:38:33.178Z","LogLevel":"INFO","target":"playground","component":"marker","service":"fregate","version":"0.0.0","msg":"message","STATIC_KEY":"STATIC_VALUE","address":"127.0.0.1:8080","address_debug":"127.0.0.1:8080"} 65 | ///``` 66 | #[derive(Default)] 67 | pub struct TracingFields<'a> { 68 | fields: HashMap<&'static str, Field<'a>>, 69 | } 70 | 71 | enum Field<'a> { 72 | Str(&'a str), 73 | String(String), 74 | ValuableRef(&'a (dyn Valuable + Send + Sync)), 75 | ValuableOwned(Box), 76 | } 77 | 78 | impl<'a> Field<'a> { 79 | fn as_value(&self) -> Value<'_> { 80 | match self { 81 | Field::Str(s) => s.as_value(), 82 | Field::String(s) => s.as_value(), 83 | Field::ValuableOwned(s) => s.as_value(), 84 | Field::ValuableRef(r) => r.as_value(), 85 | } 86 | } 87 | } 88 | 89 | impl<'a> Debug for TracingFields<'a> { 90 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 91 | let mut f = f.debug_struct("TracingFields"); 92 | for (k, v) in self.fields.iter() { 93 | let value = v.as_value(); 94 | 95 | f.field(k, &value as _); 96 | } 97 | f.finish() 98 | } 99 | } 100 | 101 | impl<'a> TracingFields<'a> { 102 | /// Create empty [`TracingFields`]. 103 | pub fn new() -> Self { 104 | Self::default() 105 | } 106 | 107 | /// Create empty [`TracingFields`] with specified capacity 108 | pub fn with_capacity(capacity: usize) -> Self { 109 | Self { 110 | fields: HashMap::with_capacity(capacity), 111 | } 112 | } 113 | 114 | /// Wraps value into [`Box`] and insert key-value pair into the map. Overwrites value if key is present. 115 | pub fn insert(&mut self, key: &'static str, value: V) { 116 | self.fields 117 | .insert(key, Field::ValuableOwned(Box::new(value))); 118 | } 119 | 120 | /// Inserts a key-value pair of references into the map. Overwrites value if key is present. 121 | pub fn insert_ref(&mut self, key: &'static str, value: &'a V) { 122 | self.fields.insert(key, Field::ValuableRef(value)); 123 | } 124 | 125 | /// Inserts a key-value pair of references into the map. Overwrites value if key is present. 126 | pub fn insert_str(&mut self, key: &'static str, value: &'a str) { 127 | self.fields.insert(key, Field::Str(value)); 128 | } 129 | 130 | /// Converts value to [`String`] using [`Display`] implementation before insertion. Overwrites value if key is present. 131 | pub fn insert_as_string(&mut self, key: &'static str, value: &V) { 132 | self.fields.insert(key, Field::String(value.to_string())); 133 | } 134 | 135 | /// Converts value to [`String`] using [`Debug`] implementation before insertion. Overwrites value if key is present. 136 | pub fn insert_as_debug(&mut self, key: &'static str, value: &V) { 137 | self.fields.insert(key, Field::String(format!("{value:?}"))); 138 | } 139 | 140 | /// Removes each key from the map. 141 | pub fn remove_keys<'b>(&mut self, keys: impl IntoIterator) { 142 | for key in keys { 143 | self.remove_by_key(key); 144 | } 145 | } 146 | 147 | /// Removes key from the map. 148 | pub fn remove_by_key(&mut self, key: &str) { 149 | self.fields.remove(key); 150 | } 151 | 152 | /// Merge with other [`TracingFields`] consuming other. 153 | pub fn merge<'b: 'a>(&mut self, other: TracingFields<'b>) { 154 | self.fields.reserve(other.fields.len()); 155 | 156 | other.fields.into_iter().for_each(|(k, v)| { 157 | self.fields.insert(k, v); 158 | }); 159 | } 160 | } 161 | 162 | impl<'a> Valuable for TracingFields<'a> { 163 | fn as_value(&self) -> Value<'_> { 164 | Value::Structable(self) 165 | } 166 | 167 | fn visit(&self, visit: &mut dyn Visit) { 168 | for (field, value) in self.fields.iter() { 169 | let value_ref = value.as_value(); 170 | 171 | visit.visit_named_fields(&NamedValues::new(&[NamedField::new(field)], &[value_ref])); 172 | } 173 | } 174 | } 175 | 176 | impl<'a> Structable for TracingFields<'a> { 177 | fn definition(&self) -> StructDef<'_> { 178 | StructDef::new_dynamic(TRACING_FIELDS_STRUCTURE_NAME, Fields::Named(&[])) 179 | } 180 | } 181 | 182 | #[cfg(test)] 183 | mod test { 184 | use crate::observability::TracingFields; 185 | use std::net::{IpAddr, Ipv4Addr, SocketAddr}; 186 | 187 | #[test] 188 | fn test_merge_with_static() { 189 | let mut destination = TracingFields::new(); 190 | let mut source = TracingFields::new(); 191 | 192 | let socket = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8080).to_string(); 193 | 194 | destination.insert_ref("static", &"static"); 195 | source.insert_ref("socket", &socket); 196 | 197 | destination.merge(source); 198 | 199 | assert_eq!(destination.fields.len(), 2); 200 | assert!(destination.fields.contains_key("static")); 201 | assert!(destination.fields.contains_key("socket")); 202 | } 203 | 204 | #[test] 205 | fn test_merge_with_local() { 206 | let mut destination = TracingFields::new(); 207 | let mut source = TracingFields::new(); 208 | 209 | let first = "first".to_owned(); 210 | let second = "second".to_owned(); 211 | 212 | destination.insert_ref("first", &first); 213 | source.insert_ref("second", &second); 214 | 215 | destination.merge(source); 216 | 217 | assert_eq!(destination.fields.len(), 2); 218 | assert!(destination.fields.contains_key("first")); 219 | assert!(destination.fields.contains_key("second")); 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /src/resources/default_conf.toml: -------------------------------------------------------------------------------- 1 | # TODO(kos): Consider describing each field and adding example with all full 2 | # config. It will make more comfortable to use the crate. 3 | # As example: 4 | # https://github.com/coturn/coturn/blob/4.6.0/examples/etc/turnserver.conf 5 | 6 | host = "0.0.0.0" 7 | port = 8000 8 | 9 | [log] 10 | level = "info" 11 | 12 | [log.msg] 13 | length = 8192 #in bytes 14 | 15 | [trace] 16 | level = "info" 17 | 18 | [service] 19 | name = "default" 20 | 21 | [component] 22 | name = "default" 23 | version = "default" 24 | 25 | [private] 26 | 27 | [server.tls] 28 | handshake_timeout = 10000 # in milliseconds 29 | 30 | [server.metrics] 31 | update_interval = 1000 # in milliseconds 32 | 33 | [headers] 34 | include = "*" 35 | 36 | #[server.tls] 37 | #key_path = "/tls.key" 38 | #cert_path = "/tls.cert" 39 | -------------------------------------------------------------------------------- /src/resources/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elefant-dev/fregate-rs/311a4fc2adf9efbd88d39eef0b45c31c468f37b8/src/resources/favicon.png -------------------------------------------------------------------------------- /src/static_assert.rs: -------------------------------------------------------------------------------- 1 | /// # Example: 2 | /// 3 | /// ```rust 4 | /// use fregate::static_assert; 5 | /// 6 | /// #[repr(i32)] 7 | /// enum Enum { 8 | /// One = 1, 9 | /// Two, 10 | /// Three, 11 | /// } 12 | /// 13 | /// static_assert!(Enum::One as i32 == 1); 14 | /// ``` 15 | #[macro_export] 16 | macro_rules! static_assert { 17 | ($cond:expr) => { 18 | const _: () = assert!($cond); 19 | }; 20 | } 21 | 22 | /// # Example: 23 | /// 24 | /// ```rust 25 | /// use fregate::static_trait_assert; 26 | /// 27 | /// #[repr(i32)] 28 | /// enum Enum { 29 | /// One, 30 | /// Two, 31 | /// Three, 32 | /// } 33 | /// 34 | /// impl From for i32 { 35 | /// fn from(value: Enum) -> Self { 36 | /// value as i32 37 | /// } 38 | /// } 39 | /// 40 | /// static_trait_assert!(Enum, Into); 41 | /// ``` 42 | #[macro_export] 43 | macro_rules! static_trait_assert { 44 | ($t:ty, $traits:path) => { 45 | const fn type_trait_check() 46 | where 47 | $t: $traits, 48 | { 49 | } 50 | 51 | const _: () = type_trait_check(); 52 | }; 53 | } 54 | -------------------------------------------------------------------------------- /src/sugar.rs: -------------------------------------------------------------------------------- 1 | //! Collection of syntax-sugar-like functions 2 | pub mod grpc_codes; 3 | pub mod hash_builder; 4 | pub mod yaml_response; 5 | -------------------------------------------------------------------------------- /src/sugar/grpc_codes.rs: -------------------------------------------------------------------------------- 1 | //! Syntax-sugar-like functions for gRPC protocol 2 | use tonic::Code::{self, *}; 3 | 4 | /// All gRPC response codes 5 | pub const GRPC_CODES: [Code; 17] = [ 6 | Ok, 7 | Cancelled, 8 | Unknown, 9 | InvalidArgument, 10 | DeadlineExceeded, 11 | NotFound, 12 | AlreadyExists, 13 | PermissionDenied, 14 | ResourceExhausted, 15 | FailedPrecondition, 16 | Aborted, 17 | OutOfRange, 18 | Unimplemented, 19 | Internal, 20 | Unavailable, 21 | DataLoss, 22 | Unauthenticated, 23 | ]; 24 | 25 | /// Return name of enum Code 26 | #[inline] 27 | pub const fn grpc_code_to_str(code: Code) -> &'static str { 28 | match code { 29 | Ok => "Ok", 30 | Cancelled => "Cancelled", 31 | Unknown => "Unknown", 32 | InvalidArgument => "InvalidArgument", 33 | DeadlineExceeded => "DeadlineExceeded", 34 | NotFound => "NotFound", 35 | AlreadyExists => "AlreadyExists", 36 | PermissionDenied => "PermissionDenied", 37 | ResourceExhausted => "ResourceExhausted", 38 | FailedPrecondition => "FailedPrecondition", 39 | Aborted => "Aborted", 40 | OutOfRange => "OutOfRange", 41 | Unimplemented => "Unimplemented", 42 | Internal => "Internal", 43 | Unavailable => "Unavailable", 44 | DataLoss => "DataLoss", 45 | Unauthenticated => "Unauthenticated", 46 | } 47 | } 48 | 49 | /// Return gRPC response code as string number 50 | #[inline] 51 | pub const fn grpc_code_to_num(code: Code) -> &'static str { 52 | match code { 53 | Ok => "0", 54 | Cancelled => "1", 55 | Unknown => "2", 56 | InvalidArgument => "3", 57 | DeadlineExceeded => "4", 58 | NotFound => "5", 59 | AlreadyExists => "6", 60 | PermissionDenied => "7", 61 | ResourceExhausted => "8", 62 | FailedPrecondition => "9", 63 | Aborted => "10", 64 | OutOfRange => "11", 65 | Unimplemented => "12", 66 | Internal => "13", 67 | Unavailable => "14", 68 | DataLoss => "15", 69 | Unauthenticated => "16", 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/sugar/hash_builder.rs: -------------------------------------------------------------------------------- 1 | //! [`HashBuilder`] struct wrapping [`std::collections::hash_map::RandomState`] or [`ahash::RandomState`] depends on feature "ahash" 2 | #[cfg(feature = "ahash")] 3 | use ahash::RandomState; 4 | #[cfg(not(feature = "ahash"))] 5 | use std::collections::hash_map::RandomState; 6 | use std::hash::{BuildHasher, Hash, Hasher}; 7 | 8 | /// Alias for hash result type. 9 | pub type HashId = u64; 10 | 11 | /// Struct with sugar for easier hash calculation. 12 | #[derive(Default, Debug)] 13 | pub struct HashBuilder(RandomState); 14 | 15 | impl HashBuilder { 16 | /// Creates new [`HashBuilder`] 17 | pub fn new() -> Self { 18 | Self::default() 19 | } 20 | 21 | /// Calculate hash for value. 22 | /// # Example 23 | /// ```rust 24 | /// use fregate::sugar::hash_builder::HashBuilder; 25 | /// 26 | /// let hash_builder = HashBuilder::new(); 27 | /// 28 | /// let str0_hash = hash_builder.calculate_hash("str0"); 29 | /// 30 | /// let num0 = u64::MAX; 31 | /// let num0_hash = hash_builder.calculate_hash(num0); 32 | /// let num0_ref_hash = hash_builder.calculate_hash(&num0); 33 | /// assert_eq!(num0_hash, num0_ref_hash); 34 | /// ``` 35 | #[allow(clippy::manual_hash_one)] 36 | pub fn calculate_hash(&self, value: T) -> HashId { 37 | let mut s = self.0.build_hasher(); 38 | value.hash(&mut s); 39 | s.finish() 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/sugar/yaml_response.rs: -------------------------------------------------------------------------------- 1 | //! Syntax-sugar-like functions 2 | use axum::body::BoxBody; 3 | use axum::http; 4 | use axum::http::{HeaderValue, Response}; 5 | use axum::response::IntoResponse; 6 | 7 | /// Converts &str into Response and add Headers: Content-Type: "application/yaml" and "cache-control": "24 hours" 8 | pub fn yaml(content: &'static str) -> Response { 9 | ( 10 | [ 11 | ( 12 | http::header::CONTENT_TYPE, 13 | HeaderValue::from_static("application/yaml"), 14 | ), 15 | ( 16 | http::header::CACHE_CONTROL, 17 | HeaderValue::from_static("24 hours"), 18 | ), 19 | ], 20 | content, 21 | ) 22 | .into_response() 23 | } 24 | -------------------------------------------------------------------------------- /tests/app_config.rs: -------------------------------------------------------------------------------- 1 | mod app_config_tests { 2 | use config::FileFormat; 3 | use fregate::{AppConfig, ConfigSource, Empty}; 4 | use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; 5 | 6 | #[test] 7 | fn multiple_config() { 8 | let _config = AppConfig::::default(); 9 | let _config = AppConfig::::default(); 10 | } 11 | 12 | #[test] 13 | fn default() { 14 | let config = AppConfig::::default(); 15 | 16 | assert_eq!(config.port, 8000); 17 | assert_eq!(config.host, IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0))); 18 | assert_eq!(config.private, Empty {}); 19 | 20 | let observ = config.observability_cfg; 21 | let logger = observ.logger_config; 22 | let mngmnt = config.management_cfg; 23 | 24 | assert_eq!(observ.traces_endpoint, None); 25 | assert_eq!(observ.service_name, "default".to_owned()); 26 | assert_eq!(observ.version, "default".to_owned()); 27 | assert_eq!(observ.component_name, "default".to_owned()); 28 | assert_eq!(observ.trace_level, "info".to_owned()); 29 | assert!(!observ.cgroup_metrics); 30 | 31 | assert_eq!(logger.buffered_lines_limit, None); 32 | assert_eq!(logger.log_level, "info".to_owned()); 33 | assert_eq!(logger.msg_length, Some(8192)); 34 | assert_eq!(logger.logging_file, None); 35 | assert_eq!(logger.logging_path, None); 36 | assert_eq!(logger.logging_max_file_size, None); 37 | assert_eq!(logger.logging_interval, None); 38 | assert_eq!(logger.logging_max_history, None); 39 | assert_eq!(logger.logging_max_file_count, None); 40 | assert!(!logger.logging_enable_compression); 41 | 42 | assert_eq!(mngmnt.endpoints.health.as_ref(), "/health"); 43 | assert_eq!(mngmnt.endpoints.ready.as_ref(), "/ready"); 44 | assert_eq!(mngmnt.endpoints.live.as_ref(), "/live"); 45 | assert_eq!(mngmnt.endpoints.metrics.as_ref(), "/metrics"); 46 | assert_eq!(mngmnt.endpoints.version.as_ref(), "/version"); 47 | } 48 | 49 | #[test] 50 | #[should_panic] 51 | fn no_file_found() { 52 | let _config = AppConfig::::load_from([ConfigSource::File("")]) 53 | .expect("Failed to build AppConfig"); 54 | } 55 | 56 | #[test] 57 | fn empty_str_file() { 58 | let config = AppConfig::::load_from([ConfigSource::String("", FileFormat::Toml)]) 59 | .expect("Failed to build AppConfig"); 60 | 61 | assert_eq!(config.port, 8000); 62 | assert_eq!(config.host, IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0))); 63 | assert_eq!(config.private, Empty {}); 64 | 65 | let observ = config.observability_cfg; 66 | 67 | assert_eq!(observ.traces_endpoint, None); 68 | assert_eq!(observ.service_name, "default".to_owned()); 69 | assert_eq!(observ.version, "default".to_owned()); 70 | assert_eq!(observ.component_name, "default".to_owned()); 71 | assert_eq!(observ.trace_level, "info".to_owned()); 72 | assert_eq!(observ.logger_config.log_level, "info".to_owned()); 73 | } 74 | 75 | #[test] 76 | fn read_str_from_file() { 77 | let config = AppConfig::::load_from([ConfigSource::String( 78 | include_str!("resources/test_conf.toml"), 79 | FileFormat::Toml, 80 | )]) 81 | .expect("Failed to build AppConfig"); 82 | 83 | assert_eq!(config.port, 8888); 84 | assert_eq!( 85 | config.host, 86 | IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)) 87 | ); 88 | assert_eq!(config.private, Empty {}); 89 | 90 | let observ = config.observability_cfg; 91 | 92 | assert_eq!(observ.traces_endpoint, None); 93 | assert_eq!(observ.service_name, "Test".to_owned()); 94 | assert_eq!(observ.version, "default".to_owned()); 95 | assert_eq!(observ.component_name, "default".to_owned()); 96 | assert_eq!(observ.trace_level, "debug".to_owned()); 97 | assert_eq!(observ.logger_config.log_level, "trace".to_owned()); 98 | } 99 | 100 | #[test] 101 | fn read_file() { 102 | let config = 103 | AppConfig::::load_from([ConfigSource::File("./tests/resources/test_conf.toml")]) 104 | .expect("Failed to build AppConfig"); 105 | 106 | assert_eq!(config.port, 8888); 107 | assert_eq!( 108 | config.host, 109 | IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)) 110 | ); 111 | assert_eq!(config.private, Empty {}); 112 | 113 | let observ = config.observability_cfg; 114 | 115 | assert_eq!(observ.traces_endpoint, None); 116 | assert_eq!(observ.service_name, "Test".to_owned()); 117 | assert_eq!(observ.version, "default".to_owned()); 118 | assert_eq!(observ.component_name, "default".to_owned()); 119 | assert_eq!(observ.trace_level, "debug".to_owned()); 120 | assert_eq!(observ.logger_config.log_level, "trace".to_owned()); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /tests/app_config_from_env.rs: -------------------------------------------------------------------------------- 1 | mod app_config_from_env { 2 | use fregate::{bootstrap, AppConfig, ConfigSource, Empty}; 3 | use serde::Deserialize; 4 | use std::net::{IpAddr, Ipv6Addr}; 5 | use std::time::Duration; 6 | 7 | #[derive(Deserialize, Debug, PartialEq, Eq)] 8 | pub struct TestStruct { 9 | number: u32, 10 | } 11 | 12 | #[test] 13 | fn test_load_from() { 14 | std::env::set_var("TEST_HOST", "::1"); 15 | std::env::set_var("TEST_CGROUP_METRICS", "true"); 16 | std::env::set_var("TEST_PORT", "1234"); 17 | std::env::set_var("TEST_SERVICE_NAME", "TEST"); 18 | std::env::set_var("TEST_COMPONENT_NAME", "COMPONENT_TEST"); 19 | std::env::set_var("TEST_COMPONENT_VERSION", "1.0.0"); 20 | std::env::set_var("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", "http://0.0.0.0:4317"); 21 | std::env::set_var("TEST_TRACE_LEVEL", "debug"); 22 | std::env::set_var("TEST_LOG_LEVEL", "trace"); 23 | std::env::set_var("TEST_LOG_MSG_LENGTH", "0"); 24 | std::env::set_var("TEST_NUMBER", "100"); 25 | std::env::set_var("TEST_BUFFERED_LINES_LIMIT", "999"); 26 | std::env::set_var("TEST_LOGGING_FILE", "as213%^&*("); 27 | std::env::set_var("TEST_LOGGING_PATH", "./a/b/c"); 28 | std::env::set_var("TEST_LOGGING_INTERVAL", "100"); 29 | std::env::set_var("TEST_LOGGING_MAX_FILE_SIZE", "2"); 30 | std::env::set_var("TEST_LOGGING_MAX_HISTORY", "10"); 31 | std::env::set_var("TEST_LOGGING_MAX_FILE_COUNT", "1"); 32 | std::env::set_var("TEST_LOGGING_ENABLE_COMPRESSION", "true"); 33 | std::env::set_var("TEST_LOGGING_PATH", "./a/b/c"); 34 | 35 | let config = AppConfig::::load_from([ConfigSource::EnvPrefix("TEST")]) 36 | .expect("Failed to build AppConfig"); 37 | 38 | assert_eq!(config.port, 1234); 39 | assert_eq!( 40 | config.host, 41 | IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)) 42 | ); 43 | assert_eq!(config.private, TestStruct { number: 100 }); 44 | 45 | let observ = config.observability_cfg; 46 | let logger = observ.logger_config; 47 | 48 | assert_eq!( 49 | observ.traces_endpoint, 50 | Some("http://0.0.0.0:4317".to_owned()) 51 | ); 52 | assert_eq!(observ.service_name, "TEST".to_owned()); 53 | assert_eq!(observ.component_name, "COMPONENT_TEST".to_owned()); 54 | assert_eq!(observ.version, "1.0.0".to_owned()); 55 | assert_eq!(observ.service_name, "TEST".to_owned()); 56 | assert_eq!(observ.trace_level, "debug".to_owned()); 57 | assert_eq!(logger.log_level, "trace".to_owned()); 58 | assert_eq!(logger.logging_file, Some("as213%^&*(".to_owned())); 59 | assert_eq!(logger.logging_path, Some("./a/b/c".to_owned())); 60 | assert_eq!(logger.logging_interval, Some(Duration::from_secs(100))); 61 | assert_eq!(logger.logging_max_file_size, Some(2)); 62 | assert_eq!(logger.logging_max_history, Some(Duration::from_secs(10))); 63 | assert_eq!(logger.logging_max_file_count, Some(1)); 64 | assert!(logger.logging_enable_compression); 65 | assert_eq!(logger.msg_length, Some(0)); 66 | assert_eq!(logger.buffered_lines_limit, Some(999)); 67 | assert!(observ.cgroup_metrics); 68 | } 69 | 70 | #[test] 71 | fn negative_msg_length() { 72 | std::env::set_var("WRONG_NEGATIVE_LOG_MSG_LENGTH", "-123"); 73 | let config = 74 | AppConfig::::load_from([ConfigSource::EnvPrefix("WRONG_NEGATIVE")]).unwrap(); 75 | assert!(config.observability_cfg.logger_config.msg_length.is_none()); 76 | } 77 | 78 | #[test] 79 | fn wrong_msg_length() { 80 | std::env::set_var("WRONG_STR_LOG_MSG_LENGTH", "1a123"); 81 | let config = AppConfig::::load_from([ConfigSource::EnvPrefix("WRONG_STR")]).unwrap(); 82 | assert!(config.observability_cfg.logger_config.msg_length.is_none()); 83 | } 84 | 85 | #[tokio::test] 86 | async fn test_management_config_from_env() { 87 | std::env::set_var("MNGM_MANAGEMENT_ENDPOINTS_METRICS", "/probe/metrics"); 88 | std::env::set_var("MNGM_MANAGEMENT_ENDPOINTS_HEALTH", "///valid"); 89 | std::env::set_var("MNGM_MANAGEMENT_ENDPOINTS_LIVE", "invalid"); 90 | std::env::set_var("MNGM_MANAGEMENT_ENDPOINTS_READY", ""); 91 | 92 | let config: AppConfig = 93 | bootstrap([ConfigSource::EnvPrefix("MNGM")]).expect("Failed to build AppConfig"); 94 | 95 | let management_cfg = config.management_cfg; 96 | 97 | assert_eq!(management_cfg.endpoints.metrics.as_ref(), "/probe/metrics"); 98 | assert_eq!(management_cfg.endpoints.health.as_ref(), "///valid"); 99 | assert_eq!(management_cfg.endpoints.live.as_ref(), "/live"); 100 | assert_eq!(management_cfg.endpoints.ready.as_ref(), "/ready"); 101 | } 102 | 103 | #[test] 104 | fn test_server_port_priority() { 105 | std::env::set_var("PLACEHOLDER_0_PORT", "1234"); 106 | std::env::set_var("PLACEHOLDER_0_SERVER_PORT", "5678"); 107 | 108 | let config = AppConfig::::load_from([ConfigSource::EnvPrefix("PLACEHOLDER_0")]) 109 | .expect("Failed to build AppConfig"); 110 | 111 | assert_eq!(config.port, 5678); 112 | } 113 | 114 | #[test] 115 | fn test_server_port() { 116 | std::env::set_var("PLACEHOLDER_1_SERVER_PORT", "5678"); 117 | 118 | let config = AppConfig::::load_from([ConfigSource::EnvPrefix("PLACEHOLDER_1")]) 119 | .expect("Failed to build AppConfig"); 120 | 121 | assert_eq!(config.port, 5678); 122 | } 123 | 124 | #[test] 125 | fn test_port() { 126 | std::env::set_var("PLACEHOLDER_2_PORT", "5678"); 127 | 128 | let config = AppConfig::::load_from([ConfigSource::EnvPrefix("PLACEHOLDER_2")]) 129 | .expect("Failed to build AppConfig"); 130 | 131 | assert_eq!(config.port, 5678); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /tests/app_config_source_order.rs: -------------------------------------------------------------------------------- 1 | mod app_config_source_order { 2 | use fregate::{AppConfig, ConfigSource, Empty}; 3 | 4 | #[test] 5 | fn test_load_from() { 6 | std::env::set_var("TEST_PORT", "9999"); 7 | 8 | let config = 9 | AppConfig::::load_from([ConfigSource::File("./tests/resources/test_conf.toml")]) 10 | .expect("Failed to build AppConfig"); 11 | 12 | assert_eq!(config.port, 8888); 13 | 14 | let config = AppConfig::::load_from([ 15 | ConfigSource::File("./tests/resources/test_conf.toml"), 16 | ConfigSource::EnvPrefix("TEST"), 17 | ]) 18 | .expect("Failed to build AppConfig"); 19 | 20 | assert_eq!(config.port, 9999); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/app_config_tls.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "tls")] 2 | mod app_config_tls { 3 | use fregate::{AppConfig, Application, Empty}; 4 | use std::time::Duration; 5 | use tokio::time::timeout; 6 | 7 | const TLS_KEY_FULL_PATH: &str = concat!( 8 | env!("CARGO_MANIFEST_DIR"), 9 | "/examples/examples_resources/certs/tls.key" 10 | ); 11 | const TLS_CERTIFICATE_FULL_PATH: &str = concat!( 12 | env!("CARGO_MANIFEST_DIR"), 13 | "/examples/examples_resources/certs/tls.cert" 14 | ); 15 | 16 | #[tokio::test] 17 | async fn tls_paths() { 18 | let config = AppConfig::::default(); 19 | 20 | assert!(config.tls.key_path.is_none()); 21 | assert!(config.tls.cert_path.is_none()); 22 | assert!(Application::new(config).serve_tls().await.is_err()); 23 | 24 | std::env::set_var("TEST_SERVER_TLS_KEY_PATH", TLS_KEY_FULL_PATH); 25 | let config = AppConfig::::builder() 26 | .add_default() 27 | .add_env_prefixed("TEST") 28 | .build() 29 | .unwrap(); 30 | 31 | assert!(config.tls.key_path.is_some()); 32 | assert!(config.tls.cert_path.is_none()); 33 | assert!(Application::new(config).serve_tls().await.is_err()); 34 | 35 | std::env::set_var("TEST_SERVER_TLS_CERT_PATH", TLS_CERTIFICATE_FULL_PATH); 36 | let config = AppConfig::::builder() 37 | .add_default() 38 | .add_env_prefixed("TEST") 39 | .build() 40 | .unwrap(); 41 | 42 | assert!(config.tls.key_path.is_some()); 43 | assert!(config.tls.cert_path.is_some()); 44 | assert!( 45 | timeout(Duration::from_secs(2), Application::new(config).serve_tls(),) 46 | .await 47 | .is_err() 48 | ); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /tests/bootstrap.rs: -------------------------------------------------------------------------------- 1 | mod bootstrap_fn_test { 2 | use fregate::observability::LOG_LAYER_HANDLE; 3 | use fregate::{bootstrap, AppConfig, Empty}; 4 | use std::net::{IpAddr, Ipv4Addr}; 5 | 6 | #[tokio::test] 7 | async fn bootstrap_test() { 8 | let config: AppConfig = bootstrap([]).unwrap(); 9 | 10 | assert_eq!(config.port, 8000); 11 | assert_eq!(config.host, IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0))); 12 | assert_eq!(config.private, Empty {}); 13 | 14 | let observ = &config.observability_cfg; 15 | assert_eq!(observ.traces_endpoint, None); 16 | assert_eq!(observ.service_name, "default".to_owned()); 17 | assert_eq!(observ.version, "default".to_owned()); 18 | assert_eq!(observ.component_name, "default".to_owned()); 19 | assert_eq!(observ.trace_level, "info".to_owned()); 20 | assert_eq!(observ.logger_config.log_level, "info".to_owned()); 21 | 22 | assert!(LOG_LAYER_HANDLE.get().is_some()); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/bootstrap_multiple_calls.rs: -------------------------------------------------------------------------------- 1 | mod multiple_bootstrap_calls { 2 | use fregate::{bootstrap, AppConfig}; 3 | 4 | #[tokio::test] 5 | #[should_panic] 6 | async fn multiple_bootstrap_calls() { 7 | let _config: AppConfig = bootstrap([]).unwrap(); 8 | let _config: AppConfig = bootstrap([]).unwrap(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tests/default_headers_ext.rs: -------------------------------------------------------------------------------- 1 | pub mod default_headers_ext { 2 | use fregate::extensions::HeaderFilterExt; 3 | use fregate::{bootstrap, AppConfig}; 4 | use hyper::http::HeaderValue; 5 | use hyper::{Body, Request}; 6 | 7 | #[tokio::test] 8 | async fn default_headers_ext() { 9 | let _config: AppConfig = bootstrap([]).unwrap(); 10 | 11 | let request = Request::builder() 12 | .method("GET") 13 | .header("PassworD", "PasswordValue") 14 | .header("authorization", "authorization") 15 | .header("is_client", "true") 16 | .body(Body::empty()) 17 | .expect("Failed to build request"); 18 | let sanitized_headers = request.headers().get_filtered(); 19 | 20 | assert_eq!( 21 | sanitized_headers.get("PassworD"), 22 | Some(&HeaderValue::from_str("PasswordValue").unwrap()) 23 | ); 24 | assert_eq!( 25 | sanitized_headers.get("authorization"), 26 | Some(&HeaderValue::from_str("authorization").unwrap()) 27 | ); 28 | assert_eq!( 29 | sanitized_headers.get("is_client"), 30 | Some(&HeaderValue::from_str("true").unwrap()) 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/exclude_all.rs: -------------------------------------------------------------------------------- 1 | mod exclude_one_test { 2 | use fregate::extensions::HeaderFilterExt; 3 | use fregate::{bootstrap, AppConfig, ConfigSource}; 4 | use hyper::{Body, Request}; 5 | 6 | #[tokio::test] 7 | async fn exclude_all() { 8 | std::env::set_var("TEST_HEADERS_EXCLUDE", "*"); 9 | 10 | let _config: AppConfig = bootstrap([ConfigSource::EnvPrefix("TEST")]).unwrap(); 11 | 12 | let request = Request::builder() 13 | .method("GET") 14 | .header("PassworD", "PasswordValue") 15 | .header("authorization", "authorization") 16 | .header("is_client", "true") 17 | .body(Body::empty()) 18 | .expect("Failed to build request"); 19 | 20 | let sanitized_headers = request.headers().get_filtered(); 21 | 22 | assert_eq!(sanitized_headers.get("PassworD"), None); 23 | assert_eq!(sanitized_headers.get("authorization"), None); 24 | assert_eq!(sanitized_headers.get("is_client"), None); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/exclude_one.rs: -------------------------------------------------------------------------------- 1 | mod exclude_one_test { 2 | use fregate::extensions::HeaderFilterExt; 3 | use fregate::{bootstrap, AppConfig, ConfigSource}; 4 | use hyper::http::HeaderValue; 5 | use hyper::{Body, Request}; 6 | 7 | #[tokio::test] 8 | async fn exclude_one_test() { 9 | std::env::set_var("TEST_HEADERS_EXCLUDE", "password"); 10 | 11 | let _config: AppConfig = bootstrap([ConfigSource::EnvPrefix("TEST")]).unwrap(); 12 | 13 | let request = Request::builder() 14 | .method("GET") 15 | .header("PassworD", "PasswordValue") 16 | .header("authorization", "authorization") 17 | .header("is_client", "true") 18 | .body(Body::empty()) 19 | .expect("Failed to build request"); 20 | 21 | let sanitized_headers = request.headers().get_filtered(); 22 | 23 | assert_eq!(sanitized_headers.get("PassworD"), None); 24 | assert_eq!( 25 | sanitized_headers.get("authorization"), 26 | Some(&HeaderValue::from_str("authorization").unwrap()) 27 | ); 28 | assert_eq!( 29 | sanitized_headers.get("is_client"), 30 | Some(&HeaderValue::from_str("true").unwrap()) 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/headers_ext.rs: -------------------------------------------------------------------------------- 1 | mod headers_ext_test { 2 | use fregate::extensions::HeaderFilterExt; 3 | use fregate::observability::SANITIZED_VALUE; 4 | use fregate::{bootstrap, AppConfig, ConfigSource}; 5 | use hyper::http::HeaderValue; 6 | use hyper::{Body, Request}; 7 | 8 | #[tokio::test] 9 | async fn headers_ext_test() { 10 | std::env::set_var("TEST_HEADERS_INCLUDE", "authorization,password"); 11 | std::env::set_var("TEST_HEADERS_SANITIZE", "password,authorization"); 12 | std::env::set_var("TEST_HEADERS_EXCLUDE", "password,"); 13 | 14 | let _config: AppConfig = bootstrap([ConfigSource::EnvPrefix("TEST")]).unwrap(); 15 | 16 | let request = Request::builder() 17 | .method("GET") 18 | .header("PassworD", "PasswordValue") 19 | .header("authorization", "authorization") 20 | .header("is_client", "true") 21 | .body(Body::empty()) 22 | .expect("Failed to build request"); 23 | 24 | let sanitized_headers = request.headers().get_filtered(); 25 | 26 | assert_eq!(sanitized_headers.get("PassworD"), None, "Must be Excluded"); 27 | assert_eq!( 28 | sanitized_headers.get("authorization"), 29 | Some(&HeaderValue::from_static(SANITIZED_VALUE)), 30 | "Included and sanitized" 31 | ); 32 | assert_eq!(sanitized_headers.get("is_client"), None, "Not included"); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/include_all.rs: -------------------------------------------------------------------------------- 1 | mod include_all_test { 2 | use fregate::extensions::HeaderFilterExt; 3 | use fregate::{bootstrap, AppConfig, ConfigSource}; 4 | use hyper::http::HeaderValue; 5 | use hyper::{Body, Request}; 6 | 7 | #[tokio::test] 8 | async fn include_all() { 9 | std::env::set_var("TEST_HEADERS_INCLUDE", "*"); 10 | 11 | let _config: AppConfig = bootstrap([ConfigSource::EnvPrefix("TEST")]).unwrap(); 12 | 13 | let request = Request::builder() 14 | .method("GET") 15 | .header("PassworD", "PasswordValue") 16 | .header("authorization", "authorization") 17 | .header("is_client", "true") 18 | .body(Body::empty()) 19 | .expect("Failed to build request"); 20 | 21 | let sanitized_headers = request.headers().get_filtered(); 22 | 23 | assert_eq!( 24 | sanitized_headers.get("PassworD"), 25 | Some(&HeaderValue::from_str("PasswordValue").unwrap()) 26 | ); 27 | assert_eq!( 28 | sanitized_headers.get("authorization"), 29 | Some(&HeaderValue::from_str("authorization").unwrap()) 30 | ); 31 | assert_eq!( 32 | sanitized_headers.get("is_client"), 33 | Some(&HeaderValue::from_str("true").unwrap()) 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tests/include_one.rs: -------------------------------------------------------------------------------- 1 | mod include_one_test { 2 | use fregate::extensions::HeaderFilterExt; 3 | use fregate::{bootstrap, AppConfig, ConfigSource}; 4 | use hyper::http::HeaderValue; 5 | use hyper::{Body, Request}; 6 | 7 | #[tokio::test] 8 | async fn include_one() { 9 | std::env::set_var("TEST_HEADERS_INCLUDE", "password"); 10 | 11 | let _config: AppConfig = bootstrap([ConfigSource::EnvPrefix("TEST")]).unwrap(); 12 | 13 | let request = Request::builder() 14 | .method("GET") 15 | .header("PassworD", "PasswordValue") 16 | .header("authorization", "authorization") 17 | .header("is_client", "true") 18 | .body(Body::empty()) 19 | .expect("Failed to build request"); 20 | 21 | let sanitized_headers = request.headers().get_filtered(); 22 | 23 | assert_eq!( 24 | sanitized_headers.get("PassworD"), 25 | Some(&HeaderValue::from_str("PasswordValue").unwrap()) 26 | ); 27 | assert_eq!(sanitized_headers.get("authorization"), None); 28 | assert_eq!(sanitized_headers.get("is_client"), None); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/resources/test_conf.toml: -------------------------------------------------------------------------------- 1 | host = "::1" 2 | port = 8888 3 | 4 | [log] 5 | level = "trace" 6 | 7 | [trace] 8 | level = "debug" 9 | 10 | [service] 11 | name = "Test" 12 | 13 | [private] 14 | -------------------------------------------------------------------------------- /tests/sanitize_all.rs: -------------------------------------------------------------------------------- 1 | mod exclude_one_test { 2 | use fregate::extensions::HeaderFilterExt; 3 | use fregate::observability::SANITIZED_VALUE; 4 | use fregate::{bootstrap, AppConfig, ConfigSource}; 5 | use hyper::http::HeaderValue; 6 | use hyper::{Body, Request}; 7 | 8 | #[tokio::test] 9 | async fn exclude_all() { 10 | std::env::set_var("TEST_HEADERS_SANITIZE", "*"); 11 | 12 | let _config: AppConfig = bootstrap([ConfigSource::EnvPrefix("TEST")]).unwrap(); 13 | 14 | let request = Request::builder() 15 | .method("GET") 16 | .header("PassworD", "PasswordValue") 17 | .header("authorization", "authorization") 18 | .header("is_client", "true") 19 | .body(Body::empty()) 20 | .expect("Failed to build request"); 21 | 22 | let sanitized_headers = request.headers().get_filtered(); 23 | 24 | assert_eq!( 25 | sanitized_headers.get("PassworD"), 26 | Some(&HeaderValue::from_static(SANITIZED_VALUE)) 27 | ); 28 | assert_eq!( 29 | sanitized_headers.get("authorization"), 30 | Some(&HeaderValue::from_static(SANITIZED_VALUE)) 31 | ); 32 | assert_eq!( 33 | sanitized_headers.get("is_client"), 34 | Some(&HeaderValue::from_static(SANITIZED_VALUE)) 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/sanitize_one.rs: -------------------------------------------------------------------------------- 1 | mod exclude_one_test { 2 | use fregate::extensions::HeaderFilterExt; 3 | use fregate::observability::SANITIZED_VALUE; 4 | use fregate::{bootstrap, AppConfig, ConfigSource}; 5 | use hyper::http::HeaderValue; 6 | use hyper::{Body, Request}; 7 | 8 | #[tokio::test] 9 | async fn exclude_all() { 10 | std::env::set_var("TEST_HEADERS_SANITIZE", "password"); 11 | 12 | let _config: AppConfig = bootstrap([ConfigSource::EnvPrefix("TEST")]).unwrap(); 13 | 14 | let request = Request::builder() 15 | .method("GET") 16 | .header("PassworD", "PasswordValue") 17 | .header("authorization", "authorization") 18 | .header("is_client", "true") 19 | .body(Body::empty()) 20 | .expect("Failed to build request"); 21 | 22 | let sanitized_headers = request.headers().get_filtered(); 23 | 24 | assert_eq!( 25 | sanitized_headers.get("PassworD"), 26 | Some(&HeaderValue::from_static(SANITIZED_VALUE)) 27 | ); 28 | assert_eq!( 29 | sanitized_headers.get("authorization"), 30 | Some(&HeaderValue::from_str("authorization").unwrap()) 31 | ); 32 | assert_eq!( 33 | sanitized_headers.get("is_client"), 34 | Some(&HeaderValue::from_str("true").unwrap()) 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/sugar_grpc_codes.rs: -------------------------------------------------------------------------------- 1 | use fregate::sugar::grpc_codes::{grpc_code_to_num, grpc_code_to_str, GRPC_CODES}; 2 | 3 | #[test] 4 | fn test_grpc_code_to_num() { 5 | for code in GRPC_CODES { 6 | assert_eq!(grpc_code_to_num(code), &format!("{}", code as i32)); 7 | } 8 | } 9 | 10 | #[test] 11 | fn test_grpc_code_to_str() { 12 | for code in GRPC_CODES { 13 | assert_eq!(grpc_code_to_str(code), &format!("{code:?}")); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tests/tls.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "tls")] 2 | mod tls { 3 | use fregate::{error::Error, AppConfig, Application, Empty}; 4 | use hyper::{client::HttpConnector, Client, StatusCode, Uri}; 5 | use hyper_rustls::{ConfigBuilderExt, HttpsConnector, HttpsConnectorBuilder}; 6 | use rustls::{ 7 | client::{ServerCertVerified, ServerCertVerifier}, 8 | Certificate, ClientConfig, ServerName, 9 | }; 10 | use std::{ 11 | io::ErrorKind, 12 | str::FromStr, 13 | sync::Arc, 14 | time::{Duration, SystemTime}, 15 | }; 16 | use tokio::time::timeout; 17 | 18 | const ROOTLES_PORT: u16 = 1025; 19 | const MAX_PORT: u16 = u16::MAX; 20 | 21 | const TLS_KEY_FULL_PATH: &str = concat!( 22 | env!("CARGO_MANIFEST_DIR"), 23 | "/examples/examples_resources/certs/tls.key" 24 | ); 25 | const TLS_CERTIFICATE_FULL_PATH: &str = concat!( 26 | env!("CARGO_MANIFEST_DIR"), 27 | "/examples/examples_resources/certs/tls.cert" 28 | ); 29 | 30 | async fn start_server() -> (u16, Duration) { 31 | std::env::set_var("TEST_SERVER_TLS_KEY_PATH", TLS_KEY_FULL_PATH); 32 | std::env::set_var("TEST_SERVER_TLS_CERT_PATH", TLS_CERTIFICATE_FULL_PATH); 33 | 34 | let mut config = AppConfig::::builder() 35 | .add_env_prefixed("TEST") 36 | .add_default() 37 | .build() 38 | .unwrap(); 39 | 40 | let mut free_port = None; 41 | let tls_timeout = config.tls.handshake_timeout; 42 | 43 | for port in ROOTLES_PORT..MAX_PORT { 44 | config.port = port; 45 | 46 | let next_config = config.clone(); 47 | let application_handle = timeout( 48 | Duration::from_secs(1), 49 | tokio::task::spawn(async move { Application::new(next_config).serve_tls().await }), 50 | ) 51 | .await; 52 | 53 | match application_handle { 54 | Err(_elapsed) => { 55 | free_port = Some(port); 56 | break; 57 | } 58 | Ok(Err(err)) => { 59 | panic!("Unexpected error: `{err}`."); 60 | } 61 | Ok(Ok(Err(Error::IoError(err)))) => { 62 | if err.kind() == ErrorKind::AddrInUse { 63 | continue; 64 | } else { 65 | panic!("Unexpected error: `{err}`."); 66 | } 67 | } 68 | Ok(Ok(Err(err))) => { 69 | panic!("Unexpected error: `{err}`."); 70 | } 71 | Ok(Ok(Ok(()))) => unreachable!("impossible"), 72 | } 73 | } 74 | 75 | tokio::time::sleep(Duration::from_millis(200)).await; 76 | 77 | (free_port.expect("No free ports are available"), tls_timeout) 78 | } 79 | 80 | fn build_client() -> Client> { 81 | struct DummyServerCertVerifier; 82 | impl ServerCertVerifier for DummyServerCertVerifier { 83 | fn verify_server_cert( 84 | &self, 85 | _: &Certificate, 86 | _: &[Certificate], 87 | _: &ServerName, 88 | _: &mut dyn Iterator, 89 | _: &[u8], 90 | _: SystemTime, 91 | ) -> Result { 92 | Ok(ServerCertVerified::assertion()) 93 | } 94 | } 95 | 96 | let mut tls = ClientConfig::builder() 97 | .with_safe_defaults() 98 | .with_native_roots() 99 | .with_no_client_auth(); 100 | tls.dangerous() 101 | .set_certificate_verifier(Arc::new(DummyServerCertVerifier)); 102 | 103 | let https = HttpsConnectorBuilder::new() 104 | .with_tls_config(tls) 105 | .https_only() 106 | .enable_http1() 107 | .build(); 108 | 109 | Client::builder().http2_only(true).build(https) 110 | } 111 | 112 | #[ignore] 113 | #[tokio::test] 114 | async fn test_https_request() { 115 | let (port, _) = start_server().await; 116 | 117 | let hyper = build_client(); 118 | 119 | let timeout = Duration::from_secs(2); 120 | let fut = hyper.get(Uri::from_str(&format!("https://localhost:{port}/health")).unwrap()); 121 | let response = tokio::time::timeout(timeout, fut).await.unwrap().unwrap(); 122 | 123 | let status = response.status(); 124 | let body = hyper::body::to_bytes(response.into_body()).await.unwrap(); 125 | 126 | assert_eq!(StatusCode::OK, status); 127 | assert_eq!(body.as_ref(), b"OK"); 128 | } 129 | 130 | #[ignore] 131 | #[tokio::test] 132 | async fn test_http_request() { 133 | let (port, tls_timeout) = start_server().await; 134 | 135 | let hyper = build_client(); 136 | 137 | let timeout = tls_timeout + Duration::from_secs(2); 138 | let fut = hyper.get(Uri::from_str(&format!("http://localhost:{port}/health")).unwrap()); 139 | let response = tokio::time::timeout(timeout, fut).await.unwrap(); 140 | 141 | assert!(response.is_err()); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /tests/traing_fields.rs: -------------------------------------------------------------------------------- 1 | mod tracing_fields { 2 | use fregate::observability::TracingFields; 3 | use std::net::{IpAddr, Ipv4Addr, SocketAddr}; 4 | 5 | fn is_send(_val: impl Send) {} 6 | 7 | #[test] 8 | fn tracing_fields_is_send() { 9 | let mut val = TracingFields::new(); 10 | 11 | let socket = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8080); 12 | 13 | val.insert_ref("str", &"str"); 14 | val.insert_as_string("display_address", &socket); 15 | val.insert_as_debug("debug_address", &socket); 16 | 17 | is_send(val); 18 | } 19 | 20 | #[test] 21 | fn owning_data() { 22 | let mut val = TracingFields::new(); 23 | 24 | let socket = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8080).to_string(); 25 | 26 | val.insert_ref("str", &"str"); 27 | val.insert_as_string("1", &socket); 28 | val.insert_as_debug("2", &socket); 29 | val.insert("3", socket.to_string()); 30 | 31 | drop(socket); 32 | is_send(val); 33 | } 34 | } 35 | --------------------------------------------------------------------------------