Commit a6e15a75 authored by Fabien Potencier's avatar Fabien Potencier

feature #895 Replace Monolog middlewares by an event listener (GromNaN)

This PR was merged into the 1.2.x-dev branch.

Discussion
----------

Replace Monolog middlewares by an event listener

This is an alternative to the changes introduced by #894 in order to allow replacement of the logging middlewares.
Instead of having a Closure for each listener, this PR introduces a listener class.
The listener can be used independently from the MonologServiceProvider.

| Q             | A
| ------------- | ---
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | #870
| License       | MIT
| Doc          | yes

Commits
-------

499a626f Replace Monolog middlewares by an event listener
parents c6aa99f3 499a626f
......@@ -35,6 +35,8 @@ Services
$app['monolog']->addDebug('Testing the Monolog logging.');
* **monolog.listener**: An event listener to log requests, responses and errors.
Registering
-----------
......@@ -85,11 +87,9 @@ it by extending the ``monolog`` service::
return $monolog;
}));
By default, all requests are logged through a ``before`` and ``after``
middleware at boot time. You can disable or customize this behavior by
overriding the ``monolog.boot.before`` and ``monolog.boot.after`` services
respectively. The provider also registers a default ``error`` handler which
logs errors; it can be customized via the ``monolog.boot.error`` service.
By default, all requests, responses and errors are logged by an event listener
registered as a service called `monolog.listener`. You can replace or remove
this service if you want to modify or disable the informations logged.
Traits
------
......
<?php
/*
* This file is part of the Silex framework.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Silex\EventListener;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\RedirectResponse;
/**
* Log request, response and exceptions
*/
class LogListener implements EventSubscriberInterface
{
protected $logger;
public function __construct(LoggerInterface $logger)
{
$this->logger = $logger;
}
/**
* Logs master requests on event KernelEvents::REQUEST
*
* @param GetResponseEvent $event
*/
public function onKernelRequest(GetResponseEvent $event)
{
if (HttpKernelInterface::MASTER_REQUEST !== $event->getRequestType()) {
return;
}
$this->logRequest($event->getRequest());
}
/**
* Logs master response on event KernelEvents::RESPONSE
*
* @param FilterResponseEvent $event
*/
public function onKernelResponse(FilterResponseEvent $event)
{
if (HttpKernelInterface::MASTER_REQUEST !== $event->getRequestType()) {
return;
}
$this->logResponse($event->getResponse());
}
/**
* Logs uncaught exceptions on event KernelEvents::EXCEPTION
*
* @param GetResponseForExceptionEvent $event
*/
public function onKernelException(GetResponseForExceptionEvent $event)
{
$this->logException($event->getException());
}
/**
* Logs a request
*
* @param Request $request
*/
protected function logRequest(Request $request)
{
$this->logger->info('> '.$request->getMethod().' '.$request->getRequestUri());
}
/**
* Logs a response
*
* @param Response $response
*/
protected function logResponse(Response $response)
{
if ($response instanceof RedirectResponse) {
$this->logger->info('< '.$response->getStatusCode().' '.$response->getTargetUrl());
} else {
$this->logger->info('< '.$response->getStatusCode());
}
}
/**
* Logs an exception
*
* @param Exception $e
*/
protected function logException(\Exception $e)
{
$message = sprintf('%s: %s (uncaught exception) at %s line %s', get_class($e), $e->getMessage(), $e->getFile(), $e->getLine());
if ($e instanceof HttpExceptionInterface && $e->getStatusCode() < 500) {
$this->logger->error($message, array('exception' => $e));
} else {
$this->logger->critical($message, array('exception' => $e));
}
}
public static function getSubscribedEvents()
{
return array(
KernelEvents::REQUEST => array('onKernelRequest', 0),
KernelEvents::RESPONSE => array('onKernelResponse', 0),
/*
* Priority -4 is used to come after those from SecurityServiceProvider (0)
* but before the error handlers added with Silex\Application::error (defaults to -8)
*/
KernelEvents::EXCEPTION => array('onKernelException', -4),
);
}
}
......@@ -15,11 +15,8 @@ use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use Silex\Application;
use Silex\ServiceProviderInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Bridge\Monolog\Handler\DebugHandler;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
use Silex\EventListener\LogListener;
/**
* Monolog Provider.
......@@ -66,47 +63,18 @@ class MonologServiceProvider implements ServiceProviderInterface
return Logger::DEBUG;
};
$app['monolog.boot.before'] = $app->protect(
function (Request $request) use ($app) {
$app['monolog']->addInfo('> '.$request->getMethod().' '.$request->getRequestUri());
}
);
$app['monolog.boot.error'] = $app->protect(
function (\Exception $e) use ($app) {
$message = sprintf('%s: %s (uncaught exception) at %s line %s', get_class($e), $e->getMessage(), $e->getFile(), $e->getLine());
if ($e instanceof HttpExceptionInterface && $e->getStatusCode() < 500) {
$app['monolog']->addError($message, array('exception' => $e));
} else {
$app['monolog']->addCritical($message, array('exception' => $e));
}
}
);
$app['monolog.boot.after'] = $app->protect(
function (Request $request, Response $response) use ($app) {
if ($response instanceof RedirectResponse) {
$app['monolog']->addInfo('< '.$response->getStatusCode().' '.$response->getTargetUrl());
} else {
$app['monolog']->addInfo('< '.$response->getStatusCode());
}
}
);
$app['monolog.listener'] = $app->share(function () use ($app) {
return new LogListener($app['logger']);
});
$app['monolog.name'] = 'myapp';
}
public function boot(Application $app)
{
$app->before($app['monolog.boot.before']);
/*
* Priority -4 is used to come after those from SecurityServiceProvider (0)
* but before the error handlers added with Silex\Application::error (defaults to -8)
*/
$app->error($app['monolog.boot.error'], -4);
$app->after($app['monolog.boot.after']);
if (isset($app['monolog.listener'])) {
$app['dispatcher']->addSubscriber($app['monolog.listener']);
}
}
public static function translateLevel($name)
......
<?php
/*
* This file is part of the Silex framework.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace Silex\Tests\EventListener;
use Silex\EventListener\LogListener;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\Exception\HttpException;
/**
* LogListener
*
* @author Jérôme Tamarelle <jerome@tamarelle.net>
*/
class LogListenerTest extends \PHPUnit_Framework_TestCase
{
public function testRequestListener()
{
$logger = $this->getMock('Psr\\Log\\LoggerInterface');
$logger
->expects($this->once())
->method('info')
->with($this->equalTo('> GET /foo'))
;
$dispatcher = new EventDispatcher();
$dispatcher->addSubscriber(new LogListener($logger));
$kernel = $this->getMock('Symfony\\Component\\HttpKernel\\HttpKernelInterface');
$dispatcher->dispatch(KernelEvents::REQUEST, new GetResponseEvent($kernel, Request::create('/subrequest'), HttpKernelInterface::SUB_REQUEST), 'Skip sub requests');
$dispatcher->dispatch(KernelEvents::REQUEST, new GetResponseEvent($kernel, Request::create('/foo'), HttpKernelInterface::MASTER_REQUEST), 'Log master requests');
}
public function testResponseListener()
{
$logger = $this->getMock('Psr\\Log\\LoggerInterface');
$logger
->expects($this->once())
->method('info')
->with($this->equalTo('< 301'))
;
$dispatcher = new EventDispatcher();
$dispatcher->addSubscriber(new LogListener($logger));
$kernel = $this->getMock('Symfony\\Component\\HttpKernel\\HttpKernelInterface');
$dispatcher->dispatch(KernelEvents::RESPONSE, new FilterResponseEvent($kernel, Request::create('/foo'), HttpKernelInterface::SUB_REQUEST, Response::create('subrequest', 200)), 'Skip sub requests');
$dispatcher->dispatch(KernelEvents::RESPONSE, new FilterResponseEvent($kernel, Request::create('/foo'), HttpKernelInterface::MASTER_REQUEST, Response::create('bar', 301)), 'Log master requests');
}
public function testExceptionListener()
{
$logger = $this->getMock('Psr\\Log\\LoggerInterface');
$logger
->expects($this->once())
->method('critical')
->with($this->equalTo('RuntimeException: Fatal error (uncaught exception) at '.__FILE__.' line '.(__LINE__+14)))
;
$logger
->expects($this->once())
->method('error')
->with($this->equalTo('Symfony\Component\HttpKernel\Exception\HttpException: Http error (uncaught exception) at '.__FILE__.' line '.(__LINE__+10)))
;
$dispatcher = new EventDispatcher();
$dispatcher->addSubscriber(new LogListener($logger));
$kernel = $this->getMock('Symfony\\Component\\HttpKernel\\HttpKernelInterface');
$dispatcher->dispatch(KernelEvents::EXCEPTION, new GetResponseForExceptionEvent($kernel, Request::create('/foo'), HttpKernelInterface::SUB_REQUEST, new \RuntimeException('Fatal error')));
$dispatcher->dispatch(KernelEvents::EXCEPTION, new GetResponseForExceptionEvent($kernel, Request::create('/foo'), HttpKernelInterface::SUB_REQUEST, new HttpException(400, 'Http error')));
}
}
......@@ -156,6 +156,16 @@ class MonologServiceProviderTest extends \PHPUnit_Framework_TestCase
$app['monolog.handler']->getLevel();
}
public function testDisableListener()
{
$app = $this->getApplication();
unset($app['monolog.listener']);
$app->handle(Request::create('/404'));
$this->assertEmpty($app['monolog.handler']->getRecords(), "Expected no logging to occur");
}
protected function assertMatchingRecord($pattern, $level, $handler)
{
$found = false;
......@@ -177,6 +187,7 @@ class MonologServiceProviderTest extends \PHPUnit_Framework_TestCase
$app['monolog.handler'] = $app->share(function () use ($app) {
$level = MonologServiceProvider::translateLevel($app['monolog.level']);
return new TestHandler($level);
});
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment