Server Monitoring Best Practices: Keeping Your Laravel App and PostgreSQL Clusters Alive on AWS
Proactive PostgreSQL Monitoring with CloudWatch and RDS Performance Insights
Maintaining the health and performance of PostgreSQL clusters on AWS RDS is paramount for any production Laravel application. Relying solely on reactive alerts is a recipe for disaster. We need a multi-layered approach that combines AWS’s native monitoring tools with deep-dive analysis capabilities.
AWS CloudWatch provides the foundational metrics for RDS instances. Key metrics to monitor include:
CPUUtilization: High CPU can indicate inefficient queries or insufficient instance sizing.FreeableMemory: Low freeable memory suggests memory pressure, impacting cache performance and potentially leading to swapping.ReadIOPSandWriteIOPS: Spikes or sustained high IOPS can point to I/O bottlenecks.ReadLatencyandWriteLatency: Increasing latency is a direct indicator of performance degradation.DatabaseConnections: A sudden surge or consistently high number of connections can exhaust resources.DiskQueueDepth: High queue depth signifies that the storage subsystem cannot keep up with the I/O requests.
Setting up CloudWatch alarms for these metrics is the first line of defense. We’ll configure alarms to trigger at thresholds that indicate potential issues before they become critical. For instance, a CPUUtilization alarm at 80% for 15 minutes, or FreeableMemory below 20% for 10 minutes.
Configuring CloudWatch Alarms via AWS CLI
Automating alarm creation is crucial for consistency. Here’s how to set up an alarm for CPUUtilization using the AWS CLI:
aws cloudwatch put-metric-alarm \
--alarm-name "RDS-MyLaravelDB-HighCPU" \
--alarm-description "Alarm when CPU utilization exceeds 80% for 15 minutes" \
--metric-name CPUUtilization \
--namespace AWS/RDS \
--statistic Average \
--period 900 \
--threshold 80 \
--comparison-operator GreaterThanOrEqualToThreshold \
--dimensions "Name=DBInstanceIdentifier,Value=my-laravel-rds-instance" \
--evaluation-periods 1 \
--datapoints-to-alarm 1 \
--treat-missing-data notBreaching \
--alarm-actions arn:aws:sns:us-east-1:123456789012:my-monitoring-sns-topic
Replace my-laravel-rds-instance and the SNS topic ARN with your specific values. Repeat this process for other critical metrics.
Beyond basic metrics, RDS Performance Insights offers invaluable granular insights into database load. It helps identify which SQL queries, wait events, and hosts are consuming the most resources. This is indispensable for diagnosing performance bottlenecks that CloudWatch alone might miss.
Leveraging RDS Performance Insights for Query Optimization
Once Performance Insights is enabled on your RDS instance, you can access its dashboard in the AWS console. Key areas to scrutinize include:
- Top SQL by Load: Identify the most resource-intensive queries. Look for queries with high
DB Load,Execution Count, andAvg Elapsed Time. - Top Wait Events: Understand what is causing the database to pause. Common culprits include
IO/FileRead,Lock/Lock, andLWLock/WALWriteLock. - Top Hosts/Users: Pinpoint which applications or users are contributing most to the database load.
For instance, if you see a query like SELECT * FROM users WHERE email = ? dominating the “Top SQL by Load” and the wait event is primarily IO/FileRead, it strongly suggests a missing index on the email column. A quick addition of an index can dramatically improve performance.
Laravel Application Monitoring with Prometheus and Grafana
While RDS handles the database, your Laravel application instances (running on EC2, ECS, or EKS) require their own robust monitoring. Prometheus and Grafana are a powerful open-source combination for this purpose.
Instrumenting Laravel with Prometheus Exporters
To expose application metrics to Prometheus, you need to instrument your Laravel application. The promphp/prometheus_client_php library is an excellent choice.
<?php
require 'vendor/autoload.php';
use Prometheus\CollectorRegistry;
use Prometheus\Render\CallbackRenderer;
use Prometheus\Storage\InMemory;
$adapter = new InMemory();
$registry = new CollectorRegistry($adapter);
// Example: Counter for HTTP requests
$counter = $registry->registerCounter('myapp_http_requests_total', 'Total number of HTTP requests', ['method', 'code']);
// Example: Gauge for current active users
$gauge = $registry->registerGauge('myapp_active_users', 'Number of currently active users', ['type']);
// Example: Histogram for request duration
$histogram = $registry->registerHistogram('myapp_request_duration_seconds', 'HTTP request duration in seconds', ['method', 'route']);
// In your Laravel middleware or controller:
// $method = $request->getMethod();
// $route = $request->route() ? $request->route()->getName() : 'no_route';
// $counter->inc([$method, $response->getStatusCode()]);
// $histogram->observe(microtime(true) - LARAVEL_REQUEST_START_TIME, [$method, $route]);
// Endpoint to expose metrics (e.g., /metrics)
header('Content-type: text/plain');
$renderer = new CallbackRenderer($registry);
echo $renderer->render();
?>
You would typically create a dedicated route (e.g., /metrics) in your Laravel application that serves these metrics. This route should be accessible by your Prometheus server.
Configuring Prometheus Scrape Targets
Your Prometheus server needs to know where to find these metrics endpoints. Edit your prometheus.yml configuration file.
scrape_configs:
- job_name: 'laravel_app'
static_configs:
- targets: ['app-instance-1.example.com:80', 'app-instance-2.example.com:80'] # Replace with your app instance IPs/hostnames
labels:
env: 'production'
app: 'laravel'
- job_name: 'laravel_app_metrics'
metrics_path: '/metrics' # The path where your app exposes metrics
static_configs:
- targets: ['app-instance-1.example.com:80', 'app-instance-2.example.com:80'] # Same instances, but hitting the /metrics endpoint
labels:
env: 'production'
app: 'laravel'
- job_name: 'postgres_exporter'
static_configs:
- targets: ['postgres-exporter.example.com:9187'] # Assuming you run postgres_exporter
labels:
env: 'production'
db: 'postgres'
For PostgreSQL, consider running the postgres_exporter alongside your database or on a dedicated instance. This exporter provides detailed PostgreSQL metrics that can be scraped by Prometheus.
Visualizing Data with Grafana Dashboards
Grafana connects to your Prometheus data source and allows you to build rich, interactive dashboards. You can import pre-built dashboards for PostgreSQL and create custom ones for your Laravel application metrics.
A good Grafana dashboard for your Laravel app should include:
- HTTP request rates and error rates (using
myapp_http_requests_total). - Request latency distributions (using
myapp_request_duration_seconds). - Active user counts (using
myapp_active_users). - PHP-FPM pool statistics (if applicable).
- Server-level metrics (CPU, memory, network) from node_exporter.
For PostgreSQL, a dashboard visualizing metrics from postgres_exporter and RDS Performance Insights (if you can integrate it, though direct integration is complex and often involves custom solutions or third-party tools) is essential. Key PostgreSQL metrics to visualize include:
- Replication lag.
- Connection counts.
- Query throughput and latency.
- Cache hit ratios.
- Disk I/O and utilization.
- Transaction rates.
Alerting with Alertmanager
Prometheus itself doesn’t handle alerting; it relies on Alertmanager. Alertmanager deduplicates, groups, and routes alerts to the correct notification channels (Slack, PagerDuty, email, etc.).
Defining Alerting Rules in Prometheus
Alerting rules are defined in separate YAML files that Prometheus loads. Here are examples for Laravel and PostgreSQL:
groups:
- name: laravel_alerts
rules:
- alert: HighHttpRequestRate
expr: sum(rate(myapp_http_requests_total{env="production"}[5m])) by (app) > 1000
for: 10m
labels:
severity: warning
annotations:
summary: "High HTTP request rate detected for {{ $labels.app }}"
description: "The application {{ $labels.app }} is experiencing a high request rate ({{ $value | humanize }} req/s)."
- alert: HighHttpRequestLatency
expr: histogram_quantile(0.95, sum(rate(myapp_request_duration_seconds_bucket{env="production"}[5m])) by (le, method, route)) > 2.0
for: 5m
labels:
severity: critical
annotations:
summary: "High 95th percentile request latency for {{ $labels.method }} {{ $labels.route }}"
description: "95% of requests for {{ $labels.method }} {{ $labels.route }} are taking longer than 2.0 seconds."
- name: postgres_alerts
rules:
- alert: HighPostgresCPU
expr: avg by (instance) (rate(pg_stat_activity_numbackends{state="active"}[5m])) > 100 # Example, adjust based on your instance type
for: 15m
labels:
severity: warning
annotations:
summary: "High CPU utilization on PostgreSQL instance {{ $labels.instance }}"
description: "PostgreSQL instance {{ $labels.instance }} has high CPU utilization."
- alert: ReplicationLagging
expr: pg_replication_lag_seconds{master_replica="replica"} > 60
for: 5m
labels:
severity: critical
annotations:
summary: "PostgreSQL replication lag detected on replica {{ $labels.replica_name }}"
description: "Replication lag on {{ $labels.replica_name }} is {{ $value }} seconds, exceeding the 60-second threshold."
These rules are then configured in Prometheus to be sent to Alertmanager.
Configuring Alertmanager Routing
The alertmanager.yml file defines how alerts are routed. A typical configuration might route critical alerts to PagerDuty and warnings to Slack.
global:
resolve_timeout: 5m
route:
group_by: ['alertname', 'cluster', 'service']
group_wait: 30s
group_interval: 5m
repeat_interval: 4h
receiver: 'default-receiver' # Default receiver if no specific routes match
routes:
- match:
severity: 'critical'
receiver: 'pagerduty-critical'
continue: true # Allows further routing if needed
- match:
severity: 'warning'
receiver: 'slack-warnings'
continue: true
receivers:
- name: 'default-receiver'
slack_configs:
- api_url: 'https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX'
channel: '#monitoring-alerts'
- name: 'pagerduty-critical'
pagerduty_configs:
- service_key: 'YOUR_PAGERDUTY_INTEGRATION_KEY'
- name: 'slack-warnings'
slack_configs:
- api_url: 'https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX'
channel: '#monitoring-warnings'
Ensure your Alertmanager is configured to receive alerts from Prometheus and that the notification integrations (Slack webhooks, PagerDuty keys) are correctly set up.
System-Level Monitoring with EC2/ECS/EKS Agents
For infrastructure-level metrics, we rely on AWS CloudWatch Agent (for EC2) or the CloudWatch Container Insights agents (for ECS/EKS). These agents collect system-level metrics and logs that are crucial for understanding the overall health of your compute resources.
CloudWatch Agent Configuration for EC2
The CloudWatch Agent can be configured to collect system metrics (CPU, memory, disk, network) and application logs. The configuration is typically stored in a JSON file.
{
"agent": {
"metrics_collection_interval": 60,
"run_as_user": "cwagent"
},
"metrics": {
"namespace": "MyLaravelApp/EC2",
"metrics_collected": {
"cpu": {
"resources": [
"*"
],
"measurement": [
"cpu_usage_idle",
"cpu_usage_iowait",
"cpu_usage_user",
"cpu_usage_system"
],
"totalcpu_time_metrics": true
},
"mem": {
"measurement": [
"mem_used_percent",
"mem_used",
"mem_total"
]
},
"disk": {
"resources": [
"/",
"/var/log"
],
"measurement": [
"used_percent",
"inodes_free"
]
},
"net": {
"resources": [
"eth0"
],
"measurement": [
"bytes_sent",
"bytes_recv",
"packets_sent",
"packets_recv"
]
}
}
},
"logs": {
"logs_collected": {
"files": {
"collect_list": [
{
"file_path": "/var/www/html/storage/logs/laravel.log",
"log_group_name": "MyLaravelApp/LaravelLogs",
"log_stream_name": "{instance_id}/laravel",
"timezone": "UTC"
}
]
}
}
}
}
Install the CloudWatch Agent on your EC2 instances and point it to this configuration file. You can then create CloudWatch Alarms based on these custom metrics (e.g., MyLaravelApp/EC2/mem_used_percent).
CloudWatch Container Insights for ECS/EKS
For containerized applications, CloudWatch Container Insights simplifies the collection of performance metrics and logs from your ECS or EKS clusters. It automatically collects:
- Cluster-level metrics (CPU, memory utilization).
- Service-level metrics (task counts, desired vs. running tasks).
- Task-level metrics (individual container CPU/memory usage).
- Pod-level metrics (for EKS).
- Container logs.
Enabling Container Insights is done via the AWS console or AWS CLI when creating or updating your ECS cluster or EKS cluster configuration. Once enabled, you can view these metrics and logs directly within the CloudWatch console under the “Container Insights” section.
You can also create CloudWatch Alarms based on Container Insights metrics, such as high CPU or memory utilization for specific services or tasks.
Conclusion: A Holistic Monitoring Strategy
A robust server monitoring strategy for a Laravel application on AWS involves a layered approach. It starts with foundational RDS metrics and Performance Insights for database health, extends to application-level instrumentation with Prometheus and Grafana for deep visibility into Laravel’s performance, and is complemented by system-level metrics from CloudWatch Agents or Container Insights. Proactive alerting via Alertmanager ensures that your team is notified of issues before they impact users. This comprehensive strategy is key to maintaining high availability and performance for your critical applications.