Logging Guzzle Requests

When you’re building an API consumer, you should log your API requests and responses. Otherwise it can feel like you’re working blind.

Here I’ll explain how to configure a Guzzle Middleware to log requests to a file. This is useful for debugging and historical purposes.

The way to do this is to pass a new HandlerStack with our logging middleware into our Client instance.

The syntax for creating a new Guzzle instance is as follows:

$client = new \GuzzleHttp\Client([
    'base_uri' => "http://www.example.com/api",
    'handler' => $handlerStack,
    ]),
]);

The $handlerStack will eventually hold the middleware that handles the logging.

Setting up the log file

Before I can set up the middleware, I need to instantiate something that handles writing our log messages to file. I’ll use the Monolog package to handle this – it makes it easy to achieve my desired criteria of having a daily log file next to my default laravel.log.

In my AppServiceProvider I’ll set up a helper function to create the logger:

private function getLogger()
{
    if (! $this->logger) {
        $this->logger = with(new \Monolog\Logger('api-consumer'))->pushHandler(
            new \Monolog\Handler\RotatingFileHandler(storage_path('logs/api-consumer.log'))
        );
    }

    return $this->logger;
}

Setting up the middleware

Guzzle’s logging middleware requires a logger (that I’ve just created above) and a MessageFormatter instance that controls what gets logged.

I’ll set up a helper function to create the middleware:

private function createGuzzleLoggingMiddleware(string $messageFormat)
{
    return \GuzzleHttp\Middleware::log(
        $this->getLogger(),
        new \GuzzleHttp\MessageFormatter($messageFormat)
    );
}

The $messageFormat is a string that “Formats log messages using variable substitutions for requests, responses, and other transactional data.” Possible substitutions can be seen here – but I’ve come up with some handy ones:

Logging the request: {method} {uri} HTTP/{version} {req_body}
Logging the response: RESPONSE: {code} – {res_body}

Creating the HandlerStack

Now that I have a way to easily new up an instance of Guzzle’s logging middleware, I can create a new HandlerStack to push the middleware onto:

$handlerStack = \GuzzleHttp\HandlerStack::create();

$handlerStack->push(
    $this->createGuzzleLoggingMiddleware($messageFormat)
);

Logging the request and response separately

I decided to log the request and response in two separate log entries. It seems like the only way to accomplish this is to push multiple logging middlewares to Guzzle, so I set up a helper function to create a HandlerStack with all the required logging middleware from an array of message format strings that I would like to log.

private function createLoggingHandlerStack(array $messageFormats)
{
    $stack = \GuzzleHttp\HandlerStack::create();

    collect($messageFormats)->each(function ($messageFormat) use ($stack) {
        // We'll use unshift instead of push, to add the middleware to the bottom of the stack, not the top
        $stack->unshift(
            $this->createGuzzleLoggingMiddleware($messageFormat)
        );
    });

    return $stack;
}

And there we have it! Now, adding multiple loggers to our Guzzle Client is as easy as the code below:

$client = new Client([
    'base_uri' => "http://example.com",
    'handler' => $this->createLoggingHandlerStack([
        '{method} {uri} HTTP/{version} {req_body}',
        'RESPONSE: {code} - {res_body}',
    ]),
]);

A short note on working with Guzzle responses

After setting up the logger, I realised that I was no longer able to access the response body – it turns out it was my mistake. 

Previously, I was accessing the response content using the following line:

$this->client->get($endpoint)->getBody()->getContents();

But it turns out that this gets the “remaining contents of the body as a string” – and since the middleware already accessed the body in order to log it, there isn’t any content remaining to retrieve.

The correct way to access the response content is to cast the body to a string:

(string) $this->client->get($endpoint)->getBody();

Resources

I lead the tech team at Mindbeat, follow me on Twitter as I continue to document lessons learned on this journey.

7 comments

  1. You can’t get content twice because it is “stream”. You need rewind stream for reading content second (etc) time:
    $stream = $response->getBody();
    $stream->rewind();
    $content = $stream->getContents();

      1. Starting from PHP 5.4, You can use just round brackets:

        (new MonologLogger(‘api-consumer’))->WhatEverFunctionInLogger();

Leave a comment

Your email address will not be published.