MediaWiki  master
LogstashFormatter.php
Go to the documentation of this file.
1 <?php
2 
4 
13 class LogstashFormatter extends \Monolog\Formatter\LogstashFormatter {
14 
15  public const V0 = 0;
16  public const V1 = 1;
17 
19  protected $reservedKeys = [
20  // from LogstashFormatter
21  'message', 'channel', 'level', 'type',
22  // from WebProcessor
23  'url', 'ip', 'http_method', 'server', 'referrer',
24  // from WikiProcessor
25  'host', 'wiki', 'reqId', 'mwversion',
26  // from config magic
27  'normalized_message',
28  ];
29 
33  protected $version;
34 
47  public function __construct( string $applicationName, ?string $systemName = null,
48  string $extraKey = '', string $contextKey = 'ctxt_', $version = self::V0
49  ) {
50  $this->version = $version;
51  parent::__construct( $applicationName, $systemName, $extraKey, $contextKey );
52  }
53 
54  public function format( array $record ): string {
55  $record = \Monolog\Formatter\NormalizerFormatter::format( $record );
56  if ( $this->version === self::V1 ) {
57  $message = $this->formatv1( $record );
58  } elseif ( $this->version === self::V0 ) {
59  $message = $this->formatV0( $record );
60  } else {
61  $message = __METHOD__ . ' unknown version ' . $this->version;
62  }
63 
64  return $this->toJson( $message ) . "\n";
65  }
66 
72  protected function formatV0( array $record ) {
73  if ( $this->contextKey !== '' ) {
74  return $this->formatMonologV0( $record );
75  }
76 
77  $context = !empty( $record['context'] ) ? $record['context'] : [];
78  $record['context'] = [];
79  $formatted = $this->formatMonologV0( $record );
80 
81  $formatted['@fields'] = $this->fixKeyConflicts( $formatted['@fields'], $context );
82 
83  return $formatted;
84  }
85 
93  protected function formatMonologV0( array $record ) {
94  if ( empty( $record['datetime'] ) ) {
95  $record['datetime'] = gmdate( 'c' );
96  }
97  $message = [
98  '@timestamp' => $record['datetime'],
99  '@source' => $this->systemName,
100  '@fields' => [],
101  ];
102  if ( isset( $record['message'] ) ) {
103  $message['@message'] = $record['message'];
104  }
105  if ( isset( $record['channel'] ) ) {
106  $message['@tags'] = [ $record['channel'] ];
107  $message['@fields']['channel'] = $record['channel'];
108  }
109  if ( isset( $record['level'] ) ) {
110  $message['@fields']['level'] = $record['level'];
111  }
112  if ( $this->applicationName ) {
113  $message['@type'] = $this->applicationName;
114  }
115  if ( isset( $record['extra']['server'] ) ) {
116  $message['@source_host'] = $record['extra']['server'];
117  }
118  if ( isset( $record['extra']['url'] ) ) {
119  $message['@source_path'] = $record['extra']['url'];
120  }
121  if ( !empty( $record['extra'] ) ) {
122  foreach ( $record['extra'] as $key => $val ) {
123  $message['@fields'][$this->extraKey . $key] = $val;
124  }
125  }
126  if ( !empty( $record['context'] ) ) {
127  foreach ( $record['context'] as $key => $val ) {
128  $message['@fields'][$this->contextKey . $key] = $val;
129  }
130  }
131 
132  return $message;
133  }
134 
140  protected function formatV1( array $record ) {
141  if ( $this->contextKey ) {
142  return $this->formatMonologV1( $record );
143  }
144 
145  $context = !empty( $record['context'] ) ? $record['context'] : [];
146  $record['context'] = [];
147  $formatted = $this->formatMonologV1( $record );
148 
149  return $this->fixKeyConflicts( $formatted, $context );
150  }
151 
159  protected function formatMonologV1( array $record ) {
160  if ( empty( $record['datetime'] ) ) {
161  $record['datetime'] = gmdate( 'c' );
162  }
163  $message = [
164  '@timestamp' => $record['datetime'],
165  '@version' => 1,
166  'host' => $this->systemName,
167  ];
168  if ( isset( $record['message'] ) ) {
169  $message['message'] = $record['message'];
170  }
171  if ( isset( $record['channel'] ) ) {
172  $message['type'] = $record['channel'];
173  $message['channel'] = $record['channel'];
174  }
175  if ( isset( $record['level_name'] ) ) {
176  $message['level'] = $record['level_name'];
177  }
178  // level -> monolog_level is new in 2.0
179  // https://github.com/Seldaek/monolog/blob/2.0.2/src/Monolog/Formatter/LogstashFormatter.php#L86-L88
180  if ( isset( $record['level'] ) ) {
181  $message['monolog_level'] = $record['level'];
182  }
183  if ( $this->applicationName ) {
184  $message['type'] = $this->applicationName;
185  }
186  if ( !empty( $record['extra'] ) ) {
187  foreach ( $record['extra'] as $key => $val ) {
188  $message[$this->extraKey . $key] = $val;
189  }
190  }
191  if ( !empty( $record['context'] ) ) {
192  foreach ( $record['context'] as $key => $val ) {
193  $message[$this->contextKey . $key] = $val;
194  }
195  }
196 
197  return $message;
198  }
199 
207  protected function fixKeyConflicts( array $fields, array $context ) {
208  foreach ( $context as $key => $val ) {
209  if (
210  in_array( $key, $this->reservedKeys, true ) &&
211  isset( $fields[$key] ) && $fields[$key] !== $val
212  ) {
213  $fields['logstash_formatter_key_conflict'][] = $key;
214  $key = 'c_' . $key;
215  }
216  $fields[$key] = $val;
217  }
218  return $fields;
219  }
220 
228  protected function normalizeException( \Throwable $e, int $depth = 0 ) {
229  $data = [
230  'class' => get_class( $e ),
231  'message' => $e->getMessage(),
232  'code' => $e->getCode(),
233  'file' => $e->getFile() . ':' . $e->getLine(),
235  ];
236 
237  $previous = $e->getPrevious();
238  if ( $previous ) {
239  $data['previous'] = $this->normalizeException( $previous );
240  }
241 
242  return $data;
243  }
244 }
static getRedactedTraceAsString(Throwable $e)
Generate a string representation of a throwable's stack trace.
LogstashFormatter squashes the base message array and the context and extras subarrays into one.
formatV1(array $record)
Prevent key conflicts.
__construct(string $applicationName, ?string $systemName=null, string $extraKey='', string $contextKey='ctxt_', $version=self::V0)
TODO: See T247675 for removing this override.
formatMonologV0(array $record)
Borrowed from monolog/monolog 1.25.3 https://github.com/Seldaek/monolog/blob/1.x/src/Monolog/Formatte...
normalizeException(\Throwable $e, int $depth=0)
Use a more user-friendly trace format than Monolog\Formatter\NormalizerFormatter.
fixKeyConflicts(array $fields, array $context)
Rename any context field that would otherwise overwrite a message key.
formatV0(array $record)
Prevent key conflicts.
formatMonologV1(array $record)
Borrowed mostly from monolog/monolog 1.25.3 https://github.com/Seldaek/monolog/blob/1....
array $reservedKeys
Keys which should not be used in log context.
int $version
Logstash format version to use.