friendship ended with social-app. php is my new best friend
1--- 2title: Logging 3description: Learn how to configure and use logging with the Fetch HTTP package 4--- 5 6# Logging 7 8The Fetch PHP package provides built-in support for logging HTTP requests and responses. This guide explains how to configure and use logging to help with debugging and monitoring. 9 10## PSR-3 Logger Integration 11 12The package integrates with any PSR-3 compatible logger, such as Monolog: 13 14```php 15use Fetch\Http\ClientHandler; 16use Monolog\Logger; 17use Monolog\Handler\StreamHandler; 18 19// Create a PSR-3 compatible logger 20$logger = new Logger('http'); 21$logger->pushHandler(new StreamHandler('logs/http.log', Logger::DEBUG)); 22 23// Set the logger on a client 24$client = ClientHandler::create(); 25$client->setLogger($logger); 26 27// Now all requests and responses will be logged 28$response = $client->get('https://api.example.com/users'); 29``` 30 31## Using Logger with Helper Functions 32 33You can also set a logger on the global client: 34 35```php 36use Monolog\Logger; 37use Monolog\Handler\StreamHandler; 38 39// Create a logger 40$logger = new Logger('http'); 41$logger->pushHandler(new StreamHandler('logs/http.log', Logger::DEBUG)); 42 43// Configure the global client with the logger 44$client = fetch_client(); 45$client->setLogger($logger); 46 47// All requests will now be logged 48$response = fetch('https://api.example.com/users'); 49``` 50 51## What Gets Logged 52 53The package logs the following events: 54 551. **Requests**: HTTP method, URI, and sanitized options 562. **Responses**: Status code, reason phrase, and timing 573. **Retry Attempts**: When retries occur due to errors 58 59### Request Logging 60 61``` 62[2023-01-15 14:30:10] http.DEBUG: Sending HTTP request {"method":"GET","uri":"https://api.example.com/users","options":{"timeout":30,"headers":{"User-Agent":"MyApp/1.0","Accept":"application/json"}}} 63``` 64 65### Response Logging 66 67``` 68[2023-01-15 14:30:11] http.DEBUG: Received HTTP response {"status_code":200,"reason":"OK","duration":0.532,"content_length":"1250"} 69``` 70 71### Retry Logging 72 73``` 74[2023-01-15 14:30:12] http.INFO: Retrying request {"attempt":1,"max_attempts":3,"uri":"https://api.example.com/unstable-endpoint","method":"GET","error":"Connection timed out","code":28} 75``` 76 77## Security and Sensitive Data 78 79The package automatically redacts sensitive information in logs: 80 81- Authentication headers are replaced with `[REDACTED]` 82- Basic auth credentials are replaced with `[REDACTED]` 83- Bearer tokens are replaced with `[REDACTED]` 84 85For example, this request: 86 87```php 88$client->withToken('secret-token') 89 ->withHeader('X-API-Key', 'private-key') 90 ->get('https://api.example.com/users'); 91``` 92 93Would be logged as: 94 95``` 96[2023-01-15 14:30:10] http.DEBUG: Sending HTTP request {"method":"GET","uri":"https://api.example.com/users","options":{"timeout":30,"headers":{"Authorization":"[REDACTED]","X-API-Key":"private-key"}}} 97``` 98 99## Custom Logging Configuration 100 101For more control over logging, you can configure different log levels for different events: 102 103```php 104use Monolog\Logger; 105use Monolog\Handler\StreamHandler; 106use Monolog\Handler\RotatingFileHandler; 107 108// Create a logger with multiple handlers 109$logger = new Logger('http'); 110 111// Debug level logs to a rotating file (1MB max size, keep 10 files) 112$logger->pushHandler(new RotatingFileHandler('logs/http-debug.log', 10, Logger::DEBUG)); 113 114// Info level and above goes to main log 115$logger->pushHandler(new StreamHandler('logs/http.log', Logger::INFO)); 116 117// Errors go to a separate file 118$logger->pushHandler(new StreamHandler('logs/http-error.log', Logger::ERROR)); 119 120// Set the logger 121$client = ClientHandler::create(); 122$client->setLogger($logger); 123``` 124 125## Logging Request and Response Bodies 126 127By default, the package doesn't log request or response bodies to avoid excessive log sizes and potential security issues. If you need this information for debugging, you can create a custom middleware: 128 129```php 130use GuzzleHttp\Middleware; 131use GuzzleHttp\HandlerStack; 132use GuzzleHttp\MessageFormatter; 133use GuzzleHttp\Client; 134use Fetch\Http\ClientHandler; 135use Monolog\Logger; 136use Monolog\Handler\StreamHandler; 137 138// Create a logger 139$logger = new Logger('http-verbose'); 140$logger->pushHandler(new StreamHandler('logs/http-verbose.log', Logger::DEBUG)); 141 142// Create a message formatter that includes bodies 143$formatter = new MessageFormatter( 144 "Request: {method} {uri} HTTP/{version}\n" . 145 "Request Headers: {req_headers}\n" . 146 "Request Body: {req_body}\n" . 147 "Response: HTTP/{version} {code} {phrase}\n" . 148 "Response Headers: {res_headers}\n" . 149 "Response Body: {res_body}" 150); 151 152// Create middleware with the formatter 153$middleware = Middleware::log($logger, $formatter, 'debug'); 154 155// Create a handler stack with the middleware 156$stack = HandlerStack::create(); 157$stack->push($middleware); 158 159// Create a Guzzle client with the stack 160$guzzleClient = new Client(['handler' => $stack]); 161 162// Create a ClientHandler with the custom Guzzle client 163$client = ClientHandler::createWithClient($guzzleClient); 164 165// Use the client 166$response = $client->post('https://api.example.com/users', [ 167 'name' => 'John Doe', 168 'email' => 'john@example.com' 169]); 170``` 171 172## Logging with Status Enums 173 174When logging with status codes, you can use the type-safe Status enum for better readability: 175 176```php 177use Fetch\Enum\Status; 178use Monolog\Logger; 179use Monolog\Handler\StreamHandler; 180 181$logger = new Logger('http'); 182$logger->pushHandler(new StreamHandler('logs/http.log', Logger::DEBUG)); 183 184// Custom log processing 185$logger->pushProcessor(function ($record) { 186 // If we have a response status in the context, convert it to a human-readable format 187 if (isset($record['context']['status_code'])) { 188 $statusCode = $record['context']['status_code']; 189 $statusEnum = Status::tryFrom($statusCode); 190 if ($statusEnum) { 191 $record['context']['status_text'] = $statusEnum->phrase(); 192 } 193 } 194 return $record; 195}); 196 197// Set the logger 198$client = ClientHandler::create(); 199$client->setLogger($logger); 200``` 201 202## Logging in Different Environments 203 204It's often useful to adjust logging behavior based on the environment: 205 206```php 207use Monolog\Logger; 208use Monolog\Handler\StreamHandler; 209use Monolog\Handler\NullHandler; 210use Monolog\Formatter\LineFormatter; 211use Fetch\Http\ClientHandler; 212 213function createLogger(): Logger 214{ 215 $env = getenv('APP_ENV') ?: 'production'; 216 $logger = new Logger('http'); 217 218 // Configure based on environment 219 switch ($env) { 220 case 'development': 221 // In development, log everything to stdout with details 222 $formatter = new LineFormatter( 223 "[%datetime%] %channel%.%level_name%: %message% %context% %extra%\n", 224 null, true, true 225 ); 226 $handler = new StreamHandler('php://stdout', Logger::DEBUG); 227 $handler->setFormatter($formatter); 228 $logger->pushHandler($handler); 229 break; 230 231 case 'testing': 232 // In testing, don't log anything 233 $logger->pushHandler(new NullHandler()); 234 break; 235 236 case 'staging': 237 // In staging, log to files with rotation 238 $logger->pushHandler(new StreamHandler('logs/http.log', Logger::INFO)); 239 break; 240 241 default: 242 // In production, only log warnings and above 243 $logger->pushHandler(new StreamHandler('logs/http.log', Logger::WARNING)); 244 break; 245 } 246 247 return $logger; 248} 249 250// Create a client with the environment-specific logger 251$client = ClientHandler::create(); 252$client->setLogger(createLogger()); 253``` 254 255## Log Analysis and Troubleshooting 256 257HTTP logs can be invaluable for troubleshooting issues. Here are some techniques for analyzing logs: 258 259### Identifying Slow Requests 260 261Look for response logs with high duration values: 262 263``` 264grep "duration" logs/http.log | sort -k5 -nr | head -10 265``` 266 267This will show the 10 slowest requests based on the duration field. 268 269### Finding Error Patterns 270 271Search for failed requests: 272 273``` 274grep "status_code\":4" logs/http.log # Client errors (4xx) 275grep "status_code\":5" logs/http.log # Server errors (5xx) 276``` 277 278### Tracking Retry Patterns 279 280Identify endpoints that frequently require retries: 281 282``` 283grep "Retrying request" logs/http.log | sort | uniq -c | sort -nr 284``` 285 286This will show the most frequently retried endpoints. 287 288## Logging to External Services 289 290For production environments, you might want to send logs to external monitoring services: 291 292```php 293use Monolog\Logger; 294use Monolog\Handler\StreamHandler; 295use Monolog\Handler\SlackWebhookHandler; 296use Fetch\Http\ClientHandler; 297 298// Create a logger that sends critical errors to Slack 299$logger = new Logger('http'); 300 301// Log to file 302$logger->pushHandler(new StreamHandler('logs/http.log', Logger::INFO)); 303 304// Also send critical errors to Slack 305$logger->pushHandler(new SlackWebhookHandler( 306 'https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX', 307 '#api-errors', 308 'API Monitor', 309 true, 310 null, 311 false, 312 false, 313 Logger::CRITICAL 314)); 315 316// Use the logger 317$client = ClientHandler::create(); 318$client->setLogger($logger); 319``` 320 321## Logging in Asynchronous Requests 322 323When making asynchronous requests, logging still works the same way: 324 325```php 326use function async; 327use function await; 328use function all; 329use Monolog\Logger; 330use Monolog\Handler\StreamHandler; 331 332// Create a logger 333$logger = new Logger('http'); 334$logger->pushHandler(new StreamHandler('logs/http-async.log', Logger::DEBUG)); 335 336// Set up the client with the logger 337$client = ClientHandler::create(); 338$client->setLogger($logger); 339 340// Use async/await pattern 341await(async(function() use ($client) { 342 // Process multiple requests in parallel 343 $results = await(all([ 344 'users' => async(fn() => $client->get('https://api.example.com/users')), 345 'posts' => async(fn() => $client->get('https://api.example.com/posts')), 346 'comments' => async(fn() => $client->get('https://api.example.com/comments')) 347 ])); 348 349 // All requests will be logged 350 return $results; 351})); 352 353// Or using the traditional promise approach 354$handler = $client->getHandler(); 355$handler->async(); 356 357// Create promises for multiple requests 358$usersPromise = $handler->get('https://api.example.com/users'); 359$postsPromise = $handler->get('https://api.example.com/posts'); 360 361// All requests will be logged, even though they're async 362$handler->all(['users' => $usersPromise, 'posts' => $postsPromise]) 363 ->then(function ($results) { 364 // Process results 365 }); 366``` 367 368## Context-Aware Logging 369 370You can create a custom logger that adds context to each log entry: 371 372```php 373use Fetch\Http\ClientHandler; 374use Monolog\Logger; 375use Monolog\Handler\StreamHandler; 376use Monolog\Processor\WebProcessor; 377use Monolog\Processor\IntrospectionProcessor; 378use Monolog\Processor\ProcessIdProcessor; 379 380// Create a logger with additional context 381$logger = new Logger('http'); 382$logger->pushHandler(new StreamHandler('logs/http.log', Logger::DEBUG)); 383 384// Add request information (IP, URL, etc.) 385$logger->pushProcessor(new WebProcessor()); 386 387// Add file and line where the log was triggered 388$logger->pushProcessor(new IntrospectionProcessor()); 389 390// Add process ID 391$logger->pushProcessor(new ProcessIdProcessor()); 392 393// Add custom context 394$logger->pushProcessor(function ($record) { 395 $record['extra']['user_id'] = $_SESSION['user_id'] ?? null; 396 $record['extra']['request_id'] = $_SERVER['HTTP_X_REQUEST_ID'] ?? uniqid(); 397 return $record; 398}); 399 400// Use the logger 401$client = ClientHandler::create(); 402$client->setLogger($logger); 403``` 404 405## Structured Logging 406 407For easier log parsing and analysis, you might want to use JSON-formatted logs: 408 409```php 410use Monolog\Logger; 411use Monolog\Handler\StreamHandler; 412use Monolog\Formatter\JsonFormatter; 413use Fetch\Http\ClientHandler; 414 415// Create a logger with JSON formatting 416$logger = new Logger('http'); 417 418$handler = new StreamHandler('logs/http.json.log', Logger::DEBUG); 419$handler->setFormatter(new JsonFormatter()); 420$logger->pushHandler($handler); 421 422// Use the logger 423$client = ClientHandler::create(); 424$client->setLogger($logger); 425``` 426 427This will produce logs in JSON format that can be easily parsed by log analysis tools. 428 429## Logging Request IDs 430 431To correlate multiple log entries for a single client request, you can use request IDs: 432 433```php 434// Generate a request ID at the start of the application 435$requestId = uniqid('req-', true); 436 437// Create a processor that adds the request ID to all log entries 438$requestIdProcessor = function ($record) use ($requestId) { 439 $record['extra']['request_id'] = $requestId; 440 return $record; 441}; 442 443// Create a logger with the processor 444$logger = new Logger('http'); 445$logger->pushProcessor($requestIdProcessor); 446$logger->pushHandler(new StreamHandler('logs/http.log', Logger::DEBUG)); 447 448// Use the logger 449$client = ClientHandler::create(); 450$client->setLogger($logger); 451 452// Add the request ID to all requests as well 453$client->withHeader('X-Request-ID', $requestId); 454``` 455 456## Logging Debug Information 457 458You can use the `debug()` method to get detailed information about a request for logging purposes: 459 460```php 461$client = ClientHandler::create(); 462$response = $client->get('https://api.example.com/users'); 463 464// Get debug information after the request 465$debugInfo = $client->debug(); 466 467// Log it manually if needed 468$logger->debug('Request debug information', $debugInfo); 469 470// Debug info includes: 471// - uri: The full URI 472// - method: The HTTP method used 473// - headers: Request headers (sensitive data redacted) 474// - options: Other request options 475// - is_async: Whether the request was asynchronous 476// - timeout: The timeout setting 477// - retries: The number of retries configured 478// - retry_delay: The retry delay setting 479``` 480 481## Best Practices 482 4831. **Don't Log Sensitive Data**: Be careful about logging request and response bodies that might contain sensitive information. 484 4852. **Use Different Log Levels**: Use appropriate log levels (DEBUG, INFO, WARNING, ERROR) to categorize log entries. 486 4873. **Rotate Log Files**: Implement log rotation to prevent logs from growing too large. 488 4894. **Add Context**: Include request IDs, user IDs, and other contextual information to make logs more useful. 490 4915. **Structure Logs**: Use structured logging (e.g., JSON format) for easier parsing and analysis. 492 4936. **Monitor Error Rates**: Set up alerts for increases in error rates or other anomalies. 494 4957. **Correlation IDs**: Use correlation IDs to trace requests across multiple services. 496 4978. **Regular Log Analysis**: Regularly analyze logs to identify issues and optimize performance. 498 4999. **Adjust Based on Environment**: Use different logging configurations for different environments. 500 50110. **Performance Consideration**: Be mindful of logging performance impact, especially in high-traffic applications. 502 50311. **Use Type-Safe Enums**: When logging status codes or content types, consider using the enums for better readability. 504 50512. **Log Asynchronous Operations**: Make sure to apply the same logging principles to asynchronous requests. 506 507## Next Steps 508 509- Learn about [Error Handling](/guide/error-handling) for comprehensive error management 510- Explore [Retry Handling](/guide/retry-handling) for handling transient errors 511- See [Testing](/guide/testing) for how to test code with logging 512- Check out [Asynchronous Requests](/guide/async-requests) for logging in async operations