DEV Community

Cover image for How to Integrate PromPHP(Prometheus) with Laravel's Cache Facade in few Minutes πŸš€
Hussein El-Hewehi
Hussein El-Hewehi

Posted on

How to Integrate PromPHP(Prometheus) with Laravel's Cache Facade in few Minutes πŸš€

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
Enter fullscreen mode Exit fullscreen mode

Create a provider:

php artisan make:provider PrometheusServiceProvider
Enter fullscreen mode Exit fullscreen mode

Register the PrometheusServiceProvider:

Add the PrometheusServiceProvider in config/app.php

'providers' => [
    // Other providers...
    App\Providers\PrometheusServiceProvider::class,
],
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
});
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
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;
    }
}

Enter fullscreen mode Exit fullscreen mode

Register Middleware:

Register middleware in app/Http/Kernel.php

protected $middleware = [
    // ...
    \App\Http\Middleware\PrometheusMiddleware::class,
];
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Now, Refresh http://localhost:8000/metrics, and you should see No metrics found again.


Congratulations !! πŸŽ‰

You're now Using the Laravel Cache with PromPHP 🎯

Top comments (0)