friendship ended with social-app. php is my new best friend

title: Logging description: Learn how to configure and use logging with the Fetch HTTP package#

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:

  1. Requests: HTTP method, URI, and sanitized options
  2. Responses: Status code, reason phrase, and timing
  3. 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#

  1. Don't Log Sensitive Data: Be careful about logging request and response bodies that might contain sensitive information.

  2. Use Different Log Levels: Use appropriate log levels (DEBUG, INFO, WARNING, ERROR) to categorize log entries.

  3. Rotate Log Files: Implement log rotation to prevent logs from growing too large.

  4. Add Context: Include request IDs, user IDs, and other contextual information to make logs more useful.

  5. Structure Logs: Use structured logging (e.g., JSON format) for easier parsing and analysis.

  6. Monitor Error Rates: Set up alerts for increases in error rates or other anomalies.

  7. Correlation IDs: Use correlation IDs to trace requests across multiple services.

  8. Regular Log Analysis: Regularly analyze logs to identify issues and optimize performance.

  9. Adjust Based on Environment: Use different logging configurations for different environments.

  10. Performance Consideration: Be mindful of logging performance impact, especially in high-traffic applications.

  11. Use Type-Safe Enums: When logging status codes or content types, consider using the enums for better readability.

  12. Log Asynchronous Operations: Make sure to apply the same logging principles to asynchronous requests.

Next Steps#