3 namespace MediaWiki\Logger\Monolog
;
6 * Modified version of Monolog\Formatter\LogstashFormatter
8 * - Squash the base message array, the context and extra subarrays into one.
9 * This can result in unfortunately named context fields overwriting other data (T145133).
10 * - Improve exception JSON-ification, which is done poorly by the standard class.
15 class LogstashFormatter
extends \Monolog\Formatter\LogstashFormatter
{
20 /** @var array Keys which should not be used in log context */
21 protected $reservedKeys = [
22 // from LogstashFormatter
23 'message', 'channel', 'level', 'type',
25 'url', 'ip', 'http_method', 'server', 'referrer',
27 'host', 'wiki', 'reqId', 'mwversion',
33 * @var int Logstash format version to use
38 * TODO: See T247675 for removing this override.
40 * @param string $applicationName The application that sends the data, used as the "type"
42 * @param string|null $systemName The system/machine name, used as the "source" field of
43 * logstash, defaults to the hostname of the machine
44 * @param string $extraKey The key for extra keys inside logstash "fields", defaults to ''
45 * @param string $contextKey The key for context keys inside logstash "fields", defaults
46 * @param int $version The logstash format version to use, defaults to V0
49 public function __construct( string $applicationName, ?
string $systemName = null,
50 string $extraKey = '', string $contextKey = 'ctxt_', $version = self
::V0
52 $this->version
= $version;
53 parent
::__construct( $applicationName, $systemName, $extraKey, $contextKey );
56 public function format( array $record ): string {
57 $record = \Monolog\Formatter\NormalizerFormatter
::format( $record );
58 if ( $this->version
=== self
::V1
) {
59 $message = $this->formatV1( $record );
60 } elseif ( $this->version
=== self
::V0
) {
61 $message = $this->formatV0( $record );
63 $message = __METHOD__
. ' unknown version ' . $this->version
;
66 return $this->toJson( $message ) . "\n";
70 * Prevent key conflicts
71 * @param array $record
74 protected function formatV0( array $record ) {
75 if ( $this->contextKey
!== '' ) {
76 return $this->formatMonologV0( $record );
79 $context = !empty( $record['context'] ) ?
$record['context'] : [];
80 $record['context'] = [];
81 $formatted = $this->formatMonologV0( $record );
83 $formatted['@fields'] = $this->fixKeyConflicts( $formatted['@fields'], $context );
89 * Borrowed from monolog/monolog 1.25.3
90 * https://github.com/Seldaek/monolog/blob/1.x/src/Monolog/Formatter/LogstashFormatter.php#L87-L128
92 * @param array $record
95 protected function formatMonologV0( array $record ) {
96 if ( empty( $record['datetime'] ) ) {
97 $record['datetime'] = gmdate( 'c' );
100 '@timestamp' => $record['datetime'],
101 '@source' => $this->systemName
,
104 if ( isset( $record['message'] ) ) {
105 $message['@message'] = $record['message'];
107 if ( isset( $record['channel'] ) ) {
108 $message['@tags'] = [ $record['channel'] ];
109 $message['@fields']['channel'] = $record['channel'];
111 if ( isset( $record['level'] ) ) {
112 $message['@fields']['level'] = $record['level'];
114 if ( $this->applicationName
) {
115 $message['@type'] = $this->applicationName
;
117 if ( isset( $record['extra']['server'] ) ) {
118 $message['@source_host'] = $record['extra']['server'];
120 if ( isset( $record['extra']['url'] ) ) {
121 $message['@source_path'] = $record['extra']['url'];
123 if ( !empty( $record['extra'] ) ) {
124 foreach ( $record['extra'] as $key => $val ) {
125 $message['@fields'][$this->extraKey
. $key] = $val;
128 if ( !empty( $record['context'] ) ) {
129 foreach ( $record['context'] as $key => $val ) {
130 $message['@fields'][$this->contextKey
. $key] = $val;
138 * Prevent key conflicts
139 * @param array $record
142 protected function formatV1( array $record ) {
143 if ( $this->contextKey
) {
144 return $this->formatMonologV1( $record );
147 $context = !empty( $record['context'] ) ?
$record['context'] : [];
148 $record['context'] = [];
149 $formatted = $this->formatMonologV1( $record );
151 return $this->fixKeyConflicts( $formatted, $context );
155 * Borrowed mostly from monolog/monolog 1.25.3
156 * https://github.com/Seldaek/monolog/blob/1.25.3/src/Monolog/Formatter/LogstashFormatter.php#L130-165
158 * @param array $record
161 protected function formatMonologV1( array $record ) {
162 if ( empty( $record['datetime'] ) ) {
163 $record['datetime'] = gmdate( 'c' );
166 '@timestamp' => $record['datetime'],
168 'host' => $this->systemName
,
170 if ( isset( $record['message'] ) ) {
171 $message['message'] = $record['message'];
173 if ( isset( $record['channel'] ) ) {
174 $message['type'] = $record['channel'];
175 $message['channel'] = $record['channel'];
177 if ( isset( $record['level_name'] ) ) {
178 $message['level'] = $record['level_name'];
180 // level -> monolog_level is new in 2.0
181 // https://github.com/Seldaek/monolog/blob/2.0.2/src/Monolog/Formatter/LogstashFormatter.php#L86-L88
182 if ( isset( $record['level'] ) ) {
183 $message['monolog_level'] = $record['level'];
185 if ( $this->applicationName
) {
186 $message['type'] = $this->applicationName
;
188 if ( !empty( $record['extra'] ) ) {
189 foreach ( $record['extra'] as $key => $val ) {
190 $message[$this->extraKey
. $key] = $val;
193 if ( !empty( $record['context'] ) ) {
194 foreach ( $record['context'] as $key => $val ) {
195 $message[$this->contextKey
. $key] = $val;
203 * Rename any context field that would otherwise overwrite a message key.
205 * @param array $fields Fields to be sent to logstash
206 * @param array $context Copy of the original $record['context']
207 * @return array Updated version of $fields
209 protected function fixKeyConflicts( array $fields, array $context ) {
210 foreach ( $context as $key => $val ) {
212 in_array( $key, $this->reservedKeys
, true ) &&
213 isset( $fields[$key] ) && $fields[$key] !== $val
215 $fields['logstash_formatter_key_conflict'][] = $key;
218 $fields[$key] = $val;
224 * Use a more user-friendly trace format than Monolog\Formatter\NormalizerFormatter.
226 * @param \Throwable $e
230 protected function normalizeException( \Throwable
$e, int $depth = 0 ) {
232 'class' => get_class( $e ),
233 'message' => $e->getMessage(),
234 'code' => $e->getCode(),
235 'file' => $e->getFile() . ':' . $e->getLine(),
236 'trace' => \MWExceptionHandler
::getRedactedTraceAsString( $e ),
239 $previous = $e->getPrevious();
241 $data['previous'] = $this->normalizeException( $previous );