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