Security & Performance
Monitoring and Observability
Monitoring and Observability
Monitoring and observability help you understand system behavior, detect issues early, and maintain application health in production.
Application Monitoring Basics
Key Monitoring Concepts:
- Metrics: Numerical measurements over time (CPU, memory, requests/sec)
- Logs: Event records with timestamps and context
- Traces: Request journey through distributed systems
- Alerts: Notifications when metrics exceed thresholds
- Dashboards: Visual representation of system health
Prometheus & Grafana Setup
Prometheus collects metrics, Grafana visualizes them:
# docker-compose.yml
version: '3.8'
services:
prometheus:
image: prom/prometheus:latest
ports:
- "9090:9090"
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus_data:/prometheus
command:
- '--config.file=/etc/prometheus/prometheus.yml'
grafana:
image: grafana/grafana:latest
ports:
- "3000:3000"
environment:
- GF_SECURITY_ADMIN_PASSWORD=admin
volumes:
- grafana_data:/var/lib/grafana
node_exporter:
image: prom/node-exporter:latest
ports:
- "9100:9100"
volumes:
prometheus_data:
grafana_data:
version: '3.8'
services:
prometheus:
image: prom/prometheus:latest
ports:
- "9090:9090"
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus_data:/prometheus
command:
- '--config.file=/etc/prometheus/prometheus.yml'
grafana:
image: grafana/grafana:latest
ports:
- "3000:3000"
environment:
- GF_SECURITY_ADMIN_PASSWORD=admin
volumes:
- grafana_data:/var/lib/grafana
node_exporter:
image: prom/node-exporter:latest
ports:
- "9100:9100"
volumes:
prometheus_data:
grafana_data:
Prometheus Configuration
Define scrape targets and intervals:
# prometheus.yml
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
- job_name: 'prometheus'
static_configs:
- targets: ['localhost:9090']
- job_name: 'node_exporter'
static_configs:
- targets: ['node_exporter:9100']
- job_name: 'laravel_app'
static_configs:
- targets: ['app:9091']
metrics_path: '/metrics'
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
- job_name: 'prometheus'
static_configs:
- targets: ['localhost:9090']
- job_name: 'node_exporter'
static_configs:
- targets: ['node_exporter:9100']
- job_name: 'laravel_app'
static_configs:
- targets: ['app:9091']
metrics_path: '/metrics'
Exposing Metrics in Laravel
Create a metrics endpoint:
// routes/web.php
Route::get('/metrics', [MetricsController::class, 'index'])
->middleware('metrics.auth'); // Secure with IP whitelist
// app/Http/Controllers/MetricsController.php
use Illuminate\Support\Facades\DB;
class MetricsController extends Controller
{
public function index()
{
$metrics = [];
// HTTP request count
$metrics[] = '# HELP http_requests_total Total HTTP requests';
$metrics[] = '# TYPE http_requests_total counter';
$metrics[] = 'http_requests_total ' . Cache::get('http_requests', 0);
// Database connections
$metrics[] = '# HELP db_connections Active database connections';
$metrics[] = '# TYPE db_connections gauge';
$metrics[] = 'db_connections ' . DB::connection()->select('SHOW STATUS LIKE "Threads_connected"')[0]->Value;
// Memory usage
$metrics[] = '# HELP memory_usage_bytes Memory usage in bytes';
$metrics[] = '# TYPE memory_usage_bytes gauge';
$metrics[] = 'memory_usage_bytes ' . memory_get_usage();
return response(implode("\n", $metrics), 200)
->header('Content-Type', 'text/plain');
}
}
Route::get('/metrics', [MetricsController::class, 'index'])
->middleware('metrics.auth'); // Secure with IP whitelist
// app/Http/Controllers/MetricsController.php
use Illuminate\Support\Facades\DB;
class MetricsController extends Controller
{
public function index()
{
$metrics = [];
// HTTP request count
$metrics[] = '# HELP http_requests_total Total HTTP requests';
$metrics[] = '# TYPE http_requests_total counter';
$metrics[] = 'http_requests_total ' . Cache::get('http_requests', 0);
// Database connections
$metrics[] = '# HELP db_connections Active database connections';
$metrics[] = '# TYPE db_connections gauge';
$metrics[] = 'db_connections ' . DB::connection()->select('SHOW STATUS LIKE "Threads_connected"')[0]->Value;
// Memory usage
$metrics[] = '# HELP memory_usage_bytes Memory usage in bytes';
$metrics[] = '# TYPE memory_usage_bytes gauge';
$metrics[] = 'memory_usage_bytes ' . memory_get_usage();
return response(implode("\n", $metrics), 200)
->header('Content-Type', 'text/plain');
}
}
Error Tracking with Sentry
Sentry captures and organizes errors:
# Install Sentry SDK
composer require sentry/sentry-laravel
# Publish config
php artisan sentry:publish --dsn=your-dsn-here
# .env
SENTRY_LARAVEL_DSN=https://examplePublicKey@o0.ingest.sentry.io/0
SENTRY_TRACES_SAMPLE_RATE=1.0 # 100% of transactions
SENTRY_ENVIRONMENT=production
composer require sentry/sentry-laravel
# Publish config
php artisan sentry:publish --dsn=your-dsn-here
# .env
SENTRY_LARAVEL_DSN=https://examplePublicKey@o0.ingest.sentry.io/0
SENTRY_TRACES_SAMPLE_RATE=1.0 # 100% of transactions
SENTRY_ENVIRONMENT=production
Configure error handling:
// app/Exceptions/Handler.php
use Sentry\Laravel\Integration;
public function register()
{
$this->reportable(function (Throwable $e) {
if (app()->bound('sentry')) {
app('sentry')->captureException($e);
}
});
}
// Manually capture errors
try {
$this->processPayment();
} catch (\Exception $e) {
\Sentry\captureException($e);
\Sentry\captureMessage('Payment processing failed', [
'user_id' => auth()->id(),
'amount' => $amount,
]);
}
use Sentry\Laravel\Integration;
public function register()
{
$this->reportable(function (Throwable $e) {
if (app()->bound('sentry')) {
app('sentry')->captureException($e);
}
});
}
// Manually capture errors
try {
$this->processPayment();
} catch (\Exception $e) {
\Sentry\captureException($e);
\Sentry\captureMessage('Payment processing failed', [
'user_id' => auth()->id(),
'amount' => $amount,
]);
}
Uptime Monitoring
Monitor application availability:
// Using Laravel Pulse (built-in monitoring)
composer require laravel/pulse
php artisan pulse:install
php artisan migrate
// config/pulse.php
return [
'recorders' => [
Recorders\Servers::class => [
'server_name' => env('PULSE_SERVER_NAME', gethostname()),
],
Recorders\Requests::class => [
'enabled' => env('PULSE_REQUESTS_ENABLED', true),
'sample_rate' => env('PULSE_REQUESTS_SAMPLE_RATE', 1),
],
Recorders\SlowQueries::class => [
'enabled' => env('PULSE_SLOW_QUERIES_ENABLED', true),
'threshold' => env('PULSE_SLOW_QUERIES_THRESHOLD', 1000),
],
],
];
// Access dashboard at /pulse
Route::get('/pulse', [PulseController::class, 'index']);
composer require laravel/pulse
php artisan pulse:install
php artisan migrate
// config/pulse.php
return [
'recorders' => [
Recorders\Servers::class => [
'server_name' => env('PULSE_SERVER_NAME', gethostname()),
],
Recorders\Requests::class => [
'enabled' => env('PULSE_REQUESTS_ENABLED', true),
'sample_rate' => env('PULSE_REQUESTS_SAMPLE_RATE', 1),
],
Recorders\SlowQueries::class => [
'enabled' => env('PULSE_SLOW_QUERIES_ENABLED', true),
'threshold' => env('PULSE_SLOW_QUERIES_THRESHOLD', 1000),
],
],
];
// Access dashboard at /pulse
Route::get('/pulse', [PulseController::class, 'index']);
Performance Dashboards
Create Grafana dashboard panels:
// Grafana PromQL queries examples
// Request rate (requests per second)
rate(http_requests_total[5m])
// Error rate percentage
(rate(http_requests_total{status=~"5.."}[5m]) /
rate(http_requests_total[5m])) * 100
// Average response time
rate(http_request_duration_seconds_sum[5m]) /
rate(http_request_duration_seconds_count[5m])
// Database connection pool usage
(db_connections / db_max_connections) * 100
// Memory usage percentage
(memory_usage_bytes / memory_limit_bytes) * 100
// Request rate (requests per second)
rate(http_requests_total[5m])
// Error rate percentage
(rate(http_requests_total{status=~"5.."}[5m]) /
rate(http_requests_total[5m])) * 100
// Average response time
rate(http_request_duration_seconds_sum[5m]) /
rate(http_request_duration_seconds_count[5m])
// Database connection pool usage
(db_connections / db_max_connections) * 100
// Memory usage percentage
(memory_usage_bytes / memory_limit_bytes) * 100
Alerting Strategies
Configure alerts in Prometheus:
# alert.rules.yml
groups:
- name: application_alerts
interval: 30s
rules:
- alert: HighErrorRate
expr: rate(http_requests_total{status=~"5.."}[5m]) > 0.05
for: 5m
labels:
severity: critical
annotations:
summary: "High error rate detected"
description: "Error rate is {{ $value }} errors/sec"
- alert: HighResponseTime
expr: http_request_duration_seconds > 2
for: 10m
labels:
severity: warning
annotations:
summary: "High response time"
description: "Response time is {{ $value }}s"
- alert: DatabaseConnectionsHigh
expr: (db_connections / db_max_connections) > 0.8
for: 5m
labels:
severity: warning
annotations:
summary: "Database connection pool 80% full"
groups:
- name: application_alerts
interval: 30s
rules:
- alert: HighErrorRate
expr: rate(http_requests_total{status=~"5.."}[5m]) > 0.05
for: 5m
labels:
severity: critical
annotations:
summary: "High error rate detected"
description: "Error rate is {{ $value }} errors/sec"
- alert: HighResponseTime
expr: http_request_duration_seconds > 2
for: 10m
labels:
severity: warning
annotations:
summary: "High response time"
description: "Response time is {{ $value }}s"
- alert: DatabaseConnectionsHigh
expr: (db_connections / db_max_connections) > 0.8
for: 5m
labels:
severity: warning
annotations:
summary: "Database connection pool 80% full"
Log Aggregation
Centralized logging with ELK stack:
# filebeat.yml (ship logs to Elasticsearch)
filebeat.inputs:
- type: log
enabled: true
paths:
- /var/www/storage/logs/*.log
json.keys_under_root: true
json.add_error_key: true
output.elasticsearch:
hosts: ["localhost:9200"]
index: "laravel-%{+yyyy.MM.dd}"
# Laravel log format for structured logging
// config/logging.php
'stack' => [
'driver' => 'stack',
'channels' => ['daily', 'sentry'],
'formatter' => \Monolog\Formatter\JsonFormatter::class,
],
filebeat.inputs:
- type: log
enabled: true
paths:
- /var/www/storage/logs/*.log
json.keys_under_root: true
json.add_error_key: true
output.elasticsearch:
hosts: ["localhost:9200"]
index: "laravel-%{+yyyy.MM.dd}"
# Laravel log format for structured logging
// config/logging.php
'stack' => [
'driver' => 'stack',
'channels' => ['daily', 'sentry'],
'formatter' => \Monolog\Formatter\JsonFormatter::class,
],
Best Practice: Monitor the Four Golden Signals: Latency, Traffic, Errors, and Saturation. Set up alerts for actionable issues only to avoid alert fatigue.
Exercise: Set up basic monitoring for your Laravel app. Install Laravel Pulse or create a custom metrics endpoint. Configure one critical alert (e.g., error rate threshold). Test the alerting by triggering the condition.