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; 16use Monolog\Formatter\NormalizerFormatter; 17use Monolog\Formatter\FormatterInterface; 18 19/** 20 * Class to record a log on a NewRelic application. 21 * Enabling New Relic High Security mode may prevent capture of useful information. 22 * 23 * This handler requires a NormalizerFormatter to function and expects an array in $record['formatted'] 24 * 25 * @see https://docs.newrelic.com/docs/agents/php-agent 26 * @see https://docs.newrelic.com/docs/accounts-partnerships/accounts/security/high-security 27 */ 28class NewRelicHandler extends AbstractProcessingHandler 29{ 30 /** 31 * Name of the New Relic application that will receive logs from this handler. 32 * 33 * @var ?string 34 */ 35 protected $appName; 36 37 /** 38 * Name of the current transaction 39 * 40 * @var ?string 41 */ 42 protected $transactionName; 43 44 /** 45 * Some context and extra data is passed into the handler as arrays of values. Do we send them as is 46 * (useful if we are using the API), or explode them for display on the NewRelic RPM website? 47 * 48 * @var bool 49 */ 50 protected $explodeArrays; 51 52 /** 53 * {@inheritDoc} 54 * 55 * @param string|null $appName 56 * @param bool $explodeArrays 57 * @param string|null $transactionName 58 */ 59 public function __construct( 60 $level = Logger::ERROR, 61 bool $bubble = true, 62 ?string $appName = null, 63 bool $explodeArrays = false, 64 ?string $transactionName = null 65 ) { 66 parent::__construct($level, $bubble); 67 68 $this->appName = $appName; 69 $this->explodeArrays = $explodeArrays; 70 $this->transactionName = $transactionName; 71 } 72 73 /** 74 * {@inheritDoc} 75 */ 76 protected function write(array $record): void 77 { 78 if (!$this->isNewRelicEnabled()) { 79 throw new MissingExtensionException('The newrelic PHP extension is required to use the NewRelicHandler'); 80 } 81 82 if ($appName = $this->getAppName($record['context'])) { 83 $this->setNewRelicAppName($appName); 84 } 85 86 if ($transactionName = $this->getTransactionName($record['context'])) { 87 $this->setNewRelicTransactionName($transactionName); 88 unset($record['formatted']['context']['transaction_name']); 89 } 90 91 if (isset($record['context']['exception']) && $record['context']['exception'] instanceof \Throwable) { 92 newrelic_notice_error($record['message'], $record['context']['exception']); 93 unset($record['formatted']['context']['exception']); 94 } else { 95 newrelic_notice_error($record['message']); 96 } 97 98 if (isset($record['formatted']['context']) && is_array($record['formatted']['context'])) { 99 foreach ($record['formatted']['context'] as $key => $parameter) { 100 if (is_array($parameter) && $this->explodeArrays) { 101 foreach ($parameter as $paramKey => $paramValue) { 102 $this->setNewRelicParameter('context_' . $key . '_' . $paramKey, $paramValue); 103 } 104 } else { 105 $this->setNewRelicParameter('context_' . $key, $parameter); 106 } 107 } 108 } 109 110 if (isset($record['formatted']['extra']) && is_array($record['formatted']['extra'])) { 111 foreach ($record['formatted']['extra'] as $key => $parameter) { 112 if (is_array($parameter) && $this->explodeArrays) { 113 foreach ($parameter as $paramKey => $paramValue) { 114 $this->setNewRelicParameter('extra_' . $key . '_' . $paramKey, $paramValue); 115 } 116 } else { 117 $this->setNewRelicParameter('extra_' . $key, $parameter); 118 } 119 } 120 } 121 } 122 123 /** 124 * Checks whether the NewRelic extension is enabled in the system. 125 * 126 * @return bool 127 */ 128 protected function isNewRelicEnabled(): bool 129 { 130 return extension_loaded('newrelic'); 131 } 132 133 /** 134 * Returns the appname where this log should be sent. Each log can override the default appname, set in this 135 * handler's constructor, by providing the appname in it's context. 136 * 137 * @param mixed[] $context 138 */ 139 protected function getAppName(array $context): ?string 140 { 141 if (isset($context['appname'])) { 142 return $context['appname']; 143 } 144 145 return $this->appName; 146 } 147 148 /** 149 * Returns the name of the current transaction. Each log can override the default transaction name, set in this 150 * handler's constructor, by providing the transaction_name in it's context 151 * 152 * @param mixed[] $context 153 */ 154 protected function getTransactionName(array $context): ?string 155 { 156 if (isset($context['transaction_name'])) { 157 return $context['transaction_name']; 158 } 159 160 return $this->transactionName; 161 } 162 163 /** 164 * Sets the NewRelic application that should receive this log. 165 */ 166 protected function setNewRelicAppName(string $appName): void 167 { 168 newrelic_set_appname($appName); 169 } 170 171 /** 172 * Overwrites the name of the current transaction 173 */ 174 protected function setNewRelicTransactionName(string $transactionName): void 175 { 176 newrelic_name_transaction($transactionName); 177 } 178 179 /** 180 * @param string $key 181 * @param mixed $value 182 */ 183 protected function setNewRelicParameter(string $key, $value): void 184 { 185 if (null === $value || is_scalar($value)) { 186 newrelic_add_custom_parameter($key, $value); 187 } else { 188 newrelic_add_custom_parameter($key, Utils::jsonEncode($value, null, true)); 189 } 190 } 191 192 /** 193 * {@inheritDoc} 194 */ 195 protected function getDefaultFormatter(): FormatterInterface 196 { 197 return new NormalizerFormatter(); 198 } 199} 200