diff --git a/.changesets/config_aaron_allow_custom_healthcheck_endpoint.md b/.changesets/config_aaron_allow_custom_healthcheck_endpoint.md new file mode 100644 index 0000000000..f874fd7d9a --- /dev/null +++ b/.changesets/config_aaron_allow_custom_healthcheck_endpoint.md @@ -0,0 +1,9 @@ +### Configuration: allow custom health check endpoint ([Issue #2938](https://github.com/apollographql/router/issues/2938)) + +Adds a configuration option for custom health check endpoints, using `/health` as default + + + +--- + +By [@aaronArinder](https://github.com/aaronArinder) in https://github.com/apollographql/router/pull/4145 diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 4f6c80a468..bb0e356c81 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -76,6 +76,9 @@ The CI checks require `cargo-deny` and `cargo-about` which can both be installed - `cargo install cargo-deny` - `cargo install cargo-about` +Updating the snapshots used during testing requires installing `cargo-insta`: +- `cargo install cargo-insta` + They also need you to have the federation-demo project up and running, as explained in the Getting started section above. diff --git a/apollo-router/src/axum_factory/axum_http_server_factory.rs b/apollo-router/src/axum_factory/axum_http_server_factory.rs index 1373586dec..9c791f7c37 100644 --- a/apollo-router/src/axum_factory/axum_http_server_factory.rs +++ b/apollo-router/src/axum_factory/axum_http_server_factory.rs @@ -103,13 +103,14 @@ where if configuration.health_check.enabled { tracing::info!( - "Health check endpoint exposed at {}/health", - configuration.health_check.listen + "Health check exposed at {}/{}", + configuration.health_check.listen, + configuration.health_check.path ); endpoints.insert( configuration.health_check.listen.clone(), Endpoint::from_router_service( - "/health".to_string(), + configuration.health_check.path.clone(), service_fn(move |req: router::Request| { let mut status_code = StatusCode::OK; let health = if let Some(query) = req.router_request.uri().query() { diff --git a/apollo-router/src/configuration/mod.rs b/apollo-router/src/configuration/mod.rs index d1eb9bef9f..3f8c26a8a4 100644 --- a/apollo-router/src/configuration/mod.rs +++ b/apollo-router/src/configuration/mod.rs @@ -1161,25 +1161,43 @@ pub(crate) struct HealthCheck { /// Defaults to 127.0.0.1:8088 pub(crate) listen: ListenAddr, - /// Set to false to disable the health check endpoint + /// Set to false to disable the health check pub(crate) enabled: bool, + + /// Optionally set a custom healthcheck path + /// Defaults to /health + pub(crate) path: String, } fn default_health_check_listen() -> ListenAddr { SocketAddr::from_str("127.0.0.1:8088").unwrap().into() } -fn default_health_check() -> bool { +fn default_health_check_enabled() -> bool { true } +fn default_health_check_path() -> String { + "/health".to_string() +} + #[buildstructor::buildstructor] impl HealthCheck { #[builder] - pub(crate) fn new(listen: Option, enabled: Option) -> Self { + pub(crate) fn new( + listen: Option, + enabled: Option, + path: Option, + ) -> Self { + let mut path = path.unwrap_or_else(default_health_check_path); + if !path.starts_with('/') { + path = format!("/{path}").to_string(); + } + Self { listen: listen.unwrap_or_else(default_health_check_listen), - enabled: enabled.unwrap_or_else(default_health_check), + enabled: enabled.unwrap_or_else(default_health_check_enabled), + path, } } } @@ -1188,10 +1206,20 @@ impl HealthCheck { #[buildstructor::buildstructor] impl HealthCheck { #[builder] - pub(crate) fn fake_new(listen: Option, enabled: Option) -> Self { + pub(crate) fn fake_new( + listen: Option, + enabled: Option, + path: Option, + ) -> Self { + let mut path = path.unwrap_or_else(default_health_check_path); + if !path.starts_with('/') { + path = format!("/{path}").to_string(); + } + Self { listen: listen.unwrap_or_else(test_listen), - enabled: enabled.unwrap_or_else(default_health_check), + enabled: enabled.unwrap_or_else(default_health_check_enabled), + path, } } } diff --git a/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__schema_generation.snap b/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__schema_generation.snap index 73498d68bc..37d55452cf 100644 --- a/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__schema_generation.snap +++ b/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__schema_generation.snap @@ -1557,12 +1557,13 @@ expression: "&schema" "description": "Health check configuration", "default": { "listen": "127.0.0.1:8088", - "enabled": true + "enabled": true, + "path": "/health" }, "type": "object", "properties": { "enabled": { - "description": "Set to false to disable the health check endpoint", + "description": "Set to false to disable the health check", "default": true, "type": "boolean" }, @@ -1579,6 +1580,11 @@ expression: "&schema" "type": "string" } ] + }, + "path": { + "description": "Optionally set a custom healthcheck path Defaults to /health", + "default": "/health", + "type": "string" } }, "additionalProperties": false diff --git a/apollo-router/src/configuration/tests.rs b/apollo-router/src/configuration/tests.rs index db6f317d53..8ef806aefb 100644 --- a/apollo-router/src/configuration/tests.rs +++ b/apollo-router/src/configuration/tests.rs @@ -934,6 +934,39 @@ fn test_deserialize_derive_default() { } } +#[test] +fn it_defaults_health_check_configuration() { + let conf = Configuration::default(); + let addr: ListenAddr = SocketAddr::from_str("127.0.0.1:8088").unwrap().into(); + + assert_eq!(conf.health_check.listen, addr); + assert_eq!(&conf.health_check.path, "/health"); + + // Defaults to enabled: true + assert!(conf.health_check.enabled); +} + +#[test] +fn it_sets_custom_health_check_path() { + let conf = Configuration::builder() + .health_check(HealthCheck::new(None, None, Some("/healthz".to_string()))) + .build() + .unwrap(); + + assert_eq!(&conf.health_check.path, "/healthz"); +} + +#[test] +fn it_adds_slash_to_custom_health_check_path_if_missing() { + let conf = Configuration::builder() + // NB the missing `/` + .health_check(HealthCheck::new(None, None, Some("healthz".to_string()))) + .build() + .unwrap(); + + assert_eq!(&conf.health_check.path, "/healthz"); +} + fn has_field_level_serde_defaults(lines: &[&str], line_number: usize) -> bool { let serde_field_default = Regex::new( r#"^\s*#[\s\n]*\[serde\s*\((.*,)?\s*default\s*=\s*"[a-zA-Z0-9_:]+"\s*(,.*)?\)\s*\]\s*$"#, diff --git a/docs/source/configuration/health-checks.mdx b/docs/source/configuration/health-checks.mdx index 26ba5b80bc..a65df0c3f3 100644 --- a/docs/source/configuration/health-checks.mdx +++ b/docs/source/configuration/health-checks.mdx @@ -11,6 +11,23 @@ You can change this by setting `health_check`: health_check: listen: 127.0.0.1:8088 enabled: true + path: /health +``` + +Each option is configurable. For example, we might want to set our health check to `127.0.0.1:8090/healthz`: + +```yaml title="router.yaml" +health_check: + listen: 127.0.0.1:8090 + enabled: true + path: /healthz +``` + +We can also disable the health check: + +```yaml title="router.yaml" +health_check: + enabled: false ``` ## Testing with `curl`