configure Mezzio swoole/openswoole to use monolog for access logging and error logging

I tend to favor Mezzio micro framework for highly available PHP projects for it’s close adherence to PHP standards and following dev best practices – coupled with swoole/openswoole library for all things concurrency && async – this makes a mean performance machine.

Anywho – out of the box mezzio swoole logging comes with some default PSR3 logger enabled (for access logs and some other minor stuff) however I always tend to just use monolog logging library for it’s ubiquity. Its quite simple to setup – here’s what you need to do.

Add monolog to your project

This is self explanatory but nonetheless here we go:

composer require monolog/monolog

Enable factories

Enable these 2 factories (wherever you are configuring your dependency injection container in your mezzio swoole/openswoole applications):

'factories'  => [
    //Loggers
    'logger-sys'  => \Application\Logger\SysLoggerFactory::class,
    'logger-app' => \Application\Logger\AppLoggerFactory::class,
    
    //other factories
]

Technically you might only use one logger, but I tend to like to separate loggers configured for access logs and general purpose application/error logs – if even just so I see different channel name in logs that would allow me to easily search for just errors for example without seeing millions of useless access logs.

Monolog factories

Add following 2 factories for monolog access and error loggers:

<?php

declare(strict_types=1);

namespace Application\Logger;

use Monolog\Handler\StreamHandler;
use Monolog\Logger;
use Monolog\Processor\PsrLogMessageProcessor;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;

class AppLoggerFactory
{
    public function __invoke(ContainerInterface $container): LoggerInterface
    {
        // Init logger with "logging channel" (descriptive name that is attached to all logs)
        $logger = new Logger("LOGGER-APP");

        // Dumping to stdout as with modern containerized approach this is very common
        $handler = new StreamHandler(
            'php://stdout',
            //Logger::ERROR,
        );
        
        $formatter = new \Monolog\Formatter\LineFormatter();
        $formatter->allowInlineLineBreaks(false);
        $formatter->includeStacktraces(true);
        $formatter->ignoreEmptyContextAndExtra(true);
        $handler->setFormatter($formatter);

        $logger->pushHandler($handler);

        // Mezzio does PSR3 curly bracketed logging (different from default monolog format).
        // ex: Worker started in {cwd} with ID {pid} {"cwd":"/var/www/site","pid":6}
        // So we need this to expand curly PSR3 logging notation .  
        $processor = new PsrLogMessageProcessor($dateFormat = null, $removeUsedContextFields = true);
        $logger->pushProcessor($processor);

        return $logger;
    }
}
<?php

declare(strict_types=1);

namespace Application\Logger;

use Monolog\Handler\StreamHandler;
use Monolog\Logger;
use Monolog\Processor\PsrLogMessageProcessor;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;

class SysLoggerFactory
{
    public function __invoke(ContainerInterface $container): LoggerInterface
    {
        // Init logger with "logging channel" (descriptive name that is attached to all logs)
        $logger = new Logger('LOGGER-SYS');

        // Dumping to stdout as with modern containerized approach this is most common
        $handler = new StreamHandler(
            'php://stdout',
            Logger::INFO,
        );

        $formatter = new \Monolog\Formatter\LineFormatter();
        $formatter->allowInlineLineBreaks(false);
        $formatter->includeStacktraces(false);
        $formatter->ignoreEmptyContextAndExtra(true);
        $handler->setFormatter($formatter);

        $logger->pushHandler($handler);

        // Expand PSR3 curly bracketed logging (different from default monolog format).
        // add this if you plan on using PSR3 curly variable notation in messages
        $logger->pushProcessor(new PsrLogMessageProcessor($dateFormat = null, $removeUsedContextFields = true));

        return $logger;
    }
}

Now that we have enabled and configured our monolog loggers – let’s figure out how we could use them right?

Replacing mezzio default access logger

There are multiple ways of doing this mentioned in official docs, however I tend to just do the following:

<?php 

// In config/autoload/swoole.local.php:
return [
    'mezzio-swoole' => [
        'enable_coroutine'   => true,
        'swoole-http-server' => [
            'process-name'   => 'my-awesome-project',
            'host'           => '0.0.0.0',
            'port'           => 9501,
            'mode'           => SWOOLE_PROCESS,
            'options'        => [
                'task_enable_coroutine' => true,
                'max_conn' => 1024,
                'task_worker_num' => 4,
                'pid_file' => sys_get_temp_dir() . '/my-app-com.pid',
            ],
            'static-files' => [
                'enable' => false,
            ],
            // Here - this is important part for enabling monolog logger
            'logger' => [
                 // Give it the service name u configured earlier
                'logger-name' => 'logger-sys',
            ],
        ],
    ],
];

Simple enough m’I’right?

Now lets configure exceptions logging to use monolog.

Log uncaught exceptions with monolog

Mezzio uses this middleware to catch exceptions: Laminas\Stratigility\Middleware\ErrorHandler. Its typically located as one of the last middlewares in mezzio application (so we can catch all the exceptions). Basically we need to use it’s attachListener method to add object that will then do actual logging.

We have to use another level of abstraction here called “listener delegator” to do the act of attaching listener.

Im not gonna bore you anymore – just enable this “delegator factory”:

'delegators' => [
    ErrorHandler::class => [
         \Application\Logger\LoggingErrorListenerDelegator::class,
    ],
],

^ If you dont know what “delegator factory” is, – just read above code like this – we create “ErrorHandler” middleware object, then pass it to “listener delegator” object (in this case: LoggingErrorListenerDelegator) that does something with it.

Add following “listener delegator” code. All it does is it attaches error logger object (called LoggingErrorListener) to error catching middleware:

<?php

declare(strict_types=1);

namespace Application\Logger;

use Laminas\Stratigility\Middleware\ErrorHandler;
use Psr\Container\ContainerInterface;

class LoggingErrorListenerDelegator
{
    public function __invoke(
        ContainerInterface $container,
        string $serviceName,
        callable $callback
    ): ErrorHandler {
        $errorHandler = $callback();
        $errorHandler->attachListener(
            new LoggingErrorListener($container->get('logger-app'))
        );
        return $errorHandler;
    }
}

And actual class that would log uncaught exceptions:

<?php

declare(strict_types=1);

namespace Application\Logger;

use Exception;
use Psr\Log\LoggerInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Throwable;

class LoggingErrorListener
{
    private $logger;

    public function __construct(LoggerInterface $logger)
    {
        $this->logger = $logger;
    }

    public function __invoke(Throwable $error, ServerRequestInterface $request, ResponseInterface $response)
    {
        // Monolog suggests this simple approach for logging exceptions
        // https://github.com/Seldaek/monolog/issues/1535
        $this->logger->error('Unhandled exception', ['exception' => $error]);
    }
}

And we are all done – just reboot your stack and your monolog loggers should be used automatically for access logs and exceptions logs.

thanks for reading. 🤪️

Leave a Comment