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:

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

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

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

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.


Congratulations !! 🎉

You're now Using the Laravel Cache with PromPHP 🎯