From b42214b95be266a135d78ce2e4d7481925e75f6a Mon Sep 17 00:00:00 2001 From: Gaƫtan Lehmann Date: Tue, 11 Jul 2023 14:20:58 +0200 Subject: [PATCH] feat: optional `/health` endpoint (#238) * feat: add an optional and quiet /health * HEAD support for /health * health endpoint doc * Update docs/content/features/health-endpoint.md --------- Co-authored-by: Jose Quintana <1700322+joseluisq@users.noreply.github.com> --- docs/content/configuration/environment-variables.md | 3 +++ docs/content/features/health-endpoint.md | 35 +++++++++++++++++++++++++++++++++++ docs/mkdocs.yml | 1 + src/handler.rs | 41 ++++++++++++++++++++++++++++++++++------- src/server.rs | 5 +++++ src/settings/cli.rs | 13 +++++++++++++ src/settings/file.rs | 3 +++ src/settings/mod.rs | 5 +++++ 8 files changed, 99 insertions(+), 7 deletions(-) create mode 100644 docs/content/features/health-endpoint.md diff --git a/docs/content/configuration/environment-variables.md b/docs/content/configuration/environment-variables.md index a8dce67..0eb3b47 100644 --- a/docs/content/configuration/environment-variables.md +++ b/docs/content/configuration/environment-variables.md @@ -105,6 +105,9 @@ Check for a trailing slash in the requested directory URI and redirect permanent ### SERVER_IGNORE_HIDDEN_FILES Ignore hidden files/directories (dotfiles), preventing them to be served and being included in auto HTML index pages (directory listing). +### SERVER_HEALTH +Activate the health endpoint. + ## Windows The following options and commands are Windows platform-specific. diff --git a/docs/content/features/health-endpoint.md b/docs/content/features/health-endpoint.md new file mode 100644 index 0000000..7588836 --- /dev/null +++ b/docs/content/features/health-endpoint.md @@ -0,0 +1,35 @@ +# Health endpoint + +SWS provides an optional `/health` endpoint that can be used to check if it is running properly. +When the `/health` is requested, SWS will generate a log only at the `debug` level instead of the usual `info` level for a regular file. + +The HTTP methods supported are `GET` and `HEAD`. + +This feature is disabled by default and can be controlled by the boolean `--health` option or the equivalent [SERVER_HEALTH](./../configuration/environment-variables.md#health) env. + +## Usage with kubernetes liveness probe + +The health endpoint is well suited for the kubernetes liveness probe: + +```yaml +apiVersion: v1 +kind: Pod +metadata: + name: frontend +spec: + containers: + - name: sws + image: frontend:1.0.0 + command: + - static-web-server + - --root=/public + - --log-level=info + - --health + ports: + - containerPort: 80 + name: http + livenessProbe: + httpGet: + path: /health + port: http +``` diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 2c2f785..4d24609 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -161,6 +161,7 @@ nav: - 'Windows Service': 'features/windows-service.md' - 'Trailing Slash Redirect': 'features/trailing-slash-redirect.md' - 'Ignore Files': 'features/ignore-files.md' + - 'Health endpoint': 'features/health-endpoint.md' - 'Platforms & Architectures': 'platforms-architectures.md' - 'Migrating from v1 to v2': 'migration.md' - 'Changelog v2 (stable)': 'https://github.com/static-web-server/static-web-server/blob/master/CHANGELOG.md' diff --git a/src/handler.rs b/src/handler.rs index 066e1b2..0e8fea3 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -6,7 +6,7 @@ //! Request handler module intended to manage incoming HTTP requests. //! -use headers::HeaderValue; +use headers::{ContentType, HeaderMapExt, HeaderValue}; use hyper::{Body, Request, Response, StatusCode}; use std::{future::Future, net::IpAddr, net::SocketAddr, path::PathBuf, sync::Arc}; @@ -76,6 +76,8 @@ pub struct RequestHandlerOpts { pub redirect_trailing_slash: bool, /// Ignore hidden files feature. pub ignore_hidden_files: bool, + /// Health endpoint feature. + pub health: bool, /// Advanced options from the config file. pub advanced_opts: Option, @@ -111,9 +113,13 @@ impl RequestHandler { let redirect_trailing_slash = self.opts.redirect_trailing_slash; let compression_static = self.opts.compression_static; let ignore_hidden_files = self.opts.ignore_hidden_files; + let health = self.opts.health; let mut cors_headers: Option = None; + let health_request = + health && uri_path == "/health" && (method.is_get() || method.is_head()); + // Log request information with its remote address if available let mut remote_addr_str = String::new(); if log_remote_addr { @@ -130,14 +136,35 @@ impl RequestHandler { remote_addr_str.push_str(&client_ip_address.to_string()) } } - tracing::info!( - "incoming request: method={} uri={}{}", - method, - uri, - remote_addr_str, - ); + + if health_request { + tracing::debug!( + "incoming request: method={} uri={}{}", + method, + uri, + remote_addr_str, + ); + } else { + tracing::info!( + "incoming request: method={} uri={}{}", + method, + uri, + remote_addr_str, + ); + } async move { + if health_request { + let body = if method.is_get() { + Body::from("OK") + } else { + Body::empty() + }; + let mut resp = Response::new(body); + resp.headers_mut().typed_insert(ContentType::html()); + return Ok(resp); + } + // Reject in case of incoming HTTP request method is not allowed if !method.is_allowed() { return error_page::error_response( diff --git a/src/server.rs b/src/server.rs index d90bd5b..bcc8cbd 100644 --- a/src/server.rs +++ b/src/server.rs @@ -257,6 +257,10 @@ impl Server { let grace_period = general.grace_period; tracing::info!("grace period before graceful shutdown: {}s", grace_period); + // Health endpoint option + let health = general.health; + tracing::info!("health endpoint: enabled={}", health); + // Create a service router for Hyper let router_service = RouterService::new(RequestHandler { opts: Arc::from(RequestHandlerOpts { @@ -281,6 +285,7 @@ impl Server { log_remote_address, redirect_trailing_slash, ignore_hidden_files, + health, advanced_opts, }), }); diff --git a/src/settings/cli.rs b/src/settings/cli.rs index 0c560a4..470f58b 100644 --- a/src/settings/cli.rs +++ b/src/settings/cli.rs @@ -378,6 +378,19 @@ pub struct General { /// Ignore hidden files/directories (dotfiles), preventing them to be served and being included in auto HTML index pages (directory listing). pub ignore_hidden_files: bool, + #[arg( + long, + default_value = "false", + default_missing_value("true"), + num_args(0..=1), + require_equals(true), + action = clap::ArgAction::Set, + env = "SERVER_HEALTH", + )] + /// Add a /health endpoint that doesn't generate any log entry and returns a 200 status code. + /// This is especially useful with Kubernetes liveness and readiness probes. + pub health: bool, + // // Windows specific arguments and commands // diff --git a/src/settings/file.rs b/src/settings/file.rs index 3feee8d..a199f2c 100644 --- a/src/settings/file.rs +++ b/src/settings/file.rs @@ -219,6 +219,9 @@ pub struct General { /// Ignore hidden files feature. pub ignore_hidden_files: Option, + /// Health endpoint feature. + pub health: Option, + #[cfg(windows)] /// windows service feature. pub windows_service: Option, diff --git a/src/settings/mod.rs b/src/settings/mod.rs index 03d786a..d0f3757 100644 --- a/src/settings/mod.rs +++ b/src/settings/mod.rs @@ -132,6 +132,7 @@ impl Settings { let mut log_remote_address = opts.log_remote_address; let mut redirect_trailing_slash = opts.redirect_trailing_slash; let mut ignore_hidden_files = opts.ignore_hidden_files; + let mut health = opts.health; // Windows-only options #[cfg(windows)] @@ -277,6 +278,9 @@ impl Settings { if let Some(v) = general.ignore_hidden_files { ignore_hidden_files = v } + if let Some(v) = general.health { + health = v + } // Windows-only options #[cfg(windows)] @@ -446,6 +450,7 @@ impl Settings { log_remote_address, redirect_trailing_slash, ignore_hidden_files, + health, // Windows-only options and commands #[cfg(windows)] -- libgit2 1.7.2