# httptheus

> Latency, status and failure reason for every outbound call through Laravel's HTTP client or Guzzle, as Prometheus metrics, with a Grafana dashboard.

Source: https://boring-observability.dev/open-source/httptheus
Install: `composer require boring-o11y/httptheus`
Requires: PHP 8.2+, Laravel 12 or 13, Guzzle 7 or 8
Code and full documentation: https://github.com/boring-o11y/httptheus
License: MIT

---

When a page gets slow, the cause is often a call your app made to somebody else's API. Your APM shows the request took four seconds. It rarely shows that 3.8 of them went to one vendor's search endpoint, or that the vendor has been timing out since nine this morning.

httptheus puts those calls in Prometheus. It hooks Laravel's HTTP client globally, so every `Http::get()` and `Http::pool()` is recorded without touching the code that makes them. The same middleware can be pushed onto a raw Guzzle handler stack, which covers the SDKs that build their own client.

## Two metrics, and what each one answers

`httptheus_client_request_duration_seconds` is a histogram labelled by host, method, endpoint and status class. It answers "how slow is Stripe at p95" and "how many requests a minute go to this API", from the same series:

```promql
histogram_quantile(0.95, sum by (le) (
  rate(httptheus_client_request_duration_seconds_bucket{host="api.stripe.com"}[5m])
))
```

`httptheus_client_request_errors_total` counts only the transfers that never got a response, with a `reason` of timeout, DNS, connection refused, TLS or network. A 500 already has a status class. What a status cannot tell you is whether the other side is down or your DNS is broken, and that is what this counter is for.

Bodies and headers are never read, and nothing is written to your database. A Grafana dashboard for both metrics ships with the package and imports onto any Prometheus datasource.

## Endpoints without a series explosion

A label per URL path would create a series for every user id you ever called. httptheus normalises paths before they become labels: numeric ids, UUIDs, ULIDs and long segments become `:id`, and only the first three segments are kept. So `/v1/users/8134/orders/99` is recorded as `/v1/users/:id/*`. If that heuristic is not enough for an API you call, an allow-list of patterns puts everything else into a single `other` series. The dashboard has a panel counting distinct endpoint labels, so you see growth before Prometheus does.

## Where the numbers are kept between requests

Under PHP-FPM each request is a fresh process, so counters need shared storage to survive until the scrape. httptheus uses APCu when it is available and can use Redis, which is what queue workers need because APCu is per process. If you already run [spatie/laravel-prometheus](https://github.com/spatie/laravel-prometheus), the metrics appear on its `/prometheus` endpoint with no configuration.

Otherwise httptheus serves its own scrape at `/httptheus/metrics`. A scrape lists every host your app talks to, so until you allow an IP range or add a gate it answers only in the `local` environment.

## Two things to know

- It counts transfers, not calls. A redirect chain, or a request retried with Laravel's `retry()`, records every hop, so the count can be higher than the number of calls your code made.
- Aggregates only. When you need the individual request that failed, with its response body, that is a different tool.

