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\Formatter\FormatterInterface;
16use Monolog\Formatter\LogglyFormatter;
17use function array_key_exists;
18use CurlHandle;
19
20/**
21 * Sends errors to Loggly.
22 *
23 * @author Przemek Sobstel <przemek@sobstel.org>
24 * @author Adam Pancutt <adam@pancutt.com>
25 * @author Gregory Barchard <gregory@barchard.net>
26 */
27class LogglyHandler extends AbstractProcessingHandler
28{
29    protected const HOST = 'logs-01.loggly.com';
30    protected const ENDPOINT_SINGLE = 'inputs';
31    protected const ENDPOINT_BATCH = 'bulk';
32
33    /**
34     * Caches the curl handlers for every given endpoint.
35     *
36     * @var resource[]|CurlHandle[]
37     */
38    protected $curlHandlers = [];
39
40    /** @var string */
41    protected $token;
42
43    /** @var string[] */
44    protected $tag = [];
45
46    /**
47     * @param string $token API token supplied by Loggly
48     *
49     * @throws MissingExtensionException If the curl extension is missing
50     */
51    public function __construct(string $token, $level = Logger::DEBUG, bool $bubble = true)
52    {
53        if (!extension_loaded('curl')) {
54            throw new MissingExtensionException('The curl extension is needed to use the LogglyHandler');
55        }
56
57        $this->token = $token;
58
59        parent::__construct($level, $bubble);
60    }
61
62    /**
63     * Loads and returns the shared curl handler for the given endpoint.
64     *
65     * @param string $endpoint
66     *
67     * @return resource|CurlHandle
68     */
69    protected function getCurlHandler(string $endpoint)
70    {
71        if (!array_key_exists($endpoint, $this->curlHandlers)) {
72            $this->curlHandlers[$endpoint] = $this->loadCurlHandle($endpoint);
73        }
74
75        return $this->curlHandlers[$endpoint];
76    }
77
78    /**
79     * Starts a fresh curl session for the given endpoint and returns its handler.
80     *
81     * @param string $endpoint
82     *
83     * @return resource|CurlHandle
84     */
85    private function loadCurlHandle(string $endpoint)
86    {
87        $url = sprintf("https://%s/%s/%s/", static::HOST, $endpoint, $this->token);
88
89        $ch = curl_init();
90
91        curl_setopt($ch, CURLOPT_URL, $url);
92        curl_setopt($ch, CURLOPT_POST, true);
93        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
94
95        return $ch;
96    }
97
98    /**
99     * @param string[]|string $tag
100     */
101    public function setTag($tag): self
102    {
103        $tag = !empty($tag) ? $tag : [];
104        $this->tag = is_array($tag) ? $tag : [$tag];
105
106        return $this;
107    }
108
109    /**
110     * @param string[]|string $tag
111     */
112    public function addTag($tag): self
113    {
114        if (!empty($tag)) {
115            $tag = is_array($tag) ? $tag : [$tag];
116            $this->tag = array_unique(array_merge($this->tag, $tag));
117        }
118
119        return $this;
120    }
121
122    protected function write(array $record): void
123    {
124        $this->send($record["formatted"], static::ENDPOINT_SINGLE);
125    }
126
127    public function handleBatch(array $records): void
128    {
129        $level = $this->level;
130
131        $records = array_filter($records, function ($record) use ($level) {
132            return ($record['level'] >= $level);
133        });
134
135        if ($records) {
136            $this->send($this->getFormatter()->formatBatch($records), static::ENDPOINT_BATCH);
137        }
138    }
139
140    protected function send(string $data, string $endpoint): void
141    {
142        $ch = $this->getCurlHandler($endpoint);
143
144        $headers = ['Content-Type: application/json'];
145
146        if (!empty($this->tag)) {
147            $headers[] = 'X-LOGGLY-TAG: '.implode(',', $this->tag);
148        }
149
150        curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
151        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
152
153        Curl\Util::execute($ch, 5, false);
154    }
155
156    protected function getDefaultFormatter(): FormatterInterface
157    {
158        return new LogglyFormatter();
159    }
160}
161