Skip to content

Commit e36e40f

Browse files
committed
Add more options to log formatter, be more relax on context logging
1 parent 4fe77a9 commit e36e40f

File tree

4 files changed

+211
-62
lines changed

4 files changed

+211
-62
lines changed

src/Logger/Console.php

Lines changed: 9 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -49,15 +49,22 @@ class Console extends AbstractLogger
4949
private $formatter;
5050
private $handle;
5151

52-
//TODO use a WritableStreamInterface for output, see https://github.com/WyriHaximus/reactphp-psr-3-stdio/blob/master/src/StdioLogger.php
52+
/**
53+
* Console logger
54+
* TODO use a WritableStreamInterface for output, see https://github.com/WyriHaximus/reactphp-psr-3-stdio/blob/master/src/StdioLogger.php
55+
*
56+
* @param string $minLevel
57+
* @param string $output
58+
* @param callable|null $formatter Callable that will receive string $level, string $message, array $context and must return a string without throwing
59+
*/
5360
public function __construct(string $minLevel = LogLevel::WARNING, $output = 'php://stderr', callable $formatter = null)
5461
{
5562
if (!isset(self::$levels[$minLevel])) {
5663
throw new InvalidArgumentException(sprintf('The log level "%s" does not exist.', $minLevel));
5764
}
5865

5966
$this->minLevelIndex = self::$levels[$minLevel];
60-
$this->formatter = $formatter ?: array($this, 'format');
67+
$this->formatter = $formatter ?? new ConsoleFormatter();
6168
if (false === $this->handle = \is_resource($output) ? $output : @fopen($output, 'a')) {
6269
throw new InvalidArgumentException(sprintf('Unable to open "%s".', $output));
6370
}
@@ -81,61 +88,4 @@ public function log($level, $message, array $context = array())
8188
//TODO use WritableStreamInterface
8289
fwrite($this->handle, $formatter($level, $message, $context));
8390
}
84-
85-
private function format(string $level, string $message, array $context): string
86-
{
87-
$exception = null;
88-
if (isset($context['exception'])) {
89-
$exception = $context['exception'];
90-
unset($context['exception']);
91-
}
92-
93-
if (!empty($context)) {
94-
$message .= ' ' . json_encode($context);
95-
}
96-
97-
if (isset($exception)) {
98-
$message .= ' ' . $this->formatException($exception);
99-
}
100-
101-
return sprintf('%s [%s] %s', date(\DateTime::RFC3339), $level, $message) . \PHP_EOL;
102-
}
103-
104-
/**
105-
* Format an exception
106-
* @param \Throwable $e
107-
* @param string $newLine Optionnal new line char
108-
* @return string
109-
*/
110-
protected function formatException(\Throwable $e, $newLine = PHP_EOL): string
111-
{
112-
$message = '';
113-
114-
$currentException = $e;
115-
while (true) {
116-
if ($currentException === $e) {
117-
$message .= $this->_formatException($currentException);
118-
} else {
119-
$message .= $newLine . 'Caused by: ' . $this->_formatException($currentException, $newLine);
120-
}
121-
122-
$currentException = $currentException->getPrevious();
123-
if (!isset($currentException)) {
124-
break;
125-
}
126-
}
127-
return $message;
128-
}
129-
130-
/**
131-
* Format an exception as string
132-
*
133-
* @param \Throwable $e
134-
* @param string $newLine
135-
* @return string
136-
*/
137-
protected function _formatException(\Throwable $e, string $newLine = PHP_EOL): string
138-
{
139-
return get_class($e) . ': ' . $e->getMessage() . $newLine . '#> ' . $e->getFile() . '(' . $e->getLine() . ')' . $newLine . $e->getTraceAsString();
140-
}
14191
}

src/Logger/ConsoleFormatter.php

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<?php
2+
3+
/**
4+
* MIT License
5+
*
6+
* Copyright (c) 2019 Samuel CHEMLA
7+
*
8+
* Permission is hereby granted, free of charge, to any person obtaining a copy
9+
* of this software and associated documentation files (the "Software"), to deal
10+
* in the Software without restriction, including without limitation the rights
11+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12+
* copies of the Software, and to permit persons to whom the Software is
13+
* furnished to do so, subject to the following conditions:
14+
*
15+
* The above copyright notice and this permission notice shall be included in all
16+
* copies or substantial portions of the Software.
17+
*
18+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24+
* SOFTWARE.
25+
*/
26+
27+
namespace PhpBg\MiniHttpd\Logger;
28+
29+
class ConsoleFormatter
30+
{
31+
use ExceptionFormatterTrait;
32+
33+
/**
34+
* @var bool
35+
*/
36+
protected $withDate;
37+
38+
/**
39+
* ConsoleFormatter constructor
40+
*
41+
* @param bool $withDate Default: true; Wether to include a RFC3339 date in the message
42+
*/
43+
public function __construct(bool $withDate = true)
44+
{
45+
$this->withDate = $withDate;
46+
}
47+
48+
public function __invoke(string $level, string $message, array $context): string
49+
{
50+
$exception = null;
51+
if (isset($context['exception'])) {
52+
$exception = $context['exception'];
53+
unset($context['exception']);
54+
}
55+
56+
if (!empty($context)) {
57+
$message .= ' ' . \json_encode($context, JSON_PARTIAL_OUTPUT_ON_ERROR);
58+
$jsonMsg = \json_last_error_msg();
59+
if (!empty($jsonMsg)) {
60+
$message .= "<could not log full context because of: {$jsonMsg}>";
61+
}
62+
}
63+
64+
if (isset($exception)) {
65+
$message .= ' ' . $this->formatException($exception);
66+
}
67+
68+
if ($this->withDate) {
69+
return sprintf('%s [%s] %s', \date(\DateTime::RFC3339), $level, $message) . \PHP_EOL;
70+
} else {
71+
return sprintf('[%s] %s', $level, $message) . \PHP_EOL;
72+
}
73+
}
74+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<?php
2+
3+
/**
4+
* MIT License
5+
*
6+
* Copyright (c) 2019 Samuel CHEMLA
7+
*
8+
* Permission is hereby granted, free of charge, to any person obtaining a copy
9+
* of this software and associated documentation files (the "Software"), to deal
10+
* in the Software without restriction, including without limitation the rights
11+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12+
* copies of the Software, and to permit persons to whom the Software is
13+
* furnished to do so, subject to the following conditions:
14+
*
15+
* The above copyright notice and this permission notice shall be included in all
16+
* copies or substantial portions of the Software.
17+
*
18+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24+
* SOFTWARE.
25+
*/
26+
27+
namespace PhpBg\MiniHttpd\Logger;
28+
29+
trait ExceptionFormatterTrait
30+
{
31+
/**
32+
* Format an exception as a full stack trace string, including previous exceptions
33+
*
34+
* @param \Throwable $e
35+
* @param string $newLine Optionnal new line char, e.g "\r\n", or "<br>", default: PHP_EOL
36+
* @return string
37+
*/
38+
protected function formatException(\Throwable $e, $newLine = PHP_EOL): string
39+
{
40+
$message = '';
41+
42+
$currentException = $e;
43+
while (true) {
44+
if ($currentException === $e) {
45+
$message .= $this->_formatException($currentException);
46+
} else {
47+
$message .= $newLine . 'Caused by: ' . $this->_formatException($currentException, $newLine);
48+
}
49+
50+
$currentException = $currentException->getPrevious();
51+
if (!isset($currentException)) {
52+
break;
53+
}
54+
}
55+
return $message;
56+
}
57+
58+
/**
59+
* Format an exception as string
60+
*
61+
* @param \Throwable $e
62+
* @param string $newLine
63+
* @return string
64+
*/
65+
private function _formatException(\Throwable $e, string $newLine = PHP_EOL): string
66+
{
67+
return get_class($e) . ': ' . $e->getMessage() . $newLine . '#> ' . $e->getFile() . '(' . $e->getLine() . ')' . $newLine . $e->getTraceAsString();
68+
}
69+
}

tests/Logger/ConsoleTest.php

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,14 @@
2727
namespace PhpBg\MiniHttpd\Tests\Logger;
2828

2929
use PhpBg\MiniHttpd\Logger\Console;
30+
use PhpBg\MiniHttpd\Logger\ConsoleFormatter;
3031
use PHPUnit\Framework\TestCase;
3132
use Psr\Log\LogLevel;
3233

3334
class ConsoleTest extends TestCase
3435
{
35-
public function testLogException() {
36+
public function testLogException()
37+
{
3638
$output = fopen('php://memory', 'w+');
3739
$logger = new Console(LogLevel::DEBUG, $output);
3840

@@ -42,7 +44,8 @@ public function testLogException() {
4244
$this->assertNotEmpty(stream_get_contents($output));
4345
}
4446

45-
public function testLogExceptionWithPrevious() {
47+
public function testLogExceptionWithPrevious()
48+
{
4649
$output = fopen('php://memory', 'w+');
4750
$logger = new Console(LogLevel::DEBUG, $output);
4851

@@ -52,7 +55,8 @@ public function testLogExceptionWithPrevious() {
5255
$this->assertNotEmpty(stream_get_contents($output));
5356
}
5457

55-
public function testLogError() {
58+
public function testLogError()
59+
{
5660
$output = fopen('php://memory', 'w+');
5761
$logger = new Console(LogLevel::DEBUG, $output);
5862

@@ -62,4 +66,56 @@ public function testLogError() {
6266
$this->assertNotEmpty(stream_get_contents($output));
6367
}
6468

69+
public function testLogContextNotJsonEncodable()
70+
{
71+
$output = fopen('php://memory', 'w+');
72+
$logger = new Console(LogLevel::DEBUG, $output);
73+
74+
$logger->error("foo", ['bar' => \log(0)]);
75+
76+
rewind($output);
77+
$msg = stream_get_contents($output);
78+
$this->assertNotEmpty($msg);
79+
$this->assertTrue(strpos($msg, 'could not log full context') !== false);
80+
}
81+
82+
public function testLogStartWithDate()
83+
{
84+
$output = fopen('php://memory', 'w+');
85+
$logger = new Console(LogLevel::DEBUG, $output);
86+
87+
$logger->error("foo");
88+
89+
rewind($output);
90+
$msg = stream_get_contents($output);
91+
$this->assertNotEmpty($msg);
92+
$this->assertSame(1, preg_match('/^[0-9]{4}-[0-9]{2}-[0-9]{2}/', $msg));
93+
}
94+
95+
public function testLogContainLevel()
96+
{
97+
$output = fopen('php://memory', 'w+');
98+
$logger = new Console(LogLevel::DEBUG, $output);
99+
100+
$logger->error("foo");
101+
102+
rewind($output);
103+
$msg = stream_get_contents($output);
104+
$this->assertNotEmpty($msg);
105+
$this->assertTrue(strrpos($msg, LogLevel::ERROR) !== false);
106+
}
107+
108+
public function testCustomFormatterWithoutDate()
109+
{
110+
$output = fopen('php://memory', 'w+');
111+
$logger = new Console(LogLevel::DEBUG, $output, new ConsoleFormatter(false));
112+
113+
$logger->error("foo");
114+
115+
rewind($output);
116+
$msg = stream_get_contents($output);
117+
$this->assertNotEmpty($msg);
118+
$this->assertTrue(strrpos($msg, LogLevel::ERROR) !== false);
119+
$this->assertSame(0, preg_match('/^[0-9]{4}-[0-9]{2}-[0-9]{2}/', $msg));
120+
}
65121
}

0 commit comments

Comments
 (0)