From 57746adede1dbddd28c5d00f44e10eb8645125e7 Mon Sep 17 00:00:00 2001 From: Pavel Kirilin <win10@list.ru> Date: Sat, 2 Apr 2022 19:35:42 +0400 Subject: [PATCH] Added prometheus metrics. (#70) Signed-off-by: Pavel Kirilin <win10@list.ru> --- .github/workflows/release.yml | 2 +- Cargo.lock | 35 +++++++++++++++++++++++++++++++ Cargo.toml | 3 +++ README.md | 5 +++-- deploy/Dockerfile | 2 +- docs/deploy.md | 5 +++++ docs/index.md | 5 +++-- src/errors.rs | 2 ++ src/main.rs | 36 +++++++++++++++++++++++++++----- src/protocol/core/write_bytes.rs | 20 ++++++++---------- src/protocol/creation/routes.rs | 12 +++++++++++ src/routes.rs | 6 ++---- src/state.rs | 6 +++--- 13 files changed, 110 insertions(+), 29 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7a07c2e..1a65f14 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -60,7 +60,7 @@ jobs: command: build use-cross: ${{ matrix.job.use-cross }} toolchain: ${{ matrix.rust }} - args: --release --features=all --target ${{ matrix.job.target }} + args: --release --features=all,metrics --target ${{ matrix.job.target }} - name: install strip command shell: bash diff --git a/Cargo.lock b/Cargo.lock index 3512ee2..8e6aadc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -205,6 +205,18 @@ dependencies = [ "syn", ] +[[package]] +name = "actix-web-prom" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9df3127d20a5d01c9fc9aceb969a38d31a6767e1b48a54d55a8f56c769a84923" +dependencies = [ + "actix-web", + "futures-core", + "pin-project-lite", + "prometheus", +] + [[package]] name = "adler" version = "1.0.2" @@ -1992,6 +2004,27 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "prometheus" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f64969ffd5dd8f39bd57a68ac53c163a095ed9d0fb707146da1b27025a3504" +dependencies = [ + "cfg-if", + "fnv", + "lazy_static", + "memchr", + "parking_lot 0.11.2", + "protobuf", + "thiserror", +] + +[[package]] +name = "protobuf" +version = "2.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf7e6d18738ecd0902d30d1ad232c9125985a3422929b16c65517b38adc14f96" + [[package]] name = "py_sql" version = "1.0.1" @@ -2389,6 +2422,7 @@ dependencies = [ "actix-files", "actix-rt", "actix-web", + "actix-web-prom", "async-trait", "base64", "bytes", @@ -2405,6 +2439,7 @@ dependencies = [ "mobc-lapin", "mobc-redis", "openssl", + "prometheus", "rbatis", "rbson", "reqwest", diff --git a/Cargo.toml b/Cargo.toml index 9150071..56dabcb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,6 +30,8 @@ strfmt = "^0.1.6" thiserror = "^1.0" url = "^2.2.2" bytes = "^1.1.0" +prometheus = "^0.13.0" +actix-web-prom = "^0.6.0" [dependencies.digest] version = "0.10.3" @@ -133,6 +135,7 @@ default = [] http_notifier = ["reqwest"] redis_info_storage = ["mobc-redis"] hashers = ["md-5", "sha1", "sha2", "digest"] +metrics = [] ### For testing test_redis = [] diff --git a/README.md b/README.md index 9483793..a9e9c48 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ Preferred version is 1.59.0. ```bash git clone https://github.com/s3rius/rustus.git cd rustus -cargo install --path . --features=all +cargo install --path . --features=all,metrics ``` Also you can speedup build by disabling some features. @@ -44,7 +44,8 @@ Available features: * `http_notifier` - adds support for notifying about upload status via http protocol; * `redis_info_storage` - adds support for storing information about upload in redis database; * `hashers` - adds support for checksum verification; -* `all` - enables all rustus features. +* `metrics` - adds rustus specific metrics to prometheus endpoint; +* `all` - enables all rustus features except `metrics`. All precompiled binaries have all features enabled. diff --git a/deploy/Dockerfile b/deploy/Dockerfile index 4fca83a..acd2de4 100644 --- a/deploy/Dockerfile +++ b/deploy/Dockerfile @@ -8,7 +8,7 @@ RUN cargo chef prepare --recipe-path recipe.json FROM chef AS builder COPY --from=planner /app/recipe.json recipe.json # Build dependencies - this is the caching Docker layer! -RUN cargo chef cook --release --features=all --recipe-path recipe.json +RUN cargo chef cook --release --features=all,metrics --recipe-path recipe.json # Build application COPY . . RUN cargo build --release --bin rustus --features=all diff --git a/docs/deploy.md b/docs/deploy.md index 2583bf2..b7f2432 100644 --- a/docs/deploy.md +++ b/docs/deploy.md @@ -9,6 +9,11 @@ Deploying an application is always a challenge. Rustus was made to make deployme Since Rustus works with files you have to be careful while scaling it. All rustus instances must have access to the same data and info storages. +!!! info + + If you want to track you rustus instances with **prometheus** you can + always get metrics at `/metrics` endpoint. + ## Docker compose ``` yaml title="docker-compose.yml" diff --git a/docs/index.md b/docs/index.md index ad36757..af486d4 100644 --- a/docs/index.md +++ b/docs/index.md @@ -30,7 +30,7 @@ Preferred version is 1.59.0. ```bash git clone https://github.com/s3rius/rustus.git cd rustus -cargo install --path . --features=all +cargo install --path . --features=all,metrics ``` Also, you can speedup build by disabling some features. @@ -42,7 +42,8 @@ Available features: * `http_notifier` - adds support for notifying about upload status via `HTTP` protocol; * `redis_info_storage` - adds support for storing information about upload in `Redis` database; * `hashers` - adds support for checksum verification; -* `all` - enables all Rustus features. +* `metrics` - adds rustus specific metrics to prometheus endpoint; +* `all` - enables all rustus features except `metrics`. All precompiled binaries have all features enabled. diff --git a/src/errors.rs b/src/errors.rs index 13ad2ae..0f17602 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -65,6 +65,8 @@ pub enum RustusError { WrongChecksum, #[error("The header value is incorrect")] WrongHeaderValue, + #[error("Metrics error: {0}")] + PrometheusError(#[from] prometheus::Error), } /// This conversion allows us to use `RustusError` in the `main` function. diff --git a/src/main.rs b/src/main.rs index 6fc08e6..26ab5eb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,14 +11,17 @@ use fern::{ colors::{Color, ColoredLevelConfig}, Dispatch, }; -use log::LevelFilter; +use log::{error, LevelFilter}; use config::RustusConf; use crate::{ - errors::RustusResult, info_storages::InfoStorage, - notifiers::models::notification_manager::NotificationManager, server::rustus_service, - state::State, storages::Storage, + errors::{RustusError, RustusResult}, + info_storages::InfoStorage, + notifiers::models::notification_manager::NotificationManager, + server::rustus_service, + state::State, + storages::Storage, }; mod config; @@ -72,14 +75,37 @@ fn greeting(app_conf: &RustusConf) { /// if the server can't be bound to the /// given address. #[cfg_attr(coverage, no_coverage)] -pub fn create_server(state: State) -> Result<Server, std::io::Error> { +pub fn create_server(state: State) -> RustusResult<Server> { let host = state.config.host.clone(); let port = state.config.port; let workers = state.config.workers; let state_data: web::Data<State> = web::Data::from(Arc::new(state)); + let metrics = actix_web_prom::PrometheusMetricsBuilder::new("") + .endpoint("/metrics") + .build() + .map_err(|err| { + error!("{}", err); + RustusError::Unknown + })?; + let active_uploads = + prometheus::IntGauge::new("active_uploads", "Number of active file uploads")?; + let file_sizes = prometheus::Histogram::with_opts( + prometheus::HistogramOpts::new("uploads_sizes", "Size of uploaded files in bytes") + .buckets(prometheus::exponential_buckets(2., 2., 40)?), + )?; + #[cfg(feature = "metrics")] + { + metrics + .registry + .register(Box::new(active_uploads.clone()))?; + metrics.registry.register(Box::new(file_sizes.clone()))?; + } let mut server = HttpServer::new(move || { App::new() + .app_data(web::Data::new(active_uploads.clone())) + .app_data(web::Data::new(file_sizes.clone())) .configure(rustus_service(state_data.clone())) + .wrap(metrics.clone()) .wrap(middleware::Logger::new("\"%r\" \"-\" \"%s\" \"%a\" \"%D\"")) // Middleware that overrides method of a request if // "X-HTTP-Method-Override" header is provided. diff --git a/src/protocol/core/write_bytes.rs b/src/protocol/core/write_bytes.rs index fafc64e..c63e72d 100644 --- a/src/protocol/core/write_bytes.rs +++ b/src/protocol/core/write_bytes.rs @@ -14,6 +14,7 @@ pub async fn write_bytes( request: HttpRequest, bytes: Bytes, state: web::Data<State>, + #[cfg(feature = "metrics")] active_uploads: web::Data<prometheus::IntGauge>, ) -> RustusResult<HttpResponse> { // Checking if request has required headers. let check_content_type = |val: &str| val == "application/offset+octet-stream"; @@ -109,10 +110,8 @@ pub async fn write_bytes( state.info_storage.set_info(&file_info, false).await?; let mut hook = Hook::PostReceive; - let mut keep_alive = true; if file_info.length == Some(file_info.offset) { hook = Hook::PostFinish; - keep_alive = false; } if state.config.hook_is_active(hook) { let message = state @@ -128,16 +127,15 @@ pub async fn write_bytes( .await }); } - if keep_alive { - Ok(HttpResponse::NoContent() - .insert_header(("Upload-Offset", file_info.offset.to_string())) - .keep_alive() - .finish()) - } else { - Ok(HttpResponse::NoContent() - .insert_header(("Upload-Offset", file_info.offset.to_string())) - .finish()) + + #[cfg(feature = "metrics")] + if hook == Hook::PostFinish { + active_uploads.dec(); } + + Ok(HttpResponse::NoContent() + .insert_header(("Upload-Offset", file_info.offset.to_string())) + .finish()) } #[cfg(test)] diff --git a/src/protocol/creation/routes.rs b/src/protocol/creation/routes.rs index 86c8b34..52fc2c7 100644 --- a/src/protocol/creation/routes.rs +++ b/src/protocol/creation/routes.rs @@ -71,6 +71,8 @@ fn get_upload_parts(request: &HttpRequest) -> Vec<String> { /// extension is enabled. #[allow(clippy::too_many_lines)] pub async fn create_file( + #[cfg(feature = "metrics")] active_uploads: web::Data<prometheus::IntGauge>, + #[cfg(feature = "metrics")] file_sizes: web::Data<prometheus::Histogram>, state: web::Data<State>, request: HttpRequest, bytes: Bytes, @@ -140,6 +142,16 @@ pub async fn create_file( // Create file and get the it's path. file_info.path = Some(state.data_storage.create_file(&file_info).await?); + // Incrementing number of active uploads + #[cfg(feature = "metrics")] + active_uploads.inc(); + + #[cfg(feature = "metrics")] + if let Some(length) = file_info.length { + #[allow(clippy::cast_precision_loss)] + file_sizes.observe(length as f64); + } + if file_info.is_final { let mut final_size = 0; let mut parts_info = Vec::new(); diff --git a/src/routes.rs b/src/routes.rs index 6a07692..fa54631 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -1,14 +1,12 @@ use actix_web::HttpResponse; -use crate::errors::{RustusError, RustusResult}; - /// Default response to all unknown URLs. /// All protocol urls can be found /// at `crate::protocol::*`. #[allow(clippy::unused_async)] #[cfg_attr(coverage, no_coverage)] -pub async fn not_found() -> RustusResult<HttpResponse> { - Err(RustusError::FileNotFound) +pub async fn not_found() -> HttpResponse { + HttpResponse::NotFound().finish() } /// Checks that application is accepting connections correctly. diff --git a/src/state.rs b/src/state.rs index 45fa622..f74a2d6 100644 --- a/src/state.rs +++ b/src/state.rs @@ -25,7 +25,7 @@ impl State { } #[cfg(test)] - pub async fn from_config(config: RustusConf) -> Self { + pub async fn from_config_test(config: RustusConf) -> Self { Self { config: config.clone(), data_storage: Box::new(crate::storages::file_storage::FileStorage::new( @@ -56,13 +56,13 @@ impl State { ] .into_iter(), ); - Self::from_config(config).await + Self::from_config_test(config).await } #[cfg(test)] pub async fn test_clone(&self) -> Self { let config = self.config.clone(); - Self::from_config(config).await + Self::from_config_test(config).await } #[cfg(test)] -- GitLab