diff options
-rw-r--r-- | includes/debug/logger/monolog/LogstashFormatter.php | 110 | ||||
-rw-r--r-- | tests/phpunit/unit/includes/debug/logger/monolog/LogstashFormatterTest.php | 8 |
2 files changed, 103 insertions, 15 deletions
diff --git a/includes/debug/logger/monolog/LogstashFormatter.php b/includes/debug/logger/monolog/LogstashFormatter.php index 3740d4121bf6..178562670b2f 100644 --- a/includes/debug/logger/monolog/LogstashFormatter.php +++ b/includes/debug/logger/monolog/LogstashFormatter.php @@ -11,6 +11,10 @@ namespace MediaWiki\Logger\Monolog; * @since 1.29 */ class LogstashFormatter extends \Monolog\Formatter\LogstashFormatter { + + public const V0 = 0; + public const V1 = 1; + /** @var array Keys which should not be used in log context */ protected $reservedKeys = [ // from LogstashFormatter @@ -24,9 +28,12 @@ class LogstashFormatter extends \Monolog\Formatter\LogstashFormatter { ]; /** - * Constructor exists to keep the default values for $extraKey and $contextKey as they were - * in monolog/monolog 1.25.3 (null), rather than the populated default string that they are - * in >=2.0. See T247675 for removing this override. + * @var int Logstash format version to use + */ + protected $version; + + /** + * See T247675 for removing this override. * * @param string $applicationName The application that sends the data, used as the "type" * field of logstash @@ -34,41 +41,120 @@ class LogstashFormatter extends \Monolog\Formatter\LogstashFormatter { * logstash, defaults to the hostname of the machine * @param string $extraKey The key for extra keys inside logstash "fields", defaults to '' * @param string $contextKey The key for context keys inside logstash "fields", defaults + * @param int $version The logstash format version to use, defaults to V0 * to '' */ public function __construct( string $applicationName, ?string $systemName = null, - string $extraKey = '', string $contextKey = '' + string $extraKey = '', string $contextKey = 'ctxt_', $version = self::V0 ) { + $this->version = $version; parent::__construct( $applicationName, $systemName, $extraKey, $contextKey ); } + public function format( array $record ): string { + $record = \Monolog\Formatter\NormalizerFormatter::format( $record ); + if ( $this->version === self::V1 ) { + $message = $this->formatv1( $record ); + } elseif ( $this->version === self::V0 ) { + $message = $this->formatV0( $record ); + } + + return $this->toJson( $message ) . "\n"; + } + /** * Prevent key conflicts * @param array $record - * @return string + * @return array */ - public function format( array $record ): string { + protected function formatV0( array $record ) { if ( $this->contextKey !== '' ) { - return $this->toJson( $this->formatV1( $record ) ) . "\n"; + return $this->formatMonologV0( $record ); } $context = !empty( $record['context'] ) ? $record['context'] : []; $record['context'] = []; + $formatted = $this->formatMonologV0( $record ); - $formatted = $this->formatV1( $record ); + $formatted['@fields'] = $this->fixKeyConflicts( $formatted['@fields'], $context ); - return $this->toJson( $this->fixKeyConflicts( $formatted, $context ) ) . "\n"; + return $formatted; } /** * Borrowed from monolog/monolog 1.25.3 - * https://github.com/Seldaek/monolog/blob/1.25.3/src/Monolog/Formatter/LogstashFormatter.php#L130-L165 - * https://github.com/Seldaek/monolog/blob/2.0.0/src/Monolog/Formatter/LogstashFormatter.php#L86-L88 + * https://github.com/Seldaek/monolog/blob/1.x/src/Monolog/Formatter/LogstashFormatter.php#L87-L128 * * @param array $record * @return array */ + protected function formatMonologV0( array $record ) { + if ( empty( $record['datetime'] ) ) { + $record['datetime'] = gmdate( 'c' ); + } + $message = [ + '@timestamp' => $record['datetime'], + '@source' => $this->systemName, + '@fields' => [], + ]; + if ( isset( $record['message'] ) ) { + $message['@message'] = $record['message']; + } + if ( isset( $record['channel'] ) ) { + $message['@tags'] = [ $record['channel'] ]; + $message['@fields']['channel'] = $record['channel']; + } + if ( isset( $record['level'] ) ) { + $message['@fields']['level'] = $record['level']; + } + if ( $this->applicationName ) { + $message['@type'] = $this->applicationName; + } + if ( isset( $record['extra']['server'] ) ) { + $message['@source_host'] = $record['extra']['server']; + } + if ( isset( $record['extra']['url'] ) ) { + $message['@source_path'] = $record['extra']['url']; + } + if ( !empty( $record['extra'] ) ) { + foreach ( $record['extra'] as $key => $val ) { + $message['@fields'][$this->extraKey . $key] = $val; + } + } + if ( !empty( $record['context'] ) ) { + foreach ( $record['context'] as $key => $val ) { + $message['@fields'][$this->contextKey . $key] = $val; + } + } + + return $message; + } + + /** + * Prevent key conflicts + * @param array $record + * @return array + */ protected function formatV1( array $record ) { + if ( $this->contextKey ) { + return $this->formatMonologV1( $record ); + } + + $context = !empty( $record['context'] ) ? $record['context'] : []; + $record['context'] = []; + $formatted = $this->formatMonologV1( $record ); + + return $this->fixKeyConflicts( $formatted, $context ); + } + + /** + * Borrowed mostly from monolog/monolog 1.25.3 + * https://github.com/Seldaek/monolog/blob/1.25.3/src/Monolog/Formatter/LogstashFormatter.php#L130-165 + * + * @param array $record + * @return array + */ + protected function formatMonologV1( array $record ) { if ( empty( $record['datetime'] ) ) { $record['datetime'] = gmdate( 'c' ); } @@ -87,6 +173,8 @@ class LogstashFormatter extends \Monolog\Formatter\LogstashFormatter { if ( isset( $record['level_name'] ) ) { $message['level'] = $record['level_name']; } + // level -> monolog_level is new in 2.0 + // https://github.com/Seldaek/monolog/blob/2.0.2/src/Monolog/Formatter/LogstashFormatter.php#L86-L88 if ( isset( $record['level'] ) ) { $message['monolog_level'] = $record['level']; } diff --git a/tests/phpunit/unit/includes/debug/logger/monolog/LogstashFormatterTest.php b/tests/phpunit/unit/includes/debug/logger/monolog/LogstashFormatterTest.php index 54a08508385e..fe015f2abd85 100644 --- a/tests/phpunit/unit/includes/debug/logger/monolog/LogstashFormatterTest.php +++ b/tests/phpunit/unit/includes/debug/logger/monolog/LogstashFormatterTest.php @@ -5,13 +5,13 @@ namespace MediaWiki\Logger\Monolog; class LogstashFormatterTest extends \MediaWikiUnitTestCase { /** * @dataProvider provideV1 - * @covers MediaWiki\Logger\Monolog\LogstashFormatter::format + * @covers \MediaWiki\Logger\Monolog\LogstashFormatter::format * @param array $record The input record. * @param array $expected Associative array of expected keys and their values. * @param array $notExpected List of keys that should not exist. */ public function testV1( array $record, array $expected, array $notExpected ) { - $formatter = new LogstashFormatter( 'app', 'system' ); + $formatter = new LogstashFormatter( 'app', 'system', '', '', LogstashFormatter::V1 ); $formatted = json_decode( $formatter->format( $record ), true ); foreach ( $expected as $key => $value ) { $this->assertArrayHasKey( $key, $formatted ); @@ -44,10 +44,10 @@ class LogstashFormatterTest extends \MediaWikiUnitTestCase { } /** - * @covers MediaWiki\Logger\Monolog\LogstashFormatter::format + * @covers \MediaWiki\Logger\Monolog\LogstashFormatter::format */ public function testV1WithPrefix() { - $formatter = new LogstashFormatter( 'app', 'system', '', 'ctx_' ); + $formatter = new LogstashFormatter( 'app', 'system', '', 'ctx_', LogstashFormatter::V1 ); $record = [ 'extra' => [ 'url' => 1 ], 'context' => [ 'url' => 2 ] ]; $formatted = json_decode( $formatter->format( $record ), true ); $this->assertArrayHasKey( 'url', $formatted ); |