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>
Diff
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(-)
@@ -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.
@@ -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
```
@@ -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'
@@ -6,7 +6,7 @@
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,
pub ignore_hidden_files: bool,
pub health: bool,
pub advanced_opts: Option<Advanced>,
@@ -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<http::HeaderMap> = None;
let health_request =
health && uri_path == "/health" && (method.is_get() || method.is_head());
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);
}
if !method.is_allowed() {
return error_page::error_response(
@@ -257,6 +257,10 @@ impl Server {
let grace_period = general.grace_period;
tracing::info!("grace period before graceful shutdown: {}s", grace_period);
let health = general.health;
tracing::info!("health endpoint: enabled={}", health);
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,
}),
});
@@ -378,6 +378,19 @@ pub struct General {
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",
)]
pub health: bool,
@@ -219,6 +219,9 @@ pub struct General {
pub ignore_hidden_files: Option<bool>,
pub health: Option<bool>,
#[cfg(windows)]
pub windows_service: Option<bool>,
@@ -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;
#[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
}
#[cfg(windows)]
@@ -446,6 +450,7 @@ impl Settings {
log_remote_address,
redirect_trailing_slash,
ignore_hidden_files,
health,
#[cfg(windows)]