Introduction
In Laravel applications, Prometheus is a powerful tool for monitoring and metrics collection, especially when using packages for Laravel Prometheus like Spatie's. However, Spatie's package (at the time of writing this article) doesn't support all Prometheus features like counters & histograms, even though the relevant methods are present in the Prometheus class.
The Issue β οΈ
Typically, following the PromPHP package's convention for short-term storage requires configuring extensions like Redis or APCu for storing metrics. This may not be ideal for everyone, especially if you are already using Laravelβs Cache facade for caching purposes.
In this article, I'll show you how to integrate PromPHP with Laravel's Cache facade .
Acknowledgement π
This Tutorial is inspired by Ayooluwa Isaiah for Using Prometheus
and, the custom Adapter was already implemented under the hood in the Spatie package to be used for Spatie usage, I copied & extended it π
Let's get Started π₯
Install the package:
composer require promphp/prometheus_client_php
Create a provider:
php artisan make:provider PrometheusServiceProvider
Register the PrometheusServiceProvider
:
Add the PrometheusServiceProvider in config/app.php
'providers' => [
// Other providers...
App\Providers\PrometheusServiceProvider::class,
],
Create a Custom Adapter Class:
This class is supposed to implement Prometheus\Storage\Adapter
interface, but since the package already has some classes that implement that Interface, we will inherit one Prometheus\Storage\InMemory
of them and override it, as following:
<?php
namespace App\Services;
use Illuminate\Contracts\Cache\Repository;
use Prometheus\Counter;
use Prometheus\Gauge;
use Prometheus\Histogram;
use Prometheus\MetricFamilySamples;
use Prometheus\Storage\InMemory;
class PrometheusCustomAdapter extends InMemory
{
protected string $cacheKeyPrefix = 'PROMETHEUS_';
protected string $cacheKeySuffix = '_METRICS';
/** @var string[] */
private array $stores = [
'gauges' => Gauge::TYPE,
'counters' => Counter::TYPE,
'histograms' => Histogram::TYPE,
];
public function __construct(protected readonly Repository $cache)
{
}
/**
* @return MetricFamilySamples[]
*/
public function collect(bool $sortMetrics = true): array
{
foreach ($this->stores as $store => $storeName) {
$this->{$store} = $this->fetch($storeName);
}
return parent::collect($sortMetrics);
}
public function updateHistogram(array $data): void
{
$this->histograms = $this->fetch(Histogram::TYPE);
parent::updateHistogram($data);
$this->update(Histogram::TYPE, $this->histograms);
}
public function updateGauge(array $data): void
{
$this->gauges = $this->fetch(Gauge::TYPE);
parent::updateGauge($data);
$this->update(Gauge::TYPE, $this->gauges);
}
public function updateCounter(array $data): void
{
$this->counters = $this->fetch(Counter::TYPE);
parent::updateCounter($data);
$this->update(Counter::TYPE, $this->counters);
}
public function wipeStorage(): void
{
$this->cache->deleteMultiple(
array_map(fn ($store) => $this->cacheKey($store), $this->stores)
);
}
protected function fetch(string $type): array
{
return $this->cache->get($this->cacheKey($type)) ?? [];
}
protected function update(string $type, $data): void
{
$this->cache->put($this->cacheKey($type), $data, 3600);
}
protected function cacheKey(string $type): string
{
return $this->cacheKeyPrefix . $type . $this->cacheKeySuffix;
}
}
Register the Custom Adapter:
Update the PrometheusServiceProvider
by adding App\Services\PrometheusCustomAdapter
to the Prometheus\CollectorRegistry
<?php
namespace App\Providers;
use Illuminate\Contracts\Cache\Repository;
use Illuminate\Support\ServiceProvider;
use App\Services\PrometheusCustomAdapter;
use Prometheus\CollectorRegistry;
class PrometheusServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->singleton(CollectorRegistry::class, function ($app) {
// typically, we would need to use one from the PromPHP package
// $adapter = new \Prometheus\Storage\APC();
// OR
// $adapter = new \Prometheus\Storage\Redis($redisConfigurations);
$adapter = new PrometheusCustomAdapter($app->make(Repository::class));
return new CollectorRegistry($adapter, false);
});
}
}
Create Metrics Endpoint:
Create new Endpoint metrics
to render the Metrics routes/web.php
<?php
use Prometheus\CollectorRegistry;
use Prometheus\RenderTextFormat;
Route::get('/metrics', function (CollectorRegistry $registry) {
$renderer = new RenderTextFormat();
$metrics = $registry->getMetricFamilySamples();
if (empty($metrics)) {
return response('No metrics found');
}
return response($renderer->render($metrics))
->header('Content-Type', RenderTextFormat::MIME_TYPE);
});
This endpoint should render metrics in a Prometheus-compatible format.
Open your browser & visit http://localhost:8000/metrics
.
As we haven't registered any metrics yet, we will get No metrics found
.
Now Let's add some Metrics π€:
Add Metrics:
Create New Middleware
Create New Middleware to Register Metrics
php artisan make:middleware PrometheusMiddleware
Register Metrics
Register Metrics in app/Http/Middleware/PrometheusMiddleware.php
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Prometheus\CollectorRegistry;
use Prometheus\Counter;
use Prometheus\Exception\MetricsRegistrationException;
class PrometheusMiddleware
{
private Counter $counter;
/**
* @throws MetricsRegistrationException
*/
public function __construct(CollectorRegistry $registry)
{
$this->counter = $registry->getOrRegisterCounter(
env('APP_NAME'),
'http_requests_total',
'Total count of HTTP requests',
['status', 'path', 'method']
);
}
public function handle(Request $request, Closure $next)
{
$response = $next($request);
$this->counter->inc([
'status' => $response->getStatusCode(),
'path' => $request->path(),
'method' => $request->method()
]);
return $response;
}
}
Register Middleware:
Register middleware in app/Http/Kernel.php
protected $middleware = [
// ...
\App\Http\Middleware\PrometheusMiddleware::class,
];
Now refresh the Tab where http://localhost:8000/metrics
is one several times & try calling other endpoint several times, you'll see output like:
"# HELP YOUR_APP_NAME_http_requests_total Total count of HTTP requests"
"# TYPE YOUR_APP_NAME_http_requests_total counter"
YOUR_APP_NAME_http_requests_total{status="200",path="metrics",method="GET"} 2
YOUR_APP_NAME_http_requests_total{status="200",path="otherendpoint",method="GET"} 1
so far so good ? π€, Now let's test the Cache π
Test our Laravel Caching:
Clear the cache using Laravel
php artisan cache:clear
Now, Refresh http://localhost:8000/metrics
, and you should see No metrics found
again.
Top comments (0)