Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
58.43% |
52 / 89 |
|
14.29% |
1 / 7 |
CRAP | |
0.00% |
0 / 1 |
Logger | |
58.43% |
52 / 89 |
|
14.29% |
1 / 7 |
100.05 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
debugNeeded | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
log | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
12 | |||
getSeverityFromException | |
50.00% |
3 / 6 |
|
0.00% |
0 / 1 |
6.00 | |||
logException | |
91.11% |
41 / 45 |
|
0.00% |
0 / 1 |
9.06 | |||
removeWriter | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 | |||
argumentToString | |
42.86% |
6 / 14 |
|
0.00% |
0 / 1 |
24.11 |
1 | <?php |
2 | |
3 | /** |
4 | * VuFind Logger |
5 | * |
6 | * PHP version 8 |
7 | * |
8 | * Copyright (C) Villanova University 2010. |
9 | * |
10 | * This program is free software; you can redistribute it and/or modify |
11 | * it under the terms of the GNU General Public License version 2, |
12 | * as published by the Free Software Foundation. |
13 | * |
14 | * This program is distributed in the hope that it will be useful, |
15 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
16 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
17 | * GNU General Public License for more details. |
18 | * |
19 | * You should have received a copy of the GNU General Public License |
20 | * along with this program; if not, write to the Free Software |
21 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
22 | * |
23 | * @category VuFind |
24 | * @package Error_Logging |
25 | * @author Chris Hallberg <challber@villanova.edu> |
26 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
27 | * @link https://vufind.org Main Site |
28 | */ |
29 | |
30 | namespace VuFind\Log; |
31 | |
32 | use Laminas\Log\Logger as BaseLogger; |
33 | use Laminas\Log\Writer\WriterInterface; |
34 | use Laminas\Stdlib\SplPriorityQueue; |
35 | use Traversable; |
36 | use VuFind\Net\UserIpReader; |
37 | |
38 | use function in_array; |
39 | use function is_array; |
40 | use function is_bool; |
41 | use function is_float; |
42 | use function is_int; |
43 | use function is_object; |
44 | |
45 | /** |
46 | * This class wraps the BaseLogger class to allow for log verbosity |
47 | * |
48 | * @category VuFind |
49 | * @package Error_Logging |
50 | * @author Chris Hallberg <challber@villanova.edu> |
51 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
52 | * @link https://vufind.org Main Site |
53 | */ |
54 | class Logger extends BaseLogger |
55 | { |
56 | /** |
57 | * Is debug logging enabled? |
58 | * |
59 | * @var bool |
60 | */ |
61 | protected $debugNeeded = false; |
62 | |
63 | /** |
64 | * User IP address reader |
65 | * |
66 | * @var UserIpReader |
67 | */ |
68 | protected $userIpReader; |
69 | |
70 | /** |
71 | * Constructor |
72 | * |
73 | * Set options for a logger. Accepted options are: |
74 | * - writers: array of writers to add to this logger |
75 | * - exceptionhandler: if true register this logger as exceptionhandler |
76 | * - errorhandler: if true register this logger as errorhandler |
77 | * |
78 | * @param UserIpReader $userIpReader User IP reader |
79 | * @param array|Traversable $options Configuration options |
80 | * |
81 | * @throws \Laminas\Log\Exception\InvalidArgumentException |
82 | */ |
83 | public function __construct(UserIpReader $userIpReader, $options = null) |
84 | { |
85 | $this->userIpReader = $userIpReader; |
86 | parent::__construct($options); |
87 | } |
88 | |
89 | /** |
90 | * Is one of the log writers listening for debug messages? (This is useful to |
91 | * know, since some code can save time that would be otherwise wasted generating |
92 | * debug messages if we know that no one is listening). |
93 | * |
94 | * @param bool $newState New state (omit to leave current state unchanged) |
95 | * |
96 | * @return bool |
97 | */ |
98 | public function debugNeeded($newState = null) |
99 | { |
100 | if (null !== $newState) { |
101 | $this->debugNeeded = $newState; |
102 | } |
103 | return $this->debugNeeded; |
104 | } |
105 | |
106 | /** |
107 | * Add a message as a log entry |
108 | * |
109 | * @param int $priority Priority |
110 | * @param mixed $message Message |
111 | * @param array|Traversable $extra Extras |
112 | * |
113 | * @return Logger |
114 | */ |
115 | public function log($priority, $message, $extra = []) |
116 | { |
117 | // Special case to handle arrays of messages (for multi-verbosity-level |
118 | // logging, not supported by base class): |
119 | if (is_array($message)) { |
120 | $timestamp = new \DateTime(); |
121 | foreach ($this->writers->toArray() as $writer) { |
122 | $writer->write( |
123 | [ |
124 | 'timestamp' => $timestamp, |
125 | 'priority' => (int)$priority, |
126 | 'priorityName' => $this->priorities[$priority], |
127 | 'message' => $message, |
128 | 'extra' => $extra, |
129 | ] |
130 | ); |
131 | } |
132 | return $this; |
133 | } |
134 | return parent::log($priority, $message, $extra); |
135 | } |
136 | |
137 | /** |
138 | * Given an exception, return a severity level for logging purposes. |
139 | * |
140 | * @param \Exception $error Exception to analyze |
141 | * |
142 | * @return int |
143 | */ |
144 | protected function getSeverityFromException($error) |
145 | { |
146 | // If the exception provides the severity level, use it: |
147 | if ($error instanceof \VuFind\Exception\SeverityLevelInterface) { |
148 | return $error->getSeverityLevel(); |
149 | } |
150 | // Treat unexpected or 5xx errors as more severe than 4xx errors. |
151 | if ( |
152 | $error instanceof \VuFind\Exception\HttpStatusInterface |
153 | && in_array($error->getHttpStatus(), [403, 404]) |
154 | ) { |
155 | return BaseLogger::WARN; |
156 | } |
157 | return BaseLogger::CRIT; |
158 | } |
159 | |
160 | /** |
161 | * Log an exception triggered by the framework for administrative purposes. |
162 | * |
163 | * @param \Exception $error Exception to log |
164 | * @param \Laminas\Stdlib\Parameters $server Server metadata |
165 | * |
166 | * @return void |
167 | */ |
168 | public function logException($error, $server) |
169 | { |
170 | // We need to build a variety of pieces so we can supply |
171 | // information at five different verbosity levels: |
172 | $baseError = $error::class . ' : ' . $error->getMessage(); |
173 | $prev = $error->getPrevious(); |
174 | while ($prev) { |
175 | $baseError .= ' ; ' . $prev::class . ' : ' . $prev->getMessage(); |
176 | $prev = $prev->getPrevious(); |
177 | } |
178 | $referer = $server->get('HTTP_REFERER', 'none'); |
179 | $ipAddr = $this->userIpReader->getUserIp(); |
180 | $basicServer |
181 | = '(Server: IP = ' . $ipAddr . ', ' |
182 | . 'Referer = ' . $referer . ', ' |
183 | . 'User Agent = ' |
184 | . $server->get('HTTP_USER_AGENT') . ', ' |
185 | . 'Host = ' |
186 | . $server->get('HTTP_HOST') . ', ' |
187 | . 'Request URI = ' |
188 | . $server->get('REQUEST_URI') . ')'; |
189 | $detailedServer = "\nServer Context:\n" |
190 | . print_r($server->toArray(), true); |
191 | $basicBacktrace = $detailedBacktrace = "\nBacktrace:\n"; |
192 | if (is_array($error->getTrace())) { |
193 | foreach ($error->getTrace() as $line) { |
194 | if (!isset($line['file'])) { |
195 | $line['file'] = 'unlisted file'; |
196 | } |
197 | if (!isset($line['line'])) { |
198 | $line['line'] = 'unlisted'; |
199 | } |
200 | $basicBacktraceLine = $detailedBacktraceLine = $line['file'] . |
201 | ' line ' . $line['line'] . ' - ' . |
202 | (isset($line['class']) ? 'class = ' . $line['class'] . ', ' : '') |
203 | . 'function = ' . $line['function']; |
204 | $basicBacktrace .= "{$basicBacktraceLine}\n"; |
205 | if (!empty($line['args'])) { |
206 | $args = []; |
207 | foreach ($line['args'] as $i => $arg) { |
208 | $args[] = $i . ' = ' . $this->argumentToString($arg); |
209 | } |
210 | $detailedBacktraceLine .= ', args: ' . implode(', ', $args); |
211 | } else { |
212 | $detailedBacktraceLine .= ', args: none.'; |
213 | } |
214 | $detailedBacktrace .= "{$detailedBacktraceLine}\n"; |
215 | } |
216 | } |
217 | |
218 | $errorDetails = [ |
219 | 1 => $baseError, |
220 | 2 => $baseError . $basicServer, |
221 | 3 => $baseError . $basicServer . $basicBacktrace, |
222 | 4 => $baseError . $detailedServer . $basicBacktrace, |
223 | 5 => $baseError . $detailedServer . $detailedBacktrace, |
224 | ]; |
225 | |
226 | $this->log($this->getSeverityFromException($error), $errorDetails); |
227 | } |
228 | |
229 | /** |
230 | * Remove a writer. |
231 | * |
232 | * @param WriterInterface $writer Writer to remove |
233 | * |
234 | * @return void |
235 | */ |
236 | public function removeWriter(WriterInterface $writer): void |
237 | { |
238 | $newQueue = new SplPriorityQueue(); |
239 | foreach ($this->getWriters() as $i => $current) { |
240 | if ($current !== $writer) { |
241 | $newQueue->insert($current, $i); |
242 | } |
243 | } |
244 | $this->setWriters($newQueue); |
245 | } |
246 | |
247 | /** |
248 | * Convert function argument to a loggable string |
249 | * |
250 | * @param mixed $arg Argument |
251 | * |
252 | * @return string |
253 | */ |
254 | protected function argumentToString($arg) |
255 | { |
256 | if (is_object($arg)) { |
257 | return $arg::class . ' Object'; |
258 | } |
259 | if (is_array($arg)) { |
260 | $args = []; |
261 | foreach ($arg as $key => $item) { |
262 | $args[] = "$key => " . $this->argumentToString($item); |
263 | } |
264 | return 'array(' . implode(', ', $args) . ')'; |
265 | } |
266 | if (is_bool($arg)) { |
267 | return $arg ? 'true' : 'false'; |
268 | } |
269 | if (is_int($arg) || is_float($arg)) { |
270 | return (string)$arg; |
271 | } |
272 | if (null === $arg) { |
273 | return 'null'; |
274 | } |
275 | return "'$arg'"; |
276 | } |
277 | } |