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