Permalink
Browse files

feature #24300 [HttpKernel][FrameworkBundle] Add a minimalist default…

… PSR-3 logger (dunglas)

This PR was squashed before being merged into the 3.4 branch (closes #24300).

Discussion
----------

[HttpKernel][FrameworkBundle] Add a minimalist default PSR-3 logger

| Q             | A
| ------------- | ---
| Branch?       | 3.4
| Bug fix?      | no
| New feature?  | yes <!-- don't forget updating src/**/CHANGELOG.md files -->
| BC breaks?    | no
| Deprecations? | no <!-- don't forget updating UPGRADE-*.md files -->
| Tests pass?   | yes
| Fixed tickets | n/a
| License       | MIT
| Doc PR        | n/a

This PR provides a minimalist PSR-3 logger that is always available when FrameworkBundle is installed.
By default, it writes errors on `stderr`, regular logs on `stdout` and discards debug data (this is configurable).

This approach has several benefits:

- It's what expect from an app logging systems of major containerization and orchestration tools including [Docker](https://docs.docker.com/engine/admin/logging/view_container_logs/) and [Kubernetes](https://kubernetes.io/docs/concepts/cluster-administration/logging/), as well as most cloud providers such as [Heroku](https://devcenter.heroku.com/articles/logging#writing-to-your-log) and [Google Container Engine](https://kubernetes.io/docs/tasks/debug-application-cluster/logging-stackdriver/). If the app follows this standard (and it's not currently the case with Symfony by default) logs will be automatically collected, aggregated and stored.
- It's in sync with the "back to Unix roots" philosophy of Flex
- Logs are directly displayed in the console when running the integrated PHP web server (`bin/console server:start` or Flex's `make serve`), Create React App also do that for instance.
- It fixes a common problem when installing Flex recipes: many bundles expect a logger service but currently there is none available by default, and you usually get a `"logger" service not found error` (because packages depend of the PSR, but the PSR doesn't provide a logger service).

Commits
-------

9a06513 [HttpKernel][FrameworkBundle] Add a minimalist default PSR-3 logger
  • Loading branch information...
fabpot committed Sep 29, 2017
2 parents 1b30098 + 9a06513 commit 09afa6490980a95df005c6103d04eab61c1179a2
View
@@ -109,6 +109,7 @@
"provide": {
"psr/cache-implementation": "1.0",
"psr/container-implementation": "1.0",
"psr/log-implementation": "1.0",
"psr/simple-cache-implementation": "1.0"
},
"autoload": {
@@ -4,6 +4,7 @@ CHANGELOG
3.4.0
-----
* Always register a minimalist logger that writes in `stderr`
* Deprecated `profiler.matcher` option
* Added support for `EventSubscriberInterface` on `MicroKernelTrait`
* Removed `doctrine/cache` from the list of required dependencies in `composer.json`
@@ -28,6 +28,7 @@
use Symfony\Component\Console\Application;
use Symfony\Component\Console\DependencyInjection\AddConsoleCommandPass;
use Symfony\Component\HttpKernel\DependencyInjection\ControllerArgumentValueResolverPass;
use Symfony\Component\HttpKernel\DependencyInjection\LoggerPass;
use Symfony\Component\HttpKernel\DependencyInjection\RegisterControllerArgumentLocatorsPass;
use Symfony\Component\HttpKernel\DependencyInjection\RemoveEmptyControllerArgumentLocatorsPass;
use Symfony\Component\HttpKernel\DependencyInjection\ResettableServicePass;
@@ -82,6 +83,7 @@ public function build(ContainerBuilder $container)
{
parent::build($container);
$container->addCompilerPass(new LoggerPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, -32);
$container->addCompilerPass(new RegisterControllerArgumentLocatorsPass());
$container->addCompilerPass(new RemoveEmptyControllerArgumentLocatorsPass(), PassConfig::TYPE_BEFORE_REMOVING);
$container->addCompilerPass(new RoutingResolverPass());
@@ -101,6 +101,8 @@ public function log($level, $message, array $context = array())
/**
* Returns true when any messages have been logged at error levels.
*
* @return bool
*/
public function hasErrored()
{
@@ -4,6 +4,7 @@ CHANGELOG
3.4.0
-----
* added a minimalist PSR-3 `Logger` class that writes in `stderr`
* made kernels implementing `CompilerPassInterface` able to process the container
* deprecated bundle inheritance
* added `RebootableInterface` and implemented it in `Kernel`
@@ -0,0 +1,45 @@
<?php
/*
* This file is part of the Symfony package.
*
* (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 Symfony\Component\HttpKernel\DependencyInjection;
use Psr\Log\LoggerInterface;
use Psr\Log\LogLevel;
use Symfony\Component\HttpKernel\Log\Logger;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
/**
* Registers the default logger if necessary.
*
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class LoggerPass implements CompilerPassInterface
{
/**
* {@inheritdoc}
*/
public function process(ContainerBuilder $container)
{
$alias = $container->setAlias(LoggerInterface::class, 'logger');
$alias->setPublic(false);
if ($container->has('logger')) {
return;
}
$loggerDefinition = $container->register('logger', Logger::class);
$loggerDefinition->setPublic(false);
if ($container->getParameter('kernel.debug')) {
$loggerDefinition->addArgument(LogLevel::DEBUG);
}
}
}
@@ -0,0 +1,98 @@
<?php
/*
* This file is part of the Symfony package.
*
* (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 Symfony\Component\HttpKernel\Log;
use Psr\Log\AbstractLogger;
use Psr\Log\InvalidArgumentException;
use Psr\Log\LogLevel;
/**
* Minimalist PSR-3 logger designed to write in stderr or any other stream.
*
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class Logger extends AbstractLogger
{
private static $levels = array(
LogLevel::DEBUG => 0,
LogLevel::INFO => 1,
LogLevel::NOTICE => 2,
LogLevel::WARNING => 3,
LogLevel::ERROR => 4,
LogLevel::CRITICAL => 5,
LogLevel::ALERT => 6,
LogLevel::EMERGENCY => 7,
);
private $minLevelIndex;
private $formatter;
private $handle;
public function __construct($minLevel = LogLevel::WARNING, $output = 'php://stderr', callable $formatter = null)
{
if (!isset(self::$levels[$minLevel])) {
throw new InvalidArgumentException(sprintf('The log level "%s" does not exist.', $minLevel));
}
$this->minLevelIndex = self::$levels[$minLevel];
$this->formatter = $formatter ?: array($this, 'format');
if (false === $this->handle = @fopen($output, 'a')) {
throw new InvalidArgumentException(sprintf('Unable to open "%s".', $output));
}
}
/**
* {@inheritdoc}
*/
public function log($level, $message, array $context = array())
{
if (!isset(self::$levels[$level])) {
throw new InvalidArgumentException(sprintf('The log level "%s" does not exist.', $level));
}
if (self::$levels[$level] < $this->minLevelIndex) {
return;
}
$formatter = $this->formatter;
fwrite($this->handle, $formatter($level, $message, $context));
}
/**
* @param string $level
* @param string $message
* @param array $context
*
* @return string
*/
private function format($level, $message, array $context)
{
if (false !== strpos($message, '{')) {
$replacements = array();
foreach ($context as $key => $val) {
if (null === $val || is_scalar($val) || (\is_object($val) && method_exists($val, '__toString'))) {
$replacements["{{$key}}"] = $val;
} elseif ($val instanceof \DateTimeInterface) {
$replacements["{{$key}}"] = $val->format(\DateTime::RFC3339);
} elseif (\is_object($val)) {
$replacements["{{$key}}"] = '[object '.\get_class($val).']';
} else {
$replacements["{{$key}}"] = '['.\gettype($val).']';
}
}
$message = strtr($message, $replacements);
}
return sprintf('%s [%s] %s', date(\DateTime::RFC3339), $level, $message).\PHP_EOL;
}
}
@@ -0,0 +1,68 @@
<?php
/*
* This file is part of the Symfony package.
*
* (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 Symfony\Component\HttpKernel\Tests\DependencyInjection;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
use Psr\Log\LogLevel;
use Symfony\Component\HttpKernel\DependencyInjection\LoggerPass;
use Symfony\Component\HttpKernel\Log\Logger;
use Symfony\Component\DependencyInjection\ContainerBuilder;
/**
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class LoggerPassTest extends TestCase
{
public function testAlwaysSetAutowiringAlias()
{
$container = new ContainerBuilder();
$container->register('logger', 'Foo');
(new LoggerPass())->process($container);
$this->assertFalse($container->getAlias(LoggerInterface::class)->isPublic());
}
public function testDoNotOverrideExistingLogger()
{
$container = new ContainerBuilder();
$container->register('logger', 'Foo');
(new LoggerPass())->process($container);
$this->assertSame('Foo', $container->getDefinition('logger')->getClass());
}
public function testRegisterLogger()
{
$container = new ContainerBuilder();
$container->setParameter('kernel.debug', false);
(new LoggerPass())->process($container);
$definition = $container->getDefinition('logger');
$this->assertSame(Logger::class, $definition->getClass());
$this->assertFalse($definition->isPublic());
}
public function testSetMinLevelWhenDebugging()
{
$container = new ContainerBuilder();
$container->setParameter('kernel.debug', true);
(new LoggerPass())->process($container);
$definition = $container->getDefinition('logger');
$this->assertSame(LogLevel::DEBUG, $definition->getArgument(0));
}
}
Oops, something went wrong.

0 comments on commit 09afa64

Please sign in to comment.