HealthCheck & Debug Toolbar

HealthCheck & Debug Toolbar

Two quite separate tools: HealthCheck answers “is it running right now?” (monitoring), the Debug Toolbar answers “what did this request do?” (development).

HealthCheck

Niang\Core\HealthCheck::run() is the logic shared between GET /health (and its alias /up) and niang health on the CLI — one check, two front ends. Four services tested for real, not just “the config file exists”:

ServiceActual check
databaseSELECT 1 on the configured connection
cachewrites then reads back a key (Cache::put()/get())
storagestorage/ exists and is writable
queueQueue::pending() does not throw
bash
curl http://localhost:8000/health
text
{"status":"ok","services":{"database":"ok","cache":"ok","storage":"ok","queue":"ok"}}

HTTP status 200 if everything is ok, 503 otherwise — designed for a load balancer check or a monitoring tool (Uptime Kuma...), ready to plug in. The CLI equivalent, to check before a deployment without making an HTTP request:

bash
./bin/niang health

Not to be confused with niang doctor: doctor checks the static environment (PHP version, loaded extensions, .env present, APP_KEY set, write permissions, loadable routes) — useful right after an installation or a deployment. health/HealthCheck checks the runtime state — are these services responding right now?

Live and ready probes

RouteChecksUse
GET /health, /up, /health/readydatabase, cache, storage, queue; 503 if one failsmonitoring, readiness probe
GET /health/livenothing: the PHP process answersliveness probe (restart a stuck container)

All four routes stay reachable in maintenance mode.

Observability

Request ID and traces

Every response carries an X-Request-Id header, also added to every log line written during the request: an error reported by a user can be found in the logs from that ID. An ID provided by the load balancer (X-Request-Id, 8 to 128 safe characters) is kept. A queued job keeps the ID of the request that created it.

The framework follows W3C Trace Context: an incoming traceparent header (proxy, APM, OpenTelemetry collector) is joined, and Http\Client calls (webhooks, OAuth, S3) pass it on to the called service. No SDK: the tool in front of the application links its traces to the logs.

php
use Niang\Core\Log;
use Niang\Core\Trace;

Log::withContext(['user_id' => $user['id']]);   // added to every following message
Trace::requestId();                              // to show it on an error page, for example

JSON logs

.env
LOG_FORMAT=json
LOG_CHANNEL=stderr   # containers; or daily, syslog...
json
{"timestamp":"2026-09-27T13:34:33.500+00:00","level":"info","message":"order 7 created","context":{"id":7},"request_id":"4bf92f3577b34da6a3ce929d0e0e4736"}

One object per line, for Loki, Elasticsearch, CloudWatch or Datadog. An exception passed as context keeps its class, message and location; sensitive keys stay masked. line (default) keeps the readable format.

Prometheus metrics

.env
METRICS_ENABLED=true
METRICS_TOKEN=a-long-random-value
prometheus.yml
scrape_configs:
  - job_name: niangpro
    scheme: https
    authorization:
      credentials: a-long-random-value
    static_configs:
      - targets: ['app.example.com']
MetricType
niangpro_http_requests_total{method, status}counter (status by class: 2xx, 5xx...)
niangpro_http_request_duration_secondshistogram
niangpro_jobs_processed_total, niangpro_jobs_failed_totalcounters (workers)
app_*your counters and gauges
php
// config/metrics.php
'counters' => ['orders_created_total' => 'Orders created'],

// in your code
Metrics::increment('orders_created_total');

// in AppServiceProvider::boot(): computed on every scrape
Metrics::gauge('jobs_pending', 'Pending jobs', fn () => DB::table('jobs')->count());

Without METRICS_TOKEN, /metrics answers 404: never exposed by default. Counters live in the cache: with several servers, CACHE_DRIVER=database or redis shares them; cache:clear resets them, which Prometheus treats as a restart. No per-URL label: the number of series stays fixed. niang doctor reports metrics enabled without a token.

Debug Toolbar

Injected automatically at the bottom of every HTML response when APP_DEBUG=true (see request lifecycle, Application::handle()): response time, number of SQL queries run (DB::queryCount(), reset on every request), peak memory, status code. Visible on every page of this documentation in local development.

Three conditions must all be true for it to be injected — never into a response it could corrupt:

  • APP_DEBUG=true in .env.
  • The response has a Content-Type starting with text/html — never on JSON.
  • The body contains a </body> tag to hook onto (the bar is inserted right before it).

APP_DEBUG=false in production — otherwise the debug bar is shown to your visitors. niang doctor warns (without failing) if APP_ENV=production and APP_DEBUG=true are both active.

⏱ 7.92 ms 🗄 0 requête(s) SQL 🧠 4.00 MB ↩ 200