Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 72 |
|
0.00% |
0 / 6 |
CRAP | |
0.00% |
0 / 1 |
LegacyHandler | |
0.00% |
0 / 72 |
|
0.00% |
0 / 6 |
600 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
openSink | |
0.00% |
0 / 32 |
|
0.00% |
0 / 1 |
72 | |||
errorTrap | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
useUdp | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
write | |
0.00% |
0 / 29 |
|
0.00% |
0 / 1 |
110 | |||
close | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 |
1 | <?php |
2 | /** |
3 | * This program is free software; you can redistribute it and/or modify |
4 | * it under the terms of the GNU General Public License as published by |
5 | * the Free Software Foundation; either version 2 of the License, or |
6 | * (at your option) any later version. |
7 | * |
8 | * This program is distributed in the hope that it will be useful, |
9 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
10 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
11 | * GNU General Public License for more details. |
12 | * |
13 | * You should have received a copy of the GNU General Public License along |
14 | * with this program; if not, write to the Free Software Foundation, Inc., |
15 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
16 | * http://www.gnu.org/copyleft/gpl.html |
17 | * |
18 | * @file |
19 | */ |
20 | |
21 | namespace MediaWiki\Logger\Monolog; |
22 | |
23 | use LogicException; |
24 | use MediaWiki\Logger\LegacyLogger; |
25 | use Monolog\Handler\AbstractProcessingHandler; |
26 | use Monolog\Logger; |
27 | use Socket; |
28 | use UnexpectedValueException; |
29 | |
30 | /** |
31 | * Monolog imitation of MediaWiki\Logger\LegacyLogger |
32 | * |
33 | * This replicates the behavior of LegacyLogger, which in turn replicates |
34 | * MediaWiki's former wfErrorLog() function. |
35 | * |
36 | * The main use case of the LegacyHandler is to enable adoption of Monolog |
37 | * features (such as alternate formatters, extra processors, and enabling multiple |
38 | * destinations/handlers at the same time), where one of the handlers (this one) |
39 | * essentiallly does what the LegacySpi would do if you hadn't enabled |
40 | * MonologSpi. In particular: writing to a file like $wgDebugLogFile, |
41 | * and sending messages to a PHP stream or udp2log server. |
42 | * |
43 | * For udp2log output, the stream specification must have the form: |
44 | * "udp://HOST:PORT[/PREFIX]" |
45 | * where: |
46 | * |
47 | * - HOST: IPv4, IPv6 or hostname |
48 | * - PORT: server port |
49 | * - PREFIX: optional (but recommended) prefix telling udp2log how to route |
50 | * the log event. The special prefix "{channel}" will use the log event's |
51 | * channel as the prefix value. |
52 | * |
53 | * When not targeting a udp2log server, this class will act as a drop-in |
54 | * replacement for \Monolog\Handler\StreamHandler. |
55 | * |
56 | * @since 1.25 |
57 | * @ingroup Debug |
58 | * @copyright © 2013 Wikimedia Foundation and contributors |
59 | */ |
60 | class LegacyHandler extends AbstractProcessingHandler { |
61 | |
62 | /** |
63 | * Log sink descriptor |
64 | * @var string |
65 | */ |
66 | protected $uri; |
67 | |
68 | /** |
69 | * Filter log events using legacy rules |
70 | * @var bool |
71 | */ |
72 | protected $useLegacyFilter; |
73 | |
74 | /** |
75 | * Log sink |
76 | * @var Socket|resource|null |
77 | */ |
78 | protected $sink; |
79 | |
80 | /** |
81 | * @var string|null |
82 | */ |
83 | protected $error; |
84 | |
85 | /** |
86 | * @var string |
87 | */ |
88 | protected $host; |
89 | |
90 | /** |
91 | * @var int |
92 | */ |
93 | protected $port; |
94 | |
95 | /** |
96 | * @var string |
97 | */ |
98 | protected $prefix; |
99 | |
100 | /** |
101 | * @param string $stream Stream URI |
102 | * @param bool $useLegacyFilter Filter log events using legacy rules |
103 | * @param int $level Minimum logging level that will trigger handler |
104 | * @param bool $bubble Can handled messages bubble up the handler stack? |
105 | */ |
106 | public function __construct( |
107 | $stream, |
108 | $useLegacyFilter = false, |
109 | $level = Logger::DEBUG, |
110 | $bubble = true |
111 | ) { |
112 | parent::__construct( $level, $bubble ); |
113 | $this->uri = $stream; |
114 | $this->useLegacyFilter = $useLegacyFilter; |
115 | } |
116 | |
117 | /** |
118 | * Open the log sink described by our stream URI. |
119 | */ |
120 | protected function openSink() { |
121 | if ( !$this->uri ) { |
122 | throw new LogicException( |
123 | 'Missing stream uri, the stream can not be opened.' ); |
124 | } |
125 | $this->error = null; |
126 | set_error_handler( [ $this, 'errorTrap' ] ); |
127 | |
128 | if ( str_starts_with( $this->uri, 'udp:' ) ) { |
129 | $parsed = parse_url( $this->uri ); |
130 | if ( !isset( $parsed['host'] ) ) { |
131 | throw new UnexpectedValueException( sprintf( |
132 | 'Udp transport "%s" must specify a host', $this->uri |
133 | ) ); |
134 | } |
135 | if ( !isset( $parsed['port'] ) ) { |
136 | throw new UnexpectedValueException( sprintf( |
137 | 'Udp transport "%s" must specify a port', $this->uri |
138 | ) ); |
139 | } |
140 | |
141 | $this->host = $parsed['host']; |
142 | $this->port = $parsed['port']; |
143 | $this->prefix = ''; |
144 | |
145 | if ( isset( $parsed['path'] ) ) { |
146 | $this->prefix = ltrim( $parsed['path'], '/' ); |
147 | } |
148 | |
149 | if ( filter_var( $this->host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6 ) ) { |
150 | $domain = AF_INET6; |
151 | |
152 | } else { |
153 | $domain = AF_INET; |
154 | } |
155 | |
156 | $this->sink = socket_create( $domain, SOCK_DGRAM, SOL_UDP ); |
157 | |
158 | } else { |
159 | $this->sink = fopen( $this->uri, 'a' ); |
160 | } |
161 | restore_error_handler(); |
162 | |
163 | if ( !$this->sink ) { |
164 | $this->sink = null; |
165 | throw new UnexpectedValueException( sprintf( |
166 | 'The stream or file "%s" could not be opened: %s', |
167 | // @phan-suppress-next-line PhanTypeMismatchArgumentInternalProbablyReal Set by error handler |
168 | $this->uri, $this->error |
169 | ) ); |
170 | } |
171 | } |
172 | |
173 | /** |
174 | * Custom error handler. |
175 | * @param int $code Error number |
176 | * @param string $msg Error message |
177 | */ |
178 | protected function errorTrap( $code, $msg ) { |
179 | $this->error = $msg; |
180 | } |
181 | |
182 | /** |
183 | * Should we use UDP to send messages to the sink? |
184 | * @return bool |
185 | */ |
186 | protected function useUdp() { |
187 | return $this->host !== null; |
188 | } |
189 | |
190 | protected function write( array $record ): void { |
191 | if ( $this->useLegacyFilter && |
192 | !LegacyLogger::shouldEmit( |
193 | $record['channel'], $record['message'], |
194 | $record['level'], $record |
195 | ) ) { |
196 | // Do not write record if we are enforcing legacy rules and they |
197 | // do not pass this message. This used to be done in isHandling(), |
198 | // but Monolog 1.12.0 made a breaking change that removed access |
199 | // to the needed channel and context information. |
200 | return; |
201 | } |
202 | |
203 | if ( $this->sink === null ) { |
204 | $this->openSink(); |
205 | } |
206 | |
207 | $text = (string)$record['formatted']; |
208 | if ( $this->useUdp() ) { |
209 | // Clean it up for the multiplexer |
210 | if ( $this->prefix !== '' ) { |
211 | $leader = ( $this->prefix === '{channel}' ) ? |
212 | $record['channel'] : $this->prefix; |
213 | $text = preg_replace( '/^/m', "{$leader} ", $text ); |
214 | |
215 | // Limit to 64 KiB |
216 | if ( strlen( $text ) > 65506 ) { |
217 | $text = substr( $text, 0, 65506 ); |
218 | } |
219 | |
220 | if ( !str_ends_with( $text, "\n" ) ) { |
221 | $text .= "\n"; |
222 | } |
223 | |
224 | } elseif ( strlen( $text ) > 65507 ) { |
225 | $text = substr( $text, 0, 65507 ); |
226 | } |
227 | |
228 | socket_sendto( |
229 | $this->sink, |
230 | $text, |
231 | strlen( $text ), |
232 | 0, |
233 | $this->host, |
234 | $this->port |
235 | ); |
236 | |
237 | } else { |
238 | fwrite( $this->sink, $text ); |
239 | } |
240 | } |
241 | |
242 | public function close(): void { |
243 | if ( $this->sink ) { |
244 | if ( $this->useUdp() ) { |
245 | socket_close( $this->sink ); |
246 | } else { |
247 | fclose( $this->sink ); |
248 | } |
249 | } |
250 | $this->sink = null; |
251 | } |
252 | } |