ianfoxdev / slo-kit
SLOs for PHP services, measured right: latency histograms with a bucket at every SLO threshold, route templates, an explicit definition of an error, a check of /metrics against the SLO file, and a Sloth spec for burn-rate alerts.
Requires
- php: ^8.3
- promphp/prometheus_client_php: ^2.15
- psr/http-server-middleware: ^1.0
- symfony/yaml: ^6.4.8 || ^7.0.8 || ^8.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.64
- nyholm/psr7: ^1.8
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^12.5 || ^13.0
- predis/predis: ^2.2
- symfony/http-kernel: ^6.4 || ^7.0 || ^8.0
Suggests
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-17 05:58:52 UTC
README
SLOs for PHP services, measured right. Latency histograms get a bucket at every SLO
threshold, routes are templates, and what counts as an error is written down. A check reads
/metrics against the SLO file, and a generated Sloth spec
gives the multi-window, multi-burn-rate alerts. A docker-compose stack runs it end to end.
Status: v0.1. Until 1.0 a minor version may change the API or the SLO file; such changes are marked BREAKING in the CHANGELOG. A change to the generated queries is listed there too, because it changes what your alerts measure.
Collecting metrics in PHP is solved (promphp, artprima/prometheus-metrics-bundle, spatie/laravel-prometheus), and so is turning an SLO into burn-rate alerts (Sloth, Pyrra). What breaks is in between:
- No bucket at the threshold. "95% of checkouts within 300 ms" is read from the bucket
le="0.3". promphp's default buckets have 0.25 and 0.5. On the same 1,027 requests, 78.0% are under 0.25 s and 97.7% under 0.5 s, so the SLO is failing or met depending on which bucket the query happens to use. Nobody chose either threshold. - URLs instead of routes.
/orders/8812as a label is one series per order. - Errors nobody defined. A bot's 404 eats the budget; a 429 that turned users away does not.
- Names that drift. The alert reads
http_requests_total, the app exportsapp_http_requests_total. Valid PromQL that never fires.
Install
composer require ianfoxdev/slo-kit
PHP 8.3 or later. Metrics go through promphp/prometheus_client_php: APCu, Redis or in memory, as you already use it.
The SLO file
service: checkout metrics: prefix: app # app_http_requests_total, app_http_request_duration_seconds slos: - name: checkout-availability objective: 99.9 # percent routes: ['checkout_*', 'api_payment_*'] errors: ['5xx', 429] # the default - name: checkout-latency objective: 95 threshold_ms: 300 routes: ['checkout_*']
Measuring
use IanFoxDev\SloKit\{Recorder, SloFile, SloMiddleware}; use Prometheus\Storage\Redis; $recorder = new Recorder(SloFile::fromYaml('slo.yaml'), new Redis(['host' => 'redis'])); // Any framework, Yaf included: $recorder->observe('checkout_confirm', 200, 0.214, 'POST'); // PSR-15, after your router: $middleware = new SloMiddleware($recorder); // GET /metrics: echo $recorder->render();
The duration histogram's buckets are a fixed grid plus every threshold_ms in the file. A
missing route is recorded as unmatched; a route that looks like a URL throws. Symfony:
register IanFoxDev\SloKit\Symfony\SloSubscriber, which records on kernel.terminate, after
the response is sent under PHP-FPM.
Checking
vendor/bin/slo-kit check slo.yaml --metrics=http://checkout:8080/metrics
error checkout-latency: Threshold 0.3 s has no bucket (nearest: 0.25 and 0.5). The SLI can
only count requests under 0.25 s or 0.5 s, not under 0.3 s: the dashboard is green
or red for a threshold nobody chose. Add le="0.3".
2 SLOs checked against http://checkout:8080/metrics: 1 error, 0 warnings
It also reports a missing counter (and the similar names it did find), missing route or
code labels, routes that look like URLs, more routes than --max-routes, a large share of
unmatched requests, and SLOs whose routes get no traffic. Exit code 1 on errors, so it
runs in CI against a test instance. It reads any Prometheus exporter, so it checks a Go
service with the same metric names too.
Alerts
vendor/bin/slo-kit sloth slo.yaml > sloth/checkout.yaml
sloth generate -i sloth/checkout.yaml -o prometheus/rules/checkout.yaml
An availability SLO counts responses whose code is in its errors; a latency SLO counts
requests slower than its threshold (all requests minus the threshold's bucket). Sloth turns
that into recording rules and the alerts of the Google SRE Workbook: page at 14.4 times the
budget rate over 1 hour and 5 minutes or 6 times over 6 hours and 30 minutes; ticket at 3
times over 1 day and 2 hours or once over 3 days and 6 hours. Tested against Sloth v0.16.0
and promtool 3.15.0.
The stack
composer install
docker compose -f stack/compose.yaml up -d
curl 'http://localhost:18080/chaos?errors=0.3'
An example service with the SLO file above, a load generator, Sloth, Prometheus,
Alertmanager and Grafana with an SLO dashboard (budget left, burn rate, SLI over 5 minutes
and 1 hour) at http://localhost:13000. With 30% of checkouts failing, the page alert fires in
about half a minute; stack/e2e.sh checks exactly that in CI.
More: docs/usage.md. Why it is built this way: docs/adr/0001-the-sli-is-measured-in-the-app.md.
Not yet
Laravel, queue consumers and outgoing HTTP calls, OpenTelemetry metrics instead of Prometheus, FrankenPHP worker mode storage, Pyrra. Open an issue if you need one of them first.