# Laravel Spy Laravel Spy is a lightweight Laravel package designed to track and log all outgoing HTTP requests made by your Laravel application. It provides zero-configuration monitoring of external API calls and HTTP requests, capturing comprehensive details including URLs, methods, headers, request/response bodies, and status codes. The package operates transparently by hooking into Laravel's HTTP client middleware, requiring no code changes to existing applications. This package is particularly useful for debugging third-party API integrations, monitoring application behavior, auditing external communications, and troubleshooting production issues. It includes built-in features for data obfuscation to protect sensitive information like passwords and API keys, automatic log retention management, content-type filtering to exclude binary data, and an optional web dashboard for visualizing HTTP traffic patterns and failure rates. ## Installation and Setup Install the package via Composer and publish its configuration and migrations. ```bash # Install the package composer require farayaz/laravel-spy # Publish configuration file to config/spy.php php artisan vendor:publish --provider="Farayaz\LaravelSpy\LaravelSpyServiceProvider" # Run migrations to create http_logs table php artisan migrate ``` ## Basic HTTP Request Tracking Automatically log all outgoing HTTP requests with zero configuration. ```php use Illuminate\Support\Facades\Http; // After installing Laravel Spy, this request is automatically logged $response = Http::get('https://api.example.com/users'); // POST requests with data are also logged $response = Http::post('https://api.example.com/users', [ 'name' => 'John Doe', 'email' => 'john@example.com', 'password' => 'secret123', // Will be obfuscated in logs ]); // Complex requests with headers $response = Http::withHeaders([ 'Authorization' => 'Bearer token123', // Will be obfuscated 'Accept' => 'application/json', ])->post('https://api.example.com/data', [ 'api_key' => 'sk_test_123', // Will be obfuscated 'payload' => ['foo' => 'bar'], ]); // Failed requests are also logged with status 0 try { $response = Http::timeout(5)->get('https://slow-api.example.com/endpoint'); } catch (\Exception $e) { // Exception is logged with error message in response_body } ``` ## Environment Configuration Configure Laravel Spy behavior via environment variables. ```bash # .env file configuration # Enable/disable tracking SPY_ENABLED=true # Database configuration SPY_TABLE_NAME=http_logs SPY_DB_CONNECTION=mysql # Exclude specific URLs from logging (comma-separated) SPY_EXCLUDE_URLS=api/health,ping,status,metrics # Obfuscate sensitive fields (comma-separated) SPY_OBFUSCATES=password,token,api_key,secret,authorization,bearer # Custom obfuscation mask (default: 🫣) SPY_OBFUSCATION_MASK=***REDACTED*** # Auto-cleanup configuration (days) SPY_CLEAN_DAYS=30 # Exclude binary content types from request bodies SPY_REQUEST_BODY_EXCLUDE_CONTENT_TYPES=image/,video/,audio/,application/pdf # Exclude binary content types from response bodies SPY_RESPONSE_BODY_EXCLUDE_CONTENT_TYPES=image/,video/,application/pdf,application/zip # Dashboard configuration SPY_DASHBOARD_ENABLED=true SPY_DASHBOARD_PREFIX=spy SPY_DASHBOARD_MIDDLEWARE=web,auth ``` ## Configuration File Usage Programmatic configuration via config/spy.php file. ```php // config/spy.php return [ // Enable/disable spy functionality 'enabled' => env('SPY_ENABLED', true), // Database table name 'table_name' => env('SPY_TABLE_NAME', 'http_logs'), // Database connection (null = default) 'db_connection' => env('SPY_DB_CONNECTION', null), // URLs to exclude from logging 'exclude_urls' => [ 'api/health', 'webhook/internal', 'metrics', ], // Fields to obfuscate 'obfuscates' => [ 'password', 'token', 'api_key', 'secret', 'authorization', 'client_secret', ], // Obfuscation mask 'obfuscation_mask' => '🫣', // Retention period in days 'clean_days' => 30, // Content types to exclude from request body logging 'request_body_exclude_content_types' => [ 'image/', 'video/', 'application/pdf', ], // Content types to exclude from response body logging 'response_body_exclude_content_types' => [ 'video/', 'application/pdf', ], // Dashboard settings 'dashboard' => [ 'enabled' => true, 'prefix' => 'spy', 'middleware' => ['web', 'auth'], ], ]; ``` ## HttpLog Model Access and query logged HTTP requests using the HttpLog Eloquent model. ```php use Farayaz\LaravelSpy\Models\HttpLog; use Illuminate\Support\Carbon; // Get all logs $logs = HttpLog::all(); // Get recent logs (last 24 hours) $recentLogs = HttpLog::where('created_at', '>=', Carbon::now()->subDay()) ->orderBy('created_at', 'desc') ->get(); // Find failed requests (5xx errors) $failures = HttpLog::where('status', '>=', 500) ->orWhere('status', 0) // Connection failures ->get(); foreach ($failures as $log) { echo "Failed request to {$log->url}\n"; echo "Method: {$log->method}\n"; echo "Status: {$log->status}\n"; echo "Error: " . json_encode($log->response_body) . "\n\n"; } // Find requests to specific URL $apiLogs = HttpLog::where('url', 'like', '%api.stripe.com%') ->where('created_at', '>=', Carbon::now()->subWeek()) ->get(); // Get successful requests only $successfulRequests = HttpLog::whereBetween('status', [200, 299])->get(); // Access log data $log = HttpLog::first(); echo $log->url; // "https://api.example.com/users" echo $log->method; // "POST" echo $log->status; // 200 var_dump($log->request_headers); // Array of headers var_dump($log->request_body); // Array/string of request data var_dump($log->response_headers); // Array of headers var_dump($log->response_body); // Array/string of response data echo $log->created_at; // Carbon instance // Count requests by status code $stats = [ '2xx' => HttpLog::whereBetween('status', [200, 299])->count(), '4xx' => HttpLog::whereBetween('status', [400, 499])->count(), '5xx' => HttpLog::where('status', '>=', 500)->count(), 'failed' => HttpLog::where('status', 0)->count(), ]; ``` ## Clean Command Remove old HTTP logs using the artisan command. ```bash # Clean logs based on config (default: 30 days) php artisan spy:clean # Clean logs older than 7 days php artisan spy:clean --days=7 # Clean logs for specific URL pattern php artisan spy:clean --url=api/users # Combine filters: delete old logs matching URL php artisan spy:clean --days=1 --url=webhook # Force cleanup in production (requires confirmation otherwise) php artisan spy:clean --force # Examples of what gets cleaned: # php artisan spy:clean --days=30 --url=stripe.com # Deletes all logs containing "stripe.com" in URL older than 30 days # php artisan spy:clean --days=0 --url=test # Deletes all logs containing "test" regardless of age ``` ## Scheduled Log Cleanup Automate log cleanup using Laravel's task scheduler. ```php // app/Console/Kernel.php namespace App\Console; use Illuminate\Console\Scheduling\Schedule; use Illuminate\Foundation\Console\Kernel as ConsoleKernel; class Kernel extends ConsoleKernel { protected function schedule(Schedule $schedule) { // Clean logs daily at 2 AM (uses config: spy.clean_days) $schedule->command('spy:clean')->dailyAt('02:00'); // Clean logs older than 7 days, every week $schedule->command('spy:clean --days=7')->weekly(); // Clean specific URL patterns daily $schedule->command('spy:clean --days=1 --url=webhook')->daily(); // Aggressive cleanup: remove test data hourly $schedule->command('spy:clean --days=0 --url=test')->hourly(); // Multiple cleanup strategies $schedule->command('spy:clean --days=90')->monthly(); // Long-term $schedule->command('spy:clean --days=7')->weekly(); // Medium-term $schedule->command('spy:clean --days=1 --url=debug')->daily(); // Debug logs } } ``` ## Dashboard Access View HTTP traffic analytics through the built-in web dashboard. ```php // routes/web.php - Dashboard is automatically registered at /spy // Access at: http://your-app.test/spy // Configure dashboard in .env // SPY_DASHBOARD_ENABLED=true // SPY_DASHBOARD_PREFIX=spy // SPY_DASHBOARD_MIDDLEWARE=web,auth // Or in config/spy.php: return [ 'dashboard' => [ 'enabled' => true, 'prefix' => 'spy', // URL: /spy 'middleware' => ['web', 'auth'], // Require authentication ], ]; // Custom middleware for advanced access control // config/spy.php return [ 'dashboard' => [ 'enabled' => true, 'prefix' => 'admin/http-logs', // URL: /admin/http-logs 'middleware' => ['web', 'auth', 'admin.only'], ], ]; // Disable dashboard in production // .env.production // SPY_DASHBOARD_ENABLED=false // The dashboard shows: // - Total requests in period (24h, 7d, 30d) // - Success rate (2xx, 4xx, 5xx) // - Top failing URLs with 500+ errors // - Time-series chart of request volume // - Request statistics grouped by time bucket ``` ## Data Obfuscation Automatically obfuscate sensitive data in logs to protect credentials. ```php use Illuminate\Support\Facades\Http; // Configure obfuscation in .env // SPY_OBFUSCATES=password,token,api_key,secret // SPY_OBFUSCATION_MASK=***HIDDEN*** // Request with sensitive data $response = Http::post('https://api.example.com/auth', [ 'username' => 'john@example.com', // Logged as-is 'password' => 'secret123', // Logged as: ***HIDDEN*** 'api_key' => 'sk_live_abc123', // Logged as: ***HIDDEN*** 'metadata' => [ 'user_id' => 42, // Logged as-is 'token' => 'refresh_token_xyz', // Logged as: ***HIDDEN*** ], ]); // Headers are also obfuscated $response = Http::withHeaders([ 'Authorization' => 'Bearer secret_token', // Logged as: Bearer ***HIDDEN*** 'X-API-Key' => 'key_123', // Logged as: ***HIDDEN*** 'Content-Type' => 'application/json', // Logged as-is ])->get('https://api.example.com/data'); // URL query parameters are obfuscated $response = Http::get('https://api.example.com/users?token=abc123&page=1'); // URL logged as: https://api.example.com/users?token=***HIDDEN***&page=1 // Nested obfuscation works recursively $response = Http::post('https://api.example.com/nested', [ 'user' => [ 'email' => 'user@example.com', 'password' => 'pass123', // Obfuscated 'profile' => [ 'bio' => 'Developer', 'secret' => 'value', // Obfuscated ], ], ]); // Case-insensitive obfuscation $response = Http::post('https://api.example.com/data', [ 'Password' => 'test', // Obfuscated 'PASSWORD' => 'test2', // Obfuscated 'password' => 'test3', // Obfuscated ]); ``` ## Content-Type Filtering Exclude specific content types from being logged to prevent binary data storage. ```bash # .env configuration # Exclude images from request body logging SPY_REQUEST_BODY_EXCLUDE_CONTENT_TYPES=image/,video/ # Exclude PDFs and videos from response body logging SPY_RESPONSE_BODY_EXCLUDE_CONTENT_TYPES=video/,application/pdf,application/zip ``` ```php use Illuminate\Support\Facades\Http; // Image upload - request body excluded from logging $response = Http::attach( 'avatar', file_get_contents('/path/to/image.jpg'), 'avatar.jpg' )->post('https://api.example.com/upload'); // Request body logged as: ["content excluded by configuration"] // PDF download - response body excluded from logging $response = Http::get('https://api.example.com/invoice.pdf'); // Response body logged as: ["content excluded by configuration"] // JSON data - logged normally $response = Http::post('https://api.example.com/users', [ 'name' => 'John Doe', ]); // Request body logged as: {"name": "John Doe"} // XML data - parsed and logged $response = Http::withHeaders(['Content-Type' => 'application/xml']) ->send('POST', 'https://api.example.com/soap', [ 'body' => 'John' ]); // Request body logged as parsed XML array // Multipart form data - base64 encoded $response = Http::asMultipart()->post('https://api.example.com/form', [ ['name' => 'field', 'contents' => 'value'], ]); // Request body logged as base64 encoded string ``` ## URL Exclusion Prevent specific URLs from being logged to reduce noise. ```bash # .env configuration # Exclude health checks, webhooks, and monitoring endpoints SPY_EXCLUDE_URLS=api/health,webhook/internal,metrics,ping,/status ``` ```php use Illuminate\Support\Facades\Http; // These requests are NOT logged (excluded by config) Http::get('https://api.example.com/api/health'); Http::post('https://api.example.com/webhook/internal/process'); Http::get('https://monitor.example.com/metrics'); // These requests ARE logged (not excluded) Http::get('https://api.example.com/api/users'); Http::post('https://api.example.com/api/orders'); // Programmatic exclusion in config/spy.php return [ 'exclude_urls' => [ 'health', 'heartbeat', 'webhook/internal', 'debug', 'test', ], ]; // Partial URL matching works // Config: SPY_EXCLUDE_URLS=stripe.com/health Http::get('https://api.stripe.com/health/check'); // Not logged Http::get('https://api.stripe.com/v1/charges'); // Logged ``` ## Custom Log Analysis Analyze HTTP logs programmatically for monitoring and alerting. ```php use Farayaz\LaravelSpy\Models\HttpLog; use Illuminate\Support\Carbon; use Illuminate\Support\Facades\DB; // Find all failed requests in last hour $recentFailures = HttpLog::where('created_at', '>=', Carbon::now()->subHour()) ->where(function ($query) { $query->where('status', '>=', 500) ->orWhere('status', 0); }) ->orderBy('created_at', 'desc') ->get(); if ($recentFailures->count() > 10) { // Alert: too many failures \Log::critical('High failure rate detected', [ 'count' => $recentFailures->count(), 'urls' => $recentFailures->pluck('url')->unique(), ]); } // Calculate average response times by endpoint $endpoints = HttpLog::select('url', DB::raw('AVG(TIMESTAMPDIFF(SECOND, created_at, updated_at)) as avg_seconds')) ->where('created_at', '>=', Carbon::now()->subDay()) ->whereBetween('status', [200, 299]) ->groupBy('url') ->orderByDesc('avg_seconds') ->get(); // Find most frequently called APIs $topApis = HttpLog::select('url', DB::raw('COUNT(*) as call_count')) ->where('created_at', '>=', Carbon::now()->subWeek()) ->groupBy('url') ->orderByDesc('call_count') ->limit(10) ->get(); // Detect rate limit errors (429 status) $rateLimited = HttpLog::where('status', 429) ->where('created_at', '>=', Carbon::now()->subDay()) ->get() ->groupBy('url'); foreach ($rateLimited as $url => $logs) { echo "Rate limited on {$url}: {$logs->count()} times\n"; } // Find slow external APIs (comparing request/response timing) $slowRequests = HttpLog::whereBetween('status', [200, 299]) ->where('created_at', '>=', Carbon::now()->subHour()) ->get() ->filter(function ($log) { return $log->created_at->diffInSeconds($log->updated_at) > 5; }); // Export logs to CSV for analysis $logs = HttpLog::where('created_at', '>=', Carbon::now()->subWeek())->get(); $csv = fopen('/tmp/http_logs.csv', 'w'); fputcsv($csv, ['Timestamp', 'URL', 'Method', 'Status', 'Response Time']); foreach ($logs as $log) { fputcsv($csv, [ $log->created_at, $log->url, $log->method, $log->status, $log->created_at->diffInSeconds($log->updated_at), ]); } fclose($csv); ``` ## Summary Laravel Spy provides comprehensive HTTP request tracking for Laravel applications with minimal setup and zero code changes. The package is ideal for debugging third-party API integrations, monitoring production traffic, identifying failing external services, and auditing application behavior. It automatically captures all outgoing HTTP requests made through Laravel's HTTP client facade, storing detailed information about URLs, methods, headers, request/response bodies, and status codes in a database table. The package excels in production environments with features like automatic data obfuscation to protect sensitive credentials, configurable content-type filtering to exclude binary data, URL-based exclusion rules to reduce noise, and scheduled log cleanup to manage database growth. The optional web dashboard provides real-time visibility into HTTP traffic patterns, success rates, and failure trends. Integration requires only installing the package, running migrations, and optionally configuring environment variables for custom behavior. The stored logs are accessible via an Eloquent model for programmatic analysis, custom reporting, and integration with existing monitoring systems.