Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
4.35% |
4 / 92 |
|
0.00% |
0 / 8 |
CRAP | |
0.00% |
0 / 1 |
LogstashFormatter | |
4.35% |
4 / 92 |
|
0.00% |
0 / 8 |
1440.25 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
format | |
57.14% |
4 / 7 |
|
0.00% |
0 / 1 |
3.71 | |||
formatV0 | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
12 | |||
formatMonologV0 | |
0.00% |
0 / 27 |
|
0.00% |
0 / 1 |
156 | |||
formatV1 | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
formatMonologV1 | |
0.00% |
0 / 25 |
|
0.00% |
0 / 1 |
132 | |||
fixKeyConflicts | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
30 | |||
normalizeException | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
6 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Logger\Monolog; |
4 | |
5 | /** |
6 | * Modified version of Monolog\Formatter\LogstashFormatter |
7 | * |
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. |
11 | * |
12 | * @since 1.29 |
13 | * @ingroup Debug |
14 | */ |
15 | class LogstashFormatter extends \Monolog\Formatter\LogstashFormatter { |
16 | |
17 | public const V0 = 0; |
18 | public const V1 = 1; |
19 | |
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', |
30 | ]; |
31 | |
32 | /** |
33 | * @var int Logstash format version to use |
34 | */ |
35 | protected $version; |
36 | |
37 | /** |
38 | * TODO: See T247675 for removing this override. |
39 | * |
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 '' |
48 | */ |
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 ); |
54 | } |
55 | |
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; |
64 | } |
65 | |
66 | return $this->toJson( $message ) . "\n"; |
67 | } |
68 | |
69 | /** |
70 | * Prevent key conflicts |
71 | * @param array $record |
72 | * @return array |
73 | */ |
74 | protected function formatV0( array $record ) { |
75 | if ( $this->contextKey !== '' ) { |
76 | return $this->formatMonologV0( $record ); |
77 | } |
78 | |
79 | $context = !empty( $record['context'] ) ? $record['context'] : []; |
80 | $record['context'] = []; |
81 | $formatted = $this->formatMonologV0( $record ); |
82 | |
83 | $formatted['@fields'] = $this->fixKeyConflicts( $formatted['@fields'], $context ); |
84 | |
85 | return $formatted; |
86 | } |
87 | |
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 |
91 | * |
92 | * @param array $record |
93 | * @return array |
94 | */ |
95 | protected function formatMonologV0( array $record ) { |
96 | if ( empty( $record['datetime'] ) ) { |
97 | $record['datetime'] = gmdate( 'c' ); |
98 | } |
99 | $message = [ |
100 | '@timestamp' => $record['datetime'], |
101 | '@source' => $this->systemName, |
102 | '@fields' => [], |
103 | ]; |
104 | if ( isset( $record['message'] ) ) { |
105 | $message['@message'] = $record['message']; |
106 | } |
107 | if ( isset( $record['channel'] ) ) { |
108 | $message['@tags'] = [ $record['channel'] ]; |
109 | $message['@fields']['channel'] = $record['channel']; |
110 | } |
111 | if ( isset( $record['level'] ) ) { |
112 | $message['@fields']['level'] = $record['level']; |
113 | } |
114 | if ( $this->applicationName ) { |
115 | $message['@type'] = $this->applicationName; |
116 | } |
117 | if ( isset( $record['extra']['server'] ) ) { |
118 | $message['@source_host'] = $record['extra']['server']; |
119 | } |
120 | if ( isset( $record['extra']['url'] ) ) { |
121 | $message['@source_path'] = $record['extra']['url']; |
122 | } |
123 | if ( !empty( $record['extra'] ) ) { |
124 | foreach ( $record['extra'] as $key => $val ) { |
125 | $message['@fields'][$this->extraKey . $key] = $val; |
126 | } |
127 | } |
128 | if ( !empty( $record['context'] ) ) { |
129 | foreach ( $record['context'] as $key => $val ) { |
130 | $message['@fields'][$this->contextKey . $key] = $val; |
131 | } |
132 | } |
133 | |
134 | return $message; |
135 | } |
136 | |
137 | /** |
138 | * Prevent key conflicts |
139 | * @param array $record |
140 | * @return array |
141 | */ |
142 | protected function formatV1( array $record ) { |
143 | if ( $this->contextKey ) { |
144 | return $this->formatMonologV1( $record ); |
145 | } |
146 | |
147 | $context = !empty( $record['context'] ) ? $record['context'] : []; |
148 | $record['context'] = []; |
149 | $formatted = $this->formatMonologV1( $record ); |
150 | |
151 | return $this->fixKeyConflicts( $formatted, $context ); |
152 | } |
153 | |
154 | /** |
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 |
157 | * |
158 | * @param array $record |
159 | * @return array |
160 | */ |
161 | protected function formatMonologV1( array $record ) { |
162 | if ( empty( $record['datetime'] ) ) { |
163 | $record['datetime'] = gmdate( 'c' ); |
164 | } |
165 | $message = [ |
166 | '@timestamp' => $record['datetime'], |
167 | '@version' => 1, |
168 | 'host' => $this->systemName, |
169 | ]; |
170 | if ( isset( $record['message'] ) ) { |
171 | $message['message'] = $record['message']; |
172 | } |
173 | if ( isset( $record['channel'] ) ) { |
174 | $message['type'] = $record['channel']; |
175 | $message['channel'] = $record['channel']; |
176 | } |
177 | if ( isset( $record['level_name'] ) ) { |
178 | $message['level'] = $record['level_name']; |
179 | } |
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']; |
184 | } |
185 | if ( $this->applicationName ) { |
186 | $message['type'] = $this->applicationName; |
187 | } |
188 | if ( !empty( $record['extra'] ) ) { |
189 | foreach ( $record['extra'] as $key => $val ) { |
190 | $message[$this->extraKey . $key] = $val; |
191 | } |
192 | } |
193 | if ( !empty( $record['context'] ) ) { |
194 | foreach ( $record['context'] as $key => $val ) { |
195 | $message[$this->contextKey . $key] = $val; |
196 | } |
197 | } |
198 | |
199 | return $message; |
200 | } |
201 | |
202 | /** |
203 | * Rename any context field that would otherwise overwrite a message key. |
204 | * |
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 |
208 | */ |
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 |
214 | ) { |
215 | $fields['logstash_formatter_key_conflict'][] = $key; |
216 | $key = 'c_' . $key; |
217 | } |
218 | $fields[$key] = $val; |
219 | } |
220 | return $fields; |
221 | } |
222 | |
223 | /** |
224 | * Use a more user-friendly trace format than Monolog\Formatter\NormalizerFormatter. |
225 | * |
226 | * @param \Throwable $e |
227 | * @param int $depth |
228 | * @return array |
229 | */ |
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 ), |
237 | ]; |
238 | |
239 | $previous = $e->getPrevious(); |
240 | if ( $previous ) { |
241 | $data['previous'] = $this->normalizeException( $previous ); |
242 | } |
243 | |
244 | return $data; |
245 | } |
246 | } |