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