Logging
The 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.
PSR-3 Logger Integration
The package integrates with any PSR-3 compatible logger, such as Monolog:
use Fetch\Http\ClientHandler;
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
// Create a PSR-3 compatible logger
$logger = new Logger('http');
$logger->pushHandler(new StreamHandler('logs/http.log', Logger::DEBUG));
// Set the logger on a client
$client = ClientHandler::create();
$client->setLogger($logger);
// Now all requests and responses will be logged
$response = $client->get('https://api.example.com/users');
Using Logger with Helper Functions
You can also set a logger on the global client:
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
// Create a logger
$logger = new Logger('http');
$logger->pushHandler(new StreamHandler('logs/http.log', Logger::DEBUG));
// Configure the global client with the logger
$client = fetch_client();
$client->setLogger($logger);
// All requests will now be logged
$response = fetch('https://api.example.com/users');
What Gets Logged
The package logs the following events:
- Requests: HTTP method, URI, and sanitized options
- Responses: Status code, reason phrase, and timing
- Retry Attempts: When retries occur due to errors
Request Logging
[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"}}}
Response Logging
[2023-01-15 14:30:11] http.DEBUG: Received HTTP response {"status_code":200,"reason":"OK","duration":0.532,"content_length":"1250"}
Retry Logging
[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}
Security and Sensitive Data
The package automatically redacts sensitive information in logs:
- Authentication headers are replaced with
[REDACTED]
- Basic auth credentials are replaced with
[REDACTED]
- Bearer tokens are replaced with
[REDACTED]
For example, this request:
$client->withToken('secret-token')
->withHeader('X-API-Key', 'private-key')
->get('https://api.example.com/users');
Would be logged as:
[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"}}}
Custom Logging Configuration
For more control over logging, you can configure different log levels for different events:
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use Monolog\Handler\RotatingFileHandler;
// Create a logger with multiple handlers
$logger = new Logger('http');
// Debug level logs to a rotating file (1MB max size, keep 10 files)
$logger->pushHandler(new RotatingFileHandler('logs/http-debug.log', 10, Logger::DEBUG));
// Info level and above goes to main log
$logger->pushHandler(new StreamHandler('logs/http.log', Logger::INFO));
// Errors go to a separate file
$logger->pushHandler(new StreamHandler('logs/http-error.log', Logger::ERROR));
// Set the logger
$client = ClientHandler::create();
$client->setLogger($logger);
Logging Request and Response Bodies
By 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:
use GuzzleHttp\Middleware;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\MessageFormatter;
use GuzzleHttp\Client;
use Fetch\Http\ClientHandler;
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
// Create a logger
$logger = new Logger('http-verbose');
$logger->pushHandler(new StreamHandler('logs/http-verbose.log', Logger::DEBUG));
// Create a message formatter that includes bodies
$formatter = new MessageFormatter(
"Request: {method} {uri} HTTP/{version}\n" .
"Request Headers: {req_headers}\n" .
"Request Body: {req_body}\n" .
"Response: HTTP/{version} {code} {phrase}\n" .
"Response Headers: {res_headers}\n" .
"Response Body: {res_body}"
);
// Create middleware with the formatter
$middleware = Middleware::log($logger, $formatter, 'debug');
// Create a handler stack with the middleware
$stack = HandlerStack::create();
$stack->push($middleware);
// Create a Guzzle client with the stack
$guzzleClient = new Client(['handler' => $stack]);
// Create a ClientHandler with the custom Guzzle client
$client = ClientHandler::createWithClient($guzzleClient);
// Use the client
$response = $client->post('https://api.example.com/users', [
'name' => 'John Doe',
'email' => 'john@example.com'
]);
Logging with Status Enums
When logging with status codes, you can use the type-safe Status enum for better readability:
use Fetch\Enum\Status;
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
$logger = new Logger('http');
$logger->pushHandler(new StreamHandler('logs/http.log', Logger::DEBUG));
// Custom log processing
$logger->pushProcessor(function ($record) {
// If we have a response status in the context, convert it to a human-readable format
if (isset($record['context']['status_code'])) {
$statusCode = $record['context']['status_code'];
$statusEnum = Status::tryFrom($statusCode);
if ($statusEnum) {
$record['context']['status_text'] = $statusEnum->phrase();
}
}
return $record;
});
// Set the logger
$client = ClientHandler::create();
$client->setLogger($logger);
Logging in Different Environments
It's often useful to adjust logging behavior based on the environment:
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use Monolog\Handler\NullHandler;
use Monolog\Formatter\LineFormatter;
use Fetch\Http\ClientHandler;
function createLogger(): Logger
{
$env = getenv('APP_ENV') ?: 'production';
$logger = new Logger('http');
// Configure based on environment
switch ($env) {
case 'development':
// In development, log everything to stdout with details
$formatter = new LineFormatter(
"[%datetime%] %channel%.%level_name%: %message% %context% %extra%\n",
null, true, true
);
$handler = new StreamHandler('php://stdout', Logger::DEBUG);
$handler->setFormatter($formatter);
$logger->pushHandler($handler);
break;
case 'testing':
// In testing, don't log anything
$logger->pushHandler(new NullHandler());
break;
case 'staging':
// In staging, log to files with rotation
$logger->pushHandler(new StreamHandler('logs/http.log', Logger::INFO));
break;
default:
// In production, only log warnings and above
$logger->pushHandler(new StreamHandler('logs/http.log', Logger::WARNING));
break;
}
return $logger;
}
// Create a client with the environment-specific logger
$client = ClientHandler::create();
$client->setLogger(createLogger());
Log Analysis and Troubleshooting
HTTP logs can be invaluable for troubleshooting issues. Here are some techniques for analyzing logs:
Identifying Slow Requests
Look for response logs with high duration values:
grep "duration" logs/http.log | sort -k5 -nr | head -10
This will show the 10 slowest requests based on the duration field.
Finding Error Patterns
Search for failed requests:
grep "status_code\":4" logs/http.log # Client errors (4xx)
grep "status_code\":5" logs/http.log # Server errors (5xx)
Tracking Retry Patterns
Identify endpoints that frequently require retries:
grep "Retrying request" logs/http.log | sort | uniq -c | sort -nr
This will show the most frequently retried endpoints.
Logging to External Services
For production environments, you might want to send logs to external monitoring services:
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use Monolog\Handler\SlackWebhookHandler;
use Fetch\Http\ClientHandler;
// Create a logger that sends critical errors to Slack
$logger = new Logger('http');
// Log to file
$logger->pushHandler(new StreamHandler('logs/http.log', Logger::INFO));
// Also send critical errors to Slack
$logger->pushHandler(new SlackWebhookHandler(
'https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX',
'#api-errors',
'API Monitor',
true,
null,
false,
false,
Logger::CRITICAL
));
// Use the logger
$client = ClientHandler::create();
$client->setLogger($logger);
Logging in Asynchronous Requests
When making asynchronous requests, logging still works the same way:
use function async;
use function await;
use function all;
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
// Create a logger
$logger = new Logger('http');
$logger->pushHandler(new StreamHandler('logs/http-async.log', Logger::DEBUG));
// Set up the client with the logger
$client = ClientHandler::create();
$client->setLogger($logger);
// Use async/await pattern
await(async(function() use ($client) {
// Process multiple requests in parallel
$results = await(all([
'users' => async(fn() => $client->get('https://api.example.com/users')),
'posts' => async(fn() => $client->get('https://api.example.com/posts')),
'comments' => async(fn() => $client->get('https://api.example.com/comments'))
]));
// All requests will be logged
return $results;
}));
// Or using the traditional promise approach
$handler = $client->getHandler();
$handler->async();
// Create promises for multiple requests
$usersPromise = $handler->get('https://api.example.com/users');
$postsPromise = $handler->get('https://api.example.com/posts');
// All requests will be logged, even though they're async
$handler->all(['users' => $usersPromise, 'posts' => $postsPromise])
->then(function ($results) {
// Process results
});
Context-Aware Logging
You can create a custom logger that adds context to each log entry:
use Fetch\Http\ClientHandler;
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use Monolog\Processor\WebProcessor;
use Monolog\Processor\IntrospectionProcessor;
use Monolog\Processor\ProcessIdProcessor;
// Create a logger with additional context
$logger = new Logger('http');
$logger->pushHandler(new StreamHandler('logs/http.log', Logger::DEBUG));
// Add request information (IP, URL, etc.)
$logger->pushProcessor(new WebProcessor());
// Add file and line where the log was triggered
$logger->pushProcessor(new IntrospectionProcessor());
// Add process ID
$logger->pushProcessor(new ProcessIdProcessor());
// Add custom context
$logger->pushProcessor(function ($record) {
$record['extra']['user_id'] = $_SESSION['user_id'] ?? null;
$record['extra']['request_id'] = $_SERVER['HTTP_X_REQUEST_ID'] ?? uniqid();
return $record;
});
// Use the logger
$client = ClientHandler::create();
$client->setLogger($logger);
Structured Logging
For easier log parsing and analysis, you might want to use JSON-formatted logs:
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use Monolog\Formatter\JsonFormatter;
use Fetch\Http\ClientHandler;
// Create a logger with JSON formatting
$logger = new Logger('http');
$handler = new StreamHandler('logs/http.json.log', Logger::DEBUG);
$handler->setFormatter(new JsonFormatter());
$logger->pushHandler($handler);
// Use the logger
$client = ClientHandler::create();
$client->setLogger($logger);
This will produce logs in JSON format that can be easily parsed by log analysis tools.
Logging Request IDs
To correlate multiple log entries for a single client request, you can use request IDs:
// Generate a request ID at the start of the application
$requestId = uniqid('req-', true);
// Create a processor that adds the request ID to all log entries
$requestIdProcessor = function ($record) use ($requestId) {
$record['extra']['request_id'] = $requestId;
return $record;
};
// Create a logger with the processor
$logger = new Logger('http');
$logger->pushProcessor($requestIdProcessor);
$logger->pushHandler(new StreamHandler('logs/http.log', Logger::DEBUG));
// Use the logger
$client = ClientHandler::create();
$client->setLogger($logger);
// Add the request ID to all requests as well
$client->withHeader('X-Request-ID', $requestId);
Logging Debug Information
You can use the debug()
method to get detailed information about a request for logging purposes:
$client = ClientHandler::create();
$response = $client->get('https://api.example.com/users');
// Get debug information after the request
$debugInfo = $client->debug();
// Log it manually if needed
$logger->debug('Request debug information', $debugInfo);
// Debug info includes:
// - uri: The full URI
// - method: The HTTP method used
// - headers: Request headers (sensitive data redacted)
// - options: Other request options
// - is_async: Whether the request was asynchronous
// - timeout: The timeout setting
// - retries: The number of retries configured
// - retry_delay: The retry delay setting
Best Practices
Don't Log Sensitive Data: Be careful about logging request and response bodies that might contain sensitive information.
Use Different Log Levels: Use appropriate log levels (DEBUG, INFO, WARNING, ERROR) to categorize log entries.
Rotate Log Files: Implement log rotation to prevent logs from growing too large.
Add Context: Include request IDs, user IDs, and other contextual information to make logs more useful.
Structure Logs: Use structured logging (e.g., JSON format) for easier parsing and analysis.
Monitor Error Rates: Set up alerts for increases in error rates or other anomalies.
Correlation IDs: Use correlation IDs to trace requests across multiple services.
Regular Log Analysis: Regularly analyze logs to identify issues and optimize performance.
Adjust Based on Environment: Use different logging configurations for different environments.
Performance Consideration: Be mindful of logging performance impact, especially in high-traffic applications.
Use Type-Safe Enums: When logging status codes or content types, consider using the enums for better readability.
Log Asynchronous Operations: Make sure to apply the same logging principles to asynchronous requests.
Next Steps
- Learn about Error Handling for comprehensive error management
- Explore Retry Handling for handling transient errors
- See Testing for how to test code with logging
- Check out Asynchronous Requests for logging in async operations