promphp/prometheus_client_php
Prometheus client library for PHP with Redis or APCu-based aggregation (plus in-memory adapter). Register and update counters, gauges, histograms, and summaries via CollectorRegistry, then expose metrics in Prometheus text format for scraping.
Install the package:
composer require promphp/prometheus_client_php
Choose a storage adapter (default: Redis):
docker run -p 6379:6379 redis
apcu PHP extension is installed (pecl install apcu).First metric in Laravel: Add this to a controller or service:
use Prometheus\CollectorRegistry;
use Prometheus\RenderTextFormat;
// Increment a counter
CollectorRegistry::getDefault()
->getOrRegisterCounter('app', 'requests_total', 'Total HTTP requests')
->inc();
// Expose metrics endpoint (e.g., `/metrics`)
Route::get('/metrics', function () {
$registry = CollectorRegistry::getDefault();
$renderer = new RenderTextFormat();
header('Content-type: ' . RenderTextFormat::MIME_TYPE);
return $renderer->render($registry->getMetricFamilySamples());
});
$requestCounter = CollectorRegistry::getDefault()
->getOrRegisterCounter('http', 'requests_total', 'Total HTTP requests', ['method']);
$requestCounter->inc(['method' => request()->method()]);
$histogram = CollectorRegistry::getDefault()
->getOrRegisterHistogram('http', 'request_duration_seconds', 'Request duration', ['route'], [0.1, 0.5, 1, 2.5, 5]);
$start = microtime(true);
// ... execute logic ...
$histogram->observe(microtime(true) - $start, ['route' => request()->path()]);
Centralize metric registration in a service class:
class MetricsService {
protected $registry;
public function __construct() {
$this->registry = CollectorRegistry::getDefault();
}
public function registerRequestMetrics() {
$this->registry->getOrRegisterCounter('app', 'requests_total', 'Total requests');
$this->registry->getOrRegisterGauge('app', 'active_requests', 'Active requests');
$this->registry->getOrRegisterHistogram('app', 'request_latency_seconds', 'Request latency', [], [0.01, 0.05, 0.1, 0.5, 1]);
}
}
Register in AppServiceProvider:
public function boot() {
app(MetricsService::class)->registerRequestMetrics();
}
class MetricsMiddleware {
public function handle($request, Closure $next) {
$start = microtime(true);
$response = $next($request);
$latency = microtime(true) - $start;
$histogram = CollectorRegistry::getDefault()
->getCounter('http', 'requests_total')
->inc(['method' => $request->method(), 'path' => $request->path()]);
$histogram = CollectorRegistry::getDefault()
->getHistogram('http', 'request_latency_seconds')
->observe($latency, ['path' => $request->path()]);
return $response;
}
}
Use closures or helper methods to generate labels dynamically:
$counter = CollectorRegistry::getDefault()
->getOrRegisterCounter('app', 'errors_total', 'Total errors', ['type', 'severity']);
$counter->inc([
'type' => 'database',
'severity' => $this->getErrorSeverity($exception)
]);
class LabelSets {
public static function getUserLabels($userId) {
return ['user_id' => $userId, 'role' => auth()->user()->role];
}
}
// Usage:
$counter->inc(array_merge(
['type' => 'auth'],
LabelSets::getUserLabels($userId)
));
Configure Redis connection in config/services.php:
'redis' => [
'host' => env('REDIS_HOST', '127.0.0.1'),
'port' => env('REDIS_PORT', 6379),
'password' => env('REDIS_PASSWORD', null),
],
Override default Redis options:
\Prometheus\Storage\Redis::setDefaultOptions([
'host' => config('services.redis.host'),
'port' => config('services.redis.port'),
'password' => config('services.redis.password'),
'timeout' => 0.5,
]);
$registry = new CollectorRegistry(new \Prometheus\Storage\APCu());
CollectorRegistry::setDefault($registry);
$client = new \Predis\Client(['host' => '127.0.0.1', 'password' => 'secret']);
$registry = new CollectorRegistry(\Prometheus\Storage\Predis::fromExistingConnection($client));
Extend the library for domain-specific metrics:
class CustomSummary extends \Prometheus\Summary {
public function observeWithQuantiles(float $value, array $labels, array $quantiles) {
// Custom logic
}
}
Use CollectorRegistry in tests to verify metrics:
public function testRequestMetrics() {
$counter = CollectorRegistry::getDefault()
->getOrRegisterCounter('test', 'counter', 'Test counter');
$counter->inc();
$samples = CollectorRegistry::getDefault()->getMetricFamilySamples();
$this->assertCount(1, $samples);
}
| Adapter | Gotcha | Tip |
|---|---|---|
| Redis | Connection timeouts if Redis is overloaded. | Use timeout and read_timeout in config. |
| Data loss if Redis crashes. | Enable Redis persistence (appendonly yes). |
|
| APCu | Metrics reset on cache eviction. | Use apcu.enable_cli=1 for CLI scripts. |
| APCng | Memory leaks with large label sets. | Prefer Redis for high-cardinality labels. |
| PDO | Slow performance with frequent writes. | Use batch inserts or a dedicated metrics database. |
| InMemory | Metrics lost between requests. | Only use for short-lived scripts (e.g., cron jobs). |
int, bool) are auto-converted.
// Works:
$counter->inc(['status' => 200]); // Converted to "200"
// Avoid:
$counter->inc(['status' => true]); // Converted to "1" (unexpected)
['id' => 1] and ['id' => 2]).
Fix: Use a prefix (e.g., ['user_id' => 1]).$registry->batchUpdate(function ($registry) {
$registry->getCounter('app', 'events_total')->inc();
$registry->getGauge('app', 'active_users')->set(42);
});
if (!env('DISABLE_METRICS', false)) {
$registry = CollectorRegistry::getDefault();
// ... register metrics ...
}
$histogram = CollectorRegistry::getDefault()
->getOrRegisterHistogram('http', 'latency_seconds', 'Request latency', [], Histogram::exponentialBuckets(0.01, 2, 10));
MetricNotFoundException. Check:
try {
$counter = $registry->getCounter('nonexistent', 'metric');
How can I help you explore Laravel packages today?