LegacyHandler.php 5.7 KB

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