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”:
| Service | Actual check |
|---|---|
| database | SELECT 1 on the configured connection |
| cache | writes then reads back a key (Cache::put()/get()) |
| storage | storage/ exists and is writable |
| queue | Queue::pending() does not throw |
curl http://localhost:8000/health
{"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:
./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
| Route | Checks | Use |
|---|---|---|
GET /health, /up, /health/ready | database, cache, storage, queue; 503 if one fails | monitoring, readiness probe |
GET /health/live | nothing: the PHP process answers | liveness 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.
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
LOG_FORMAT=json
LOG_CHANNEL=stderr # containers; or daily, syslog...
{"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
METRICS_ENABLED=true
METRICS_TOKEN=a-long-random-value
scrape_configs:
- job_name: niangpro
scheme: https
authorization:
credentials: a-long-random-value
static_configs:
- targets: ['app.example.com']
| Metric | Type |
|---|---|
niangpro_http_requests_total{method, status} | counter (status by class: 2xx, 5xx...) |
niangpro_http_request_duration_seconds | histogram |
niangpro_jobs_processed_total, niangpro_jobs_failed_total | counters (workers) |
app_* | your counters and gauges |
// 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=truein.env.- The response has a
Content-Typestarting withtext/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.