Localisation updates from https://translatewiki.net.
[mediawiki.git] / includes / debug / logger / monolog / LogstashFormatter.php
blobe3e2a20b36a54dcc5a0af94cd5d6cd63902fd5b5
1 <?php
3 namespace MediaWiki\Logger\Monolog;
5 /**
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.
12 * @since 1.29
13 * @ingroup Debug
15 class LogstashFormatter extends \Monolog\Formatter\LogstashFormatter {
17 public const V0 = 0;
18 public const V1 = 1;
20 /** @var array Keys which should not be used in log context */
21 protected $reservedKeys = [
22 // from LogstashFormatter
23 'message', 'channel', 'level', 'type',
24 // from WebProcessor
25 'url', 'ip', 'http_method', 'server', 'referrer',
26 // from WikiProcessor
27 'host', 'wiki', 'reqId', 'mwversion',
28 // from config magic
29 'normalized_message',
32 /**
33 * @var int Logstash format version to use
35 protected $version;
37 /**
38 * TODO: See T247675 for removing this override.
40 * @param string $applicationName The application that sends the data, used as the "type"
41 * field of logstash
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
47 * to ''
49 public function __construct( string $applicationName, ?string $systemName = null,
50 string $extraKey = '', string $contextKey = 'ctxt_', $version = self::V0
51 ) {
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 );
62 } else {
63 $message = __METHOD__ . ' unknown version ' . $this->version;
66 return $this->toJson( $message ) . "\n";
69 /**
70 * Prevent key conflicts
71 * @param array $record
72 * @return array
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 );
85 return $formatted;
88 /**
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
93 * @return array
95 protected function formatMonologV0( array $record ) {
96 if ( empty( $record['datetime'] ) ) {
97 $record['datetime'] = gmdate( 'c' );
99 $message = [
100 '@timestamp' => $record['datetime'],
101 '@source' => $this->systemName,
102 '@fields' => [],
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;
134 return $message;
138 * Prevent key conflicts
139 * @param array $record
140 * @return array
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
159 * @return array
161 protected function formatMonologV1( array $record ) {
162 if ( empty( $record['datetime'] ) ) {
163 $record['datetime'] = gmdate( 'c' );
165 $message = [
166 '@timestamp' => $record['datetime'],
167 '@version' => 1,
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;
199 return $message;
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 ) {
211 if (
212 in_array( $key, $this->reservedKeys, true ) &&
213 isset( $fields[$key] ) && $fields[$key] !== $val
215 $fields['logstash_formatter_key_conflict'][] = $key;
216 $key = 'c_' . $key;
218 $fields[$key] = $val;
220 return $fields;
224 * Use a more user-friendly trace format than Monolog\Formatter\NormalizerFormatter.
226 * @param \Throwable $e
227 * @param int $depth
228 * @return array
230 protected function normalizeException( \Throwable $e, int $depth = 0 ) {
231 $data = [
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();
240 if ( $previous ) {
241 $data['previous'] = $this->normalizeException( $previous );
244 return $data;