1<?php declare(strict_types=1); 2 3/* 4 * This file is part of the Monolog package. 5 * 6 * (c) Jordi Boggiano <j.boggiano@seld.be> 7 * 8 * For the full copyright and license information, please view the LICENSE 9 * file that was distributed with this source code. 10 */ 11 12namespace Monolog\Handler; 13 14use Monolog\Logger; 15use Monolog\Utils; 16 17/** 18 * Logs to Cube. 19 * 20 * @link http://square.github.com/cube/ 21 * @author Wan Chen <kami@kamisama.me> 22 */ 23class CubeHandler extends AbstractProcessingHandler 24{ 25 /** @var resource|\Socket|null */ 26 private $udpConnection = null; 27 /** @var resource|\CurlHandle|null */ 28 private $httpConnection = null; 29 /** @var string */ 30 private $scheme; 31 /** @var string */ 32 private $host; 33 /** @var int */ 34 private $port; 35 /** @var string[] */ 36 private $acceptedSchemes = ['http', 'udp']; 37 38 /** 39 * Create a Cube handler 40 * 41 * @throws \UnexpectedValueException when given url is not a valid url. 42 * A valid url must consist of three parts : protocol://host:port 43 * Only valid protocols used by Cube are http and udp 44 */ 45 public function __construct(string $url, $level = Logger::DEBUG, bool $bubble = true) 46 { 47 $urlInfo = parse_url($url); 48 49 if ($urlInfo === false || !isset($urlInfo['scheme'], $urlInfo['host'], $urlInfo['port'])) { 50 throw new \UnexpectedValueException('URL "'.$url.'" is not valid'); 51 } 52 53 if (!in_array($urlInfo['scheme'], $this->acceptedSchemes)) { 54 throw new \UnexpectedValueException( 55 'Invalid protocol (' . $urlInfo['scheme'] . ').' 56 . ' Valid options are ' . implode(', ', $this->acceptedSchemes) 57 ); 58 } 59 60 $this->scheme = $urlInfo['scheme']; 61 $this->host = $urlInfo['host']; 62 $this->port = (int) $urlInfo['port']; 63 64 parent::__construct($level, $bubble); 65 } 66 67 /** 68 * Establish a connection to an UDP socket 69 * 70 * @throws \LogicException when unable to connect to the socket 71 * @throws MissingExtensionException when there is no socket extension 72 */ 73 protected function connectUdp(): void 74 { 75 if (!extension_loaded('sockets')) { 76 throw new MissingExtensionException('The sockets extension is required to use udp URLs with the CubeHandler'); 77 } 78 79 $udpConnection = socket_create(AF_INET, SOCK_DGRAM, 0); 80 if (false === $udpConnection) { 81 throw new \LogicException('Unable to create a socket'); 82 } 83 84 $this->udpConnection = $udpConnection; 85 if (!socket_connect($this->udpConnection, $this->host, $this->port)) { 86 throw new \LogicException('Unable to connect to the socket at ' . $this->host . ':' . $this->port); 87 } 88 } 89 90 /** 91 * Establish a connection to an http server 92 * 93 * @throws \LogicException when unable to connect to the socket 94 * @throws MissingExtensionException when no curl extension 95 */ 96 protected function connectHttp(): void 97 { 98 if (!extension_loaded('curl')) { 99 throw new MissingExtensionException('The curl extension is required to use http URLs with the CubeHandler'); 100 } 101 102 $httpConnection = curl_init('http://'.$this->host.':'.$this->port.'/1.0/event/put'); 103 if (false === $httpConnection) { 104 throw new \LogicException('Unable to connect to ' . $this->host . ':' . $this->port); 105 } 106 107 $this->httpConnection = $httpConnection; 108 curl_setopt($this->httpConnection, CURLOPT_CUSTOMREQUEST, "POST"); 109 curl_setopt($this->httpConnection, CURLOPT_RETURNTRANSFER, true); 110 } 111 112 /** 113 * {@inheritDoc} 114 */ 115 protected function write(array $record): void 116 { 117 $date = $record['datetime']; 118 119 $data = ['time' => $date->format('Y-m-d\TH:i:s.uO')]; 120 unset($record['datetime']); 121 122 if (isset($record['context']['type'])) { 123 $data['type'] = $record['context']['type']; 124 unset($record['context']['type']); 125 } else { 126 $data['type'] = $record['channel']; 127 } 128 129 $data['data'] = $record['context']; 130 $data['data']['level'] = $record['level']; 131 132 if ($this->scheme === 'http') { 133 $this->writeHttp(Utils::jsonEncode($data)); 134 } else { 135 $this->writeUdp(Utils::jsonEncode($data)); 136 } 137 } 138 139 private function writeUdp(string $data): void 140 { 141 if (!$this->udpConnection) { 142 $this->connectUdp(); 143 } 144 145 socket_send($this->udpConnection, $data, strlen($data), 0); 146 } 147 148 private function writeHttp(string $data): void 149 { 150 if (!$this->httpConnection) { 151 $this->connectHttp(); 152 } 153 154 if (null === $this->httpConnection) { 155 throw new \LogicException('No connection could be established'); 156 } 157 158 curl_setopt($this->httpConnection, CURLOPT_POSTFIELDS, '['.$data.']'); 159 curl_setopt($this->httpConnection, CURLOPT_HTTPHEADER, [ 160 'Content-Type: application/json', 161 'Content-Length: ' . strlen('['.$data.']'), 162 ]); 163 164 Curl\Util::execute($this->httpConnection, 5, false); 165 } 166} 167