This article provides a detailed guide on integrating Prometheus metrics into your Laravel application.
It explores key concepts, including instrumenting your application with various metric types, monitoring HTTP request activity, and exposing metrics for Prometheus to scrape.
The complete source code for this tutorial is available in this GitHub repository.
Let's get started!
Prerequisites
- Prior experience with PHP and Laravel, along with a recent version of PHP installed.
- Familiarity with Docker and Docker Compose.
- Basic understanding of how Prometheus works.
Step 1 — Setting up the demo project
To demonstrate Prometheus instrumentation in Laravel applications, let's set up a simple "Hello World" Laravel application along with the Prometheus server.
First, clone the repository to your local machine and navigate into the project directory:
git clone https://github.com/betterstack-community/prometheus-laravel
cd prometheus-laravel
Here's the Laravel route configuration you'll be instrumenting:
<?php
use Illuminate\Support\Facades\Route;
Route::get('/metrics', function () {
return response('', 200);
});
Route::get('/', function () {
return 'Hello world!';
});
This app exposes two endpoints: /
returns a simple "Hello world!" message, and
/metrics
endpoint that will eventually expose the instrumented metrics.
This project also includes a compose.yaml
file, which defines two services:
services:
app:
build:
conpromql: .
dockerfile: Dockerfile
environment:
APP_PORT: ${APP_PORT}
env_file:
- ./.env
ports:
- 8000:8000
volumes:
- .:/var/www/html
prometheus:
image: prom/prometheus:latest
container_name: prometheus
restart: unless-stopped
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus_data:/prometheus
command:
- --config.file=/etc/prometheus/prometheus.yml
- --storage.tsdb.path=/prometheus
- --web.console.libraries=/etc/prometheus/console_libraries
- --web.console.templates=/etc/prometheus/consoles
- --web.enable-lifecycle
expose:
- 9090
ports:
- 9090:9090
volumes:
prometheus_data:
The app
service is the Laravel application running on port 8000, while
prometheus
configures a Prometheus server to scrape the Laravel app via the
prometheus.yml
file:
global:
scrape_interval: 10s
scrape_configs:
- job_name: laravel-app
static_configs:
- targets:
- app:8000
Before starting the services, rename .env.example
to .env
. This file
contains the application's PORT
setting:
PORT=8000
Rename it with:
mv .env.example .env
Then launch both services in detached mode with:
docker compose up -d
You should see output similar to this:
[+] Running 3/3
✔ Network prometheus-laravel_default Created 0.8s
✔ Container prometheus Started 1.3s
✔ Container app Started 1.3s
To confirm that the Laravel application is running, send a request to the root endpoint:
curl http://localhost:8000
Hello world
To verify that Prometheus is able to access the exposed /metrics
endpoint,
visit http://localhost:9090/targets
in your browser:
With everything up and running, you're ready to integrate Prometheus in your Laravel application in the next step.
Step 2 — Installing the Prometheus client
Before instrumenting your Laravel application with Prometheus, you need to install a Prometheus client package for PHP.
We'll use the popular promphp/prometheusclientphp package, which provides a comprehensive PHP implementation of Prometheus metric types.
Install it via Composer:
composer require promphp/prometheus_client_php
Now, let's create a new service provider to handle Prometheus configuration. In Laravel, service providers are the central place to configure your application's services:
php artisan make:provider PrometheusServiceProvider
Then edit app/Providers/PrometheusServiceProvider.php
:
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Prometheus\CollectorRegistry;
use Prometheus\Storage\APC;
class PrometheusServiceProvider extends ServiceProvider
{
public function register()
{
$this->app->singleton(CollectorRegistry::class, function () {
return new CollectorRegistry(new APC());
});
}
}
This provider creates a singleton instance of CollectorRegistry
, which is the
central registry for all Prometheus metrics in your application.
For simplicity, we're using in-memory storage (InMemory
), but in production,
you might want to use Redis or another persistent storage adapter.
Register the provider in config/app.php
:
'providers' => [
// Other providers...
App\Providers\PrometheusServiceProvider::class,
],
Now update your metrics endpoint in routes/web.php
to expose the metrics:
<?php
use Prometheus\CollectorRegistry;
use Prometheus\RenderTextFormat;
Route::get('/metrics', function (CollectorRegistry $registry) {
$renderer = new RenderTextFormat();
return response($renderer->render($registry->getMetricFamilySamples()))
->header('Content-Type', RenderTextFormat::MIME_TYPE);
});
This endpoint will expose metrics in a Prometheus-compatible format. Visit
http://localhost:8000/metrics
in your browser or use curl
to see the
response:
curl http://localhost:8000/metrics
Currently, the response will be empty since we haven't registered any metrics yet. In the following sections, we'll instrument the application with different metric types.
Step 3 — Instrumenting a Counter metric
Let's start with a fundamental metric that tracks the total number of HTTP requests made to your Laravel application. Since this value always increases, it is best represented as a Counter.
A Counter in Prometheus is a cumulative metric that represents a single monotonically increasing counter. It can only increase or be reset to zero on restart. Counters are perfect for metrics like:
- Total number of requests
- Total number of completed tasks
- Total number of errors
Create a new middleware to handle the metrics collection:
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Prometheus\CollectorRegistry;
class PrometheusMiddleware
{
private $registry;
private $counter;
public function __construct(CollectorRegistry $registry)
{
$this->registry = $registry;
$this->counter = $registry->getOrRegisterCounter(
'app',
'http_requests_total',
'Total number 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;
}
}
This implementation creates a Counter metric named http_requests_total
with
labels for status code, path, and HTTP method. The middleware uses Laravel's
HTTP lifecycle to automatically count all requests by incrementing the counter
after each request is processed.
Register the middleware in app/Http/Kernel.php
:
protected $middleware = [
// ...
\App\Http\Middleware\PrometheusMiddleware::class,
];
If you refresh http://localhost:8000/metrics
several times, you'll see output
like:
# HELP app_http_requests_total Total number of HTTP requests
# TYPE app_http_requests_total counter
app_http_requests_total{status="200",path="metrics",method="GET"} 2
app_http_requests_total{status="200",path="/",method="GET"} 1
You can view your metrics in the Prometheus UI by heading to
http://localhost:9090
. Type app_http_requests_total
into the query box and
click Execute to see the raw values:
Switch to the Graph tab to visualize the counter increasing over time:
Step 4 — Instrumenting a Gauge metric
A Gauge represents a value that can fluctuate up or down, making it ideal for tracking real-time values such as active connections, queue sizes, or memory usage. Unlike Counters that only increase, Gauges can both increase and decrease.
In this section, we'll use a Prometheus Gauge to monitor the number of active
requests being processed by your Laravel application. Update your
PrometheusMiddleware.php
:
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Prometheus\CollectorRegistry;
class PrometheusMiddleware
{
private $registry;
private $counter;
private $gauge;
public function __construct(CollectorRegistry $registry)
{
$this->registry = $registry;
// Previous counter definition...
$this->gauge = $this->registry->getOrRegisterGauge(
'app', // namespace
'http_active_requests', // name
'Number of active HTTP requests', // help text
);
}
public function handle(Request $request, Closure $next)
{
$this->gauge->inc(); // Increment before processing
$response = $next($request);
$this->counter->inc([
$response->getStatusCode(),
$request->path(),
$request->method()
]);
$this->gauge->dec(); // Decrement after processing
return $response;
}
}
The http_active_requests
gauge metric is incremented when a new request starts
processing and decremented when the request completes.
To observe the gauge in action, let's add some artificial delay to the root
route to simulate longer-running requests. Update your routes/web.php
:
Route::get('/', function () {
$delay = random_int(1, 5); // Random delay between 1 and 5 seconds
sleep($delay);
return 'Hello world!';
});
Now use a load testing tool like wrk to generate concurrent requests:
wrk -t 10 -c 100 -d 1m --latency "http://localhost:8000"
Visiting the /metrics
endpoint will show something like:
# HELP app_http_active_requests Number of active HTTP requests
# TYPE app_http_active_requests gauge
app_http_active_requests 42
This indicates that there are currently 42 active requests being processed by your Laravel application.
Tracking absolute values
Gauges are also perfect for tracking absolute but fluctuating values. For example, to track the current memory usage of your Laravel application, you can modify the middleware:
class PrometheusMiddleware
{
public function __construct(CollectorRegistry $registry)
{
// Previous metrics...
$this->memoryGauge = $this->registry->getOrRegisterGauge(
'app',
'memory_usage_bytes',
'Current memory usage in bytes',
['type']
);
}
public function handle(Request $request, Closure $next)
{
// Previous metric collection...
// Set absolute memory values
$this->memoryGauge->set(
memory_get_usage(true),
['real']
);
$this->memoryGauge->set(
memory_get_usage(false),
['emalloc']
);
return $response;
}
}
This will produce metrics like:
# HELP app_memory_usage_bytes Current memory usage in bytes
# TYPE app_memory_usage_bytes gauge
app_memory_usage_bytes{type="real"} 6291456
app_memory_usage_bytes{type="emalloc"} 2097152
You can visualize these gauge values over time in Prometheus's Graph view at
http://localhost:9090
:
Step 5 — Instrumenting a Histogram metric
Histograms are useful for tracking the distribution of measurements, such as request durations. A histogram samples observations (usually request durations or response sizes) and counts them in configurable buckets.
Let's instrument your Laravel application with a histogram to track HTTP request
durations. Update your PrometheusMiddleware.php
:
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Prometheus\CollectorRegistry;
class PrometheusMiddleware
{
private $registry;
private $counter;
private $gauge;
private $histogram;
public function __construct(CollectorRegistry $registry)
{
$this->registry = $registry;
// Previous metrics...
$this->histogram = $registry->getOrRegisterHistogram(
'app',
'http_request_duration_seconds',
'HTTP request duration in seconds',
['status', 'path', 'method'],
[0.1, 0.25, 0.5, 1, 2.5, 5]
);
}
public function handle(Request $request, Closure $next)
{
$this->gauge->inc();
$start = microtime(true);
$response = $next($request);
$duration = microtime(true) - $start;
$this->counter->inc([
'status' => $response->getStatusCode(),
'path' => $request->path(),
'method' => $request->method()
]);
$this->histogram->observe(
$duration,
[
'status' => $response->getStatusCode(),
'path' => $request->path(),
'method' => $request->method()
]
);
$this->gauge->dec();
return $response;
}
}
After generating some traffic to your application, the /metrics
endpoint will
show histogram data like this:
# HELP app_http_request_duration_seconds HTTP request duration in seconds
# TYPE app_http_request_duration_seconds histogram
app_http_request_duration_seconds_bucket{status="200",path="/",method="GET",le="0.1"} 12
app_http_request_duration_seconds_bucket{status="200",path="/",method="GET",le="0.25"} 25
app_http_request_duration_seconds_bucket{status="200",path="/",method="GET",le="0.5"} 45
app_http_request_duration_seconds_bucket{status="200",path="/",method="GET",le="0.75"} 78
app_http_request_duration_seconds_bucket{status="200",path="/",method="GET",le="1.0"} 89
app_http_request_duration_seconds_bucket{status="200",path="/",method="GET",le="2.5"} 95
app_http_request_duration_seconds_bucket{status="200",path="/",method="GET",le="5.0"} 98
app_http_request_duration_seconds_bucket{status="200",path="/",method="GET",le="7.5"} 99
app_http_request_duration_seconds_bucket{status="200",path="/",method="GET",le="10.0"} 100
app_http_request_duration_seconds_bucket{status="200",path="/",method="GET",le="+Inf"} 100
app_http_request_duration_seconds_sum{status="200",path="/",method="GET"} 47.423
app_http_request_duration_seconds_count{status="200",path="/",method="GET"} 100
Let's understand what this output means:
- Each
_bucket
line represents the number of requests that took less than or equal to a specific duration. For example,le="0.5"}
45 means 45 requests completed within 0.5 seconds. - The
_sum
value (47.423) is the total of all observed durations. - The
_count
value (100) is the total number of observations.
This data allows you to analyze the distribution of request durations. For example, in Prometheus you can calculate the 95th percentile latency using:
histogram_quantile(0.95, sum(rate(app_http_request_duration_seconds_bucket[5m])) by (le))
This query shows the response time that 95% of requests fall under, which is more useful than averages for understanding real user experience.
Step 6 — Instrumenting a Summary metric
A Summary metric in Prometheus is similar to a histogram but calculates quantiles on the client side. This makes it valuable when you need precise quantiles per instance without relying on Prometheus for aggregation.
Let's create a service to monitor external API calls using a Summary metric:
php artisan make:service ExternalApiService
Edit app/Services/ExternalApiService.php
:
<?php
namespace App\Services;
use Illuminate\Support\Facades\Http;
use Prometheus\CollectorRegistry;
class PostsService
{
private $registry;
private $summary;
public function __construct(CollectorRegistry $registry)
{
$this->registry = $registry;
$this->summary = $registry->getOrRegisterSummary(
'app',
'external_api_request_duration_seconds',
'Duration of external API requests',
['endpoint']
);
}
public function getPosts()
{
$start = microtime(true);
try {
$response = Http::get('https://jsonplaceholder.typicode.com/posts');
$response->throw();
return $response->json();
} finally {
$duration = microtime(true) - $start;
$this->summary->observe($duration, ['endpoint' => 'posts']);
}
}
}
Add a new route to test the summary metric:
use App\Services\PostsService;
Route::get('/posts', function (PostsService $service) {
return $service->getPosts();
});
The summary metrics will look like this:
# HELP app_external_api_request_duration_seconds External API request duration in seconds
# TYPE app_external_api_request_duration_seconds summary
app_external_api_request_duration_seconds{endpoint="posts",quantile="0.5"} 0.342
app_external_api_request_duration_seconds{endpoint="posts",quantile="0.9"} 0.456
app_external_api_request_duration_seconds{endpoint="posts",quantile="0.99"} 0.891
app_external_api_request_duration_seconds_sum{endpoint="posts"} 12.423
app_external_api_request_duration_seconds_count{endpoint="posts"} 32
This tells us that:
- The median (50th percentile) request time is 342ms
- 90% of requests complete within 456ms
- 99% of requests complete within 891ms
- We've made 32 requests with a total duration of 12.423 seconds
Final thoughts
We've explored how to integrate Prometheus metrics into a Laravel application, covering the setup of monitoring infrastructure and the implementation of different metric types. Through this tutorial, you've learned how to use Counters for tracking cumulative values, Gauges for fluctuating measurements, Histograms for value distributions, and Summaries for client-side quantiles.
To build on this foundation, you might want to set up Prometheus Alertmanager for metric-based alerts, connect your metrics to Better Stack or other visualization tools, explore PromQL for sophisticated queries, and add business-specific metrics for your application.
The metrics we've implemented provide a solid starting point for monitoring your Laravel application, but they're just the beginning. Consider what aspects of your specific application would benefit from monitoring, and extend these patterns to create a comprehensive observability solution that meets your needs.
Thanks for reading, and happy monitoring!
Make your mark
Join the writer's program
Are you a developer and love writing and sharing your knowledge with the world? Join our guest writing program and get paid for writing amazing technical guides. We'll get them to the right readers that will appreciate them.
Write for us
Build on top of Better Stack
Write a script, app or project on top of Better Stack and share it with the world. Make a public repository and share it with us at our email.
community@betterstack.comor submit a pull request and help us build better products for everyone.
See the full list of amazing projects on github