Merge "Add error checking for file argument"
[lhc/web/wiklou.git] / includes / debug / logger / monolog / LegacyHandler.php
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 UnexpectedValueException;
28
29 /**
30 * Log handler that replicates the behavior of MediaWiki's wfErrorLog()
31 * logging service. Log output can be directed to a local file, a PHP stream,
32 * or a udp2log server.
33 *
34 * For udp2log output, the stream specification must have the form:
35 * "udp://HOST:PORT[/PREFIX]"
36 * where:
37 * - HOST: IPv4, IPv6 or hostname
38 * - PORT: server port
39 * - PREFIX: optional (but recommended) prefix telling udp2log how to route
40 * the log event. The special prefix "{channel}" will use the log event's
41 * channel as the prefix value.
42 *
43 * When not targeting a udp2log stream this class will act as a drop-in
44 * replacement for \Monolog\Handler\StreamHandler.
45 *
46 * @since 1.25
47 * @author Bryan Davis <bd808@wikimedia.org>
48 * @copyright © 2013 Bryan Davis and Wikimedia Foundation.
49 */
50 class LegacyHandler extends AbstractProcessingHandler {
51
52 /**
53 * Log sink descriptor
54 * @var string $uri
55 */
56 protected $uri;
57
58 /**
59 * Filter log events using legacy rules
60 * @var bool $useLegacyFilter
61 */
62 protected $useLegacyFilter;
63
64 /**
65 * Log sink
66 * @var resource $sink
67 */
68 protected $sink;
69
70 /**
71 * @var string $error
72 */
73 protected $error;
74
75 /**
76 * @var string $host
77 */
78 protected $host;
79
80 /**
81 * @var int $port
82 */
83 protected $port;
84
85 /**
86 * @var string $prefix
87 */
88 protected $prefix;
89
90
91 /**
92 * @param string $stream Stream URI
93 * @param bool $useLegacyFilter Filter log events using legacy rules
94 * @param int $level Minimum logging level that will trigger handler
95 * @param bool $bubble Can handled meesages bubble up the handler stack?
96 */
97 public function __construct(
98 $stream,
99 $useLegacyFilter = false,
100 $level = Logger::DEBUG,
101 $bubble = true
102 ) {
103 parent::__construct( $level, $bubble );
104 $this->uri = $stream;
105 $this->useLegacyFilter = $useLegacyFilter;
106 }
107
108 /**
109 * Open the log sink described by our stream URI.
110 */
111 protected function openSink() {
112 if ( !$this->uri ) {
113 throw new LogicException(
114 'Missing stream uri, the stream can not be opened.' );
115 }
116 $this->error = null;
117 set_error_handler( array( $this, 'errorTrap' ) );
118
119 if ( substr( $this->uri, 0, 4 ) == 'udp:' ) {
120 $parsed = parse_url( $this->uri );
121 if ( !isset( $parsed['host'] ) ) {
122 throw new UnexpectedValueException( sprintf(
123 'Udp transport "%s" must specify a host', $this->uri
124 ) );
125 }
126 if ( !isset( $parsed['port'] ) ) {
127 throw new UnexpectedValueException( sprintf(
128 'Udp transport "%s" must specify a port', $this->uri
129 ) );
130 }
131
132 $this->host = $parsed['host'];
133 $this->port = $parsed['port'];
134 $this->prefix = '';
135
136 if ( isset( $parsed['path'] ) ) {
137 $this->prefix = ltrim( $parsed['path'], '/' );
138 }
139
140 if ( filter_var( $this->host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6 ) ) {
141 $domain = AF_INET6;
142
143 } else {
144 $domain = AF_INET;
145 }
146
147 $this->sink = socket_create( $domain, SOCK_DGRAM, SOL_UDP );
148
149 } else {
150 $this->sink = fopen( $this->uri, 'a' );
151 }
152 restore_error_handler();
153
154 if ( !is_resource( $this->sink ) ) {
155 $this->sink = null;
156 throw new UnexpectedValueException( sprintf(
157 'The stream or file "%s" could not be opened: %s',
158 $this->uri, $this->error
159 ) );
160 }
161 }
162
163
164 /**
165 * Custom error handler.
166 * @param int $code Error number
167 * @param string $msg Error message
168 */
169 protected function errorTrap( $code, $msg ) {
170 $this->error = $msg;
171 }
172
173
174 /**
175 * Should we use UDP to send messages to the sink?
176 * @return bool
177 */
178 protected function useUdp() {
179 return $this->host !== null;
180 }
181
182
183 protected function write( array $record ) {
184 if ( $this->useLegacyFilter &&
185 !LegacyLogger::shouldEmit(
186 $record['channel'], $record['message'],
187 $record['level'], $record
188 ) ) {
189 // Do not write record if we are enforcing legacy rules and they
190 // do not pass this message. This used to be done in isHandling(),
191 // but Monolog 1.12.0 made a breaking change that removed access
192 // to the needed channel and context information.
193 return;
194 }
195
196 if ( $this->sink === null ) {
197 $this->openSink();
198 }
199
200 $text = (string)$record['formatted'];
201 if ( $this->useUdp() ) {
202
203 // Clean it up for the multiplexer
204 if ( $this->prefix !== '' ) {
205 $leader = ( $this->prefix === '{channel}' ) ?
206 $record['channel'] : $this->prefix;
207 $text = preg_replace( '/^/m', "{$leader} ", $text );
208
209 // Limit to 64KB
210 if ( strlen( $text ) > 65506 ) {
211 $text = substr( $text, 0, 65506 );
212 }
213
214 if ( substr( $text, -1 ) != "\n" ) {
215 $text .= "\n";
216 }
217
218 } elseif ( strlen( $text ) > 65507 ) {
219 $text = substr( $text, 0, 65507 );
220 }
221
222 socket_sendto(
223 $this->sink, $text, strlen( $text ), 0, $this->host, $this->port
224 );
225
226 } else {
227 fwrite( $this->sink, $text );
228 }
229 }
230
231
232 public function close() {
233 if ( is_resource( $this->sink ) ) {
234 if ( $this->useUdp() ) {
235 socket_close( $this->sink );
236
237 } else {
238 fclose( $this->sink );
239 }
240 }
241 $this->sink = null;
242 }
243 }