Server Monitoring Best Practices: Keeping Your Laravel App and DynamoDB Clusters Alive on Google Cloud
Proactive Laravel Application Health Checks
Maintaining the health of a Laravel application deployed on Google Cloud Platform (GCP) requires a multi-layered monitoring strategy. Beyond basic uptime checks, we need to ensure the application is not just running, but also performing optimally and responding to requests within acceptable latency. This involves instrumenting the application itself to expose key metrics.
A common and effective approach is to create a dedicated health check endpoint within your Laravel application. This endpoint should perform critical checks: database connectivity, cache accessibility, and the status of any essential external services. We’ll expose this endpoint via a public URL, but secure it appropriately.
Implementing a Laravel Health Check Endpoint
Let’s define a route and a controller for our health check. This example assumes you have a `HealthCheckController` and a `health` route.
Route Definition (routes/api.php)
<?php
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\HealthCheckController;
Route::middleware('auth:sanctum')->get('/health', [HealthCheckController::class, 'index']);
Health Check Controller (app/Http/Controllers/HealthCheckController.php)
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Artisan;
class HealthCheckController extends Controller
{
/**
* Perform comprehensive health checks.
*
* @return \Illuminate\Http\JsonResponse
*/
public function index()
{
$checks = [];
$status = 'healthy';
// 1. Database Connection Check
try {
DB::connection()->getPdo();
$checks['database'] = 'connected';
} catch (\Exception $e) {
$status = 'unhealthy';
$checks['database'] = 'connection_failed: ' . $e->getMessage();
Log::error('Database connection failed during health check: ' . $e->getMessage());
}
// 2. Cache Connection Check (using default cache driver)
try {
$cacheKey = 'health_check_cache_test_' . uniqid();
Cache::put($cacheKey, 'test', 1);
if (Cache::get($cacheKey) === 'test') {
$checks['cache'] = 'accessible';
Cache::forget($cacheKey); // Clean up
} else {
$status = 'unhealthy';
$checks['cache'] = 'failed_to_read_write';
Log::error('Cache accessibility failed during health check.');
}
} catch (\Exception $e) {
$status = 'unhealthy';
$checks['cache'] = 'connection_failed: ' . $e->getMessage();
Log::error('Cache connection failed during health check: ' . $e->getMessage());
}
// 3. Optional: Check essential external services (e.g., an external API)
// Example: Check if a critical third-party API is reachable
/*
try {
$client = new \GuzzleHttp\Client();
$response = $client->get('https://api.example.com/status', ['timeout' => 5]);
if ($response->getStatusCode() === 200) {
$checks['external_service'] = 'reachable';
} else {
$status = 'unhealthy';
$checks['external_service'] = 'unreachable_status_code: ' . $response->getStatusCode();
Log::warning('External service check returned non-200 status code.');
}
} catch (\Exception $e) {
$status = 'unhealthy';
$checks['external_service'] = 'connection_failed: ' . $e->getMessage();
Log::error('External service check failed: ' . $e->getMessage());
}
*/
// 4. Optional: Run a quick Artisan command (e.g., queue:listen status, though this is tricky for a simple health check)
// A better approach for queue health is dedicated queue monitoring.
// For a simple check, we might just ensure Artisan itself can run.
try {
Artisan::call('cache:clear', [], null, null, false, true); // Example, use with caution
$checks['artisan_run'] = 'successful';
} catch (\Exception $e) {
$status = 'unhealthy';
$checks['artisan_run'] = 'failed: ' . $e->getMessage();
Log::error('Artisan command execution failed during health check: ' . $e->getMessage());
}
return response()->json([
'status' => $status,
'checks' => $checks,
'timestamp' => now()->toIso8601String(),
], $status === 'healthy' ? 200 : 503); // 503 Service Unavailable for unhealthy
}
}
Security Note: The `auth:sanctum` middleware ensures only authenticated users can access this endpoint. For GCP’s load balancer health checks, you’ll need to configure a separate, unauthenticated endpoint or use IP whitelisting. A common pattern is to have a public `/liveness` or `/readiness` endpoint that performs minimal checks (e.g., just checks if the web server is responding) and a more comprehensive, authenticated `/health` endpoint for internal monitoring tools.
GCP Load Balancer Health Checks for Laravel
Google Cloud Load Balancing (GCLB) is crucial for distributing traffic and performing automated health checks on your backend instances. We’ll configure GCLB to periodically ping a specific endpoint on your Laravel application.
Configuring the Health Check in GCP Console
Navigate to your Load Balancer configuration in the GCP Console. Under the “Backend services” section, you’ll find or create a health check. Here are the key parameters:
- Protocol: HTTP (or HTTPS if your app uses it directly)
- Port: 80 (or 443)
- Request path: This is critical. For a public, unauthenticated check, you might create a simple route like
/livenessthat returns a 200 OK. If you’re using IP whitelisting for your monitoring tools, you could point to the authenticated/healthendpoint. For simplicity, let’s assume a public/livenessendpoint. - Check interval: How often to check (e.g., 5s, 10s).
- Timeout: How long to wait for a response (e.g., 5s).
- Healthy threshold: Number of consecutive successes to mark an instance as healthy (e.g., 2).
- Unhealthy threshold: Number of consecutive failures to mark an instance as unhealthy (e.g., 3).
Example Public Liveness Endpoint (routes/web.php or routes/api.php)
<?php
use Illuminate\Support\Facades\Route;
Route::get('/liveness', function () {
return response('OK', 200);
});
This minimal endpoint ensures that the web server is up and responding. The more comprehensive checks from the HealthCheckController should be polled by a separate monitoring system (like Cloud Monitoring or a dedicated APM tool).
Monitoring DynamoDB with Cloud Monitoring and Custom Metrics
DynamoDB, being a managed NoSQL database, offers extensive built-in metrics through AWS CloudWatch. However, for a unified view within GCP, we can leverage Google Cloud Monitoring (formerly Stackdriver) to ingest these metrics or use custom metrics for application-specific insights.
Ingesting AWS CloudWatch Metrics into Google Cloud Monitoring
Google Cloud’s operations suite can ingest metrics from other cloud providers. This is typically done via the Cloud Monitoring API or by setting up a bridge.
Method 1: Using the Cloud Monitoring API (Programmatic Ingestion)
You can write a script (e.g., in Python) that runs on a VM or Cloud Function, fetches metrics from AWS CloudWatch using the AWS SDK, and then pushes them to Google Cloud Monitoring using the Cloud Monitoring client library.
import boto3
from google.cloud import monitoring_v3
import google.auth
import time
from datetime import datetime, timedelta
# AWS Configuration
aws_region = 'us-east-1'
dynamodb_table_name = 'your-dynamodb-table-name'
cloudwatch_metric_names = [
'ConsumedReadCapacityUnits',
'ConsumedWriteCapacityUnits',
'SuccessfulRequestLatency',
'ThrottledRequests',
'ProvisionedReadCapacityUnits',
'ProvisionedWriteCapacityUnits',
]
# GCP Configuration
project_id = google.auth.default()[1] # Gets project ID from environment or gcloud config
client = monitoring_v3.MetricServiceClient()
project_name = f"projects/{project_id}"
# Initialize AWS CloudWatch client
cloudwatch = boto3.client('cloudwatch', region_name=aws_region)
def get_dynamodb_metrics(metric_name, start_time, end_time, period=300):
try:
response = cloudwatch.get_metric_statistics(
Namespace='AWS/DynamoDB',
MetricName=metric_name,
Dimensions=[
{
'Name': 'TableName',
'Value': dynamodb_table_name
},
],
StartTime=start_time,
EndTime=end_time,
Period=period,
Statistics=['Sum', 'Average', 'Maximum'] # Adjust as needed
)
return response['Datapoints']
except Exception as e:
print(f"Error fetching CloudWatch metric {metric_name}: {e}")
return []
def create_gcp_time_series(metric_type, datapoints, timestamp_unit='s'):
series = monitoring_v3.TimeSeries()
series.metric.type = f"custom.googleapis.com/dynamodb/{metric_type}"
series.resource.type = "gce_instance" # Or appropriate resource type
series.resource.labels["instance_id"] = "your-instance-id" # Replace with actual instance ID or use global resource
series.resource.labels["project_id"] = project_id
for dp in datapoints:
point = monitoring_v3.Point()
point.value.double_value = dp['Average'] # Or Sum, Maximum based on metric
point.interval.end_time.seconds = int(dp['Timestamp'].timestamp())
point.interval.end_time.nanos = 0
series.points.append(point)
return series
def main():
end_time = datetime.utcnow()
start_time = end_time - timedelta(minutes=5) # Fetch last 5 minutes of data
all_time_series = []
for metric_name in cloudwatch_metric_names:
datapoints = get_dynamodb_metrics(metric_name, start_time, end_time)
if datapoints:
# Map CloudWatch metric names to GCP custom metric types
metric_type_map = {
'ConsumedReadCapacityUnits': 'ConsumedReadCapacityUnits_Average',
'ConsumedWriteCapacityUnits': 'ConsumedWriteCapacityUnits_Average',
'SuccessfulRequestLatency': 'SuccessfulRequestLatency_Average',
'ThrottledRequests': 'ThrottledRequests_Sum', # ThrottledRequests is usually a count
'ProvisionedReadCapacityUnits': 'ProvisionedReadCapacityUnits_Average',
'ProvisionedWriteCapacityUnits': 'ProvisionedWriteCapacityUnits_Average',
}
gcp_metric_type = metric_type_map.get(metric_name, metric_name)
time_series = create_gcp_time_series(gcp_metric_type, datapoints)
all_time_series.append(time_series)
if all_time_series:
try:
client.create_time_series(name=project_name, time_series=all_time_series)
print(f"Successfully wrote {len(all_time_series)} time series to Cloud Monitoring.")
except Exception as e:
print(f"Error writing to Cloud Monitoring: {e}")
if __name__ == "__main__":
main()
This script needs to be scheduled to run periodically (e.g., every minute) using cron or Cloud Scheduler. Ensure the service account running this script has the necessary IAM permissions: monitoring.timeSeries.create and permissions to access AWS CloudWatch.
Method 2: Using Third-Party Connectors or Integrations
Several third-party tools and services specialize in cross-cloud monitoring and can simplify this process. Tools like Datadog, Dynatrace, or even specific GCP Marketplace solutions might offer pre-built integrations for ingesting AWS metrics.
Custom DynamoDB Metrics for Laravel Application Performance
While AWS provides low-level metrics, your Laravel application might benefit from higher-level metrics that correlate application behavior with DynamoDB performance. You can use the AWS SDK within your Laravel application to emit custom metrics to CloudWatch, which can then be ingested into GCP Monitoring.
<?php
namespace App\Services;
use Aws\CloudWatch\CloudWatchClient;
use Aws\Exception\AwsException;
use Illuminate\Support\Facades\Log;
class DynamoDbMetricService
{
protected $cloudWatch;
protected $tableName;
public function __construct()
{
$this->cloudWatch = new CloudWatchClient([
'region' => config('aws.dynamodb.region'), // e.g., 'us-east-1'
'version' => 'latest',
'credentials' => [ // Ensure these are properly configured for your environment
'key' => config('aws.credentials.key'),
'secret' => config('aws.credentials.secret'),
],
]);
$this->tableName = config('aws.dynamodb.table_name');
}
/**
* Emits a custom metric to AWS CloudWatch.
*
* @param string $metricName The name of the metric (e.g., 'UserFetchLatency').
* @param float $value The metric value.
* @param array $dimensions Additional dimensions for the metric.
* @param string $unit The unit of the metric (e.g., 'Milliseconds', 'Count').
*/
public function putMetric(string $metricName, float $value, array $dimensions = [], string $unit = 'None')
{
$defaultDimensions = [
['Name' => 'TableName', 'Value' => $this->tableName],
];
$allDimensions = array_merge($defaultDimensions, $dimensions);
try {
$this->cloudWatch->putMetricData([
'Namespace' => 'MyApp/DynamoDB', // Custom namespace for your app
'MetricData' => [
[
'MetricName' => $metricName,
'Dimensions' => $allDimensions,
'Timestamp' => new \DateTime(),
'Value' => $value,
'Unit' => $unit,
],
],
]);
Log::debug("CloudWatch metric '$metricName' emitted successfully.");
} catch (AwsException $e) {
Log::error("Failed to emit CloudWatch metric '$metricName': " . $e->getMessage());
}
}
/**
* Example: Measure latency for a DynamoDB operation.
*
* @param string $operationName e.g., 'GetItem', 'PutItem'
* @param callable $callback The function to execute and time.
* @return mixed The result of the callback.
*/
public function measureLatency(string $operationName, callable $callback)
{
$startTime = microtime(true);
$result = null;
try {
$result = $callback();
$endTime = microtime(true);
$latencyMs = ($endTime - $startTime) * 1000;
$this->putMetric(
'OperationLatency',
$latencyMs,
[['Name' => 'Operation', 'Value' => $operationName]],
'Milliseconds'
);
} catch (\Exception $e) {
$endTime = microtime(true);
$latencyMs = ($endTime - $startTime) * 1000;
$this->putMetric(
'OperationLatency',
$latencyMs,
[['Name' => 'Operation', 'Value' => $operationName], ['Name' => 'Status', 'Value' => 'Error']],
'Milliseconds'
);
Log::error("DynamoDB operation '$operationName' failed: " . $e->getMessage());
throw $e; // Re-throw the exception
}
return $result;
}
/**
* Example: Count successful or failed operations.
*/
public function countOperation(string $operationName, bool $success = true)
{
$this->putMetric(
'OperationCount',
1, // Increment count by 1
[['Name' => 'Operation', 'Value' => $operationName], ['Name' => 'Status', 'Value' => $success ? 'Success' : 'Failure']],
'Count'
);
}
}
You would then use this service within your repository or service classes:
<?php
namespace App\Repositories;
use App\Services\DynamoDbMetricService;
use Illuminate\Support\Facades\Log;
class UserRepository
{
protected $dynamoDbMetricService;
// ... other dependencies
public function __construct(DynamoDbMetricService $dynamoDbMetricService)
{
$this->dynamoDbMetricService = $dynamoDbMetricService;
// ...
}
public function findById(string $userId)
{
try {
$result = $this->dynamoDbMetricService->measureLatency('GetItem', function () use ($userId) {
// Actual DynamoDB GetItem call here
// e.g., $item = $this->dynamoDbClient->getItem([...]);
// For demonstration:
sleep(rand(50, 200) / 1000); // Simulate latency
$item = ['id' => $userId, 'name' => 'Example User']; // Simulated item
return $item;
});
if ($result) {
$this->dynamoDbMetricService->countOperation('GetItem', true);
return $result;
} else {
$this->dynamoDbMetricService->countOperation('GetItem', false);
return null;
}
} catch (\Exception $e) {
Log::error("Error fetching user by ID {$userId}: " . $e->getMessage());
$this->dynamoDbMetricService->countOperation('GetItem', false);
return null;
}
}
}
These custom metrics, once in CloudWatch, can be ingested into GCP Monitoring, allowing you to correlate application-level performance issues with specific DynamoDB operations and latencies.
Leveraging Google Cloud Monitoring for Alerts and Dashboards
Once metrics are flowing into Google Cloud Monitoring, the real power lies in creating actionable alerts and informative dashboards.
Setting Up Alerting Policies
Alerting policies in Cloud Monitoring notify you when specific conditions are met. For your Laravel app and DynamoDB:
- High Latency: Alert if the average
SuccessfulRequestLatencyfor a DynamoDB table exceeds a threshold (e.g., > 100ms) for 5 minutes. - Throttling: Alert if
ThrottledRequestsfor a DynamoDB table is greater than 0 for 1 minute. - Application Errors: Monitor your Laravel application’s error logs (ingested via Cloud Logging) and alert on a spike in
ERRORlevel logs. - Unhealthy Instances: Alert if the GCLB health check reports an instance as unhealthy for more than 2 consecutive checks.
- Resource Utilization: Monitor CPU, memory, and network utilization of your GCE instances running Laravel.
- Custom Application Metrics: Alert on custom metrics like
MyApp/DynamoDB/OperationCountwhereStatus=Failureexceeds a certain rate.
Example Alerting Policy Configuration (Conceptual)
In the GCP Console, navigate to “Monitoring” -> “Alerting”. Create a new policy:
- Condition Type: Metric Threshold
- Resource Type: GCE VM Instance (for Laravel app) or DynamoDB Table (if metrics are ingested)
- Metric: Select the relevant metric (e.g.,
CPU utilization,ConsumedReadCapacityUnits, or your customcustom.googleapis.com/dynamodb/ConsumedReadCapacityUnits_Average). - Filter: Specify your Laravel instances or DynamoDB table.
- Trigger: Threshold is
abovea value (e.g., 80% for CPU) orabovea value for a duration (e.g.,ConsumedReadCapacityUnits> 1000 for 5 minutes). - Notification Channels: Configure email, Slack, PagerDuty, etc.
Building Dashboards for Visibility
Dashboards provide a consolidated view of your system’s health. Create dashboards that include:
- GCLB Health Status: A chart showing the number of healthy vs. unhealthy backend instances.
- Laravel App Performance: Request latency, error rates (from logs), and resource utilization (CPU, memory) for your GCE instances.
- DynamoDB Performance: Key metrics like
ConsumedReadCapacityUnits,ConsumedWriteCapacityUnits,SuccessfulRequestLatency, andThrottledRequests. Include both AWS-provided and your custom application metrics. - Key Application Metrics: Visualizations of critical business metrics that rely on DynamoDB.
By combining application-level health checks, robust load balancer configurations, and comprehensive cloud monitoring with custom metrics, you can build a resilient and observable system for your Laravel application and DynamoDB clusters on Google Cloud.