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