Cet article est aussi disponible en français
Monitoring a Drupal site with Uptime Kuma (or any other probe) often comes down to checking that the homepage returns a 200. The problem is that the homepage returns a 200 in plenty of situations where the site is in bad shape: the page is served from cache while the database is down, Redis has stopped answering but the page cache is still holding up, Solr is down and search returns an empty page, the disk is full and no upload goes through anymore… You find out from a user, not from the probe.
So I wrote a small module, kgaut/healthcheck, which exposes a /health route that actually queries the services the site depends on, and returns a JSON verdict that a probe or a CI job can read.
$ curl -sS https://www.example.com/health
{"status":"ok","checks":{"database":"ok","redis":"ok","search_api":"warning","files":"ok"}}Installation
composer require kgaut/healthcheck
drush en healthcheckNo configuration is needed. The module requires Drupal 11.1 or later and PHP 8.3.
What gets checked
| Check | What it does | Critical |
|---|---|---|
database | a SELECT 1 on the default connection | yes |
redis | a PING through the redis module, if a connection is declared | yes |
search_api | the availability of each active Search API server (a ping for Solr) | no |
files | writes, reads back, then deletes a file in public://.healthcheck | yes |
Each check returns one of four verdicts:
ok: the service answered;failed: a critical check failed, and the response switches to 503;warning: a non-critical check failed, and the response stays at 200 (a Solr outage degrades search, it doesn't make the site unavailable);not_configured: the site doesn't use this service (no Redis, no Search API server). This is never a failure.
The response body contains no hostname, no path and no error message: the cause of a failure goes to the Drupal log (healthcheck channel). The verdict is kept for 30 seconds, so a burst of requests doesn't hammer the database or Redis, but the HTTP response itself is never cached (Cache-Control: no-store).
One rule for the probe
If you only remember one thing: a probe should only consider the site healthy on an HTTP 200 whose body contains "status":"ok", without following redirects.
Why so much caution? Because a database that can't be reached when Drupal boots gives a 500 (not a 503 from the module, Drupal doesn't even start), because a proxy or a maintenance page can answer 200 with its own content, and because a misconfigured site can redirect /health to the installer or to /fr/health. In Uptime Kuma, that's an "HTTP(s) - Keyword" monitor on "status":"ok", with redirects disabled.
Per-site and per-environment settings
Everything lives in the healthcheck.settings config, exported along with the rest of the site's configuration. You can, for instance, make Search API critical on a site where search is at the heart of the business, or disable a check. And like any config, it can be overridden in settings.php:
// Search API critical in production only.
$config['healthcheck.settings']['checks']['search_api']['critical'] = TRUE;
// The write test in another directory.
$config['healthcheck.settings']['files_directory'] = 'private://healthcheck';Adding your own checks
Checks are plugins. A site that depends on a third-party API, a mail server or any other service can add its own from any module, with a PHP attribute:
namespace Drupal\my_module\Plugin\HealthCheck;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\healthcheck\Attribute\HealthCheck;
use Drupal\healthcheck\CheckResult;
use Drupal\healthcheck\HealthCheckBase;
#[HealthCheck(id: 'my_service', label: new TranslatableMarkup('My service'), critical: TRUE, weight: 40)]
final class MyService extends HealthCheckBase {
public function check(): CheckResult {
// Actually query the service, with a short timeout.
return CheckResult::ok();
}
}With drush
drush healthcheck:run # table: check, verdict, critical, cause
drush healthcheck:run --format=json # for a CIThe command checks on every call (without the 30-second cached verdict), shows the cause of each failure, and exits with an error if a critical check fails. Handy at the end of a deployment script, or to understand why the probe is red without digging through the logs.
Post-deployment check in CI
This is the use case that made me write the module: on my uzinasit infrastructure, each tag deploys the sites, then a CI job checks that each one answers properly on its health endpoint. The module ships the script for that, scripts/ci-health.sh, which only needs bash and curl:
prod:health:
stage: postdeploy
image: wodby/drupal-php:8.3-dev # any image with bash and curl
needs:
- job: prod:clear-cache
artifacts: false
variables:
HEALTH_URL: https://www.example.com/health
script:
- bash web/modules/contrib/healthcheck/scripts/ci-health.sh "$HEALTH_URL"
rules:
- if: $CI_COMMIT_TAGThe script makes up to 6 attempts, 10 seconds apart (time for the caches to warm up after being cleared), applies the "200 + "status":"ok" without redirects" rule, and shows the status code and body of each attempt when it fails. It doesn't block anything, the deployment is already done, but the pipeline turns red, and you know right away.
Where the module stands
The module is in beta (1.0.0-beta5 at the time of writing), it runs on my sites and 1.0.0 is getting close. The code and the full documentation (operations, writing a check, design decisions) are on GitHub (in French for now), and the package is on Packagist. Feedback is welcome in the issues.
Comments (0)