Skip to content

Commit

Permalink
feature #24300 [HttpKernel][FrameworkBundle] Add a minimalist default…
Browse files Browse the repository at this point in the history
… 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 09afa64
Show file tree
Hide file tree
Showing 10 changed files with 433 additions and 0 deletions.
1 change: 1 addition & 0 deletions composer.json
Expand Up @@ -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": {
Expand Down
1 change: 1 addition & 0 deletions src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md
Expand Up @@ -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`
Expand Down
2 changes: 2 additions & 0 deletions src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php
Expand Up @@ -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;
Expand Down Expand Up @@ -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());
Expand Down
2 changes: 2 additions & 0 deletions src/Symfony/Component/Console/Logger/ConsoleLogger.php
Expand Up @@ -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()
{
Expand Down
1 change: 1 addition & 0 deletions src/Symfony/Component/HttpKernel/CHANGELOG.md
Expand Up @@ -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`
Expand Down
@@ -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);
}
}
}
98 changes: 98 additions & 0 deletions src/Symfony/Component/HttpKernel/Log/Logger.php
@@ -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));
}
}

0 comments on commit 09afa64

Please sign in to comment.