Skip to main content
Laravel, shipping fast.

Chapter 8

Monitoring & Observability

Julian Beaujardin

You can't fix what you can't see. Your API is running, so you need to know whether it is healthy, whether requests are getting slower, and whether errors are happening.

Monitoring gives you visibility. It tells you when things are going wrong before your users notice. It lets you optimize based on real data, not guesses.

This chapter is about instrumenting your API so you know what's happening at all times.

Health Check Endpoint

Route::get('/health', HealthController::class);

class HealthController extends Controller
{
    public function __invoke(): Response
    {
        $health = [
            'status' => 'ok',
            'timestamp' => now()->toIso8601String(),
            'services' => [
                'database' => $this->checkDatabase(),
                'cache' => $this->checkCache(),
                'queue' => $this->checkQueue(),
            ],
        ];

        $allHealthy = collect($health['services'])
            ->every(fn ($service) => $service['status'] !== 'down');

        return response()->json(
            ['data' => $health],
            $allHealthy ? 200 : 503
        );
    }

    private function checkDatabase(): array
    {
        try {
            DB::select('SELECT 1');
            return ['status' => 'connected'];
        } catch (Exception $e) {
            return ['status' => 'down', 'error' => $e->getMessage()];
        }
    }

    private function checkCache(): array
    {
        try {
            Cache::put('health_check', true, now()->addMinute());
            Cache::forget('health_check');
            return ['status' => 'connected'];
        } catch (Exception $e) {
            return ['status' => 'down', 'error' => $e->getMessage()];
        }
    }

    private function checkQueue(): array
    {
        try {
            Queue::connection()->connection();
            return ['status' => 'active'];
        } catch (Exception $e) {
            return ['status' => 'down'];
        }
    }
}

Request/Response Logging

Log all requests for analysis:

class LogApiRequestsMiddleware
{
    public function handle(Request $request, Closure $next): Response
    {
        // Skip noise
        if ($request->is('*/health')) {
            return $next($request);
        }

        $start = microtime(true);
        $response = $next($request);
        $duration = round((microtime(true) - $start) * 1000);

        Log::channel('api')->info('API Request', [
            'timestamp' => now()->toIso8601String(),
            'method' => $request->getMethod(),
            'path' => $request->getPathInfo(),
            'status' => $response->getStatusCode(),
            'duration_ms' => $duration,
            'user_id' => auth()->id(),
            'remote_ip' => $request->ip(),
        ]);

        return $response;
    }
}

Error Tracking Integration

Integrate with services like Sentry for automatic error reporting:

composer require sentry/sentry-laravel
php artisan sentry:publish
// config/sentry.php
return [
    'dsn' => env('SENTRY_LARAVEL_DSN'),
    'traces_sample_rate' => 0.1, // Sample 10% of requests
    'environment' => env('APP_ENV'),
];

// Exceptions automatically sent to Sentry
try {
    // Code
} catch (Exception $e) {
    \Sentry\captureException($e);
    throw $e;
}

Custom Metrics

Track application-specific metrics:

class MetricsService
{
    public function recordApiCall(string $endpoint, int $duration): void
    {
        Metrics::record('api_call_duration', $duration, [
            'endpoint' => $endpoint,
            'status' => 'success',
        ]);
    }

    public function recordError(string $code): void
    {
        Metrics::increment('api_errors_total', [
            'code' => $code,
        ]);
    }

    public function recordRateLimit(): void
    {
        Metrics::increment('rate_limits_hit');
    }
}

// Use in middleware
class LogApiRequestsMiddleware
{
    public function handle(Request $request, Closure $next): Response
    {
        $start = microtime(true);
        $response = $next($request);

        app(MetricsService::class)->recordApiCall(
            endpoint: $request->getPathInfo(),
            duration: (microtime(true) - $start) * 1000,
        );

        return $response;
    }
}

Alert Policy

Define alerts for critical issues:

Alert "High Error Rate"
When: Error rate > 5% in last 5 minutes
Then: Page on-call engineer

Alert "Slow API Response"
When: p95 latency > 500ms
Then: Page infrastructure team

Alert "Database Down"
When: Health check fails for >1 minute
Then: Page database team immediately

Chapter 8 Summary

Monitoring is visibility. You can't fix what you can't see.

Key instrumentation:

  1. Health checks for quick status
  2. Request/response logging with details
  3. Slow query logging
  4. Error tracking with stack traces
  5. Metrics on response times, error rates
  6. Alerts for critical issues

You don't need enterprise tools to start. Basic logging gets you 80% of the way there. The key is collecting the right data and acting on it.