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\Formatter; 13 14use Monolog\Logger; 15use Gelf\Message; 16use Monolog\Utils; 17 18/** 19 * Serializes a log message to GELF 20 * @see http://docs.graylog.org/en/latest/pages/gelf.html 21 * 22 * @author Matt Lehner <mlehner@gmail.com> 23 * 24 * @phpstan-import-type Level from \Monolog\Logger 25 */ 26class GelfMessageFormatter extends NormalizerFormatter 27{ 28 protected const DEFAULT_MAX_LENGTH = 32766; 29 30 /** 31 * @var string the name of the system for the Gelf log message 32 */ 33 protected $systemName; 34 35 /** 36 * @var string a prefix for 'extra' fields from the Monolog record (optional) 37 */ 38 protected $extraPrefix; 39 40 /** 41 * @var string a prefix for 'context' fields from the Monolog record (optional) 42 */ 43 protected $contextPrefix; 44 45 /** 46 * @var int max length per field 47 */ 48 protected $maxLength; 49 50 /** 51 * Translates Monolog log levels to Graylog2 log priorities. 52 * 53 * @var array<int, int> 54 * 55 * @phpstan-var array<Level, int> 56 */ 57 private $logLevels = [ 58 Logger::DEBUG => 7, 59 Logger::INFO => 6, 60 Logger::NOTICE => 5, 61 Logger::WARNING => 4, 62 Logger::ERROR => 3, 63 Logger::CRITICAL => 2, 64 Logger::ALERT => 1, 65 Logger::EMERGENCY => 0, 66 ]; 67 68 public function __construct(?string $systemName = null, ?string $extraPrefix = null, string $contextPrefix = 'ctxt_', ?int $maxLength = null) 69 { 70 if (!class_exists(Message::class)) { 71 throw new \RuntimeException('Composer package graylog2/gelf-php is required to use Monolog\'s GelfMessageFormatter'); 72 } 73 74 parent::__construct('U.u'); 75 76 $this->systemName = (is_null($systemName) || $systemName === '') ? (string) gethostname() : $systemName; 77 78 $this->extraPrefix = is_null($extraPrefix) ? '' : $extraPrefix; 79 $this->contextPrefix = $contextPrefix; 80 $this->maxLength = is_null($maxLength) ? self::DEFAULT_MAX_LENGTH : $maxLength; 81 } 82 83 /** 84 * {@inheritDoc} 85 */ 86 public function format(array $record): Message 87 { 88 $context = $extra = []; 89 if (isset($record['context'])) { 90 /** @var mixed[] $context */ 91 $context = parent::normalize($record['context']); 92 } 93 if (isset($record['extra'])) { 94 /** @var mixed[] $extra */ 95 $extra = parent::normalize($record['extra']); 96 } 97 98 if (!isset($record['datetime'], $record['message'], $record['level'])) { 99 throw new \InvalidArgumentException('The record should at least contain datetime, message and level keys, '.var_export($record, true).' given'); 100 } 101 102 $message = new Message(); 103 $message 104 ->setTimestamp($record['datetime']) 105 ->setShortMessage((string) $record['message']) 106 ->setHost($this->systemName) 107 ->setLevel($this->logLevels[$record['level']]); 108 109 // message length + system name length + 200 for padding / metadata 110 $len = 200 + strlen((string) $record['message']) + strlen($this->systemName); 111 112 if ($len > $this->maxLength) { 113 $message->setShortMessage(Utils::substr($record['message'], 0, $this->maxLength)); 114 } 115 116 if (isset($record['channel'])) { 117 $message->setFacility($record['channel']); 118 } 119 if (isset($extra['line'])) { 120 $message->setLine($extra['line']); 121 unset($extra['line']); 122 } 123 if (isset($extra['file'])) { 124 $message->setFile($extra['file']); 125 unset($extra['file']); 126 } 127 128 foreach ($extra as $key => $val) { 129 $val = is_scalar($val) || null === $val ? $val : $this->toJson($val); 130 $len = strlen($this->extraPrefix . $key . $val); 131 if ($len > $this->maxLength) { 132 $message->setAdditional($this->extraPrefix . $key, Utils::substr((string) $val, 0, $this->maxLength)); 133 134 continue; 135 } 136 $message->setAdditional($this->extraPrefix . $key, $val); 137 } 138 139 foreach ($context as $key => $val) { 140 $val = is_scalar($val) || null === $val ? $val : $this->toJson($val); 141 $len = strlen($this->contextPrefix . $key . $val); 142 if ($len > $this->maxLength) { 143 $message->setAdditional($this->contextPrefix . $key, Utils::substr((string) $val, 0, $this->maxLength)); 144 145 continue; 146 } 147 $message->setAdditional($this->contextPrefix . $key, $val); 148 } 149 150 /** @phpstan-ignore-next-line */ 151 if (null === $message->getFile() && isset($context['exception']['file'])) { 152 if (preg_match("/^(.+):([0-9]+)$/", $context['exception']['file'], $matches)) { 153 $message->setFile($matches[1]); 154 $message->setLine($matches[2]); 155 } 156 } 157 158 return $message; 159 } 160} 161