diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 430d33b..afd6afb 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -86,4 +86,14 @@ jobs: toolchain: stable - name: Install Protoc uses: arduino/setup-protoc@v1 - - run: cargo build --features static-files \ No newline at end of file + - run: cargo build --features static-files + build-with-metrics: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + toolchain: stable + - name: Install Protoc + uses: arduino/setup-protoc@v1 + - run: cargo build --features metrics \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index a57e757..57076e0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,5 +8,5 @@ members = [ "my-http-server-signal-r-middleware", "static-files-middleware", "signal-r-macros", - "my-http-server-test", + "my-http-server-test", "my-http-metrics", ] diff --git a/my-http-metrics/Cargo.toml b/my-http-metrics/Cargo.toml new file mode 100644 index 0000000..1dc1e9d --- /dev/null +++ b/my-http-metrics/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "my-http-metrics" +version = "0.1.0" +edition = "2021" + +[dependencies] +my-http-server-core = { path = "../my-http-server-core" } +metrics = "*" +tokio = { version = "*", features = ["full"] } +async-trait = "*" +stopwatch = "*" +prometheus = "*" \ No newline at end of file diff --git a/my-http-metrics/src/lib.rs b/my-http-metrics/src/lib.rs new file mode 100644 index 0000000..4b91e73 --- /dev/null +++ b/my-http-metrics/src/lib.rs @@ -0,0 +1,3 @@ +mod middleware; + +pub use middleware::*; \ No newline at end of file diff --git a/my-http-metrics/src/middleware/metrics_middleware.rs b/my-http-metrics/src/middleware/metrics_middleware.rs new file mode 100644 index 0000000..69229a7 --- /dev/null +++ b/my-http-metrics/src/middleware/metrics_middleware.rs @@ -0,0 +1,79 @@ +use async_trait::async_trait; +use my_http_server_core::{ + HttpContext, HttpFailResult, HttpOkResult, HttpOutput, HttpServerMiddleware, + HttpServerRequestFlow, +}; +use stopwatch::Stopwatch; + +pub struct MetricsMiddleware; + +impl MetricsMiddleware { + pub fn new() -> Self { + Self {} + } +} + +#[async_trait] +impl HttpServerMiddleware for MetricsMiddleware { + async fn handle_request( + &self, + ctx: &mut HttpContext, + get_next: &mut HttpServerRequestFlow, + ) -> Result { + let path = ctx.request.http_path.as_str().to_string(); + + if path == "/metrics" { + let report = prometheus::TextEncoder::new() + .encode_to_string(&prometheus::default_registry().gather()); + + match report { + Ok(report) => { + let response = HttpOutput::as_text(report).into_ok_result(false); + return response; + } + Err(err) => { + let response = + HttpOutput::as_text(err.to_string()).into_fail_result(502, false); + return response; + } + } + } + + let mut sw = Stopwatch::start_new(); + let result = get_next.next(ctx).await; + sw.stop(); + + let duration = sw.elapsed(); + let method = ctx.request.method.as_str().to_string(); + let path = ctx.request.http_path.as_str().to_string(); + let common_labels = &[("method", method.clone()), ("path", path.clone())]; + + let mut has_to_write_metrics = false; + + if let Err(result) = &result { + if result.status_code == 404 { + has_to_write_metrics = true + } else { + let failed_labels = &[ + ("method", method.clone()), + ("path", path.clone()), + ("status_code", result.status_code.to_string()), + ]; + + metrics::counter!("failed_request_count", failed_labels).increment(1); + metrics::counter!("failed_request_duration_sum", failed_labels).increment(duration.as_millis() as u64); + metrics::histogram!("failed_request_duration_nanos", failed_labels).record(duration.as_millis() as f64); + } + } + + if has_to_write_metrics { + return result; + } + + metrics::histogram!("request_duration_nanos", common_labels).record(duration.as_millis() as f64); + metrics::counter!("request_duration_sum", common_labels).increment(duration.as_millis() as u64); + metrics::counter!("request_count", common_labels).increment(1); + + return result; + } +} diff --git a/my-http-metrics/src/middleware/mod.rs b/my-http-metrics/src/middleware/mod.rs new file mode 100644 index 0000000..abeafcc --- /dev/null +++ b/my-http-metrics/src/middleware/mod.rs @@ -0,0 +1,3 @@ +mod metrics_middleware; + +pub use metrics_middleware::*; \ No newline at end of file diff --git a/my-http-server-core/src/http_fail_result.rs b/my-http-server-core/src/http_fail_result.rs index 6365d9a..f379635 100644 --- a/my-http-server-core/src/http_fail_result.rs +++ b/my-http-server-core/src/http_fail_result.rs @@ -66,7 +66,7 @@ impl HttpFailResult { pub fn as_not_found(text: String, write_telemetry: bool) -> Self { Self::new( WebContentType::Text, - 400, + 404, text.into_bytes(), write_telemetry, false, diff --git a/my-http-server/Cargo.toml b/my-http-server/Cargo.toml index a908926..2758a36 100644 --- a/my-http-server/Cargo.toml +++ b/my-http-server/Cargo.toml @@ -14,6 +14,7 @@ full = [ "signal-r", "with-telemetry", "static-files", + "metrics", ] controllers = ["my-http-server-controllers"] macros = ["my-http-server-macros", "my-http-server-controllers"] @@ -25,6 +26,7 @@ with-telemetry = [ ] static-files = ["static-files-middleware"] +metrics = ["my-http-metrics"] [dependencies] @@ -35,3 +37,4 @@ my-http-server-controllers = { optional = true, path = "../my-http-server-contro my-http-server-core = { path = "../my-http-server-core" } signal-r-macros = { optional = true, path = "../signal-r-macros" } static-files-middleware = { path = "../static-files-middleware", optional = true } +my-http-metrics = { path = "../my-http-metrics", optional = true } diff --git a/my-http-server/src/lib.rs b/my-http-server/src/lib.rs index f46af65..4d94ea4 100644 --- a/my-http-server/src/lib.rs +++ b/my-http-server/src/lib.rs @@ -18,3 +18,6 @@ pub mod signal_r { pub use static_files_middleware::*; pub use my_http_server_core::*; + +#[cfg(feature = "metrics")] +pub use my_http_metrics as metrics; \ No newline at end of file